内核模块开发实战

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_char

6. 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 modules

8. 调试

QEMU 测试(推荐)

qemu-system-x86_64 \
    -kernel linux/arch/x86/boot/bzImage \
    -initrd initramfs.cpio \
    -nographic -append "console=ttyS0" -m 512M

QEMU 隔离测试环境,模块 bug 只影响虚拟机不损坏主机。

理解 Oops

内核 Oops 包含 RIP 指令地址,用 addr2line 翻译为源码位置:

addr2line -e samples/rust/my_module.o -f 0x42

9. 安全实践

避免的陷阱:

  • 不用 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引用计数循环