内联汇编就像一把尖刀,它允许我们在C的高级抽象和CPU底层硬件之间自由穿梭~
1. 什么时候用到?
嵌入式开发,经常有这样的场景:
-
访问CPU特殊寄存器, 比如通过
CPSR切换工作模式,通过CP15配置协处理器(MMU、缓存等) -
通过
nop指令精准延时 -
中断的入口封装、现场保护等
-
特殊硬件指令比如
swi等
这个时候,在C/C++编程中,就需要使用到内联汇编去处理这样的场景。
2. 核心语法
C
__asm__ volatile (
"汇编指令模板\n\t" /* 第1部分:你要执行的指令 */
: 输出操作数列表 /* 第2部分:C 变量 <- 寄存器 (由内向外写) */
: 输入操作数列表 /* 第3部分:寄存器 <- C 变量 (由外向内读) */
: 破坏描述部(Clobber)/* 第4部分:告诉编译器你弄脏了哪些东西 */
);
举例1,
C
unsigned long cpsr, out, in = 1;
__asm__ volatile (
// 读取cpsr
"mrs %0, cpsr\n\t"
// 禁用IRQ/FIQ, 切换至IRQ模式
"msr cpsr_c, #0b11010010\n\t"
// 设置IRQ模式的SP
"ldr sp, =0x40001000\n\t"
// 恢复原来模式和原始中断状态
"msr cpsr, %0\n\t"
// 纯凑数
"mov %1, %2\n\t"
: "=&r" (cpsr), "=r" (out)
: "r" (in)
: "sp", "cc", "memory"
);
举例2,
C
void restore_irq_mask(unsigned long old_cpsr)
{
__asm__ volatile (
"msr cpsr, %0\n\t"
:
: "r" (old_cpsr)
: "cc", "memory"
);
}
2.1 汇编指令模板
就是ARM汇编指令, 但是操作数变成了占位符(%0, %1, %2...)
多天指令之间用\n\t分割
占位符对应输出和输入列表中出现的顺序, 参见举例1中的占位符用法
2.2 操作数约束
上述举例中在输出和输入部分有这样的符号组合,有些是修饰符(只用于输出部分),有些是约束字符
C
: "=&r" (cpsr), "=r" (out)
: "r" (in)
| 约束符号 | 说明 |
|---|---|
| "r" | 让编译器随便挑一个通用寄存器(R0-R12),把变量放进去 |
| "m" | 内存操作数。告诉编译器这个变量在内存里,不要放进寄存器 |
| "I" | 立即数 |
| 修饰符 | 说明 |
|---|---|
| "=" | 只写(Write-only)。表示这条汇编会把新值写入这个 C 变量,变量之前的值不重要了 |
| "+" | 读写(Read-write)。表示这条汇编既会先读后写这个 C 变量(此时变量即是输入数又是输出数) |
| "&" | 早占(Earlyclobber)。这是一个高级且救命的修饰符!它告诉编译器:"在这个汇编块执行完之前,这个输出寄存器就已经被写入了。所以,千万不要把任何输入变量分配到同一个物理寄存器上,否则输入数据会被提前覆盖!"比如在上述举例中"=&r" (cpsr),cpsr如果使用了编译器自动分配的寄存器r2,那么"r" (in)就不会分配给r2,以免r2被提前写入发生值改变,让in失效 |
2.3 破坏描述部
编译器可能会聪明过头,悄悄把很多C变量还存在寄存器中。如果汇编中修改了某些寄存器又不告诉编译器,那么执行就可能出现问题。
那么就需要用一些口令告诉编译器你修改了什么部分。
| 破坏描述符 | 说明 |
|---|---|
| "cc" | 告诉编译器,这段汇编修改了 CPSR 的标志位(N, Z, C, V),比如你用了 adds 或 cmp 指令。编译器之后就不会依赖之前的条件判断了 |
| "memory" | 内存屏障。告诉编译器,这段汇编偷偷修改了内存。编译器会强制将缓存在寄存器里的所有变量重新写回内存,并在汇编执行后重新从内存读取,防止读到脏数据 |
| 具体寄存器名(如 "r1", "lr") | 如果你在汇编里硬编码(写死)使用了某个物理寄存器,必须在这里声明,编译器就会在执行汇编前把那个寄存器压栈保护起来 |