一、基础知识
模块时一种向Linux内核添加设备驱动、文件系统及其他组件的有效方法,不需要重新编译内核或者重启系统。
内核模块具有以下优点:
1.通过使用模块,内核发布者能够预先编译大量驱动程序,而不会使内核映像的尺寸放生膨胀。
2.内核开发者可以将试验性的代码打包到模块中,模块可以卸载,修改代码或者重新打包后再重新装载。
1.添加和删除
从用户角度看,模块可以通过两个不同的系统程序添加到运行的内核中。他们分别是modprobe和insmod。modprobe在识别出目标模块所依赖的模块之后,在内核也会使用insmod,在用户空间对模块的处理也是基于insmod。
在处理该系统调用时,模块代码首先复制到内核内存中。接下来是重定位工作和解决模块中未定义的引用。因为模块使用了持久编译到内核中的函数,在模块本身编译时无法确定这些函数的地址,所以需要在这里处理未定义的引用。
处理未解决的引用,为与内核的剩余部分协作,模块必须使用内核提供的函数。这些可能是通用的辅助函数,比如几乎内核每一部分都是用的printk或者kmalloc。
很明显这些函数定义在内核的基础代码中,因而已经加载到内存,但是如本找到与相关函数名称匹配的地址,以便解决这些引用呢?为此,内核提供了一个所有到处函数的列表。该列表给出了所有到处函数的内存地址和对应的函数名,可以通过proc文件系统访问。即/proc/kallsyms:
场景描述
假设需要加载驱动模块A(
driverA.ko
),其依赖模块B(driverB.ko
),操作流程如下:步骤1:确认依赖关系
# 查看模块A的依赖信息 modinfo driverA.ko | grep depends # 输出示例:depends: driverB
步骤2:递归加载依赖
# 加载基础依赖模块B(需使用绝对路径) sudo insmod /lib/modules/$(uname -r)/kernel/drivers/base/driverB.ko# 验证模块B是否加载成功 lsmod | grep driverB # 应显示模块B信息# 加载主模块A sudo insmod /path/to/driverA.ko# 验证完整加载 dmesg | tail -10 # 查看内核日志中的初始化消息
步骤3:卸载逆向操作
# 先卸载主模块A sudo rmmod driverA# 再卸载依赖模块B(需确保无其他模块依赖它) sudo rmmod driverB
在加载模块时所需要的操作,与通过ld和ld.so借助于动态库借链接应用程序的操作。从外部来看,模块只是普通的可重定位目标文件。file命令的输出表明模块文件是可重定位的,这是用户空间程序设计中一种专业术语。可重定位文件的函数都不会引用绝对地址,而只是向指向代码中的相对地址,因此可以在内存的任意偏移地址加载,当然在映像加载到内存时映像中的地址要由动态链接器ld.so进行适当的修改即可。重定位的工作有内核自身执行,而不是动态装载器。
ramfs模块允许在内存中建立一个文件系统(成为RAM磁盘),因而需要调用register_filesystem函数将自身添加到内核中可用于文件系统的列表。此模块还使用内核代码中的generic_file_read/generic_write标准函数,基本大多数内核文件系统都会使用这些普通函数。
二、插入和删除模块
用户空间工具和内核的模块实现之间的接口,包块两个系统调用。
init_module
:用于将新模块插入内核,用户空间工具提供二进制数据,重定位和解决引用等工作由内核完成。delete_module
:功能是从内核移除模块,但要求该模块代码及其导出函数都不再被使用。request_module
:不是系统调用,可从内核端加载模块,还能实现热插拔功能 。
1.模块的表示
module是一个非常重要的数据结构。内核中驻留的每个模块,都分配了该结构的一个实例。
state表示该模块的当前状态,可以从枚举类型module_state取值:
syms,num_syms和crc用于管理模块导出的符号。syms是一个数组,有num_syms个数组项,数组项类型为kernel_symbol,负责将标识符(name)分配到内存地址(value)
在导出符号时,内核不仅考虑可以有所有模块使用符号,还要考虑只能由GPL兼容模块使用的符号。
模块的二进制数据有两个部分 :初始化部分和核心部分
内核提供license_is_gpl_compatible函数来判断给许可是否与GPL兼容
2.依赖关系和引用
若模块 B 使用模块 A 提供的函数,二者就存在关联,有两种理解角度:
一是模块 B 依赖模块 A,即模块 A 未驻留内核内存时,模块 B 无法装载;
二是模块 B 引用模块 A,即模块 B 未移除时,模块 A 无法从内核移除,实际上需所有引用模块 A 的模块都移除才行,内核将这种关系称为模块 B 使用模块 A。为管理这些依赖关系,内核需引入新的数据结构。
3.模块的二进制结构
模块采用 ELF 二进制格式,包含一些普通程序或库中没有的额外段(少量编译器产生的重定位段与讨论无关)。
生成模块的步骤有三步:
首先,将模块源代码中的所有 C 文件编译成普通的.o 目标文件;
然后,内核分析目标文件,把找到的附加信息(如模块依赖关系)保存在独立文件中并编译为二进制文件;
最后,把前两步产生的二进制文件链接起来,得到最终模块。
a.初始化及清理函数
<init.h>中的module_init和module_exit宏用于定义init函数和exit函数。
b.导出符号
内核为导出符号提供了两个宏:EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL。前者用于一般的导出符号,后者只用于 GPL 兼容代码的导出符号,目的是将相应符号放置到模块二进制映象的适当段中。
c.一般模块信息
模块许可证、开发者和描述、备选名称、基本版本控制
4.插入模块
init_module系统调用是用户空间和内核之间用于装载新模块的接口,通过load_module函数将二进制数据传输到内核地址空间中:
在实现load_module时会出现异常,内核源代码中该函数:完成所有碰到的异常问题,此函数可以完成的任务如下:
1.从用户空间复制模块数据到内核地址空间中的一个临时内存位置
2.查找各个段的位置
3.确保内核和模块版本控制字符串和struct module的定义匹配问题
4.将存在的各个段分配到其在内存中的最终位置
5.重定位符号并解决引用,链接到模块符号任何版本控制信息都会注意到处理模块的参数。
5.删除模块
从内核移除模块比插入模块简单得多,系统调用delete_module函数来实现移除模块:
6.整体流程
在Linux内核中,模块的加载过程涉及用户空间工具与内核函数的协同工作。以下是
insmod
命令触发模块加载时的详细流程:
1. 用户空间操作:
insmod
触发系统调用
- 当用户执行
insmod example.ko
时,insmod
工具会:
- 读取模块的二进制文件(
.ko
)。- 调用系统调用
init_module
,将模块数据传递给内核。
2. 内核处理:
init_module
系统调用
init_module
是内核提供的系统调用(系统调用号由内核版本决定,如sys_init_module
)。- 它的主要任务:
- 权限检查:验证用户是否有权限加载模块(需
CAP_SYS_MODULE
权限)。- 调用
load_module
:将模块数据交给内核内部的load_module
函数处理。
3. 内核核心函数:
load_module
load_module
是模块加载的核心函数,负责:
- 解析ELF格式:检查模块的ELF头、段信息。
- 版本校验:确保模块与当前内核版本兼容(通过
vermagic
字符串)。- 符号重定位:处理模块对其他模块或内核符号的引用(如调用
resolve_symbol
)。- 分配内存:为模块的代码(
.text
)、数据(.data
)等段分配内存。- 注册模块元数据:创建
struct module
实例,记录模块状态、符号表等信息。- 调用模块初始化函数:执行由
module_init
宏定义的函数(如example_init
)。
4. 模块初始化:
module_init
函数
- 开发者通过
module_init(example_init)
定义的函数example_init
:
- 在
load_module
完成后被调用。- 负责模块特有的初始化工作(如注册设备驱动、注册文件系统等)。
- 如果初始化失败,内核会回滚加载操作。