概述
当您需要一次性指令、与传统 ABI 的互操作性,或访问标准库尚未封装处理器功能时,内联汇编为您提供了突破 Zig 抽象层次的能力。33 Zig 0.15.2 通过强制执行指针转换的对齐检查并提供更清晰的约束诊断来强化内联汇编,使其比以前的版本更安全且更容易调试。v0.15.2
学习目标
- 识别 Zig 的 GNU 风格内联汇编块结构,并将操作数映射到寄存器或内存。
- 应用寄存器和冲突约束来协调 Zig 变量与机器指令之间的数据流。
- 使用编译时检查保护特定于架构的代码片段,以便您的构建在不支持的目标上快速失败。
构建汇编块
Zig 采用熟悉的 GCC/Clang 内联汇编布局:模板字符串后跟由冒号分隔的输出、输入和冲突列表。从简单的算术开始,让您习惯操作数绑定,然后再接触更奇特的指令。第一个示例使用 addl 来组合两个 32 位值,将两个操作数绑定到寄存器而不接触内存。x86_64.zig
//! Minimal inline assembly example that adds two integers.
const std = @import("std");
pub fn addAsm(a: u32, b: u32) u32 {
var result: u32 = undefined;
asm volatile ("addl %[lhs], %[rhs]\n\t"
: [out] "=r" (result),
: [lhs] "r" (a),
[rhs] "0" (b),
);
return result;
}
test "addAsm produces sum" {
try std.testing.expectEqual(@as(u32, 11), addAsm(5, 6));
}
$ zig test chapters-data/code/59__advanced-inline-assembly/01_inline_add.zigAll 1 tests passed.操作数占位符(如 %[lhs])引用您在约束列表中指定的符号名称;保持这些名称的助记性,一旦您的模板增长到超过单个指令,就会得到回报。58
Register Choreography Without Footguns
更复杂的代码片段通常需要双向操作数(读/写)或指令完成后的额外簿记工作。下面的 xchg 序列完全在寄存器中交换两个整数,然后将更新后的值写回 Zig 管理的内存。4 使用 @compileError 保护函数可防止在非 x86 平台上的意外使用,而 +r 约束表示每个操作数既被读取又被写入。pie.zig
//! Swaps two words using the x86 xchg instruction with memory constraints.
const std = @import("std");
const builtin = @import("builtin");
pub fn swapXchg(a: *u32, b: *u32) void {
if (builtin.cpu.arch != .x86_64) @compileError("swapXchg requires x86_64");
var lhs = a.*;
var rhs = b.*;
asm volatile ("xchgl %[left], %[right]"
: [left] "+r" (lhs),
[right] "+r" (rhs),
);
a.* = lhs;
b.* = rhs;
}
test "swapXchg swaps values" {
var lhs: u32 = 1;
var rhs: u32 = 2;
swapXchg(&lhs, &rhs);
try std.testing.expectEqual(@as(u32, 2), lhs);
try std.testing.expectEqual(@as(u32, 1), rhs);
}
$ zig test chapters-data/code/59__advanced-inline-assembly/02_xchg_swap.zigAll 1 tests passed.因为交换仅在寄存器上操作,您可以避免棘手的内存约束;当您确实需要直接接触内存时,添加显式的 "memory" 冲突,这样 Zig 的优化器不会重新排序周围的加载或存储。36
可观测性和护栏
一旦您信任语法,内联汇编就成为硬件提供的计数器或尚未在其他地方公开的指令的精确工具。使用 rdtsc 读取 x86 时间戳计数器可为您提供循环级计时,同时演示多输出约束和在 0.15.x 中引入的新对齐断言。39 该示例将计数器的低半部分和高半部分捆绑到 u64 中,并在非 x86_64 目标上回退到编译错误。
//! Reads the x86 time stamp counter using inline assembly outputs.
const std = @import("std");
const builtin = @import("builtin");
pub fn readTimeStampCounter() u64 {
if (builtin.cpu.arch != .x86_64) @compileError("rdtsc example requires x86_64");
var lo: u32 = undefined;
var hi: u32 = undefined;
asm volatile ("rdtsc"
: [low] "={eax}" (lo),
[high] "={edx}" (hi),
);
return (@as(u64, hi) << 32) | @as(u64, lo);
}
test "readTimeStampCounter returns non-zero" {
const a = readTimeStampCounter();
const b = readTimeStampCounter();
// The counter advances monotonically; allow equality in case calls land in the same cycle.
try std.testing.expect(b >= a);
}
$ zig test chapters-data/code/59__advanced-inline-assembly/03_rdtsc.zigAll 1 tests passed.rdtsc 等指令可以围绕其他操作重新排序;当精确测量很重要时,考虑将它们与序列化指令(如 lfence)或显式内存冲突配对。39
需要掌握的模式
注意事项和警告
- 内联汇编是特定于目标的;始终记录所需的最小 CPU 功能,并在执行块之前考虑功能探测。29
- 冲突列表很重要——忘记
"cc"或"memory"可能导致仅在优化下才暴露的错误编译。36 - 混合 Zig 和外部 ABI 时,仔细检查调用约定和寄存器保存规则;编译器不会为您保存寄存器。builtin.zig