对于以下代码:
long buf[64];
register long rrax asm ("rax");
register long rrbx asm ("rbx");
register long rrsi asm ("rsi");
rrax = 0x34;
rrbx = 0x39;
__asm__ __volatile__ ("movq $buf,%rsi");
__asm__ __volatile__ ("movq %rax, 0(%rsi);");
__asm__ __volatile__ ("movq %rbx, 8(%rsi);");
printf( "buf[0] = %lx, buf[1] = %lx!\n", buf[0], buf[1] );
我得到以下输出:
buf[0] = 0, buf[1] = 346161cbc0!
虽然应该是:
buf[0] = 34, buf[1] = 39!
任何想法为什么它不能正常工作,以及如何解决它?
答案 0 :(得分:22)
你破坏了内存,但没有告诉GCC,所以GCC可以在buf
汇总调用中缓存值。如果您想使用输入和输出,请告诉GCC所有内容。
__asm__ (
"movq %1, 0(%0)\n\t"
"movq %2, 8(%0)"
: /* Outputs (none) */
: "r"(buf), "r"(rrax), "r"(rrbx) /* Inputs */
: "memory"); /* Clobbered */
您通常也希望让GCC处理大部分mov
,寄存器选择等 - 即使您明确约束寄存器(rrax是stil %rax
)让信息流经GCC或你会得到意想不到的结果。
__volatile__
错了。 __volatile__
存在的原因是,您可以保证编译器将您的代码准确放置在原来的位置......这是此代码的完全不必要的保证。实现内存屏障等高级功能是必要的,但如果只修改内存和寄存器,则几乎完全没用。
GCC已经知道它无法在printf
之后移动此程序集,因为printf
调用访问buf
,并且buf
可能被程序集破坏。 GCC已经知道它不能在rrax=0x39;
之前移动程序集,因为rax
是汇编代码的输入。那么__volatile__
会给你带来什么?什么都没有。
如果您的代码在没有__volatile__
的情况下无效,那么代码中的错误应该是已修复,而不是仅添加__volatile__
,并希望这会使一切变得更好。 __volatile__
关键字不是魔术,不应该这样对待。
替代方案:
原始代码需要__volatile__
吗?不。只需正确标记输入和符号值。
/* The "S" constraint means %rsi, "b" means %rbx, and "a" means %rax
The inputs and clobbered values are specified. There is no output
so that section is blank. */
rsi = (long) buf;
__asm__ ("movq %%rax, 0(%%rsi)" : : "a"(rrax), "S"(rssi) : "memory");
__asm__ ("movq %%rbx, 0(%%rsi)" : : "b"(rrbx), "S"(rrsi) : "memory");
为什么__volatile__
对您没有帮助:
rrax = 0x34; /* Dead code */
GCC完全有权完全删除上述行,因为上述问题中的代码声称它从不使用rrax
。
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)");
}
反汇编或多或少与您在-O0
,
movl $5, %rax
movq %rax, (global)
但是如果关闭优化,你可能会对装配很邋..我们试试-O2
:
movq %rax, (global)
糟糕! rax = 5;
去了哪里?这是死代码,因为%rax
从未在函数中使用 - 至少就GCC而言。海湾合作委员会没有偷看内部装配。删除__volatile__
后会发生什么?
; empty
好吧,你可能会认为__volatile__
正在通过让GCC放弃你宝贵的装配来为你服务,但它只是掩盖了GCC认为你的装配不是做任何东西的事实。 GCC认为你的程序集不需要输入,不产生输出,并且没有内存。你最好把它理顺:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ __volatile__ ("movq %%rax, (global)" : : : "memory");
}
现在我们得到以下输出:
movq %rax, (global)
更好。但是如果你告诉GCC输入信息,它会确保%rax
首先正确初始化:
long global;
void store_5(void)
{
register long rax asm ("rax");
rax = 5;
__asm__ ("movq %%rax, (global)" : : "a"(rax) : "memory");
}
输出,优化:
movl $5, %eax
movq %rax, (global)
正确!我们甚至不需要使用__volatile__
。
__volatile__
存在? __volatile__
的主要正确用法是,如果汇编代码除了输入,输出或破坏内存之外还执行其他操作。也许它与GCC不了解或影响IO的特殊寄存器相混淆。你在Linux内核中看到了很多,但它在用户空间中经常被滥用。
__volatile__
关键字非常诱人,因为我们C程序员经常喜欢认为我们已经用汇编语言进行了几乎编程。不是。 C编译器进行了大量的数据流分析 - 因此您需要向编译器解释汇编代码的数据流。这样,编译器就可以安全地操纵你的程序集块,就像它操纵它生成的程序集一样。
如果您发现自己经常使用__volatile__
,可以在汇编文件中编写整个函数或模块。
答案 1 :(得分:4)
编译器使用寄存器,它可以覆盖你放入它们的值。
在这种情况下,编译器可能在rbx
赋值之后和内联汇编部分之前使用rrbx
寄存器。
通常,您不应期望寄存器在内联汇编代码序列之后和之间保留其值。
答案 2 :(得分:2)
稍微偏离主题但我想跟进gcc内联汇编。
__volatile__
的(非)需求来自GCC 优化内联汇编的事实。 GCC检查汇编语句的副作用/先决条件,如果发现它们不存在,它可能会选择移动汇编指令,甚至决定删除它。所有__volatile__
所做的就是告诉编译器"停止关怀并将其放在那里"。
这通常不是你真正想要的。
这就是需要约束的地方。名称重载并实际用于GCC内联汇编中的不同内容:
asm()
块asm()
。在许多情况下,开发人员滥用 __volatile__
因为他们发现他们的代码要么被移动,要么在没有它的情况下消失。如果发生这种情况,通常情况下,开发人员已经尝试不告诉GCC有关装配的副作用/先决条件。例如,这个错误的代码:
register int foo __asm__("rax") = 1234;
register int bar __adm__("rbx") = 4321;
asm("add %rax, %rbx");
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
它有几个错误:
%%
,但如果您实际指定它们,则会得到编译器/汇编程序错误/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'
。asm()
。对于Microsoft Visual C ++可能是这样,但对于gcc而言不是。如果您编译而不进行优化,则会创建:
0000000000400524 <main>: [ ... ] 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: bb e1 10 00 00 mov $0x10e1,%ebx 40053e: 48 01 c3 add %rax,%rbx 400541: 48 89 da mov %rbx,%rdx 400544: b8 5c 06 40 00 mov $0x40065c,%eax 400549: 48 89 d6 mov %rdx,%rsi 40054c: 48 89 c7 mov %rax,%rdi 40054f: b8 00 00 00 00 mov $0x0,%eax 400554: e8 d7 fe ff ff callq 400430 <printf@plt> [...]您可以找到
add
指令以及两个寄存器的初始化,并打印出预期的寄存器。另一方面,如果您进行优化,则会发生其他情况:0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: 48 01 c3 add %rax,%rbx 400537: be e1 10 00 00 mov $0x10e1,%esi 40053c: bf 3c 06 40 00 mov $0x40063c,%edi 400541: 31 c0 xor %eax,%eax 400543: e8 e8 fe ff ff callq 400430 <printf@plt> [ ... ]您对&#34;二手&#34;的初始化寄存器不再存在。编译器放弃了它们,因为没有任何东西可以看到它们正在使用它们,并且它保留了汇编指令,它在之前使用了两个变量。它就在那里,但它没有做任何事情(幸运的是......如果
rax
/ rbx
一直在使用谁可以告诉他们发生了什么。 ..)。
原因是你实际上告诉 GCC,程序集正在使用这些寄存器/这些操作数值。这没有任何意义使用volatile
,但所有事实都与您使用无约束asm()
表达式有关。
正确执行 的方式是通过约束,即您使用:
int foo = 1234;
int bar = 4321;
asm("add %1, %0" : "+r"(bar) : "r"(foo));
printf("I'm expecting 'bar' to be 5555 it is: %d\n", bar);
这告诉编译器程序集:
"+r"(...)
中有一个参数,它们都需要在汇编语句之前初始化,并由汇编语句修改,并将变量bar
与它相关联。"r"(...)
需要在汇编语句之前初始化,并且被声明视为readonly / not modified。在此处,将foo
与此相关联。注意没有指定寄存器赋值 - 编译器根据编译的变量/状态选择它。以上的(优化的)输出:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: b8 d2 04 00 00 mov $0x4d2,%eax 400539: be e1 10 00 00 mov $0x10e1,%esi 40053e: bf 4c 06 40 00 mov $0x40064c,%edi 400543: 01 c6 add %eax,%esi 400545: 31 c0 xor %eax,%eax 400547: e8 e4 fe ff ff callq 400430 <printf@plt> [ ... ]GCC内联汇编约束几乎总是必要以某种形式或另一种形式
asm("add %1, %0" : "=r"(bar) : "r"(foo), "0"(bar));
这告诉gcc:
bar
,在语句中找到后,"=r"(...)
foo
,它将放入寄存器"r"(...)
bar
或者,又一个替代方案:
asm("add %1, %0" : "+r"(bar) : "g"(foo));
告诉gcc:
bar
输入/输出)foo
,该语句并不关心它是在寄存器中,在内存中还是在编译时常量中(&#39;是"g"(...)
约束)结果与前者不同:
0000000000400530 <main>: 400530: 48 83 ec 08 sub $0x8,%rsp 400534: bf 4c 06 40 00 mov $0x40064c,%edi 400539: 31 c0 xor %eax,%eax 40053b: be e1 10 00 00 mov $0x10e1,%esi 400540: 81 c6 d2 04 00 00 add $0x4d2,%esi 400546: e8 e5 fe ff ff callq 400430 <printf@plt> [ ... ]因为现在,GCC 实际上已经找到了
foo
是一个编译时常量只需将值嵌入 add
指令即可!那不是很整洁吗?
不可否认,这很复杂,需要习惯。优点是让编译器选择哪些寄存器用于哪些操作数允许整体优化代码;例如,如果在宏和/或static inline
函数中使用内联汇编语句,则编译器可以根据调用上下文在代码的不同实例中选择不同的寄存器。或者,如果某个值在一个地方是编译时可评估/常量而在另一个地方没有,则编译器可以为其定制创建的程序集。
将GCC内联装配约束视为&#34;扩展函数原型&#34; - 它们告诉编译器参数/返回值的类型和位置,以及更多。如果你没有指定这些约束,你的内联汇编就会创建仅对全局变量/状态进行操作的函数模拟 - 正如我们可能都认为的那样,它们很少完全按照你的意图行事。