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 告诉编译器:“别优化我,别动我,老老实实按顺序执行。” 这在嵌入式里尤其重要,比如你要操作一个内存映射的寄存器,编译器如果觉得“这地址没被写过”就给你优化掉了,那硬件就罢工了。

警告: 不要省略 volatile,除非你非常清楚自己在做什么。我曾经因为少写一个 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 被破坏的寄存器列表

这个列表非常重要。你想想看,你在汇编里用了 eaxecx,但编译器不知道啊!它可能正把某个重要变量放在 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"(表示修改了标志寄存器)。

核心原则: 宁可多列,不要少列。漏掉一个寄存器,可能引发极其诡异的 bug。我曾在多线程环境下漏掉了 "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 知识体系图

下面这张图总结了内联汇编的核心要素和它们之间的关系:

GCC内联汇编 基本语法 asm volatile 扩展语法 约束条件 输出操作数 输入操作数 被破坏列表 r / m / i / g = / + / & 寄存器 / memory / cc 核心:告诉编译器“我做了什么,别乱优化”

15.7 避坑指南

最后,分享几个我踩过的坑:

  • 忘记加 volatile:编译器把内联汇编当成纯计算,直接优化掉了。硬件没反应,你还在那傻等。
  • 约束写错:比如输出用了 "r" 而不是 "=r",编译器会报错或者生成错误代码。我刚开始学的时候经常犯这个。
  • 寄存器冲突:汇编里用了 ebx 但没写在破坏列表里。结果函数返回时 ebx 的值变了,调用者崩溃。这种 bug 极难复现。
  • 64位 vs 32位:x86-64 下 pushfq/popfq 是 64 位操作,32 位下要用 pushfl/popfl。移植代码时一定要注意。

内联汇编是个强大的工具,但也是一把双刃剑。能用C解决的问题,尽量别用汇编。只有当你需要精确控制指令序列、访问特殊寄存器、或者做性能极致优化时,才把它请出来。嗯,记住这个原则,你会少踩很多坑。


公众号:蓝海资料掘金营,微信deep3321