【问题标题】:GCC inline assembly with stack operation具有堆栈操作的 GCC 内联汇编
【发布时间】:2018-07-28 22:56:09
【问题描述】:

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

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

像这样:

__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 的东西。

附:您可能会建议我可以将eax 放在Clobbers 中,但这只是一个示例代码。我不想说明我不接受这个解决方案的全部原因。

【问题讨论】:

    标签: gcc assembly x86 inline-assembly


    【解决方案1】:

    直接使用栈指针来引用局部变量可能是因为使用了编译器优化。我认为您可以通过以下几种方式解决此问题:

    • 禁用帧指针优化(GCC 中的-fno-omit-frame-pointer);
    • 在 Clobbers 中插入 esp,以便编译器知道它的值正在被修改(检查编译器的兼容性)。

    【讨论】:

    • 刚刚尝试了第二种解决方案,在我的一台计算机上运行良好,但在另一台计算机上失败,可能是旧版本 gcc 的原因。
    【解决方案2】:

    不要将move放到汇编代码中的ecx中,而是将操作数直接放入ecx中:

        : : "c"(foo)
    

    【讨论】:

    • 我已经编辑了我的代码,在移动指令之前我有一些需要 ecx 的操作怎么样。而上面的代码只是一个粗略的示例,我需要的是 push/pop 操作 + memroy 变量作为输入
    • @prl 你确定那个clobber规则吗?它对我来说很好(godbolt.org/g/iRnDj6)。它可能取决于编译器版本。
    • @Matteo,我确定,但显然我错了。我会从答案中删除它。
    • @MatteoPiano :除非您检查适当的编译器和版本,否则您不能可靠地将esp 用作破坏者。在许多编译器的旧版本中,它会接受 esp 作为破坏者,但不会做任何事情,也不会警告你。
    【解决方案3】:

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

    在 x86-64 代码中,安全使用 push/pop 存在额外的障碍,因为 you can't tell the compiler you want to clobber the red-zone 低于 RSP。 (您可以使用-mno-red-zone 进行编译,但无法从C 源代码中禁用它。)您可能会遇到问题like this,因为您在堆栈上破坏了编译器的数据。但是,没有 32 位 x86 ABI 具有红色区域,因此这只适用于 x86-64 System V。(或具有红色区域的非 x86 ISA。)

    如果您想将 push 之类的纯 asm 操作作为堆栈数据结构,则您只需要 -fno-omit-frame-pointer 用于该功能,因此推送量是可变的。或者,如果针对代码大小进行优化。

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


    您似乎在 inline asm 中使用 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
    }
    

    这将编译为带有 gcc7.3 -O3 -m32 on the Godbolt compiler explorer 的以下 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
    

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

    【讨论】:

    • pushfdpopfd 指令呢?是否可以在不使用堆栈的情况下推送和弹出 EFLAGS?
    • @Cubi73: pushfpush %ebx%rbx 没有区别。 popf 很慢,因此您通常希望避免它:agner.org/optimize。 sahf / lahf 可能很有用,但会跳过 OF。如果您确实想要推送/弹出,并且您的代码可能在红色区域中运行(即在 x86-64 System V 下),您需要在 asm 的开头 add $-128, %rsp 并在结尾处恢复。或者使用-mno-red-zone 编译该文件。
    • 不幸的是,sahflahf 没有选择,因为我需要位 21 (ID)。我想我会坚持使用-mno-red-zone,谢谢。
    • @Cubi73:哦,如果你想检测CPUID支持,#include <cpuid.h>GCC's cpuid.h 做到了这一切。此外,x86-64 保证支持 CPUID,因此您无需在那里检查 ID。并且没有 32 位约定有红色区域。所以这最终对 cpuid.h 来说不是问题。 How do I call "cpuid" in Linux?。当然你不需要popf,只需要pop %[output]
    猜你喜欢
    • 2014-12-19
    • 1970-01-01
    • 2012-10-20
    • 2012-05-14
    • 2011-09-07
    • 1970-01-01
    • 2011-09-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多