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

#1 楼

这是文档[PDF链接]中完整的寄存器表及其使用情况:



r12r13r14r15rbxrsprbp是被调用者保存的寄存器-在“跨函数调用保留”列中有一个“是”。

评论


那旗帜呢?喜欢DF?

–套接字对
2014年5月3日13:55

@socketpair:DF必须在任何调用或返回之前都未设置,因此可以假定没有cld的向上计数行为。条件标志(如ZF)被调用。我完全忘记了ABI文档关于FP舍入模式和非正规数为零的说法。也许就像某个函数对其进行了修改一样,它必须在返回之前保存/恢复以前的状态,但请不要误解FP部分。

– Peter Cordes
16年2月1日在1:00

我建议从这些来源之一而不是uclibc网站获取PDF:stackoverflow.com/questions/18133812/…:-)

– Ciro Santilli郝海东冠状病六四事件法轮功
19 Mar 17 '19 at 13:02

#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上游。

编译和反汇编:

 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    
rbx
r12
r13
r14
r15
rbp
x8,%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 q4312078qx1,%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)


,因此我们清楚地看到以下内容已被推送并弹出:

q4312078q

规范中唯一缺少的一个是rsp,但是我们当然希望堆栈可以恢复。仔细阅读程序集可以确认在这种情况下它得到维护:



sub %rdix8, %rsp:在堆栈上分配8个字节以将%rdi, -0x30(%rbp)保存在+m,这对于内联程序集是完成的lea -0x28(%rbp), %rsp约束

%rspsub恢复到mov %rsp, %rbp之前,即在%rsp之后弹出5次

有6次推送和6次相应的弹出
没有其他指令触摸q4312079q


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

评论


为什么在与不同选项相同的约束中使用+ a和+ r? “ + rax”极具欺骗性,因为它看起来像是在RAX寄存器中请求输入(您不能因为输入被破坏而无法输入)。但是事实并非如此,您实际上是在任何GP寄存器(r),RAX(a)或任何XMM寄存器(x)中要求它。即相当于“ + xr”。由于将一个XMM寄存器保持原状,因此编译器选择XMM15。您可以通过将asm模板字符串设置为“ nop#%0”来看到此内容,以便它在注释中扩展%0。 godbolt.org/z/_cLq2T。

– Peter Cordes
19年3月17日在17:17

叮cho声是“ + rx”的,但不是“ + xr”的。我认为clang实际上并没有正确地使用约束替代方案,而只选择了一种。这可能就是为什么“ + rm”约束经常让clang溢出寄存器,就像无缘无故地选择了“ + m”选项一样。

– Peter Cordes
19年3月17日在17:19

@PeterCordes哎呀,我有点着急,只是想成为+ r人,我爱这个东西不会在rax上爆炸。在这种情况下,+ m更好。

– Ciro Santilli郝海东冠状病六四事件法轮功
19年3月17日在18:17

您可能已经找到了编译器错误。您在RSP和RBP上声明了一个破坏者,但是gcc和clang都在asm语句之后使用RBP(以还原RSP),即它们假定RBP仍然有效。他们还使用%0的RBP相对寻址模式,但是我想clobber声明不是早期的专家。尽管如此,这还是令人惊讶的。如果我们仅声明RSP交换器(godbolt.org/z/LhpXWX注释了RBP交换器),则它们将构成堆栈帧并使用RBP相对寻址模式,这与两个交换器相同。 TL:DR:RSP + RBP clobber = bug,即使其他法规没有被破坏。

– Peter Cordes
19 Mar 17 '19在23:43

#3 楼

ABI规定了允许符合标准的软件。它主要是为编译器,链接器和其他语言处理软件的作者编写的。这些作者希望他们的编译器生成可以与由相同(或不同)编译器编译的代码一起正常工作的代码。他们都必须同意一套规则:如何将函数的形式参数从调用方传递给被调用方,如何将函数返回值从被调用方传递回给调用方,哪些寄存器在调用边界内被保留/暂存/未定义,等等。例如,一条规则指出,为函数生成的汇编代码必须在更改值之前先保存一个保留寄存器的值,并且该代码必须在返回到其值之前恢复保存的值。呼叫者。对于暂存寄存器,不需要生成的代码来保存和恢复寄存器值;如果需要,它可以这样做,但是不允许符合标准的软件依赖此行为(如果符合标准,则不是标准软件)。

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

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

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

static foo(int x);


在编译时,编译器100%确定该函数只能由当前正在编译的文件中的其他代码调用。鉴于定义为静态的含义,函数foo不能被任何其他函数调用。因为编译器在编译时就知道foo的所有调用方,所以编译器可以自由使用所需的任何调用序列(直到并完全不进行调用,即将foo的代码内联到foo的调用方中) 。

作为汇编代码的作者,您也可以执行此操作,即,您可以在两个或多个例程之间实现“私有协议”,只要该协议不干扰或违反了符合标准的软件的期望。