Chapter 59Advanced Inline Assembly

附录E. 高级内联汇编

概述

当您需要一次性指令、与传统 ABI 的互操作性,或访问标准库尚未封装处理器功能时,内联汇编为您提供了突破 Zig 抽象层次的能力。33 Zig 0.15.2 通过强制执行指针转换的对齐检查并提供更清晰的约束诊断来强化内联汇编,使其比以前的版本更安全且更容易调试。v0.15.2

学习目标

  • 识别 Zig 的 GNU 风格内联汇编块结构,并将操作数映射到寄存器或内存。
  • 应用寄存器和冲突约束来协调 Zig 变量与机器指令之间的数据流。
  • 使用编译时检查保护特定于架构的代码片段,以便您的构建在不支持的目标上快速失败。

构建汇编块

Zig 采用熟悉的 GCC/Clang 内联汇编布局:模板字符串后跟由冒号分隔的输出、输入和冲突列表。从简单的算术开始,让您习惯操作数绑定,然后再接触更奇特的指令。第一个示例使用 addl 来组合两个 32 位值,将两个操作数绑定到寄存器而不接触内存。x86_64.zig

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));
}
运行
Shell
$ zig test chapters-data/code/59__advanced-inline-assembly/01_inline_add.zig
输出
Shell
All 1 tests passed.

操作数占位符(如 %[lhs])引用您在约束列表中指定的符号名称;保持这些名称的助记性,一旦您的模板增长到超过单个指令,就会得到回报。58

Register Choreography Without Footguns

更复杂的代码片段通常需要双向操作数(读/写)或指令完成后的额外簿记工作。下面的 xchg 序列完全在寄存器中交换两个整数,然后将更新后的值写回 Zig 管理的内存。4 使用 @compileError 保护函数可防止在非 x86 平台上的意外使用,而 +r 约束表示每个操作数既被读取又被写入。pie.zig

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);
}
运行
Shell
$ zig test chapters-data/code/59__advanced-inline-assembly/02_xchg_swap.zig
输出
Shell
All 1 tests passed.

因为交换仅在寄存器上操作,您可以避免棘手的内存约束;当您确实需要直接接触内存时,添加显式的 "memory" 冲突,这样 Zig 的优化器不会重新排序周围的加载或存储。36

可观测性和护栏

一旦您信任语法,内联汇编就成为硬件提供的计数器或尚未在其他地方公开的指令的精确工具。使用 rdtsc 读取 x86 时间戳计数器可为您提供循环级计时,同时演示多输出约束和在 0.15.x 中引入的新对齐断言。39 该示例将计数器的低半部分和高半部分捆绑到 u64 中,并在非 x86_64 目标上回退到编译错误。

Zig
//! 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);
}
运行
Shell
$ zig test chapters-data/code/59__advanced-inline-assembly/03_rdtsc.zig
输出
Shell
All 1 tests passed.

rdtsc 等指令可以围绕其他操作重新排序;当精确测量很重要时,考虑将它们与序列化指令(如 lfence)或显式内存冲突配对。39

需要掌握的模式

  • 将特定于架构的块包装在 if (builtin.cpu.arch != …) @compileError 保护中,以便交叉编译提前失败。41
  • 原型设计时优先仅使用寄存器操作数——一旦逻辑正确,有意引入内存操作数和冲突。33
  • 将内联汇编视为紧急出口;如果标准库(或内置函数)公开了该指令,优先使用该更高级别的 API 以保持可移植性。mem.zig

注意事项和警告

  • 内联汇编是特定于目标的;始终记录所需的最小 CPU 功能,并在执行块之前考虑功能探测。29
  • 冲突列表很重要——忘记 "cc""memory" 可能导致仅在优化下才暴露的错误编译。36
  • 混合 Zig 和外部 ABI 时,仔细检查调用约定和寄存器保存规则;编译器不会为您保存寄存器。builtin.zig

练习

  • rdtsc 之前添加 lfence 指令并测量对稳定性的影响;比较 Debug 和 ReleaseFast 构建中的结果。39
  • 使用 "memory" 冲突扩展 swapXchg 并在紧密循环中交换值时对差异进行基准测试。time.zig
  • 使用基于布尔参数发出 addsub 的编译时格式字符串重写 addAsm15

替代方案和边界情况

  • 某些指令(如特权系统调用)需要提升的权限——将它们包装在运行时检查中,这样它们永远不会无意中执行。48
  • 在具有乱序执行的微架构上,将计时读取与屏障配对以避免测量偏差。39
  • 对于可移植计时,优先使用 std.time.Timer 或平台 API,并将内联汇编保留给真正特定于架构的热路径。

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.