内联汇编:如何在 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语言和汇编语言之间的桥梁。它让你在享受C语言高级特性的同时,又能直接控制硬件。但记住:能力越大,责任越大。滥用内联汇编会让代码变得难以维护和移植。
我个人建议:先把C语言的优化做到极致,确认瓶颈确实在编译器生成的代码上,再考虑用内联汇编。而且,尽量把内联汇编封装成独立的函数,这样即使以后换平台,也只需要重写这几个函数就行。
最后送你一句话:内联汇编是工具,不是玩具。用对了地方,它是神器;用错了地方,它是灾难。