https://zhuanlan.zhihu.com/p/137077998
最近,我用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 driver
");
return my_drv_init();
}
static void _my_drv_exit(void)
{
printk("exiting my driver
");
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工作流
按照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”
函数的访问,采用的是R_X86_64_PLT32
标记重定位,Linux4.15内核开始支持此标记,而我们使用的3.x内核仅支持R_X86_64_PC32
标记。内核中相应提交可以看出内核对这两个标记是无区别对待的:
"PLT32 relocation is used as marker for PC-relative branches. Because
of EBX, it looks odd to generate PLT32 relocation on i386 when EBX
doesn't have GOT.
As for symbol resolution, PLT32 and PC32 relocations are almost
interchangeable. But when linker sees PLT32 relocation against a
protected symbol, it can resolved locally at link-time since it is
used on a branch instruction. Linker can't do that for PC32
relocation"
but for the kernel use, the two are basically the same, and this
commit gets things building and working with the current binutils
master - Linus
因此,我们可以简单地将编译出的二进制文件中的PLT32
标记替换为PC32
就能解决此问题。readelf命令可以帮我们找出这些标记都在什么位置,故甚至都不需要了解elf文件结构,可以写脚本完成替换:
#!/usr/bin/env python
import sys
import os
import re
py3 = sys.version_info.major >= 3
def get_relocs(filename):
"""
readelf output:
Relocation section '.rela.text' at offset 0x1e8 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000a00000002 R_X86_64_PC32 0000000000000000 hello - 4
Relocation section '.rela.eh_frame' at offset 0x200 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
"""
relocs = []
sec = ''
idx = 0
os.environ["LANG"] = ''
f = os.popen('readelf -r "%s"' % filename)
while True:
line = f.readline()
if not line:
break
if line.startswith('Relocation section'):
arr = re.findall(r'0x[0-9a-f]*', line)
sec = int(arr[0], base=16)
idx = 0
f.readline()
continue
off = idx * 24 + 8
idx += 1
arr = line.strip().split()[:4]
if len(arr) != 4:
continue
offset, info, typ, val = arr
if typ != 'R_X86_64_PLT32':
continue
relocs.append((sec, off, val))
return relocs
def main():
PLT32 = 4 if py3 else 'x04'
PC32 = 2 if py3 else 'x02'
infile = sys.argv[1]
outfile = sys.argv[2] if len(sys.argv) == 3 else infile
obj = list(open(infile, 'rb').read())
for sec, offset, val in get_relocs(infile):
goff