• 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 的调用约定,可以参考微软的官方文档:

    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. 参考文章

    1. Windows平台X64函数调用约定与汇编代码分析 | http://kelvinh.github.io/blog/2013/08/05/windows-x64-calling-conventions/
    2. x64 参数传递 | http://hyperiris.blog.163.com/blog/static/1808400592011715111957863/
    3. W​i​n​d​o​w​s​ ​X​6​4​汇​编​入​门​(​1​) | http://wenku.baidu.com/view/3093d52d453610661ed9f4b0.html
    4. x86 x64下调用约定浅析 | https://www.cnblogs.com/Toring/p/6650043.html
    5. linux 64位函数调用约定 | https://blog.csdn.net/u013043103/article/details/107381244

    5. 更新历史

    2020/09/18: 重新整理,修正错漏,并新增 Linux 下的 x64 调用约定。

    2014/06/14: 初始版本。

  • 相关阅读:
    01 drf源码剖析之restful规范
    08 Flask源码剖析之flask拓展点
    07 flask源码剖析之用户请求过来流程
    06 flask源码剖析之路由加载
    05 flask源码剖析之配置加载
    04 flask源码剖析之LocalStack和Local对象实现栈的管理
    03 flask源码剖析之threading.local和高级
    02 flask源码剖析之flask快速使用
    01 flask源码剖析之werkzurg 了解wsgi
    MVC之Filter
  • 原文地址:https://www.cnblogs.com/shines77/p/3788514.html
Copyright © 2020-2023  润新知