64位86 函数调用约定

汇编yulabu2026-09-07 22 次阅读

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

评论

正在加载评论区…