有点题外话,但我想跟进一下 gcc 内联汇编。
__volatile__ 的(非)需求来自 GCC 优化 内联汇编这一事实。 GCC 检查汇编语句的副作用/先决条件,如果发现它们不存在,它可能会选择移动汇编指令,甚至决定删除它。 __volatile__ 所做的只是告诉编译器“停止关心并把它放在那里”。
这通常不是你真正想要的。
这就是需要约束的地方。名称被重载,实际上用于GCC内联汇编中的不同事物:
- 约束指定
asm() 块中使用的输入/输出操作数
- 约束指定“clobber 列表”,其中详细说明了受
asm() 影响的“状态”(寄存器、条件代码、内存)。
- 约束指定操作数的类别(寄存器、地址、偏移量、常量……)
- 约束声明汇编程序实体与 C/C++ 变量/表达式之间的关联/绑定
在许多情况下,开发人员滥用 __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);
它有几个错误:
- 首先,它仅因 gcc 错误 (!) 而编译。通常,要在内联汇编中写入寄存器名称,需要双精度
%%,但在上面,如果您实际指定它们,则会出现编译器/汇编器错误,/tmp/ccYPmr3g.s:22: Error: bad register name '%%rax'。
- 其次,它不会告诉编译器您何时何地需要/使用这些变量。相反,它假定编译器从字面上尊重
asm()。对于 Microsoft Visual C++ 可能是这样,但对于 gcc,情况并非如此。
如果你编译它没有优化,它会创建:
0000000000400524 :
[ ... ]
400534: b8 d2 04 00 00 移动 $0x4d2,%eax
400539: bb e1 10 00 00 移动 $0x10e1,%ebx
40053e: 48 01 c3 添加 %rax,%rbx
400541: 48 89 大移动 %rbx,%rdx
400544: b8 5c 06 40 00 移动 $0x40065c,%eax
400549: 48 89 d6 移动 %rdx,%rsi
40054c: 48 89 c7 移动 %rax,%rdi
40054f: b8 00 00 00 00 移动 $0x0,%eax
400554:e8 d7 fe ff ff callq 400430
[...]
你可以找到你的
add 指令,以及两个寄存器的初始化,它会打印出预期的结果。另一方面,如果您加速优化,则会发生其他事情:
0000000000400530 :
400530: 48 83 ec 08 sub $0x8,%rsp
400534: 48 01 c3 添加 %rax,%rbx
400537: 是 e1 10 00 00 移动 $0x10e1,%esi
40053c: bf 3c 06 40 00 移动 $0x40063c,%edi
400541: 31 c0 xor %eax,%eax
400543:e8 e8 fe ff ff callq 400430
[ ... ]
您对两个“已使用”寄存器的初始化不再存在。编译器丢弃了它们,因为它看不到任何东西在使用它们,并且在保留汇编指令的同时,它把它放在
之前对这两个变量的任何使用。它就在那里,但它什么也没做(幸运的是……如果
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"(...),需要在汇编语句之前初始化,并被视为只读/不被语句修改。在这里,将 foo 与此关联。
注意没有指定寄存器分配 - 编译器根据编译的变量/状态来选择。以上的(优化的)输出:
0000000000400530 :
400530: 48 83 ec 08 sub $0x8,%rsp
400534: b8 d2 04 00 00 移动 $0x4d2,%eax
400539: 是 e1 10 00 00 移动 $0x10e1,%esi
40053e: bf 4c 06 40 00 移动 $0x40064c,%edi
400543: 01 c6 添加 %eax,%esi
400545: 31 c0 xor %eax,%eax
400547:e8 e4 fe ff ff callq 400430
[ ... ]
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:
-
bla(打哈欠 - 和以前一样,
bar 输入/输出)
- 该语句有一个输入操作数,变量
foo,该语句不关心它是在寄存器中、内存中还是编译时常量(即"g"(...) 约束)
结果与前者不同:
0000000000400530 :
400530: 48 83 ec 08 sub $0x8,%rsp
400534: bf 4c 06 40 00 移动 $0x40064c,%edi
400539: 31 c0 xor %eax,%eax
40053b: 是 e1 10 00 00 移动 $0x10e1,%esi
400540: 81 c6 d2 04 00 00 添加 $0x4d2,%esi
400546:e8 e5 fe ff ff callq 400430
[ ... ]
因为现在,GCC
实际上已经弄清楚了 foo 是一个编译时常量,并简单地将值嵌入 add 说明!那不是很整洁吗?
诚然,这很复杂,需要习惯。优点是让编译器选择哪些寄存器用于哪些操作数允许整体优化代码;例如,如果在宏和/或static inline 函数中使用内联汇编语句,则编译器可以根据调用上下文在代码的不同实例中选择不同的寄存器。或者,如果某个值在一个地方是编译时可评估/常量,但在另一个地方不是,编译器可以为它定制创建的程序集。
将 GCC 内联汇编约束视为一种“扩展函数原型”——它们告诉编译器参数/返回值的类型和位置,以及更多信息。如果您不指定这些约束,则您的内联汇编正在创建仅对全局变量/状态进行操作的函数的模拟——正如我们可能都同意的那样,这些函数很少会完全按照您的意图进行。