1. 为什么要在意 Rust for Linux 这件事
Rust 进入 Linux 内核主线这件事,从 2021 年那几轮补丁开始就一直是圈内热点。5.16.0-rc3 这个时间点很特殊,它是 Rust for Linux 支持第一次以相对完整形态出现在内核源码树里的版本之一,虽然还挂着 RFC 的尾巴,但基础设施、Kconfig 开关、样例模块都已经能跑通了。很多人看到新闻的第一反应是“内核要换语言了”,实际并不是——Rust 只是作为第二语言被引入,用于编写驱动和模块,核心调度、内存管理这些还是 C 的天下。
那为什么还要折腾编译?因为你想真正上手写一个 Rust 内核模块,第一步不是写代码,而是让整个工具链跑起来。这中间涉及内核源码、Rust 编译器版本、bindgen、libclang、LLVM 版本、目标架构配置等一堆东西的匹配。我前后在三个不同环境上试过,踩的坑足够写一篇避雷指南。这篇内容就是把我实际编译 5.16.0-rc3 的完整过程拆开讲,包括每个参数为什么这么选、版本为什么必须卡死、报错怎么定位。适合已经会编译普通 Linux 内核、想进一步尝试 Rust 模块的开发者,也适合对内核构建系统好奇、想了解 Rust 如何嵌入 C 项目的人。
核心关键词就几个:Rust for Linux、内核源码编译、5.16.0-rc3、bindgen、LLVM、Kconfig。下面所有操作都围绕这些展开,不跑题。
2. 编译前的整体思路与版本选型逻辑
2.1 为什么版本匹配是生死线
Rust for Linux 在早期阶段对版本极其敏感。内核 5.16.0-rc3 对应的 Rust 支持补丁要求 rustc 版本在 1.56 到 1.57 附近,bindgen 要求 0.56 左右,libclang 要求 LLVM 12 或 13。你如果随手装个最新的 rustc 1.75,编译内核模块时大概率会在core::ffi或者alloc相关的 trait 上炸掉,因为内核里用的#![no_std]环境和标准库的 ABI 假设在版本间有变动。
我试过用 rustup 默认的 stable 去编,报错信息是error[E0463]: can't find crate for 'core',看起来像缺组件,实际是 target 配置和编译器内置的 core 版本对不上。后来锁定到 1.57.0 才顺利通过。所以第一条经验:不要用最新版,用内核文档里明确写的版本。5.16-rc3 的Documentation/rust/quick-start.rst里写的是 rustc 1.56.0 及以上,但实测 1.57.0 最稳。
2.2 工具链三件套的依赖关系
整个编译链里三个东西必须协同:
- rustc:负责把
.rs编译成目标文件。它自带core、alloc、compiler_builtins等 crate 的源码,编译内核模块时需要重新编译这些 crate 以适配内核的no_std环境。 - bindgen:负责把 C 头文件转成 Rust 的 FFI 绑定。它依赖 libclang 来解析 C 代码,所以 libclang 的版本决定了 bindgen 能识别哪些语法。
- libclang:LLVM 的 C 接口库。内核里大量宏、
static inline函数、位域结构体,bindgen 靠它来生成正确的repr(C)布局。
这三者的版本关系是:rustc 决定 bindgen 的可用范围,bindgen 决定 libclang 的最低版本。5.16-rc3 时期,官方推荐 bindgen 0.56.0 + libclang 12.0。我实际用 libclang 13.0 也能过,但 14 以上会出现clang诊断信息格式变化导致 bindgen 解析失败。
2.3 目标架构与交叉编译的取舍
如果你只是在 x86_64 本机上跑,那目标就是x86_64-unknown-linux-gnu,不需要交叉编译。但如果你想在 ARM64 上跑,就需要aarch64-unknown-linux-gnu的 Rust target,同时内核的CROSS_COMPILE也要配好。我建议第一次尝试一定用 x86_64 本机,减少变量。等本机跑通了,再去折腾交叉编译,否则报错你都分不清是 Rust 的问题还是交叉工具链的问题。
另外,内核配置里CONFIG_RUST依赖CONFIG_HAVE_RUST,而HAVE_RUST只在部分架构上被 select。5.16-rc3 里 x86_64 和 arm64 是支持的,riscv 还在补。所以选 x86_64 最省事。
3. 环境准备与依赖安装的实操细节
3.1 基础构建工具不能省
编译内核本身需要一堆工具,Rust 部分又额外加了几样。我列一个完整的清单,按安装顺序来:
# 基础内核编译工具 sudo apt update sudo apt install -y build-essential flex bison libssl-dev libelf-dev \ bc dwarves zstd libncurses-dev # Rust 相关 sudo apt install -y llvm-13 clang-13 libclang-13-dev lld-13这里注意llvm-13和clang-13要版本一致,libclang-13-dev提供libclang.so,bindgen 运行时通过LIBCLANG_PATH找它。如果你系统里已经有其他版本的 LLVM,比如 14 或 15,那libclang.so可能指向新版本,导致 bindgen 行为异常。我遇到过libclang-14和bindgen 0.56组合下,解析include/linux/compiler_types.h时卡死,换回 13 就正常。
3.2 rustup 安装与版本锁定
不要用系统包管理器装的 rustc,版本太旧且组件不全。用 rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env rustup install 1.57.0 rustup default 1.57.0 rustup component add rust-src rustup component add llvm-tools-previewrust-src是必须的,因为内核编译时会用-Z build-std重新编译 core 和 alloc。llvm-tools-preview提供llvm-objcopy等工具,内核的objcopy步骤会用到。
装完后验证:
rustc --version # 应该输出 rustc 1.57.0 rustc --print sysroot # 确认 sysroot 路径3.3 bindgen 的安装与路径配置
bindgen 可以用 cargo 装:
cargo install bindgen --version 0.56.0 --locked--locked很重要,保证依赖树和发布时一致,避免某个间接依赖升级导致 API 变化。装完后~/.cargo/bin/bindgen就是可执行文件。内核构建系统会通过BINDGEN变量找它,默认就在 PATH 里。
libclang 的路径要显式指定,因为内核的 Makefile 不会自动探测:
export LIBCLANG_PATH=/usr/lib/llvm-13/lib你可以用llvm-config-13 --libdir确认具体路径。如果这个变量没设,bindgen 会报Unable to find libclang。
3.4 内核源码的获取与分支确认
5.16.0-rc3 是候选版本,不在稳定分支里。直接从内核官方仓库拿:
git clone --depth 1 --branch v5.16-rc3 \ https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git cd linux--depth 1只拉一个提交,省时间。如果你需要后续打补丁,可以不加--depth。确认一下 Rust 相关文件存在:
ls rust/ # 应该看到 Makefile、kernel、alloc 等目录 ls Documentation/rust/ # quick-start.rst 等文档如果rust/目录不存在,说明你拿到的不是带 Rust 支持的版本,需要检查 tag 是否正确。
4. 内核配置与 Rust 支持的开启方法
4.1 从 defconfig 开始还是从零配
我建议从defconfig开始,然后手动开 Rust 相关选项。直接make menuconfig从零配太耗时,而且容易漏掉依赖。x86_64 的默认配置:
make defconfig然后打开配置界面:
make menuconfig在General setup里找到Rust support,把它设为y。这个选项会连带 selectHAVE_RUST、RUST_IS_AVAILABLE等。如果你在 menuconfig 里搜不到 Rust support,说明你的源码树里init/Kconfig没有包含rust/Kconfig,检查一下init/Kconfig末尾有没有source "rust/Kconfig"。
4.2 关键 Kconfig 选项逐个解释
开完 Rust support 后,还有几个子选项需要确认:
| 选项 | 作用 | 建议值 |
|---|---|---|
| CONFIG_RUST | 总开关 | y |
| CONFIG_RUST_IS_AVAILABLE | 检测工具链是否可用 | 自动 |
| CONFIG_RUSTC_VERSION | 记录 rustc 版本 | 自动 |
| CONFIG_RUST_OPTIMIZATION_LEVEL | 优化等级 | 2 |
| CONFIG_RUST_DEBUG_ASSERTIONS | 调试断言 | y(开发时) |
| CONFIG_SAMPLES_RUST | 编译 Rust 样例模块 | y |
CONFIG_SAMPLES_RUST一定要开,它会编译samples/rust/下的样例模块,是验证工具链是否正常的最快方式。如果这个能编过,说明 rustc、bindgen、libclang 都配对了。
4.3 用 scripts/config 做非交互式配置
如果你要写脚本自动化,可以用scripts/config:
scripts/config --enable RUST scripts/config --enable SAMPLES_RUST scripts/config --set-val RUST_OPTIMIZATION_LEVEL 2 make olddefconfigolddefconfig会把新增的依赖自动补全,避免手动处理 select 关系。这一步之后,.config里应该能看到CONFIG_RUST=y和CONFIG_SAMPLES_RUST=y。
注意:如果你在 menuconfig 里开了 Rust 但
olddefconfig后CONFIG_RUST又变回 n,说明RUST_IS_AVAILABLE检测失败,通常是 rustc 版本不对或rust-src组件没装。
5. 编译过程与核心环节拆解
5.1 第一次编译:先编 Rust 样例
不要一上来就make -j$(nproc)全量编译,那样出错信息会被淹没。先单独编 Rust 样例:
make LLVM=1 rust_samplesLLVM=1告诉内核用 LLVM 工具链而不是 GNU 工具链。虽然内核主体还是用 gcc 编,但 Rust 部分需要 clang 来生成 LLVM IR,所以这个变量必须加。如果你不加,会报clang: not found或者llvm-objcopy: not found。
编译过程中你会看到类似输出:
RUSTC [M] samples/rust/rust_minimal.o BINDGEN samples/rust/rust_minimal.bindings.h CC [M] samples/rust/rust_minimal.mod.o LD [M] samples/rust/rust_minimal.ko如果走到LD这步,恭喜你,工具链通了。如果卡在BINDGEN,多半是 libclang 路径问题;卡在RUSTC,多半是 rustc 版本或rust-src问题。
5.2 全量编译的并行度与内存控制
样例编过后,可以全量编:
make LLVM=1 -j$(nproc)但注意,Rust 编译比 C 编译吃内存。每个rustc进程可能占 1-2 GB,如果你nproc是 16 而内存只有 16 GB,很容易 OOM。我建议并行度设为min(nproc, 内存GB/2)。比如 16 GB 内存就用-j8。
如果中途 OOM,内核的 OOM killer 会杀掉 rustc,报错信息可能是signal: 9或Killed。这时候降低并行度重跑即可,已经编好的目标文件不会重编。
5.3 编译产物在哪里
Rust 模块的产物和 C 模块一样,在samples/rust/下:
samples/rust/rust_minimal.ko samples/rust/rust_minimal.o samples/rust/rust_minimal.bindings.o.ko就是最终的内核模块,可以用insmod加载。.bindings.o是 bindgen 生成的绑定代码编译出的目标文件,里面是 C 头文件对应的 Rust FFI 声明。
你可以用modinfo看一下:
modinfo samples/rust/rust_minimal.ko应该能看到license: GPL、description: Rust minimal sample等信息。
5.4 加载与卸载测试
加载前先确认内核版本匹配:
uname -r # 必须和你编译的内核版本一致,否则 insmod 会报 version magic 错误如果你是在当前运行内核上编的模块,直接:
sudo insmod samples/rust/rust_minimal.ko dmesg | tail # 应该看到 "Rust minimal sample (init)" sudo rmmod rust_minimal dmesg | tail # 应该看到 "Rust minimal sample (exit)"如果insmod报Invalid module format,说明模块编译时的内核配置和当前运行内核不一致,需要重启到新编的内核或者用--force(不推荐)。
6. 常见报错与排查速查
6.1 报错信息与对应原因
| 报错 | 原因 | 解决 |
|---|---|---|
can't find crate for 'core' | rust-src 没装或版本不对 | rustup component add rust-src |
Unable to find libclang | LIBCLANG_PATH 未设 | export LIBCLANG_PATH=/usr/lib/llvm-13/lib |
clang: not found | 没加 LLVM=1 | make LLVM=1 |
unknown argument: -Zbuild-std | rustc 版本太旧 | 升级到 1.57.0 |
bindgen: command not found | bindgen 没装或不在 PATH | cargo install bindgen --version 0.56.0 |
Killed或signal: 9 | 内存不足 | 降低-j并行度 |
version magic mismatch | 模块与运行内核不匹配 | 重启到新内核或重新编译 |
6.2 我踩过的三个坑
第一个坑是 libclang 版本冲突。我系统里原本有 LLVM 14,libclang.so指向 14,bindgen 0.56 解析include/linux/types.h时直接段错误。后来用update-alternatives把默认 libclang 切到 13 才解决。所以如果你机器上有多个 LLVM,一定要确认LIBCLANG_PATH指向正确版本。
第二个坑是 rustc 的-Z build-std需要 nightly 特性,但 1.57.0 是 stable。实际上内核构建系统用的是RUSTC_BOOTSTRAP=1环境变量来在 stable 上开 nightly 特性。如果你手动跑 rustc 命令测试,不加这个变量会报-Z参数未知。内核 Makefile 里已经设了,不用你操心,但你要知道这个机制。
第三个坑是CONFIG_RUST开了但CONFIG_SAMPLES_RUST没开,导致make rust_samples找不到目标。rust_samples这个 make target 只在SAMPLES_RUST=y时才存在。所以配置时两个都要开。
6.3 如何确认工具链版本匹配
写一个简单的检查脚本:
#!/bin/bash echo "rustc: $(rustc --version)" echo "bindgen: $(bindgen --version)" echo "libclang: $(llvm-config-13 --version)" echo "LIBCLANG_PATH: $LIBCLANG_PATH" ls $LIBCLANG_PATH/libclang.so* 2>/dev/null || echo "libclang.so not found"输出里 rustc 应该是 1.57.0,bindgen 是 0.56.0,libclang 是 13.x。三者都对了,编译基本不会出大问题。
7. 从编译到写第一个 Rust 内核模块
7.1 样例模块的代码结构
samples/rust/rust_minimal.rs是最简单的模块,核心就两个函数:
#![no_std] #![feature(allocator_api, global_asm)] use kernel::prelude::*; module! { type: RustMinimal, name: b"rust_minimal", author: b"Rust for Linux Contributors", description: b"Rust minimal sample", license: b"GPL", } struct RustMinimal; impl kernel::Module for RustMinimal { fn init(_name: &'static CStr, _module: &'static ThisModule) -> Result<Self> { pr_info!("Rust minimal sample (init)\n"); Ok(RustMinimal) } } impl Drop for RustMinimal { fn drop(&mut self) { pr_info!("Rust minimal sample (exit)\n"); } }module!宏展开后生成 C 侧的module_init和module_exit符号,impl Module对应初始化,Drop对应卸载。pr_info!是内核的打印宏,底层调用printk。
7.2 自己加一个模块的步骤
在samples/rust/下新建my_rust_mod.rs,复制rust_minimal.rs改个名字,然后在samples/rust/Makefile里加一行:
obj-$(CONFIG_SAMPLES_RUST) += my_rust_mod.o再在samples/rust/Kconfig里加一个配置项:
config SAMPLE_RUST_MY_MOD tristate "My Rust module" depends on SAMPLES && RUST help This is my first Rust kernel module.然后make menuconfig里选中它,重新make LLVM=1 rust_samples就能编出来。这个流程和加 C 模块几乎一样,区别只是源文件后缀和编译规则由rust/Makefile里的规则处理。
7.3 调试技巧:printk 与 dmesg
Rust 模块里没有println!,只能用pr_info!、pr_err!、pr_debug!这些宏。pr_debug!默认不输出,需要开CONFIG_DYNAMIC_DEBUG并在运行时用echo 'module my_rust_mod +p' > /sys/kernel/debug/dynamic_debug/control打开。
如果模块加载后 dmesg 没输出,先确认dmesg -n 8把控制台日志级别调到最高,再确认pr_info!的日志级别是 6,低于默认控制台级别 7 的话不会打到串口,但 dmesg 里能看到。
8. 这套流程的适用边界与后续扩展
5.16.0-rc3 的 Rust 支持还处于早期,能做的事情有限:只能写最简单的字符设备、platform driver 的骨架,很多内核 API 还没有 Rust 绑定。比如你要操作struct file_operations,得自己用 bindgen 生成绑定,然后手写unsafe块调用。到了 6.x 系列,Rust 抽象层完善了很多,kernel::file、kernel::io_buffer这些模块都有了,写起来舒服得多。
所以如果你只是想体验一下,5.16-rc3 足够;如果你想真正用 Rust 写生产级驱动,建议直接上 6.1 或更新的 LTS 版本,工具链要求也变成了 rustc 1.62+、bindgen 0.60+,但整体流程和这篇里讲的完全一样,只是版本号换一下。
另外,编译过程中生成的.bindings.o文件可以单独拿出来看,里面是 bindgen 生成的 Rust 代码对应的目标文件。如果你想研究某个 C 结构体在 Rust 里长什么样,可以用bindgen --output手动生成一份.rs文件,比在内核构建系统里翻来翻去快得多。
我个人在实际操作中的体会是,Rust for Linux 的编译难点不在 Rust 本身,而在工具链版本的对齐和 libclang 的路径管理。只要把 rustc、bindgen、libclang 三者的版本锁死,剩下的就是普通内核编译的流程。第一次编建议留出至少 30 GB 磁盘空间和 8 GB 以上内存,全量编译大概 20 到 40 分钟,取决于机器性能。编过一次之后,后续增量编译就很快了,改一个 Rust 文件重新编也就几秒钟。