【问题标题】:Inline assembly that clobbers the red zone破坏红色区域的内联汇编
【发布时间】:2011-09-16 21:11:07
【问题描述】:

我正在编写一个密码学程序,核心(一个宽乘法例程)是用 x86-64 汇编编写的,这既是为了提高速度,也是因为它广泛使用像 adc 这样的指令,而这些指令不容易从 C 中访问。我不想内联这个函数,因为它很大,并且在内部循环中被多次调用。

理想情况下,我还想为这个函数定义一个自定义调用约定,因为它在内部使用所有寄存器(rsp 除外),不会破坏它的参数,并在寄存器中返回。现在,它已适应 C 调用约定,但这当然会使其速度变慢(大约 10%)。

为避免这种情况,我可以使用asm("call %Pn" : ... : my_function... : "cc", all the registers); 调用它,但有没有办法告诉 GCC 调用指令与堆栈混淆?否则 GCC 只会将所有这些寄存器放在红色区域中,而顶部的寄存器将被破坏。我可以用 -mno-red-zone 编译整个模块,但我更喜欢告诉 GCC,比如说,红色区域的前 8 个字节将被破坏,这样它就不会在那里放任何东西。

【问题讨论】:

  • 只是一个未经测试的,但你不能只指定一个额外的虚拟输入,以便 GCC 将其置于红色区域并(无害地)被破坏吗?
  • 嗯。可能不可靠。我发现很难控制 GCC 溢出到堆栈的时间和地点。在我写的其他加密内容中,我尝试了不同程度的成功来抑制 GCC 写入的倾向,例如,无缘无故将整个密钥表写入堆栈。
  • sp 添加为破坏者?添加内存破坏器?
  • 如何将加密例程定义为宏(使用文件顶部的顶级 asm)?然后通过扩展 asm 从 C 代码中的多个位置调用它(而不是 calling 它)稍微不那么可怕(尽管它确实使可执行文件膨胀)。您仍然可以破坏所有寄存器,但堆栈不受影响。顺便说一句,加密货币如何知道要加密什么?通过内联访问全局变量可能很棘手。此外,破坏 sp 有 no effect.

标签: c gcc x86 inline-assembly red-zone


【解决方案1】:

从你原来的问题我没有意识到 gcc 限制红区使用叶功能。我不认为 x86_64 ABI 要求这样做,但对于编译器来说,这是一个合理的简化假设。在这种情况下,您只需要将调用您的汇编例程的函数设为非叶以进行编译:

int global;

was_leaf()
{
    if (global) other();
}

GCC 无法判断global 是否为真,因此它无法优化对other() 的调用,因此was_leaf() 不再是叶函数。我编译了这个(使用更多触发堆栈使用的代码)并观察到作为叶子它没有移动%rsp,并且显示它的修改。

我还尝试在一片叶子中简单地分配超过 128 个字节(只是 char buf[150]),但我很震惊地看到它只做了部分减法:

    pushq   %rbp
    movq    %rsp, %rbp
    subq    $40, %rsp
    movb    $7, -155(%rbp)

如果我把打败叶子的代码放回去,就会变成subq $160, %rsp

【讨论】:

  • __attribute__(leaf),但不幸的是没有像__attribute__(nonleaf)这样的东西
  • 当 gcc 必须保留一些堆栈空间时,它没有放弃红色区域,我并不感到震惊:红色区域的好处之一是能够达到更多的内存与 disp8 位移,因此在本地人中间有 rsp 意味着它可以使用[rsp-128..+127] 寻址模式到达所有这些人。这是一个很好的优化。 (或者,如果您使用 -O3 + volatile char buf[150] 来获得相对于 RSP 的寻址模式而不是 -155(%rbp),则会是这样)
【解决方案2】:

难道你不能修改你的汇编函数以满足 x86-64 ABI 中信号的要求,方法是在函数入口时将堆栈指针移动 128 个字节吗?

或者,如果您指的是返回指针本身,请将移位放入您的调用宏中(所以sub %rsp; call...

【讨论】:

  • 我不能从函数本身中执行它,因为call 使用堆栈,因此会自行破坏。 sub $128, %rsp; call...; add $128, %rsp 有效,但并不理想。我想总的来说,最好让我的功能符合 ABI。
【解决方案3】:

最大性能的方法可能是在 asm 中编写整个内部循环(包括 call 指令,如果它真的值得展开但不是内联的话。如果完全内联导致太多 uop-cache 未命中当然是合理的其他地方)。

无论如何,让 C 调用一个包含优化循环的 asm 函数。

顺便说一句,破坏 所有 寄存器会使 gcc 很难创建一个非常好的循环,因此您很可能会自己优化整个循环。 (例如,可能在寄存器中保留一个指针,在内存中保留一个端点,因为cmp mem,reg 仍然相当有效)。

查看代码 gcc/clang 环绕修改数组元素的 asm 语句(在 Godbolt 上):

void testloop(long *p, long count) {
  for (long i = 0 ; i < count ; i++) {
    asm("  #    XXX  asm operand in %0"
    : "+r" (p[i])
    :
    : // "rax",
     "rbx", "rcx", "rdx", "rdi", "rsi", "rbp",
      "r8", "r9", "r10", "r11", "r12","r13","r14","r15"
    );
  }
}

#gcc7.2 -O3 -march=haswell

    push registers and other function-intro stuff
    lea     rcx, [rdi+rsi*8]      ; end-pointer
    mov     rax, rdi
   
    mov     QWORD PTR [rsp-8], rcx    ; store the end-pointer
    mov     QWORD PTR [rsp-16], rdi   ; and the start-pointer

.L6:
    # rax holds the current-position pointer on loop entry
    # also stored in [rsp-16]
    mov     rdx, QWORD PTR [rax]
    mov     rax, rdx                 # looks like a missed optimization vs. mov rax, [rax], because the asm clobbers rdx

         XXX  asm operand in rax

    mov     rbx, QWORD PTR [rsp-16]   # reload the pointer
    mov     QWORD PTR [rbx], rax
    mov     rax, rbx            # another weird missed-optimization (lea rax, [rbx+8])
    add     rax, 8
    mov     QWORD PTR [rsp-16], rax
    cmp     QWORD PTR [rsp-8], rax
    jne     .L6

  # cleanup omitted.

clang 将一个单独的计数器向下计数至零。但它使用加载/添加-1/存储而不是内存目标add [mem], -1/jnz

如果您自己在 asm 中编写整个循环,而不是将热循环的那部分留给编译器,您可能会做得更好。

如果可能,考虑使用一些 XMM 寄存器进行整数运算,以减少整数寄存器上的寄存器压力。在 Intel CPU 上,在 GP 和 XMM 寄存器之间移动只需要 1 个 ALU uop 和 1c 延迟。 (在 AMD 上它仍然是 1 uop,但延迟更高,尤其是在 Bulldozer 系列上)。在 XMM 寄存器中做标量整数的事情并没有更糟,如果总 uop 吞吐量是您的瓶颈,或者它节省的溢出/重新加载比成本多,那么它可能是值得的。

当然,XMM 对于循环计数器不是很可行(paddd/pcmpeq/pmovmskb/cmp/jccpsubd/ptest/jcc 相比不是很好到sub [mem], 1 / jcc),或指针,或扩展精度算术(手动进行进位比较和进位另一个paddq即使在64位整数regs的32位模式下也很糟糕'不可用)。如果您在加载/存储微指令上没有遇到瓶颈,通常最好将溢出/重新加载到内存而不是 XMM 寄存器。


如果您还需要从循环外部调用函数(清理或其他),请编写一个包装器或使用add $-128, %rsp ; call ; sub $-128, %rsp 来保留这些版本中的红色区域。 (注意-128 可以编码为imm8,但+128 不是。)

不过,在 C 函数中包含实际的函数调用并不一定可以安全地假设红色区域未使用。 (编译器可见)函数调用之间的任何溢出/重新加载都可能使用红色区域,因此破坏 asm 语句中的所有寄存器很可能会触发该行为。

// a non-leaf function that still uses the red-zone with gcc
void bar(void) {
  //cryptofunc(1);  // gcc/clang don't use the redzone after this (not future-proof)

  volatile int tmp = 1;
  (void)tmp;
  cryptofunc(1);  // but gcc will use the redzone before a tailcall
}

# gcc7.2 -O3 output
    mov     edi, 1
    mov     DWORD PTR [rsp-12], 1
    mov     eax, DWORD PTR [rsp-12]
    jmp     cryptofunc(long)

如果您想依赖编译器特定的行为,您可以在热循环之前调用(使用常规 C)非内联函数。使用当前的 gcc / clang,这将使他们保留足够的堆栈空间,因为无论如何他们都必须调整堆栈(在 call 之前对齐 rsp)。这根本不是面向未来的,但应该会起作用。


GNU C 有一个__attribute__((target("options"))) x86 function attribute,但它不能用于任意选项,并且-mno-red- zone 不是您可以基于每个功能或使用@987654350 切换的选项之一@ 在编译单元中。

你可以使用类似的东西

__attribute__(( target("sse4.1,arch=core2") ))
void penryn_version(void) {
  ...
}

但不是__attribute__(( target("mno-red-zone") ))

有一个 #pragma GCC optimize 和一个 optimize 函数属性(两者都不适用于生产代码),但 #pragma GCC optimize ("-mno-red-zone") 也不起作用。我认为这个想法是让一些重要的功能即使在调试版本中也可以使用-O2 进行优化。您可以设置-f 选项或-O

不过,您可以将函数单独放在一个文件中,然后使用-mno-red-zone 编译该编译单元。 (希望 LTO 不会破坏任何东西……)

【讨论】:

    【解决方案4】:

    不确定,但查看GCC documentation for function attributes,我发现了可能感兴趣的stdcall 函数属性。

    我仍然想知道您的 asm 调用版本有什么问题。如果只是为了美观,你可以把它变成一个宏,或者一个内联函数。

    【讨论】:

    • call 指令将当前指令指针压入堆栈。如果堆栈下没有任何东西(在“红色区域”中),这很好,但在 x86-64 上,ABI 允许编译器将东西放在叶函数中,即那些不调用任何东西的东西。但是,GCC 不会将此 call 视为调用,因为它隐藏在 inline asm 中。所以它可能会在红色区域中放置一些东西,它会被调用破坏。这不仅仅是理论上的可能性,它实际上发生并且实际上导致了我的代码中的错误。此外,stdcall 不会这样做。
    • 特别是 stdcall 的问题在于它只适用于实际的非内联函数。但是要为我的函数定义一个自定义调用约定,我试图通过内联 asm 调用它。所以 GCC 根本没有意识到它是一个函数调用(这首先是问题所在),因此我无法为其附加属性。
    【解决方案5】:

    如果创建一个用 C 语言编写并且只调用内联程序集什么都不做的虚拟函数呢?

    【讨论】:

    • 并将该函数标记为__attribute__((noinline))?使用-O0,编译器可能仍会将函数 args 溢出到红色区域。
    猜你喜欢
    • 1970-01-01
    • 2018-07-09
    • 1970-01-01
    • 2012-08-12
    • 2015-05-30
    • 2012-10-20
    • 2017-01-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多