Windows/Linux x64汇编函数调用约定
1. 前言
最近在写一些字符串函数的优化,用到x64汇编,我也是第一次接触,故跟大家分享一下。
2. 简介
x86:又名 x32 ,表示 Intel x86 架构,即 Intel 的 32位 80386 汇编指令集。
x64:表示 AMD64 和 Intel 的 EM64T ,而不包括 IA64 。至于三者间的区别,可自行搜索。
x64 跟 x86 相比寄存器的变化,如图:
从图上可以看到,X64架构相对于X86架构的主要变化,是将原来所有的寄存器都扩大了一倍,例如EAX现在扩充成RAX,同时,又新增加了从R8~R15这8个64位的寄位器,有点RISC的味道(RISC特点就是寄存器多)。
3. x64 调用约定
在 x86 模式下,有三种常用调用约定,cdecl (C规范) / stdcall(WinAPI默认) / fastcall 函数调用约定。
而在 x64 模式下,调用约定只有一种,就是 fastcall,但是 Windows 下和 Linux 下还是略有不同的,下面分别介绍。
3.1 Windows 下的 x64
一些细节:
- Windows 的 x64 下只有一种函数调用约定,即 __fastcall ,其他调用约定的关键字会被忽略,也就是说 ABI 只有 __fastcall ;
- 一个函数在调用时,前四个参数是从左至右依次存放于 RCX、RDX、R8、R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 如果是 int f(double a, double b, double c, double d, double e, double f) 这样的函数,前四个浮点类型参数从左到右由 XMM0,XMM1,XMM2,XMM3 依次传递,剩下的参数通过栈传递,从右至左顺序入栈;
- 调用者负责在栈上分配32字节的“shadow space”,用于存放那四个存放调用参数的寄存器的值(亦即前四个调用参数);
- 小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),也就是说结构体或union如果大小是1,2,4,8字节,用值传递(相应的寄存器),大于8字节(64位)必须按照地址(指针)传递;
- 被调用函数的返回值是64位以内(包括64位)的整形或指针时,则返回值会被存放于RAX;
- 如果返回值是浮点值,则返回值存放在XMM0;
- 更大的返回值(比如结构体),由调用方在栈上分配空间,并由 RCX 持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用 RDX,R8,R9,3个寄存器,其余参数通过栈传递。函数调用结束后,RAX 返回该空间的指针(即函数调用开始时的 RCX 值)。
- 调用者 (caller) 负责清理栈,被调用函数 (callee) 不用清栈,可是为什么有时候我们看到调用者 (caller) 也没有清栈呢?后面会讲;
- 除了 RCX,RDX,R8,R9 以外,RAX,R10,R11 和 XMM5,XMM6 也是“易挥发”的,不用特别保护,其余寄存器需要保护。(x86下只有 eax, ecx, edx 是易挥发的)
- 栈需要16字节对齐,“call”指令会入栈一个8字节的函数返回地址(函数调用指令后的下一个指令的地址)(注:即函数调用前原来的RIP指令寄存器的值),这样一来,栈就对不齐了(因为RCX、RDX、R8、R9四个寄存器刚好是32个字节,是16字节对齐的,现在多出来了8个字节)。所以,所有非叶子结点调用的函数,都必须调整栈RSP的地址为16n+8,来使栈对齐。
- 对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
关于 Windows x64 的调用约定,可以参考微软的官方文档:
https://docs.microsoft.com/zh-cn/cpp/build/x64-calling-convention?view=vs-2017
一些其他要注意的小问题:
- 另外一些小问题要注意,AMD64不支持 push 32bit 寄存器的指令,push 和 pop 都要用64位寄存器,即 push rbx ,不能使用 push ebx 。
- 另外要补充的一点是,在一般情况下,x64 平台的 RBP 栈基指针被废弃掉,只作为普通寄存器来用,所有的栈操作都通过 RSP 指针来完成。
关于有时候我们看到调用者 (caller) 也没有清栈的原因:
都说 x64 下 __fastcall 由调用者 (caller) 清理栈区空间。但是我们有时候发现 main() 函数或被 main() 函数调用的函数中,没有清理子函数栈空间的过程呢?
这是由于 64 位平台下栈区空间开辟问题导致。我在CSDN上看到这样一句话:与通过 PUSH 和 POP 指令在堆栈中显式添加和移除参数的 x86 编译器不同,x64 模式下,编译器会预留足够的堆栈空间,以调用最大目标函数(参数方法)所使用的任何内容。随后,在调用子函数时,它重复使用相同的堆栈区域来设置这些参数,从而实现不用调用者 (caller) 反复清栈的过程。
这句话什么意思呢?它的意思就是我们在 x64 模式下一开始系统会为 main() 函数开辟一个很大的栈区,但是 main() 函数并未消耗掉这么大的栈区空间,这时候怎么办呢?子函数就会还继续利用 main() 函数的预留的栈区空间,所以 main() 函数或其他被 main() 调用的函数,并不用对子函数栈区空间进行清理。
示例:
; 示例代码 1.asm ; 语法:GoASM DATA SECTION text db 'Hello x64!', 0 caption db 'My First x64 Application', 0 CODE SECTION START: sub rsp, 28h ; 堆栈预留 shadow space (32)字节 + 8 字节,让栈对齐到 16 字节 xor r9d, r9d ; r9 lea r8, caption ; r8 lea rdx, text ; rdx xor rcx, rcx ; rcx call MessageBoxA add rsp, 28h ; 调用者自己恢复堆栈 ret
3.2 Linux 下的 x64
调用约定细节:
- Linux 下的调用约定叫做 “System V AMD64 ABI”,此约定主要在 Solaris,GNU/Linux,FreeBSD 和其他非微软OS上使用;
- Linux 的 x64 下也只有一种函数调用约定,即 __fastcall ,其他调用约定的关键字会被忽略,也就是说 ABI 只有 __fastcall ;
- 一个函数在调用时,如果参数个数小于等于 6 个时,前 6 个参数是从左至右依次存放于 RDI,RSI,RDX,RCX,R8,R9 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 如果参数个数大于 6 个时,前 5 个参数是从左至右依次存放于 RDI,RSI,RDX,RCX,RAX 寄存器里面,剩下的参数通过栈传递,从右至左顺序入栈;
- 对于系统调用,使用 R10 代替 RCX;
- XMM0 ~ XMM7 用于传递浮点参数;
- 小于64位(bit)的参数传递时高位并不填充零(例如只传递ecx),也就是说结构体或union如果大小是1,2,4,8字节,用值传递(相应的寄存器),大于8字节(64位)必须按照地址(指针)传递;
- 被调用函数的返回值是64位以内(包括64位)的整形或指针时,则返回值会被存放于 RAX,如果返回值是128位的,则高64位放入 RDX;
- 如果返回值是浮点值,则返回值存放在XMM0;
- 更大的返回值(比如结构体),由调用方在栈上分配空间,并由 RCX 持有该空间的指针并传递给被调用函数,因此整型参数使用的寄存器依次右移一格,实际只可以利用 RDI,RSI,RDX,R8,R9,5个寄存器,其余参数通过栈传递。函数调用结束后,RAX 返回该空间的指针(即函数调用开始时的 RCX 值)。
- 可选地,被调函数推入 RBP,以使 caller-return-rip 在其上方8个字节,并将 RBP 设置为已保存的 RBP 的地址。这允许遍历现有堆栈帧,通过指定GCC的 -fomit-frame-pointer 选项可以消除此问题。
- 调用者 (caller) 负责清理栈,被调用函数 (callee) 不用清栈;
- 除了 RDI,RSI,RDX,RCX,R8,R9 以外,RAX,R10,R11 也是“易挥发”的,不用特别保护,其余寄存器需要保护。
- 在调用 call 指令之前,必须保证堆栈是16字节对齐的;
- 对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
4. 参考文章
- Windows平台X64函数调用约定与汇编代码分析 | http://kelvinh.github.io/blog/2013/08/05/windows-x64-calling-conventions/
- x64 参数传递 | http://hyperiris.blog.163.com/blog/static/1808400592011715111957863/
- Windows X64汇编入门(1) | http://wenku.baidu.com/view/3093d52d453610661ed9f4b0.html
- x86 x64下调用约定浅析 | https://www.cnblogs.com/Toring/p/6650043.html
- linux 64位函数调用约定 | https://blog.csdn.net/u013043103/article/details/107381244
5. 更新历史
2020/09/18: 重新整理,修正错漏,并新增 Linux 下的 x64 调用约定。
2014/06/14: 初始版本。