3.25【A]linux-ko格式ELF文件,module加载卸载

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 = { };

  1. 权限检查:调用may_init_module()检查当前进程是否有权限加载内核模块。
  2. 调试信息输出:使用pr_debug()输出传入的参数信息。
  3. 复制模块代码:调用copy_module_from_user()从用户空间复制模块代码到内核空间。
  4. 加载模块:调用load_module()执行模块加载。

内核模块
  • 定义:内核模块是可以动态加载到内核中的代码片段,用于扩展内核的功能。
  • 优点
    • 无需重启系统即可添加或移除功能。
    • 减少内核占用空间,只加载需要的功能。
  • 常见用途
    • 驱动程序加载。
    • 文件系统支持。
    • 系统调用扩展。
2. 用户空间与内核空间
  • 用户空间:用户程序运行的空间,权限受限。
  • 内核空间:内核代码运行的空间,具有最高权限。
  • 系统调用:用户空间程序通过系统调用请求内核服务,如加载模块。
3. copy_from_usercopy_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能力,或者模块功能被禁用,条件为真。
  1. 检查权限
    • 使用capable(CAP_SYS_MODULE)检查当前进程是否具有加载内核模块的权限。
    • 如果没有权限,返回-EPERM
  2. 检查模块功能是否禁用
    • 检查全局变量modules_disabled,如果模块功能被禁用,返回-EPERM
  3. 允许加载模块
    • 如果权限检查通过且模块功能未被禁用,返回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_usercopy_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 文件)的完整性和来源。通过模块签名,内核可以确保加载的模块未被篡改,并且来自可信的来源。这对于安全性至关重要的系统(如生产环境或关键基础设施)尤为重要。

  1. 签名过程
    • 模块开发者使用私钥对模块进行签名。
    • 签名信息通常附加在模块的末尾,包含数字签名和证书。
  2. 验证过程
    • 内核在加载模块时,会检查模块是否包含有效的签名。
    • 内核使用公钥(通常嵌入在内核中或通过系统信任链提供)验证签名。
    • 如果签名有效,内核认为模块是可信的;否则,拒绝加载模块。
  3. 目的
    • 防止恶意模块被加载。
    • 确保模块的完整性和来源可信。

  • struct load_info *info
    • 指向包含加载信息的结构体,通常包含指向 ELF 文件头和文件长度的指针。
    • info->hdr:指向 ELF 文件头的指针。
    • info->len:ELF 文件的总长度。
  • 目的:确保 ELF 文件的长度至少足够包含 ELF 文件头。
  • 逻辑
    • 如果文件长度小于 ELF 文件头的大小,返回错误码 -ENOEXEC,表示无法执行该文件。

  • info->hdr->e_shentsize != sizeof(Elf_Shdr)
    • 检查节头表(section header table)的条目大小是否与预期的 Elf_Shdr 结构大小一致。
    • 如果不一致,返回 -ENOEXEC

  1. ELF 文件头(Elf_Ehdr
    • 包含 ELF 文件的基本信息,如魔数、文件类型、架构、节头表偏移等。
  2. 节头表(Section Header Table)
    • 描述 ELF 文件中的各个节(section)的信息,如代码段、数据段等。
  3. 关键字段
    • 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 魔数匹配。

具体解释
  1. info->hdr->e_ident
    • 指向 ELF 文件头的 e_ident 字段。
    • e_ident 是一个字符数组,用于存储 ELF 文件的魔数和其他标识信息。
  2. ELFMAG
    • 是一个宏,定义了 ELF 文件的魔数。
    • 典型的 ELF 魔数是 "\x7fELF",即:
      • 0x7f:表示这是一个 ELF 文件。
      • 'E''L''F':ASCII 字符,表示 "ELF"。
  3. SELFMAG
    • 是一个宏,定义了魔数的长度。
    • 对于典型的 ELF 文件,SELFMAG 的值是 4(即 "\x7fELF" 的长度)。
  4. 比较过程
    • 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
  • 其他字段
    • 保留字段,通常设置为 0。

  • 可重定位文件是一种包含代码和数据的文件,但其代码和数据在文件中的地址并不是最终的加载地址。
  • 这些文件在链接时不需要指定绝对地址,可以在加载时根据实际的内存布局进行重定位。

为什么内核模块通常是可重定位文件?
  1. 灵活性
    • 内核模块需要在内核的地址空间内加载,而内核的地址空间是动态变化的。
    • 使用可重定位文件,内核可以在加载模块时根据当前的内存布局调整模块的地址。
  2. 位置无关代码(PIC)
    • 内核模块通常编译为位置无关代码,这意味着代码可以在内存中的任何位置执行,而不需要重定位每个指令。
    • 这提高了加载效率,并减少了内存碎片。
  3. 动态加载
    • 内核模块可以在系统运行时动态加载和卸载,而不需要重启系统。
    • 可重定位文件使得这种动态加载成为可能,因为模块可以在加载时进行重定位。
  4. 安全性
    • 通过在加载时进行重定位,内核可以确保模块不会覆盖其他重要的内存区域,提高了系统的安全性。

  • 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 宏)。
  1. setup_load_info
    • 调用 setup_load_info 函数,根据 info 和 flags 设置模块的加载信息。
    • 返回值 mod 是指向 struct module 的指针,如果设置失败,返回错误指针。
    • IS_ERR(mod):检查 mod 是否为错误指针,如果是,直接返回。
  1. 布局模块节和符号表
    • 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 必须不满足的标志(如不可写、非小数据等)。
  • 规则顺序:
    1. 可执行代码段(SHF_EXECINSTR | SHF_ALLOC)。
    2. 只读数据段(SHF_ALLOC,但不可写)。
    3. 读写数据段(SHF_WRITE | SHF_ALLOC)。
    4. 小数据段(ARCH_SHF_SMALL | SHF_ALLOC)。

  • 遍历所有 section,将每个 section 的 sh_entsize 字段初始化为 ~0UL(全 1 的无符号长整型值)。
  • sh_entsize 在后续用于存储偏移量,初始化为 ~0UL 表示该 section 尚未被分配

外层循环
  • 遍历 masks 数组,依次处理每种类型的 section。
内层循环
  • 遍历所有 section,检查每个 section 是否符合当前规则:
    1. s->sh_flags & masks[m][0] == masks[m][0]:section 必须满足第一个条件。
    2. s->sh_flags & masks[m][1] == 0:section 必须不满足第二个条件。
    3. s->sh_entsize == ~0UL:section 尚未被分配。
    4. !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 函数:

  1. 设置模块状态
    • 将模块的状态设置为 MODULE_STATE_UNFORMED,表示模块尚未完全加载。
  2. 检查模块唯一性
    • 确保模块名称在模块列表中是唯一的,避免重复加载。
  3. 插入模块
    • 如果模块名称唯一,将模块添加到全局模块列表中。
  4. 处理并发和等待
    • 使用互斥锁保护模块列表的访问。
    • 如果发现另一个模块正在加载,等待其完成。

在 Linux 内核中,模块(module)是可以动态加载和卸载的内核代码单元。模块卸载是指从内核中移除一个已经加载的模块,释放其占用的资源(如内存、符号表、设备号等),并清理相关的数据结构。模块卸载是内核模块管理的重要部分,确保系统资源的有效利用和系统的稳定性。

  • 条件编译
    • #ifdef CONFIG_MODULE_UNLOAD 表示这段代码只有在启用了模块卸载功能时才会编译。
  • 导出符号
    • EXPORT_TRACEPOINT_SYMBOL(module_get) 导出 module_get 符号,用于跟踪或调试模块引用计数的变化。
  1. 引用计数(refcnt
    • 引用计数用于跟踪模块的使用情况。
    • 当引用计数为 0 时,表示模块不再被使用,可以被卸载。
    • 引用计数通过原子操作(atomic_setatomic_inc)进行管理,确保在多核环境下的线程安全。
  2. 模块依赖关系
    • source_list 和 target_list 用于管理模块之间的依赖关系。
    • 在卸载模块时,内核会检查这些列表,确保不会卸载正在被其他模块使用的模块。
  3. 原子操作
    • 使用原子操作(如 atomic_setatomic_inc)来修改引用计数,确保在多核环境下的线程安全。
    • 原子操作可以防止多个 CPU 同时修改引用计数,导致数据不一致。
  4. 初始化过程
    • 在模块初始化过程中,临时增加引用计数,防止模块被意外卸载。
    • 初始化完成后,引用计数会根据模块的实际使用情况进行调整。

内核模块的生命周期

  1. 加载
    • 内核模块可以在系统运行时动态加载到内核中。加载过程通常使用 insmod 或 modprobe 命令。
    • 加载时,内核会解析模块文件,提取模块的头部信息,检查模块是否依赖于其他模块,如果依赖则先加载依赖的模块。
    • 然后为模块分配必要的内核资源,如内存空间,并调用模块的初始化函数(通常标记为 __init),完成模块的初始化工作。
    • 初始化完成后,模块信息被注册到内核,使其可以被内核和其他模块使用。
  2. 运行
    • 模块加载后,它就可以执行其功能了。这通常涉及到对硬件的访问、系统调用的处理、内核数据的操作等。
    • 模块运行期间,它可能会响应来自用户空间或其他内核模块的请求。
  3. 卸载
    • 内核模块可以在不再需要时从内核中卸载。卸载过程通常使用 rmmod 或 modprobe -r 命令。
    • 卸载时,内核会检查模块的引用计数,如果计数不为 0,则模块不能卸载。
    • 然后调用模块的清理函数(通常标记为 __exit),释放模块占用的资源,如释放内存、注销设备、销毁数据结构等。
    • 清理完成后,从内核中注销模块,撤销模块注册的信息,并释放模块加载时分配的资源。
    • 如果该模块被其他模块依赖,则先卸载这些依赖模块。

module_unload_init 函数的主要任务是初始化与模块卸载相关的数据结构,确保模块在加载时就已经准备好被正确卸载。具体来说,它执行以下操作:

  • 初始化引用计数
    • 将模块的引用计数(refcnt)设置为 MODULE_REF_BASE,表示模块的基本引用计数。
    • 引用计数用于跟踪模块的使用情况,当引用计数为 0 时,表示模块可以被卸载。
  • 初始化依赖列表
    • 初始化模块的 source_list 和 target_list,用于管理模块之间的依赖关系。
    • source_list 记录依赖当前模块的其他模块。
    • target_list 记录当前模块依赖的其他模块。
  • 增加引用计数
    • 在初始化过程中临时增加引用计数,防止模块在初始化完成之前被意外卸载。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值