3. 内核模块基础:模块的加载与卸载、module_init/module_exit、LICENSE声明、内核打印printk

内核模块是Linux内核在运行时动态扩展功能的机制。在系统调用表Hook技术中,所有拦截逻辑都必须以内核模块的形式加载到内核空间。理解模块的加载与卸载生命周期、入口/出口函数声明、许可证合规性以及内核调试输出,是编写稳定、可维护的Hook模块的前提。

3.1 内核模块的生命周期

一个内核模块从编译完成到从内核中移除,经历以下四个阶段:

  1. 加载(Loading):通过 insmod 或 modprobe 将 .ko 文件插入内核地址空间。
  2. 初始化(Initialization):内核自动调用模块中声明的 module_init() 函数,执行资源分配、注册、Hook安装等操作。
  3. 运行(Running):模块驻留内核,提供功能(如拦截系统调用、注册文件系统、驱动设备)。
  4. 卸载(Unloading):通过 rmmod 触发 module_exit() 函数,执行资源释放、Hook恢复等清理工作。
关键原则: 模块的初始化与卸载函数必须成对出现。如果在 init 中申请了资源(内存、注册回调、修改系统调用表),则 exit 中必须逐一释放和恢复,否则会导致内核内存泄漏或系统崩溃。

3.2 module_init 与 module_exit 宏

Linux内核通过两个宏来标记模块的入口和出口函数。这两个宏展开后会生成特定的 .init.text 和 .exit.text 段,内核加载器根据这些段来定位调用地址。

宏 作用 函数原型 返回值
module_init(x) 声明模块加载时调用的初始化函数 int __init init_function(void) 0 表示成功,负数表示失败(模块加载被中止)
module_exit(x) 声明模块卸载时调用的清理函数 void __exit exit_function(void) 无返回值

以下是一个标准的内核模块骨架代码:

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

static int __init my_hook_init(void)
{
    printk(KERN_INFO "my_hook: module loaded, preparing syscall table...\n");
    /* 在此处获取系统调用表地址、保存原始调用、写入Hook */
    return 0;  /* 返回0表示加载成功 */
}

static void __exit my_hook_exit(void)
{
    printk(KERN_INFO "my_hook: module unloaded, syscall table restored.\n");
    /* 在此处恢复原始系统调用、释放资源 */
}

module_init(my_hook_init);
module_exit(my_hook_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Hacker");
MODULE_DESCRIPTION("A simple syscall table hook example");
注意: __init 和 __exit 标记不仅用于语义提示,还会告诉内核在初始化完成后释放该函数占用的内存(如果模块被编译进内核而非动态加载)。对于动态加载的模块,__exit 标记的函数在模块加载后会被丢弃,因此卸载时无法调用——但动态模块的卸载路径由内核模块加载器特殊处理,实际仍可正常调用。

3.3 LICENCE 声明:GPL 与内核符号导出

每个内核模块必须包含 MODULE_LICENSE() 声明。这不仅是一个法律声明,还直接影响模块能否访问内核导出的符号(函数和变量)。

许可证字符串 含义 对符号访问的影响
"GPL" GNU General Public License v2 或更高版本 可访问所有 EXPORT_SYMBOL_GPL() 导出的符号
"GPL v2" 仅 GPL v2 同上
"Dual BSD/GPL" BSD 或 GPL 双许可 同上
"Proprietary" 专有许可证 只能访问 EXPORT_SYMBOL() 导出的符号,无法使用 EXPORT_SYMBOL_GPL() 符号
对Hook技术的影响: 系统调用表地址通常通过 kallsyms_lookup_name 或直接内存搜索获取,这些函数本身是 EXPORT_SYMBOL_GPL 导出的。如果模块声明为非GPL许可证,将无法调用 kallsyms_lookup_name,必须使用其他方法(如 /proc/kallsyms 用户态解析)来定位系统调用表。因此,绝大多数公开的Hook示例模块都声明为 "GPL"。

3.4 内核打印 printk:调试与日志

printk 是内核空间的“printf”,用于向内核日志缓冲区输出消息。通过 dmesg 命令可以查看这些输出。在开发Hook模块时,printk 是最直接的调试手段。

printk 的日志级别通过宏指定,每个级别对应一个数字,数字越小优先级越高:

宏 数字 含义 典型使用场景
KERN_EMERG 0 紧急事件,系统不可用 内核崩溃前
KERN_ALERT 1 必须立即处理 严重硬件故障
KERN_CRIT 2 临界条件 严重软件错误
KERN_ERR 3 错误条件 Hook安装失败、内存分配失败
KERN_WARNING 4 警告 不推荐的操作、兼容性提示
KERN_NOTICE 5 正常但重要的消息 模块加载/卸载通知
KERN_INFO 6 信息性消息 Hook触发记录、调试信息
KERN_DEBUG 7 调试消息 详细跟踪、变量值输出

使用示例:

printk(KERN_INFO "my_hook: syscall nr %ld called by pid %d\n", nr, current->pid);
printk(KERN_ERR "my_hook: failed to allocate memory for hook structure\n");
printk(KERN_DEBUG "my_hook: original syscall address = %px\n", (void *)orig_syscall);
调试技巧: 在开发阶段,建议使用 KERN_INFO 或 KERN_DEBUG 级别输出关键变量的值。使用 %px 打印指针地址(而非 %p,后者可能被内核混淆)。通过 dmesg -w 实时监控日志,或使用 tail -f /var/log/kern.log 查看持久化日志。

3.5 模块加载与卸载的完整流程(SVG架构图)

下图展示了从用户态执行 insmod 到模块初始化完成,再到 rmmod 触发清理的完整流程:

内核模块加载与卸载流程架构图 内核模块加载与卸载完整流程 用户态操作 insmod my_hook.ko ↓ 系统调用 init_module ↓ 内核加载器接管 内核态初始化 module_init() 被调用 ↓ 分配内存 / 获取系统调用表 ↓ 保存原始调用 / 写入Hook 模块运行中 系统调用被拦截 ↓ Hook函数处理 ↓ 调用原始系统调用 用户态卸载 rmmod my_hook ↓ 系统调用 delete_module 内核态清理 module_exit() 被调用 ↓ 恢复原始系统调用 / 释放内存 模块已卸载 内核符号表移除 ↓ 内存完全回收 ⚠ 关键约束 • init 函数返回 0 表示成功;返回负数时模块加载被中止,exit 不会被调用。 • 如果 init 中部分资源申请成功、部分失败,必须手动释放已成功的资源再返回错误码。

3.6 模块编译与加载实践

编写一个最简单的内核模块并测试其加载与卸载:

  1. 创建 Makefile:利用内核构建系统自动编译。
  2. 编译:执行 make 生成 .ko 文件。
  3. 加载:sudo insmod my_hook.ko,检查 dmesg | tail 查看 init 输出。
  4. 查看已加载模块:lsmod | grep my_hook 确认模块存在。
  5. 卸载:sudo rmmod my_hook,再次检查 dmesg 查看 exit 输出。
# Makefile 示例
obj-m += my_hook.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
常见问题排查: 如果 insmod 失败返回 Operation not permitted,检查是否以 root 权限执行。如果返回 Unknown symbol,说明模块引用了未导出的内核符号,需要检查 MODULE_LICENSE 声明或使用 kallsyms_lookup_name 动态查找。

3.7 本章小结

  • 内核模块通过 module_init 和 module_exit 宏定义生命周期函数,init 返回 0 表示成功。
  • MODULE_LICENSE("GPL") 是访问 EXPORT_SYMBOL_GPL 符号的必要条件,对Hook技术至关重要。
  • printk 配合不同日志级别(KERN_INFO / KERN_ERR / KERN_DEBUG)是内核模块开发的核心调试手段。
  • 模块的加载与卸载必须对称:init 中获取的资源,exit 中必须释放;init 中修改的内核状态,exit 中必须恢复。