当前位置: 首页 > 知识库问答 >
问题:

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

陶睿
2023-03-14

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

共有3个答案

漆雕和雅
2023-03-14

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

例如,一条规则规定,为函数生成的程序集代码必须在更改值之前保存保留的寄存器的值,并且代码必须在返回给其调用方之前还原保存的值。对于暂存寄存器,不需要生成的代码来保存和恢复寄存器值;如果需要,它可以这样做,但是符合标准的软件不允许依赖于此行为(如果这样做,它就不是符合标准的软件)。

如果您正在编写汇编代码,您将负责遵守这些规则(您正在扮演编译器的角色)。也就是说,如果您的代码更改了被调用方保留的寄存器,您将负责插入保存和恢复原始寄存器值的指令。如果程序集代码调用外部函数,则代码必须以符合标准的方式传递参数,这可能取决于以下事实:当被调用方返回时,保留的寄存器值实际上是保留的。

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

例如,考虑一个名为foo的C函数,其声明如下,并且永远不会占用其地址:

static foo(int x);

编译时,编译器100%确定这个函数只能被它当前编译的文件中的其他代码调用。函数foo永远不能被其他任何东西调用,因为它是静态的定义。因为编译器在编译时知道foo的所有调用者,所以编译器可以自由地使用它想要的任何调用序列(直到并包括根本不调用,也就是说,将foo的代码内联到foo的调用者中。

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

诸葛煜
2023-03-14

实验方法:反汇编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);
}

GitHub上游。

编译和反汇编:

 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

规范中唯一缺少的是< code>rsp,但是我们当然希望栈被恢复。仔细阅读组件可以确认它在这种情况下得到维护:

  • sub $0x8, %rsp: 在堆栈上分配 8 个字节以将 %rdi 保存在 %rdi, -0x30(%rbp),这是针对内联程序集 m 约束完成的
  • lea -0x28(%rbp), %rsp%rsp 恢复到 sub 之前,即 mov %rsp 之后有 5 个 pops, %rbp
  • 有6个推送和6个相应的弹出
  • 没有其他说明触摸 %rsp

在Ubuntu 18.10、GCC 8.2.0中测试

盛骏祥
2023-03-14

下面是文档中完整的寄存器及其用法表[PDF链接]:

r12r13r14r15、rbxrsprbp是被调用方保存的寄存器-它们在“跨函数调用保存”列中有“是”。

 类似资料:
  • Windows上是否有任何方法可以解决XMM寄存器保留在函数调用中的要求?(除了将其全部写入汇编中) 不幸的是,我有许多AVX2内在函数因此而臃肿。 例如,这将被编译器(MSVC)放置在函数的顶部: 00007FF9D0EBC602 vmovaps xmmword ptr[rsp 1490h]、xmm6 00007FF9 D0EBC60B vmovaps XMMWORDPTR[rsp 1480h]

  • 在x86-64中,如果某些通用寄存器比其他寄存器更受欢迎,某些指令会执行得更快吗? 例如,会比执行得更快吗?我可以想象后者需要一个REX前缀,这会使指令获取速度变慢? 使用代替怎么样?或呢?其他操作?更小的寄存器,如vs?vs? AMD vs Intel?更新的处理器?较旧的处理器?指令的组合? 澄清:某些通用登记册是否应该优先于其他登记册,它们是哪些?

  • 本文向大家介绍js函数调用方式有哪些?相关面试题,主要包含被问及js函数调用方式有哪些?时的应答技巧和注意事项,需要的朋友参考一下 function func() { console.log(1) } func(); func.apply(); func.call();

  • 基本上指令有8->16、8->32、8->64、16->32和16->64。 32->64的转换在哪里?我必须使用签名版本吗? 如果是的话,您如何使用完整的64位来表示无符号整数?

  • 如果反射值对象(reflect.Value)中值的类型为函数时,可以通过 reflect.Value 调用该函数。使用反射调用函数时,需要将参数使用反射值对象的切片 []reflect.Value 构造后传入 Call() 方法中,调用完成时,函数的返回值通过 []reflect.Value 返回。 下面的代码声明一个加法函数,传入两个整型值,返回两个整型值的和。将函数保存到反射值对象(refle

  • 本文向大家介绍请问线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的相关面试题,主要包含被问及请问线程需要保存哪些上下文,SP、PC、EAX这些寄存器是干嘛用的时的应答技巧和注意事项,需要的朋友参考一下 参考回答: 线程在切换的过程中需要保存当前线程Id、线程状态、堆栈、寄存器状态等信息。其中寄存器主要包括SP PC EAX等寄存器,其主要功能如下: SP:堆栈指针,指向当前栈的栈顶地