Linux 内核模块开发深度实战:从 KBuild 到内核签名与安全启动

Linux 内核模块开发深度实战:从 KBuild 到内核签名与安全启动

本文从模块化开发的角度出发,系统梳理 Linux 内核模块开发的完整工具链——从 KBuild 编译系统、DKMS 动态支持、内核模块签名到 Secure Boot 安全启动集成,涵盖从开发到生产部署的全链路实战经验。


一、为什么内核模块开发仍然是系统工程师的硬通货

在 eBPF 横空出世的今天,有人问:传统内核模块开发还有必要学吗?答案是肯定的。eBPF 擅长的是"观测与策略层",而真正需要硬件交互、自定义文件系统、网络协议栈、设备驱动的场景,仍然必须依赖内核模块。

内核模块开发的核心挑战在于:你写的代码运行在 Ring 0,一个指针错误就能让整个系统崩溃(Kernel Panic)。因此 Linux 社区为内核模块开发构建了一整套从编译、加载、调试到安全分发的完整工具链。

本文将覆盖四个核心维度:

  1. KBuild 编译系统——理解内核是如何被模块编译链接的
  2. DKMS 动态内核模块支持——解决升级内核后模块自动重编译的问题
  3. 内核模块签名——确保模块的完整性和来源可信
  4. Secure Boot 集成——在生产环境中启用 UEFI Secure Boot 加载自定义模块

二、KBuild 编译系统深度解析

2.1 KBuild 的核心设计哲学

KBuild 不是一个新的构建系统,而是在 Makefile 基础上的一层抽象。它的设计哲学非常优雅:顶层 Makefile 定义通用规则,子目录 Makefile 只描述"要构建什么"。

一个典型的内核模块 Makefile 长这样:

# Makefile
obj-m += hello.o
hello-objs := hello_main.o hello_utils.o

KERNEL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

这段 Makefile 做了什么?

  • obj-m 告诉 KBuild:编译成一个可加载的内核模块(.ko 文件)
  • hello-objs 指定了 hello.c 拆分成多个 .o 文件时的链接顺序
  • M=$(PWD) 是 KBuild 的关键参数——告诉内核源码树"外部模块在哪个目录"
  • KBuild 内部会读取内核的 .config 和 autoconf.h,确保你的模块和当前内核使用完全相同的编译选项和符号版本

2.2 多文件模块与条件编译

当模块规模增长时,需要更复杂的 KBuild 配置:

# Makefile for multi-file module
obj-m += mydriver.o
mydriver-objs := mydriver_main.o mydriver_ioctl.o mydriver_dma.o mydriver_debugfs.o

# 根据内核配置条件编译
ccflags-y += -DDEBUG
ccflags-y += -I$(PWD)/include

# 测试覆盖率
GCOV_PROFILE := y

对应的 Kbuild 文件(也可以使用 Kbuild 而不是 Makefile):

# Kbuild
obj-y += mydriver.o
mydriver-y := mydriver_main.o mydriver_ioctl.o

# 如果内核配置开启了 CONFIG_MYDRV_FEATURE_X,才编译这个文件
mydriver-$(CONFIG_MYDRV_FEATURE_X) += mydriver_feature_x.o

# CFLAGS 覆盖
CFLAGS_mydriver_dma.o := -O2 -fno-inline-functions-called-once

2.3 编译产物与 vermagic 校验

编译完成后,.ko 文件中嵌入了一段 vermagic 字符串,格式类似于:

5.15.0-91-generic SMP preempt mod_unload modversions

这段字符串包含了内核版本、配置选项、SMP 支持状态等元信息。当你尝试 insmod 一个模块时,内核会严格校验 vermagic。如果编译环境和运行环境不匹配,会出现经典的错误:

mydriver: disagrees about version of symbol module_layout
module_init: ERROR: could not insert module mydriver.ko: Invalid module format

解决策略: - DKMS(自动重编译见第三节) - 使用 modprobe --force(危险操作,仅调试) - 确保开发环境和生产环境的内核头文件完全一致


三、DKMS——跨内核版本自动构建

3.1 DKMS 解决什么问题

在 Linux 运维中,一个常见的痛点是:系统内核升级后,第三方内核模块失效(常见于 NVIDIA 网卡驱动、文件系统、网络过滤模块等)。DKMS(Dynamic Kernel Module Support)的理念非常简单:把模块源码注册到 DKMS 数据库,任何内核升级事件触发时,DKMS hook 自动重编译模块。

3.2 DKMS 实战配置

以一个示例模块 example-dkms 为例:

dkms.conf:

PACKAGE_NAME="example-dkms"
PACKAGE_VERSION="1.0.0"
BUILT_MODULE_NAME[0]="example"
DEST_MODULE_LOCATION[0]/extra
AUTOINSTALL="YES"
REMAKE_INITRD="YES"

# 自定义编译参数
MAKE[0]="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build modules"
CLEAN="make -C ${kernel_source_dir} M=${dkms_tree}/${PACKAGE_NAME}/${PACKAGE_VERSION}/build clean"

注册到 DKMS:

# 1. 安装到 DKMS 目录
sudo mkdir -p /usr/src/example-dkms-1.0.0
sudo cp -r * /usr/src/example-dkms-1.0.0/

# 2. 添加、构建、安装
sudo dkms add -m example-dkms -v 1.0.0
sudo dkms build -m example-dkms -v 1.0.0
sudo dkms install -m example-dkms -v 1.0.0

# 3. 验证
dkms status
# example-dkms/1.0.0, 5.15.0-91-generic, x86_64: installed

3.3 生产环境 DKMS 最佳实践

在 CI/CD 中集成 DKMS:

# 在你的项目 Makefile 中添加 dkms 目标
DKMODNAME := example-dkms
DKMODVERSION := 1.0.0
MODSRCROOT := /usr/src/$(DKMODNAME)-$(DKMODVERSION)

.PHONY: dkms-install dkms-uninstall

dkms-install: clean all
    sudo mkdir -p $(MODSRCROOT)
    sudo rsync -a --exclude='.git' --exclude='*.cmd' ./ $(MODSRCROOT)/
    sudo dkms add -m $(DKMODNAME) -v $(DKMODVERSION)
    sudo dkms build -m $(DKMODNAME) -v $(DKMODVERSION)
    sudo dkms install -m $(DKMODNAME) -v $(DKMODVERSION)

dkms-uninstall:
    sudo dkms remove $(DKMODNAME)/$(DKMODVERSION) --all
    sudo rm -rf $(MODSRCROOT)

DKMS hook 在 Ubuntu/Debian 系统中自动集成到 apt 的 kernel 更新流程:

/etc/kernel/postinst.d/dkms  -> 内核安装后触发
/etc/kernel/prerm.d/dkms     -> 内核卸载前清理

这意味着内核自动升级后,所有注册的 DKMS 模块会被自动重编译并安装到新内核目录 /lib/modules/<new-kernel>/updates/ 中。


四、内核模块签名——代码完整性的最后防线

4.1 为什么需要签名

现代 Linux 发行版通过 CONFIG_MODULE_SIG 强制要求模块签名。签名的核心目的:

  • 防篡改:模块在编译后被修改则校验失败
  • 防加载非信任模块:未签名模块在 Secure Boot 启用后无法加载
  • 供应链安全:类似于 iOS 的代码签名,确保模块来自可验证的开发者

4.2 签名流程实战

Step 1:生成 MOK(Machine Owner Key)密钥对

# 生成自签名密钥对
openssl req -new -x509 -newkey rsa:2048 \
  -keyout mok.key \
  -outform DER -out mok.der \
  -nodes -days 36500 \
  -subj "/CN=Kernel Module Signing Key/"

# 生成证书(X.509)用于注册到 MOK list
openssl x509 -in mok.der -inform DER -out mok.pem -outform PEM

Step 2:注册 MOK 密钥到 UEFI

# 注册到 Shim 和固件
sudo mokutil --import mok.der
# 重启后进入 MOK Manager 界面,确认导入
sudo reboot

重启后 UEFI 固件会进入 MOK Manager 蓝色界面,按照提示确认导入新密钥。这是安全设计的关键——恶意软件无法在用户不知情的情况下注册自己的密钥。

Step 3:签名内核模块

# 使用内核自带的 signing 脚本
/usr/src/linux-headers-$(uname -r)/scripts/sign-file \
  sha256 mok.key mok.pem mydriver.ko

# 验证签名
tail -c 256 mydriver.ko | hexdump -C
# 模块尾部会增加 ~256 字节的签名数据

# 或者使用 modinfo 查看签名信息
modinfo mydriver.ko | grep signer
# signer: Kernel Module Signing Key

Step 4:配置内核签名策略

# 查看当前签名策略
cat /proc/sys/kernel/modules_disabled
# 0 = 可以接受未签名模块(仅警告)
# 1 = 完全禁止加载新模块(启动后锁定)

# 查看 Secure Boot 状态
mokutil --sb-state
# SecureBoot enabled

在启用了 Secure Boot 的系统上,内核默认拒绝加载未签名模块。尝试 insmod 会得到:

insmod: ERROR: could not insert module mydriver.ko: Required key not available

4.3 CI/CD 中的自动化签名

#!/bin/bash
# sign-module.sh - 用于 CI 流水线的模块签名脚本

MODULE_PATH=$1
KEY_DIR=${2:-"/opt/ci-keys"}
HASH_ALGO="sha256"

SIGN_FILE="/usr/src/linux-headers-$(uname -r)/scripts/sign-file"

if [ ! -f "$SIGN_FILE" ]; then
    echo "ERROR: sign-file script not found. Install linux-headers package."
    exit 1
fi

if [ -z "$CI_SIGN_PASSWORD" ]; then
    echo "ERROR: CI_SIGN_PASSWORD environment variable not set."
    exit 1
fi

# 使用 GPG 或 OpenSSL 对密钥进行访问控制
echo "$CI_SIGN_PASSWORD" | gpg --batch --yes --passphrase-fd 0 \
    --decrypt "${KEY_DIR}/mok.key.gpg" > /tmp/mok-decrypted.key

$HASH_ALGO $SIGN_FILE ${KEY_DIR}/mok.pem /tmp/mok-decrypted.key "$MODULE_PATH"

# 清理
shred -u /tmp/mok-decrypted.key

echo "Module signed: $MODULE_PATH"

五、Secure Boot 集成——从安全启动到模块验证

5.1 Secure Boot 信任链

UEFI Secure Boot 建立了一条从固件到 OS 内核到模块的完整信任链:

UEFI Firmware
    ↓ (PK - Platform Key)
Shim / GRUB
    ↓ (KEK - Key Exchange Key)
vmlinuz (Kernel image, 已签名)
    ↓ (MOK - Machine Owner Key)
Kernel Modules (已签名)

关键点: - PK:平台密钥,由硬件 OEM 或最终用户控制 - KEK:密钥交换密钥,用于更新 DB/DBX - DB:签名数据库,存储允许的公钥/哈希 - DBX:禁止签名数据库,存储已吊销的密钥 - MOK:用户自定义密钥,由 shim 管理

5.2 在生产环境中部署签名模块

假设你是一个 Linux 发行版维护者或企业内部 OS 管理员,标准部署流程:

Step 1:准备签名密钥基础设施

# 硬件安全模块(HSM)或 KMS 中生成私钥
# 公钥证书发布在企业内部 Wiki/LDAP

Step 2:构建阶段签名

# 在 RPM/DEB 包构建流水线中
rpmbuild --sign \
  --define "_gpg_name Kernel Signing Key" \
  --define "_gpg_path /opt/gnupg" \
  kernel-module-package.src.rpm

Step 3:部署阶段验证

# 企业端验证脚本
modinfo --field=signer mydriver.ko | grep "Trusted Enterprise Key"
if [ $? -ne 0 ]; then
    echo "UNSIGNED MODULE DETECTED - REJECTING"
    exit 1
fi

modprobe mydriver

5.3 紧急情况分析:模块被吊销

如果发现签名密钥泄露,需要吊销:

# 将泄露的证书哈希添加到 DBX
sudo mokutil --revoke-import mok-revoke.der

# 重启进入 Manager 确认吊销
sudo reboot

# 吊销后所有使用该密钥签名的模块将拒绝加载
# 也就是 "Required key not available" 错误

六、调试与故障排查工具箱

6.1 printk 与动态调试

// 使用 printk 记录内核日志
#include <linux/kernel.h>
#include <linux/module.h>

static int __init mydriver_init(void)
{
    pr_info("mydriver: initializing, version %s\n", DRIVER_VERSION);
    pr_debug("mydriver: debug mode enabled\n");
    // ...
    return 0;
}

module_init(mydriver_init);

// 启用动态调试 (无需重新编译)
// echo "module mydriver +p" > /sys/kernel/debug/dynamic_debug/control

6.2 使用 QEMU 调试内核模块

不冒着物理机崩溃的风险调试内核模块:

# 1. 编译带 debug info 的内核
make defconfig
make kvm_guest.config
./scripts/config --enable DEBUG_INFO
make -j$(nproc)

# 2. 创建最小 rootfs using initramfs
echo "mydriver.ko" > initramfs.list
find . -name "mydriver.ko" | cpio -o -H newc | gzip > initrd.gz

# 3. QEMU 启动
qemu-system-x86_64 \
  -kernel arch/x86/boot/bzImage \
  -initrd initrd.gz \
  -append "console=ttyS0 nokaslr" \
  -nographic \
  -gdb tcp::1234 \
  -S

# 4. 另一个终端用 GDB 连接
gdb vmlinUX
(gdb) target remote :1234
(gdb) add-symbol-file mydriver.ko 0xffffffffc0000000
(gdb) b mydriver_init
(gdb) c

注意:-nokaslr 选项在此处调试时使用,生产环境请勿使用。

6.3 SystemTap 与 eBPF 辅助分析

# 使用 eBPF 追踪模块加载行为
bpftrace -e '
kprobe:do_init_module {
    printf("Loading module: %s name=%s\n", 
           str(((struct module *)arg0)->name));
}

kprobe:load_module {
    printf("Loading module %s size=%d\n",
           str(((struct module *)arg0)->name),
           ((struct module *)arg0)->core_size);
}'

七、经验总结:内核模块开发的「安全红线」

基于多年 Linux 内核开发与运维经验,总结以下安全红线:

红线 1:永远不要在生产环境使用 --force 加载模块

modprobe --force 跳过 vermagic 和签名校验,可能导致: - 内核 ABI 不匹配导致的随机崩溃 - 模块与内核版本之间数据结构的隐式偏移

红线 2:DKMS 包必须严格版本化

版本号变更必须触发 rebuild,否则模块源码变更后旧版本 .ko 仍在系统中残留。

红线 3:签名密钥生命周期管理

  • 私钥必须存储在 HSM 或隔离的 KMS 中
  • 证书过期时间不超过 2 年
  • 吊销证书必须立即同步到所有部署节点的 DBX

红线 4:生产环境启用 lockdown 模式

# 启用 lockdown (integrity 或 confidentiality 模式)
echo confidentiality > /sys/kernel/security/lockdown

# 启用 lockdown 后:
# - /dev/mem 和 /dev/kmem 被禁用
# - ACPI 自定义方法被阻止
# - 未签名模块加载被阻止
# - kexec 加载未签名内核被阻止

lockdown=confidentiality 是保护内核模块攻击面的最后防线,但需要确保所有模块已正确签名——否则会导致系统功能异常。


八、总结

Linux 内核模块开发是一个系统工程——它不仅需要理解操作系统内核原理,更需要掌握从编译工具链、版本管理、构建安全到部署的整个生命周期。本文梳理的 KBuild → DKMS → 签名 → Secure Boot 流程,构成了现代 Linux 内核模块的工程化闭环。

随着 Rust for Linux 项目的推进,未来内核模块的安全边界还将进一步提升——通过借用检查器在编译阶段消除空指针解引用和数据竞争等传统 C 语言痛点。但无论语言如何演进,对内核模块签名认证机制和 Secure Boot 信任链的理解,都是 Linux 系统工程师不可或缺的核心能力。


作者注:本文相关示例代码与完整 Makefile 已整理到 GitHub 仓库,欢迎参考交流。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部