☰
Linux 5.16.0-rc3 Rust内核模块编译环境搭建与版本匹配实战
2026/10/22 5:13:20 网站建设 项目流程

1. 为什么要在5.16.0-rc3这个时间节点折腾Rust内核模块

把Rust代码编译进Linux内核这件事,真正动手过的人都知道,最折磨人的不是写Rust本身,而是环境搭建和版本匹配。5.16.0-rc3这个版本在Rust for Linux的历史上是个挺特殊的位置——它是Rust支持正式进入主线之前,相对成熟但又没有完全稳定的一个快照。选这个版本做实验,好处是代码结构已经比较清晰,rust/目录下的基础设施基本成型,坏处是它对外部工具链的要求非常挑剔,版本差一点就编译不过。

我最初决定在这个版本上做尝试,是因为手头有个老项目需要验证内核态的内存安全策略,用C写回调逻辑太容易出悬垂指针,想试试Rust的所有权模型能不能在内核模块这个场景下真正落地。结果光是让make LLVM=1 rustavailable通过就花了大半天。这篇文章就是把这大半天的踩坑过程、版本选择的逻辑、以及最终跑通一个最小Rust模块的完整链路整理出来。

需要先说明的是,5.16.0-rc3的Rust支持属于实验性特性,它依赖一套特定的工具链组合:特定版本的Rust编译器、匹配的rustc目标、以及bindgen。这三者任何一个版本对不上,编译就会在链接阶段或者bindgen生成绑定的时候报错。所以本文的重点不是教你写复杂的Rust内核代码,而是把"能编译、能加载、能打印一行日志"这条最小闭环打通。适合已经熟悉Linux内核模块基本概念、想尝鲜Rust内核开发的读者,也适合被版本问题卡住、想找一份可复现配置清单的人。

2. 工具链版本矩阵:5.16.0-rc3到底认哪些组合

2.1 内核Rust支持的版本约束从哪来

5.16.0-rc3的rust/目录下有个Makefile,里面通过rustc --version做检查,但它检查的不是一个精确版本号,而是一组特性开关。真正卡人的是rustc的nightly特性——这个版本的内核代码用到了#![feature(...)]里的一些不稳定特性,比如allocator_api、core_intrinsics这些。稳定版Rust编译器直接拒绝编译,所以必须用nightly。

但nightly也不是随便哪个都行。我实测下来,2021年11月到12月之间的nightly相对稳妥,太新的nightly会因为某些特性被移除或改名而报错,太旧的又缺少内核需要的特性。这里给一个我验证过的组合:

组件版本说明
Rust编译器nightly-2021-11-01特性集与5.16.0-rc3匹配
rust-src组件同编译器版本提供core/alloc源码,内核需要
bindgen0.56.0生成C头文件绑定的关键工具
LLVM13.0.0内核Rust编译依赖LLVM后端
GNU Make4.2以上内核构建系统要求

这个表格里的每一项都不是随便填的。bindgen 0.56.0是因为5.16.0-rc3的rust/bindgen_parameters里用到的参数格式和这个版本兼容,0.57之后参数解析有变化。LLVM 13是因为rustc的LLVM版本要和内核编译时用的LLVM版本尽量一致,否则在llvm-objcopy处理目标文件时可能出现段属性不匹配。

2.2 安装nightly工具链的正确姿势

用rustup管理工具链是最省事的。命令本身很简单:

rustup toolchain install nightly-2021-11-01 rustup component add rust-src --toolchain nightly-2021-11-01 rustup component add llvm-tools-preview --toolchain nightly-2021-11-01

但这里有个坑:rust-src组件必须装,因为内核编译时会去$(rustc --print sysroot)/lib/rustlib/src/rust/library下面找core和alloc的源码,然后重新编译成内核能用的rlib。如果你只装了编译器没装rust-src,编译到一半会报找不到corecrate的源码。

另一个坑是llvm-tools-preview。内核构建过程中会调用llvm-objcopy和llvm-ar,如果你系统里已经有一套LLVM,可能会和rustup自带的这套冲突。我的建议是统一用rustup自带的这套,在make的时候通过LLVM=1让内核构建系统优先使用rustup工具链里的LLVM工具,避免版本混用。

2.3 bindgen的安装与版本锁定

bindgen的安装有两种方式:cargo install bindgen或者用系统包管理器。我强烈建议用cargo install并锁定版本:

cargo install bindgen --version 0.56.0 --locked

--locked这个参数很关键,它会严格按照Cargo.lock里的依赖版本来编译,避免bindgen自己依赖的clang-sys等库被解析到不兼容的新版本。我一开始没加--locked,结果bindgen编译出来运行时报libclang符号找不到,排查了半天才发现是clang-sys的版本和系统libclang对不上。

安装完之后,把~/.cargo/bin加到PATH里,然后用bindgen --version确认输出是bindgen 0.56.0。如果系统里之前装过其他版本的bindgen,注意which bindgen指向的是不是刚装的那个。

3. 内核源码的获取与Rust相关配置开关

3.1 拉取5.16.0-rc3源码的注意事项

内核源码从官方仓库拉取,指定tag:

git clone --depth 1 --branch v5.16.0-rc3 \ https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

--depth 1是为了省时间,只拉这一个快照。如果你后续想对比其他版本,可以把--depth去掉做完整克隆,但完整克隆体积很大,第一次实验没必要。

拉下来之后先别急着编译。5.16.0-rc3的Rust支持默认是关闭的,需要手动打开几个配置项。这里有个细节:这个版本的Rust配置项在make menuconfig里藏得比较深,在General setup下面有个Rust support,打开之后才会出现子选项。

3.2 必须打开的配置项清单

用make menuconfig或者直接改.config,确保以下配置是打开的:

  • CONFIG_RUST=y:总开关,不开这个后面都免谈
  • CONFIG_RUST_IS_AVAILABLE=y:这个通常由构建系统自动检测,但如果你手动改.config要注意别漏
  • CONFIG_SAMPLES=y:编译示例模块,包括Rust的示例
  • CONFIG_SAMPLE_RUST_MINIMAL=m:最小Rust模块示例,编译成可加载模块
  • CONFIG_SAMPLE_RUST_PRINT=m:打印示例,用来验证Rust模块能正常输出

这里解释一下为什么建议先编译示例模块而不是自己写。示例模块是内核开发者已经调通的代码,它能编译通过说明你的工具链和内核配置没问题。如果示例都编译不过,那问题一定在环境上,不用去怀疑自己的Rust代码。这是排查问题的一个基本策略:先用已知能工作的代码验证环境,再引入自己的代码。

3.3 用LLVM=1统一构建后端

5.16.0-rc3的Rust支持依赖LLVM后端,所以编译命令要带上LLVM=1:

make LLVM=1 defconfig make LLVM=1 menuconfig make LLVM=1 -j$(nproc)

LLVM=1这个参数会让内核构建系统把CC、LD、AR、OBJCOPY等工具全部指向LLVM工具链。如果你不加这个参数,构建系统会用GCC编译C部分,用LLVM编译Rust部分,最后链接的时候可能出现ABI不匹配的问题。我试过一次不加LLVM=1,结果Rust模块编译出来了,但加载时报Unknown symbol,原因是C部分和Rust部分对某些内联函数的处理不一致。

注意:LLVM=1要求系统里安装的LLVM版本和rustc使用的LLVM版本尽量接近。用rustc --version --verbose可以看到rustc背后的LLVM版本,用llvm-config --version看系统LLVM版本,两者主版本号最好一致。

4. 从零编译:完整命令链路与每步的意图

4.1 环境变量准备

在开始编译之前,先把工具链路径固定下来。我习惯写一个env.sh,每次开新终端先source一下:

export RUSTC="rustup run nightly-2021-11-01 rustc" export BINDGEN="$HOME/.cargo/bin/bindgen" export LLVM=1

把RUSTC指向具体的nightly工具链,而不是依赖rustup default,是为了避免系统里默认工具链变化导致编译结果不稳定。BINDGEN显式指定路径,避免PATH里有多个bindgen时选错。

4.2 配置与编译的完整流程

第一步,生成基础配置:

make LLVM=1 defconfig

第二步,打开Rust相关选项。可以用scripts/config脚本非交互式修改,比menuconfig更适合脚本化:

scripts/config --enable RUST scripts/config --enable SAMPLES scripts/config --module SAMPLE_RUST_MINIMAL scripts/config --module SAMPLE_RUST_PRINT

第三步,处理旧配置并编译:

make LLVM=1 olddefconfig make LLVM=1 -j$(nproc)

olddefconfig这一步不能省。它会把你手动改的配置和内核的依赖关系重新对齐,自动补全缺失的依赖项。我有一次跳过这步直接make,结果CONFIG_RUST虽然打开了,但CONFIG_RUST_IS_AVAILABLE没被自动置位,编译到Rust部分直接跳过,白等了一场编译。

编译过程大概需要十几分钟到半小时,取决于机器性能。第一次编译会重新生成很多中间文件,后面增量编译就快了。

4.3 编译产物在哪里

编译成功后,Rust示例模块的产物在:

samples/rust/rust_minimal.ko samples/rust/rust_print.ko

用modinfo看一下模块信息:

modinfo samples/rust/rust_minimal.ko

如果能看到license、description这些字段,说明模块格式是正确的。这里有个经验:Rust编译出来的.ko文件和C编译出来的在格式上没有本质区别,都是标准的ELF可重定位目标,所以modinfo、insmod这些工具都能正常处理。

5. 加载验证:模块能不能跑起来才是硬道理

5.1 加载最小模块并观察输出

加载rust_minimal.ko:

sudo insmod samples/rust/rust_minimal.ko

然后用dmesg | tail看内核日志。如果看到类似Rust minimal sample (init)的输出,说明模块的初始化函数被正确调用了。再lsmod | grep rust_minimal确认模块在列表里。

卸载:

sudo rmmod rust_minimal

再看dmesg,应该能看到退出函数的输出。这一进一出都正常,说明Rust模块的生命周期管理是通的。

5.2 加载失败时的排查顺序

如果insmod报错,按这个顺序排查:

现象可能原因排查方法
Invalid module format模块和内核对不上uname -r确认当前内核版本,modinfo看模块的vermagic
Unknown symbol依赖的内核符号没导出dmesg看具体缺哪个符号,检查配置里对应功能是否编译进内核
Operation not permitted内核签名或安全策略限制确认是否开启了模块签名强制,实验环境可临时关闭
加载成功但无日志初始化函数没被调用检查模块的module_init宏是否正确展开

我遇到过一次Unknown symbol,缺的是__rust_alloc。原因是CONFIG_RUST打开了,但alloc相关的支持没有正确链接进内核。解决办法是确认rust/alloc目录下的代码被编译进了vmlinux,而不是只编译了模块。这个问题的根源是配置依赖没对齐,重新跑一遍olddefconfig就好了。

5.3 验证Rust的打印功能

rust_print.ko这个示例会演示Rust里怎么调用内核的打印宏。加载之后dmesg里会看到格式化的输出。这个模块的价值在于验证Rust和内核C代码之间的FFI(外部函数接口)是通的——Rust代码能调用C写的printk,说明bindgen生成的绑定是正确的。

如果rust_print加载后没有输出,但rust_minimal正常,那问题大概率出在bindgen生成的绑定上。可以去看rust/bindings/bindings_generated.rs这个文件,确认里面有没有printk相关的函数声明。这个文件是编译时自动生成的,如果它是空的或者缺少关键函数,说明bindgen运行的时候头文件路径不对。

6. 踩坑实录:那些让我卡了半天的版本问题

6.1 rustc版本太新导致的特性报错

我第一次尝试用的是当时最新的nightly,结果编译到rust/kernel/lib.rs时报:

error[E0554]: #![feature] may not be used on the stable release channel

这个报错看起来像是用了稳定版,但实际上我用的是nightly。真正的原因是那个nightly版本把某个特性从#![feature]列表里移除了,而内核代码还在引用它。解决办法就是退回到2021-11-01这个时间点的nightly。这个教训是:内核的Rust支持对nightly版本非常敏感,不要盲目追新。

6.2 bindgen生成的绑定缺少函数

有一次bindgen跑完了,但生成的bindings_generated.rs里只有类型定义,没有任何函数声明。排查发现是bindgen调用时传的--allowlist-function参数没生效。5.16.0-rc3的rust/Makefile里用了一个bindgen_parameters文件来传参数,这个文件的格式对bindgen版本有要求。换成0.56.0之后问题消失。

这个坑的隐蔽之处在于:bindgen不会报错,它只是安静地生成一个不完整的文件,然后编译到链接阶段才报undefined reference。所以如果你看到链接错误说某个内核函数找不到,先去看bindings_generated.rs里有没有这个函数的声明。

6.3 LLVM工具链混用导致的链接错误

系统里原本装了LLVM 12,rustup自带的是LLVM 13。编译的时候llvm-objcopy用了系统的12,rustc用了13,结果链接时报:

relocation R_X86_64_32S against `.rodata' can not be used when making a shared object

这个错误本质上是目标文件的段属性不一致。解决办法是在make命令里显式指定LLVM=1,并且确保PATH里rustup的工具链路径在系统LLVM路径之前。或者更彻底一点,把系统LLVM升级到和rustc一致的版本。

提示:判断LLVM是否混用,可以看make输出里OBJCOPY那一行用的是哪个路径的llvm-objcopy。如果路径是/usr/bin/llvm-objcopy而不是~/.rustup/toolchains/.../bin/llvm-objcopy,那就是混用了。

7. 跑通之后:这套配置还能怎么用

最小模块跑通之后,这套环境就可以用来做更进一步的实验了。比如在rust_minimal的基础上加一个字符设备,或者实现一个简单的procfs接口。5.16.0-rc3的rust/kernel模块里已经封装了一些常用的抽象,比如chrdev、miscdev、sync这些,虽然还不完整,但足够做原型验证。

我个人的体会是,这个版本的Rust内核支持最大的价值不在于能写多复杂的功能,而在于它验证了Rust的所有权模型和内核的C API之间可以建立一套可行的绑定机制。bindgen负责把C头文件翻译成Rust绑定,rust/kernel里的封装负责把裸绑定包装成安全的Rust接口,这个分层思路是后面所有Rust内核开发的基础。

如果你也想在自己的机器上复现这套流程,建议严格按照本文的版本矩阵来,不要随意升级任何一个组件。内核的Rust支持在5.16这个阶段还处于"能跑但脆弱"的状态,版本匹配的优先级高于一切。等这套最小闭环跑通之后,再去尝试修改示例代码、增加自己的逻辑,这样出问题的时候你能快速判断是环境问题还是代码问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询