C 语言内联汇编入门
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。C 已经足够接近硬件了,但有些事 C 表达不了——比如读 CPU 的时间戳计数器、执行特定的 SIMD 指令、精确控制内存屏障。这时要用内联汇编,把汇编指令直接嵌进 C 代码。本文讲清 GCC 内联汇编的语法和适用边界。
本文要回答的问题
- 什么时候需要内联汇编?普通 C 做不到吗?
- GCC 的
__asm__语法怎么读?约束(constraint)和 clobber 是什么? - 内联汇编有什么坑?为什么说"别轻易用"?
一、什么时候需要内联汇编
绝大多数场景不需要内联汇编——编译器生成的代码已经足够好。但少数场景 C 表达不了:
| 场景 | 为什么 C 做不到 | 例子 |
|---|---|---|
| 读 CPU 特殊寄存器 | 时间戳计数器 TSC 没有 C 运算符对应 | rdtsc |
| 精确内存屏障 | 需要 mfence/lfence 等特定指令 | 无锁编程 |
| 特定 SIMD 指令 | 内建函数(intrinsic)覆盖不到的新指令 | 部分 AVX 指令 |
| 内核/驱动代码 | 操作特权指令、IO 端口 | in/out |
首选 intrinsic(内建函数):如
__builtin_prefetch、_mm_*,它们比手写内联汇编安全、可移植。只有 intrinsic 覆盖不到时,才考虑内联汇编。
二、GCC 内联汇编语法
2.1 基本结构
c
__asm__ volatile (
"汇编指令模板"
: 输出操作数 // 可选
: 输入操作数 // 可选
: 破坏的寄存器列表 // 可选(clobber)
);2.2 实测:用 rdtsc 读时间戳计数器
c
#include <stdint.h>
static inline uint64_t rdtsc(void) {
unsigned int lo, hi;
__asm__ volatile ("rdtsc" : "=a"(lo), "=d"(hi));
return ((uint64_t)hi << 32) | lo;
}逐部分拆解:
| 部分 | 含义 |
|---|---|
"rdtsc" | 汇编指令:读时间戳计数器,结果放 edx:eax(高 32 位在 edx,低 32 位在 eax) |
"=a"(lo) | 输出约束:eax 寄存器的值写入变量 lo(= 表示只写,a 表示 eax) |
"=d"(hi) | 输出约束:edx 的值写入变量 hi |
volatile | 告诉编译器"别优化掉这条指令"(即使结果看似没用) |
实测运行:
text
; 两次 rdtsc 间隔
两次 rdtsc 间隔 52 个时钟周期rdtsc 常用于精确计时(性能基准测试),比 clock() 精度高得多(直接数 CPU 周期)。
2.3 约束(constraint)速查
约束告诉编译器"这个操作数放哪个寄存器/内存":
| 约束 | 含义 |
|---|---|
a | eax/rax |
b | ebx/rbx |
c | ecx/rcx |
d | edx/rdx |
S / D | esi/rsi、edi/rdi |
r | 任意通用寄存器 |
m | 内存地址 |
i | 立即数常量 |
2.4 clobber 列表
: 第三部分声明"这条汇编破坏了哪些寄存器",让编译器知道要保存/恢复:
c
__asm__ volatile ("cpuid"
: "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(leaf));cpuid 会破坏 eax/ebx/ecx/edx 四个寄存器,全都要在 clobber 里声明。
三、内联汇编的坑
| 坑 | 说明 |
|---|---|
| 可移植性差 | 语法是 GCC 扩展,且 x86/ARM 指令完全不同,换平台要重写 |
| 编译器交互复杂 | 约束写错会导致变量读到错寄存器、优化后结果错乱 |
| 易破坏寄存器 | 漏写 clobber,编译器以为寄存器没变,产生隐蔽 bug |
| 阻碍优化 | 内联汇编是编译器的"黑盒",阻断其跨指令优化 |
四、与本站主线衔接
- 性能基准测试:
rdtsc精确计时是基准测试的基础,见 基准测试方法论。 - 内存屏障:
mfence/lfence内联汇编与原子操作,见 内存屏障与原子。 - 汇编对照:内联汇编与纯汇编的关系,见 与汇编、性能剖析衔接。
- 编译优化:
volatile如何阻止优化器动这条指令,见 编译优化行为。
一句话总结
内联汇编 = 把特定指令(如 rdtsc)嵌进 C 的 __asm__,靠约束(=a/=d)绑定变量到寄存器、clobber 声明破坏的寄存器;它是"intrinsic 覆盖不到"时的最后手段,功能强但可移植性差、易踩坑,能不用就不用。