15

我希望能够%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;
}

我希望,因为我使用的是通常的序言/尾声函数调用方法来推送和弹出 old %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。

4

2 回答 2

26

有关其他 inline-asm 问答的链接集合,请参阅此答案的底部。

您的代码已损坏,因为您踩到了pushGCC 保持值的 RSP(带有 )下方的红色区域。


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

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

如果您一开始只是想学习 asm,那么 GNU 内联 asm 是一个糟糕的选择。 您必须完全理解 asm 发生的几乎所有事情,并了解编译器需要知道什么,才能编写正确的输入/输出约束并正确处理所有事情。错误将导致破坏和难以调试的损坏。函数调用 ABI 更简单,更容易跟踪代码和编译器代码之间的边界。


为什么这会中断

使用 编译-O0,因此 gcc 的代码将函数参数从溢出%rdi到堆栈上的某个位置。(即使使用 ,这也可能发生在非平凡的函数中-O3)。

由于目标 ABI 是x86-64 SysV ABI,它使用“红色区域”(低于 128 个字节%rsp,即使异步信号处理程序也不允许破坏),而不是浪费指令递减堆栈指针以保留空间。

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

当你的内联汇编完成后,

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

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

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

如果您想使用内联 asm 中的暂存空间,您可能应该将数组声明为局部变量并将其用作仅输出操作数(您从未从中读取)。

AFAIK,没有声明您修改红色区域的语法,因此您唯一的选择是:

  • 使用"=m"输出操作数(可能是数组)作为暂存空间;编译器可能会使用相对于 RBP 或 RSP 的寻址模式填充该操作数。您可以使用诸如此类的常量对其进行索引4 + %[tmp]。您可能会收到汇编程序警告,4 + (%rsp)但不会收到错误。
  • add $-128, %rsp用/sub $-128, %rsp围绕您的代码跳过红色区域。(如果您想使用未知数量的额外堆栈空间,例如推入循环或进行函数调用,则这是必需的。在纯 C 中取消引用函数指针的另一个原因,而不是内联 asm。)
  • 编译-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;
}

注意/部分%rbp之外的代码中的推送/弹出,由 gcc 发出。另请注意,它为您提供的暂存记忆位于红色区域。如果你用 编译,你会看到它与溢出的位置不同。#APP#NO_APP-O0&x

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

如果这看起来太复杂,请不要使用内联 asm。要么使用 C将编译器引导到您想要的 asm,就像最佳 asm 一样,要么在 asm 中编写一个完整的函数。

使用内联 asm 时,尽可能保持它的小:理想情况下,只有 gcc 不会自行发出的一两条指令,并带有输入/输出约束来告诉它如何将数据输入/输出 asm 语句。这就是它的设计目的。

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


脚注

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

内联汇编链接:

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

于 2015-12-30T04:06:57.607 回答
3

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

这个:

subq $12, %rsp;      // make room

应该:

subq $16, %rsp;      // make room
于 2015-12-29T22:30:04.963 回答