ko文件在数据组织形式上是ELF(Excutable And Linking Format)格式,是一种普通的可重定位目标文件。这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
ELF文件中的主要内容为 program header 和 section header ,两者的大小、在ELF文件中的位置和数量都能通过文件头来获取。而后又可以通过program header来获得每个segment的属性,通过section header来获得每个section的属性。
SYSCALL_DEFINE3:- 这是一个宏,用于定义一个包含三个参数的系统调用。
init_module是系统调用的名称。- 参数:
void __user *umod:用户空间传递的模块代码的起始地址。unsigned long len:模块代码的长度。const char __user *uargs:用户空间传递的模块参数(通常是命令行参数)。
may_init_module():- 检查当前进程是否有权限加载内核模块。
- 如果进程没有权限,返回错误码(通常是
-EPERM,表示操作不被允许)。 - 如果权限检查失败,直接返回错误码,终止模块加载过程。
copy_module_from_user():- 从用户空间复制模块代码到内核空间。
- 参数:
umod:用户空间模块代码的起始地址。len:模块代码的长度。&info:指向load_info结构的指针,用于存储复制后的模块信息。
- 如果复制失败(例如,用户空间地址无效或长度超出限制),返回错误码。
load_info结构:- 用于存储模块的相关信息,如模块代码在内核空间的地址、大小等。
- 这里初始化为空
struct load_info info = { };。
- 权限检查:调用
may_init_module()检查当前进程是否有权限加载内核模块。 - 调试信息输出:使用
pr_debug()输出传入的参数信息。 - 复制模块代码:调用
copy_module_from_user()从用户空间复制模块代码到内核空间。 - 加载模块:调用
load_module()执行模块加载。
内核模块
- 定义:内核模块是可以动态加载到内核中的代码片段,用于扩展内核的功能。
- 优点:
- 无需重启系统即可添加或移除功能。
- 减少内核占用空间,只加载需要的功能。
- 常见用途:
- 驱动程序加载。
- 文件系统支持。
- 系统调用扩展。
2. 用户空间与内核空间
- 用户空间:用户程序运行的空间,权限受限。
- 内核空间:内核代码运行的空间,具有最高权限。
- 系统调用:用户空间程序通过系统调用请求内核服务,如加载模块。
3. copy_from_user与copy_to_user
copy_from_user():从用户空间复制数据到内核空间。copy_to_user():从内核空间复制数据到用户空间。- 作用:确保数据在用户和内核空间之间的安全传输,处理页表、权限等问题。
static:表示该函数的作用域仅限于定义它的源文件,不会被其他文件访问。int:函数返回类型,表示返回一个整数值。may_init_module:函数名称。void:函数不接受任何参数。
capable(CAP_SYS_MODULE):capable:是一个内核函数,用于检查当前进程是否具有指定的能力(capability)。CAP_SYS_MODULE:是一个能力标志,表示进程具有加载和卸载内核模块的权限。!capable(CAP_SYS_MODULE):如果当前进程没有CAP_SYS_MODULE能力,条件为真。
modules_disabled:- 这是一个全局变量,用于指示内核模块功能是否被禁用。
- 如果
modules_disabled为真,表示模块加载功能被禁用。
- 逻辑或(
||):- 如果当前进程没有
CAP_SYS_MODULE能力,或者模块功能被禁用,条件为真。
- 如果当前进程没有
- 检查权限:
- 使用
capable(CAP_SYS_MODULE)检查当前进程是否具有加载内核模块的权限。 - 如果没有权限,返回
-EPERM。
- 使用
- 检查模块功能是否禁用:
- 检查全局变量
modules_disabled,如果模块功能被禁用,返回-EPERM。
- 检查全局变量
- 允许加载模块:
- 如果权限检查通过且模块功能未被禁用,返回
0,表示允许加载模块
- 如果权限检查通过且模块功能未被禁用,返回
security_kernel_module_from_file(NULL):- 调用安全模块的函数,检查是否允许从文件加载内核模块。
- 传入
NULL表示没有具体的文件名(因为数据来自用户空间指针)。
__vmalloc:- 分配内核虚拟内存,可以分配大块内存,适用于内核模块数据。
- 参数:
info->len:需要分配的内存大小。GFP_KERNEL | __GFP_HIGHMEM | __GFP_NOWARN:内存分配标志。GFP_KERNEL:表示这是一个内核分配请求。__GFP_HIGHMEM:允许分配高端内存(如果可用)。__GFP_NOWARN:禁止分配失败时打印警告信息。
PAGE_KERNEL:页面属性,表示这是内核使用的页面。
if (!info->hdr):如果内存分配失败,返回-ENOMEM,表示内存不足。
load_info结构
- 作用:用于存储模块的相关信息,如模块头部和长度。
- 字段:
hdr:指向模块头部的指针。len:模块数据的长度。
内存分配函数
__vmalloc:分配内核虚拟内存,适用于大块内存分配。vfree:释放由__vmalloc分配的内存。
3. 安全检查
security_kernel_module_from_file:内核安全模块提供的函数,用于检查是否允许加载内核模块。
4. 用户空间与内核空间数据复制
copy_from_user和copy_to_user:标准的用户空间与内核空间数据复制函数。copy_chunked_from_user:自定义的分块复制函数,适用于大块数据,可能更高效。
5. 错误码
-ENOEXEC:表示无法执行该文件(模块长度太小)。-ENOMEM:表示内存不足。-EFAULT:表示访问用户空间时发生错误。
Elf_Ehdr *hdr
- 类型:指向
Elf_Ehdr结构的指针。 - 作用:指向 ELF(Executable and Linkable Format)文件的头部,
Elf_Ehdr包含了 ELF 文件的基本信息,如文件类型、机器架构、入口点地址等。 - 用途:在加载模块时,首先读取并解析 ELF 头部信息。
2. unsigned long len
- 类型:无符号长整型。
- 作用:表示模块数据的总长度。
- 用途:用于分配内存和验证模块数据的完整性。
ELF 文件格式
- ELF(Executable and Linkable Format):一种标准的可执行文件格式,广泛用于类 Unix 系统。
- 结构:
- ELF 头部:描述文件的基本信息。
- 节区头部表:描述每个节区的属性。
- 节区数据:实际的数据,如代码、数据、符号表等。
2. 符号表和字符串表
- 符号表:包含函数和变量的信息,如名称、地址、类型等。
- 字符串表:符号表中使用的字符串存储在这里,用于节省空间。
- 从用户空间内存区域复制数据到内核空间,支持分段复制以处理大数据量。
- 确保在复制过程中可以调度其他任务,避免长时间占用 CPU。
min(len, COPY_CHUNK_SIZE):计算本次复制的数据量,取剩余长度和COPY_CHUNK_SIZE的较小值。copy_from_user(dst, usrc, n):从用户空间地址usrc复制n字节到内核空间地址dst。- 如果复制失败,返回
-EFAULT。
- 如果复制失败,返回
cond_resched():检查是否需要调度其他任务,允许内核在复制过程中进行任务切换。- 指针更新:
dst和usrc分别向后移动n字节,len减少n。 - 循环:直到所有数据复制完毕。
- 负责加载内核模块,包括验证、分配内存、初始化、解析参数等步骤。
- 调用
module_sig_check检查模块签名。 - 如果签名检查失败,跳转到
free_copy标签进行清理。
在 Linux 内核中,模块签名是一种安全机制,用于验证内核模块(.ko 文件)的完整性和来源。通过模块签名,内核可以确保加载的模块未被篡改,并且来自可信的来源。这对于安全性至关重要的系统(如生产环境或关键基础设施)尤为重要。
- 签名过程:
- 模块开发者使用私钥对模块进行签名。
- 签名信息通常附加在模块的末尾,包含数字签名和证书。
- 验证过程:
- 内核在加载模块时,会检查模块是否包含有效的签名。
- 内核使用公钥(通常嵌入在内核中或通过系统信任链提供)验证签名。
- 如果签名有效,内核认为模块是可信的;否则,拒绝加载模块。
- 目的:
- 防止恶意模块被加载。
- 确保模块的完整性和来源可信。
struct load_info *info:- 指向包含加载信息的结构体,通常包含指向 ELF 文件头和文件长度的指针。
info->hdr:指向 ELF 文件头的指针。info->len:ELF 文件的总长度。
- 目的:确保 ELF 文件的长度至少足够包含 ELF 文件头。
- 逻辑:
- 如果文件长度小于 ELF 文件头的大小,返回错误码
-ENOEXEC,表示无法执行该文件。
- 如果文件长度小于 ELF 文件头的大小,返回错误码
info->hdr->e_shentsize != sizeof(Elf_Shdr):- 检查节头表(section header table)的条目大小是否与预期的
Elf_Shdr结构大小一致。 - 如果不一致,返回
-ENOEXEC。
- 检查节头表(section header table)的条目大小是否与预期的
- ELF 文件头(
Elf_Ehdr):- 包含 ELF 文件的基本信息,如魔数、文件类型、架构、节头表偏移等。
- 节头表(Section Header Table):
- 描述 ELF 文件中的各个节(section)的信息,如代码段、数据段等。
- 关键字段:
e_ident:ELF 文件的魔数,用于标识文件类型。e_type:ELF 文件的类型,如可重定位文件、可执行文件等。e_shoff:节头表的偏移量。e_shnum:节头表中的条目数量。e_shentsize:节头表条目的大小。
memcmp 是一个标准库函数,用于比较两块内存区域的内容。它的原型定义在 <string.h> 头文件中。memcmp 函数在内核开发、文件解析、数据验证等场景中非常常用。
ptr1:指向第一块内存区域的指针。ptr2:指向第二块内存区域的指针。num:要比较的字节数。
在 elf_header_check 函数中,memcmp(info->hdr->e_ident, ELFMAG, SELFMAG) 用于比较 ELF 文件头的魔数(magic number)是否与预期的 ELF 魔数匹配。
具体解释
info->hdr->e_ident:- 指向 ELF 文件头的
e_ident字段。 e_ident是一个字符数组,用于存储 ELF 文件的魔数和其他标识信息。
- 指向 ELF 文件头的
ELFMAG:- 是一个宏,定义了 ELF 文件的魔数。
- 典型的 ELF 魔数是
"\x7fELF",即:0x7f:表示这是一个 ELF 文件。'E'、'L'、'F':ASCII 字符,表示 "ELF"。
SELFMAG:- 是一个宏,定义了魔数的长度。
- 对于典型的 ELF 文件,
SELFMAG的值是 4(即"\x7fELF"的长度)。
- 比较过程:
memcmp函数比较info->hdr->e_ident所指向的内存区域的前SELFMAG个字节与ELFMAG所表示的魔数。- 如果两者相等,返回 0,表示这是一个有效的 ELF 文件。
- 如果不相等,返回一个非零值,表示这不是一个有效的 ELF 文件。
e_ident 字段是一个长度为 EI_NIDENT(通常为 16)的字节数组,其结构如下:
e_ident[EI_MAG0]到e_ident[EI_MAG3]:- 魔数的前四个字节,通常为
"\x7fELF"。
- 魔数的前四个字节,通常为
e_ident[EI_CLASS]:- 文件类,表示是 32 位还是 64 位 ELF 文件。
ELFCLASS32:32 位。ELFCLASS64:64 位。
e_ident[EI_DATA]:- 数据编码,表示是大端序还是小端序。
ELFDATA2LSB:小端序(least significant byte first)。ELFDATA2MSB:大端序(most significant byte first)。
e_ident[EI_VERSION]:- ELF 文件版本,通常为
EV_CURRENT。
- ELF 文件版本,通常为
- 其他字段:
- 保留字段,通常设置为 0。
- 可重定位文件是一种包含代码和数据的文件,但其代码和数据在文件中的地址并不是最终的加载地址。
- 这些文件在链接时不需要指定绝对地址,可以在加载时根据实际的内存布局进行重定位。
为什么内核模块通常是可重定位文件?
- 灵活性:
- 内核模块需要在内核的地址空间内加载,而内核的地址空间是动态变化的。
- 使用可重定位文件,内核可以在加载模块时根据当前的内存布局调整模块的地址。
- 位置无关代码(PIC):
- 内核模块通常编译为位置无关代码,这意味着代码可以在内存中的任何位置执行,而不需要重定位每个指令。
- 这提高了加载效率,并减少了内存碎片。
- 动态加载:
- 内核模块可以在系统运行时动态加载和卸载,而不需要重启系统。
- 可重定位文件使得这种动态加载成为可能,因为模块可以在加载时进行重定位。
- 安全性:
- 通过在加载时进行重定位,内核可以确保模块不会覆盖其他重要的内存区域,提高了系统的安全性。
- ELF 文件头(ELF Header):位于文件的开头,包含文件的基本信息。
- 程序头表(Program Header Table):描述段(segments)的信息,用于可执行文件和共享库。
- 节头表(Section Header Table):描述节(sections)的信息,用于链接和重定位。
- ELF 文件头是 ELF 文件的第一个部分:
- 它包含了文件的基本信息,如文件类型、架构、入口点、程序头表和节头表的偏移等。
- 通过解析 ELF 文件头,加载器可以了解文件的结构和内容。
- ELF 文件头的作用:
- 验证文件的合法性。
- 确定文件的类型和架构。
- 找到程序头表和节头表的位置。
info->len:表示 ELF 文件的总长度。info->hdr:指向 ELF 文件头的指针。sizeof(*(info->hdr)):表示 ELF 文件头的大小
-
struct load_info *info:指向load_info结构体的指针,包含加载模块所需的信息。int flags:加载标志,用于控制加载行为。
- 返回值:
- 成功时返回指向
struct module的指针。 - 失败时返回错误指针(通过
ERR_PTR宏)。
- 成功时返回指向
setup_load_info:- 调用
setup_load_info函数,根据info和flags设置模块的加载信息。 - 返回值
mod是指向struct module的指针,如果设置失败,返回错误指针。 IS_ERR(mod):检查mod是否为错误指针,如果是,直接返回。
- 调用
- 布局模块节和符号表:
layout_sections:计算模块中各个节的总大小,并将偏移量存储在sh_entsize字段中。layout_symtab:布局模块的符号表,确保符号在内存中的正确位置。
layout_sections是一个静态函数,意味着它的作用域仅限于定义它的文件。- 参数:
struct module *mod:表示模块信息,包含模块的元数据和布局信息。struct load_info *info:表示加载信息,包含ELF文件头、段头表(section headers)以及字符串表等信息。
masks定义了 section 分类的规则,每种规则包含两个条件:- 第一个条件(
masks[m][0]):section 必须满足的标志(如可执行、可分配等)。 - 第二个条件(
masks[m][1]):section 必须不满足的标志(如不可写、非小数据等)。
- 第一个条件(
- 规则顺序:
- 可执行代码段(
SHF_EXECINSTR | SHF_ALLOC)。 - 只读数据段(
SHF_ALLOC,但不可写)。 - 读写数据段(
SHF_WRITE | SHF_ALLOC)。 - 小数据段(
ARCH_SHF_SMALL | SHF_ALLOC)。
- 可执行代码段(
- 遍历所有 section,将每个 section 的
sh_entsize字段初始化为~0UL(全 1 的无符号长整型值)。 sh_entsize在后续用于存储偏移量,初始化为~0UL表示该 section 尚未被分配
外层循环
- 遍历
masks数组,依次处理每种类型的 section。
内层循环
- 遍历所有 section,检查每个 section 是否符合当前规则:
s->sh_flags & masks[m][0] == masks[m][0]:section 必须满足第一个条件。s->sh_flags & masks[m][1] == 0:section 必须不满足第二个条件。s->sh_entsize == ~0UL:section 尚未被分配。!strstarts(sname, ".init"):section 名称不以.init开头(核心区域不包含初始化 section)。
- 如果 section 符合条件,调用
get_offset计算偏移量,并存储到sh_entsize。
- 这段代码的核心功能是根据 section 的标志和名称,将 ELF 文件的 sections 分配到核心区域或初始化区域,并计算偏移量和大小。
- 它遵循一定的布局顺序(代码段、只读数据段、读写数据段、小数据段),并使用
sh_entsize字段存储偏移量。 - 初始化区域的 section 偏移量通过
INIT_OFFSET_MASK标记,以便后续区分。
-
第16行:setup_load_info()加载struct load_info 和 struct module, rewrite_section_headers,将每个section的sh_addr修改为当前镜像所在的内存地址, section 名称字符串表地址的获取方式是从ELF头中的e_shstrndx获取到节区头部字符串表的标号,找到对应section在ELF文件中的偏移,再加上ELF文件起始地址就得到了字符串表在内存中的地址。
-
第18行:在layout_and_allocate()中,layout_sections() 负责将section 归类为core和init这两大类,为ko的第二次搬移做准备。move_module()把ko搬移到最终的运行地址。内核模块加载代码搬运过程到此就结束了。
add_unformed_module 函数:
- 设置模块状态:
- 将模块的状态设置为
MODULE_STATE_UNFORMED,表示模块尚未完全加载。
- 将模块的状态设置为
- 检查模块唯一性:
- 确保模块名称在模块列表中是唯一的,避免重复加载。
- 插入模块:
- 如果模块名称唯一,将模块添加到全局模块列表中。
- 处理并发和等待:
- 使用互斥锁保护模块列表的访问。
- 如果发现另一个模块正在加载,等待其完成。
在 Linux 内核中,模块(module)是可以动态加载和卸载的内核代码单元。模块卸载是指从内核中移除一个已经加载的模块,释放其占用的资源(如内存、符号表、设备号等),并清理相关的数据结构。模块卸载是内核模块管理的重要部分,确保系统资源的有效利用和系统的稳定性。
- 条件编译:
#ifdef CONFIG_MODULE_UNLOAD表示这段代码只有在启用了模块卸载功能时才会编译。
- 导出符号:
EXPORT_TRACEPOINT_SYMBOL(module_get)导出module_get符号,用于跟踪或调试模块引用计数的变化。
- 引用计数(
refcnt):- 引用计数用于跟踪模块的使用情况。
- 当引用计数为 0 时,表示模块不再被使用,可以被卸载。
- 引用计数通过原子操作(
atomic_set、atomic_inc)进行管理,确保在多核环境下的线程安全。
- 模块依赖关系:
source_list和target_list用于管理模块之间的依赖关系。- 在卸载模块时,内核会检查这些列表,确保不会卸载正在被其他模块使用的模块。
- 原子操作:
- 使用原子操作(如
atomic_set、atomic_inc)来修改引用计数,确保在多核环境下的线程安全。 - 原子操作可以防止多个 CPU 同时修改引用计数,导致数据不一致。
- 使用原子操作(如
- 初始化过程:
- 在模块初始化过程中,临时增加引用计数,防止模块被意外卸载。
- 初始化完成后,引用计数会根据模块的实际使用情况进行调整。
内核模块的生命周期
- 加载:
- 内核模块可以在系统运行时动态加载到内核中。加载过程通常使用
insmod或modprobe命令。 - 加载时,内核会解析模块文件,提取模块的头部信息,检查模块是否依赖于其他模块,如果依赖则先加载依赖的模块。
- 然后为模块分配必要的内核资源,如内存空间,并调用模块的初始化函数(通常标记为
__init),完成模块的初始化工作。 - 初始化完成后,模块信息被注册到内核,使其可以被内核和其他模块使用。
- 内核模块可以在系统运行时动态加载到内核中。加载过程通常使用
- 运行:
- 模块加载后,它就可以执行其功能了。这通常涉及到对硬件的访问、系统调用的处理、内核数据的操作等。
- 模块运行期间,它可能会响应来自用户空间或其他内核模块的请求。
- 卸载:
- 内核模块可以在不再需要时从内核中卸载。卸载过程通常使用
rmmod或modprobe -r命令。 - 卸载时,内核会检查模块的引用计数,如果计数不为 0,则模块不能卸载。
- 然后调用模块的清理函数(通常标记为
__exit),释放模块占用的资源,如释放内存、注销设备、销毁数据结构等。 - 清理完成后,从内核中注销模块,撤销模块注册的信息,并释放模块加载时分配的资源。
- 如果该模块被其他模块依赖,则先卸载这些依赖模块。
- 内核模块可以在不再需要时从内核中卸载。卸载过程通常使用
module_unload_init 函数的主要任务是初始化与模块卸载相关的数据结构,确保模块在加载时就已经准备好被正确卸载。具体来说,它执行以下操作:
- 初始化引用计数:
- 将模块的引用计数(
refcnt)设置为MODULE_REF_BASE,表示模块的基本引用计数。 - 引用计数用于跟踪模块的使用情况,当引用计数为 0 时,表示模块可以被卸载。
- 将模块的引用计数(
- 初始化依赖列表:
- 初始化模块的
source_list和target_list,用于管理模块之间的依赖关系。 source_list记录依赖当前模块的其他模块。target_list记录当前模块依赖的其他模块。
- 初始化模块的
- 增加引用计数:
- 在初始化过程中临时增加引用计数,防止模块在初始化完成之前被意外卸载。
2415

被折叠的 条评论
为什么被折叠?



