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\n"); return my_drv_init(); } static void _my_drv_exit(void) { printk("exiting my driver\n"); 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 = sec +