时间轴
时间轴
2025-09-27
init
本文介绍了GCC内嵌汇编(Inline Assembly)的两种模式:基础内嵌汇编与扩展内嵌汇编。基础内嵌汇编使用asm关键字,将汇编代码作为字符串嵌入C语言,但GCC不解析其内容,且优化器可能移动指令顺序。扩展内嵌汇编则通过asm关键字配合修饰词(volatile、inline、goto)以及输出部、输入部、损坏部(Clobbers)来精确控制汇编与C变量的交互。文章详细说明了输出部与输入部的约束条件(如“=”表示只写,“+”表示可读可写,“r”表示通用寄存器),以及损坏部用于告知编译器寄存器、条件码或内存可能被修改,从而防止优化器重排或缓存变量。还介绍了操作数编号规则(从0开始,先输出后输入)、约束修饰符在ARM64下的差异,以及通过汇编符号名引用变量的方法。此外,文章通过实验演示了用内嵌汇编实现memcpy、memset、原子操作、系统寄存器读写等功能,并探讨了与宏结合的高级用法,包
参考文档:
内嵌汇编(Inline Assembly Language)在 C 语言中嵌入汇编代码
目的:
- 优化:针对特定重要代码(time-sensitive)进行优化
- C 语言需要访问某些特殊指令来实现特殊功能,比如内存屏障指令
内嵌汇编两种模式
- 基础内嵌汇编
- 扩展内嵌汇编
基础内嵌汇编
格式:
1 | asm asm-qualifiers(AssemblerInstructions) |
asm 关键字:表明这是一个 GNU 扩展
修饰词(qualifiers)
- volatile:在基础内嵌汇编中通常不需要这个修饰词
- inline:内联,asm 汇编的代码会尽可能小
汇编代码块(AssemblerInstructions)
GCC 编译器把内嵌汇编当成一个字符串
GCC 编译不会去解析和分析内嵌汇编
多条汇编指令,需要使用”\n\t“来换行
GCC 的优化器,可以移动汇编指令的前后位置。如果你需要保存汇编指令的顺序,最好使用多个内嵌汇编的方式

扩展内嵌汇编

- 格式
- asm 关键字:表明这个是一个 GNU 扩展
- 修饰词(asm-qualifiers)
- volatile:用来关闭 GCC 优化
- inline 内联,asm 汇编的代码会尽可能小
- goto 在内嵌汇编结束后会跳转到 C 语言的标签里

输出部:
用于描述在指令部中可以被修改的 C 语言变量以及约束条件
- 每个输出约束通常以"="开头,接着的字母表示对操作数类型的说明,然后是关于变量结合的约束
- 输出部通常使用"=“或者”+“作为输出约束,其中”=“表示被修饰的操作数只写(原值在汇编执行前已死,可写入寄存器)“,”+"表示被修饰的操作数可读可写(输入的初值会被保留并使用)"
- 输出部可以是空的
1 | "=/+" + 约束修饰符 + 变量 |
约束修饰符常用的包括:
- ‘r’ A register operand is allowed provided that it is in a general register.
输入部
用来描述在指令部只能被读取访问的 C 语言变量以及约束条件
- 输入部描述的参数是只有只读属性,不要试图修改输入部的参数内容,因为 GCC 编译器假定,输入部的参数的内容在内嵌汇编之前和之后都是一致的
- 在输入部中不能使用"="或者”+“约束条件,否则编译器会报错
- 输入部可以是空的
损坏部(Clobbers)
- 汇编代码可能修改除了输出之外的寄存器。
- 如果不告诉编译器,会导致编译器认为这些寄存器保持不变,最终可能出现错误或不可预测的行为。
- Clobber 也是一种内存屏障(尤其
"memory"),确保编译器不会重排或缓存变量。
| 名称 | 作用 |
|---|---|
"x0","x1", … | 通用寄存器被修改 |
"cc" | 条件码寄存器(flags)被修改 |
"memory" | 汇编访问了内存,保证编译器刷新寄存器到内存并重新加载 |
"redzone" | 在 x86-64 的 redzone 区域使用栈空间 |
"memory"告诉 GCC 编译器内联汇编指令改变了内存中的值,强迫编译器在执行该汇编代码前将所有缓存在寄存器中的值写回内存,执行完后再重新加载,目的是防止编译器重排内存访问"cc"表示内嵌汇编代码修改了条件码/状态寄存器相关的标志位
不允许在 clobber 列表里写 栈指针寄存器(esp/rsp)。
不要重复列出输出寄存器。
四种可以情况可以组合,用逗号分割即可
- 指令部的参数表示
- 操作数编号从 0 开始,先依次列出所有输出部操作数,再依次列出所有输入部操作数。因此 %0 是第一个输出部操作数(若无输出部则为第一个输入部操作数),%1 为第二个,以此类推


输出和输入部的约束修饰符

输出部和输入部的约束修饰符——通用

输出部和输入部的约束修饰符——ARM64

汇编符号名字来代替前缀%
%[name]→ 引用约束变量%w[name]→ 引用 低 32 位寄存器(如w0, w1)%x[name]→ 引用 完整 64 位寄存器(如x0, x1)

实验 1:实现简单的 memcpy 函数


陷阱与坑
- GDB 无法以源码方式单步进入内嵌汇编的每条指令(
stepi可在汇编层面单步,但无法与源码行一一对应) - 输出部和输入部的修饰符不能用错,否则程序会跑错
实验 3:使用内嵌汇编实现 memset 函数

内嵌汇编的高级玩法:和宏结合
- 技巧 1:使用了 C 语言的#运算符。在带参数的宏中,“#”运算符作为一个预处理运算符,可以把记号转换为字符串

123456789 |
可以用这个宏来生成多个原子操作函数:
12 | ATOMIC_OP(add, "addl")ATOMIC_OP(sub, "subl") |
这会展开为:
12345678910111213 | static inline void atomic_add(int i, atomic_t *v) { __asm__ __volatile__( "addl %1, %0" : "+m" (v->counter) : "ir" (i));}static inline void atomic_sub(int i, atomic_t *v) { __asm__ __volatile__( "subl %1, %0" : "+m" (v->counter) : "ir" (i));} |
注意asm_op是宏参数,在宏展开时会替换为你提供的值,且调用时必须传入字符串字面量(带双引号),如"addl",这样它才能作为指令字符串直接嵌入汇编模板:
1 | ATOMIC_OP(add, "addl") |
宏展开后是:
12345 | __asm__ __volatile__( "addl %1, %0" : "+m" (v->counter) : "ir" (i)); |
如果错误地传入不带引号的
addl,它会被当作标识符而非字符串,展开后变成addl "%1, %0"导致汇编语法错误。
##name—— 符号连接(token pasting)运算符
把宏参数和前后的标识符直接拼接成一个新的标识符(不是字符串)。
常用来生成变量名、函数名等。
123 | MAKE_FUNC(test); // 展开成:void func_test(void) {} |
实验 4:使用内嵌汇编与宏的结合


实验 5:实现读写系统寄存器的宏


这里用到了GNU C 的语句表达式 (statement expression) 语法:
({ ... })是什么
这是 GNU 扩展,不是标准 C。
它让一段代码块既能像语句一样执行,也能返回一个值。
语法规则:
({ statement1; statement2; ...; expression; })
代码块里最后的那个 表达式(不加分号)就是返回值。

内嵌汇编:goto
内嵌汇编的 goto 模板,可以跳转到 C 语言的 label 标签里

- Goto 模板的输出部必须为空
- 新增一个 gotolabels 的部,里面列出了 C 语言的 label,是允许跳转的 label

实验 6:goto 模板的内嵌汇编


%l[label]是 GCC 内联汇编中asm goto语法的特殊写法,表示跳转到 C 代码中的标签label。
详细解释:
%l[...]是告诉编译器这是一个标签符号(label),而不是普通的寄存器或立即数。label是你在 C 代码里定义的标签名,比如你代码中的label:。asm goto允许汇编代码通过条件跳转直接跳转到 C 代码中的某个标签,实现条件分支。
