内核模块开发实战
1. 开发环境准备
软件依赖
# Debian/Ubuntu
sudo apt install -y build-essential flex bison libelf-dev libssl-dev bc cpio clang lld llvm-dev libclang-dev
# Rust 工具链
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 安装内核所需的 rustc 版本
rustup override set $(scripts/min-tool-version.sh rustc)
rustup component add rust-src rustfmt clippy
cargo install bindgen-cli验证环境:make LLVM=1 rustavailable 输出 “Rust is available!”。
内核配置
make LLVM=1 x86_64_defconfig
make LLVM=1 menuconfig # General setup -> Rust support -> [*] Rust support
# 或脚本配置:
scripts/config --enable CONFIG_RUST
scripts/config --enable CONFIG_SAMPLE_RUST_MINIMAL
make LLVM=1 olddefconfig参阅 设备驱动(C) 理解底层驱动模型,中断与系统调用 理解中断处理。
2. 最小内核模块
use kernel::prelude::*;
module! {
type: HelloModule,
name: "hello_rust",
author: "Rust for Linux Contributors",
description: "A hello world kernel module in Rust",
license: "GPL",
}
struct HelloModule;
impl kernel::Module for HelloModule {
fn init(_module: &'static ThisModule) -> Result<Self> {
pr_info!("Hello from Rust kernel module!\n");
Ok(HelloModule)
}
}
impl Drop for HelloModule {
fn drop(&mut self) {
pr_info!("Goodbye from Rust kernel module!\n");
}
}module! 宏
module! 宏展开后注册内核模块结构体,支持字段:
| 字段 | 必填 | 说明 |
|---|---|---|
type | 是 | 实现 Module trait 的类型 |
name | 是 | 模块名称(insmod/rmmod) |
author | 是 | 作者 |
description | 是 | 描述 |
license | 是 | 许可证 |
params | 否 | 模块参数块 |
Kconfig 和 Makefile
config SAMPLE_RUST_MY_HELLO
tristate "My Hello World Module (Rust)"
depends on RUST
help
This option builds a hello world kernel module in Rust.obj-$(CONFIG_SAMPLE_RUST_MY_HELLO) += my_hello.o编译和加载
make LLVM=1 -j$(nproc) modules
sudo insmod my_hello.ko
sudo dmesg | tail -5 # Hello from Rust kernel module!
sudo rmmod my_hello
sudo dmesg | tail -3 # Goodbye from Rust kernel module!3. 内核日志
| Rust 宏 | C 等效 | 用途 |
|---|---|---|
pr_emerg! | pr_emerg() | 系统崩溃 |
pr_err! | pr_err() | 错误条件 |
pr_warn! | pr_warn() | 警告 |
pr_info! | pr_info() | 信息 |
pr_debug! | pr_debug() | 调试(默认不打印) |
注意:pr_info! 不会自动换行,需显式 \n;消息大小受内核日志缓冲区限制。
4. 模块参数
module! {
type: ParamModule,
name: "rust_params",
license: "GPL",
params: {
my_int: i32 {
default: 42,
permissions: 0o644,
description: "An integer parameter",
},
my_bool: bool {
default: true,
permissions: 0,
description: "A boolean parameter",
},
},
}
// 使用参数:
pr_info!("my_int: {}\n", my_int());
pr_info!("my_bool: {}\n", my_bool());支持类型:i8/i16/i32/i64/u8/u16/u32/u64/bool/str/ByteParam。
5. 创建字符设备
use kernel::{chrdev, file::{File, FileOperations}, io_buffer::{IoBufferReader, IoBufferWriter}, sync::Mutex, prelude::*};
module! { type: CharDeviceModule, name: "rust_char", license: "GPL", /* ... */ }
struct DeviceData { content: Mutex<Vec<u8>>, capacity: usize }
#[vtable]
impl FileOperations for DeviceData {
type Data = kernel::sync::Arc<Self>;
type OpenData = ();
fn open(_context: &Self::OpenData, _file: &File) -> Result<Self::Data> {
Arc::new(DeviceData::new(4096)?, GFP_KERNEL)
}
fn read(data: &Self, _file: &File, writer: &mut impl IoBufferWriter, offset: u64) -> Result<usize> {
let offset = offset as usize;
let guard = data.content.lock();
if offset >= guard.len() { return Ok(0); }
writer.write_slice(&guard[offset..])?;
Ok(guard.len() - offset)
}
fn write(data: &Self, _file: &File, reader: &mut impl IoBufferReader, offset: u64) -> Result<usize> {
let offset = usize::try_from(offset).map_err(|_| Error::ERANGE)?;
let mut guard = data.content.lock();
if guard.len() < offset + reader.len() {
guard.resize(offset + reader.len(), 0);
}
let n = reader.read_slice(&mut guard[offset..])?;
Ok(n)
}
fn release(_data: Self::Data, _file: &File) {}
}
struct CharDeviceModule { _dev: chrdev::Registration<1> }
impl kernel::Module for CharDeviceModule {
fn init(module: &'static ThisModule) -> Result<Self> {
let chrdev_reg = chrdev::Registration::new_pinned(module, "rust_char", 0)?;
let major = chrdev_reg.as_ref().major();
pr_info!("rust_char: registered major {}, use mknod /dev/rust_char c {} 0\n", major, major);
Ok(CharDeviceModule { _dev: chrdev_reg })
}
}加载测试:
sudo insmod rust_char.ko
sudo mknod /dev/rust_char c $(grep rust_char /proc/devices | awk '{print $1}') 0
echo "Hello" > /dev/rust_char
cat /dev/rust_char
sudo rmmod rust_char6. ioctl 支持
const IOCTL_CLEAR: u32 = 0x7001;
const IOCTL_GET_SIZE: u32 = 0x7002;
fn ioctl(data: &DeviceData, _file: &File, cmd: u32, _arg: usize) -> Result<u32> {
match cmd {
IOCTL_CLEAR => { data.content.lock().clear(); Ok(0) }
IOCTL_GET_SIZE => { Ok(data.content.lock().len() as u32) }
_ => Err(Error::ENOTTY),
}
}7. 树外模块 vs 树内模块
| 属性 | 树内模块 | 树外模块 |
|---|---|---|
| 位置 | 内核源码树内 | 任意目录 |
| 构建 | make modules | 需要指向内核构建目录 |
| 贡献 | 可提交主线 | 仅本地开发 |
| 推荐 | 正式开发 | 快速原型 |
树外模块 Makefile:
KDIR ?= /lib/modules/$(shell uname -r)/build
obj-m := my_module.o
all: $(MAKE) -C $(KDIR) M=$(PWD) LLVM=1 modules8. 调试
QEMU 测试(推荐)
qemu-system-x86_64 \
-kernel linux/arch/x86/boot/bzImage \
-initrd initramfs.cpio \
-nographic -append "console=ttyS0" -m 512MQEMU 隔离测试环境,模块 bug 只影响虚拟机不损坏主机。
理解 Oops
内核 Oops 包含 RIP 指令地址,用 addr2line 翻译为源码位置:
addr2line -e samples/rust/my_module.o -f 0x429. 安全实践
避免的陷阱:
- 不用
unwrap()/expect()— 用ok_or(Error::EINVAL)? - 不用
x[off]索引 — 用x.get(off).ok_or(Error::ERANGE)? - 不用
x / y— 用x.checked_div(y).ok_or(Error::EINVAL)? - 注意除零、整数溢出、大分配失败
- 内核栈很小(8KB/16KB),避免递归和大局部变量
| 陷阱 | Rust 保护 | 仍需关注 |
|---|---|---|
| UAF | 借用检查器 | unsafe 块中的手动管理 |
| 缓冲区溢出 | 切片边界检查 | unsafe { get_unchecked } |
| 竞争条件 | Send/Sync | 中断上下文中的死锁 |
| 内存泄漏 | RAII/Drop | 引用计数循环 |