内联汇编:如何在 C 代码中嵌入汇编?

说实话,很多嵌入式工程师一听到「内联汇编」就头大。觉得这东西又难又容易出bug,能不用就不用。但我个人的看法是:内联汇编是嵌入式开发的最后一张王牌。你平时可能用不上,但真到了性能瓶颈或者需要直接操作特殊指令的时候,它就是你唯一的出路。

我在项目中遇到过好几次这样的情况:C语言编译出来的代码,就是差那么几个时钟周期。优化等级开到最高也没用。最后没办法,只能手写几行汇编,问题瞬间解决。嗯,这就是内联汇编的价值所在。

什么是内联汇编?

说白了,就是在C代码里直接插入汇编指令。GCC编译器提供了 asm() 关键字来实现这个功能。基本语法长这样:

asm("汇编指令");

比如你想在C代码里执行一条 NOP(空操作)指令:

asm("nop");

就这么简单。但实际项目中,我们往往需要让汇编代码和C代码交互——比如把C变量的值传给汇编,或者把汇编的结果存回C变量。这时候就需要用到扩展内联汇编了。

扩展内联汇编的完整语法

GCC的扩展内联汇编格式如下:

asm [volatile] (
    "汇编指令"
    : 输出操作数
    : 输入操作数
    : 被破坏的寄存器列表
);

看着有点复杂?我拆开给你讲。

部分 说明
asm 内联汇编关键字
volatile 告诉编译器不要优化这段汇编(强烈建议加上)
汇编指令 你要执行的汇编代码,用 %0、%1 等引用操作数
输出操作数 汇编结果要写回到哪些C变量
输入操作数 C变量作为输入传给汇编
被破坏的寄存器 汇编中修改了哪些寄存器,需要告诉编译器

一个完整的例子

咱们来看一个实际例子:用内联汇编实现两个整数的加法。

#include <stdio.h>

int main() {
    int a = 10, b = 20, result;

    asm volatile (
        "add %0, %1, %2"
        : "=r" (result)
        : "r" (a), "r" (b)
    );

    printf("结果: %d\n", result);
    return 0;
}

这里 %0 对应第一个操作数(输出),%1 对应第二个(输入a),%2 对应第三个(输入b)。"=r" 表示输出操作数使用通用寄存器,"r" 表示输入操作数也使用通用寄存器。

小提示:我个人习惯在每条内联汇编后面都加上 volatile。因为编译器有时候会自作主张地把你的汇编优化掉,尤其是当它觉得结果没被用到的时候。加了 volatile 就是告诉编译器:「别动我的代码,我有我的理由。」

操作数约束符

约束符是内联汇编里最容易搞混的地方。我列个表给你看:

约束符 含义
"r" 通用寄存器
"m" 内存地址
"i" 立即数(编译时常量)
"=r" 输出操作数,只写
"+r" 输入输出操作数,可读可写
"=&r" 早期破坏操作数(先写后读)

你想想看,为什么要有 "=&r" 这种奇怪的约束?

这是因为编译器在分配寄存器时,可能会把输入和输出分配到同一个寄存器。如果你的汇编指令在读取输入之前就修改了输出,那输入值就被覆盖了。加上 "=&" 就是告诉编译器:「这个输出操作数在读取所有输入之前就会被写入,别给我共用寄存器。」

注意:我曾经在一个项目中因为没加 "=&r" 导致了一个极其隐蔽的bug。代码在调试模式下运行正常,一开优化就出错。查了两天才发现是寄存器冲突的问题。从那以后,只要输出操作数在输入之前被写入,我一定加上 "=&"

被破坏的寄存器列表

这个列表告诉编译器:你的汇编代码修改了哪些寄存器,编译器需要保存和恢复它们。

asm volatile (
    "mov r0, #0x01\n\t"
    "str r0, [%0]"
    :
    : "r" (addr)
    : "r0", "memory"
);

这里我们修改了 r0 寄存器,还修改了内存(通过 str 指令),所以需要在破坏列表里声明 "r0""memory"

"memory" 这个约束特别重要。它告诉编译器:「这段汇编修改了内存,你之前缓存的值可能已经失效了,重新读吧。」如果不加这个,编译器可能会使用过期的缓存值,导致逻辑错误。

内联汇编的常见应用场景

我总结了一下,内联汇编主要用在以下几个地方:

  • 特殊指令操作:比如 ARM 的 CPS(修改处理器状态)、WFI(等待中断)等C语言无法直接表达的指令
  • 性能关键代码:比如 DSP 算法中的乘累加操作,手写汇编能比编译器优化快 2-3 倍
  • 原子操作:比如 LDREX/STREX 实现的无锁编程
  • 寄存器直接访问:比如读取 CPU 的周期计数器

举个例子,读取 ARM Cortex-M 系列的系统节拍计数器:

uint32_t get_systick(void) {
    uint32_t val;
    asm volatile (
        "mrs %0, SYST_CVR"
        : "=r" (val)
    );
    return val;
}

这种操作在C语言里没法直接做,必须靠内联汇编。

内联汇编的陷阱

嗯,这里要注意几个容易踩的坑:

  • 不要乱改寄存器:如果你修改了某个寄存器但没有在破坏列表中声明,编译器会崩溃给你看
  • 注意指令格式:不同架构的汇编语法不同,ARM 用 %0,x86 用 %%eax,别搞混了
  • 小心编译器优化:即使加了 volatile,编译器也可能调整汇编代码的顺序(除非你加了内存屏障)
  • 可移植性差:内联汇编是架构相关的,换一个平台就得重写
核心原则:能用C语言解决的问题,就不要用内联汇编。内联汇编是最后的手段,不是第一选择。我见过太多人为了炫技而滥用内联汇编,结果代码维护成本暴增。

知识体系图

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

内联汇编知识体系 内联汇编 asm() 基本语法 asm("指令") asm volatile("指令") 扩展语法 输出操作数 : "=r"(var) 输入操作数 : "r"(var) 破坏列表 : "r0","memory" 约束符 "r" 通用寄存器 "m" 内存地址 "i" 立即数 "=&r" 早期破坏 应用场景 常见陷阱

总结

内联汇编是C语言和汇编语言之间的桥梁。它让你在享受C语言高级特性的同时,又能直接控制硬件。但记住:能力越大,责任越大。滥用内联汇编会让代码变得难以维护和移植。

我个人建议:先把C语言的优化做到极致,确认瓶颈确实在编译器生成的代码上,再考虑用内联汇编。而且,尽量把内联汇编封装成独立的函数,这样即使以后换平台,也只需要重写这几个函数就行。

最后送你一句话:内联汇编是工具,不是玩具。用对了地方,它是神器;用错了地方,它是灾难。


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