0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

在Rust中使用内联汇编

jf_wN0SrCdH 来源:Rust语言中文社区 2023-05-04 09:54 次阅读

自 Rust 1.59 以降,在 Rust 代码中内联汇编代码的语言特性已然 stable^1。参考知乎上一篇文章^3,我用 Rust 的内联汇编实现了有栈协程^4。在此过程中学到了一些知识。

本文假设读者对 x86 汇编有基础了解。

局部内联汇编与自动分配寄存器

Rust 的内联汇编一开始是对标 GCC 的内联汇编设计的,长得像这样^5:


	

asm!("mov $4, %eax cpuid mov %eax, $0 mov %ebx, $1 mov %ecx, $2 mov %edx, $3" : "=r"(a), "=r"(b), "=r"(c), "=r"(d) : "m"(info) : "eax", "ebx", "ecx", "edx" )

后来才变成如今富有 Rust 特色的样子^6:


	

asm!( "mov edi, ebx", "cpuid", "xchg edi, ebx", in("eax") info, lateout("eax") a, out("edi") b, out("ecx") c, out("edx") d, )

与 GCC 内联汇编语法一样,Rust 希望即使需要手写汇编,程序员也能将一部分工作交给编译器来高效完成,这部分工作就是寄存器分配,毕竟只有编译器了解内联汇编前后的上下文,知道该怎么分配寄存器最合适。

asm宏的inoutinoutlateoutinlateout参数就是为了让编译器帮助分配寄存器的。

in表示将变量的值传给寄存器,编译器生成的汇编代码会使得在内联汇编代码中读取相应的寄存器,就得到了传入的变量的值;

out表示将寄存器的值写到变量中,在内联汇编代码中写入相应寄存器,编译器在内联汇编之后生成的汇编代码会使得相应变量具有写入相应寄存器的值;

late则是代表编译器可以采取进一步的策略来优化寄存器分配:默认的分配策略给每个参数分配不同的寄存器,使用lateoutinlateout的参数则允许编译器复用某个in参数的寄存器,只要内联汇编代码中先读完所有的in寄存器,再输出lateoutinlateout寄存器即可。

具体细节以及此处没讲到的option可参考^1。

全局内联汇编与名称修饰(Name Mangling)

除了需要写在函数体中的asm宏,还有需要写在函数之外的global_asm宏,其作用与独立的汇编代码相差不大,一切全由程序员掌控,没有上节所述寄存器自动分配之功能,还需要手动管理参数传递,栈对齐等等。

global_asm我们可以写出源代码完全是汇编代码的函数,函数名就是汇编代码中的标签,函数参数和返回值需要按照 ABI 约定来处理^7:


	

use std::global_asm; extern "C" { fn my_asm_add(a: i32, b: i32) -> i32; } global_asm!{ "my_asm_add:", "mov eax, edi", "add eax, esi", "ret", } fn main() { let a = 114; let b = 514; let x = unsafe { my_asm_add(a, b) }; dbg!(x); }

这段代码在x86_64-unknown-linux-gnu的目标,也就是 Rust Playground 的运行环境下会通过编译并输出正确结果 628,但在 64 位 Windows 下则会得到错误的结果,因为 64 位 Windows 所用的 C ABI 和 64 位 Linux 不一样,虽然都是通过寄存器传递参数,但 64 位 Windows 的 C ABI 的第一二参数是用 RCX 和 RDX 传递,而非示例中的 RDI 和 RSI。

而在MacOS上编译,结果是编译不过——虽然和 64 位 Linux 一样使用 System V AMD64 ABI,但 MacOS 进行 C 语言函数名名称修饰时会在函数名前加一个下划线,所以编译器会试图寻找_my_asm_add符号,结果找不到。在汇编代码中把"my_asm_add:"改成"_my_asm_add:"即可编译通过。

由此可见汇编语言的不可移植性:即使是同一架构,甚至同一 ABI 约定的汇编代码也相当不可移植。

在代码编写过程中,我发现一个技巧可以规避掉名称修饰的影响。asmglobal_asm宏可以接受格式为sym SYMBOL的参数来引用符号,其中SYMBOL是函数或者静态变量,这种参数的目的是在汇编语言中直接引用 Rust 函数或静态变量的符号,尽管 Rust 的名称修饰算法尚未 stable,但代码中可以不写出来而由编译器来计算。这个功能也可以用在extern符号上,因此可以这样写:


	

global_asm!{ ".extern {0}", "{0}:", "mov eax, edi", "add eax, esi", "ret", sym my_asm_add, }

这样在编译 x86_64-unknown-linux-gnu 目标时生成的汇编代码中的标签是my_asm_add,而对于x86_64-apple-darwin目标,生成的标签则是_my_asm_add

这样的技巧不够方便,更直观的写法是 naked function^9,这种函数从外部看来就是一个 unsafe 函数,而内部只允许有一个asm宏调用,编译器不生成一般函数中会有的各种上下文代码,函数本体完全由该 asm 宏调用生成。

程序重定位与位置无关代码

为了加载动态链接库或者避免被黑客利用固定程序地址攻击,操作系统加载程序时会将其载入到随机的内存地址,这个过程就是程序重定位。

对于 32 位 x86 程序,需要在加载时修改程序中所有的绝对地址,包括函数的和数据的。在汇编语言中可以直接将标号作为常量使用,但最好不要写mov eax, LABEL这样的语句,因为这样的语句加载器不会识别和修改。应该写lea eax, LABEL

x86_64 支持相对 RIP 寻址,Rust 编译器默认将代码编译为使用这项特性的位置无关可执行程序(PIE),因此在 Rust 的内联汇编中取符号地址需要写成lea rax, [rip+SYMBOL]

示例,x86_64 平台下给 static 变量X加 1 的函数用汇编语言实现[11]:


	

use std::global_asm; static mut X: usize = 0; extern "C" { fn incr_x(); } global_asm!{ "{0}:", "add dword ptr [rip+{X}], 1", "ret", sym incr_x, X = sym X, } fn main() { unsafe { incr_x(); dbg!(X); incr_x(); dbg!(X); } }

32 位 x86 代码下则需要把汇编代码改成:


	

global_asm!{ "{0}:", "lea eax, {X}", "add dword ptr[eax], 1", "ret", sym incr_x, X = sym X, }

审核编辑 :李倩
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 代码
    +关注

    关注

    30

    文章

    4556

    浏览量

    66812
  • 编译器
    +关注

    关注

    1

    文章

    1577

    浏览量

    48627
  • Rust
    +关注

    关注

    1

    文章

    223

    浏览量

    6387

原文标题:在 Rust 中使用内联汇编

文章出处:【微信号:Rust语言中文社区,微信公众号:Rust语言中文社区】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何在Rust中使用Memcached

    了Memcached协议的实现,使得开发者可以在Rust中使用Memcached。 基础用法 创建连接 使用Rust语言Memcached需要先创建一个连接。可以使用 memcached::Client
    的头像 发表于 09-19 16:30 885次阅读

    RealView MDK中使内联函数时需要注意的问题

    的文件中申明为extern类型。原因是MDK中使用的__inline函数时和标准C++中的inline函数有相同的语义。C++标准中,一个内联函数在用到它的每个解释单元需要相同的定义,这样看来只有内部
    发表于 08-02 10:10

    如何编写内联汇编MAC指令?

    我使用的是DSPIC33 FJ。我所有的代码都是用C编写的,但是考虑到应用程序的复杂性,我不得不使用内联汇编指令。我的C代码中,我有三个变量foo,bar和foo bar,其中foo bar
    发表于 08-01 09:38

    内联函数和汇编指令疑问

    我想用内联函数来取两个int型变量(32位)的较大值,看了一下内联函数有_max2和_max4,和汇编指令MAX2和 MAXU4 但是他们的用法比较奇特,他们用法是把32位的int型变量拆成4个8位
    发表于 10-22 15:28

    为什么用内联汇编调用CPSIE退出临界态系统会被卡住?

    出问题的地方和反汇编结果如图。这个是MDK里优化等级为Level-2时编出来的,开关中是用内联汇编调用指令#if OS_CRITICAL_METHOD
    发表于 11-11 04:35

    如何使用内联函数法调用汇编函数?

    从C/C++中调用汇编代码中的函数使用内联函数法调用汇编函数从C/C++中调用汇编代码中的变量或者常量
    发表于 04-02 06:57

    哪几种情况中必须使用内联汇编或嵌入型汇编

    。Linux驱动入门可以一起交流。一、gcc 内联汇编内联汇编即在C中直接使用汇编语句进行编程,使程序可以C程序中实现C语言不能完成的一些
    发表于 12-20 08:00

    RUST嵌入式开发中的应用是什么

    的文档、有用的错误消息、友好编译器、一流的工具,只是Rust的几个好处。它带有一个集成的包管理器和构建工具,支持自动完成和类型检查的智能多编辑器,一个自动格式化程序,等等。为什么嵌入式开发中使
    发表于 12-24 08:34

    Rust代码中加载静态库时,出现错误 ` rust-lld: error: undefined symbol: malloc `怎么解决?

    “ [i]malloc ”、“ [i]exit ”。我验证了使用 ` [i]nm ` 命令。 问题是我打算使用 ffi rust 中使用这个静态库。当我尝试我的
    发表于 06-09 08:44

    内联汇编的技巧

      有时我们的程序需要一些很高的执行效率或者执行系统底层的功能模块,这些关键的部分我们可以采用内联汇编直接插入汇编指令来达到我们的要求,以下是几个技巧与大家共同
    发表于 08-29 10:20 835次阅读

    内联汇编和嵌入型汇编的使用

    编译器中的汇编器。使用它可以在C/C++程序中实现C/C++语言不能完成的一些工作。例如,在下面几种情况中必须使用内联汇编或嵌入型汇编。 程序中使
    发表于 10-19 09:30 0次下载

    Rust相比Go的优劣势

    Rust可以做内联汇编,Go不行(Rust的SIMD库也在开发中,这种事情你不会用Go做)。
    发表于 06-29 11:19 3664次阅读

    哪几种情况中必须使用内联汇编或嵌入型汇编

    ARM系列文章,请点击以下汇总链接:《从0学arm合集》一、gcc 内联汇编内联汇编即在C中直接使用汇编语句进行编程,使程序可以在C程序中实现C语言不能完成的一些工作,例如,在下面几种
    的头像 发表于 12-24 12:55 736次阅读

    C和汇编如何互相调用?

    一、gcc 内联汇编 内联汇编即在C中直接使用汇编语句进行编程,使程序可以在C程序中实现C语言不能完成的一些工作,例如,在下面几种情况中必须
    的头像 发表于 12-25 15:50 2718次阅读

    Rust的内部工作原理

    Rust汇编:了解 Rust 的内部工作原理 非常好的Rust系列文章,通过生成的汇编代码,让你了解很多
    的头像 发表于 06-14 10:34 470次阅读
    <b class='flag-5'>Rust</b>的内部工作原理