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個參數。