Calling conventions 是指一個函式呼叫另一個函式時,這兩個函式應該遵守的規範。例如,它們之間如何傳遞參數與回傳值。Calling conventions 是 application binary interface(ABI)的一部分。
Table of Contents
Registers
x86-64 有 16 個 general purpose registers,如下表。所謂的 general purpose registers,是指在撰寫 assembly 時,作為一般用途使用的 registers。然而,C 語言將一些 general purpose registers 賦予特殊的用途。當一個 C 的函式名叫 caller 要呼叫另一個 C 的函式名叫 callee 時,caller 會將參數存放在 %rdi
、%rsi
等 registers,而 callee 就可以透過這些 registers 取得參數。所以,caller 和 callee 必須要知道哪些 registers 被用來傳遞參數,以及它們的順序。這些規範就是 calling conventions。
Callee 的前 6 個參數是 caller 透過 %rdi
、%rsi
、%rdx
、%rcx
、%r8
、以及 %r9
依序傳遞給 callee,而 callee 的回傳值是透過 %rax
回傳給 caller。
Callee-saved registers 是指,這些 registers 是屬於 caller。換句話說,如果 callee 要使用這些 registers 的話,它必須要先將它們的值保存在 stack 中。當 callee 要結束回到 caller 之前,它必須從 stack 中回復這些 registers 的值。
Caller-saved registers 是指,這些 registers 是屬於 callee。換句話說,當 caller 呼叫 callee 之後,callee 可以任意使用這些 registers。所以,caller 必須要先將這些 registers 的值保存在 stack 中,再呼叫 callee。當 callee 結束回到 caller 後, caller 可以從 stack 中回復這些 registers 的值。
64-bit | 32-bit | 16-bit | 8-bit | Special Purpose | Caller-saved | Callee-saved |
---|---|---|---|---|---|---|
rax | eax | ax | ah, al | Return value | ✓ | |
rbx | ebx | bx | bh, bl | ✓ | ||
rcx | ecx | cx | ch, cl | 4th argument | ✓ | |
rdx | edx | dx | dh, dl | 3rd argument | ✓ | |
rsi | esi | si | sil | 2nd argument | ✓ | |
rdi | edi | di | dil | 1st argument | ✓ | |
rbp | ebp | bp | bpl | Frame pointer | ✓ | |
rsp | esp | sp | spl | Stack pointer | ✓ | |
r8 | r8d | r8w | r8b | 5th argument | ✓ | |
r9 | r9d | r9w | r9b | 6th argument | ✓ | |
r10 | r10d | r10w | r10b | ✓ | ||
r11 | r11d | r11w | r11b | ✓ | ||
r12 | r12d | r12w | r12b | ✓ | ||
r13 | r13d | r13w | r13b | ✓ | ||
r14 | r14d | r14w | r14b | ✓ | ||
r15 | r15d | r15w | r15b | ✓ |
Stack Frame
如果 callee 的參數超過 6 個的話,除了前 6 個是透過 registers 傳遞,剩下的參數則是透過 stack 傳遞。另外,stack 也被用來保存 caller-saved 和 callee-saved 的 registers。所以,stack 在 calling conventions 中是相當重要的一環。
假設 callee 函式有 8 個參數、3 個 local variables、和一個回傳值,如下。
long callee(long a, long b, long c, long d, long e, long f, long g, long h) { long x; long y; long z; return 10; } void caller() { ... long x = calc(1, 2, 3, 4, 5, 6, 7, 8); ... }
Caller 的 assembly code 可能如下。因為 stack 是由 high address 向 low address 增長,所以 caller 先將 %rsp
減掉 16 來分配兩個空間,並將 7th 和 8th 參數放入裡面,再將前 6 個參數放入 registers。然後,呼叫 callee,此時 call
指令會將下一個指令的地址放入到 stack 中,並將 %rsp
減掉 8。
當 callee 結束後,回到 caller 時,caller 必須要清除剛剛在 stack 分配的兩空間,所以將 %rsp
加上 16。然後,從 %rax
中取得 callee 的傳回值,並儲存到 local variable。
caller: ... subq $16, %rsp # Make stack space for the 7th and 8th parameters movq $8, 8(%rsp) movq $7, (%rsp) movq $6, %r9 movq $5, %r8 movq $4, %rcx movq $3, %rdx movq $2, %rsi movq $1, %rdi call callee # Call callee and push the return address onto the stack addq $16, %rsp # Clean up the stack movq %rax, -8(%rbp) # Save the return value to a local variable ...
Caller 的 assembly code 可能如下。Callee 先儲存 caller 的 %rbp
到 stack,並設定好自己的 %rbp
。然後,將 %rsp
減掉 24 來分配三個空間給三個 local variables。
Callee 結束前,將要回傳給 caller 的值放入 %rax
。然後,leave
指令會複製 %rbp
到 %rsp
,並從 stack 中 pop 前一個 %rbp
的值。最後,ret
指令從 stack 中 pop return address。
callee: pushq %rbp # Save previous %rbp to the stack movq %rsp, %rbp # Move %rsp to %rbp subq $24, %rsp # Allocate space for the local variables ... movq $10, %rax # Move return value to %rax leave # Copy %rbp to %rsp, restore previous %rbp from the stack ret # Return by pop the return address from the stack
當 caller 呼叫 callee 後,stack 會如下圖。請搭配下圖,再重新看一下以上 caller 呼叫 caller 的過程,你會更加了解 stack 的變化。
Red zone 是指,在 %rsp
之後的 128 bytes。函式可以使用 red zone 來儲存暫時的資料。特別是當函式是一個 leaf function,也就是不會呼叫其他函式的函式,則它可以直接使用此區域而不需要調整 %rsp
來分配空間。
範例
現在讓我們用以下的 C 程式碼作為例子,來看看 GCC 是如何處理函式呼叫。首先,將以下的 C 程式碼儲存為 test.c。
#include <stdio.h> long callee(long a, long b, long c, long d, long e, long f, long g, long h) { long sum = a + b + c + d + e + f + g + h; long value = sum * 10; return value; } void caller() { long value = callee(1, 2, 3, 4, 5, 6, 7, 8); printf("value is %ld\n", value); }
我們可以使用 gcc -S test.c
來產生 test.s,如下。我們可以看到 caller 呼叫 callee 時,並不完全如同我們在本章所描述的過程,尤其是在 %rsp
的處理上很不一樣。這是因為 GCC 非常地聰明。它可以判斷是否可以不需要變動 %rsp
,且達到相同的結果。這樣可以減少一些 assembly code 以增進效能。
.file "test.c" .text .globl callee .type callee, @function callee: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movq %rdi, -24(%rbp) movq %rsi, -32(%rbp) movq %rdx, -40(%rbp) movq %rcx, -48(%rbp) movq %r8, -56(%rbp) movq %r9, -64(%rbp) movq -32(%rbp), %rax movq -24(%rbp), %rdx addq %rax, %rdx movq -40(%rbp), %rax addq %rax, %rdx movq -48(%rbp), %rax addq %rax, %rdx movq -56(%rbp), %rax addq %rax, %rdx movq -64(%rbp), %rax addq %rax, %rdx movq 16(%rbp), %rax addq %rax, %rdx movq 24(%rbp), %rax addq %rdx, %rax movq %rax, -8(%rbp) movq -8(%rbp), %rdx movq %rdx, %rax salq $2, %rax addq %rdx, %rax addq %rax, %rax movq %rax, -16(%rbp) movq -16(%rbp), %rax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size callee, .-callee .section .rodata .LC0: .string "value is %ld\n" .text .globl caller .type caller, @function caller: .LFB1: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $32, %rsp movq $8, 8(%rsp) movq $7, (%rsp) movl $6, %r9d movl $5, %r8d movl $4, %ecx movl $3, %edx movl $2, %esi movl $1, %edi call callee movq %rax, -8(%rbp) movq -8(%rbp), %rax movq %rax, %rsi movl $.LC0, %edi movl $0, %eax call printf leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1: .size caller, .-caller .ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)" .section .note.GNU-stack,"",@progbits
結語
如果你需要使用 assembly 呼叫 C 的函式,或是 C 呼叫 assembly code,那絕對是必須要了解 calling conventions。此外,如果你有在撰寫 C/C++ 程式碼,了解 calling conventions 也是非常有幫助的。
參考
- System V Application Binary Interface AMD64 Architecture Processor Supplement.
- Calling Conventions, OSDev Wiki.
- x86 calling conventions, Wikipedia.
- Stack frame layout on x86_64, Eli Bendersky’s website.
- x86-64 Registers.