在 C++ 内联汇编中使用基址指针寄存器

Using base pointer register in C++ inline asm

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

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

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

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

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

我希望,因为我使用的是常用的 prologue/epilogue 推入和弹出旧 %rbp 函数调用方法,所以这会没问题。但是,当我在内联 asm.

之后尝试访问 x 时,它会出现段错误

GCC 生成的汇编代码(略微精简)是:

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

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

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

main:
    pushq   %rbp
    movq    %rsp, %rbp
    subq    , %rsp
    leaq    -4(%rbp), %rax
    movq    %rax, %rdi
    call    _Foo
    movl    [=11=], %eax
    leave
    ret

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

我是 运行 GCC 4.8.4 on 64-bit Ubuntu 14.04.

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

这个:

subq , %rsp;      // make room

应该是:

subq , %rsp;      // make room

请参阅此答案的底部以获取指向其他 inline-asm 问答的链接集合。

您的代码已损坏,因为您踩到了 GCC 保留值的 RSP(带有 push)下方的 red-zone。


您希望通过内联汇编来学习什么?如果你想学习内联汇编,学习使用它来编写高效的代码,而不是像这样可怕的东西。如果你想写函数序言和 push/pop 到 save/restore 寄存器,你应该在 asm 中写整个函数。 (然后您可以轻松地使用 nasm 或 yasm,而不是 less-preferred-by-most AT&T 语法和 GNU 汇编器指令1。)

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

如果您一开始只是想学习 asm,GNU 内联 asm 是一个糟糕的选择。您必须完全理解 asm 的几乎所有内容,并了解编译器需要知道什么,才能编写正确的 input/output 约束并使一切正确。错误会导致破坏和 hard-to-debug 破损。 function-call ABI 更简单,更容易跟踪您的代码和编译器代码之间的边界。


为什么会中断

compiled with -O0,所以 gcc 的代码将函数参数从 %rdi 溢出到堆栈上的某个位置。 (即使使用 -O3,这也可能发生在 non-trivial 函数中)。

因为目标 ABI 是 x86-64 SysV ABI, it uses the "Red Zone"(低于 %rsp 的 128 个字节,即使异步信号处理程序也不允许破坏),而不是浪费一条指令来递减堆栈指针以保留 space.

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

当你的内联 asm 完成后,

  • gcc 重新加载 -8(%rbp)(已被 %rbp 覆盖)并将其用作 4B 存储的地址。
  • Foo returns 到 main%rbp = (upper32)|5(低 32 的原始值设置为 5)。
  • main 运行 leave: %rsp = (upper32)|5
  • main 使用 %rsp = (upper32)|5 运行 ret,从虚拟地址 (void*)(upper32|5) 读取 return 地址,根据您的评论是 0x7fff0000000d .

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

即使添加“内存”破坏程序也无法让 gcc 避免使用红色区域,因此看起来从内联 asm 分配您自己的堆栈内存只是一个坏主意。 (内存破坏意味着您可能已经写入了一些允许写入的内存,例如全局变量或全局变量 pointed-to ,而不是您可能已经覆盖了您不应该写入的内容。)

如果你想从内联 asm 中使用 scratch space,你可能应该将数组声明为局部变量并将其用作 output-only 操作数(你从未从中读取过)。

AFAIK,没有声明您修改 red-zone 的语法,所以您唯一的选择是:

  • 使用 "=m" 输出操作数(可能是一个数组)作为 scratch space;编译器可能会用相对于 RBP 或 RSP 的寻址模式填充该操作数。您可以使用 4 + %[tmp] 之类的常量对其进行索引。您可能会收到来自 4 + (%rsp) 的汇编程序警告,但不是错误。
  • 跳过代码周围带有 add $-128, %rsp / sub $-128, %rsp 的 red-zone。 (如果你想使用未知数量的额外堆栈 space,例如推入循环或进行函数调用,则这是必需的。在纯 C 中取消引用函数指针的另一个原因,而不是内联 asm。)
  • -mno-red-zone 编译(我不认为你可以在 per-function 的基础上启用它,只能 per-file)
  • 首先不要使用 scratch space。告诉编译器你破坏了哪些寄存器并让它保存它们。

Here's what you should have done:

void Bar(int &x)
{
    int tmp;
    long tmplong;
    asm ("lea  -16 + %[mem1], %%rbp\n\t"
         "imul , %%rbp, %q[reg1]\n\t"  // q modifier: 64bit name.
         "add  %k[reg1], %k[reg1]\n\t"    // k modifier: 32bit name
         "movl , %[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;
}

请注意 #APP / #NO_APP 部分之外的代码中 %rbp 的 push/pop,由 gcc 发出。另请注意,它为您提供的暂存存储器位于红色区域。如果使用 -O0 进行编译,您会发现它与溢出 &x.

的位置不同

要获得更多暂存寄存器,最好声明更多从未被周围 non-asm 代码使用的输出操作数。这将寄存器分配留给了编译器,因此当内联到不同的地方时它可能会有所不同。仅当您需要使用特定寄存器(例如 %cl 中的移位计数)时,提前选择并声明一个 clobber 才有意义。当然,像 "c" (count) 这样的输入约束让 gcc 将计数放入 rcx/ecx/cx/cl,因此您不会发出潜在冗余的 mov %[count], %%ecx.

如果这看起来太复杂,不要使用内联 asm。要么 使用 C 就像最佳 asm,要么用 asm 编写整个函数。

当使用内联 asm 时,让它尽可能的小:理想情况下只是 gcc 不会自己发出的一两个指令,用 input/output 约束来告诉它如何将数据放入 /在 asm 语句之外。这就是它的设计目的。

经验法则:如果你的 GNU C 内联汇编以 mov 开始或结束,你通常做错了,应该使用约束来代替。


脚注:

  1. 您可以通过 -masm=intel 在 inline-asm 中使用 GAS 的 intel-syntax(在这种情况下,您的代码将 使用该选项), 或使用 dialect alternatives 以便它与 Intel 或 AT&T asm 输出语法中的编译器一起工作。但这并没有改变指令,并且 GAS 的 Intel-syntax 没有很好的记录。 (不过它就像 MASM,而不是 NASM。)我不推荐它,除非你真的讨厌 AT&T 语法。

内嵌 asm 链接:

  • wiki. (The tag wiki 也链接 这个问题,对于这个链接集合)

  • tag wiki

  • The manual。读这个。请注意,内联 asm 旨在包装编译器通常不会发出的单个指令。这就是为什么用“指令”而不是“代码块”来表达的原因。

  • A tutorial

  • 对 pointers/indices 使用 r 约束并使用您选择的寻址模式,与使用 m 约束让 gcc 在两者之间进行选择递增指针与索引数组。

  • (寄存器中的指针输入意味着pointed-to内存被读取and/or写入,所以如果你不告诉编译器它可能不同步)。

  • 。使用 %q0 得到 %rax%w0 得到 %ax。使用 %g[scalar] 得到 %zmm0 而不是 %xmm0.

  • Efficient 128-bit addition using carry flag Stephen Canon's answer explains a case where an early-clobber declaration is needed on a read+write operand. Also note that x86/x86-64 inline asm doesn't need to declare a "cc" clobber (the condition codes, aka flags); it's implicit. (gcc6 introduces syntax for using flag conditions as input/output operands。在此之前,您必须 setcc 一个 gcc 将向 test 发出代码的寄存器,这显然更糟。)

  • Questions about the performance of different implementations of strlen:我对一些 badly-used 内联汇编的问题的回答,答案与此类似。

  • :使用可偏移内存操作数(在x86中,所有有效地址都是可偏移的:你总是可以添加一个位移)。

  • ,以 32b/32b => 32b 除法和余数为例,编译器已经可以用单个 div 完成。 (问题中的代码是 not 如何使用内联 asm 的示例:许多设置说明和 save/restore 应该通过编写正确的 [=251] 留给编译器=]约束。)

  • MSVC inline asm vs. GNU C inline asm for wrapping a single instruction,以及 64b/32b=>32bit 除法 的内联汇编的正确示例。 MSVC 的设计和语法需要通过内存来回输入和输出,这对于短函数来说非常糟糕。根据 Ross Ridge 对该答案的评论,它也“从来都不是很可靠”。

  • 。这不是一个很好的例子,因为我没有找到让 gcc 发出理想代码的方法。

其中一些 re-iterate 与我在此处解释的内容相同。我没有 re-read 他们试图避免冗余,抱歉。