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

17

我希望能够在内联汇编中使用基址指针寄存器 (%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,这应该没问题。然而,在内联汇编后尝试访问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。


不要添加无关语言的标签。 - too honest for this site
1
对于汇编代码:使用汇编器参数来指定 C 端变量;不要依赖于汇编代码中的特定寄存器布局。并始终指定占用寄存器。 - too honest for this site
1
movq %rdi, -8(%rbp)RDI 存入红区。然后执行 pushq %rbp;,这会将 RSP 减少 8 并将值存入 RBP 中。不幸的是,由于 RSP=RBP,您刚刚覆盖了 GCC 存储在那里的值(应该是 _RDI_)。当内联汇编器完成后,尝试执行 movq -8(%rbp), %rax。现在我们已经知道您破坏了内存位置 -8(%rbp) 处的数据,因此它现在包含一个伪造的值,然后我们尝试使用 movl $5, (%rax) 解引用它。由于 RAX 不再具有有效指针,因此此指令可能导致段错误。 - Michael Petch
如果你想在内联汇编中使用C / C ++变量,你真的需要开始使用输入(和输出,如果需要的话)约束来允许数据被传入(和/或传出)。 - Michael Petch
2个回答

27

请查看本答案底部的链接收藏,其中包含其他内联汇编的问答。

你的代码有问题,因为你使用push指令覆盖了RSP下面的红区,而GCC正是在这里保存了一个值。


你希望通过内联汇编学到什么?如果你想学习内联汇编,那就学会如何使用它来编写高效的代码,而不是像这样可怕的东西。如果你想编写函数序言并推入/弹出以保存/恢复寄存器,你应该整个函数都用汇编语言编写。(然后你可以轻松地使用nasm或yasm,而不是GNU汇编器指令中较不受大多数人欢迎的AT&T语法。)
GNU内联汇编难以使用,但允许您在C和C++中混合使用自定义汇编片段,同时让编译器处理寄存器分配和任何必要的保存/恢复操作。有时编译器将能够避免保存和恢复操作,给您一个允许被破坏的寄存器。没有volatile,它甚至可以将汇编语句提升出循环,当输入相同时。(即除非您使用volatile,否则假定输出是输入的“纯”函数。)
如果你只是想学习汇编语言,GNU内联汇编是一个糟糕的选择。你需要完全理解几乎所有与汇编相关的内容,并且要了解编译器需要知道什么,才能编写正确的输入/输出约束并使一切正确。错误将导致覆盖东西和难以调试的故障。函数调用ABI是代码和编译器代码之间边界的一个更简单、更易于跟踪的方式。
为什么这会出错
你使用了 -O0编译选项,因此gcc会将函数参数从%rdi溢出到堆栈位置。即使使用-O3,这在复杂的函数中仍然可能发生。
由于目标ABI是x86-64 SysV ABI,因此它使用"Red Zone"(即在%rsp下方128个字节,即使异步信号处理程序也不允许破坏),而不是浪费指令来减少堆栈指针以保留空间。
它将8B指针函数参数存储在-8(rsp_at_function_entry)处。然后,您的内联asm推送%rbp,它通过减少%rsp 8并将其写入那里来破坏&x(指针)的低32位。
当您的内联asm完成时,
  • gcc重新加载-8(%rbp)(已被覆盖为%rbp),并将其用作4B存储的地址。
  • Foo返回到main,其中%rbp = (upper32)|5(原始值的低32位设置为5)。
  • main运行leave%rsp = (upper32)|5
  • main使用%rsp = (upper32)|5运行ret,从虚拟地址(void*)(upper32|5)读取返回地址,根据您的评论,该地址为0x7fff0000000d

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

即使添加“memory” clobber也无法让gcc避免使用red zone,因此似乎从inline asm中分配自己的堆栈内存只是一个坏主意。(memory clobber指可能已经写入了一些允许写入的内存,例如全局变量或由全局指向的东西,而不是可能已经覆盖了不应该覆盖的内容。)
如果您想从inline asm中使用scratch空间,您应该将数组声明为本地变量,并将其用作仅输出操作数(您永远不会从中读取)。
据我所知,没有语法可以声明您修改red-zone,因此您的唯一选择是:
  • 使用一个"=m"输出操作数(可能是数组)作为临时空间;编译器可能会用相对于RBP或RSP的寻址模式填充该操作数。您可以使用类似4 + %[tmp]的常量进行索引。您可能会收到来自4 + (%rsp)的汇编警告,但不会出错。
  • 通过add $-128, %rsp / sub $-128, %rsp跳过红区域以绕过代码。(如果想使用未知数量的额外堆栈空间,例如在循环中推入或进行函数调用,则必须这样做。这也是在纯C中解引用函数指针而不是内联汇编的另一个原因。)
  • 使用-mno-red-zone编译(我认为您无法在每个函数基础上启用它,只能在每个文件上启用)
  • 首先不要使用临时空间。告诉编译器您破坏了哪些寄存器,让它保存它们。


这是你应该做的

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在#APP / #NO_APP部分之外的代码中push/pop %rbp。此外,请注意它给你的临时存储器位于红区。如果您使用-O0编译,则会看到它与溢出&x的位置不同。
要获取更多的临时寄存器,最好只声明更多的输出操作数,在周围非汇编代码中从未被使用。这样做可以将寄存器分配留给编译器,因此在不同地方内联时可能会有所不同。提前选择并声明一个clobber仅在需要使用特定寄存器(例如%cl中的移位计数)时才有意义。当然,像"c"(count)这样的输入约束会让gcc将计数放入rcx/ecx/cx/cl中,因此您不需要发出潜在冗余的mov %[count],%%ecx
如果这看起来太复杂,不要使用内联汇编。通过使用与最佳汇编相似的 C 代码引导编译器到你想要的汇编,或者编写整个函数的汇编代码来实现目标。
在使用内联汇编时,尽可能保持它的简短:理想情况下仅包含一个或两个指令,告诉编译器如何将数据输入/输出到汇编语句中。这就是内联汇编的设计初衷。
经验法则:如果您的 GNU C 内联汇编以 mov 开头或结尾,则通常做错了,应该使用约束条件代替。

脚注:

  1. 您可以使用GAS的intel语法在内联汇编中构建,方法是使用-masm=intel(这种情况下,您的代码将仅适用于该选项),或者使用方言替代以便与Intel或AT&T汇编输出语法的编译器一起工作。但这不会改变指令,并且GAS的Intel语法文档不太完善。(它类似于MASM而不是NASM)。除非您真的很讨厌AT&T语法,否则我不建议使用它。

内联汇编链接:


彼得,你的回答太棒了!但是在你的链接中,你重复了同一个链接。 - Z boson
1
@jaw:用C语言编写原型,将函数编写在单独的.S(GNU语法)或.asm(NASM/YASM语法)文件中。使用命令gcc -Wall -O3 main.c myfunc.S -o myprog进行编译。参见https://dev59.com/a2zXa4cB1Zd3GeqPQSTr。如果您使用NASM/YASM,请运行`yasm -felf64 myfunc.asm生成一个.o`文件,然后可以与C语言链接。确保您的函数遵循ABI(要保留哪些寄存器以及如何查找其参数),否则当gcc调用它时会出现错误。如果我没记错的话,Agner Fog的优化汇编指南花了一些时间来介绍如何做到这一点。(链接在x86标签wiki中) - Peter Cordes
@Zboson:我想我记得另外两个链接,而不是重复的链接:早期破坏讨论。还有操作数大小修饰符问题。 - Peter Cordes
很好的链接。关于Stephen Canon的答案,最初没有使用clobber修饰符。我按照他的答案进行了256位加法操作,但是一直得不到正确的答案,直到我想到使用clobber修饰符为止。对于没有使用clobber修饰符的128位加法,我从未遇到过问题。问题发生在第三次加法之后。但这可能只是巧合。这就是为什么我在他的答案中留下了评论的原因。 - Z boson
显示剩余5条评论

3
在x86-64中,堆栈指针需要对齐到8个字节。
这是因为:
subq $12, %rsp;      // make room

should be:

subq $16, %rsp;      // make room

实际上,即使没有“// some asm instruction”语句,它仍然失败。为什么推送和弹出会搞乱$rbp? - jaw
1
gdb 的输出如下(它在汇编几行后发生了段错误):第 11 行:Foo (x=@0x7fffffffe034: 32767): " : : : );",第 12 行:Foo (x=@0x7fffffffe020: -8128): "x = 5",第 13 行:"}":无法访问地址 0x7fff0000000d 的内存。程序收到信号 SIGSEGV,分段错误。 - jaw
请将以下与编程相关的内容从英语翻译成中文。仅返回翻译后的文本: - jaw
你可能想要反汇编它以查看实际的指令是什么,但在我看来,你已经完全搞乱了堆栈指针。 - Mats Petersson
1
在崩溃后,请使用“disass foo”和“info reg”编辑问题。 - Mats Petersson
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接