64位86 函数调用约定
64位86 函数调用约定
64 位程序中,操作系统通常会规定一套主要的 ABI,不再像 32 位 Windows 那样存在大量 stdcall、cdecl、fastcall、thiscall 等互相竞争的调用约定。
调用约定无非就规定几个问题:
- 使用栈还是寄存器传参?
- 栈的清理责任归属
64 位程序不再像 32 位 Windows 那样存在大量历史调用约定,但不同操作系统仍然有不同的 ABI
Windows x64 使用 Microsoft x64 ABI;Linux x86-64 通常使用 System V AMD64 ABI
ABI(Application Binary Interface,应用二进制接口)就是定义二进制程序模块之间如何交互的规范,确保不同软件组件在操作系统和硬件平台上能够正确协作
Windows x64
Windows 和 Linux 使用的是不同 ABI
对于整型参数和指针参数,我们通常会按顺序放在如下寄存器:
- RCX(64位)/ECX(低32位)
- RDX(64位)/EDX(低32位)
- R8(64位)/R8D(低32位)
- R9(64位)/R9D(低32位)
超过的参数用栈传递
对于浮点参数:
- XMM0
- XMM1
- XMM2
- XMM3
Linux x86-64 System V ABI
- RDI
- RSI
- RDX
- RCX
- R8
- R9
注意:类对象的this指针永远作为第一个参数被传递(放在RCX)
既有浮点数又有整型是如何传参的?
我们的这个寄存器的使用是分槽位的,一般提供四个槽位,超过四个的参数都通过栈传递,而槽位是这样定的
| 第一个槽位 | 第二个槽位 | 第三个槽位 | 第四个槽位 |
|---|---|---|---|
| RCX | RDX | R8 | R9 |
| XMM0 | XMM1 | XMM2 | XMM3 |
64位的调用约定,对固定槽位的寄存器做了强制的位置约定,按照参数的位置来使用对应的寄存器
假设我们这样传的参数:int,float,int,float
那就分别用到RCX,XMM1,R8,XMM3寄存器
Linux这里暂时不作补充
什么是ShadowSpace
Shadow Space 是 Windows x64 ABI 规定的 32 字节栈空间,由调用者在调用前预留,供被调用者使用
给被调用函数预留的 32 字节栈空间,使寄存器参数有对应的栈位置可以被保存/使用
asm
复制代码
sub rsp, xxh
call xxx
add rsp, xxh
就可能出现以上情况:在call之前给函数预留空间,在call结束之后平栈,预留了多少就还多少
分配的字节数由很多因素决定:
- Shadow Space
- 局部变量
- 保存非易失寄存器
- 栈对齐
最终共同决定:
asm
复制代码
sub rsp, XXh
栈布局
什么栈布局来着?
之前学32位的时候,栈布局大概长这样:
复制代码
高地址
┌─────────────────┐
│ b │
├─────────────────┤
│ a │
├─────────────────┤
│ 返回地址 │
├─────────────────┤
│ 保存的 EBP │ ← EBP
├─────────────────┤
│ x │
└─────────────────┘
低地址
就是说:当前函数的栈空间里,各种东西分别在哪里,就是栈布局
那64位的栈布局又有什么不同呢?
根据前面讲的特征,我们可以粗略地理解为:
复制代码
高地址
┌────────────────────┐
│ 第5个参数 e │
├────────────────────┤
│ Shadow Space │
│ 4 × 8 bytes │
│ │
├────────────────────┤
│ 返回地址 │ ← CALL 压入
├────────────────────┤
│ │
│ func自己的栈空间 │
│ │
│ 局部变量 x │
└────────────────────┘
低地址
总之,这就是栈布局,这只是帮助理解的简化图
栈对齐
指栈顶指针(如x86_64的rsp)必须按特定字节边界对齐,以保证指令执行正确性和内存访问效率
Windows x64 ABI 通常要求栈保持 16 字节对齐
以前面栈布局的情况举例,执行call的时候,有8字节的返回地址被压入,然后我们又希望我们函数内部保持16字节对齐,这个时候我们的RSP就需要
asm
复制代码
sub rsp, 28h
此时调整了40个字节,八个字节补齐call的位置,32个字节预留shadow space
为什么需要对齐?
CPU 某些数据类型/指令对内存地址存在对齐要求或对齐性能考虑,按照16字节边界排列会更加符合很多底层操作的要求
所以ABI会规定:函数调用过程中,栈必须按照规定保持对齐
寄存器保存规则
哪些寄存器函数可以随便改?哪些必须恢复?
在windows x64中 我们先简单记成:
Volatile(易失寄存器):
以下寄存器中的数据,函数可以随便破坏
复制代码
RAX
RCX
RDX
R8
R9
R10
R11
以及部分XMM寄存器
Non-volatile(不可变寄存器)
以下寄存器如果函数要修改,就必须保存原值
复制代码
RBX
RBP
RSI
RDI
RSP
R12
R13
R14
R15
举个例子:
asm
复制代码
push rbx
...
pop rbx
ret
把这个RBX存的数据入栈后再传入数据,用完再把数据还回去
这里这么做的原因就是因为:RBX 属于 non-volatile register,函数有义务保护调用者原来的 RBX
评论