资讯详情

资讯详情

Linux 5.16.0-rc3 Rust内核模块编译环境搭建与版本匹配实战

1. 为什么要在5.16.0-rc3这个时间节点折腾Rust内核模块把Rust代码编译进Linux内核这件事真正动手过的人都知道最折磨人的不是写Rust本身而是环境搭建和版本匹配。5.16.0-rc3这个版本在Rust for Linux的历史上是个挺特殊的位置——它是Rust支持正式进入主线之前相对成熟但又没有完全稳定的一个快照。选这个版本做实验好处是代码结构已经比较清晰rust/目录下的基础设施基本成型坏处是它对外部工具链的要求非常挑剔版本差一点就编译不过。我最初决定在这个版本上做尝试是因为手头有个老项目需要验证内核态的内存安全策略用C写回调逻辑太容易出悬垂指针想试试Rust的所有权模型能不能在内核模块这个场景下真正落地。结果光是让make LLVM1 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的时候通过LLVM1让内核构建系统优先使用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源码的注意事项内核源码从官方仓库拉取指定taggit 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_RUSTy总开关不开这个后面都免谈CONFIG_RUST_IS_AVAILABLEy这个通常由构建系统自动检测但如果你手动改.config要注意别漏CONFIG_SAMPLESy编译示例模块包括Rust的示例CONFIG_SAMPLE_RUST_MINIMALm最小Rust模块示例编译成可加载模块CONFIG_SAMPLE_RUST_PRINTm打印示例用来验证Rust模块能正常输出这里解释一下为什么建议先编译示例模块而不是自己写。示例模块是内核开发者已经调通的代码它能编译通过说明你的工具链和内核配置没问题。如果示例都编译不过那问题一定在环境上不用去怀疑自己的Rust代码。这是排查问题的一个基本策略先用已知能工作的代码验证环境再引入自己的代码。3.3 用LLVM1统一构建后端5.16.0-rc3的Rust支持依赖LLVM后端所以编译命令要带上LLVM1make LLVM1 defconfig make LLVM1 menuconfig make LLVM1 -j$(nproc)LLVM1这个参数会让内核构建系统把CC、LD、AR、OBJCOPY等工具全部指向LLVM工具链。如果你不加这个参数构建系统会用GCC编译C部分用LLVM编译Rust部分最后链接的时候可能出现ABI不匹配的问题。我试过一次不加LLVM1结果Rust模块编译出来了但加载时报Unknown symbol原因是C部分和Rust部分对某些内联函数的处理不一致。注意LLVM1要求系统里安装的LLVM版本和rustc使用的LLVM版本尽量接近。用rustc --version --verbose可以看到rustc背后的LLVM版本用llvm-config --version看系统LLVM版本两者主版本号最好一致。4. 从零编译完整命令链路与每步的意图4.1 环境变量准备在开始编译之前先把工具链路径固定下来。我习惯写一个env.sh每次开新终端先source一下export RUSTCrustup run nightly-2021-11-01 rustc export BINDGEN$HOME/.cargo/bin/bindgen export LLVM1把RUSTC指向具体的nightly工具链而不是依赖rustup default是为了避免系统里默认工具链变化导致编译结果不稳定。BINDGEN显式指定路径避免PATH里有多个bindgen时选错。4.2 配置与编译的完整流程第一步生成基础配置make LLVM1 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 LLVM1 olddefconfig make LLVM1 -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.kosudo 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看模块的vermagicUnknown 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 12rustup自带的是LLVM 13。编译的时候llvm-objcopy用了系统的12rustc用了13结果链接时报relocation R_X86_64_32S against .rodata can not be used when making a shared object这个错误本质上是目标文件的段属性不一致。解决办法是在make命令里显式指定LLVM1并且确保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这个阶段还处于能跑但脆弱的状态版本匹配的优先级高于一切。等这套最小闭环跑通之后再去尝试修改示例代码、增加自己的逻辑这样出问题的时候你能快速判断是环境问题还是代码问题。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →