在 C++ 内联 asm 中使用基指针寄存器
Posted
技术标签:
【中文标题】在 C++ 内联 asm 中使用基指针寄存器【英文标题】:Using base pointer register in C++ inline asm 【发布时间】:2016-04-03 20:47:54 【问题描述】:我希望能够在内联汇编中使用基址指针寄存器 (%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。
【问题讨论】:
不要添加无关语言的标签。 对于汇编代码:使用汇编参数指定C端变量;不要依赖汇编代码中的特定寄存器布局。并且总是指定clobbers。movq %rdi, -8(%rbp)
将 RDI 置于红色区域。然后您执行pushq %rbp;
将 RSP 减 8 并将值放在 RBP 中。不幸的是,由于 RSP=RBP 您只是覆盖了 GCC 存储在那里的值(假设是 RDI)。在您的内联汇编程序完成后,它尝试了movq -8(%rbp), %rax
。好吧,我们刚刚了解到您在内存位置 -8(%rbp)
处删除了数据,因此它现在包含一个虚假值,然后我们尝试使用 movl $5, (%rax)
取消引用它。该指令可能会出现段错误,因为 RAX 不再具有有效指针。
如果你想在内联汇编器中使用 C/C++ 变量,你真的需要开始使用输入(和输出,如果需要)约束来允许数据传入(和/或传出)。跨度>
【参考方案1】:
有关其他 inline-asm 问答的链接集合,请参阅此答案的底部。
您的代码已损坏,因为您踩到了 RSP(带有push
)下方的红色区域,其中 GCC 保留了一个值。
您希望通过 inline asm 学习完成什么?如果你想学习内联汇编,学习使用它来编写高效的代码,而不是像这样可怕的东西。如果你想编写函数序言和 push/pop 来保存/恢复寄存器,你应该在 asm 中编写整个函数。 (然后您可以轻松地使用 nasm 或 yasm,而不是使用 GNU 汇编器指令1 的大多数 AT&T 语法不太喜欢。)
GNU 内联 asm 很难使用,但允许您将自定义 asm 片段混合到 C 和 C++ 中,同时让编译器处理寄存器分配和任何必要的保存/恢复。有时编译器可以通过给你一个允许被破坏的寄存器来避免保存和恢复。如果没有volatile
,它甚至可以在输入相同的情况下将 asm 语句从循环中提升出来。 (即除非您使用volatile
,否则输出被假定为输入的“纯”函数。)
如果您一开始只是想学习 asm,那么 GNU 内联 asm 是一个糟糕的选择。您必须完全理解 asm 的几乎所有内容,并了解编译器需要知道、编写正确的输入/输出约束并正确处理所有内容。错误将导致破坏和难以调试的损坏。函数调用 ABI 更简单,更容易跟踪代码和编译器代码之间的边界。
为什么会中断
你 compiled with -O0
,所以 gcc 的代码将函数参数从 %rdi
溢出到堆栈上的某个位置。 (即使使用-O3
,这也可能发生在非平凡的函数中)。
由于目标 ABI 是 x86-64 SysV ABI,它使用 "Red Zone"(比 %rsp
低 128 个字节,即使异步信号处理程序也不允许破坏),而不是浪费一条指令递减堆栈指针以保留空间。
它将 8B 指针函数 arg 存储在 -8(rsp_at_function_entry)
。然后你的内联 asm 推送 %rbp
,它将 %rsp 减 8,然后写入那里,破坏了 &x
(指针)的低 32b。
当你的内联汇编完成后,
gcc 重新加载-8(%rbp)
(已被 %rbp
覆盖)并将其用作 4B 商店的地址。
Foo
返回到 main
和 %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
编译(我认为您不能在每个函数的基础上启用它,只能在每个文件中启用)
首先不要使用暂存空间。告诉编译器你破坏了什么寄存器并让它保存它们。
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;
注意 %rbp
在 #APP
/ #NO_APP
部分之外的代码中的推送/弹出,由 gcc 发出。另请注意,它为您提供的暂存记忆位于红色区域。如果您使用-O0
进行编译,您会看到它与&x
溢出的位置不同。
为了获得更多的临时注册,最好只声明更多的输出操作数,这些操作数从不被周围的非 asm 代码使用。这将寄存器分配留给编译器,因此当内联到不同的位置时它可能会有所不同。提前选择并声明一个clobber 仅在您需要使用特定寄存器时才有意义(例如%cl
中的移位计数)。当然,像 "c" (count)
这样的输入约束让 gcc 将计数放入 rcx/ecx/cx/cl,因此您不会发出潜在的冗余 mov %[count], %%ecx
。
如果这看起来太复杂,不要使用内联 asm。 lead the compiler to the asm you want 用 C 就像最佳 asm 一样,或者在 asm 中编写一个完整的函数。
使用内联 asm 时,尽量保持小:理想情况下,只有 gcc 不会自行发出的一两条指令,并通过输入/输出约束告诉它如何将数据输入/输出 asm陈述。这就是它的设计目的。
经验法则:如果您的 GNU C 内联汇编以 mov
开头或结尾,您通常做错了,应该改用约束。
脚注:
-
您可以在 inline-asm 中使用 GAS 的 intel-syntax,方法是使用
-masm=intel
构建(在这种情况下,您的代码将仅使用该选项),或使用 dialect alternatives,因此它适用于Intel 或 AT&T asm 输出语法中的编译器。但这并没有改变指令,而且 GAS 的 Intel 语法没有很好的文档记录。 (不过,它就像 MASM,而不是 NASM。)除非你真的讨厌 AT&T 语法,否则我不推荐它。
内联 asm 链接:
x86 维基。 (tag wiki 也将 链接到 这个问题,对于这个链接集合)
inline-assembly 标签wiki
The manual。读这个。请注意,内联 asm 旨在包装编译器通常不会发出的单个指令。这就是为什么它的措辞是“指令”而不是“代码块”。
A tutorial
Looping over arrays with inline assembly 对指针/索引使用 r
约束并使用您选择的寻址模式,而不是使用 m
约束让 gcc 在递增指针和索引数组之间进行选择。
How can I indicate that the memory *pointed* to by an inline ASM argument may be used?(寄存器中的指针输入确实不暗示指向的内存已被读取和/或写入,因此如果您不告诉编译器它可能不同步) .
In GNU C inline asm, what're the modifiers for xmm/ymm/zmm for a single operand?。使用%q0
获得%rax
与%w0
获得%ax
。使用%g[scalar]
获取%zmm0
而不是%xmm0
。
Efficient 128-bit addition using carry flagStephen Canon 的回答解释了在读写操作数上需要提前声明的情况。另请注意,x86/x86-64 内联 asm 不需要声明 "cc"
clobber(条件代码,又名标志);这是隐含的。 (gcc6 引入了syntax for using flag conditions as input/output operands。在此之前你必须给setcc
一个寄存器,gcc 会向test
发出代码,这显然更糟。)
Questions about the performance of different implementations of strlen:我对一个问题的答案,其中包含一些使用不当的内联汇编,答案与此类似。
llvm reports: unsupported inline asm: input with type 'void *' matching output with type 'int':使用可偏移的内存操作数(在 x86 中,所有有效地址都是可偏移的:您始终可以添加位移)。
When not to use inline asm,带有 32b/32b => 32b
除法和余数的示例,编译器已经可以使用单个 div
完成。 (问题中的代码是 not 如何使用内联 asm 的示例:许多设置和保存/恢复指令应通过编写适当的输入/输出约束留给编译器。)
MSVC inline asm vs. GNU C inline asm for wrapping a single instruction,以及 64b/32b=>32bit
除法 的正确内联汇编示例。 MSVC 的设计和语法需要在内存中进行输入和输出的往返,这对于短函数来说非常糟糕。根据罗斯里奇对该答案的评论,它也“永远不会非常可靠”。
Using x87 floating point, and commutative operands。不是一个很好的例子,因为我没有找到让 gcc 发出理想代码的方法。
其中一些重复了我在此处解释的一些相同内容。为了避免冗余,我没有重新阅读它们,抱歉。
【讨论】:
非常感谢您提供详细而翔实的答案。 “用 asm 编写整个函数”是什么意思——然后我将如何将它与 C/C++ 代码集成?还是你的意思是用 asm 写整个程序? 彼得的回答太棒了!但是在您的链接中,您两次访问了同一个链接。 @jaw:用 C 语言编写原型,并在单独的.S
(GNU 语法)或 .asm
(NASM/YASM 语法)中编写函数。 gcc -Wall -O3 main.c myfunc.S -o myprog
。见***.com/questions/13901261/…。如果您使用 NASM/YASM,请运行 yasm -felf64 myfunc.asm
以创建一个可以与 C 链接的 .o
。确保您的函数遵循 ABI(要保留的 regs 以及在哪里找到它的 args),否则它将中断当 gcc 的代码 call
s 时。 IIRC,Agner Fog 的 Optimizing Assembly 指南花了一些时间来说明如何做到这一点。 (x86 标签 wiki 中的链接)
@Zboson:我想我记得我想拥有的其他链接,而不是重复的:早期的破坏讨论。还有操作数大小修饰符问题。
很棒的链接。关于斯蒂芬佳能的回答,它最初没有clobber修饰符。 I followed his answer but for 256-bit add and I could not get the correct answer until I figured out to use a clobber modifier。如果没有 clobber 修饰符,我对他的 128 位添加从未有过问题。问题发生在第三次添加之后。但这可能只是巧合。这就是为什么我在他的回答中留下评论。【参考方案2】:
在 x86-64 中,堆栈指针需要对齐到 8 个字节。
这个:
subq $12, %rsp; // make room
应该是:
subq $16, %rsp; // make room
【讨论】:
对不起,我的错误:它实际上似乎并没有解决问题。它仍然是段错误。还有其他想法吗? 事实上,即使没有“//一些asm指令”语句,它仍然会失败。为什么推送和弹出会搞砸$rbp
?
段错误到底在哪里?它在访问什么地址,在什么指令上?
gdb 的输出看起来像这样(它在 asm 之后出现了几行错误): 第 11 行:Foo (x=@0x7ffffffffe034: 32767): " : : : );" ,第 12 行:Foo (x=@0x7ffffffffe020:-8128):“x = 5”,第 13 行:“”:无法访问地址 0x7fff0000000d 处的内存。程序收到信号 SIGSEGV,分段错误。”
在崩溃后使用disass foo
和info reg
编辑问题。以上是关于在 C++ 内联 asm 中使用基指针寄存器的主要内容,如果未能解决你的问题,请参考以下文章