x86-64 Calling Conventions

Photo by Patrick on Unsplash
Photo by Patrick on Unsplash
Calling conventions 是指一個函式呼叫另一個函式時,這兩個函式應該遵守的規範。例如,它們之間如何傳遞參數與回傳值。Calling conventions 是 application binary interface(ABI)的一部分。

Calling conventions 是指一個函式呼叫另一個函式時,這兩個函式應該遵守的規範。例如,它們之間如何傳遞參數與回傳值。Calling conventions 是 application binary interface(ABI)的一部分。

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-bit32-bit16-bit8-bitSpecial PurposeCaller-savedCallee-saved
raxeaxaxah, alReturn value
rbxebxbxbh, bl
rcxecxcxch, cl4th argument
rdxedxdxdh, dl3rd argument
rsiesisisil2nd argument
rdiedididil1st argument
rbpebpbpbplFrame pointer
rspespspsplStack pointer
r8r8dr8wr8b5th argument
r9r9dr9wr9b6th argument
r10r10dr10wr10b
r11r11dr11wr11b
r12r12dr12wr12b
r13r13dr13wr13b
r14r14dr14wr14b
r15r15dr15wr15b
x86-64 Registers.

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 的變化。

x86-64 Calling Conventions.
x86-64 Calling Conventions.

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 也是非常有幫助的。

參考

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

You May Also Like
Photo by Timothée Geenens on Unsplash
Read More

x86 Memory Map

在 x86 PC 起動後,它會處在 real mode。此時,我們可以存取 1 MB 以下的 memory。然而,BIOS 也會使用一些 memory。因此,我們必須要知道 BIOS 佔據哪些範圍,才能夠避開它們
Read More
Photo by Lanju Fotografie on Unsplash
Read More

Makefile

Makefile 是 Linux 中最常用的編譯工具了。Stuart Feldman 在 1967 的 Bell Labs 裡創造了它。雖然它可能比你我的年紀都還大,但是它現在還是依然地活躍。本文章將介紹 Makefile 的一些基本語法。
Read More
Photo by NEOM on Unsplash
Read More

ELF

ELF file 是 Linux 使用的檔案格式。不管是要學習 linkers 或是反組譯,都需要了解 ELF file。本文章將介紹 ELF 檔案格式。
Read More