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
- 执行
make,生成hello.ko - 执行
sudo insmod hello.ko加载模块 - 执行
lsmod | grep hello确认加载 - 执行
sudo rmmod hello卸载模块 - 执行
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 在编译外部模块时的核心流程:
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 的变量赋值和编译流程,有助于排查复杂构建问题。
完成本章的环境搭建后,你已经具备了编译和加载第一个内核模块的能力。接下来的章节将深入模块参数、内存分配和并发控制等核心主题。