我希望能够%rbp
在内联asm中使用基指针寄存器().这样的玩具示例是这样的:
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
联asm之后访问时,它会出现故障.
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++中,同时让编译器处理寄存器分配以及必要时的任何保存/恢复.有时编译器会通过给你一个允许被破坏的寄存器来避免保存和恢复.没有push
,当输入相同时,它甚至可以将asm语句从循环中提升出来.(即除非您使用volatile
,否则输出被假定为输入的"纯"函数.)
如果你只是想在第一时间学习asm,那么GNU inline asm是一个糟糕的选择. 你必须完全理解asm所发生的一切,并了解编译器需要知道什么,编写正确的输入/输出约束并使一切正确.错误将导致破坏事物和难以调试的破损.函数调用ABI更简单,更容易跟踪代码和编译器代码之间的边界.
你编译volatile
,所以gcc的代码将函数参数从-O0
堆栈溢出到一个位置.(这可能发生在非平凡的功能中,即使有%rdi
).由于目标ABI是x86-64 SysV ABI,它使用"红区"(-O3
即使异步信号处理程序不允许使用下方128B ),而不是浪费指令将堆栈指针递减到预留空间.
它将8B指针函数arg存储在%rsp
.然后你的内联asm推送-8(rsp_at_function_entry)
,它将%rsp递减8然后在那里写入,破坏%rbp
(指针)的低32b .
当你的内联asm完成后,
gcc重新加载&x
(已被覆盖-8(%rbp)
)并将其用作4B商店的地址.
%rbp
返回到Foo
与main
(与低32设置为原稿值%rbp = (upper32)|5
).
5
运行main
: leave
%rsp = (upper32)|5
运行main
有ret
,阅读从虚拟地址的回邮地址%rsp = (upper32)|5
,从您的评论是(void*)(upper32|5)
.
我没有用调试器检查; 其中一个步骤可能略有偏差,但问题肯定是你破坏了红色区域,导致gcc的代码摧毁了堆栈.
即使添加"内存"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发出0x7fff0000000d
的"=m"
/ 4 + %[tmp]
section 之外的代码中的push/pop .另请注意,它为您提供的临时存储器位于红色区域中.如果你编译4 + (%rsp)
,你会看到它与溢出的位置不同add $-128, %rsp
.
为了获得更多的临时寄存器,最好只声明更多输出操作数,这些操作数永远不会被周围的非asm代码使用.这会将寄存器分配给编译器,因此在内联到不同位置时可能会有所不同.如果您需要使用特定的寄存器(例如,移位计数sub $-128, %rsp
),提前选择并声明一个clobber是有意义的.当然,输入约束如-mno-red-zone
获取gcc将计数放在rcx/ecx/cx/cl中,因此您不会发出潜在的冗余%rbp
.
如果这看起来太复杂,请不要使用内联asm.要么将编译器引导到你想要的asm,就像最优的asm一样,或者在asm中写一个完整的函数.
当使用内联asm时,尽可能地保持它:理想情况下只是gcc没有自己发出的一两条指令,输入/输出约束告诉它如何将数据输入/输出asm语句.这就是它的设计目标.
经验法则:如果你的GNU C内联asm以a开头或结尾#APP
,你通常会做错,应该使用约束.
脚注:
您可以在inline-asm中使用GAS的intel-syntax,通过构建#NO_APP
(在这种情况下,您的代码只能使用该选项),或者使用方言替代方案,以便它与Intel中的编译器或AT&T asm输出语法一起使用.但这并没有改变指令,GAS的英特尔语法也没有很好的记录.(它就像MASM,而不是NASM.)除非你真的讨厌AT&T语法,否则我不推荐它.
x86维基.(标签维基也链接到这个问题,对于这个链接集合)
手册.读这个.请注意,内联asm旨在包装编译器通常不会发出的单个指令.这就是为什么说出"指令"之类的东西,而不是"代码块".
一个教程
使用内联汇编对数组进行循环使用-O0
指针/索引的约束并使用您选择的寻址模式,使用&x
约束让gcc在递增指针与索引数组之间进行选择.
在GNU C inline asm中,对于单个操作数,xmm/ymm/zmm的修饰符是什么?.使用%cl
得到"c" (count)
与mov %[count], %%ecx
获取mov
.用-masm=intel
得到r
,而不是m
.
使用进位标志进行高效的128位加法 Stephen Canon的答案解释了在读+写操作数上需要早期删除声明的情况.另请注意,x86/x86-64内联asm不需要声明%q0
clobber(条件代码,也就是标志); 这是隐含的.(gcc6引入了使用标志条件作为输入/输出操作数的语法.在此之前你需要%rax
一个gcc将发出代码的寄存器%w0
,这显然更糟.)
关于strlen的不同实现的性能的问题:我对一些使用不当的内联asm的问题的答案,答案与此类似.
llvm报告:不支持的内联asm:输入类型'void*'匹配输出类型'int':使用offsetable内存操作数(在x86中,所有有效地址都是可偏移的:你总是可以添加一个位移).
当不使用内联asm时,%ax
编译器已经可以使用一个除法和余数的示例%g[scalar]
.(问题中的代码是如何不使用内联asm 的示例:设置和保存/恢复的许多指令应通过编写适当的输入/输出约束留给编译器.)
用于包装单个指令的MSVC内联asm与GNU C内联asm,以及用于%zmm0
除法的内联asm的正确示例.MSVC的设计和语法需要通过存储器往返输入和输出,这使得它对于短函数来说非常糟糕.根据罗斯里奇对该答案的评论,它也"永远不会非常可靠".
使用x87浮点和交换操作数.不是一个很好的例子,因为我没有找到让gcc发出理想代码的方法.
其中一些重复了我在这里解释的一些相同的东西.我没有重新阅读它们以试图避免冗余,抱歉.