15、内联汇编:GCC内联汇编语法,约束条件,在C中嵌入汇编代码
说实话,很多C语言开发者一听到“内联汇编”就觉得头大。我当年刚接触的时候也一样——好好的C代码里突然蹦出几行汇编,感觉像是两个世界的东西硬凑在一起。但后来我在做嵌入式驱动优化时,发现有些操作不用汇编根本搞不定,比如直接读写某个特殊寄存器、关中断、或者做那种C语言无法表达的原子操作。
说白了,内联汇编就是让你在C代码里直接写汇编指令。GCC提供了一套相当灵活的语法,核心就是 asm 关键字。嗯,这里要注意,GCC的内联汇编和微软的VC编译器语法完全不同,我们今天只聊GCC。
15.1 基本语法:asm volatile
最简单的形式长这样:
asm("movl %eax, %ebx");
但实际项目中几乎不会这么用。为什么?因为编译器不知道你动了哪些寄存器,它可能会在优化时把你的汇编指令挪走,或者把某个寄存器里的重要数据覆盖掉。所以,我个人的习惯是永远加上 volatile:
asm volatile("movl %eax, %ebx");
volatile 告诉编译器:“别优化我,别动我,老老实实按顺序执行。” 这在嵌入式里尤其重要,比如你要操作一个内存映射的寄存器,编译器如果觉得“这地址没被写过”就给你优化掉了,那硬件就罢工了。
15.2 扩展内联汇编:输入、输出与约束
真正实用的内联汇编,需要和C变量交互。GCC扩展语法格式如下:
asm volatile(
"汇编指令"
: 输出操作数列表
: 输入操作数列表
: 被破坏的寄存器列表
);
每个操作数用 "约束"(C表达式) 表示。约束条件告诉编译器:这个变量应该放在哪里(寄存器?内存?立即数?)。
来看一个实际例子——两个整数相加:
int a = 10, b = 20, result;
asm volatile(
"addl %2, %0"
: "=r"(result) // 输出:result 放在寄存器,= 表示只写
: "r"(a), "r"(b) // 输入:a 和 b 放在寄存器
: "cc" // 影响标志寄存器
);
// result 现在等于 30
这里 %0 对应第一个操作数(result),%1 对应 a,%2 对应 b。注意顺序:输出在前,输入在后。
15.3 常用约束条件一览
| 约束 | 含义 | 示例 |
|---|---|---|
r |
通用寄存器 | "r"(x) |
m |
内存地址 | "m"(x) |
i |
立即数(编译时常量) | "i"(100) |
g |
通用:寄存器、内存或立即数 | "g"(x) |
= |
只写(用于输出操作数) | "=r"(out) |
+ |
读写(输入输出同一变量) | "+r"(x) |
& |
早期破坏(不与输入共用寄存器) | "=&r"(out) |
"g" 是最安全的选择——编译器自己决定放寄存器还是内存。但性能敏感场景下,"r" 通常更快。
15.4 被破坏的寄存器列表
这个列表非常重要。你想想看,你在汇编里用了 eax、ecx,但编译器不知道啊!它可能正把某个重要变量放在 eax 里。所以你要明确告诉编译器:我用了这些寄存器,你帮我保存和恢复。
asm volatile(
"movl %0, %%eax\n\t"
"addl $1, %%eax\n\t"
"movl %%eax, %0"
: "+r"(x)
:
: "eax" // 告诉编译器 eax 被修改了
);
注意:在汇编代码里引用寄存器要加两个百分号 %%eax,因为单个 % 被用来引用操作数了。
常见的被破坏列表包括:"eax", "ebx", "ecx", "edx", "memory"(表示修改了内存), "cc"(表示修改了标志寄存器)。
"cc",结果条件判断偶尔出错,查了三天才发现是标志寄存器被内联汇编污染了。
15.5 实战:关中断与开中断
在嵌入式系统里,关中断是常见操作。C语言没有直接表达这个的能力,必须用汇编:
static inline void disable_interrupts(void) {
unsigned long flags;
asm volatile(
"pushfq\n\t" // 保存标志寄存器
"cli\n\t" // 关中断
"popq %0\n\t" // 弹出到 flags
: "=r"(flags)
:
: "memory"
);
}
这里 cli 是 x86 的关中断指令。pushfq/popq 保存和恢复标志寄存器,这样不会影响调用者的条件码。嗯,这个模式我在 RTOS 的临界区保护里用过无数次。
15.6 知识体系图
下面这张图总结了内联汇编的核心要素和它们之间的关系:
15.7 避坑指南
最后,分享几个我踩过的坑:
- 忘记加 volatile:编译器把内联汇编当成纯计算,直接优化掉了。硬件没反应,你还在那傻等。
- 约束写错:比如输出用了
"r"而不是"=r",编译器会报错或者生成错误代码。我刚开始学的时候经常犯这个。 - 寄存器冲突:汇编里用了
ebx但没写在破坏列表里。结果函数返回时ebx的值变了,调用者崩溃。这种 bug 极难复现。 - 64位 vs 32位:x86-64 下
pushfq/popfq是 64 位操作,32 位下要用pushfl/popfl。移植代码时一定要注意。
内联汇编是个强大的工具,但也是一把双刃剑。能用C解决的问题,尽量别用汇编。只有当你需要精确控制指令序列、访问特殊寄存器、或者做性能极致优化时,才把它请出来。嗯,记住这个原则,你会少踩很多坑。