簡體   English   中英

破壞紅色區域的內聯程序集

[英]Inline assembly that clobbers the red zone

我正在編寫一個加密程序,核心(一個寬乘法例程)是用 x86-64 匯編編寫的,既是為了速度,也是因為它廣泛使用了像adc這樣不容易從 C 訪問的指令。我不想內聯這個函數,因為它很大並且在內循環中被調用了幾次。

理想情況下,我還想為這個函數定義一個自定義調用約定,因為它在內部使用所有寄存器(除了rsp ),不會破壞它的參數,並在寄存器中返回。 現在,它適應了 C 調用約定,但這當然使它變慢了(大約 10%)。

為了避免這種情況,我可以用asm("call %Pn" : ... : my_function... : "cc", all the registers);調用它asm("call %Pn" : ... : my_function... : "cc", all the registers); 但是有沒有辦法告訴 GCC 調用指令與堆棧混淆了? 否則 GCC 只會將所有這些寄存器放在紅色區域中,而頂部的寄存器將被破壞。 我可以用 -mno-red-zone 編譯整個模塊,但我更喜歡告訴 GCC,比如說,紅色區域的前 8 個字節將被破壞,這樣它就不會在那里放任何東西。

從您最初的問題中,我沒有意識到 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葉子的代碼放回subq $160, %rsp變成subq $160, %rsp

最大性能的方法可能是在 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 將一個單獨的計數器向下計數到零。 但它使用 load / add -1 / store 而不是 memory-destination add [mem], -1 / jnz

如果您自己在 asm 中編寫整個循環而不是將熱循環的那部分留給編譯器,那么您可能會做得比這更好。

如果可能,請考慮使用一些 XMM 寄存器進行整數運算,以減少整數寄存器上的寄存器壓力。 在 Intel CPU 上,在 GP 和 XMM 寄存器之間移動僅花費 1 ALU uop 和 1c 延遲。 (它在 AMD 上仍然是 1 uop,但延遲更高,尤其是在推土機系列上)。 在 XMM 寄存器中執行標量整數內容並不會更糟,如果總 uop 吞吐量是您的瓶頸,或者它節省的溢出/重新加載比成本多的話,這可能是值得的。

但當然 XMM 對於循環計數器( paddd / pcmpeq / pmovmskb / cmp / jccpsubd / ptest / jccsub [mem], 1 / jcc 相比並不是很好),或者對於指針,或者對於擴展-精度算術(即使在 64 位整數 regs 不可用的 32 位模式下,手動進行比較和進位與另一個paddq的進位也paddq糟糕)。 如果您沒有在加載/存儲 uops 上遇到瓶頸,通常最好將溢出/重新加載到內存而不是 XMM 寄存器。


如果您還需要從循環外部調用該函數(清理或其他東西),請編寫一個包裝器或使用add $-128, %rsp ; call ; sub $-128, %rsp add $-128, %rsp ; call ; sub $-128, %rsp 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 函數屬性,但它不能用於任意選項,並且-mno-red- zone不是您可以在每個函數基礎上切換的選項之一,或在編譯單元中使用#pragma GCC target ("options")

你可以使用類似的東西

__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放在一個文件中,並使用-mno-red-zone編譯該編譯單元。 (希望 LTO 不會破壞任何東西......)

您不能通過在函數入口處將堆棧指針移動 128 個字節來修改您的匯編函數以滿足 x86-64 ABI 中信號的要求嗎?

或者,如果您指的是返回指針本身,請將移位放入您的 call 宏中(因此sub %rsp; call...

創建一個用 C 編寫並且只調用內聯程序集的虛擬函數怎么樣?

不確定,但查看函數屬性的 GCC 文檔,我發現了可能感興趣的stdcall函數屬性。

我仍然想知道你覺得你的 asm 調用版本有什么問題。 如果只是美觀,您可以將其轉換為宏或內聯函數。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM