最近,我用Rust重写了一个2W+行C代码的linux内核模块。在此记录一点经验。我此前没写过内核模块,认识比较疏浅,有错误欢迎指正。
为什么要重写?
这个模块2W+行代码量看起来不多,却在线上时常故障,永远改不完。十多年的老代码,经手了无数程序员,没人能解决其中的内存安全问题。拿过来一看,代码中的确有不少会产生UB的写法,线上的故障从core来看都飘得太远,难以定位根本原因在哪里。所以我没有把握(没有能力)在原代码基础上能将所有线上故障修复。 而Rust是一个现代的、高性能、无GC、内存安全的编程语言,我想它非常适合用来重写这个内核模块。
Hello World
首先来介绍下如何用Rust写linux内核模块吧。也可以参考这里, 该项目正在尝试写一个safe的rust内核框架,目前的状态还不实用,我没使用该框架,仅参考了其基本编译配置。
基本思路就是分别建立一个linux内核c工程和rust的hello world工程,把它们放到一块儿(不放到一块儿也行),文件分布如下:
├── Cargo.toml
├── Makefile
├── mydriver.c
└── src
└── lib.rs
然后在linux内核模块的入口和出口函数分别调用rust中实现的入口和出口函数,rust中将入口、出口函数标记为extern "C"
,所有业务逻辑在Rust中完成。
// mydriver.c
// ... include headers
extern int my_drv_init(void); // defined in rust
extern void my_drv_exit(void); // defined in rust
static int _my_drv_init(void)
{
printk("loading my drivern");
return my_drv_init();
}
static void _my_drv_exit(void)
{
printk("exiting my drivern");
my_drv_exit();
}
module_init(_my_drv_init);
module_exit(_my_drv_exit);
// lib.rs
#[no_mangle]
pub extern "C" fn my_drv_init() -> i32 {
KLogger::install();
info!("loading my driver in rust");
0
}
#[no_mangle]
pub extern "C" fn my_drv_exit() {
info!("exiting my driver in rust");
}
Cargo.toml中需要配置输出staticlib
:
[lib]
name = "mydriver"
crate-type = ["staticlib", "rlib"]
模块的Makefile调用cargo编译rust库,然后将其和c一块儿链接成ko,大概这个样子:
MODNAME = mydriver
KDIR ?= /lib/modules/$(shell uname -r)/build
BUILD_TYPE = release
LIB_DIR = target/$(ARCH)-linux-kernel/$(BUILD_TYPE)
all:
$(MAKE) -C $(KDIR) M=$(CURDIR)
clean:
$(MAKE) -C $(KDIR) M=$(CURDIR) clean
rm -rf target
rlib:
# 目前需要nightly才能编译core和alloc.
cargo +nightly build --$(BUILD_TYPE) -Z features=dev_dep,build_dep -Z build-std=core,alloc --target=$(ARCH)-linux-kernel
obj-m := $(MODNAME).o
$(MODNAME)-objs := mydriver.o mydriver.rust.o
.PHONY: $(src)/lib$(MODNAME).a
$(src)/lib$(MODNAME).a:
cd $(src); make rlib
cd $(src); cp $(LIB_DIR)/lib$(MODNAME).a .
%.rust.o: lib%.a
$(LD) -r -o $@.tmp --whole-archive $<
$(src)/plt2pc.py $@.tmp $@
可行性评估
用Rust写linux内核模块还是有些担忧,目前还没看到Rust内核模块相关的严肃开源项目,Demo倒是有两个。动手之前,咱们还是尽可能评估一下可行性。之前有了解到有工具C2Rust可以将C代码转换成Rust代码,所以,我的想法是先用C2Rust将原有C代码转成Rust,看能不能编译跑起来,各功能是否正常,看看有没有什么硬伤。如果能正常使用,则可以在转出的代码的基础上逐渐将unsafe rust重构为safe rust。
按照C2Rust相关文档操作下来,遇到几个问题:
- 转换时内核头文件的时候报错。
/usr/src/kernels/.../arch/x86/include/asm/jump_label.h:16:2: error: 'asm goto' constructs are not supported yet
asm_volatile_goto("1:"
^
include/linux/compiler-gcc4.h:79:43: note: expanded from macro 'asm_volatile_goto'
# define asm_volatile_goto(x...) do { asm goto(x); asm (""); } while (0)
据C2Rust文档介绍,需要最新的libclang才能支持此语法。
2. 转换后的代码编译报错。
编译错误大致分为memcpy宏、内联汇编错误、依赖libc crate几类。
以上错误中,libc的依赖仅仅使用了libc中定义的一些C语言基本类型,因此,可以写一个简单的libc crate替代。其它错误均通过临时修改内核头文件,将不支持的语法define成其他替代品规避。
3. 编译成功后的ko文件加载报错。
加载ko报如下错误:
insmod: ERROR: could not insert module mp.ko: Invalid module format
dmesg显示:
Unknown rela relocation: 4
这是由于Rust编译器(LLVM)生成的二进制中对于extern “C