请查看本答案底部的链接收藏,其中包含其他内联汇编的问答。
你的代码有问题,因为你使用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 开头或结尾,则通常做错了,应该使用约束条件代替。
脚注:
- 您可以使用GAS的intel语法在内联汇编中构建,方法是使用
-masm=intel
(这种情况下,您的代码将仅适用于该选项),或者使用方言替代以便与Intel或AT&T汇编输出语法的编译器一起工作。但这不会改变指令,并且GAS的Intel语法文档不太完善。(它类似于MASM而不是NASM)。除非您真的很讨厌AT&T语法,否则我不建议使用它。
内联汇编链接:
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