2011-06-17 59 views
19

我正在编写一个加密程序,并且核心(一个宽泛的例程)是用x86-64程序集编写的,因为它的速度很快,而且它广泛地使用了不容易的指令,如adc可以从C中访问。我不想内联这个函数,因为它很大,并且在内部循环中被多次调用。内联程序集,用于破坏红色区域

理想情况下,我也想为这个函数定义一个自定义的调用约定,因为它在内部使用所有的寄存器(rsp除外),不会打断它的参数,并返回寄存器。现在,它适应了C调用惯例,但是这当然会使它变慢(大约10%)。

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

+0

只是一个未经测试的,但你不能只是指定一个额外的虚拟输入,这样GCC把它放在红色区域,它得到(无害)破坏? – 2011-06-17 05:55:30

+3

嗯。可能不可靠。我发现很难控制哪些GCC泄漏到堆栈,何时何地。它是我写的其他加密内容,我尝试了混合成功来压制GCC倾向于写入,例如,整个密钥表的原因很少。 – 2011-06-18 07:49:45

+0

添加'sp'作为clobber?添加内存clobber? – 2013-03-22 16:55:11

回答

0

不知道,但看着GCC documentation for function attributes,我发现stdcall函数属性可能是感兴趣的。

我仍然想知道你发现你的asm通话版本有问题。如果它只是美学,你可以把它转换成一个宏,或者一个内联函数。

+2

“调用”指令将当前指令指针压入堆栈。如果栈下没有任何东西(在“红色区域”中),那么这很好,但是在x86-64上,ABI允许编译器在叶函数中放置东西,即那些不调用任何东西的东西。然而,海湾合作委员会并没有将这个“呼叫”视为一个呼叫,因为它隐藏在内联汇编中。所以它可能会把一些东西放在红色区域,并且会被通话打乱。 这不仅仅是一种理论上的可能性,它实际上发生了并且实际上导致了我的代码中的一个错误。 此外,stdcall不会这样做。 – 2011-06-18 07:38:49

+0

具体来说,stdcall的问题在于它仅适用于实际的非内联函数。但要为我的函数定义自定义调用约定,我试图通过内联asm调用它。所以GCC并没有意识到它是一个函数调用(首先是问题),因此我不能将属性附加到它上面。 – 2011-06-18 07:53:10

2

难道你不能只修改你的汇编函数,以满足在x86-64 ABI信号的要求通过移动堆栈指针128字节进入你的函数?

或者,如果你指的是返回指针本身,把转移到您的通话宏(所以sub %rsp; call...

+3

我不能从函数本身做到这一点,因为'call'使用堆栈,并因此自行破坏。 'sub $ 128,%rsp;呼叫...;增加128美元,%rsp'的作品,但它并不理想。 我想平衡它可能是最好的只是让我的功能符合ABI。 – 2011-06-18 07:44:06

4

从你原来的问题我不知道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

+3

有'__attribute __(leaf)',但不幸的是没有什么像'__attribute __(nonleaf)' – 2011-06-18 20:57:09

+0

我不觉得它震惊gcc不会放弃在红区时,它必须保留一些堆栈空间:红区的好处是能够通过disp8位移达到更多的记忆,所以在当地居民中间有'rsp'意味着它可以通过'[rsp-128 .. + 127]'寻址模式到达所有人。这是一个很好的优化。 (或者如果你使用'-O3' +'volatile char buf [150]'来获得RSP相对寻址模式而不是'-155(%rbp)') – 2018-02-20 00:31:30

0

如何创建一个用C编写的虚拟函数,除了调用内联程序集外什么也不做?

+0

并且将该函数标记为' __attribute __((noinline始终))'?使用'-O0',编译器可能仍然会将函数参数泄漏到红区。 – 2017-12-21 15:23:09

0

最大性能的方式可能是将整个内部循环写入asm(包括call指令,如果真的值得它展开而不是内联的话,那么肯定是合理的,如果完全内联会导致其他地方的uop-cache失误太多)。

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

顺便说一下,破坏全部这些寄存器使得gcc很难做出非常好的循环,所以你可能会自己优化整个循环。 (例如,可以将指针保持在寄存器中,并且在存储器中保存结束指针,因为cmp mem,reg仍然相当有效)。

看一看周围asm语句修改一个数组元素(上Godbolt)代码的gcc /铛包装:

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. 

铛计算一个独立的计数器至零。但它使用load/add -1/store而不是内存目标add [mem], -1/jnz

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

考虑使用一些XMM寄存器进行整数运算,以尽可能减少整数寄存器上的寄存器压力。在英特尔CPU上,在GP和XMM寄存器之间移动仅需1个延迟1c的ALU uop。 (在AMD上它仍然是1Uop,但更高的延迟尤其是在推土机系列上)。在XMM寄存器中执行标量整数并不会太糟糕,如果总吞吐量是瓶颈,或者比成本节省更多的溢出/重新加载,这可能是值得的。

当然XMM的但不是循环计数器(paddd/pcmpeq/pmovmskb/cmp/jccpsubd/ptest/jcc不是很大相比sub [mem], 1/JCC),或为指针,或用于扩展精度非常可行算术运算(手动执行比较操作,并且与另一个paddq一起进入,即使在64位整数寄存器不可用的32位模式下也是如此)。如果你不是加载/存储糟糕的瓶颈,通常溢出/重新加载到内存而不是XMM寄存器更好。


如果您还需要从环(清理或东西)外的函数调用,编写一个包装或者使用add $-128, %rsp ; call ; sub $-128, %rsp保留红区在这些版本中。 (注意:-128是encodeable作为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-redzone是不是你可以切换对每个功能的基础上,或用编译单元内#pragma GCC target ("options")的那些之一。

您可以使用这样的东西

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

但不__attribute__((target("-mno-redzone")))

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