通过linux x86-64函数调用保留了哪些寄存器

时间:2013-08-02 19:22:44

标签: linux assembly x86-64 abi

我相信我理解linux x86-64 ABI如何使用寄存器和堆栈将参数传递给函数(参见previous ABI discussion)。我感到困惑的是,在函数调用中是否预期保留了哪些寄存器。也就是说,哪些寄存器被保证不被破坏?

3 个答案:

答案 0 :(得分:56)

这是完整的寄存器表及其在文档[PDF Link]中的用法:

table from docs

r12r13r14r15rbxrsprbp是被调用者保存的寄存器 - 他们在“保留跨函数调用”列中显示“是”。

答案 1 :(得分:4)

ABI指定允许符合标准的软件。它主要是为编译器,链接器和其他语言处理软件的作者编写的。这些作者希望他们的编译器能够生成能够与由相同(或不同)编译器编译的代码一起正常工作的代码。它们都必须同意一组规则:函数的形式参数如何从调用者传递给被调用者,函数返回值如何从被调用者传递回调用者,哪些寄存器在调用边界上保留/暂存/未定义,等等上。

例如,有一条规则规定,生成的函数汇编代码必须在更改值之前保存保留寄存器的值,并且代码必须在返回其调用者之前恢复保存的值。对于临时寄存器,生成的代码不需要保存和恢复寄存器值;如果需要,它可以这样做,但不允许符合标准的软件依赖于这种行为(如果它不是符合标准的软件)。

如果您正在编写汇编代码,负责按照这些相同的规则进行播放(您正在扮演编译器的角色)。也就是说,如果您的代码更改了被调用者保留的寄存器,则您负责插入保存和恢复原始寄存器值的指令。如果汇编代码调用外部函数,则代码必须以符合标准的方式传递参数,并且它可以取决于当被调用者返回时保留的寄存器值实际上被保留的事实。

规则定义了符合标准的软件如何相处。但是,编写(或生成)通过这些规则播放的代码是完全合法的!编译器一直这样做,因为他们知道在某些情况下不需要遵守规则。

例如,考虑一个名为foo的C函数,该函数声明如下,并且从不使用其地址:

static foo(int x);

在编译时,编译器100%确定此函数只能由当前正在编译的文件中的其他代码调用。函数foo不能被其他任何东西调用,因为它定义了静态意味着什么。因为编译器在编译时知道foo的所有调用者,所以编译器可以自由地使用它想要的任何调用序列(直到并且包括不进行调用,即内联{{1}的代码。 1}}进入foo的调用者。

作为汇编代码的作者,您也可以这样做。也就是说,您可以实施一个私人协议"在两个或更多例程之间,只要该协议不会干扰或违反符合标准的软件的期望。

答案 2 :(得分:2)

实验方法:反汇编GCC代码

主要是为了娱乐,同时也是您了解ABI权利的快速证明。

让我们尝试使用内联汇编破坏所有寄存器,以强制GCC保存和恢复它们:

main.c

#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);
}

GitHub upstream

编译和反汇编:

 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次
  • 有6次推动和6次相应的弹出动作
  • 没有其他说明可以触摸%rsp

在Ubuntu 18.10,GCC 8.2.0中进行了测试。