C语言内联汇编
一个示例
1 |
|
代码片段的格式和含义分解:
这段代码使用了 GCC 扩展内联汇编的通用格式,其基本结构如下:
1 |
|
asm volatile(
asm
: 关键字,表示开始内联汇编代码块。volatile
: 可选关键字。使用volatile
的目的是告诉编译器,这段内联汇编代码具有副作用(例如,修改了内存或影响了程序状态),不要对这段代码进行任何优化,每次调用都必须严格按照代码编写的顺序执行。在跳转指令这种场景下,通常需要使用volatile
,以确保跳转行为不被编译器优化掉。
"jalr x0, 0(%0)"
- 这是 汇编指令模板,是一个字符串,包含了实际的 RISC-V 汇编指令。
jalr x0, 0(%0)
: 这是 RISC-V 的 跳转指令。jalr
: 指令名,表示 “Jump and Link Register”。 它会执行跳转,并将返回地址(跳转指令的下一条指令的地址)保存在指定的寄存器中,但在这里,由于目标寄存器是x0
(zero 寄存器),所以实际上忽略了返回地址的保存,它只执行跳转功能。x0
: 目标寄存器,用于存放返回地址。 在这里被指定为x0
,表示不保存返回地址,只进行跳转。0(%0)
: 跳转目标地址的计算方式。%0
: 这是一个**占位符 (Placeholder)**,用于在汇编指令模板中引用 C 语言的变量。%0
表示 第一个操作数 (这里的第一个操作数指的是后面输入操作数列表
中的第一个操作数)。(%0)
: 表示 间接寻址。 它会将%0
所代表的寄存器中的值,作为内存地址,并从该内存地址读取数据。0(%0)
完整含义: 表示跳转目标地址是:寄存器%0
中存储的地址值 + 偏移量 0。 实际上就是直接使用寄存器%0
中的值作为跳转目标地址。
: // 输出操作数: 无
- 输出操作数列表: 位于第一个冒号
:
之后,用于指定汇编指令的输出操作数。 :
之后为空: 表示这段汇编代码 没有输出操作数,即汇编指令执行后,不会将结果值写回到 C 语言的变量中。
- 输出操作数列表: 位于第一个冒号
: "r"(jump_address) // 输入操作数: jump_address (C变量) 映射到寄存器
- 输入操作数列表: 位于第二个冒号
:
之后,用于指定汇编指令的输入操作数。 "r"(jump_address)
: 指定了一个输入操作数。"r"
: 约束 (Constraint) 字符串。"r"
约束告诉编译器,将jump_address
这个 C 变量 分配到一个通用寄存器 (register)。 编译器会选择一个合适的通用寄存器,并将jump_address
的值加载到这个寄存器中,然后在汇编指令模板中使用%0
占位符来引用这个寄存器。(jump_address)
: C 语言表达式。jump_address
是一个 C 变量,它提供了汇编指令所需的输入值,即跳转的目标地址。
- 输入操作数列表: 位于第二个冒号
: "memory" // 破坏性操作: 无
- 破坏列表 (Clobber List): 位于第三个冒号
:
之后,用于告诉编译器,这段内联汇编代码可能会修改某些寄存器或内存位置,从而阻止编译器进行某些可能导致错误的优化。 "memory"
: 破坏描述符 (Clobber Descriptor)。"memory"
告知编译器,这段内联汇编代码可能会修改内存中的数据。 即使代码中没有显式地写内存操作指令,但如果汇编代码的执行可能导致内存状态改变(例如,跳转到未知的代码区域,该区域的代码可能会修改内存),也需要使用"memory"
来保守地告诉编译器,防止编译器做出错误的假设。 在本例中,虽然jalr
指令本身不直接修改内存,但由于它是一个跳转指令,可能会跳转到任意地址的代码执行,为了安全起见,使用"memory"
是一个好的实践。: "memory"
完整含义: 表示这段内联汇编代码可能会修改内存。
- 破坏列表 (Clobber List): 位于第三个冒号
代码功能:
这段内嵌汇编代码的功能是: **执行一个间接跳转 (register jump)**。 跳转的目标地址 由 C 变量 jump_address
的值来指定。 代码会将 jump_address
的值加载到一个寄存器中,然后使用 jalr x0, 0(register)
指令,以寄存器中的值作为目标地址进行跳转,但不保存返回地址。 由于使用了 volatile
和 "memory"
clobber,编译器会保证这段代码被严格执行,并且不会因为优化而产生意外的行为。 这种代码通常用于需要直接控制程序流程,例如跳转到特定的地址执行某些初始化代码、处理异常、或者实现某些特殊的控制流逻辑等场景。
基本语法格式
1 |
|
asm
: 关键字,开始内联汇编块。[volatile]
: 可选,volatile
关键字告知编译器不要优化这段汇编代码。"assembly code template"
: 汇编指令字符串,可以包含多条指令,指令之间用分号;
或换行符\n
分隔。 可以使用占位符%0
,%1
,%2
… 引用操作数。: [output operands]
: 可选,输出操作数列表。 每个输出操作数形如"[约束](C 变量)"
,多个操作数用逗号,
分隔。: [input operands]
: 可选,输入操作数列表。 每个输入操作数形如"[约束](C 表达式)"
,多个操作数用逗号,
分隔。: [clobber list]
: 可选,破坏列表。 用双引号" "
括起来的寄存器名或特殊符号(如"memory"
),多个破坏描述符用逗号,
分隔。
操作数和约束 (Operands and Constraints)
操作数用于在 C 代码和汇编代码之间传递数据。 约束字符串用于指定操作数的类型和位置 (寄存器、内存等)。
常用约束 (RISC-V GCC 常用约束,更完整的约束列表请查阅 GCC RISC-V 扩展文档):
r
: 通用寄存器 (General-purpose register)。 编译器会为操作数分配一个合适的通用寄存器 (例如x1
-x31
)。i
: 立即数 (immediate value)。 操作数是一个编译时可知的立即数 (整数常量)。f
: 浮点寄存器 (Floating-point register) (用于浮点操作)。 RISC-V 32 位架构中,通常使用扩展指令集 ‘F’ 或 ‘D’ 支持浮点运算。m
: 内存操作数 (memory operand)。 操作数是一个内存地址。I
: 0-31 范围内的立即数 (适用于移位指令的移位量)。
操作数占位符:
- 在汇编指令模板中,使用
%0
,%1
,%2
… 来引用操作数。 %0
对应第一个操作数,%1
对应第二个操作数,以此类推。- 输出操作数从
%0
开始编号,输入操作数从输出操作数之后继续编号。 例如,如果有一个输出操作数,那么第一个输入操作数就是%1
。 - 在 RISC-V 汇编中,寄存器通常用
x0
,x1
,x2
… 表示,而在内联汇编的操作数中,我们使用占位符%0
,%1
… ,编译器会负责将这些占位符替换为实际分配的寄存器或立即数。
破坏列表 (Clobber List)
- 破坏列表用于告知编译器,内联汇编代码可能会修改某些资源,从而影响编译器的优化决策。
- 常用破坏描述符:
- 寄存器名 (例如
"x1"
,"x5"
,"x10"
): 表示汇编代码可能会修改指定的寄存器。 如果汇编代码显式地修改了某个寄存器,或者依赖于某个寄存器的值,就应该将其添加到破坏列表中。 "memory"
: 表示汇编代码可能会修改内存。 当汇编代码访问了内存(例如,通过lw
或sw
指令,或者像jalr
这样的跳转指令可能执行未知的内存操作),就应该使用"memory"
。"cc"
: 表示汇编代码可能会修改条件码寄存器 (Condition Code register, RISC-V 中条件码隐含在比较指令和分支指令中,通常不需要显式 clobber “cc”)。
- 寄存器名 (例如
简单示例教程 (RISC-V 32)
示例 1: 将立即数加载到寄存器
1 |
|
"li %0, 100"
: 汇编指令模板,li
(load immediate) 指令将立即数 100 加载到寄存器%0
。"=r"(value)
: 输出操作数。=r
: 约束字符串。=
表示输出操作数 (write-only),r
表示分配通用寄存器。- (value): C 变量
value
,汇编指令的执行结果将存储到这个变量中。
- 输出: 程序会输出 “Value: 100”。
示例 2: 寄存器加法
1 |
|
"add %0, %1, %2"
: RISC-Vadd
指令,将寄存器%1
和%2
的值相加,结果存入寄存器%0
。"=r"(sum)
: 输出操作数,结果写入 C 变量sum
。"r"(a), "r"(b)
: 输入操作数,C 变量a
和b
的值作为加法指令的输入。- 输出: 程序会输出 “Sum: 15”。
示例 3: 内存加载 (Load Word) 和破坏列表
1 |
|
"lw %0, (%1)"
: RISC-Vlw
(load word) 指令,从内存地址(%1)
加载一个 32 位字到寄存器%0
。"=r"(loaded_value)
: 输出操作数,加载的值存入loaded_value
。"r"(addr_ptr)
: 输入操作数,C 指针变量addr_ptr
(包含内存地址) 作为加载指令的地址来源。"memory"
: 破坏列表,因为lw
指令会从内存中读取数据,因此使用"memory"
是必要的,尽管本例中并没有 修改 内存,但lw
指令的内存访问也可能影响编译器的优化假设。- 输出: 程序会输出 “Loaded value: 0x12345678”。
C 语言内联汇编提供了一种在 C 代码中直接编写汇编指令的方式,可以实现一些用纯 C 代码难以或无法完成的底层操作和优化。 掌握内联汇编的关键在于理解其语法格式、操作数约束、破坏列表,并结合具体的处理器架构(如 RISC-V)的汇编指令集进行实践。