带堆栈操作的 GCC 内联汇编

Asy*_*ode 2 x86 assembly gcc inline-assembly

我需要这样的内联汇编代码:

  • 我在组件内部有一对(所以,它是平衡的)推/弹出操作
  • 我在内存中也有一个变量(所以,不是注册)作为输入

像这样:

__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
Run Code Online (Sandbox Code Playgroud)

反汇编编译后的代码时,编译器给出的内存地址是0xc(%esp),它是相对于 的esp,因此,由于我push之前有一个操作,这段代码将无法正常工作mov。因此,我怎么能告诉编译器我不喜欢foo相对于esp,但像-8(%ebp)相对于 ebp 的任何东西。

PS 你可能会建议我可以放在eaxClobbers 里面,但这只是一个示例代码。我不想展示我不接受此解决方案的完整原因。

Pet*_*des 5

当您有任何内存输入/输出时,通常应该避免在 inline-asm 中修改 ESP,因此您不必禁用优化或强制编译器以其他方式使用 EBP 制作堆栈帧。一个主要优点是您(或编译器)可以将 EBP 用作额外的空闲寄存器;如果您已经不得不溢出/重新加载东西,则可能会显着加速。如果您正在编写内联汇编,大概这是一个热点,因此值得花费额外的代码大小来使用 ESP 相对寻址模式。

在 x86-64 代码中,安全使用 push/pop 有一个额外的障碍,因为你不能告诉编译器你想要破坏RSP 下的红色区域。(您可以使用 进行编译-mno-red-zone,但无法从 C 源代码中禁用它。)当您破坏堆栈上的编译器数据时,您可能会遇到这样的问题。但是,没有 32 位 x86 ABI 具有红色区域,因此这仅适用于 x86-64 System V。(或具有红色区域的非 x86 ISA。)

-fno-omit-frame-pointer如果您只想将 asmpush用作堆栈数据结构之类的东西,则只需要该函数,因此推送量是可变的。或者如果优化代码大小。

你总是可以在 asm 中编写一个完整的非内联函数并将其放在一个单独的文件中,然后你就有了完全的控制权。但只有在您的函数足够大以值得调用/ret 开销时才这样做,例如,如果它包含整个循环;不要让编译器call成为 C 内部循环内的一个简短的非循环函数,破坏所有调用破坏的寄存器,并且必须确保全局变量同步。


似乎您在内联汇编中使用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
}
Run Code Online (Sandbox Code Playgroud)

gcc7.3 -O3 -m32 在 Godbolt 编译器资源管理器上编译为以下 asm 。请注意显示编译器为所有模板操作数选择的内容的 asm 注释:它选择12(%esp)用于%[spill1]%edi用于%[spill2](因为我用于"=&rm"该操作数,所以编译器%edi在 asm 之外保存/恢复,并将其提供给我们用于该虚拟操作数)。

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
Run Code Online (Sandbox Code Playgroud)

嗯,告诉编译器我们修改哪个内存的虚拟内存操作数似乎导致了专用寄存器,我猜是因为p操作数是早期破坏所以它不能使用相同的寄存器。我想如果您确信其他输入都不会使用与p. (即它们没有相同的值)。