rustc_codegen_gcc 开发实用技巧:向 GCC 传参、查看 GIMPLE 与汇编、配置交叉编译
本文基于 Rust 仓库中 GCC 代码生成后端 rustc_codegen_gcc 的开发者文档 tips.md 整理而成,系统讲解日常开发调试中会用到的各类小技巧:如何把参数透传给 GCC 链接器和 GCC 本身、如何查看汇编 dump 中的 personality 函数、如何查看 sysroot crate 的 LLVM IR、如何防止链接器对符号做 demangle、如何用自建 rustc 与自定义 sysroot 源码路径构建、如何用 mem-trace 做内存追踪,以及如何构建并配置交叉编译用的 libgccjit。读完后你可以独立使用 CG_RUSTFLAGS、config.toml、./y.sh 构建系统完成该后端从常规调试到交叉编译的完整工作流。
1. 背景:rustc_codegen_gcc 是什么
rustc_codegen_gcc 是 rustc 的一个 GCC 代码生成后端:它可以被现有的 rustc 前端加载,但把代码生成阶段交给 GCC(经由 libgccjit)完成,从而受益于 GCC 覆盖的更多目标架构和 GCC 的优化器。其项目说明见 Readme.md,核心定位包括:
- 主要目标:让 Rust 代码能够在 LLVM 不支持的平台上编译;
- 次要目标:验证 GCC 后端是否能在编译出的程序运行时带来性能收益;
- 尽管名字叫 libgccjit,该项目用它做的是静态的、提前编译(ahead-of-time),而非 JIT。
后端由位于 y.sh 的构建脚本驱动(它先构建 build_system 下的 Rust 构建系统,再转发参数),工具链由 rust-toolchain.toml 锁定(当前为 channel = "nightly-2026-04-29")。下面所有命令中的 ./y.sh、../y.sh 均指该脚本——文档中写作 ../y.sh 是因为典型用法是从后端目录之外(例如你的项目目录或 tests/hello-world)调用它,仓库中的 CI 工作流 m68k.yml 中同样以 ../../y.sh 的形式调用。
2. 如何向 GCC 链接器发送参数
最直接的方式是通过 CG_RUSTFLAGS 环境变量注入 -Clink-args,让 rustc 把参数原样传给链接器。官方文档给出的示例是查看链接过程的中间文件与详细信息:
CG_RUSTFLAGS="-Clink-args=-save-temps -v" ../y.sh cargo build
-save-temps让链接器保留各阶段生成的临时文件(如.o文件),便于检查链接前的目标文件;-v打印链接器完整命令;CG_RUSTFLAGS本身会被构建系统读取并转发给 rustc——可以在 config.rs 中看到它读取该环境变量并拼接到 rustflags 中,build.rs 在构建阶段同样会检查它。
这个模式在后面多个技巧中都会复用:凡是想给 rustc/rustc 工具链传 -C 参数,都可以走 CG_RUSTFLAGS。
3. 如何向 GCC 本体发送参数:-Cllvm-args 的新用途
rustc 原本有一个 -Cllvm-args 选项用于向 LLVM 传参;rustc_codegen_gcc 复用了这个选项名,将其语义改为“直接把参数传给 GCC 后端”。也就是说,-Cllvm-args=<arg> 中 <arg> 部分会原样成为传给 libgccjit 上下文的命令行选项。例如要给 GCC 传一个 -f 系列的 flag:
CG_RUSTFLAGS="-Cllvm-args=-fflag-name" ../y.sh cargo build
源码层面的依据在 base.rs:后端在创建每个 codegen 单元的 libgccjit 上下文时,会遍历会话中解析出的 llvm_args,逐个调用 add_command_line_option:
for arg in &tcx.sess.opts.cg.llvm_args {
context.add_command_line_option(arg);
}
从源码结构看,后端在此处还会自动添加一批“为兼容 Rust 语义而必需”的 GCC 选项(见 base.rs),理解它们有助于判断你手动传入的参数会与哪些内建行为交互:
-fno-strict-aliasing:Rust 依赖 LLVM 不做 TBAA(类型基础别名分析),所以显式关闭;-fwrapv:Rust 的有符号整型溢出自定义(debug 下 panic、release 下环绕)依赖有符号环绕语义,因此必须加;-fno-semantic-interposition:关闭语义插入,避免编译器对可被全局覆盖的符号做激进优化;-fno-var-tracking-assignments:编译 rustc 自举过程中的src/intrinsic/archs.rs时需要;-fno-tree-loop-distribute-patterns:当 crate 上声明了#![no_builtins]时自动添加,防止 GCC 把循环模式(如循环拷贝)替换成memcpy/memset等库调用,从而破坏用户显式声明的“不使用内置函数”语义。
这些内建选项解释了为什么向 GCC 传参时要小心:-Cllvm-args 传进去的参数是叠加在这套固定基础之上、且按 codegen 单元作用到 libgccjit 上下文上的,而不是替代它。
4. 如何在汇编 dump 中看到 personality 函数
当排查异常处理/panic 展开(unwind)相关问题时,需要确认生成的汇编里包含 personality 函数(如 _Unwind_Resume 相关的展开表与处理器函数)。文档给出的做法是让链接器保留中间文件并输出全部汇编:
CG_RUSTFLAGS="-Clink-arg=-save-temps -v -Clink-arg=-dA" ../y.sh cargo build
注意这里用的是单数 link-arg(rustc 允许多次出现以传多个参数),其中 -dA 表示输出完整汇编。保留下来的 .s 文件中即可搜索 personality 关键字,验证展开信息是否正确生成。
结合 Readme.md 中列出的 CG_GCCJIT_DUMP_* 系列环境变量,调试手段还可以更进一步,按需选择 dump 粒度:
CG_GCCJIT_DUMP_CODE:dump 最终生成的代码;CG_GCCJIT_DUMP_GIMPLE:dump 初始 GIMPLE 表示;CG_GCCJIT_DUMP_RTL/CG_GCCJIT_DUMP_RTL_ALL:dump 虚拟寄存器 RTL / 所有 RTL 遍;CG_GCCJIT_DUMP_TREE_ALL:dump 所有树(GIMPLE)遍;CG_GCCJIT_DUMP_IPA_ALL:dump 所有跨过程分析(IPA)遍;CG_GCCJIT_DUMP_TO_FILE:把 C 风格的表示 dump 到/tmp/gccjit_dumps并开启调试信息;CG_GCCJIT_DUMP_MODULE=<module_name>/CG_GCCJIT_DUMP_ALL_MODULES=1:dump 指定模块或全部模块到/tmp/reproducers/;CG_GCCJIT_KEEP_INTERMEDIATES:保留编译过程中产生的中间文件;CG_GCCJIT_VERBOSE:打开 GCC driver 的冗长输出。
因此一套常见的排查组合是:先用 -save-temps 看链接层产物,再用 CG_GCCJIT_DUMP_MODULE 把可疑的编译单元单独 dump 出来,最后结合 CG_GCCJIT_DUMP_RTL_ALL 观察 RTL 层的变换。
5. 如何查看 sysroot crate 的 LLVM IR
即使使用 GCC 后端,Rust 编译器前端到 HIR/MIR 的管线仍与 LLVM 后端共享,因此“LLVM IR”这里指的是 rustc 管线中 codegen 单元级别的中间表示文本。文档给出的方法(针对 x86_64-unknown-linux-gnu 目标):
cargo build -v --target x86_64-unknown-linux-gnu -Zbuild-std
# 从输出中复制出编译 sysroot crate 的那条 rustc 命令,并追加 --emit=llvm-ir
操作要点:
- 用
-Zbuild-std让标准库现场参与构建,用-v让 cargo 打印出完整命令; - 在打印的众多 rustc 调用中,找到你要分析的那个 sysroot crate(如
core、alloc、std)对应的命令行; - 复制该命令并在末尾追加
--emit=llvm-ir重新执行,即可得到该 crate 的 IR 文本。
这种方式不依赖任何后端特定的 dump 开关,对 sysroot crate 这种被 rustc 自举流程管理的编译目标特别实用。
6. 防止链接器对符号做 demangle
排查链接或符号相关错误时,链接器(GNU collect2/ld)默认会把 mangled 符号还原(demangle)再显示,反而让人看不清真实的符号名。此时只需在环境变量中设置:
COLLECT_NO_DEMANGLE=1
然后重新运行构建或链接命令,链接器输出的符号将保持原始 mangled 形式,方便与 Rust 命名(symbol mangling)结果逐一比对。
7. 如何使用自建的 rustc
如果你需要配合自己构建的 rustc 来测试代码生成后端,文档给出的两步流程:
- 把 stage2 编译器注册为一个 rustup 工具链:
rustup toolchain link debug-current build/x86_64-unknown-linux-gnu/stage2 - 将后端目录下的 toolchain 文件中的 channel 改为
debug-current,然后 clean 并重新构建 codegen。
对照当前仓库,这份 toolchain 配置位于 rust-toolchain.toml,内容为 channel = "nightly-2026-04-29" 及 components = ["rust-src", "rustc-dev", "llvm-tools-preview"]。把 channel 改成第 1 步注册的 debug-current 后,y.sh 及其触发的 cargo 命令都会使用你自建的 stage2 rustc,从而实现“自建 rustc + 自建 codegen 后端”的联合调试。适用前提是已经用 rustc 的 bootstrap(x.py build)构建出了 stage2 产物。
8. 如何使用自定义的 sysroot 源码路径
默认的 ./y.sh prepare 会下载并 patch 一份 sysroot 源码;如果你手头有一份修改过的 sysroot 源码,希望在构建时使用它,可以在 prepare 步骤通过 --sysroot-source 指定:
./y.sh prepare --sysroot-source /path/to/custom/source
该参数的解析逻辑在 prepare.rs 中:prepare 子命令的选项解析器识别 --sysroot-source,若其后没有值会报错 Expected a value after '--sysroot-source', found nothing,否则把路径存入 sysroot_source 字段并在后续的 sysroot 准备流程中使用。其典型用途是:你在 sysroot(如 core/std)里打了补丁或加了调试代码,希望 codegen 后端针对修改后的 sysroot 进行编译验证。
9. 如何使用 mem-trace 做内存追踪
mem-trace 是一类通过 LD_PRELOAD 注入库来拦截 malloc 等分配函数、记录分配堆栈的工具。文档特别指出一个前提条件:
rustc(这里指参与构建的工具链相关二进制)必须以不启用 jemalloc 的方式构建,mem-trace 才能重载malloc。原因是 jemalloc 以静态链接方式引入,此时LD_PRELOAD注入的库没有机会拦截到malloc调用——因为程序内部的调用直接解析到了静态链接进来的 jemalloc 符号上。
也就是说,用 mem-trace 分析构建/编译过程中的内存行为时,先确认目标二进制没有静态链接 jemalloc,否则注入不生效。这是该技巧唯一的关键点,其余用法与普通 mem-trace 工具一致(构建出 LD_PRELOAD 库后以 LD_PRELOAD=... <command> 方式运行)。
10. 如何生成并查看 GIMPLE
GIMPLE 是 GCC 的三地址形式中间表示;如果你想查看 libgccjit 针对某段代码生成的 GIMPLE,文档 gimple.md 给出了完整的手工复现方法:
- 以 GCC 自带的 JIT 测试用例
gcc/gcc/testsuite/jit.dg/test-const-attribute.c为基础,拷出create_code部分到local.c; - 在
main中获取上下文并打开 GIMPLE dump 开关,再调用create_code与编译:
int main() {
gcc_jit_context *ctxt = gcc_jit_context_acquire();
// 按需要设置 -O3
gcc_jit_context_set_int_option(ctxt, GCC_JIT_INT_OPTION_OPTIMIZATION_LEVEL, 3);
// 生成 GIMPLE 格式的关键选项
gcc_jit_context_set_bool_option(ctxt, GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE, 1);
create_code(ctxt, NULL);
gcc_jit_context_compile(ctxt);
// 也可以直接编译输出为汇编:
// gcc_jit_context_compile_to_file(ctxt, GCC_JIT_OUTPUT_KIND_ASSEMBLER, "out.s");
return 0;
}
- 编译并运行:
gcc local.c -I `pwd`/gcc/gcc/jit/ -L `pwd`/gcc-build/gcc -lgccjit -o out
LD_LIBRARY_PATH=`pwd`/gcc-build/gcc LIBRARY_PATH=`pwd`/gcc-build/gcc ./out
运行后会打印出类似函数级 GIMPLE(含标签、临时变量 D.3394、_1 等)的文本,用于确认 GCC 在给定 const 属性等条件下实际生成了怎样的中间表示。
文档还给出另一种等价的开关方式:把 GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE 换成命令行选项形式,同时可以两者共存:
gcc_jit_context_add_command_line_option(ctxt, "-fdump-tree-gimple");
用这种方式时,dump 文件会落在 /tmp/libgccjit-XXXX 临时目录中(形如 /tmp/libgccjit-9OFqkD/fake.c.006t.gimple);执行前可先清理旧目录以便定位新文件:
rm -rf /tmp/libgccjit-*
另外,若不想写独立的 C 程序,也可以直接使用后端内置的 dump 机制:设置 CG_GCCJIT_DUMP_GIMPLE 即可在正常构建流程中 dump 初始 GIMPLE(见 Readme.md)。
11. 如何构建并配置交叉编译版 libgccjit
这是文档中篇幅最大的一块,覆盖“从零到跑通 m68k 目标”的完整路径。
11.1 构建 libgccjit
按照 cross-gcc 工具仓库(cross-cg-gcc-tools 的 cross-gcc 项目)的说明构建目标架构的 GCC 交叉工具链,从中得到交叉编译版的 libgccjit。
11.2 配置 rustc_codegen_gcc
文档列出的四个步骤:
-
运行
./y.sh prepare --cross,让 sysroot 针对交叉编译场景被打补丁; -
在
config.toml中把gcc-path设为交叉版 libgccjit 的路径。仓库提供的默认配置 config.example.toml 内容为:#gcc-path = "gcc-build/gcc" download-gccjit = true即默认从 CI 下载已打好补丁的
libgccjit(download-gccjit = true);使用自建的交叉版时,需改为gcc-path = "<你的交叉 libgccjit 目录>"并注释掉download-gccjit。 -
确保目标平台的链接器(例如
m68k-unknown-linux-gnu-gcc)在$PATH中。可用CG_RUSTFLAGS="-Clinker=<linker>"显式指定链接器,并在构建 sysroot 时指定目标三元组:CG_RUSTFLAGS="-Clinker=m68k-unknown-linux-gnu-gcc" ./y.sh build --sysroot --target-triple m68k-unknown-linux-gnu -
以同样的目标与链接器构建你的项目:
CG_RUSTFLAGS="-Clinker=m68k-unknown-linux-gnu-gcc" ../y.sh cargo build --target m68k-unknown-linux-gnu
仓库的 CI 工作流 m68k.yml 正是这一流程的自动化版本:prepare --only-libcore --cross 准备 sysroot → build --sysroot --target-triple m68k-unknown-linux-gnu 构建目标 sysroot → 设置 CARGO_PROFILE_DEV_LTO=no CG_RUSTFLAGS="-Clinker=m68k-unknown-linux-gnu-gcc" 后构建 tests/hello-world 示例 → 最后把产物拷入 QEMU chroot(qemu-m68k-static)运行并校验输出为 40(见 m68k.yml)。对照 CI 可以理解两个额外细节:
- 构建示例时同时关闭 dev profile 的 LTO(
CARGO_PROFILE_DEV_LTO=no)以规避交叉 LTO 问题; - 使用 JSON target spec 构建时可加
-Zjson-target-spec(CI 中即如此),与下一步的手动 JSON 流程一致。
11.3 目标尚未被 Rust 支持时:自定义 target specification
若目标三元组还没有进入 Rust 编译器官方支持列表,需要自己编写一份 target specification JSON 文件(注意其中声明的 arch 必须是 rustc 支持的架构),然后:
-
构建 sysroot 时以绝对路径通过
--target传入该文件,同时用--target-triple指定目标三元组:./y.sh build --sysroot --target-triple m68k-unknown-linux-gnu --target $(pwd)/m68k-unknown-linux-gnu.json -
构建项目时指定该 target specification 文件:
../y.sh cargo build --target path/to/m68k-unknown-linux-gnu.json
CI 中的对应写法是把 JSON 放在工作区 target_specs/ 目录并加 -Zjson-target-spec 打开该 nightly 特性(见 m68k.yml)。
11.4 常见错误:unrecognised emulation mode
如果链接时报出:
/usr/bin/ld: unrecognised emulation mode: m68kelf
文档给出的排查方向是:确认 config.toml 中的 gcc-path 指向的是 libgccjit 的安装目录(即交叉工具链所在目录),而不是错误的其他路径——错误的 gcc-path 会导致驱动选择到宿主的默认链接器配置,从而出现宿主 ld 不认识目标 emulation mode 的报错。
12. 关键命令与参数速查
| 目的 | 命令 / 变量 | 出处 |
|---|---|---|
| 参数传给链接器 | CG_RUSTFLAGS="-Clink-args=-save-temps -v" ../y.sh cargo build |
tips.md |
| 参数传给 GCC | CG_RUSTFLAGS="-Cllvm-args=-fflag-name" ../y.sh cargo build(-Cllvm-args 被后端复用于传参给 GCC) |
tips.md、base.rs |
| 看汇编中的 personality 函数 | CG_RUSTFLAGS="-Clink-arg=-save-temps -v -Clink-arg=-dA" ../y.sh cargo build |
tips.md |
| 看 sysroot crate 的 LLVM IR | cargo build -v --target <triple> -Zbuild-std 后复制命令并追加 --emit=llvm-ir |
tips.md |
| 阻止符号 demangle | COLLECT_NO_DEMANGLE=1 |
tips.md |
| 自建 rustc | rustup toolchain link debug-current build/<triple>/stage2,并把 toolchain 文件的 channel 改为 debug-current 后 clean 重建 codegen |
tips.md、rust-toolchain.toml |
| 自定义 sysroot 源码 | ./y.sh prepare --sysroot-source /path/to/custom/source |
tips.md、prepare.rs |
| mem-trace | 相关二进制需不带 jemalloc 构建,LD_PRELOAD 才能拦截 malloc |
tips.md |
| 生成 GIMPLE | 独立 C 程序开启 GCC_JIT_BOOL_OPTION_DUMP_INITIAL_GIMPLE 或 -fdump-tree-gimple;或直接用 CG_GCCJIT_DUMP_GIMPLE |
gimple.md、Readme.md |
| 交叉编译配置 | ./y.sh prepare --cross + config.toml 的 gcc-path + -Clinker=<linker> + --target-triple |
tips.md、m68k.yml |
| 未支持目标 | 自定义 target JSON(arch 需受 rustc 支持),--target $(pwd)/<triple>.json 绝对路径 |
tips.md |
13. 小结
rustc_codegen_gcc 的日常开发调试围绕三条主线展开:
- 参数透传:
CG_RUSTFLAGS是统一入口,-Cllvm-args被后端复用为“直通 GCC”的通道(在 base.rs 中逐条注入 libgccjit 上下文),-Clink-args/-Clink-arg则作用于链接阶段; - 中间表示观察:链接层用
-save-temps -dA,GIMPLE 层用CG_GCCJIT_DUMP_GIMPLE或 gimple.md 中的独立 C 程序方法,RTL/IPA/tree 各遍有对应的CG_GCCJIT_DUMP_*开关; - 环境定制:自建 rustc(toolchain link + channel 替换)、自定义 sysroot 源码(
--sysroot-source)、交叉编译(prepare --cross+gcc-path+ 目标链接器 + 可选 target JSON),每一步都能在仓库的 CI 工作流与构建系统源码中找到可对照的实现。
以上技巧均基于当前仓库中的 tips.md 及其引用文档、构建系统源码与 CI 配置整理,适用于该后端当前所处的开发阶段(WIP 状态,测试套件预期存在部分 UI 测试失败,见 Readme.md)。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00