Odin 的新内联汇编模板
摘要
本文详细介绍了 Odin 编程语言的新内联汇编模板,其通用语法灵感来自 Go 的汇编器,并支持 amd64 目标架构。
<p><a href="https://lobste.rs/s/loltt2/odin_s_new_inline_assembly_templates">评论</a></p>
查看缓存全文
缓存时间: 2026/08/18 20:22
# 内联汇编模板概览
来源:https://odin-lang.org/docs/inline-asm/
> **注意:** 目前仅支持 amd64 目标(例如 `windows_amd64`、`linux_amd64`、`darwin_amd64`)。
## 概述
(https://odin-lang.org/docs/inline-asm/#overview)
`asm` 模板是一种可调用的实体,在每次调用时就地实例化,类似于强制内联过程。它不是被拼接到周围过程中的语句块;相反,它表现得像一个内建函数。某些特定于平台的内建函数将在不久的将来被此系统取代。
通用的 `指令 [操作数{, 操作数}]` 形式旨在成为*跨指令集架构的通用语法*:每个 ISA 都共享这种通用语法,同时仍然暴露其自身的指令和寄存器。该方法以 Go 的 Plan 9 衍生汇编器为模型,它同样在所有目标上使用一种语法(Go 的汇编器指南 (https://go.dev/doc/asm),Plan 9 汇编器手册 (https://9p.io/sys/doc/asm.html))。该语法起源于 Plan 9(Ken Thompson 的工具链),并被带入了 Go。
Odin 的内联汇编使用上下文无关文法,但这并不意味着任何助记符在 ISA 之间是共享的——只有语法本身是共享的。
## 声明
(https://odin-lang.org/docs/inline-asm/#declaration)
```
名称 :: asm(参数) -> (结果) [绑定] {
主体
}
```
- `参数` - 输入操作数。简单的名称和类型。
- `结果` - 输出操作数。简单的名称和类型。
- `绑定` - 绑定、固定、临时寄存器、宽度视图、破坏和副作用。
- `主体` - 指令流。
`-> (结果)` 和 `[绑定]` 块都是可选的。主体使用 Intel 操作数顺序(`目的, 源`);后端根据目标平台进行降低。物理寄存器使用 `%` 符号(`%rax`);参数和临时寄存器名称是裸名(`r`、`acc`)。结果可以在调用点保持未绑定状态;编译器会隐式忽略未使用的部分,因此一个结果是 ABI 伪影的模板无需解构。
```
mfence :: asm() [ #volatile, #clobber memory ] {
mfence
}
// 注意:`#volatile` 和 `#clobber memory` 在这里都从
// `mfence` 的使用中推断出来,但为了清晰起见而显式写出。
```
### 参数类型
(https://odin-lang.org/docs/inline-asm/#parameter-types)
参数类型是以下之一:整数、浮点数、布尔值、指针、多重指针或 `#simd[N]T`。`$name` 参数是编译期立即数(`$ctrl: u8`)。它不能是指针类型,其值仅在实例化时可知——并且仅在实例化时进行范围检查。
## 绑定
(https://odin-lang.org/docs/inline-asm/#bindings)
`[...]` 块包含所有非简单的输入或输出名称的内容。绑定和固定是签名中名称上的边;临时寄存器、宽度视图和破坏则在块内直接声明。
| 形式 | 含义 |
| :--- | :--- |
| `in -> out` | 绑定:`out` 是读写操作,共享 `in` 的寄存器 |
| `name = %reg` | 将 `name` 固定到物理寄存器 |
| `in -> out = %reg` | 输入/输出固定到固定寄存器 |
| `name: T` | 类型为 `T` 的临时寄存器 |
| `name: T = %reg` | 类型为 `T` 的临时寄存器,固定到物理寄存器 |
| `view: T = src` | `src` 寄存器在宽度 `T` 下的宽度视图 |
| `#clobber x` | 破坏一个寄存器、`flags` 或 `memory` |
| `#volatile` | 将整个模板标记为易失性 |
| `#align_stack` | 强制在进入模板时重新对齐栈 |
右侧消歧了两种 `=` 形式:`= %reg`(一个寄存器)是固定;`= src`(一个名称)是另一个操作数的宽度视图。裸露的 `->` 将寄存器留给编译器决定,而 `= %reg` 则固定到特定于目标平台的特定寄存器。
临时寄存器声明是模板生命周期内的寄存器,仅分配一次。它们存在于绑定块中,而不是主体中——模板中没有块作用域。
### 寄存器
(https://odin-lang.org/docs/inline-asm/#registers)
显式寄存器以 `%` 为前缀,以防止与用户提供的参数和父作用域中的其他全局常量发生命名空间冲突。寄存器名称是目标平台自身的(例如 AMD64 上的 `%rax`、`%xmm0`、`%r11`、`%al`),针对每个平台具体命名,而不是使用通用名称。
根据模板的不同,可能常见的是在任何地方都使用显式寄存器,或者常见的是使用临时寄存器参数。
### 绑定
(https://odin-lang.org/docs/inline-asm/#ties)
`in -> out` 将一个输入和一个输出绑定到同一个寄存器。它被降低为一个读写操作数(`+r`)。带有固定的绑定(`in -> out = %rax`)固定寄存器;没有固定的绑定让分配器选择。
```
add_one :: asm(x: u64) -> (r: u64) [ x -> r ] {
inc r
}
```
### 固定
(https://odin-lang.org/docs/inline-asm/#pins)
`name = %reg` 强制指定一个物理寄存器。两个操作数可以固定到同一个寄存器(一个不需要绑定的输入输出):
```
divmod_u64 :: asm(n: u64, d: u64) -> (quo, rem: u64) [
n -> quo = %rax, rem = %rdx,
#clobber flags,
] {
xor %rdx, %rdx
div d
}
```
### 临时寄存器
(https://odin-lang.org/docs/inline-asm/#scratch)
`name: T` 是一个工作寄存器,其类别来自 `T`(`i64` → 通用寄存器,`#simd[4]f32` → 向量寄存器)。未固定的临时寄存器是早期破坏的——它永远不能与输入别名。使用 `name: T = %reg` 固定临时寄存器到特定寄存器,或者如果它只是被破坏而没有命名,则使用 `#clobber %reg`。
### 宽度视图
(https://odin-lang.org/docs/inline-asm/#width-views)
`view: T = src` 是 `src` 寄存器的另一个名称,以宽度 `T` 查看。一个寄存器,两个宽度;没有固定,分配器保持自由。它仅适用于整数,并且 `T` 必须比 `src` 的宽度窄(视图存在是为了命名一个子寄存器)。
对于 `setcc` 后接算术的习语:
```
count_less :: asm(x: []i64, n: i64, thr: i64) -> (count: i64) [
acc: i64,
pred: i64,
predb: u8 = pred, // pred 的低 8 位视图
i: i64,
#clobber flags, #clobber memory,
] {
// ...
setl predb
; add acc, pred
...
}
```
## 内存操作数
(https://odin-lang.org/docs/inline-asm/#memory-operands)
Intel 风格的有效地址。通用形式是 `[基址 + 变址*比例 + 位移]`;任何组成部分都可以省略:
- `[基址]`
- `[基址 + 变址]`
- `[基址 + 变址*比例]` - 比例 `1`、`2`、`4`、`8`
- `[基址 + 变址<比例>]` 仅在支持其编码的目标(例如 arm64)上被接受,而不是 amd64。
规则:
- 基址和变址必须是 32 位或 64 位整数寄存器,并且宽度相同。
- `%rsp`/`%esp` 可以作为基址,但永远不能作为变址。
- 比例需要变址。
- 位移是一个适合有符号 32 位值的编译期整数;寄存器应放在变址位置,而不是位移中。
### 大小注释
(https://odin-lang.org/docs/inline-asm/#size-annotation)
`[基址]:T` 为内存操作数提供显式的访问宽度,用于没有寄存器操作数固定它时(`crc32 r32, r/m8`)。这是一个类型注解,仅使用 `T` 的大小和类别,而不是值转换,因此 `:u8`、`:i8`、`:b8` 是相同的。
```
crc32_buf :: asm(init: u32, p: [^]u8, len: i64) -> (crc: u32) [
init -> crc,
i: i64,
#clobber flags, #clobber memory,
] {
xor i, i
cmp i, len
jge .done
.loop:
crc32 crc, [p + i]:u8
add i, 1
cmp i, len
jl .loop
.done:
}
```
## 标签
(https://odin-lang.org/docs/inline-asm/#labels)
`.名称:` 定义一个标签;`.名称` 引用它。标签在模板内是局部的,并且每次实例化都会被修饰,因此模板可以多次内联而不会发生符号冲突。没有全局标签。
## 前缀
(https://odin-lang.org/docs/inline-asm/#prefixes)
前缀是应用于它的指令之前的一个单独的行:
前缀不接受操作数,必须紧跟在指令之后(不是标签或其他前缀),并且会根据以下指令的形式进行合法性检查(`lock` 需要内存目标;`rep`/`repne` 需要字符串指令)。
## 数据和对齐指令
(https://odin-lang.org/docs/inline-asm/#data-and-alignment-directives)
这些指令在主体中编写,在指令流中发出原始字节或控制布局:
- `#byte N[, N]` - 直接发出一个或多个字节(表示为整数)作为指令信息。
- `#skip N` - 产生 N 个零字节。
- `#nop N` - 产生相当于 N 字节的“空操作”,发出最少量的“空操作”类指令。
- `#align N` - 将下一条指令对齐到 `N` 字节,必须是 2 的幂。
## 破坏和副作用
(https://odin-lang.org/docs/inline-asm/#clobbers-and-effects)
三个正交的轴:
- `#clobber %reg` - 一个寄存器被破坏。
- `#clobber flags` - 条件码(flags)被修改。
- `#clobber memory` - 编译器看不到的内存被读取或写入;也强制顺序。
大多数破坏是从使用的指令中推断出来的;仅为无法推断的副作用显式写出它们(例如运行时相关的 AVX-512 掩码)。
## 模板指令
(https://odin-lang.org/docs/inline-asm/#template-directives)
放置在 `[...]` 块内:
- `#volatile` 将整个模板标记为*易失性*:即使其结果未被使用,也不得删除,也不得重新排序。这与 `#clobber memory`(一个顺序/可见性语句)和 `#clobber flags` 不同。
- `#align_stack` 强制在进入模板时重新对齐栈,用于需要对齐栈的指令。
## 语义检查
(https://odin-lang.org/docs/inline-asm/#semantic-checking)
模板不会原样传递给汇编器。前端会根据目标平台自身的编码表(后端编码所使用的相同数据)对每条指令进行类型检查,因此大多数错误在编译时,在有问题的标记处被捕获,而不是后来以晦涩的汇编器错误形式出现。
每条指令检查的内容:
- **未知的助记符或前缀。** 报告时会附带从目标的助记符集中提取的*是否指*建议(`movsss` → 您是否指的是 `movss`、`movsd`?)。未知寄存器也会以相同方式建议。
- **操作数数量。** 过少或过多的操作数会命名接受的数量。
- **操作数种类。** 寄存器 vs 内存 vs 立即数 vs 标签,与每种编码形式匹配。不匹配时会命名操作数以及那里期望什么(例如*操作数 2 期望寄存器,但得到立即数*)。
- **操作数大小和类别。** 将 `u32` 放入 64 位插槽、将 `#simd[8]f32` 放入 xmm 插槽,或将整数放入需要向量寄存器的地方,会报告期望和实际的大小/类别。标量浮点数在向量寄存器插槽中是可接受的(它使用低通道);`#simd` 向量必须精确匹配插槽宽度。
- **立即数范围。** 不适合该形式立即数宽度的常量会被拒绝,并显示其值和溢出的宽度(`36893488147419103232` 不适合 32 位立即数)。`$` 立即数在实例化时进行范围检查。
- **内存操作数。** 基址/变址寄存器类别和宽度一致性,`%rsp`/`%esp` 被误用作变址,有比例但无变址,超出范围或寄存器值的位移。
当一个助记符有多种编码形式时,检查器会针对*最接近的*形式进行报告——操作数最接近满足的那一种——因此建议指向您最可能打算的编码,而不是不相关的编码。
前缀合法性(`lock` 用于内存目标;`rep`/`repne` 用于字符串指令)会根据选定的形式进行检查。
破坏、条件码副作用和副作用从使用的指令中推断;显式的 `#clobber` 和 `#volatile` 形式用于表格无法推断的情况(见上文)。
## 实例化
(https://odin-lang.org/docs/inline-asm/#instantiation)
模板是一个宏:它在每次调用时展开。因此,`$` 参数的立即数值范围是每次调用时评估的,而不是在模板定义时。
## `asm` 组
(https://odin-lang.org/docs/inline-asm/#asm-groups)
就像过程组(提供显式重载)一样,`asm` 模板也可以被分组以提供重载行为:
```
store_u32 :: asm(p: ^u32, v: u32) {
mov [p], v
}
store_u64 :: asm(p: ^u64, v: u64) {
mov [p], v
}
store :: asm{
store_u32,
store_u64,
}
```
## 内联 `asm` 调用
(https://odin-lang.org/docs/inline-asm/#inline-asm-calls)
有时您只需要一个立即调用的 `asm` 模板实例:
```
asm(p: ^u64, v: u64) {
mov [p], v
}(&x[i], 123)
```
这种形式只能直接在调用表达式中使用;它不能用作常规值,因为它实际上并不存在——它纯粹是一个模板。
## 示例展示语法
(https://odin-lang.org/docs/inline-asm/#examples-showing-the-syntax)
```
add_one :: asm(x: u64) -> (r: u64) [ x -> r, ]{ inc r }
add_u64 :: asm(x, y: u64) -> (r: u64) {
mov r, x
add r, y
}
swap :: asm(x, y: u64) -> (a, b: u64) [ x -> a, y -> b, ]{ xchg a, b }
rol_imm :: asm(x: u32, $n: i32) -> (r: u32) [ x -> r, ]{ rol r, n }
rdtsc :: asm() -> (lo, hi: u32) [ lo = %eax, hi = %edx, ] {
rdtsc
}
cpuid :: asm(leaf: u32) -> (a, b, c, d: u32) [
leaf -> a = %eax, b = %ebx, c = %ecx, d = %edx,
] {
cpuid
}
// [#clobber memory] 被推断
store_u64 :: asm(p: ^u64, v: u64) {
mov [p], v
}
// [#volatile] 被推断,但为了清晰起见显式写出
mfence :: asm() [#volatile] {
mfence
}
dot_f32x4 :: asm(a, b: [^]f32, n: i64) -> (result: f32) [
acc: #simd[4]f32, tmp: #simd[4]f32, i: i64,
#clobber flags, // cmp/jl 设置标志
#clobber memory, // 保守起见:我们读取编译器看不到的内存
] {
xorps acc, acc // acc = {0,0,0,0}
xor i, i
.loop:
movups tmp, [a + i*4] // 从 a 加载 4 个浮点数;比例 4 = sizeof(f32)
mulps tmp, [b + i*4] // tmp *= 从 b 加载的 4 个浮点数 (mulps xmm, m128)
addps acc, tmp
add i, 4
cmp i, n
jl .loop // .loop 在每次展开时由前端修饰
haddps acc, acc // 水平折叠:{a0+a1, a2+a3, ...}
haddps acc, acc // {sum, sum, sum, sum}
movss result, acc // result = acc[0]
}
dot_f32x4_v2 :: asm(a, b: [^]f32, n: i64) -> (result: f32) [
acc0: #simd[4]f32, acc1: #simd[4]f32,
t0: #simd[4]f32, t1: #simd[4]f32,
i: i64,
] {
vxorps acc0, acc0, acc0
vxorps acc1, acc1, acc1
xor i, i
.loop:
vmovups t0, [a + i<<2] // 等同于 [a + i*4]
vmovups t1, [a + i<<2 + 16]
vfmadd231ps acc0, t0, [b + i<<2] // acc0 += t0 * b[i:][:4]
vfmadd231ps acc1, t1, [b + i<<2 + 16] // acc1 += t1 * b[i+4:][:4]
add i, 8
cmp i, n
jl .loop
vaddps acc0, acc0, acc1 // 组合两个链
vhaddps acc0, acc0, acc0
vhaddps acc0, acc0, acc0
vmovss result, acc0, acc0
}
shuffle4 :: asm(v: #simd[4]f32, $ctrl: u8) -> (r: #simd[4]f32) [
v -> r, // xmm 输入输出绑定;r 初始为 v
]{
shufps r, r, ctrl // 根据 imm8 控制排列 r 的 4 个通道
}
memcpy_rep :: asm(dst, src: rawptr, len: uint) -> (end_dst, end_src: rawptr, rem: uint) [
dst -> end_dst = %rdi,
src -> end_src = %rsi,
len -> rem = %rcx,
] {
rep movsb
}
divmod_u64 :: asm(n: u64, d: u64) -> (quo, rem: u64) [
n -> quo = %rax, rem = %rdx,
] {
xor %rdx, %rdx // 清除被除数的高位
div d // rax = rdx:rax / d ; rdx = 余数
}
crc32_buf :: asm(init: u32, p: [^]u8, len: i64) -> (crc: u32) [
init -> crc,
i: i64,
] {
xor i, i
cmp i, len
jge .done
.loop:
crc32 crc, [p + i + 0]:u8
add i, 1
cmp i, len
jl .loop
.done:
}
atomic_fetch_add :: asm(p: ^i64, delta: i64) -> (old: i64) [
delta -> old,
] {
lock xadd [p], old // [p] += old; old = 之前的 [p]。
}
count_less_than :: asm(src: [^]i64, n: i64, threshold: i64) -> (count: i64) [
acc: i64, // 运行计数(未固定临时寄存器 -> 由分配器选择)
pred: i64, // 谓词寄存器,用于两个宽度
predb: u8 = pred, // `pred` 的低 8 位视图,用于 setl
elem: i64, // 加载的元素
i: i64, // 循环索引
] {
xor acc, acc
xor i, i
cmp i, n
jge .done
.loop:
mov elem, [src + i*8]
xor pred, pred // 首先将整个 64 位寄存器清零
cmp elem, threshold
setl predb // predb = (elem < threshold) ? 1 : 0 -> pred 的低字节
add acc, pred // 以 64 位宽度读取 pred;高位已知为 0
add i, 1
cmp i, n
jl .loop
.done:
mov count, acc
}
tzcnt :: asm(x: u64) -> (count: u64, was_zero: bool) [
was_zero = %flags.z,
] {
tzcnt count, x
}
bit_reset :: asm(in_val: i32, bit: i32) -> (out_val: i32, f: bool) [
in_val -> out_val, f = %flags.c,
] {
btr out_val, bit
}
```
```
main :: proc() {
// ... 使用上述模板的示例调用 ...
}
```
相似文章
Odin dev-2026-06 发布
Odin dev-2026-06 已发布。Odin 是一种面向数据的编程语言,专为高性能系统开发而设计。
理解Odin编程语言
一本教授Odin编程语言的书,涵盖手动内存管理、参数多态和数据导向设计。1.10版本已发布并包含更新。
Odin 1.0 Announcement
Odin编程语言宣布将于2027年1月发布1.0版本(Odin 2027),并公布了完整规范、内联汇编、新标准库等路线图。
汤姆的命名空间:一篇 Odin 同人小说
一篇叙事风格的技术文章,探讨 Odin 编程语言中的代码组织方式,着重指出其包系统特点、缺乏包级局部命名空间的现状,以及由此引发的开发者工作流挑战。
编写可移植的ARM64汇编代码
一份关于编写可在Apple Darwin和Linux/BSD系统间移植的ARM64汇编代码的指南,涵盖ABI、符号命名和向量助记符的差异。