在C ++内联asm中使用基指针寄存器
我希望能够在内联asm中使用基指针寄存器( %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
通常的序言/结尾函数调用方法,这将是好的。 但是,当我尝试访问内联asm后访问x
时会出现seg错误。
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
谁能告诉我为什么这个seg错误? 看来,我以某种方式腐败%rbp
但我不知道如何。 提前致谢。
我在64位Ubuntu 14.04上运行GCC 4.8.4。
查看这个答案的底部,了解其他内联asm Q&A的链接。
你希望用inline asm来学习什么? 如果你想学习内联,学习使用它来制作高效的代码,而不是像这样的可怕的东西。 如果你想写功能序言和推/弹出保存/恢复寄存器, 你应该写在整个函数在ASM 。 (那么你可以很容易地使用nasm或yasm,而不是使用GNU汇编程序伪指令的最不常用的AT&T语法。)
GNU inline asm很难使用,但允许您将自定义asm片段混合到C和C ++中,同时让编译器处理寄存器分配以及必要时的任何保存/恢复。 有时编译器将能够通过给你一个允许被破坏的寄存器来避免保存和恢复。 没有volatile
,当输入相同时,甚至可以将asm语句从循环中提取出来。 (即,除非使用volatile
,否则输出被假定为输入的“纯”功能)。
如果你只是试图学习asm,那么GNU inline asm是一个糟糕的选择。 你必须充分理解几乎所有的事情,了解编译器需要知道什么,编写正确的输入/输出约束,并使一切正确。 错误会导致破坏事物和难以调试的破坏。 函数调用ABI更简单,更易于跟踪代码和编译器代码之间的边界。
你用-O0
编译 ,所以gcc的代码将函数参数从%rdi
溢出到堆栈上的一个位置。 (即使使用-O3
这也可能发生在一个不平凡的功能上)。 由于目标ABI是x86-64 SysV ABI ,因此它使用“红色区域”(即低于%rsp
128B,即使异步信号处理程序也不允许触发),而不是浪费指令来减少堆栈指针以保留空间。
它将-8B指针函数arg存储在-8(rsp_at_function_entry)
。 然后你的内联asm推送%rbp
,它将%rsp递减8,然后在那里写入,打破&x
(指针)的低32b。
当你的内联模块完成后,
- gcc重新加载
-8(%rbp)
(已被%rbp
覆盖)并将其用作4B存储的地址。 -
Foo
以%rbp = (upper32)|5
(低32为5
原始值%rbp = (upper32)|5
返回到main
。 -
main
运行leave
:%rsp = (upper32)|5
-
main
从%rsp = (upper32)|5
,从虚拟地址(void*)(upper32|5)
读取返回地址,从注释0x7fff0000000d
。
我没有用调试器检查; 其中一个步骤可能稍微偏离了一点,但是问题在于你打破了红色区域 ,导致海湾合作委员会的代码摧毁了堆栈。
即使添加一个“内存”clobber不会得到gcc,以避免使用红色区域,所以它看起来从内联asm分配自己的堆栈内存只是一个坏主意。 (内存破坏意味着你可能写了一些你可以写入的内存,而不是你可能已经覆盖了你不应该写的东西。)
如果要使用内联asm的暂存空间,则应该将数组声明为局部变量,并将其用作仅输出操作数(从不读取)。
这是你应该做的 :
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
部分外的代码中的%rbp
压入/弹出。 另请注意,它提供的暂存内存位于红色区域。 如果你使用-O0
编译,你会发现它和泄漏点&x
位置不同。
为了获得更多的临时注册表,最好声明更多的输出操作数,这些操作数是周围的非asm代码从不使用的。 这留下了寄存器分配给编译器,所以在内联到不同的地方可能会有所不同。 如果你需要使用一个特定的寄存器(例如在%cl
移位计数),提前选择并声明一个clobber只是有意义的。 当然,像"c" (count)
这样的输入约束可以让gcc把计数放在rcx / ecx / cx / cl中,所以你不会发出潜在的多余的mov %[count], %%ecx
。
如果这看起来太复杂, 不要使用内联asm 。 或者用C 编译器引导你想要的asm,就好像asm一样,或者用asm编写一个完整的函数。
当使用inline asm时,尽可能小:理想情况下,仅仅是gcc自己没有发出的一个或两个指令,输入/输出约束告诉它如何将数据导入/导出asm语句。 这是它的目的。
内联asm链接:
- x86维基。 (标签wiki也链接到这个问题,这个链接的集合)
- 手册 。 读这个。 请注意,内联asm被设计为包装编译器通常不会发出的单个指令。 这就是为什么说“像指令”这样的东西,而不是“代码块”的原因。
- 教程
- 使用内联程序集循环数组使用
r
约束指针/索引,并使用您选择的寻址模式,与使用m
约束让gcc选择增加指针与索引数组之间的关系。 - 在GNU C inline asm中,单个操作数的xmm / ymm / zmm的修饰符是什么? 。 使用
%q0
得到%rax
与%w0
得到%ax
。 使用%g[scalar]
来获取%zmm0
而不是%xmm0
。 - 使用进位标志进行高效的128位加法 Stephen Canon的回答解释了一个在读写操作数上需要早期的clobber声明的情况 。 另请注意,x86 / x86-64内联asm不需要声明
"cc"
clobber(条件代码,又名标志); 这是隐含的。 (gcc6引入了使用标志条件作为输入/输出操作数的语法,在此之前你必须setcc
一个gcc将发出代码来test
的寄存器,这显然更糟。 - 有关strlen的不同实现的性能的问题 :我用一个使用不当的内联asm的问题的答案,与此类似的答案。
- llvm报告:不受支持的inline asm:输入类型为'void *'匹配'int'类型的输出 :使用不可取的内存操作数(在x86中,所有有效地址都是可抵消的:您可以随时添加一个位移)。
- 当不使用内联asm时 ,以
32b/32b => 32b
除法为例,编译器可以用一个div
。 (问题中的代码是如何不使用内联asm的一个例子:用于设置和保存/恢复的许多指令应通过编写适当的输入/输出约束留给编译器。 - MSVC内联asm与GNU C inline asm用于封装单个指令 ,
64b/32b=>32bit
除法的内联asm的正确示例。 MSVC的设计和语法需要通过内存的输入和输出的往返行程,使得短的功能很糟糕。 根据罗斯里奇对这个答案的评论,这也是“从来不可靠的”。 - 使用x87浮点和交换操作数 。 不是一个很好的例子,因为我没有找到一种方法让gcc发出理想的代码。
其中一些重新重复我在这里解释的一些相同的东西。 我没有重读,试图避免冗余,对不起。
在x86-64中,堆栈指针需要对齐到8个字节。
这个:
subq $12, %rsp; // make room
应该:
subq $16, %rsp; // make room