January 31, 2025

详解Linux内核模块的加载机制

1、内核模块

内核模块是什么?

Linux内核模块是动态加载到内核中的代码,可以在不重启系统的情况下扩展功能,比如设备驱动或者文件系统支持。这样用户不需要把所有功能都编译进内核,节省了资源,提高了灵活性。

模块的文件格式

Linux内核模块通常是.ko文件,也就是Kernel Object的缩写。这些文件是ELF格式的,但和用户空间的程序不同,它们包含的是特定于内核的信息。比如,模块的元数据,像作者、许可证、描述等,这 些信息保存在.modinfo段里。还有.init.text和.exit.text这样的段,分别对应模块初始化和退出时的代码。这些段在模块加载和卸载时会被执行。

模块的加载过程

用户通常使用insmod或modprobe命令来加载模块。insmod是直接加载,而modprobe会处理依赖关系。

2、如何工作

那内核模块具体是怎么工作的呢?当执行insmod时,会调用系统调用init_module,或者更现代的finit_module。

这些系统调用将模块的二进制内容读入内核空间,并进行一系列检查。

这些检查包括如下:

首先是验证模块的签名,确保其完整性和来源可信,特别是启用了CONFIG_MODULE_SIG的情况下。

然后是版本检查,确认模块的版本与当前内核兼容,避免ABI不匹配导致的问题。还有许可证检查,确保模块的许可证符合GPL兼容性,避免法律问题,尤其是对于一些专有模块可能会有限制。

接下来是模块的初始化。内核会执行模块的初始化函数,通常是用module_init宏定义的函数。这个函数负责模块的启动工作,比如注册设备驱动或文件系统。如果初始化成功,模块就被标记为LIVE状态,可以使用了。

模块的卸载过程类似,用户使用rmmod命令触发,调用delete_module系统调用。系统会检查模块的引用计数,确保没有其他部分在使用它,然后执行模块的退出函数,通常用module_exit宏定义,释放资源,解除注册等。

依赖管理方面,modprobe会根据/lib/ modules/$(uname-r)/下的modules.dep文件来处理依赖关系、自动加载所需的模块。depmod命令会生成这个依赖文件,分析模块之间的符号依赖,比如模块A需要模块B提供的函数或变量,就会记录这种依赖关系。

符号导出和引用也是关键点。内核通过EXPORT_SYMBOL宏导出符号,其他模块可以引用这些符号。模块的符号表在加载时会链接到内核的符号表中,这样其他模块可以访问。同时,模块也可以引用内核或其他模块导出的符号,但需要满足GPL兼容性要求。

安全性方面,模块签名和强制模块加载控制(比如sysctl中的modules_disabled)是重要的。签名防止恶意模块加载,而modules_disabled可以完全禁止模块加载,增强系统安全性。

还有热插拔和自动加载机制,比如当插入一个新设备时,udev会根据设备信息自动加载对应的驱动模块。这是通过uevent事件和用户空间的工具配合实现的,提高了设备的即插即用能力。

3、解析加载.ko文件

ko文件为标准ELF对象文件,包含以下关键段:


$ readelf -S example.ko
  .text      代码段
  .data      已初始化数据
  .rodata    只读数据
  .modinfo   模块元信息(作者、许可证、依赖等)
  __versions 内核符号版本校验数据
  __ksymtab  导出的符号表
  .init.text 初始化函数(模块加载时执行)
  .exit.text 清理函数(模块卸载时执行)

首先解析ELF头:

1、提取代码段(.text)、数据段(.data、.bss)和特殊段(如.modinfo)。

2、检查ELF架构(e_machine字段)是否匹配当前内核(如EM_X86_64)。

如下是关键数据结构


struct module
// 包含模块的所有元信息和控制结构
struct module {
    enum module_state state;      // 状态(LIVE, COMING, GOING)
    const char *name;             // 模块名
    struct list_head list;        // 全局模块链表节点
    // 符号与依赖管理
    const struct kernel_symbol *syms;     // 导出符号
    unsigned int num_syms;
    struct module *modules_which_use_me;  // 依赖此模块的模块列表
    // 初始化与清理
    int (*init)(void);            // 初始化函数指针
    void (*exit)(void);           // 清理函数指针
    // 安全与元数据
    const char *license;          // 许可证(如"GPL")
    bool sig_ok;                  // 签名验证结果
};

然后进行内存分配

1、使用vmalloc()在内核空间分配内存,映射模块的代码和数据段。

2、标记可执行页(需CONFIG_STRICT_MODULE_RWX配置允许)。

符号解析与重定位

  • 符号表处理:
    • 导出符号:从__ksymtab和__ksymtab_gpl提取模块提供的符号。
    • 未定义符号:在内核的导出符号表(/proc/kallsyms)或已加载模块中查找匹配。
  • 重定位修正:
    • 修改代码中的地址引用(如函数调用、全局变量访问)为实际加载地址。
    • 处理版本校验(__versions段),确保符号CRC与内核一致,避免ABI不兼容。

依赖与元信息处理

  • 依赖加载:
    • 解析.modinfo中的depends=字段(如depends=ext4,mbcache)。
    • 调用request_module()递归加载依赖模块。
  • 元信息注册:
    • 解析许可证(license=)、作者(author=)等信息,存储到struct module。

初始化与注册

执行初始化函数:调用module_init()宏定义的函数(位于.init.text段)。

示例:设备驱动调用pci_register_driver()注册到PCI子系统。

状态更新:将模块添加到全局链表modules(可通过lsmod查看)。设置状态为MODULE_STATE_LIVE。

卸载处理

引用计数检查:通过try_stop_module()检查refcnt,确保无其他模块或进程依赖。

执行清理函数:调用module_exit()宏定义的函数(位于.exit.text段)。示例:驱动调用pci_unregister_driver()解除注册。内存释放:释放.init段(标记为__init的函数/数据在初始化后自动释放)。通过vfree()释放模块占用的内存。

Linux内核模块的加载过程主要包含了ELF解析、动态链接、安全验证和资源管理技术。其核心步骤包括:权限检查→ELF解析→符号重定位→依赖加载→初始化执行。

0 comments:

VxWorks