首页
/ rustc_codegen_gcc 开发实用技巧:向 GCC 传参、查看 GIMPLE 与汇编、配置交叉编译

rustc_codegen_gcc 开发实用技巧:向 GCC 传参、查看 GIMPLE 与汇编、配置交叉编译

2026-09-06 17:46:53作者:裴锟轩Denise

本文基于 Rust 仓库中 GCC 代码生成后端 rustc_codegen_gcc 的开发者文档 tips.md 整理而成,系统讲解日常开发调试中会用到的各类小技巧:如何把参数透传给 GCC 链接器和 GCC 本身、如何查看汇编 dump 中的 personality 函数、如何查看 sysroot crate 的 LLVM IR、如何防止链接器对符号做 demangle、如何用自建 rustc 与自定义 sysroot 源码路径构建、如何用 mem-trace 做内存追踪,以及如何构建并配置交叉编译用的 libgccjit。读完后你可以独立使用 CG_RUSTFLAGSconfig.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

操作要点:

  1. -Zbuild-std 让标准库现场参与构建,用 -v 让 cargo 打印出完整命令;
  2. 在打印的众多 rustc 调用中,找到你要分析的那个 sysroot crate(如 coreallocstd)对应的命令行;
  3. 复制该命令并在末尾追加 --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 来测试代码生成后端,文档给出的两步流程:

  1. 把 stage2 编译器注册为一个 rustup 工具链:
    rustup toolchain link debug-current build/x86_64-unknown-linux-gnu/stage2
    
  2. 将后端目录下的 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 给出了完整的手工复现方法:

  1. 以 GCC 自带的 JIT 测试用例 gcc/gcc/testsuite/jit.dg/test-const-attribute.c 为基础,拷出 create_code 部分到 local.c
  2. 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;
}
  1. 编译并运行:
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

文档列出的四个步骤:

  1. 运行 ./y.sh prepare --cross,让 sysroot 针对交叉编译场景被打补丁;

  2. config.toml 中把 gcc-path 设为交叉版 libgccjit 的路径。仓库提供的默认配置 config.example.toml 内容为:

    #gcc-path = "gcc-build/gcc"
    download-gccjit = true
    

    即默认从 CI 下载已打好补丁的 libgccjitdownload-gccjit = true);使用自建的交叉版时,需改为 gcc-path = "<你的交叉 libgccjit 目录>" 并注释掉 download-gccjit

  3. 确保目标平台的链接器(例如 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
    
  4. 以同样的目标与链接器构建你的项目:

    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.mdbase.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.mdrust-toolchain.toml
自定义 sysroot 源码 ./y.sh prepare --sysroot-source /path/to/custom/source tips.mdprepare.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.mdReadme.md
交叉编译配置 ./y.sh prepare --cross + config.tomlgcc-path + -Clinker=<linker> + --target-triple tips.mdm68k.yml
未支持目标 自定义 target JSON(arch 需受 rustc 支持),--target $(pwd)/<triple>.json 绝对路径 tips.md

13. 小结

rustc_codegen_gcc 的日常开发调试围绕三条主线展开:

  1. 参数透传CG_RUSTFLAGS 是统一入口,-Cllvm-args 被后端复用为“直通 GCC”的通道(在 base.rs 中逐条注入 libgccjit 上下文),-Clink-args/-Clink-arg 则作用于链接阶段;
  2. 中间表示观察:链接层用 -save-temps -dA,GIMPLE 层用 CG_GCCJIT_DUMP_GIMPLEgimple.md 中的独立 C 程序方法,RTL/IPA/tree 各遍有对应的 CG_GCCJIT_DUMP_* 开关;
  3. 环境定制:自建 rustc(toolchain link + channel 替换)、自定义 sysroot 源码(--sysroot-source)、交叉编译(prepare --cross + gcc-path + 目标链接器 + 可选 target JSON),每一步都能在仓库的 CI 工作流与构建系统源码中找到可对照的实现。

以上技巧均基于当前仓库中的 tips.md 及其引用文档、构建系统源码与 CI 配置整理,适用于该后端当前所处的开发阶段(WIP 状态,测试套件预期存在部分 UI 测试失败,见 Readme.md)。

登录后查看全文
热门项目推荐
相关项目推荐