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:
New comments are not allowed.