内联汇编精通:在 C 里直接发硬件指令 (Inline Assembly Mastery)
章节概述
前几章介绍了许多”C 语言无法表达的指令”——in/out、rdmsr/wrmsr、cpuid、lgdt/lidt。好消息是:你不必用纯汇编写整个程序来使用这些指令。GCC 的扩展内联汇编(extended asm)允许你在 C 代码中嵌入任意汇编指令,同时让编译器处理变量映射和优化。这就是本章的主题——从基础 asm("nop") 到复杂的多寄存器约束、asm goto、内存屏障和原子操作。如果你写过 C 驱动代码,你会在 C教程的 MMIO 章节 看到过简单的 asm volatile("":::"memory") —— 本章将其展开为完整的武器库。
核心理念:内联汇编是 C 与汇编之间的”接口”——你写的是汇编指令字符串,编译器负责将 C 变量映射到寄存器并插入到正确的执行流中。掌握约束语法(constraint)是精通内联汇编的关键——它决定了哪条指令用哪个寄存器、哪个变量被读写、哪些寄存器被破坏。
第一节:基础 asm vs 扩展 asm
1.1 两种内联汇编形式
// 形式一:基础 asm(Basic Asm)—— 极其简单,编译器"盲目"插入
asm("nop"); // 插入一条 nop 指令
asm("int $3"); // 插入断点(GDB 在此停下)
// 形式二:扩展 asm(Extended Asm)—— 带操作数、约束和 clobber
asm volatile (
"movl %1, %%eax\n\t" // 模板中 %% 转义 %
"addl %2, %%eax\n\t"
"movl %%eax, %0"
: "=r"(result) // 输出操作数列表
: "r"(a), "r"(b) // 输入操作数列表
: "eax", "cc" // clobber 列表(被破坏的寄存器)
);1.2 为什么需要 volatile
asm volatile 告诉编译器”即使结果看起来没用也不要优化掉这条 asm”:
// 不带 volatile: 编译器可能认为没有副作用而删除它
asm("rdtsc"); // 危险!编译器可能删除
// 带 volatile: 编译器必须保留,即使看似"无输出"
asm volatile("rdtsc" : "=a"(low), "=d"(high));
// 告诉编译器: EAX 和 EDX 被修改了,它们在 "=a" 和 "=d" 约束中
// 内存屏障: 无输出也没问题,volatile 禁止优化
asm volatile("mfence" ::: "memory");1.3 扩展 asm 的完整模板
asm [volatile] [goto] (
"汇编指令模板\n\t" ← 字符串,可用多行(\n\t 换行缩进)
: 输出操作数列表 ← [ [constraint]"变量名" ], ...
: 输入操作数列表 ← 同上
: clobber 列表 ← 被破坏的寄存器/条件码/内存
: goto 标签列表 ← 仅 asm goto,指定可能跳转的目标标签
);
每个部分都可以为空。最简形式:
asm volatile("cli" ::: "memory")—— 模板 + 只有 clobber 列表。
第二节:约束语法 —— 把 C 变量映射到汇编操作数
2.1 输出约束 (Output Constraints)
int result;
asm ("movl $42, %0" : "=r"(result));
// %0 = 第 0 个操作数 (result)
// "=r" = "写入" + "任意通用寄存器"
// 编译器选择哪个寄存器 → 用 %0 引用它
// 多个输出
int quotient, remainder;
asm ("divl %4"
: "=a"(quotient), "=d"(remainder) // %0=quotient(eax), %1=remainder(edx)
: "a"(dividend), "d"(0), "r"(divisor)); // %2=dividend(eax), %3=0(edx)
// 注意: 输入也参与编号。%0=quotient, %1=remainder, %2=dividend, %3=0, %4=divisor关键输出修饰符:
| 修饰符 | 含义 |
|---|---|
= | 只写输出(Write-only output) |
+ | 读写操作数(Read-Write)——既是输入也是输出 |
& | Earlyclobber —— 在读取所有输入操作数之前就被写入。防止编译器将此操作数分配到输入操作数所在的寄存器 |
// Earlyclobber 示例
asm ("movl %1, %0\n\taddl $1, %0"
: "=&r"(result) // & 防止 result 和 input 共享寄存器
: "r"(input));2.2 输入约束 (Input Constraints)
// "r" —— 任意通用寄存器
asm ("addl %1, %0" : "=r"(sum) : "r"(a), "0"(b));
// ^^^ "0" = 与第 0 个操作数使用同一寄存器
// 等价于: sum = b; sum += a;
// "i" —— 立即数整数(编译时常量)
asm ("int %0" :: "i"(0x80)); // 0x80 作为立即数嵌入指令
// "m" —— 内存操作数
int addr;
asm ("lgdt %0" :: "m"(gdt_desc)); // gdt_desc 必须是内存中的结构
// "g" —— 通用("rim"):寄存器/立即数/内存,编译器选择最方便的
asm ("movl %1, %0" : "=r"(x) : "g"(42));2.3 特定寄存器约束
| 约束 | 寄存器(x86) | 用途 |
|---|---|---|
a | eax / rax | 累加器、系统调用号、in/out 的 AL |
b | ebx / rbx | 基址寄存器 |
c | ecx / rcx | 计数器、rdmsr/wrmsr 的 MSR 索引 |
d | edx / rdx | 数据、rdmsr/wrmsr 的高 32 位 |
S | esi / rsi | 源索引 |
D | edi / rdi | 目标索引 |
A | eax:edx / rax:rdx | 64 位值拆分到两个 32 位寄存器 |
q | 8 位寄存器子集 (al, bl, cl, dl) | 字节操作 |
Nd | 0~255 立即数(in/out 端口约束专用) | in/out 的立即数端口号 |
// 用特定寄存器约束写 inb / outb
static inline uint8_t inb(uint16_t port) {
uint8_t value;
asm volatile ("inb %1, %0"
: "=a"(value) // AL 寄存器
: "Nd"(port)); // 端口号: 0-255 用立即数, 否则自动放入 DX
return value;
}
static inline void outb(uint16_t port, uint8_t value) {
asm volatile ("outb %0, %1"
:: "a"(value), // AL = 要写的数据
"Nd"(port)); // 端口号
}
// 用特定寄存器约束写 rdmsr
static inline uint64_t rdmsr(uint32_t msr) {
uint32_t low, high;
asm volatile ("rdmsr"
: "=a"(low), "=d"(high) // EAX 和 EDX
: "c"(msr)); // ECX = MSR 索引
return ((uint64_t)high << 32) | low;
}
// 用特定寄存器约束写 wrmsr
static inline void wrmsr(uint32_t msr, uint64_t value) {
uint32_t low = (uint32_t)(value);
uint32_t high = (uint32_t)(value >> 32);
asm volatile ("wrmsr"
:: "a"(low), "d"(high), "c"(msr));
}这些内联汇编封装就是 Linux 内核头文件
asm/msr.h的核心实现——Linux 内核用 1000+ 内联汇编来桥接 C 和硬件。
第三节:Clobber 列表 —— 告诉编译器谁被改了
3.1 三类 Clobber
// 寄存器 clobber: 告诉编译器哪些寄存器被 asm 修改了
asm volatile ("cpuid"
: "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx)
: "a"(leaf)
: ); // cpuid 修改了 EAX/EBX/ECX/EDX,但已在输出列表中声明
// 如果 asm 内部修改了不在输出列表中的寄存器,必须在 clobber 中声明:
asm volatile ("some_instruction"
: : : "rax", "rcx", "rsi"); // 这三个寄存器被破坏
// "cc" clobber: 告诉编译器条件码寄存器被修改
asm volatile ("addl %1, %0" : "=r"(x) : "r"(a), "0"(x) : "cc");
// addl 修改了 EFLAGS 的 ZF/SF/CF/OF 等位 → 添加 "cc"
// "memory" clobber: 告诉编译器"内存的内容可能被修改了"
asm volatile ("movl $0, %0" : "=m"(status_register) :: "memory");
// 编译器会假设"任何可间接访问的内存"都可能被 asm 改变
// → 强制编译器在 asm 前后刷新所有寄存器中缓存的内存值3.2 “memory” clobber 的内存屏障副作用
volatile int *mmio_register = (volatile int *)0xFED00000;
// 场景: 写命令寄存器后读状态寄存器
*mmio_register = 0x01; // 发送"开始 DMA"命令
// 问题: 编译器可能重排,把后面的读提前到这里之前!
asm volatile("" ::: "memory"); // 完整内存屏障
int status = *(mmio_register + 1); // 读取状态(保证在命令之后)
// 等价但更精确的做法:
asm volatile("sfence" ::: "memory");
// sfence 只屏障 store,不屏障 load关于内存屏障在裸机/设备驱动中的使用场景,详见 O 与 MMIO 的内存屏障节。
第四节:asm goto —— 内联汇编中的跳转
4.1 基础语法
asm goto 允许汇编代码跳转到 C 标签——这是实现 unlikely/likely 宏和硬件原子操作的基础:
// 基础语法
asm goto (
"cmp %1, %0\n\t"
"jne %l[label_true]\n\t" // %l[label] = 标签引用
: // 无输出操作数(asm goto 不允许输出)
: "r"(a), "r"(b)
: "cc" // clobber
: label_true // goto 标签列表
);
// 如果 a != b → 跳转到 label_true
printf("a == b\n");
return;
label_true:
printf("a != b\n");4.2 实际应用:原子自旋锁
// Linux 内核风格的原子 test-and-set 自旋锁
static inline int arch_spin_trylock(volatile int *lock) {
int oldval;
asm volatile (
"movl $1, %0\n\t"
"xchgl %0, %1"
: "=r"(oldval), "+m"(*lock)
: // 输入为空
: "memory"
);
return oldval == 0; // 0 = 成功获取锁
}
// 带退出的自旋(asm goto 版本)
static inline void arch_spin_lock(volatile int *lock) {
asm goto (
"1: lock; decl %0\n\t"
"jns %l[acquired]\n\t" // 如果不为负 → 成功获取
"2: pause\n\t" // 省电 + 减少总线争用
"cmpl $0, %0\n\t"
"jle 2b\n\t"
"jmp 1b"
: // 无输出(asm goto)
: "m"(*lock)
: "cc", "memory"
: acquired
);
acquired:
; // 锁已获取
}第五节:实战武器库 —— 常用内联汇编封装
5.1 CPUID 完整封装
#include <stdint.h>
#include <cpuid.h> // GCC 提供的内置 cpuid —— 但你应理解其实现
// 手动实现 —— 理解底层
static inline void cpuid(uint32_t leaf, uint32_t *eax, uint32_t *ebx,
uint32_t *ecx, uint32_t *edx) {
asm volatile ("cpuid"
: "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx)
: "a"(leaf));
}
// 调用
uint32_t eax, ebx, ecx, edx;
cpuid(1, &eax, &ebx, &ecx, &edx);
// ECX bit 28 = AVX, EDX bit 25 = SSE, EDX bit 26 = SSE25.2 RDTSC —— 高精度时间戳
static inline uint64_t rdtsc(void) {
uint32_t low, high;
asm volatile ("rdtsc" : "=a"(low), "=d"(high));
return ((uint64_t)high << 32) | low;
}
// 加内存屏障版本(防止指令重排)
static inline uint64_t rdtsc_ordered(void) {
uint32_t low, high;
asm volatile ("mfence\n\trdtsc" : "=a"(low), "=d"(high) :: "memory");
return ((uint64_t)high << 32) | low;
}5.3 停机和停中断
// 停机等待中断(仅在 Ring 0 可用)
static inline void hlt(void) {
asm volatile ("hlt" ::: "memory");
}
// 关中断(仅在 Ring 0 可用)
#define cli() asm volatile("cli" ::: "memory")
// 开中断(仅在 Ring 0 可用)
#define sti() asm volatile("sti" ::: "memory")
// 保存 EFLAGS 并关中断(禁用中断一段时间后恢复)
static inline unsigned long native_save_and_cli(void) {
unsigned long flags;
asm volatile ("pushfq\n\tpopq %0\n\tcli"
: "=r"(flags) :: "memory");
return flags;
}
static inline void native_restore_fl(unsigned long flags) {
asm volatile ("pushq %0\n\tpopfq" :: "r"(flags) : "cc", "memory");
}5.4 原子操作
// 原子地交换值 (lock xchg)
static inline uint32_t atomic_xchg(volatile uint32_t *ptr, uint32_t new_val) {
uint32_t old_val;
asm volatile ("lock; xchgl %0, %1"
: "=r"(old_val), "+m"(*ptr)
: "0"(new_val));
return old_val;
}
// 原子地比较并交换 (lock cmpxchg)
// 如果 *ptr == expected → *ptr = desired, 返回 expected
// 否则 返回 *ptr 的当前值
static inline uint32_t atomic_cmpxchg(volatile uint32_t *ptr,
uint32_t expected, uint32_t desired) {
uint32_t old;
asm volatile ("lock; cmpxchgl %2, %1"
: "=a"(old), "+m"(*ptr)
: "r"(desired), "0"(expected));
return old;
}
// 原子加法并返回旧值 (lock xadd)
static inline uint32_t atomic_fetch_add(volatile uint32_t *ptr, uint32_t val) {
asm volatile ("lock; xaddl %0, %1"
: "+r"(val), "+m"(*ptr)
:: "memory");
return val; // xadd 后 val 包含旧值
}
lock前缀告诉 CPU”这块内存总线归我用了”——在 SMP 系统中确保多核/多处理器看到一致的原子操作。这些指令构成了所有高级同步原语(互斥锁、自旋锁、信号量)的底层基础。
5.5 读/写 MSR(需 Ring 0)
// rdmsr / wrmsr 完整封装(与 Linux 内核 msr.h 一致)
static inline uint64_t __rdmsr(uint32_t msr) {
uint32_t low, high;
asm volatile ("rdmsr" : "=a"(low), "=d"(high) : "c"(msr));
return ((uint64_t)high << 32) | low;
}
static inline void __wrmsr(uint32_t msr, uint64_t value) {
uint32_t low = (uint32_t)(value);
uint32_t high = (uint32_t)(value >> 32);
asm volatile ("wrmsr" :: "a"(low), "d"(high), "c"(msr));
}第六节:NASM ↔ AT&T 转换速查
由于 GCC 内联汇编使用 AT&T 语法,而本教程的独立汇编程序使用 NASM(Intel)语法,这里提供快速转换表:
| 维度 | NASM (Intel) | AT&T (GCC 内联 asm) |
|---|---|---|
| 操作数顺序 | 目标, 源 : mov rax, 1 | 源, 目标 : mov $1, %rax |
| 寄存器前缀 | 无前缀 : rax, eax | % : %rax, %eax |
| 立即数前缀 | 无前缀 : mov rax, 42 | $ : mov $42, %rax |
| 内存引用 | [rax + 8] | 8(%rax) |
| 内存引用 + 偏移 | [rbx + rcx*4 + 16] | 16(%rbx, %rcx, 4) |
| 大小后缀 | mov dword [rax], 0 | movl / movw / movb |
| 内联 asm 中 % 转义 | — | %% 在字符串中写 % |
| 内联 asm 立即数端口 | in al, 0x60 | inb $0x60, %al / "inb %1, %0" |
| 字符串换行 | — | \n\t 分隔多条指令 |
// 同一个操作,两种语法
// NASM (Intel):
// mov rax, [rbx + 8]
// 等价 AT&T (GCC 内联 asm):
asm ("movq 8(%%rbx), %%rax" : : : "rax");
// %0, %1 等引用操作数时不需要 %%,%% 用于字面寄存器名
// 或者用操作数绑定(推荐):
asm ("movq 8(%1), %0" : "=r"(result) : "r"(ptr));
// 让编译器选择寄存器,更安全最佳实践:在内联 asm 中尽量让编译器通过约束选择寄存器,而非硬编码寄存器名。一方面可以避免 clobber 遗漏的 Bug,另一方面能充分利用编译器的寄存器分配优化。
第七节:内联汇编的陷阱和最佳实践
7.1 不要做的事
// 坏:修改寄存器但不在 clobber 中声明
asm volatile ("movl $42, %%ecx"); // 编译器不知道 ecx 被改 → 数据破坏
// 坏:修改内存但不用 "memory" clobber
int global_flag;
asm volatile ("movl $1, %0" :: "m"(global_flag));
// 后面 if (global_flag) ... 编译器可能用缓存的旧值
// 坏:假设变量在特定寄存器中
register int x asm("eax"); // register asm 声明不稳定
asm ("addl $1, %%eax" ::: "eax");
// 好:让约束系统分配
asm ("addl $1, %0" : "+r"(x)); // 编译器选寄存器,不需要指定 eax7.2 必须做的事
// 对:volatile + "memory" 防止编译器优化掉或重排
asm volatile ("wbinvd" ::: "memory"); // 写回并失效缓存——不可省略
// 对:使用 earlyclobber (&) 防止输出/输入寄存器冲突
int result;
asm ("movl %1, %0\n\taddl $1, %0"
: "=&r"(result) // & 关键: result 在读取 input 之前就被写入
: "r"(input));
// 对:"cc" clobber 在修改了 EFLAGS 时
asm ("addl %1, %0" : "+r"(x) : "r"(y) : "cc");
// addl 修改了 ZF/SF/CF/OF 等条件码 → 必须声明 "cc"7.3 性能提示
内联汇编会禁用部分编译器优化——编译器无法分析 asm 字符串的内容,因此将 asm 视为”黑盒”:
- 编译器不会通过 asm 重排指令(除非带
volatile的 asm 之间有依赖) - 编译器不在 asm 前/后缓存内存值(使用
"memory"clobber 时) - 编译器不能将 asm 内的操作与其他指令融合
原则:只有真正需要用汇编才能做到的才用内联汇编(端口 I/O、MSR、控制寄存器、特殊指令)。能用 C 标准函数或编译器 intrinsic(
__builtin_*)实现的就不要手写内联汇编。
小节练习
章节测试
一、判断题
判断题 1
asm volatile中的volatile关键字与 C 变量的volatile完全相同——都表示”不要优化掉”。 ( )
正确
错误
点击查看答案 解析: 概念类似但作用域不同。
asm volatile告诉编译器不要删除或移动该 asm 块(即使看起来无副作用)。volatile int x告诉编译器每次读取 x 必须从内存重读。两者都阻止优化但作用于不同对象。答案: 部分正确但语义不同
判断题 2
在内联汇编中使用
rdmsr指令时,MSR 索引必须放在 ECX 寄存器中——可以通过"c"约束保证这一点。 ( )
正确
错误
点击查看答案 解析:
rdmsr和wrmsr的硬件约定是 ECX = MSR 地址。"c"约束确保变量落入 ECX/RCX 寄存器。同理,"a"→ EAX,"d"→ EDX。答案: 正确
判断题 3
asm goto 可以有输出操作数,与普通扩展 asm 一样。 ( )
正确
错误
点击查看答案 解析: asm goto 不能有输出操作数。这是 GCC 的限制——因为控制流可能跳过 asm 块(跳转到标签),编译器无法保证输出操作数在每条跳转路径上都写入。
答案: 错误
判断题 4
在内联汇编中写
"=r"(result),编译器会将result变量放到某个通用寄存器中,但程序员不知道具体是哪个寄存器——只能通过%0引用。 ( )
正确
错误
点击查看答案 解析:
"=r"约束让编译器选择寄存器——程序员使用%0(第 0 个操作数) 引用它,无需知道实际是 rax、rbx 还是 rcx。编译后可通过反汇编看到最终选择。答案: 正确
判断题 5
使用
"memory"clobber 后,编译器的性能没有损失——因为在读取内存时总是从缓存中获取。 ( )
正确
错误
点击查看答案 解析:
"memory"clobber 强制编译器在 asm 前后刷新所有缓存在寄存器中的内存值,这会降低优化效果。这就是为什么内联汇编应"用最少的影响"——精确的 clobber 列表(如只声明"rax"而非"memory")可减少性能损失。答案: 错误
动手练习题
练习题 1:用内联 asm 实现 CPUID 检测
难度: 简单
编写一个 C 程序,用 GCC 内联 asm 实现
cpuid查询并打印以下信息:
- CPU 厂商字符串(“GenuineIntel” 或 “AuthenticAMD”)
- CPU 型号、家族和步进
- 是否支持 MMX、SSE、SSE2、SSE3、SSSE3、SSE4.1、SSE4.2、AVX、AVX2
要求:每条 cpuid 调用都写成独立的内联 asm 函数(如
cpuid_leaf1()),不得使用__builtin_cpu_supports或cpuid.h。
练习题 2:内联 asm rdtsc 计时器
难度: 简单
用内联 asm 封装
rdtsc指令,测量一个纯 Cfor循环(循环 1000 万次空操作)的 CPU 周期数。对比以下三种情况:
- 不加任何内存屏障
- 在 rdtsc 前加
mfence- 在 rdtsc 前加
lfence(Intel 推荐的方式)观察三种情况的时间差异——理解指令重排和内存屏障的真实影响。
练习题 3:Linux 用户态端口 I/O
难度: 简单
编写一个需要 root 权限运行的 C 程序,使用
ioperm()获取端口 0x80(POST 调试端口)的访问权限,然后用内联 asm 的outb向该端口写入值。验证:
ioperm()调用成功后内联 asm 的outb能正常执行ioperm()未调用时直接执行同样的内联 asm → 段错误(SIGSEGV)这题让你体验”用户态通过内联汇编执行特权指令”的边界限制——
ioperm()是内核授予的临时权限。
练习题 4:原子计数器
难度: 简单
用
lock xaddl内联 asm 实现一个无锁的原子计数器(atomic_inc),并用它协调两个线程各自加 1000 万次。对比:
- 使用内联 asm
lock xaddl的原子版本- 使用普通
++的非原子版本观察非原子版本的结果是否正确(在多核系统上通常不正确)。输出两个版本的结果差异——理解硬件的 lock 前缀为什么是不可替代的。
练习
以下题目从汇编/底层视角训练相关能力:
| 题号 | 题目 | 链接 | 涉及知识点 |
|---|---|---|---|
| 371 | 两整数之和 | https://leetcode.cn/problems/sum-of-two-integers/ | 位运算(add指令原理) |
| 201 | 数字范围按位与 | https://leetcode.cn/problems/bitwise-and-of-numbers-range/ | 位移与位操作 |
| 190 | 颠倒二进制位 | https://leetcode.cn/problems/reverse-bits/ | 位操作、嵌入式模式 |