在C ++内联asm中使用基指针寄存器
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在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
时,它会出现故障。
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。
请参阅本答案的底部,以获取其他内联-asm Q&A的链接集合。
您希望通过内联asm学习什么?如果你想学习内联asm,学会用它来制作高效的代码,而不是像这样的可怕的东西。如果你想编写函数序言和push / pop来保存/恢复寄存器,你应该在asm中编写整个函数。 (然后你可以很容易地使用nasm或yasm,而不是使用GNU汇编程序指令1的不太优选的AT&T语法。)
GNU内联asm很难使用,但允许您将自定义asm片段混合到C和C ++中,同时让编译器处理寄存器分配以及必要时的任何保存/恢复。有时编译器会通过给你一个允许被破坏的寄存器来避免保存和恢复。如果没有volatile
,当输入相同时,它甚至可以将asm语句从循环中提升出来。 (即除非你使用volatile
,否则输出被认为是输入的“纯粹”函数。)
如果你只是想在第一时间学习asm,那么GNU inline asm是一个糟糕的选择。你必须完全理解asm所发生的一切,并了解编译器需要知道什么,编写正确的输入/输出约束并使一切正确。错误将导致破坏事物和难以调试的破损。函数调用ABI更简单,更容易跟踪代码和编译器代码之间的边界。
你compiled with -O0
,所以gcc的代码将函数参数从%rdi
溢出到堆栈上的某个位置。 (即使使用-O3
,这也可能发生在非平凡的功能上)。由于目标ABI是x86-64 SysV ABI,它使用“红区”(%rsp
以下128B,即使异步信号处理程序也不允许破坏),而不是浪费指令递减堆栈指针以保留空间。
它将8B指针函数arg存储在-8(rsp_at_function_entry)
中。然后你的内联asm推动%rbp
,它将%rsp递减8然后在那里写入,破坏&x
(指针)的低32b。
当你的内联asm完成后,
- 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中的临时空间,则应该将数组声明为局部变量,并将其用作仅输出操作数(您从未读取过)。
Here's what you should have done:
void Bar(int &x)
{
int tmp;
long tmplong;
asm ("lea -16 + %[mem1], %%rbp
"
"imul $10, %%rbp, %q[reg1]
" // q modifier: 64bit name.
"add %k[reg1], %k[reg1]
" // k modifier: 32bit name
"movl $5, %[mem1]
" // 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代码使用。这会将寄存器分配给编译器,因此在内联到不同位置时可能会有所不同。如果你需要使用特定的寄存器(例如%cl
中的移位计数),提前选择并声明一个clobber是有意义的。当然,像"c" (count)
这样的输入约束会让gcc将计数放在rcx / ecx / cx / cl中,所以你不会发出可能冗余的mov %[count], %%ecx
。
如果这看起来太复杂,请不要使用内联asm。使用C的lead the compiler to the asm you want就像最优的asm,或者在asm中写一个完整的函数。
当使用内联asm时,尽可能地保持它:理想情况下只是gcc没有自己发出的一个或两个指令,输入/输出约束告诉它如何将数据输入/输出asm语句。这就是它的设计目标。
经验法则:如果您的GNU C内联asm以mov
开头或结尾,那么您通常会做错,应该使用约束。
脚注:
- 您可以通过使用
-masm=intel
(在这种情况下,您的代码只能使用该选项)或使用dialect alternatives在inline-asm中使用GAS的intel-syntax,因此它可以与Intel中的编译器或AT&T asm输出语法一起使用。但这并没有改变指令,GAS的英特尔语法也没有很好的记录。 (它就像MASM,而不是NASM。)除非你真的讨厌AT&T语法,否则我不推荐它。
Inline asm links:
- x86 wiki。 (标签维基也链接到这个问题,对于这个链接集合)
- The manual。读这个。请注意,内联asm旨在包装编译器通常不会发出的单个指令。这就是为什么说出“指令”之类的东西,而不是“代码块”。
- A tutorial
- Looping over arrays with inline assembly对指针/索引使用
r
约束并使用你选择的寻址模式,而使用m
约束让gcc在递增指针和索引数组之间做出选择。 - 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 flag Stephen 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:我对一个使用不当的内联asm的问题的答案,答案与此类似。
- 我怎样才能在__asm__中使用int?