1.什么是调用约定
函数的调用过程中有两个参与者,调用者caller以及被调用者callee。
调用约定规定了caller和callee之间如何相互配合来实现函数调用,如下:
- 函数的参数存放在哪里。存放在寄存器还是栈,以及哪个寄存器、栈中的哪个位置?
- 函数的参数传递顺序。从左到右将参数入栈,还是从右到左将参数入栈?
- 返回值如何传递给caller。是放在寄存器,还是其他地方?
- 等等
2.caller保存的寄存器以及callee保存的寄存器
在调用约定的规定中:
(1)有些寄存器由调用者保存(caller-saved register),此类寄存器也叫易失性寄存器(volatile register)。
调用者调用其他函数时,某些寄存器值会被被调用者改变,但是callee并不负责这些寄存器的保存和恢复,由于需要在函数返回后继续使用,
故需要caller保存这些寄存器值,通常是压入栈中,函数调用返回后再恢复这些寄存器的值。
(2)有些寄存器由被调用者保存(callee-save register),此类寄存器也叫非易失性寄存器(non-volatile register)。
同上,有些寄存器由callee保存,确保callee调用前后这些寄存器的值不变,通常是压入栈中。
3.有哪些调用约定
不同架构和操作系统,调用约定可能不同,常见的调用约定如下:
- cdecl (C declaration): 32位平台常见的一种约定,GCC、Clang、VS编译器均默认采用该约定。
- stdcall: 32位windows上的一种约定。
- Microsoft x64: 微软提出的基于x86_64架构的windows系统上一种调用约定。
- System V AMD64: 基于x86_64架构Linux系统上广泛使用的一种调用约定。
本文及后续文章,如无特别说明,均采用System V AMD64该调用约定,且平台为x64 Linux。
4.System V AMD64
部分调用约定细节:
- 调用call指令之前,必须保证栈是16字节对齐的。
- 一个函数在调用时,如果参数个数小于等于6个时,前6个参数是从左至右依次存放于RDS、RSI、RDX、RCX、R8、R9寄存器中,如果参数大于6个,剩余的参数通过栈传递,从右至左顺序入栈。
- XMM0~XMM7用于传递浮点参数,前8个参数从左至右依次存放在XMM0~XMM7中,剩余的参数通过栈传递,从右至左顺序入栈。
- 被调用函数的返回值64位以内(包括64位)的整形或指针时,则返回值存放在RAX,如果返回值128位的,则高64位放入RDX。
- 如果返回值是浮点值,则返回值存放在XMM0。
- 可选地,被调函数推入 RBP,以使 caller-return-rip 在其上方8个字节,并将 RBP 设置为已保存的 RBP 的地址。这允许遍历现有堆栈帧,通过指定GCC的 -fomit-frame-pointer 选项可以消除此问题。
- 对于 R8~R15 寄存器,我们可以使用 r8, r8d, r8w, r8b 分别代表 r8 寄存器的64位、低32位、低16位和低8位。
- 等等
5.x86跟x64相比的寄存器的变化,如图:
6.简单例子
源代码如下:
1 #include <iostream> 2 3 //八个整形参数 4 int add(int x, int y, int a, int b ,int c,int d, int e, int f) 5 { 6 return x + y + a + b + c + d + e + f; 7 } 8 9 int main() 10 { 11 int sum_8_int = add(1,2,3,4,5,6,7,8); 12 std::cout << sum_8_int << std::endl; 13 return 0; 14 }
汇编代码如下:
1 _Z3addiiiiiiii: 2 .LFB1493: 3 .cfi_startproc 4 pushq %rbp 5 .cfi_def_cfa_offset 16 6 .cfi_offset 6, -16 7 movq %rsp, %rbp 8 .cfi_def_cfa_register 6 9 movl %edi, -4(%rbp) 10 movl %esi, -8(%rbp) 11 movl %edx, -12(%rbp) 12 movl %ecx, -16(%rbp) 13 movl %r8d, -20(%rbp) 14 movl %r9d, -24(%rbp) 15 movl -4(%rbp), %edx 16 movl -8(%rbp), %eax 17 addl %eax, %edx 18 movl -12(%rbp), %eax 19 addl %eax, %edx 20 movl -16(%rbp), %eax 21 addl %eax, %edx 22 movl -20(%rbp), %eax 23 addl %eax, %edx 24 movl -24(%rbp), %eax 25 addl %eax, %edx 26 movl 16(%rbp), %eax 27 addl %eax, %edx 28 movl 24(%rbp), %eax 29 addl %edx, %eax 30 popq %rbp 31 .cfi_def_cfa 7, 8 32 ret 33 .cfi_endproc 34 35 main: 36 .LFB1494: 37 .cfi_startproc 38 pushq %rbp 39 .cfi_def_cfa_offset 16 40 .cfi_offset 6, -16 41 movq %rsp, %rbp 42 .cfi_def_cfa_register 6 43 subq $16, %rsp 44 pushq $8 45 pushq $7 46 movl $6, %r9d 47 movl $5, %r8d 48 movl $4, %ecx 49 movl $3, %edx 50 movl $2, %esi 51 movl $1, %edi 52 call _Z3addiiiiiiii 53 addq $16, %rsp 54 movl %eax, -4(%rbp) 55 movl -4(%rbp), %eax 56 movl %eax, %esi 57 leaq _ZSt4cout(%rip), %rdi 58 call _ZNSolsEi@PLT 59 movq %rax, %rdx 60 movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rax 61 movq %rax, %rsi 62 movq %rdx, %rdi 63 call _ZNSolsEPFRSoS_E@PLT 64 movl $0, %eax 65 leave 66 .cfi_def_cfa 7, 8 67 ret 68 .cfi_endproc
调用堆栈图如下:
1.图片1对应指令执行完51行的栈帧:
movl $1, %edi
寄存器状态如下:
1 rbx 0x0 0 2 rcx 0x4 4 3 rdx 0x3 3 4 rsi 0x2 2 5 rdi 0x1 1 6 rbp 0x7fffffffe3b0 0x7fffffffe3b0 7 rsp 0x7fffffffe390 0x7fffffffe390 8 r8 0x5 5 9 r9 0x6 6
参数1、2、3、4、5、6分别放在寄存器 rdi、rsi、rdx、rcx、r8、r9中(低32位),
参数7、8存放在堆栈中,如图1所示,参数8先入栈,参数7后入栈,符合从右至左的入栈顺序约定。
2.图片2对应开始调用call指令的准备工作,将call指令的下一条指令压入栈中,即53行
3.图片3对应进入add函数后,并执行至17行的栈帧。
26行指令,以及28行指令,分别获取了栈上数据,即add函数的第7、第8个参数。