[英]What registers are preserved through a linux x86-64 function call
我相信我了解 linux x86-64 ABI 如何使用寄存器和堆棧將參數傳遞給函數(參見之前的 ABI 討論)。 我感到困惑的是,如果/哪些寄存器應該在函數調用中保留。 也就是說,哪些寄存器可以保證不被破壞?
以下是文檔 [ PDF 鏈接] 中寄存器及其使用的完整表格:
r12
, r13
, r14
, r15
, rbx
, rsp
, rbp
是被調用方保存的寄存器-他們在“跨函數調用保留的”一欄中有一個“是”。
實驗方法:反匯編 GCC 代碼
主要是為了好玩,但也是為了快速驗證您是否正確理解了 ABI。
讓我們嘗試使用內聯匯編破壞所有寄存器以強制 GCC 保存和恢復它們:
主文件
#include <inttypes.h>
uint64_t inc(uint64_t i) {
__asm__ __volatile__(
""
: "+m" (i)
:
: "rax",
"rbx",
"rcx",
"rdx",
"rsi",
"rdi",
"rbp",
"rsp",
"r8",
"r9",
"r10",
"r11",
"r12",
"r13",
"r14",
"r15",
"ymm0",
"ymm1",
"ymm2",
"ymm3",
"ymm4",
"ymm5",
"ymm6",
"ymm7",
"ymm8",
"ymm9",
"ymm10",
"ymm11",
"ymm12",
"ymm13",
"ymm14",
"ymm15"
);
return i + 1;
}
int main(int argc, char **argv) {
(void)argv;
return inc(argc);
}
編譯和反匯編:
gcc -std=gnu99 -O3 -ggdb3 -Wall -Wextra -pedantic -o main.out main.c
objdump -d main.out
拆解包含:
00000000000011a0 <inc>:
11a0: 55 push %rbp
11a1: 48 89 e5 mov %rsp,%rbp
11a4: 41 57 push %r15
11a6: 41 56 push %r14
11a8: 41 55 push %r13
11aa: 41 54 push %r12
11ac: 53 push %rbx
11ad: 48 83 ec 08 sub $0x8,%rsp
11b1: 48 89 7d d0 mov %rdi,-0x30(%rbp)
11b5: 48 8b 45 d0 mov -0x30(%rbp),%rax
11b9: 48 8d 65 d8 lea -0x28(%rbp),%rsp
11bd: 5b pop %rbx
11be: 41 5c pop %r12
11c0: 48 83 c0 01 add $0x1,%rax
11c4: 41 5d pop %r13
11c6: 41 5e pop %r14
11c8: 41 5f pop %r15
11ca: 5d pop %rbp
11cb: c3 retq
11cc: 0f 1f 40 00 nopl 0x0(%rax)
所以我們清楚地看到以下內容被推送和彈出:
rbx
r12
r13
r14
r15
rbp
規范中唯一缺少的是rsp
,但我們當然希望堆棧能夠恢復。 仔細閱讀程序集確認它在這種情況下被維護:
sub $0x8, %rsp
:在堆棧上分配 8 個字節以將%rdi
保存在%rdi, -0x30(%rbp)
,這是為內聯匯編+m
約束完成的lea -0x28(%rbp), %rsp
將%rsp
恢復到sub
之前,即在mov %rsp, %rbp
之后彈出 5 sub
%rsp
在 Ubuntu 18.10、GCC 8.2.0 中測試。
ABI 指定了一個符合標准的軟件可以期待什么。 它主要是為編譯器、鏈接器和其他語言處理軟件的作者編寫的。 這些作者希望他們的編譯器生成的代碼能夠與由相同(或不同)編譯器編譯的代碼一起正常工作。 他們都必須同意一組規則:函數的形式參數如何從調用者傳遞到被調用者,函數返回值如何從被調用者傳遞回調用者,哪些寄存器在調用邊界上是保留/暫存/未定義的,等等在。
例如,一條規則規定為函數生成的匯編代碼必須在更改值之前保存保留寄存器的值,並且代碼必須在返回到其調用者之前恢復保存的值。 對於暫存寄存器,生成的代碼不需要保存和恢復寄存器值; 如果它願意,它可以這樣做,但不允許符合標准的軟件依賴於這種行為(如果它不是符合標准的軟件)。
如果您正在編寫匯編代碼,則您有責任遵守這些相同的規則(您正在扮演編譯器的角色)。 也就是說,如果您的代碼更改了被調用者保留的寄存器,則您負責插入保存和恢復原始寄存器值的指令。 如果您的匯編代碼調用外部函數,則您的代碼必須以符合標准的方式傳遞參數,這取決於這樣一個事實,即當被調用者返回時,保留的寄存器值實際上已保留。
這些規則定義了符合標准的軟件如何相處。 然而,這是完全合法的寫(或生成)的代碼不被這些游戲規則! 編譯器一直這樣做,因為他們知道在某些情況下不需要遵循規則。
例如,考慮一個名為 foo 的 C 函數,它聲明如下,並且從不獲取其地址:
static foo(int x);
在編譯時,編譯器 100% 確定此函數只能由當前正在編譯的文件中的其他代碼調用。 給定靜態的定義,函數foo
永遠不能被其他任何東西調用。 因為編譯器知道所有的呼叫者foo
在編譯時,編譯器可以自由使用任何調用就是了(序列直至並包括不使所有的呼叫,也就是內聯代碼foo
到的呼叫者foo
.
作為匯編代碼的作者,您也可以這樣做。 也就是說,您可以在兩個或多個例程之間實現“私人協議”,只要該協議不干擾或違反符合標准的軟件的期望。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.