本文参考System V ABI: AMD64 Architecture Processor Supplement,并且不考虑__float128
,__m128
,__m256
和__m512
等类型的参数的传递和作为返回值的情况。
主要内容:
- 函数调用相关的常用寄存器
- 函数调用时的stack frame:可省略的
%rbp
和red-zone - 函数调用时参数的传递:参数分类,不同类型参数的传递方式
- 函数返回值的处理
寄存器
AMD64架构有16个64bit的通用寄存器(%rax, %rbx, %rcx, %rdx, %rsp, %rbp, %rsi, %rdi, %r8 - %r15)和16个128bit的SSE寄存器(%xmm0 - %xmm15)。
进行函数调用时,%rbp, %rbx, %r12 - %r15 属于caller,callee 要保管这6个寄存器的内容。
剩下的寄存器%rax, %rcx, %rdx, %rsp, %rbp, %rdi, %r8 - %r11 和 %xmm0 - $xmm15 属于callee。
其中%rdi, %rsi, %rdx, %rcx, %r8, %r9 和 %xmm0 - %xmm7 可用来传递参数。%rax, %rdx,%xmm0, %xmm1 用来保存返回值。不能由寄存器保存的参数,将会被放置在栈上。
通过一个使用所有通用寄存器的函数,我们可以观察到%rbp, %rbx, %r12 - %r15在进入函数时被push
放入栈中保存,离开函数时pop
从栈中恢复。
栈帧(stack frame)
1 | 8 bytes 8 bytes |
caller通过call func
,即等价于1
2push return_address
jmp func
将控制权转移给函数func
后,此时rsp
指向左图中rbp
的位置。此时要求rbp + 8
的位置,也就是栈上用来传递参数部分的末尾地址和16bytes对齐。
控制权交给func
后,func
进行function prologue,将之前函数的stack-frame base pointer: rbp
通过push rbp
压栈,并将rbp
的值变为当前函数的stack-frame base: move rbp, rsp
。之后向下移动rbp
以扩展当前栈的空间。
AMD64规定rbp
以下128bytes的大小为red zone,并保证该区域不会被signal和interrupt handler修改。但不保证该区域不被之后调用的函数修改。因此该区域常用作不再调用其他函数的leaf function来保存局部变量,且不用移动rsp
。
函数调用结束后要先回复之前的保存的caller的寄存器,之后进行function epilogue恢复之前的rbp
,并跳转到return_address
的位置。即1
2leave
ret
等价于1
2
3mov rsp, rbp
pop rbp
ret
AMD64 ABI规定:可以不使用rbp
寄存器保存caller的stack-frame base地址。现代编译器仅靠rsp
就可以对栈上的变量进行定位了。不使用rbp
可以节省function prelogue和function epilogue中的两条指令,并将rbp
寄存器空出。gcc x86-64下,-O
优化默认进行不保存rbp
。使用-fno-omit-frame-pointer
可以保存rbp
。其效果可以对比下面使用不同编译参数的结果:
AMD64 ABI中的red-zone:
可以观察到上面例子中,使用-fno-omit-frame-pointer
参数进行编译的结果中,get_rt()
没有在function prelogue通过sub rsp, xxx
来扩展当前栈的大小,也没有在function epilogue中使用leave
来恢复rsp
到之前的rbp
。这时因为get_rt()
是一个leaf function,他没有调用其他函数,并且栈上的需要的空间小于read zone的128bytes。因此使用read zone来保存栈上的局部变量,注意到mov DWORD PTR [rbp-4], 1
,因此i
就被保存在rbp-4
的位置上。这里刚好是read zone。
如果我们修改编译参数,加上-mno-red-zone
来取消read zone,就可以看到函数进行完整的function prelogue和function epilogue了
参数传递
参数分类
参数根据参数传递的方式不同,可以大致分为:
- NO_CLASS: padding或空
struct
类型 - INTEGER: 使用6个通用寄存器(%rdi, %rsi, %rdx, %rcx, %r8, %r9)传递
- SSE: 使用SSE寄存器(%xmm0 - %xmm7)传递。
- MEMORY: 使用
psuh
,在栈上传递
不考虑__float128
,__m128
,__m256
和__m512
等类型的参数,basic type的参数可以被分为:
- INTEGER:
bool
,char
,short
,int
,long
,long long
和 指针 - SSE:
float
,double
- MEMORY: 不通过寄存器,而是通过栈来传递参数
聚合类型(数组 和 无虚函数、自定义构造函数、基类或private/protected数据成员的 struct
或union
)按照以下方式分类:
- 如果该对象的类型对于调用是non-trivial的,即:
- 有 non-trivial ctor 或 non-trivial move constructor 或 non-trivial dtor
- 或其所有的ctors 和 move constructors 都被声明为
delete
,则将该讲该参数构造在caller的栈上,并将该参数替换成构造在栈上的对象的引用。
- 如果聚合类型的大小超过16bytes 或 该聚合类型包含未对齐的field,则是MEMORY类型
- 如果聚合类型的大小大于8bytes且小于16bytes,则将每个8bytes都分开进行分类,将分类的结果两两按以下规则合并:
- 如果任一为MEMORY,则整个聚合类型以MEMORY方式传递。
- 如果两个8bytes分类相同,则合并的结果也相同。
- 如果其一为NO_CLASS,则合并的结果为另一个的类型。
- 如果其一为INTEGER,则合并的结果为INTEGER。
- 否则为SSE。
参数传递过程
一旦所有参数都已经分类完毕,寄存器按照以下规则进行赋值:
- 跳过MEMORY类型,MEMORY不使用寄存器,而是在
push
到栈上。 - INTEGER类型按照 %rdi,%rsi, %rdx, %rcx, %r8, %r9 的顺序赋值给通用寄存器。
- SSE类型按%xmm0 - %xmm7的顺序赋值给SSE寄存器
bool
类型的必须保证bit 0包含值,且bit 1 - bit 7 为0,但不保证其他bit的数值。
如果在传递参数的过程中寄存器全被占用了,则将该参数整个通过MEMORY的方式传递。如果寄存器全被占用,则按MEMORY类型传递的参数以从右到左的顺序入栈。
例子1:传递8个int作为参数。其中前6个int被存放在寄存器中,后2个int被放置在栈上。
例子2:传递1个大小为12bytes的聚合类型,该聚合类型的第一个和第二个data members被拼在一起用%rdi
传递,第三个data member用%rsi的低位%esi
传递。这这证明了聚合类型的参数传递的确是以8byte来分割的。
例子3: 使用__attribute__((packed))
的未对齐类型、大小大于16bytes的聚合类型 和 non-trival 的C++ class以MEMORY的方式传递。注意到callee
函数中的source都是rsp + offset
的形式吗,证明参数的确都被放在了栈上。
例子4:对于函数调用non-trivial的类型 和 trivial的类型对比。可见non-trivial的类型由caller在栈上构造:1
2mov QWORD PTR [rsp], 2
mov DWORD PTR [rsp+8], 4
并且因引用的方式传递给callee(mov rdi, rsp
)。可以看到,传引用的形式和传值的形式生成的代码是完全相同的
返回值
返回值的处理和参数传递类似。首先确定返回值的类型,之后根据返回值的类型决定如何返回:
- 如果返回值是MEMORY类型,则由caller负责提供返回值的存储空间,并将返回值当作第一个参数,将地址存储在
%rdi
之中。在返回时,%rax
中会保存%rdi
里传来的地址。 - 如果返回值是INTEGER类型,按顺序使用
%rax
,%rdx
寄存器。 - 如果返回值是SSE类型,按顺序使用
%xmm0
,%xmm1
寄存器。
在下面的例子中:struct mm
因为大小大于16bytes,因此为MEMORY类型。在caller_mm()
中,生成的汇编代码中第17行mov rdi, rsp
,即将在caller_mm()
栈上划给返回值的空间的地址传递存储在%rdi%中。在返回时,在生成的callee_mm()
代码中第4行,将%rdi
的内容又传递回了%rax
。
tuple<int, int, int>
作为一个12bytes大小的类型,应按照8,4分割给%rax
和%rdx
寄存器。在生成的代码中第15, 16行,的确看到将使用%rdx
和%rax
分别赋值。在caller_integer
中,将%rax
和%rdx
直接相加存储在%rax
中作为返回结果。
参考:
What registers are preserved through a linux x86-64 function call
Itanium C++ ABI
System V ABI: AMD64 Architecture Processor Supplement