让编译器在调用函数之前忽略设置参数寄存器

Have compiler ignoring setting an argument register before calling function

TL;DR; 我正在寻找一种标准方法来基本上告诉编译器传递给定中发生的任何事情注册到下一个功能。

基本上我有一个函数int bar(int a, int b, int c)。在某些情况下 c 未使用,我希望能够在 c 未使用的情况下调用 bar 而无需以任何方式修改 rdx

例如,如果我有

int foo(int a, int b) { 
    int no_init; 
    return bar(a, b, no_init); 
}

我希望程序集是:

尾声

    jmp bar

或正常通话

   call bar

注意:clang 通常会生成我要查找的内容。但我不确定在更复杂的函数中是否总是如此,我希望每次构建时都不必检查程序集。

GCC 产生:

尾声

    xorl %edx, %edx
    jmp bar

或正常通话

   xorl %edx, %edx
   call bar

我可以使用内联汇编获得我想要的结果,即将 foo(对于尾调用)更改为

int foo(int a, int b) {
    asm volatile("jmp bar" : : :);
    __builtin_unreachable();
}

编译为

   jmp bar

我知道 xorl %edx, %edx 的性能影响尽可能接近 0,但是

我想知道是否有一个标准的方法来实现这个。

也就是说,对于任何给定的情况,我都可能找到破解方法。但这将需要我每次都验证程序集。我正在寻找一种方法,您基本上可以告诉编译器“传递寄存器中发生的任何事情”。

参见示例:https://godbolt.org/z/eh1vK8

编辑:-O3 设置时发生了这种情况。

转换函数以具有较小的签名(即较少的参数):

extern int bar(int, int, int);

int foo(int a, int int b) {
    return ((int (*)(int,int))bar)(a, b); 
}

也许为2个参数栏做一个宏,甚至去掉foo:

extern int bar3(int, int, int);

#define bar2(a,b) ((int (*)(int,int))bar3)(a,b)

int userOfBar(int a, int b) { return bar2 (a,b); }

https://godbolt.org/z/Gn4a69

奇怪的是,鉴于上面的 gcc 没有触及 %edx,但 clang 确实...哦,好吧。

(仍然不能保证编译器不会触及某些寄存器,那是它的领域。否则,您可以直接在汇编中编写这些函数,避免中间人。)

I am wondering if there is a standard way to achieve this.

I.e I can probably find a hack for it for any given case. But that will require me verifying the assembly each time. I am looking for a method that you can basically tell the compiler "pass whatever happened to be in register".

不,在 C 或 C++ 中都没有实现它的标准方法。这两种语言都不涉及任何低级函数调用语义,甚至都不承认 CPU 寄存器的存在,* 并且两种语言都要求每个函数调用都提供对应于所有非可选参数(在 C 中只是“所有声明的参数”)。

For example if I have

int foo(int a, int b) { 
    int no_init; 
    return bar(a, b, no_init); 
}

... 然后你获得 未定义的行为 作为使用 no_init 的值的结果,而它是不确定的。任何接受它的任何特定 C 或 C++ 实现在定义上都是非标准的。

如果你想调用bar(),但你不关心第三个参数传递的是什么值,那么为什么不选择一个方便的值来传递呢?零,例如:

    return bar(a, b, 0); 

*就任一语言标准而言,即使 register 关键字也不会这样做。

一种适用于大多数平台上 gcc/clang 的方法是

    int no_init; 
    asm("" : "=r" (no_init));
    return bar(a, b, no_init); 

这样你就不必对编译器撒谎 bar 的原型(这可能会破坏一些调用约定),并且你可以让编译器认为 no_init 真的被初始化了.

我想知道像安腾这样的架构,它的“陷阱位”会在访问未初始化的寄存器时导致错误。此代码在那里可能不安全。

据我所知,没有可移植的方法来获得此行为,但您可以 ifdef 它:

#ifdef __GNUC__
#define UNUSED_INT ({ int x; asm("" : "=r" (x)); x; })
#else
#define UNUSED_INT 0
#endif
// ...
   bar(a, b, UNUSED_INT);

然后您可以在必要时回退到(无限小)效率较低但正确的代码。

它导致在 gcc/x86-64 上出现 jmp,参见 https://godbolt.org/z/d3ordK。在 x86-32 上,它不是很理想,因为它压入一个未初始化的寄存器,而不是仅仅从 esp 调整现有的减法。请注意,裸 jmp/call 在 x86-32 上是不安全的,因为第三个堆栈槽可能包含一些重要的东西,并且允许被调用者覆盖它(即使该变量在您想到的路径上未使用,编译器可以将其用作临时 space).

一种可移植的替代方法是将 bar 重写为可变参数。但是,它需要使用 va_arg 来检索存在的第三个参数,而这往往效率较低。

请注意,如果被调用函数确实读取了它的第 3 个 arg,那么不写入它可能会产生对上次使用的 EDX 的错误依赖。例如,它可能是缓存未命中加载或长链计算的结果。

在很多情况下,GCC 会小心地使用异或零来打破错误的依赖关系,例如在 cvtsi2ss(糟糕的 ISA 设计)或 popcnt(Sandybridge 系列怪癖)之前。

通常 xor edx,edx 基本上是一个浪费的 2 字节 NOP,但它确实防止了其他独立依赖链(关键路径)的可能耦合。

如果您确定要阻止编译器保护您免受此影响的尝试,那么 Nate 的 asm("" :"=r"(var)); 是执行 _mm_undefined_ps() 的整数版本的好方法,它实际上留下了一个寄存器未初始化。 (请注意,_mm_undefined_ps 不保证 XMM reg 不被写入;一些编译器会为您异或零一个,而不是完全实现内在设计允许英特尔编译器的错误依赖鲁莽。)