- 语言运行时
- JIT编译
- 编译器
【免费下载链接】wasmtime
A lightweight WebAssembly runtime that is fast, secure, and standards-compliant
cranelift-fuzzgen是 Wasmtime 仓库中一个专为 Cranelift 代码生成器打造的模糊测试(fuzzing)crate,其核心职责是生成随机的 Cranelift 模块,再通过"解释器 vs 宿主编译结果"的差分对比来发现编译器缺陷。本文以 cranelift/fuzzgen/README.md 为主线,结合 crate 源码、fuzz目录下的 fuzz target 与配置实现,逐层拆解它的生成流程、可调参数、陷阱消除 Pass 与运行方式,帮助你理解并复用这套编译器验证基础设施。
一、crate 定位:README 之外的真实角色
仓库中 cranelift/fuzzgen/README.md 只有一句话的自我描述:
This crate implements a generator to create random Cranelift modules.
这句话点明了它的本质:一个生成随机 Cranelift 中间表示(CLIF)模块的生成器。在 Wasmtime 的体系里,Cranelift 是核心的代码生成后端,而cranelift-fuzzgen为它提供了一条"用随机程序轰炸编译器"的自动化测试通道。从 cranelift/fuzzgen/Cargo.toml 可以看出它的几个关键特征:
- 版本号为
0.0.0,publish = false,是纯内部工具 crate,不对外发布; - 核心依赖只有
cranelift(Codegen 前端与 IR)、cranelift-native(宿主特性探测)、arbitrary(数据驱动的随机生成)、target-lexicon(目标架构描述)与rand; - 整个 crate 只有 7 个源文件:
lib.rs、config.rs、function_generator.rs、cranelift_arbitrary.rs、passes/{mod,fcvt,int_divz}.rs、print.rs、target_isa_extras.rs。
它生成的模块并不直接用于生产,而是被 fuzz/fuzz_targets/cranelift-fuzzgen.rs 这样的 fuzz target 消费:先随机生成函数,再用 Cranelift 解释器与宿主编译出的机器码对同一组输入分别求值,比对结果是否一致。任何不一致都意味着编译器某个环节(IR 构造、优化、指令选择或后端 lowering)存在 bug。
二、整体架构:从随机字节流到可执行测试用例
cranelift-fuzzgen的生成完全由 fuzzer 提供的随机字节流驱动:arbitrary::Unstructured就像一个"吃字节"的随机数源,每次抽取值都会消耗输入字节。这让同一个输入字节串可以稳定复现同一个测试用例,是模糊测试可回归的前提。
核心入口在 cranelift/fuzzgen/src/lib.rs 的FuzzGen结构体:
pub struct FuzzGen<'r, 'data> { pub u: &'r mut Unstructured<'data>, pub config: Config, }它的生成链路大致如下:
generate_signature(lib.rs#L59):随机决定函数的参数个数、返回值个数,结合 ISA 是否支持 SIMD 与目标架构生成函数签名;generate_func(lib.rs#L150):交给FunctionGenerator填充函数体——随机块、随机变量池、随机指令序列与终结符;run_func_passes(lib.rs#L102):对生成结果依次执行 NaN 规范化 Pass、int_divz陷阱消除 Pass、fcvt陷阱消除 Pass,产出可安全解释/执行的函数;generate_test_inputs(lib.rs#L70):按函数签名随机生成多组入参,供差分执行使用。
真正的 fuzz target 还会套一层TestCase(见 fuzz/fuzz_targets/cranelift-fuzzgen.rs#L150):一次生成1~8 个函数(testcase_funcs),后生成的函数可以调用先生成的函数,保证调用图无环;随后生成控制平面(ControlPlane)、测试输入,并随机决定本次是"解释器 vs 解释器"(优化前后对比)还是"解释器 vs 宿主机器码"(compare_against_host)。执行时逐组入参在两边求值并assert_eq比对(见run_test_inputs),任何一个不一致都会让 fuzzer 立刻报出崩溃。
三、Config:一张参数表看懂生成边界
生成空间的形状完全由 cranelift/fuzzgen/src/config.rs 的Config控制。它把"规模"与"风险"分离:规模类参数决定函数大小、块数、栈槽数量;风险类参数(比率)决定生成多少可能导致陷阱或超时的结构。默认值如下:
| 配置项 | 默认值 | 含义 |
|---|---|---|
max_test_case_inputs | 100 | 每组测试最多生成的入参数目,防止 fuzzer 塞入过多输入拖慢单次执行 |
testcase_funcs | 1..=8 | 每个测试用例生成的函数个数 |
signature_params/signature_rets | 0..=16 | 函数签名参数/返回值个数的取值范围 |
instructions_per_block | 0..=64 | 每个块随机生成的指令条数上限 |
vars_per_function | 0..=16 | 每函数变量池大小(不含签名参数) |
blocks_per_function | 0..=16 | 除入口块外的额外块数(实际块数 = 1 + 该值) |
block_signature_params | 0..=16 | 非入口块的参数个数上限 |
jump_table_entries | 0..=16 | br_table跳转表条目数上限 |
switch_cases | 0..=64 | Switch的 case 条目数上限 |
switch_max_range_size | 2..=32 | Switch单个连续区间的大小范围 |
static_stack_slots_per_function | 0..=8 | 每函数静态栈槽数量 |
static_stack_slot_size | 0..=128 | 单个栈槽大小(字节) |
stack_slot_alignment_log2 | 0..=10 | 栈槽对齐(2 的幂,最多 1024 字节) |
stack_probe_size_log2 | 6..=14 | 栈探测阈值(2 的幂,64 字节~16 KiB),保证能覆盖"不探测 / 展开探测 / 循环探测"三种路径 |
backwards_branch_ratio | (1, 1000) | 生成向后分支的概率,0.1% 的极低比例避免死循环拖垮执行速度 |
allowed_int_divz_ratio | (1, 1_000_000) | 允许保留整数除零陷阱的概率 |
allowed_fcvt_traps_ratio | (1, 1_000_000) | 允许保留 fcvt 转换陷阱的概率 |
compile_flag_ratio | regalloc_checker: (1, 1000) | 个别影响编译性能的 flag 的启用概率 |
bb_padding_log2_size | 0..=12 | 基本块间填充(2 的幂,最多 4 KiB/块) |
其中几个默认值的设计值得注意:backwards_branch_ratio只有千分之一,是因为向后分支天然容易形成无限循环,而 fuzzer 对超时极敏感;allowed_int_divz_ratio与allowed_fcvt_traps_ratio为百万分之一,默认几乎总是插入防护序列,仅保留极小概率去覆盖陷阱路径本身。bb_padding_log2_size的注释还提到:虽然显式只生成最多 16 个块,但 SSA 构造后实际可能膨胀到 400 个块,4 KiB 填充意味着单个函数可能出现约 1.5 MiB 的填充,这能显著锻炼后端处理大函数与分支布局的能力。
四、随机类型、签名与调用约定:CraneliftArbitrary
cranelift/fuzzgen/src/cranelift_arbitrary.rs 定义了CraneliftArbitrarytrait,把Unstructured扩展成"能直接长出 Cranelift 数据类型"的生成器:
_type(L28):从 13 种类型中选取——标量整数I8/I16/I32/I64/I128、标量浮点F32/F64、SIMD 整型I8X16/I16X8/I32X4/I64X2、SIMD 浮点F32X4/F64X2;当 ISA 不支持 SIMD 时降级为前 7 种;callconv(L44):随机选择调用约定,Fast、PreserveAll、SystemV、Tail全平台可用;WindowsFastcall仅 x86_64/aarch64;AppleAarch64仅 aarch64;Winch仅 x86_64;signature(L95):组合出参数与返回值,并处理约定间的约束——Winch约定下强制关闭 SIMD,PreserveAll约定下不允许有返回值;datavalue(L128):按类型生成随机值。值得留意的是浮点值不通过浮点随机生成,而是直接从随机整数位模式构造Ieee32/Ieee64,因为标准浮点随机数生成不到 Signaling NaN、带 payload 的 NaN 这类"毒值",而这些恰恰最容易暴露编译器缺陷。
五、FunctionGenerator:把一个函数"长"出来
cranelift/fuzzgen/src/function_generator.rs 是体积最大的文件(约 1900 行),实现了函数体的完整生成。generate()(L1865)分阶段进行:
generate_funcrefs:先声明所有可调用的外部函数——包括用户函数(已生成的其他函数)和 libcall(如ceil/floor/trunc),统一生成SigRef/FuncRef供后续调用指令引用;generate_blocks(L1611):创建入口块与随机数量的额外块,非入口块可以随机被标记为 cold block,并生成带参数或不带参数的块签名;generate_stack_slots(L1549):创建静态栈槽并随机分配别名分析类别(Other/Heap/Table/VmCtx),之后所有对该栈槽的访存都会带上正确的AliasRegion标记,从而覆盖 Cranelift 别名分析的 4 类区域;build_variable_pool(L1815):为签名参数建立变量,再生成一个随机变量池(每个变量用随机常量初始化),部分变量会被随机声明为需要 stack map(仅限 ≤16 字节类型);- 逐块生成指令:
generate_instructions(L1481)从预计算的OPCODE_SIGNATURES中抽取(Opcode, 参数类型列表, 返回值类型列表),再用inserter_for_format按指令格式分派到对应的插入函数(通用算子、比较、常量、访存、原子操作、shuffle、insertlane/extractlane、函数调用等); insert_terminator(L1399):为每个块补齐终结符。
5.1 六类块终结符
块终结符提前为每个块选定,包含Return、Jump、Br(带条件)、BrTable、Switch、TailCall/TailCallIndirect(见 L1043 的BlockTerminator枚举)。生成策略保证CFG 中不存在不可达块:先构造一条"主脊柱"——每个块默认可以跳向下一个块,再在此基础上随机引入分支、跳转表与 Switch,但始终保留"指向下一块"这一兜底边。Switch终结符会把switch_cases个 case 或连续区间写入cranelift::frontend::Switch,并随机选择 I8/I16/I32/I64/I128 作为索引类型;br_table则只允许指向无参数块。尾调用(TailCall/TailCallIndirect)只有在调用约定为Tail、存在返回值签名匹配的被调函数、且目标架构支持(aarch64/riscv64 恒支持,x86_64 需要开启preserve_frame_pointers)时才被允许生成。
5.2 访存地址只出自栈槽
一个值得注意的约束:所有 load/store 的地址都来自stack_addr指向的已生成栈槽(generate_load_store_address,L1230),地址值从不存入变量、也从不作为函数返回值——否则解释器与后端拿到的地址将不可比。生成时会保证访问不越界,并随机决定对齐与notrap标志;aarch64/riscv64 上的原子操作强制使用对齐地址(避免未对齐原子指令问题)。栈槽在函数入口会被显式零初始化(initialize_stack_slots,L1574),使执行结果可预测。
六、指令空间:从 Opcode 约束推导合法签名
OPCODE_SIGNATURES(L724)是懒加载的静态表,构造逻辑展示了 fuzzgen 最精妙的部分:
- 遍历
Opcode::all(),排除控制流指令(br_table/brif/jump/return/tail_call等,它们由终结符逻辑单独处理)、iconst(常量另行生成)、extract_vector(动态向量导致返回类型生成失败); - 对每个 opcode 读取其指令约束(
constraints),根据控制类型集合(ctrl_typeset)筛选出允许的控制类型; - 对每个控制类型,用约束计算出固定返回值类型,并对每个固定值参数位置展开"允许的类型列",最后做笛卡尔积生成所有合法的参数类型组合;
- 再经过两轮过滤:第一轮剔除"需要人工把关"的组合(
trap/debugtrap、128 位原子操作、大量未实现的 fcvt 组合、已知 issue 涉及的类型组合等),第二轮按FUZZGEN_ALLOWED_OPS环境变量过滤。
FUZZGEN_ALLOWED_OPS是一个面向调试的开关:传一个逗号分隔的 opcode 名列表,生成器就只产生这些指令(见 fuzz/README.md#L118):
FUZZGEN_ALLOWED_OPS=ineg,ishl cargo fuzz run cranelift-fuzzgen这对聚焦复现某个特定 opcode 的 bug 极其有用——不需要构造完整模块,只需声明"只给我ineg和ishl"。
6.1 按目标架构过滤:valid_for_target
valid_for_target(L424)维护了一份"已知后端缺陷/未实现特性"的黑名单,按x86_64、Aarch64、S390x、Riscv64分架构过滤,避免 fuzzer 反复上报已知问题。例如:x86_64 上排除UmulOverflow/SmulOverflow作用于[I128, I128]、Cls作用于标量整数、部分FcvtToUint/Sint组合与IaddPairwise;AArch64 额外排除Bnot用于浮点、VhighBits用于浮点向量等。代码注释明确写出这是"known issues with specific lowerings",目标是随修复不断收敛清单。这类过滤放在每次指令生成时而非建表时执行,是为了避免 corpus 在指令启停时整体失效。
七、三趟修正 Pass:让随机代码"能跑、可比"
随机生成的代码可能触发各种陷阱或跨平台不确定行为。run_func_passes(lib.rs#L102)依次执行三个 Pass:
- NaN 规范化(NaN Canonicalization):IEEE 754 与 Wasm 规范对 NaN 返回值都较宽松,且 x86 与 AArch64 上同一运算可能产生不同的 NaN 位模式——这会导致解释器与后端结果"都合法但不相等"的假阳性。规范化把 NaN 统一替换为固定值,消除这类干扰(这也是
generate_flags中固定开启enable_nan_canonicalization的原因); int_divzPass(cranelift/fuzzgen/src/passes/int_divz.rs):为Sdiv/Udiv/Srem/Urem前置一段防护序列——检查分母为 0,以及有符号除法特有的INT_MIN / -1陷阱(通过lhs == INT_MIN && rhs == -1判定),一旦命中就把分母替换为 1。该 Pass 按函数为单位(而非按指令)决定是否插入,这样在允许 0.1% 陷阱率时实际落入陷阱的运行比例更可控,也节省 fuzzer 输入字节;fcvtPass(cranelift/fuzzgen/src/passes/fcvt.rs):FcvtToUint/FcvtToSint在 NaN 或值越界时会陷阱,Pass 用fcmp检查 NaN、下溢与上溢三种情况,命中时把输入替换为1.0。注意边界值的处理:浮点转整数是截断语义,因此 i8 的合法最大浮点值是 127.99999,float_limits通过给整数边界 ±1.0 构造正确的浮点阈值。
这两个陷阱消除 Pass 的"放行"概率分别由allowed_int_divz_ratio与allowed_fcvt_traps_ratio控制,兼顾覆盖陷阱路径与维持执行吞吐。
八、随机编译器标志:最大化编译路径覆盖
模糊测试不仅要随机代码,还要随机编译配置。generate_flags(lib.rs#L175)负责生成 Cranelift 的语义保持型(semantics-preserving)flags:
- 优化级别:随机取
OptLevel之一; - 布尔标志:从
enable_alias_analysis、unwind_info、preserve_frame_pointers、两类 Spectre 缓解、regalloc_checker、enable_compact_unwind_abi、enable_llvm_abi_extensions等中逐个随机启停(其中regalloc_checker按compile_flag_ratio以千分之一概率开启,因为寄存器分配检查代价高昂); - 栈探测:在支持内联探测的架构(x86_64/aarch64/riscv64)上随机开启
enable_probestack+probestack_strategy=inline,并从stack_probe_size_log2范围中选探测阈值; - 基本块填充:随机设置
bb_padding_log2_minus_one; - 固定设置:
enable_verifier始终打开(生成后校验是默认义务);x86_64 强制打开enable_llvm_abi_extensions(i128 参数需要);machine_code_cfg_info常开以保证生成过程不 panic。
set_isa_flags(lib.rs#L270)则处理目标 ISA 特性标志,提供两种模式:
IsaFlagGen::Host:用cranelift-native做特性探测,只生成当前宿主 CPU 支持的特性标志(fuzz target 差分执行时使用);IsaFlagGen::All:允许生成该目标 ISA 的全部标志,枚举型标志随机取值。
一个重要的可复现性设计:每个 ISA 标志是否复制到最终 builder,由从输入字节种出的SmallRng决定(lib.rs#L310),而非直接消耗Unstructured的字节——这保证同一测试用例在不同 CPU 的宿主机上(Host模式)生成结果尽量一致,避免 corpus 因机器差异而失效。
九、可打印测试用例:把随机代码沉淀成 .clif 回归
fuzzgen 找到的 bug 需要转成可提交的回归测试。PrintableTestCase(cranelift/fuzzgen/src/print.rs)把内存中的函数与输入格式化成标准的.cliffiletest:
PrintableTestCase::compile输出test compile用例;PrintableTestCase::run输出test interpret+test run用例,并在target声明行附带非默认的 ISA 标志;- 函数按倒序打印、主函数最后输出,紧挨测试输入(
; run: func0(...) == ...),便于阅读; - 标志输出做了精简:只打印与默认值不同的 flags,布尔标志用简写语法;
- 由于生成时并不知道期望输出,
run用例的输出部分用占位零值生成,注释明确说明"probably will be wrong",后续可由cargo fuzz fmt格式化补全。
这就是cargo fuzz fmt将崩溃输入转成 filetest 回归测试的原理所在。cranelift-icachefuzz target 也复用了 fuzzgen 的生成能力,并同样支持FUZZGEN_ALLOWED_OPS(见 fuzz/README.md#L124)。
十、集成与运行:从零开始跑一次 fuzzgen
10.1 fuzz target 内部工作流
fuzz/fuzz_targets/cranelift-fuzzgen.rs 展示了完整的消费方逻辑:
- 从输入字节构造
FuzzGen,随机决定是否做宿主对比; - 用
builder_with_options(true)构建宿主 ISA,叠加随机 flags 与 ISA flags; - 倒序生成多个函数(保证调用图无环),每个函数附带随机的
ControlPlane(控制平面用于向优化器注入随机决策,即"优化阶段也模糊化"); - 为每个测试用例生成多组输入(复用一次编译、多次执行的思路,提高单次编译的覆盖率);
fuzz_target!入口(L382)按compare_against_host选择执行路径:- 解释器 vs 解释器:把原函数与
to_optimized()(经ctx.optimize优化)后的函数分别在解释器中执行对比,验证优化器的语义保持性; - 解释器 vs 宿主:用
TestFileCompiler编译到机器码,通过 trampoline 调用,与解释器结果逐位比较;
- 解释器 vs 解释器:把原函数与
- 解释器设有燃料限制(
INTERPRETER_FUEL = 4096),每次运行重建解释器避免状态串扰;超时输入整体丢弃(RunResult::Timeout),陷阱输入跳过该组继续下一组,其余不一致一律assert_eq失败; - 每 10000 个有效输入打印一次统计:有效/无效输入数、成功/超时运行数、按陷阱类型聚合的分布(
Statistics,L37)。
解释器侧的 libcall 只支持CeilF32/F64、FloorF32/F64、TruncF32/F64六个(ALLOWED_LIBCALLS,L299),生成侧会同步限制 libcall 集合,保证两边行为对等。
10.2 实际运行命令
在仓库fuzz目录下,用 cargo-fuzz 即可启动(依赖 nightly 工具链与cargo fuzz子命令):
# 运行差分模糊测试 cargo fuzz run cranelift-fuzzgen # 只生成指定 opcode,聚焦复现 FUZZGEN_ALLOWED_OPS=ineg,ishl cargo fuzz run cranelift-fuzzgen # 用 --fuel 控制优化器控制平面的燃料(默认 0) cargo fuzz run cranelift-fuzzgen -- --fuel=100 # 复现已上报的 bug cargo +nightly fuzz run cranelift-fuzzgen /path/to/testcase配合RUST_LOG=debug可获得更多诊断输出(见 fuzz/README.md#L99 的复现流程)。cranelift-icache目标同样基于 fuzzgen,用于验证增量编译与全量编译产出机器码的一致性。另外,fuzz/README.md#L83 建议从维护的 corpus 起步而非空跑——虽然可以从零开始,但带种子语料能让 libFuzzer 更快进入深覆盖状态。
十一、总结:fuzzgen 的设计要点
回看整个 crate,cranelift-fuzzgen的工程智慧可以归纳为四点:
- 数据驱动、完全可复现:一切随机都来自 fuzzer 输入的字节流,同一个输入在任何机器上都能长出同一个模块;
- 分层控制风险:
Config把规模、分支、陷阱、编译开销分别用范围与比率约束,让模糊测试在"覆盖率"与"吞吐量"之间取得平衡; - 差分语义优先:NaN 规范化与陷阱消除 Pass 都是为了消除"合法但不同"的假阳性,让解释器与宿主机器的对比结果真正可比;
- 可回归闭环:
PrintableTestCase让任何崩溃输入都能转成.cliffiletest 提交回归,FUZZGEN_ALLOWED_OPS让 bug 聚焦复现变得廉价。
作为 Wasmtime 质量保障体系的重要一环,fuzzgen 持续用随机生成的 CLIF 模块检验 Cranelift 的每个环节——如果你正在为 Cranelift 贡献后端代码,或想为其他 IR 编译器搭建类似的差分模糊测试设施,这份 crate 从架构、参数到执行路径都是值得直接参考的范本。
进一步阅读:核心生成逻辑见 cranelift/fuzzgen/src/function_generator.rs 与 cranelift/fuzzgen/src/lib.rs;生成参数与默认值见 cranelift/fuzzgen/src/config.rs;差分执行与统计见 fuzz/fuzz_targets/cranelift-fuzzgen.rs;运行与调试方式见 fuzz/README.md。
- 语言运行时
- JIT编译
- 编译器
【免费下载链接】wasmtime
A lightweight WebAssembly runtime that is fast, secure, and standards-compliant
相关推荐
Wasmtime代码生成后端对比:Cranelift vs LLVM
Wasmtime代码生成后端对比:Cranelift vs LLVM Wasmtime作为高性能WebAssembly运行时,其代码生成后端的选择直接影响执行效
语言运行时JIT编译编译器终极C++开发资源指南:从入门到精通一站式解决方案
终极C++开发资源指南:从入门到精通一站式解决方案 Awesome C++ 是一个精心整理的C++框架、库、资源和工具的精选列表,为开发者提供了从入门到精通的完
语言运行时JIT编译编译器深入Wasmtime引擎:Cranelift编译器与JIT技术
深入Wasmtime引擎:Cranelift编译器与JIT技术 本文深入解析了Wasmtime引擎的核心组件Cranelift编译器的架构设计与JIT技术实现。
语言运行时JIT编译编译器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考