在C ++内联asm中使用基指针寄存器

时间:2015-12-29 22:20:07

标签: c++ assembly x86 red-zone

我希望能够在内联asm中使用基址指针寄存器(%rbp)。这样的玩具示例如下:

void Foo(int &x)
{
    asm volatile ("pushq %%rbp;"         // 'prologue'
                  "movq %%rsp, %%rbp;"   // 'prologue'
                  "subq $12, %%rsp;"     // make room

                  "movl $5, -12(%%rbp);" // some asm instruction

                  "movq %%rbp, %%rsp;"  // 'epilogue'
                  "popq %%rbp;"         // 'epilogue'
                  : : : );
    x = 5;
}

int main() 
{
    int x;
    Foo(x);
    return 0;
}

我希望,因为我使用通常的序言/结尾函数调用方法来推送和弹出旧%rbp,这样就可以了。但是,当我尝试在内联asm之后访问x时,它会出现错误。

GCC生成的汇编代码(略微剥离)是:

_Foo:
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)

    # INLINEASM
    pushq %rbp;          // prologue
    movq %rsp, %rbp;     // prologue
    subq $12, %rsp;      // make room
    movl $5, -12(%rbp);  // some asm instruction
    movq %rbp, %rsp;     // epilogue
    popq %rbp;           // epilogue
    # /INLINEASM

    movq    -8(%rbp), %rax
    movl    $5, (%rax)      // x=5;
    popq    %rbp
    ret

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    leaq    -4(%rbp), %rax
    movq    %rax, %rdi
    call    _Foo
    movl    $0, %eax
    leave
    ret

谁能告诉我为什么这个段错了?似乎我以某种方式腐败%rbp,但我不知道如何。提前谢谢。

我在64位Ubuntu 14.04上运行GCC 4.8.4。

2 个答案:

答案 0 :(得分:19)

请参阅本答案的底部,以获取指向其他inline-asm Q& As的链接集合。

你希望通过inline asm学到什么?如果你想学习内联asm,学会用它来制作高效的代码,而不是像这样的可怕的东西。如果你想编写函数序言和push / pop来保存/恢复寄存器,你应该在asm中编写整个函数。 (然后你可以很容易地使用nasm或yasm,而不是使用GNU汇编程序指令 1 的不太优选的AT& T语法。)

GNU内联asm很难使用,但允许您将自定义asm片段混合到C和C ++中,同时让编译器处理寄存器分配以及必要时的任何保存/恢复。有时,编译器可以通过为您提供允许被破坏的寄存器来避免保存和恢复。如果没有volatile,它甚至可以在输入相同时将asm语句从循环中提升出来。 (即除非你使用volatile,否则输出被假定为输入的纯粹"函数。)

如果您只是试图首先学习asm,那么GNU inline asm是一个糟糕的选择。你必须完全理解几乎所有与之相关的内容。 asm,并了解编译器需要知道什么,编写正确的输入/输出约束并使一切正确。错误将导致破坏事物和难以调试的破损。函数调用ABI更简单,更容易跟踪代码和编译器代码之间的边界。

compiled with -O0,因此gcc的代码会将函数参数从%rdi溢出到堆栈上的某个位置。 (即使使用-O3),这也可能发生在非平凡的函数中。由于目标ABI是x86-64 SysV ABI,它使用"红区" (低于%rsp的128B甚至不允许异步信号处理程序被破坏),而不是浪费一个指令递减堆栈指针以保留空间。

它将8B指针函数arg存储在-8(rsp_at_function_entry)。然后你的内联asm推送%rbp,它将%rsp递减8然后在那里写入,破坏&x的低32b(指针)。

当你的内联asm完成后,

  • gcc重新加载-8(%rbp)(已被%rbp覆盖)并将其用作4B商店的地址。
  • Foo返回main %rbp = (upper32)|5(原值为低32设为5)。
  • main运行leave%rsp = (upper32)|5
  • mainret一起运行%rsp = (upper32)|5,读取虚拟地址(void*)(upper32|5)的返回地址,评论为0x7fff0000000d

我没有用调试器检查;其中一个步骤可能略有偏差,但问题肯定是你破坏了红区,导致gcc的代码摧毁了堆栈。

即使添加"内存" clobber没有获得gcc以避免使用红色区域,所以它看起来从内联asm分配你自己的堆栈内存只是一个坏主意。 (记忆破坏意味着你可能已经写了一些你可以写入的记忆,而不是你可能已经覆盖了你不应该写的东西。)

如果要使用内联asm中的临时空间,则应该将数组声明为局部变量,并将其用作仅输出操作数(您从未读取过)。

Here's what you should have done

void Bar(int &x)
{
    int tmp;
    long tmplong;
    asm ("lea  -16 + %[mem1], %%rbp\n\t"
         "imul $10, %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
         "add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
         "movl $5, %[mem1]\n\t" // some asm instruction writing to mem
           : [mem1] "=m" (tmp), [reg1] "=r" (tmplong)  // tmp vars -> tmp regs / mem for use inside asm
           :
           : "%rbp" // tell compiler it needs to save/restore %rbp.
  // gcc refuses to let you clobber %rbp with -fno-omit-frame-pointer (the default at -O0)
  // clang lets you, but memory operands still use an offset from %rbp, which will crash!
  // gcc memory operands still reference %rsp, so don't modify it.  Declaring a clobber on %rsp does nothing
         );
    x = 5;
}

请注意gcc发出的%rbp / #APP部分之外的代码中#NO_APP的推送/弹出式广告。另请注意,它为您提供的临时存储器位于红色区域中。如果您使用-O0进行编译,则会发现它与溢出&x的位置不同。

为了获得更多的临时注册表,最好只声明更多输出操作数,这些操作数永远不会被周围的非asm代码使用。这会将寄存器分配给编译器,因此在内联到不同位置时可能会有所不同。如果您需要使用特定的寄存器(例如%cl中的移位计数),则提前选择并声明一个clobber是有意义的。当然,像"c" (count)这样的输入约束会使gcc将计数放在rcx / ecx / cx / cl中,因此您不会发出可能多余的mov %[count], %%ecx

如果这看起来太复杂,请勿使用inline asm 。使用C lead the compiler to the asm you want和最佳asm一样,或者在asm中写一个完整的函数。

使用内联asm时,请尽可能小:理想情况下,只有gcc不会单独发出的一两条指令,输入/输出约束告诉它如何将数据输入/输出asm声明。这就是它的设计目标。

经验法则:如果你的GNU C inline asm以mov开头或结尾,你通常会做错,应该使用约束。

<强>脚注

  1. 您可以在inline-asm中使用GAS的intel-syntax,方法是-masm=intel(在这种情况下,您的代码使用该选项),或使用{ {3}}因此它适用于Intel中的编译器或AT&amp; T asm输出语法。但这并没有改变指令,而GAS的英特尔语法也没有很好的记录。 (它就像MASM,而不是NASM。)除非你真的讨厌AT&amp; T语法,否则我不推荐它。
  2. 内联asm链接:

    其中一些重复了我在这里解释的一些相同的东西。我没有重新阅读它们以试图避免冗余,抱歉。

答案 1 :(得分:3)

在x86-64中,堆栈指针需要与8个字节对齐。

此:

subq $12, %rsp;      // make room

应该是:

subq $16, %rsp;      // make room