2. 开发环境搭建:内核源码树获取、内核头文件安装、Makefile编写、Kbuild系统简介

在开始编写内核模块之前,必须搭建一个与目标运行内核严格匹配的开发环境。内核模块与用户态程序不同,它直接链接到内核符号表,任何版本或配置的不一致都可能导致模块加载失败(Invalid module format)或系统崩溃。本章将系统讲解如何获取内核源码树、安装内核头文件、编写标准 Makefile 以及理解 Kbuild 构建系统。

2.1 内核源码树获取

内核模块的编译需要完整的 内核源码树(Kernel Source Tree)或至少是 编译好的内核头文件(Kernel Headers)。源码树包含了编译模块所需的所有 Makefile、Kconfig 文件以及全局头文件。

2.1.1 方法一:使用发行版仓库安装(推荐)

对于大多数基于 Debian/Ubuntu 的发行版,可以直接安装当前运行内核的源码包:

# 查看当前内核版本
uname -r

# 安装内核头文件(包含部分构建所需头文件,但不含完整源码)
sudo apt update
sudo apt install linux-headers-$(uname -r)

# 安装完整内核源码(可选,用于深度调试)
sudo apt install linux-source-$(uname -r | cut -d'-' -f1)
cd /usr/src
sudo tar -xaf linux-source-*.tar.xz

2.1.2 方法二:从 kernel.org 获取

如果需要特定版本或自定义内核,直接从 kernel.org 下载:

wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.6.tar.xz
tar -xvf linux-6.6.tar.xz
cd linux-6.6

2.1.3 方法三:使用 Git 克隆

git clone --depth 1 --branch v6.6 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
cd linux
版本匹配原则: 模块编译时的内核版本必须与加载模块的目标内核版本完全一致。使用 uname -r 确认版本号,并确保源码树已执行过 make oldconfig 或 make prepare。

2.2 内核头文件安装

内核头文件是编译模块的最小依赖集合。它们位于 /lib/modules/$(uname -r)/build/ 目录下,通常是一个指向源码树或头文件目录的符号链接。

组件 路径 说明
内核头文件 /lib/modules/$(uname -r)/build/ 编译模块时 -C 指向的目录
模块目录 /lib/modules/$(uname -r)/kernel/ 已安装的内核模块存放位置
符号表 /lib/modules/$(uname -r)/build/Module.symvers 导出符号的 CRC 校验值

验证头文件是否正确安装:

ls -l /lib/modules/$(uname -r)/build/
# 应包含 include/ 目录、Makefile、Module.symvers 等
常见错误: 如果 /lib/modules/$(uname -r)/build/ 不存在或指向错误路径,编译时会报错 *** No rule to make target 'modules_prepare'。此时需要重新安装对应版本的 linux-headers 包。

2.3 Makefile 编写

内核模块的 Makefile 与普通 C 项目不同,它必须使用内核的 Kbuild 系统。以下是一个标准模板:

# Makefile —— 内核模块编译模板

# 模块名称
obj-m := hello.o

# 如果模块由多个源文件组成,使用模块名加 -y 变量
# hello-objs := main.o helper.o

# 当前路径
PWD := $(shell pwd)

# 内核源码树路径(指向已安装的头文件或源码)
KDIR := /lib/modules/$(shell uname -r)/build

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

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

install:
	$(MAKE) -C $(KDIR) M=$(PWD) modules_install
	depmod -a

2.3.1 Makefile 关键变量解析

变量/目标 作用
obj-m 指定要编译成模块的目标文件(.o),Kbuild 会自动生成 .ko
KDIR 内核构建目录的路径,必须指向 /lib/modules/.../build
M=$(PWD) 告诉 Kbuild 返回当前目录编译模块
modules Kbuild 目标,编译所有 obj-m 指定的模块
modules_install 将编译好的 .ko 安装到 /lib/modules/.../extra/

2.3.2 多文件模块示例

# 假设模块由 a.c 和 b.c 组成,最终生成 mydriver.ko
obj-m := mydriver.o
mydriver-objs := a.o b.o

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

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
编译与加载流程:
  1. 执行 make,生成 hello.ko
  2. 执行 sudo insmod hello.ko 加载模块
  3. 执行 lsmod | grep hello 确认加载
  4. 执行 sudo rmmod hello 卸载模块
  5. 执行 dmesg | tail 查看内核日志输出

2.4 Kbuild 系统简介

Kbuild(Kernel Build System)是 Linux 内核的构建系统,它基于 GNU Make 但扩展了大量专用语法。当我们在模块目录执行 make -C /lib/modules/.../build M=$(pwd) modules 时,Kbuild 会接管编译过程。

2.4.1 Kbuild 核心工作流程

下图展示了 Kbuild 在编译外部模块时的核心流程:

Kbuild 外部模块编译流程 Kbuild 外部模块编译流程 用户执行 make 递归调用 $(KDIR)/Makefile Kbuild 解析 obj-m 变量 编译 .c → .o → .ko 生成 .ko 文件 关键路径 KDIR 必须指向 /lib/modules/.../build M=$(PWD) 返回模块目录

2.4.2 Kbuild 常用赋值语法

语法 示例 说明
obj-m obj-m := mymod.o 将目标编译为可加载模块
obj-y obj-y := mymod.o 将目标编译进内核(内置)
module-objs mymod-objs := a.o b.o 多文件模块的对象文件列表
ccflags-y ccflags-y := -DDEBUG -O2 传递给 GCC 的额外编译标志
ldflags-y ldflags-y := --strip-debug 传递给链接器的额外标志

2.4.3 Kbuild 与普通 Makefile 的区别

特性 普通 Makefile Kbuild Makefile
编译命令 直接调用 gcc 通过 make -C KDIR 间接调用
变量赋值 自定义变量 使用 obj-m、ccflags-y 等专用变量
依赖处理 手动管理 .d 文件 自动生成 .cmd 依赖文件
交叉编译 需手动设置 CROSS_COMPILE 通过 ARCH= 和 CROSS_COMPILE= 统一支持
模块签名 不支持 自动支持 CONFIG_MODULE_SIG 签名

2.5 第一个模块:Hello World

创建一个简单的模块来验证环境是否搭建成功。

2.5.1 源文件 hello.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Kernel Module!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Kernel Module!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");

2.5.2 编译与测试

# 编译
make

# 加载模块
sudo insmod hello.ko

# 查看模块信息
modinfo hello.ko

# 查看内核日志
dmesg | tail -5

# 卸载模块
sudo rmmod hello

# 再次查看日志
dmesg | tail -5
调试技巧: 如果 dmesg 没有输出,检查 /var/log/kern.log 或使用 journalctl -k。某些发行版默认屏蔽了 KERN_INFO 级别的日志到控制台。

2.6 常见编译错误与解决方案

错误信息 原因 解决方案
*** No rule to make target 'modules_prepare' KDIR 指向的目录未准备好 安装 linux-headers-$(uname -r) 或执行 make prepare
ERROR: "symbol" [module.ko] undefined! 模块使用了未导出的内核符号 检查 /proc/kallsyms 或使用 EXPORT_SYMBOL
Invalid module format 版本魔术字不匹配 确保 KDIR 与 uname -r 完全一致
disagrees about version of symbol 符号 CRC 校验失败 重新编译模块或更新 Module.symvers

2.7 本章小结

搭建正确的开发环境是内核模块编程的第一步,也是最重要的一步。核心要点包括:

  • 版本严格匹配: 编译环境的内核版本必须与运行环境一致。
  • KDIR 指向正确: 确保 /lib/modules/$(uname -r)/build 存在且有效。
  • Makefile 遵循 Kbuild 规范: 使用 obj-m 和 make -C 递归调用。
  • Kbuild 自动化: 理解 Kbuild 的变量赋值和编译流程,有助于排查复杂构建问题。

完成本章的环境搭建后,你已经具备了编译和加载第一个内核模块的能力。接下来的章节将深入模块参数、内存分配和并发控制等核心主题。