内联汇编:约束条件、破坏列表与优化器交互

内联汇编,说白了就是在 C 代码里直接嵌入汇编指令。很多嵌入式工程师一听到这个就头大,觉得又脏又危险。嗯,确实如此。但有些场景你绕不开——比如操作特殊寄存器、实现临界区保护、或者做那些编译器死活优化不好的位操作。

我个人习惯是:能用 C 解决的问题,绝不用内联汇编。但当你不得不碰它的时候,你得搞清楚三个核心问题:约束条件怎么写、破坏列表怎么列、以及优化器会不会把你的汇编给「优化」没了。

约束条件:告诉编译器你在干什么

约束条件(Constraints)是内联汇编里最容易被忽略的部分。它的作用很简单:告诉编译器,你的汇编代码要用到哪些 C 变量,以及怎么用。

我见过太多人写内联汇编时,约束条件随便写个 "r" 就完事了。结果呢?程序跑起来时对时错,查 bug 查到怀疑人生。

常见的约束条件有这么几类:

约束 含义 示例
"r" 通用寄存器 "r"(val)
"m" 内存地址 "m"(var)
"i" 立即数(编译期常量) "i"(42)
"=&r" 早期写操作(先写后读) "=&r"(result)
"+r" 可读可写 "+r"(val)

这里有个坑:"=&r" 里的 & 表示这个操作数在汇编指令读取所有输入之前就会被写入。如果你不写这个 &,编译器可能让输入和输出共用同一个寄存器,结果就是你的输入被输出覆盖了。

核心原则:约束条件不是摆设,它是你和编译器之间的契约。写错了,编译器不会报错,但生成的代码会悄悄出错。

破坏列表:别让编译器蒙在鼓里

破坏列表(Clobber List)是另一个容易被忽视的地方。它的作用是告诉编译器:我的汇编代码会破坏哪些寄存器或内存。

为什么需要这个?因为编译器不知道你的汇编指令内部干了什么。如果你在汇编里悄悄改了 r3 寄存器的值,而编译器恰好把某个重要变量放在 r3 里——恭喜你,你得到了一个随机崩溃的程序。

常见的破坏列表项:

  • "cc" — 条件寄存器被修改(比如你做了比较操作)
  • "memory" — 内存被修改(编译器会重新加载所有内存变量)
  • "r0", "r1", ... — 具体哪些寄存器被破坏

我曾经在一个项目里,写了一段内联汇编做 CRC 计算。汇编里用了 r4r7 四个寄存器,但破坏列表里只写了 "r4","r5"。结果程序在优化等级开到 -O2 时就开始随机出错。查了两天才发现是 r6r7 没列进去。

警告:破坏列表写少了,编译器会生成错误代码;写多了,编译器会生成低效代码(多保存恢复寄存器)。但两害相权取其轻——宁可多写,不要少写。

内联汇编与优化器的交互

这是最微妙的部分。优化器(Optimizer)是个聪明但固执的家伙。它看到你的内联汇编,默认会做两件事:

  1. 重排指令 — 优化器觉得你的汇编和周围的 C 代码没有依赖关系,就把它挪到别处去了
  2. 删除「无用」代码 — 优化器发现你的汇编输出没有被使用,直接把它删了

怎么防止优化器乱动你的汇编?答案是 volatile 关键字。

// 不加 volatile,优化器可能把这段汇编删掉
asm("nop");

// 加了 volatile,优化器会保留它
asm volatile("nop");

volatile 不是万能的。它只保证汇编指令不被删除,不保证执行顺序。如果你需要严格顺序,还得靠 asm volatile("..." ::: "memory") 这种「内存屏障」写法。

技巧:如果你写的是「纯计算」型内联汇编(不访问内存、不操作外设),可以不加 volatile,让优化器自由调度。但如果你操作的是硬件寄存器、或者需要精确时序——务必加上 volatile"memory" 破坏列表。

一个完整的例子

来看一个实际场景:在 ARM Cortex-M 上关中断和开中断。

// 关中断
static inline void disable_irq(void)
{
    unsigned long tmp;
    asm volatile(
        "mrs    %0, primask\n\t"
        "cpsid  i\n\t"
        : "=r" (tmp)
        :
        : "memory"
    );
}

// 开中断
static inline void enable_irq(void)
{
    asm volatile(
        "cpsie  i\n\t"
        :
        :
        : "memory"
    );
}

注意这里:

  • 用了 volatile,防止优化器删除或重排
  • 加了 "memory" 破坏列表,告诉编译器内存可能被中断处理程序修改
  • 关中断时保存了 primask 寄存器的值,虽然这里没用到——但这是好习惯,方便以后实现嵌套中断保护

避坑指南

我曾经在一个产品里,因为内联汇编的约束条件写错,导致产品在高温下频繁死机。排查了整整一周,最后发现是 "=&r" 写成了 "=r",编译器让输入和输出共用了同一个寄存器,汇编指令把输入值覆盖了。

从那以后,我给自己定了几条规矩:

  • 每条内联汇编都写完整的约束条件和破坏列表,不偷懒
  • 操作硬件寄存器时,一定加 volatile"memory"
  • 内联汇编前后加注释,说明这段汇编在干什么、用了哪些寄存器
  • 写完以后,反汇编看看生成的机器码,确认编译器没乱来

记住:内联汇编是 C 语言里的「法外之地」。你写的是汇编,但编译器仍然在管理 C 代码。约束条件和破坏列表,就是你和编译器之间的沟通桥梁。桥断了,两边各走各的,程序就崩了。

知识体系图

下面这张图总结了内联汇编的核心知识结构:

内联汇编 约束条件 破坏列表 优化器交互 "r" 通用寄存器 "m" 内存地址 "i" 立即数 "cc" 条件寄存器 "memory" 内存 "r0","r1"... 寄存器 指令重排 删除无用代码 volatile 控制 核心原则:约束条件是契约,破坏列表是诚信 优化器不是敌人,但你需要学会和它沟通

嗯,内联汇编这东西,说难不难,说简单也不简单。关键就三点:约束条件写对、破坏列表列全、优化器沟通好。把这三点做到位,内联汇编就是你手里的利器,而不是定时炸弹。


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