[英]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.