2024-04-10
C++
最近在学习原子变量底层实现的过程中,看到了一些在 C/C++ 中嵌入汇编代码的例子,因此学习了一下相关的语法规则,有了这篇总结,便于将来查阅。
注意:本文仅适用于 GCC,并且 x86-64 汇编采用 AT&T 语法,不适用于 ARM64(如 Apple Silicon 芯片)。MSVC 内嵌汇编的语法可能有所不同。
1. 基本语法
在 C/C++ 的语言规范中,有一个 asm
关键字,它便是用来在 C/C++ 代码中内嵌汇编代码的。其基本语法如下:
asm("assembly code");
或
__asm__("assembly code");
可以认为上面两种语法没有差别,因此后文都采取更简洁的 asm
形式。
一个完整的例子:
#include <stdio.h>
int main() {
/* 把 10 个 20 加起来,将结果存储到 eax 寄存器中。 */
asm("movl $10, %eax;"
"movl $20, %ebx;"
"addl %ebx, %eax;");
return 0;
}
可以看到,基本就是只需要用 asm
把汇编代码包起来就可以了。
2. 扩展语法
除了能执行一些简单的机器指令外,更多时候我们需要在 C++ 和汇编之间进行「数据沟通」,即「在汇编代码中读写 C++ 代码中的变量」。此时就需要用到扩展语法,它允许我们指定每条指令的「输入输出」,其基本形式如下:
asm ("assembly code"
: output operands /* optional */
: input operands /* optional */
: list of clobbered registers /* optional */
);
即,在汇编后面用冒号 :
增加一些可选的输出操作数、输入操作数以及「会被操作搞乱(clobbered)的寄存器」。
接下来看几个例子就能明白了:
asm("movl %%eax, %0;" : "=r"(val));
这段代码的意思是将 eax
寄存器的内容存储到 val
变量中。更多语法细节:
- 这里额外指定了 1 个输出操作数——
"=r"(val)
,前面的字符串"=r"
被称为操作数约束,后面在括号里放一个 C++ 表达式,代表某个变量:r
约束的意思是val
操作数只能被存储在通用寄存器中;=
的意思是该操作数会被写入。
%0
就代指第一个操作数(从 0 开始)。- 这里引用寄存器的方式变成了使用 2 个 %:
%%eax
,这是为了与%0
做出区分。
另一个更复杂的例子:对两个整数求和,将结果存储在第三个数中:
#include <cstdio>
int main() {
int a = 2;
int b = 3;
int y;
asm("movl %1, %%eax;"
"movl %2, %%ebx;"
"addl %%ebx, %%eax;"
"movl %%eax, %0;"
: "=r"(y) /* output */
: "r"(a), "r"(b) /* inputs */
: "%eax", "%ebx" /* clobbered registers */
);
printf("y=%d\n", y);
return 0;
}
运行结果:
y=5
除了使用序号 %0
、%1
等来引用操作数外,还可以给操作数指定名字,这样在汇编代码中引用时会方便一些:
asm("movl %[a], %%eax;"
"movl %[b], %%ebx;"
"addl %%ebx, %%eax;"
"movl %%eax, %[y];"
: [y] "=r"(y) /* output */
: [a] "r"(a), [b] "r"(b) /* inputs */
: "%eax", "%ebx" /* clobbered registers */
);
在上述代码中,汇编代码中引用变量时,使用的是 %[name]
的形式,而在指定输入输出时,使用的是 [name]
的形式。
如果能看懂上面这段代码,那么绝大多数 C++ 内嵌汇编应该都能读懂了。如果想了解更多用法,请参考 GCC 官方文档:Using Assembly Language with C (Using the GNU Compiler Collection (GCC)).