簡體   English   中英

帶堆棧操作的 GCC 內聯匯編

[英]GCC inline assembly with stack operation

我需要這樣的內聯匯編代碼:

  • 我在組件內部有一對(所以,它是平衡的)推/彈出操作
  • 我在內存中也有一個變量(所以,不是注冊)作為輸入

像這樣:

__asm__ __volatile__ ("push %%eax\n\t"
        // ... some operations that use ECX as a temporary
        "mov %0, %%ecx\n\t"
        // ... some other operation
        "pop %%eax"
: : "m"(foo));
// foo is my local variable, that is to say, on stack

反匯編編譯后的代碼時,編譯器會給出類似0xc(%esp)的內存地址,它是相對於esp ,因此,這段代碼將無法正常工作,因為我在mov之前進行了push操作。 因此,我怎么能告訴編譯器我不喜歡foo相對於esp ,但任何像-8(%ebp)相對於 ebp 的東西。

PS 您可能會建議我可以將eax放在 Clobbers 中,但這只是一個示例代碼 我不想展示我不接受此解決方案的完整原因。

當您有任何內存輸入/輸出時,通常應該避免在 inline-asm 中修改 ESP,因此您不必禁用優化或強制編譯器以其他方式使用 EBP 制作堆棧幀。 一個主要優點是您(或編譯器)可以將 EBP 用作額外的空閑寄存器 如果您已經不得不溢出/重新加載東西,則可能會顯着加速。 如果您正在編寫內聯匯編,大概這是一個熱點,因此值得花費額外的代碼大小來使用 ESP 相對尋址模式。

在 x86-64 代碼中,安全使用 push/pop 有一個額外的障礙,因為你不能告訴編譯器你想要破壞RSP 下的紅色區域 (您可以使用-mno-red-zone進行編譯,但無法從 C 源代碼中禁用它。)當您破壞堆棧上的編譯器數據時,您可能會遇到這樣的問題。 但是,沒有 32 位 x86 ABI 具有紅色區域,因此這僅適用於 x86-64 System V。(或具有紅色區域的非 x86 ISA。)

如果您想執行 asm-only 之類的操作,例如push作為堆棧數據結構,則該函數只需要-fno-omit-frame-pointer ,因此推送的數量是可變的。 或者如果優化代碼大小。

你總是可以在 asm 中編寫一個完整的非內聯函數並將其放在一個單獨的文件中,然后你就有了完全的控制權。 但只有在您的函數足夠大以值得調用/ret 開銷時才這樣做,例如,如果它包含整個循環; 不要讓編譯器在 C 內部循環內call一個簡短的非循環函數,破壞所有調用破壞的寄存器,並且必須確保全局變量是同步的。


似乎您在內聯匯編中使用push / pop因為您沒有足夠的寄存器,並且需要保存/重新加載某些內容。 您不需要使用 push/pop 進行保存/恢復。 相反,使用帶有"=m"約束的虛擬輸出操作數讓編譯器為您分配堆棧空間,並使用mov到/從這些插槽。 (當然,您不僅限於mov ;如果您只需要一次或兩次該值,那么將內存源操作數用於 ALU 指令可能是一種勝利。)

這對於代碼大小來說可能會稍差一些,但對於性能來說通常不會更差(並且可能會更好)。 如果這還不夠好,請在 asm 中編寫整個函數(或整個循環),這樣您就不必與編譯器搏斗。

int foo(char *p, int a, int b) {
    int t1,t2;  // dummy output spill slots
    int r1,r2;  // dummy output tmp registers
    int res;

    asm ("# operands: %0  %1  %2  %3  %4  %5  %6  %7  %8\n\t"
         "imull  $123, %[b], %[res]\n\t"
         "mov   %[res], %[spill1]\n\t"
         "mov   %[a], %%ecx\n\t"
         "mov   %[b], %[tmp1]\n\t"  // let the compiler allocate tmp regs, unless you need specific regs e.g. for a shift count
         "mov   %[spill1], %[res]\n\t"
    : [res] "=&r" (res),
      [tmp1] "=&r" (r1), [tmp2] "=&r" (r2),  // early-clobber
      [spill1] "=m" (t1), [spill2] "=&rm" (t2)  // allow spilling to a register if there are spare regs
      , [p] "+&r" (p)
      , "+m" (*(char (*)[]) p) // dummy in/output instead of memory clobber
    : [a] "rmi" (a), [b] "rm" (b)  // a can be an immediate, but b can't
    : "ecx"
    );

    return res;

    // p unused in the rest of the function
    // so it's really just an input to the asm,
    // which the asm is allowed to destroy
}

這將在 Godbolt 編譯器資源管理器上使用gcc7.3 -O3 -m32編譯為以下 asm。 注意顯示欽點為所有的模板操作數是什么編譯器ASM-評論:它挑12(%esp)用於%[spill1]%edi用於%[spill2]因為我用"=&rm"的那個操作,所以編譯器在 asm 之外保存/恢復%edi ,並將其提供給我們用於該虛擬操作數)。

foo(char*, int, int):
    pushl   %ebp
    pushl   %edi
    pushl   %esi
    pushl   %ebx
    subl    $16, %esp
    movl    36(%esp), %edx
    movl    %edx, %ebp
#APP
# 19 "/tmp/compiler-explorer-compiler118120-55-w92ge8.v797i/example.cpp" 1
        # operands: %eax  %ebx  %esi  12(%esp)  %edi  %ebp  (%edx)  40(%esp)  44(%esp)
    imull  $123, 44(%esp), %eax
    mov   %eax, 12(%esp)
    mov   40(%esp), %ecx
    mov   44(%esp), %ebx
    mov   12(%esp), %eax

# 0 "" 2
#NO_APP
    addl    $16, %esp
    popl    %ebx
    popl    %esi
    popl    %edi
    popl    %ebp
    ret

嗯,告訴編譯器我們修改哪個內存的虛擬內存操作數似乎導致了專用寄存器,我猜是因為p操作數是早期破壞,所以它不能使用相同的寄存器。 如果您確信其他輸入都不會使用與p相同的寄存器,我想您可能會冒着放棄早期破壞的風險。 (即它們沒有相同的值)。

直接使用棧指針來引用局部變量,很可能是使用了編譯器優化造成的。 我認為您可以通過以下幾種方式解決問題:

  • 禁用幀指針優化(GCC 中的-fno-omit-frame-pointer );
  • 在 Clobbers 中插入esp以便編譯器知道它的值正在被修改(檢查你的編譯器的兼容性)。

不是在匯編代碼中將 move 放入 ecx 中,而是直接將操作數放入 ecx 中:

    : : "c"(foo)

暫無
暫無

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

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