2. 内核模块基础:Linux内核模块的Hello World、模块加载与卸载、Makefile编写

在深入Netfilter钩子编程之前,必须掌握Linux内核模块(Loadable Kernel Module, LKM)的基本开发流程。内核模块是运行在内核空间的动态可加载代码片段,Netfilter钩子函数正是以内核模块的形式注册到网络协议栈中的。本章通过一个经典的“Hello World”模块,系统讲解模块的编写、编译、加载与卸载全流程。

2.1 内核模块与用户态程序的本质区别

内核模块运行在内核空间,拥有最高特权级(Ring 0),可以直接访问硬件和内核数据结构。与用户态程序相比,存在以下关键差异:

对比维度 用户态程序 内核模块
运行空间 用户空间(Ring 3) 内核空间(Ring 0)
入口函数 main() module_init() / module_exit()
标准库支持 完整glibc 无,仅使用内核API
内存管理 虚拟地址空间,缺页异常 直接物理内存,需谨慎
错误处理 可崩溃,进程隔离 崩溃即内核panic
调试方式 GDB断点 printk / ftrace / kprobe
关键认知:内核模块的一个bug可能导致整个系统崩溃。在开发Netfilter钩子时,必须时刻注意指针安全、锁竞争和内存泄漏问题。

2.2 Hello World内核模块源码解析

一个最小化的内核模块包含两个核心函数:初始化函数(模块加载时调用)和清理函数(模块卸载时调用)。下面是一个完整的示例:

/* hello.c - 最小化内核模块示例 */
#include <linux/module.h>    /* 所有模块都需要 */
#include <linux/kernel.h>    /* printk() 宏 */
#include <linux/init.h>      /* __init / __exit 宏 */

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Netfilter World! Module loaded.\n");
    printk(KERN_DEBUG "Current PID: %d\n", current->pid);
    return 0;  /* 返回0表示加载成功 */
}

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

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Netfilter Course");
MODULE_DESCRIPTION("A simple Hello World kernel module for Netfilter hook practice");
MODULE_VERSION("1.0");
代码要点:
  • __init 和 __exit 宏将函数放入特殊内存段,模块加载/卸载后可释放,节省内存。
  • printk() 是内核版的printf,通过 KERN_INFO 等宏指定日志级别。
  • MODULE_LICENSE("GPL") 是必须的,否则内核会标记模块为“污染”(tainted)。
  • current 宏指向当前进程的 task_struct,在内核模块中可直接访问。

2.3 模块加载与卸载的生命周期

内核模块从编译完成到最终卸载,经历以下状态转换:

内核模块生命周期状态机 编译 (.ko) 加载 (insmod) 运行中 卸载 (rmmod) 已移除 insmod init()成功 rmmod exit()完成 注意:如果 init() 返回非零值,模块加载失败,不会进入“运行中”状态。

实际操作中,使用以下命令管理模块:

命令 功能 内核对应函数
insmod hello.ko 加载模块,调用 module_init sys_init_module()
rmmod hello 卸载模块,调用 module_exit sys_delete_module()
modprobe hello 智能加载(自动处理依赖) 同上,但解析依赖
lsmod 列出当前已加载模块 读取 /proc/modules
modinfo hello.ko 查看模块元信息 解析 .ko 文件
常见陷阱:
  • 使用 rmmod 时不要带 .ko 后缀,只写模块名。
  • 如果模块被其他模块依赖,或内核正在使用模块导出的符号,卸载会失败。
  • Netfilter钩子注册后,必须确保在卸载前注销钩子,否则内核会访问已释放的内存。

2.4 Makefile编写:内核模块的编译系统

内核模块不能使用普通的gcc编译,必须借助内核的Kbuild构建系统。Makefile的核心是调用内核源码树的Makefile来编译外部模块。

# Makefile - 编译Hello World内核模块
# 目标模块名
obj-m := hello.o

# 当前内核源码树路径(通常自动获取)
KDIR := /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

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

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

# 安装模块到标准位置(可选)
install:
    $(MAKE) -C $(KDIR) M=$(PWD) modules_install
Makefile关键点解析:
  • obj-m := hello.o:告诉Kbuild将 hello.c 编译成内核模块 hello.ko。
  • KDIR:指向当前运行内核的构建树,包含头文件和编译规则。
  • -C $(KDIR):切换到内核源码目录执行make。
  • M=$(PWD):指定外部模块源码路径。
  • 如果模块由多个源文件组成,使用 模块名-objs := file1.o file2.o 语法。

2.5 多文件模块与模块参数

实际Netfilter钩子模块通常包含多个源文件。下面展示多文件模块的Makefile写法,以及如何向模块传递参数:

/* params.c - 演示模块参数传递 */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>

/* 模块参数:可在insmod时指定 */
static int hook_priority = 0;
static char *iface_name = "eth0";

module_param(hook_priority, int, 0644);
module_param(iface_name, charp, 0644);

MODULE_PARM_DESC(hook_priority, "Netfilter hook priority (default: 0)");
MODULE_PARM_DESC(iface_name, "Network interface to monitor (default: eth0)");

static int __init params_init(void)
{
    printk(KERN_INFO "Module loaded with priority=%d, iface=%s\n",
           hook_priority, iface_name);
    return 0;
}

static void __exit params_exit(void)
{
    printk(KERN_INFO "Module unloaded.\n");
}

module_init(params_init);
module_exit(params_exit);
MODULE_LICENSE("GPL");
# Makefile - 多文件模块示例
obj-m := netfilter_demo.o
netfilter_demo-objs := main.o hook.o utils.o params.o

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

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

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

加载带参数的模块:

sudo insmod netfilter_demo.ko hook_priority=10 iface_name="wlan0"
调试技巧:使用 dmesg | tail 查看 printk 输出。如果日志级别高于控制台日志级别(通常为KERN_WARNING),需要运行 dmesg 查看完整日志。

2.6 编译与测试流程总结

完成一个内核模块从编写到验证的完整步骤:

  1. 编写源码:创建 hello.c 和 Makefile。
  2. 编译:在源码目录执行 make,生成 hello.ko。
  3. 加载:sudo insmod hello.ko,观察 dmesg 输出。
  4. 验证:lsmod | grep hello 确认模块已加载。
  5. 卸载:sudo rmmod hello,再次检查 dmesg。
  6. 清理:make clean 删除编译产物。
模块开发工作流 编写源码 make 编译 insmod 加载 dmesg 验证 rmmod 卸载 调试循环:修改源码 → 重新编译 → 重新加载