嵌入汇编允许你将汇编代码直接插入到 C 语言代码中,这对于需要直接操作硬件、优化关键性能代码或实现 C 语言无法直接表达的功能(如某些 CPU 指令)的场景非常有用。

(图片来源网络,侵删)
我们将从基本语法、操作数约束、再到一个完整的实例,一步步进行说明。
基本语法
在 GCC 中,嵌入汇编使用 asm 或 __asm__ 关键字(两者等价),后面跟着一个用括号括起来的字符串,最完整的语法如下:
asm ( "assembly code"
: output_operands
: input_operands
: clobbered_registers
);
这四个部分分别是:
- 汇编代码:这是你要执行的汇编指令字符串。
- 输出操作数:告诉编译器哪些寄存器或内存位置会被你的汇编代码修改,这些值会最终写回到 C 语言的变量中。
- 输入操作数:告诉编译器哪些 C 语言的变量需要被加载到寄存器或内存中,以便汇编代码使用。
- 破坏的寄存器:告诉编译器你的汇编代码会修改哪些寄存器(即使这些寄存器没有被列为输出操作数),这样编译器在使用这些寄存器保存临时数据时就会知道。
四个部分详解
A. 汇编代码
这是最核心的部分,就是你写的汇编指令。movl $1, %eax。

(图片来源网络,侵删)
B. 输入操作数
告诉编译器哪些 C 变量需要作为输入,格式为:
[约束] "C变量名" (C表达式)
- C变量名:在汇编代码中,你通过
%[C变量名]来引用它。 - C表达式:通常是 C 语言的变量。
- 约束:这是最关键的部分,它告诉编译器如何处理这个变量(是放入寄存器还是内存,放入哪个寄存器等)。
C. 输出操作数
告诉编译器哪些寄存器或内存中的值最终要写回到 C 变量中,格式为:
[约束] "C变量名" (C表达式)
- C变量名:在汇编代码中,你通过
%[C变量名]来引用它。 - C表达式:通常是 C 语言的变量,它的值将被修改。
- 约束:同样用于指定操作数的位置。
D. 破坏的寄存器
一个字符串列表,列出你的汇编代码会修改但没有在输出操作数中声明的寄存器,如果你在汇编代码中使用了 eax 但没有用它来输出结果,你就应该在这里写上 "eax",编译器会认为这些寄存器的值是“脏”的,如果之前有数据,会自动保存和恢复。
操作数约束
约束是嵌入汇编的精髓,它定义了 C 变量和汇编操作数之间的映射关系,常见的约束有:

(图片来源网络,侵删)
| 约束 | 含义 | 示例 |
|---|---|---|
r |
任意通用寄存器 | %eax, %ebx, %ecx, %edx |
a |
%eax 寄存器 |
a |
b |
%ebx 寄存器 |
b |
c |
%ecx 寄存器 |
c |
d |
%edx 寄存器 |
d |
S |
%esi 寄存器 |
S |
D |
%edi 寄存器 |
D |
m |
内存操作数 | 变量的内存地址 |
q |
a, b, c, d 中的任意一个 |
q |
=r |
只写的通用寄存器 | 表示输出 |
+r |
读写(输入和输出)的通用寄存器 | 表示读写 |
g |
任意通用寄存器或内存 | g |
一个完整的约束示例:"=a" (output)
- 表示这是一个输出操作数。
a:表示这个操作数应该被放入%eax寄存器。
一个完整的实例:交换两个整数
这是一个非常经典且易于理解的例子,我们用嵌入汇编来实现两个整数的交换。
C 代码 (swap.c)
#include <stdio.h>
// 函数声明
void swap(int *a, int *b);
int main() {
int x = 10;
int y = 20;
printf("Before swap: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("After swap: x = %d, y = %d\n", x, y);
return 0;
}
// 使用嵌入汇编实现的交换函数
void swap(int *a, int *b) {
int temp;
asm volatile (
"movl %[src], %%eax;" // 1. 将 src 指向的值加载到 eax
"movl %[dest], %%ebx;" // 2. 将 dest 指向的值加载到 ebx
"movl %%eax, %[dest];" // 3. 将 eax 的值(原src)存入 dest
"movl %%ebx, %[src];" // 4. 将 ebx 的值(原dest)存入 src
: [src] "=m" (*a), [dest] "=m" (*b) // 输出操作数
: [src] "m" (*a), [dest] "m" (*b) // 输入操作数
: "eax", "ebx", "memory" // 破坏的寄存器
);
// 注意:这里的 "=m" 和 "m" 表示操作数在内存中。
// 我们通过内存地址直接交换,不需要 temp 变量。
// 但为了展示寄存器使用,我们下面提供一个更典型的版本。
}
上面的版本直接在内存中操作,虽然高效,但为了更好地展示寄存器约束,我们重写 swap 函数,使用一个临时寄存器 temp。
更典型的 swap 函数版本
void swap(int *a, int *b) {
int temp;
asm volatile (
"movl (%[src]), %%eax;" // 1. 将 src 指向的内存中的值加载到 eax
"movl (%[dest]), %%ebx;" // 2. 将 dest 指向的内存中的值加载到 ebx
"movl %%eax, (%[dest]);" // 3. 将 eax 的值存入 dest 指向的内存
"movl %%ebx, (%[src]);" // 4. 将 ebx 的值存入 src 指向的内存
: // 没有输出操作数,因为我们直接修改了内存
: [src] "r" (a), [dest] "r" (b) // 输入操作数:将指针 a, b 放入任意寄存器
: "eax", "ebx", "memory" // 破坏的寄存器
);
}
代码解析:
-
asm volatile (...)asm:关键字。volatile:这是一个非常重要的修饰符,它告诉编译器不要优化这段汇编代码,即使编译器认为这段代码没有效果(它的输出没有被使用),也不要删除它,这对于硬件操作或需要精确时序的代码至关重要。
-
汇编代码
movl (%[src]), %%eax;:%[src]会被替换为 C 变量a(即&x)。(%[src])表示取a这个地址中的值,所以这行代码的意思是:把x的值放入%eax寄存器,注意, 用于寄存器, 用于在汇编字符串中表示一个 符号,因为 在汇编中有特殊含义。movl (%[dest]), %%ebx;:同理,把y的值放入%ebx。movl %%eax, (%[dest]);:把%eax中的值(原x)存入y的地址。movl %%ebx, (%[src]);:把%ebx中的值(原y)存入x的地址。
-
输入操作数
[src] "r" (a):a(&x)这个指针变量会被放入一个通用寄存器(由编译器选择,如%esi或%edi),在汇编代码中,我们通过%[src]来引用这个寄存器。[dest] "r" (b):同理,b(&y)也会被放入一个通用寄存器,通过%[dest]引用。
-
破坏的寄存器
"eax", "ebx":我们明确告诉编译器,%eax和%ebx的值在我们的汇编代码中被修改了,编译器会确保在进入asm块之前,如果这些寄存器中保存了有用的临时数据,会先将它们保存到栈上,并在asm块执行结束后恢复。"memory":这是一个特殊的约束,它告诉编译器,我们的汇编代码会修改内存中的内容(通过指针a和b),这会阻止编译器在asm块之前和之后对内存进行不必要的缓存或重排操作,确保了内存操作的可见性。
编译和运行
将代码保存为 swap.c,使用 GCC 进行编译:
# -o 指定输出文件名 gcc -o swap swap.c
运行:
./swap
预期输出:
Before swap: x = 10, y = 20
After swap: x = 20, y = 10
高级技巧与注意事项
- 扩展汇编语法:我们上面使用的
"=r","m"等是 GCC 的扩展语法,不是标准的 AT&T 汇编语法,它极大地简化了与 C 编译器的交互。 - 可移植性:嵌入汇编是高度平台相关的,上面的代码只能在 x86 或 x86-64 架构的 Linux 系统上运行,如果你需要为 ARM 编写,汇编语法和约束都会完全不同。
- 性能优化:在大多数情况下,现代 C 编译器(如 GCC, Clang)的优化能力非常强大,手动嵌入汇编并不一定能带来性能提升,甚至可能因为破坏了编译器的优化而变得更慢。只有在经过性能分析(Profiling)确认是关键瓶颈,并且你对汇编和编译器行为有深入了解时,才应考虑使用嵌入汇编。
__attribute__((__always_inline__)):如果你希望一个包含嵌入汇编的函数总是被内联,可以使用这个属性,以避免函数调用的开销。- 查阅文档:GCC 的官方文档是学习嵌入汇编最权威的资料,可以搜索 "GCC Inline Assembly HOWTO"。
希望这份详细的指南能帮助你理解在 Linux C 语言中使用嵌入汇编!
