insmod 加载内核模块 —— sys_init_module 源码剖析

sys_init_module 系统调用完整实现分析


01 | 概述

sys_init_module 是 Linux 内核中用于加载内核模块(.ko 文件)的系统调用。用户态程序(如 insmodmodprobe)通过该系统调用将内核模块的 ELF 二进制数据从用户空间传递到内核空间,由内核完成模块的解析、重定位、符号解析和初始化执行。

系统调用原型

c 复制代码
SYSCALL_DEFINE3(init_module, void __user *, umod,
                unsigned long, len, const char __user *, uargs)

参数说明:

  • umod:指向用户空间内核模块 ELF 镜像的指针
  • len:镜像的大小(字节数)
  • uargs:传递给模块的参数串(用户空间字符串)

返回值: 成功返回 0,失败返回负的错误码


02 | 总体流程图

复制代码
sys_init_module(umod, len, uargs)
│
├── [1] may_init_module()
│       权限和能力检查
│
├── [2] copy_module_from_user(umod, len, &info)
│       从用户空间拷贝模块数据到内核
│
└── [3] load_module(&info, uargs, 0)
        核心加载流程(详见第三节)

03 | 调用链详细分析

3.1 入口函数:sys_init_module

c 复制代码
// 位置: module.c:4158-4176
SYSCALL_DEFINE3(init_module, void __user *, umod,
                unsigned long, len, const char __user *, uargs)
{
    int err;
    struct load_info info = { };

    err = may_init_module();
    if (err)
        return err;

    pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
           umod, len, uargs);

    err = copy_module_from_user(umod, len, &info);
    if (err)
        return err;

    return load_module(&info, uargs, 0);
}
关键数据结构:struct load_info
c 复制代码
// 位置: module-internal.h:11-29
struct load_info {
    const char *name;           // 模块名称
    struct module *mod;         // 指向临时拷贝中的 module 结构体
    Elf_Ehdr *hdr;              // ELF 头指针
    unsigned long len;          // 模块数据长度
    Elf_Shdr *sechdrs;          // section headers 数组
    char *secstrings, *strtab;  // section 名字符串表, 符号字符串表
    unsigned long symoffs, stroffs, init_typeoffs, core_typeoffs;
    struct _ddebug *debug;      // 动态调试信息
    unsigned int num_debug;     // 动态调试条目数
    bool sig_ok;                // 签名验证是否通过
    struct {
        unsigned int sym, str, mod, vers, info, pcpu;
    } index;                    // 关键 section 的索引
};

3.2 may_init_module --- 权限检查

c 复制代码
// 位置: module.c:3821-3827
static int may_init_module(void)
{
    if (!capable(CAP_SYS_MODULE) || modules_disabled)
        return -EPERM;
    return 0;
}

调用链分析:

调用接口 功能说明
capable(CAP_SYS_MODULE) 检查当前进程是否具有 CAP_SYS_MODULE 能力(通常需要 root 权限)
modules_disabled 全局标志位,通过 /proc/sys/kernel/modules_disabled 控制,设为 1 后禁止加载任何模块

错误返回: -EPERM (Operation not permitted)


3.3 copy_module_from_user --- 从用户空间拷贝模块数据

c 复制代码
// 位置: module.c:3123-3153
static int copy_module_from_user(const void __user *umod, unsigned long len,
                                  struct load_info *info)
{
    int err;
    info->len = len;

    // 长度必须至少大于 ELF header
    if (info->len < sizeof(*(info->hdr)))
        return -ENOEXEC;

    // LSM安全检查: 内核加载数据前回调
    err = security_kernel_load_data(LOADING_MODULE, true);
    if (err)
        return err;

    // 分配内核空间内存
    info->hdr = __vmalloc(info->len, GFP_KERNEL | __GFP_NOWARN);
    if (!info->hdr)
        return -ENOMEM;

    // 分块从用户空间拷贝数据
    if (copy_chunked_from_user(info->hdr, umod, info->len) != 0) {
        err = -EFAULT;
        goto out;
    }

    // LSM安全检查: 内核加载数据后回调
    err = security_kernel_post_load_data((char *)info->hdr, info->len,
                                         LOADING_MODULE, "init_module");
out:
    if (err)
        vfree(info->hdr);
    return err;
}

调用链分析:

调用接口 功能说明
security_kernel_load_data(LOADING_MODULE, true) LSM(Linux Security Module)钩子,在内核加载数据之前 进行安全检查。LOADING_MODULE 标识数据类型为模块加载。true 表示加载全部内容后会有后续检查
`__vmalloc(info->len, GFP_KERNEL __GFP_NOWARN)`
copy_chunked_from_user(info->hdr, umod, info->len) 分块拷贝函数(见下文 3.3.1)
security_kernel_post_load_data(...) LSM 钩子,在内核加载数据之后进行安全检查
vfree(info->hdr) 错误路径下释放已分配的 vmalloc 内存
3.3.1 copy_chunked_from_user --- 分块拷贝
c 复制代码
// 位置: module.c:3073-3086
#define COPY_CHUNK_SIZE (16*PAGE_SIZE)   // 通常是 64KB

static int copy_chunked_from_user(void *dst, const void __user *usrc,
                                   unsigned long len)
{
    do {
        unsigned long n = min(len, COPY_CHUNK_SIZE);
        if (copy_from_user(dst, usrc, n) != 0)
            return -EFAULT;
        cond_resched();       // 让出 CPU,避免长时间占用
        dst += n;
        usrc += n;
        len -= n;
    } while (len);
    return 0;
}
调用接口 功能说明
copy_from_user(dst, usrc, n) 架构相关的用户空间到内核空间拷贝函数。每次最多拷贝 16*PAGE_SIZE 字节,确保不会因单次拷贝过大导致问题
cond_resched() 条件性调度点,在 CONFIG_PREEMPT=n 时允许内核主动让出 CPU,防止长时间禁用抢占

3.4 load_module --- 模块加载核心函数

这是整个模块加载流程的核心,包含 20+ 个步骤。下面按执行顺序逐一分析。

c 复制代码
// 位置: module.c:3936-4156
static int load_module(struct load_info *info, const char __user *uargs,
                       int flags)

步骤 1:module_sig_check --- 模块签名验证
c 复制代码
// 位置: module.c:2900-2958
static int module_sig_check(struct load_info *info, int flags)

功能: 检查模块末尾是否包含签名信息(~Module signature appended~\n 标记),如果有则验证签名。

调用链:

调用接口 功能说明
memcmp(mod + info->len - markerlen, MODULE_SIG_STRING, markerlen) 检查模块末尾是否有签名魔数标记
mod_verify_sig(mod, info) 验证模块的 PKCS#7 数字签名。使用内核密钥环中的公钥验证,同时通过 info->len 获取去除签名后的实际模块大小
is_module_sig_enforced() 检查内核是否强制要求模块签名(CONFIG_MODULE_SIG_FORCE 或内核启动参数 module.sig_enforce
security_locked_down(LOCKDOWN_MODULE_SIGNATURE) 检查内核是否处于锁定模式,锁定模式下禁止加载未签名模块

错误返回: -EKEYREJECTED(签名被拒绝),-ENODATA(无签名),-ENOPKG(不支持的加密算法),-ENOKEY(密钥不可用)


步骤 2:elf_validity_check --- ELF 有效性检查
c 复制代码
// 位置: module.c:2981-3069
static int elf_validity_check(struct load_info *info)

功能: 验证模块 ELF 文件的结构完整性。

检查项:

检查内容 说明
info->len >= sizeof(Elf_Ehdr) 总长度至少大于 ELF 头
memcmp(hdr->e_ident, ELFMAG, SELFMAG) ELF 魔数验证(\x7fELF
hdr->e_type == ET_REL 必须是可重定位文件类型
elf_check_arch(hdr) 架构相关的 ELF 检查(机器类型等)
hdr->e_shentsize == sizeof(Elf_Shdr) section header 大小验证
hdr->e_shoff < info->len section 表偏移不越界
section 名称表索引有效性 (e_shstrndx) 必须在有效范围内
validate_section_offset(info, strhdr) 节名称字符串表偏移验证
字符串表 NUL 结尾检查 确保字符串操作安全
第 0 号 section 检查 必须为 SHT_NULL 类型
遍历所有 section 检查偏移、大小、名称有效性

validate_section_offset(子函数):

c 复制代码
// 位置: module.c:2960-2973
static int validate_section_offset(struct load_info *info, Elf_Shdr *shdr)
{
    unsigned long secend;
    secend = shdr->sh_offset + shdr->sh_size;
    if (secend < shdr->sh_offset || secend > info->len)
        return -ENOEXEC;
    return 0;
}

检查 section 的偏移+大小是否溢出或超过模块总长度。


步骤 3:setup_load_info --- 建立加载信息
c 复制代码
// 位置: module.c:3196-3246
static int setup_load_info(struct load_info *info, int flags)

功能: 解析 ELF 的 section headers,定位关键 section,建立基本信息。

调用链:

调用接口 功能说明
find_sec(info, ".modinfo") 查找 .modinfo section(包含模块元信息)
get_modinfo(info, "name") .modinfo 中提取 name=xxx 字段作为模块名
遍历 section 找 SHT_SYMTAB 定位符号表和对应的字符串表,设置 info->index.syminfo->index.str
find_sec(info, ".gnu.linkonce.this_module") 查找包含 struct module 的 section,设置 info->mod 指向临时拷贝
find_sec(info, "__versions") 查找模块版本信息 section(CONFIG_MODVERSIONS
find_pcpusec(info) 查找 per-cpu 数据 section

find_sec 函数: 遍历所有 section headers,根据名称字符串匹配目标 section,返回索引或 0(未找到)。

get_modinfo 函数: 解析 .modinfo section 中的 key=value 格式字符串,根据 key 查找并返回 value 指针。


步骤 4:blacklisted --- 模块黑名单检查
c 复制代码
// 位置: module.c:3536-3553
static bool blacklisted(const char *module_name)

功能: 检查模块名是否在黑名单中。黑名单由内核启动参数 module_blacklist=mod1,mod2,... 指定,用于阻止特定模块的加载。

错误返回: -EPERM


步骤 5:rewrite_section_headers --- 重写 section headers
c 复制代码
// 位置: module.c:3160-3186
static int rewrite_section_headers(struct load_info *info, int flags)

功能: 将 section header 中的 sh_addr 设置为临时拷贝中的实际地址(info->hdr + sh_offset),并标记不需要分配的 section。

操作 说明
设置 sh_addr = hdr + sh_offset 使 section 地址指向临时拷贝中的实际位置
非 CONFIG_MODULE_UNLOAD 时 移除 .exit section 的 SHF_ALLOC 标志
版本/modinfo section 移除 SHF_ALLOC 标志(由内核单独管理)

步骤 6:check_modstruct_version --- 模块结构版本检查
c 复制代码
// 位置: module.c:1356-1391
static inline int check_modstruct_version(const struct load_info *info,
                                           struct module *mod)

功能: 检查模块编译时使用的 struct module 的 CRC 校验和是否与当前运行内核一致。主要用于 CONFIG_MODVERSIONS 配置下确保 ABI 兼容性。

same_magic 函数(子函数): 对比模块的 vermagic 字符串与内核的 vermagic(包含内核版本、编译器版本、架构等信息)。


步骤 7:layout_and_allocate --- 布局和分配内存
c 复制代码
// 位置: module.c:3555-3612
static struct module *layout_and_allocate(struct load_info *info, int flags)

这是最核心的内存布局和分配步骤。

7.1 check_modinfo --- 模块信息检查
c 复制代码
// 位置: module.c:3248-3290
static int check_modinfo(struct module *mod, struct load_info *info, int flags)
子调用 功能说明
get_modinfo(info, "vermagic") 获取 vermagic 字符串
same_magic(modmagic, vermagic, info->index.vers) 对比模块的 vermagic 与内核 vermagic 是否匹配
try_to_force_load(mod, "bad vermagic") 若允许强制加载(CONFIG_MODULE_FORCE_LOAD),则只加 taint 标记,不返回错误
get_modinfo(info, "intree") 检查是否内核树内模块,否则打 TAINT_OOT_MODULE 标记
check_modinfo_retpoline(mod, info) 检查模块是否使用 retpoline 编译(缓解 Spectre 漏洞)
check_modinfo_livepatch(mod, info) 检查是否为 livepatch 模块,设置 mod->klp 标志并打 TAINT_LIVEPATCH
get_modinfo(info, "staging") 检查是否为 staging 目录模块,打 TAINT_CRAP 标记
set_license(mod, ...) 设置模块许可证信息
7.2 module_frob_arch_sections --- 架构相关 section 调整
c 复制代码
int __weak module_frob_arch_sections(Elf_Ehdr *hdr, Elf_Shdr *sechdrs,
                                      char *secstrings, struct module *mod)

平台相关的弱函数,允许架构代码在布局前修改 section 内容和大小。

7.3 module_enforce_rwx_sections --- 强制执行 RWX section

检查并确保模块的代码段、只读数据段和读写数据段正确分离,为后续设置内存权限做准备。

7.4 layout_sections --- section 布局
c 复制代码
// 位置: module.c:2466-2584
static void layout_sections(struct module *mod, struct load_info *info)

功能: 按照类似链接器的规则排布所有 SHF_ALLOC section。布局顺序为:

  1. 可执行代码段 (SHF_EXECINSTR | SHF_ALLOC)
  2. 只读数据段 (SHF_ALLOC)
  3. ro_after_init 数据段 (SHF_RO_AFTER_INIT | SHF_ALLOC)
  4. 读写数据段 (SHF_WRITE | SHF_ALLOC)
  5. 小数据段 (ARCH_SHF_SMALL | SHF_ALLOC)

分别对 core_layoutinit_layout 执行两遍相同的布局。

子函数 get_offset:

c 复制代码
// 位置: module.c:2451-2460
static long get_offset(struct module *mod, unsigned int *size,
                       Elf_Shdr *sechdr, unsigned int section)
{
    *size += arch_mod_section_prepend(mod, section);
    ret = ALIGN(*size, sechdr->sh_addralign ?: 1);
    *size = ret + sechdr->sh_size;
    return ret;
}

计算每个 section 的对齐偏移,并将偏移量写入 sh_entsize 字段。init 段的偏移量最高位设置 INIT_OFFSET_MASK 以区分 core/init。

7.5 layout_symtab --- 符号表布局

计算符号表和字符串表的大小并记录偏移。

7.6 move_module --- 移动到最终位置
c 复制代码
// 位置: module.c:3407-3468
static int move_module(struct module *mod, struct load_info *info)
子调用 功能说明
module_alloc(mod->core_layout.size) 分配模块 core 区域的内存(通常是 vmalloc 可执行区域)
kmemleak_not_leak(ptr) 通知 kmemleak 此指针不是内存泄漏(被 struct module 引用)
memset(ptr, 0, ...) 清零分配的内存
module_alloc(mod->init_layout.size) 分配 init 区域内存(模块初始化后释放)
kmemleak_ignore(ptr) 通知 kmemleak 忽略 init 区域(因为将被释放)
memcpy(dest, src, size) 将所有 SHF_ALLOC section 从临时拷贝复制到最终位置,更新 sh_addr
7.7 kmemleak_load_module --- 标记模块内存

通知 kmemleak 模块的各个区域(core、init、per-cpu、data)不是内存泄漏。


步骤 8:audit_log_kern_module --- 审计日志
c 复制代码
audit_log_kern_module(mod->name);

向内核审计子系统记录模块加载事件。


步骤 9:add_unformed_module --- 添加到模块列表
c 复制代码
// 位置: module.c:3834-3866
static int add_unformed_module(struct module *mod)

功能: 在投入更多资源之前,先将模块加入全局模块链表,确保模块名唯一。

子调用 功能说明
mod->state = MODULE_STATE_UNFORMED 设置模块初始状态
find_module_all(mod->name, ...) 查找是否已有同名模块
wait_event_interruptible(module_wq, finished_loading(mod->name)) 如果同名模块正在加载,等待其完成
finished_loading(mod->name) 检查同名模块是否已加载完成
mod_update_bounds(mod) 更新全局模块地址边界 (module_addr_min/module_addr_max)
list_add_rcu(&mod->list, &modules) 将模块加入 RCU 保护的全局模块链表
mod_tree_insert(mod) 将模块插入地址查找树(用于快速地址→模块映射)

步骤 10:add_taint_module --- 模块签名污染标记
c 复制代码
// (在 CONFIG_MODULE_SIG 下)
if (!mod->sig_ok) {
    add_taint_module(mod, TAINT_UNSIGNED_MODULE, LOCKDEP_STILL_OK);
}

如果签名验证未通过,给内核打上 TAINT_UNSIGNED_MODULE 标记。


步骤 11:percpu_modalloc --- 分配 Per-CPU 内存
c 复制代码
err = percpu_modalloc(mod, info);

为模块分配 per-CPU 变量所需的内存。必须在模块唯一性确认后进行,以避免临时 per-CPU 内存耗尽。


步骤 12:module_unload_init --- 初始化卸载支持
c 复制代码
// 位置: module.c:825-840
static int module_unload_init(struct module *mod)
操作 说明
atomic_set(&mod->refcnt, MODULE_REF_BASE) 初始化引用计数基准值
INIT_LIST_HEAD(&mod->source_list) 初始化源模块列表(依赖本模块的模块)
INIT_LIST_HEAD(&mod->target_list) 初始化目标模块列表(本模块依赖的模块)
atomic_inc(&mod->refcnt) 加载期间持有引用,防止被卸载

步骤 13:init_param_lock --- 初始化参数锁
c 复制代码
init_param_lock(mod);

初始化模块参数访问互斥锁。


步骤 14:find_module_sections --- 查找可选 section
c 复制代码
// 位置: module.c:3292-3405
static int find_module_sections(struct module *mod, struct load_info *info)

功能: 定位并解析模块的各个可选 section,设置 struct module 中对应的指针。

section 名称 存储字段 说明
__param mod->kp, mod->num_kp 模块参数描述
__ksymtab mod->syms, mod->num_syms 导出的内核符号
__kcrctab mod->crcs 导出符号的 CRC
__ksymtab_gpl mod->gpl_syms GPL-only 导出符号
__ksymtab_gpl_future mod->gpl_future_syms Future GPL 导出符号
.ctors / .init_array mod->ctors, mod->num_ctors 全局构造函数
.noinstr.text mod->noinstr_text_start noinstr 代码段
__tracepoints_ptrs tracepoint 指针 (CONFIG_TRACEPOINTS)
__jump_table mod->jump_entries jump label 表 (CONFIG_JUMP_LABEL)
_ftrace_events mod->trace_events ftrace 事件 (CONFIG_EVENT_TRACING)
__ex_table mod->extable 异常表

子函数:

  • section_objs(info, name, obj_size, num):获取 section 内容为对象数组
  • section_addr(info, name):获取 section 内容的地址

步骤 15:check_module_license_and_versions --- 许可证检查
c 复制代码
// 位置: module.c:3470-3509
static int check_module_license_and_versions(struct module *mod)
检查项 说明
ndiswrapper/driverloader/lve 特殊模块污染检查:这些已知模块使用 add_taint(TAINT_PROPRIETARY_MODULE)
MODVERSIONS CRC 检查 确保有导出符号就有对应的 CRC(CONFIG_MODVERSIONS

步骤 16:setup_modinfo --- 设置模块信息属性
c 复制代码
// 位置: module.c:2618-2627
static void setup_modinfo(struct module *mod, struct load_info *info)

遍历 modinfo_attrs 数组(包含 version、srcversion、vermagic 等),从 .modinfo section 中读取值并通过各属性的 setup 回调保存到 struct module 中。


步骤 17:simplify_symbols --- 符号解析
c 复制代码
// 位置: module.c:2338-2405
static int simplify_symbols(struct module *mod, const struct load_info *info)

功能: 解析模块中所有符号的最终地址。这是模块加载中最关键的步骤之一。

对符号表中的每个符号按类型处理:

符号类型 处理方式
SHN_COMMON 忽略(__gnu_lto* 前缀)或报错(应使用 -fno-common 编译)
SHN_ABS 绝对符号,无需修改
SHN_LIVEPATCH livepatch 符号,由 livepatch 框架解析
SHN_UNDEF (未定义符号) 调用 resolve_symbol_wait(mod, info, name) 在内核中查找
其他 (已定义符号) 加上其 section 的基地址 (sh_addr);per-cpu 变量使用 mod_percpu(mod)

关键子调用:

调用接口 功能说明
resolve_symbol_wait(mod, info, name) 在内核符号表和已加载模块中查找符号。如果符号所属模块正在加载,则等待其完成。查找成功返回 kernel_symbol 结构体指针
kernel_symbol_value(ksym) 获取内核符号的实际地址值
ignore_undef_symbol(machine, name) 检查是否为可忽略的弱符号(特定架构的特殊符号)
mod_percpu(mod) 获取模块 per-cpu 区域的基地址

步骤 18:apply_relocations --- 应用重定位
c 复制代码
// 位置: module.c:2407-2440
static int apply_relocations(struct module *mod, const struct load_info *info)

功能: 遍历所有 section,对重定位 section(SHT_RELSHT_RELA)执行重定位操作。

重定位类型 处理函数 说明
SHF_RELA_LIVEPATCH klp_apply_section_relocs() livepatch 专用重定位
SHT_REL apply_relocate(sechdrs, strtab, symindex, i, mod) 标准 REL 重定位(架构相关)
SHT_RELA apply_relocate_add(sechdrs, strtab, symindex, i, mod) 标准 RELA 重定位(架构相关)

架构相关的重定位函数(如 ARM、x86)负责根据重定位类型修正确代码/数据中的地址引用。


步骤 19:post_relocation --- 重定位后处理
c 复制代码
// 位置: module.c:3630-3644
static int post_relocation(struct module *mod, const struct load_info *info)
子调用 功能说明
sort_extable(mod->extable, ...) 对异常表条目排序(用于内核异常处理快速查找)
percpu_modcopy(mod, ...) 将 per-cpu 模板数据复制到已分配的 per-cpu 区域
add_kallsyms(mod, info) 为模块设置 kallsyms(内核符号表)信息
module_finalize(info->hdr, info->sechdrs, mod) 架构相关的模块最终化处理(如 ARM 上的 PLT 处理等)

步骤 20:flush_module_icache --- 刷新指令缓存
c 复制代码
// 位置: module.c:3511-3524
static void flush_module_icache(const struct module *mod)
{
    if (mod->init_layout.base)
        flush_icache_range((unsigned long)mod->init_layout.base,
                           (unsigned long)mod->init_layout.base
                           + mod->init_layout.size);
    flush_icache_range((unsigned long)mod->core_layout.base,
                       (unsigned long)mod->core_layout.base
                       + mod->core_layout.size);
}

功能: 因为已修改代码段(应用了重定位),刷新指令缓存确保 CPU 获取最新指令。

子函数 flush_icache_range 架构相关的 I-Cache 刷新函数。在 ARM 上通常使用 flush_icache_range() 或 CP15 协处理器指令。


步骤 21:strndup_user --- 复制模块参数
c 复制代码
mod->args = strndup_user(uargs, ~0UL >> 1);

从用户空间复制模块参数串(最大 INT_MAX 长度)。


步骤 22:dynamic_debug_setup --- 动态调试设置
c 复制代码
dynamic_debug_setup(mod, info->debug, info->num_debug);

设置模块的动态调试(pr_debug/dev_dbg 等)控制。


步骤 23:ftrace_module_init --- Ftrace 初始化
c 复制代码
ftrace_module_init(mod);

注册模块的 ftrace 相关信息和 mcount 调用点位置。必须在 MODULE_STATE_UNFORMED 状态下调用。


步骤 24:complete_formation --- 完成模块成型
c 复制代码
// 位置: module.c:3868-3896
static int complete_formation(struct module *mod, struct load_info *info)
子调用 功能说明
verify_exported_symbols(mod) 检查导出符号是否与已有符号冲突(必须在 module_mutex 锁下调用)
module_bug_finalize(info->hdr, info->sechdrs, mod) 解析模块的 __bug_table section,注册 BUG/WARN 信息
module_enable_ro(mod, false) 设置模块只读区域为只读(代码段、rodata 段)
module_enable_nx(mod) 设置模块非可执行区域为不可执行(NX/DEP)
module_enable_x(mod) 设置模块可执行区域
mod->state = MODULE_STATE_COMING 将状态切换为 COMING(kallsyms 等可见,但 strong_try_module_get() 忽略)

module_enable_romodule_enable_nxmodule_enable_x 这三个函数通过 set_memory_ro()set_memory_nx()set_memory_x() 等修改页表权限,利用硬件 MMU 实现内存保护。


步骤 25:prepare_coming_module --- 准备模块到来
c 复制代码
// 位置: module.c:3898-3914
static int prepare_coming_module(struct module *mod)
子调用 功能说明
ftrace_module_enable(mod) 启用模块的 ftrace 功能
klp_module_coming(mod) 通知 livepatch 子系统新模块到来(livepatch 可能打补丁)
blocking_notifier_call_chain_robust(&module_notify_list, MODULE_STATE_COMING, MODULE_STATE_GOING, mod) 调用模块通知链上的所有回调。如果任何回调失败,则回滚(切换到 GOING 状态并重调)

步骤 26:parse_args --- 解析模块参数
c 复制代码
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
                          -32768, 32767, mod,
                          unknown_module_param_cb);

功能: 解析用户传入的模块参数串,调用各参数对应的 set 函数。参数值范围限定为 [-32768, 32767]

  • 返回非 NULL:有 -- 后的参数被忽略
  • 返回 ERR_PTR:参数解析失败

步骤 27:mod_sysfs_setup --- Sysfs 设置
c 复制代码
err = mod_sysfs_setup(mod, info, mod->kp, mod->num_kp);

/sys/module/<name>/ 下创建 sysfs 目录和属性文件(parameters、sections、version 等)。


步骤 28:copy_module_elf --- Livepatch ELF 拷贝
c 复制代码
if (is_livepatch_module(mod)) {
    err = copy_module_elf(mod, info);
}

如果是 livepatch 模块,保存模块的 ELF 信息以便后续打补丁时使用。


步骤 29:free_copy --- 释放临时拷贝
c 复制代码
// 位置: module.c:3155-3158
static void free_copy(struct load_info *info)
{
    vfree(info->hdr);
}

释放 copy_module_from_user 中分配的临时内存。


步骤 30:trace_module_load --- 跟踪事件
c 复制代码
trace_module_load(mod);

通过 ftrace tracepoint 记录模块加载事件。


步骤 31:do_init_module --- 执行模块初始化
c 复制代码
// 位置: module.c:3705-3819
static noinline int do_init_module(struct module *mod)

这是加载流程的最后一步,真正"启动"模块。

执行步骤 调用接口 功能说明
1 kmalloc(sizeof(*freeinit), GFP_KERNEL) 分配 init 释放跟踪结构
2 current->flags &= ~PF_USED_ASYNC 清除异步标志(追踪模块 init 是否使用 async)
3 do_mod_ctors(mod) 调用模块的全局构造函数(.ctors / .init_array section)
4 do_one_initcall(mod->init) 调用模块的 init_module() 函数。这是 module_init 宏注册的函数
5 mod->state = MODULE_STATE_LIVE 模块进入 LIVE 状态,成为"一等公民"
6 blocking_notifier_call_chain(&module_notify_list, MODULE_STATE_LIVE, mod) 通知各子系统模块已激活
7 kobject_uevent(&mod->mkobj.kobj, KOBJ_ADD) 发送 uevent 到用户空间(触发 udev 规则等)
8 async_synchronize_full() 如果 init 期间产生了异步任务,等待所有异步任务完成
9 ftrace_free_mem(mod, ...) 释放 init 区域中的 ftrace 内存
10 module_put(mod) 释放初始化期间持有的引用计数
11 trim_init_extable(mod) 裁剪异常表(移除 init 段的异常表条目)
12 rcu_assign_pointer(mod->kallsyms, &mod->core_kallsyms) 切换到 core kallsyms(指向 core_layout)
13 module_enable_ro(mod, true) 再次锁定只读区域(包含 ro_after_init 数据段)
14 mod_tree_remove_init(mod) 从模块地址树中移除 init 段
15 module_arch_freeing_init(mod) 架构相关的 init 清理(如释放 PLT 等)
16 清空 init_layout 字段 将 base/size/ro_size 等全部置 0
17 llist_add(&freeinit->node, &init_free_list) + schedule_work(&init_free_wq) 通过工作队列异步释放 init 段内存

init 段释放机制: 初始化成功后,init 段通过 do_free_init 工作队列延迟释放。延迟释放的原因是在 RCU 宽限期之后才能安全释放(因为 kallsyms 可能在禁用抢占的情况下遍历这些地址)。


04 | 模块状态机

模块在加载过程中经历以下状态转换:

复制代码
UNFORMED  ──→  COMING  ──→  LIVE  ──→  GOING
   ↑              ↑           ↑          ↑
add_unformed  complete_   do_init_   失败/
  _module     formation   module      卸载
状态 说明
MODULE_STATE_UNFORMED 模块已分配内存但未完成布局
MODULE_STATE_COMING 模块正在初始化中,kallsyms 可见
MODULE_STATE_LIVE 模块已完全激活,正常运行
MODULE_STATE_GOING 模块正在被卸载

05 | 数据流图

复制代码
用户空间                        内核空间
========                        ========

insmod/modprobe
     │
     │ init_module(umod, len, uargs)
     ▼
     │ copy_module_from_user ───────► info->hdr (vmalloc 临时拷贝)
     │
     │ load_module
     │   ├─ module_sig_check ────────► 签名验证
     │   ├─ elf_validity_check ──────► ELF 校验
     │   ├─ setup_load_info ─────────► info->mod, info->index 设置
     │   ├─ layout_and_allocate
     │   │   ├─ layout_sections ─────► 计算 core/init 布局大小
     │   │   └─ move_module ─────────► module_alloc (最终位置)
     │   │                              拷贝 section 到最终位置
     │   ├─ simplify_symbols ────────► 符号地址解析
     │   ├─ apply_relocations ───────► 重定位修正
     │   ├─ post_relocation ─────────► 异常表排序, kallsyms
     │   ├─ complete_formation ──────► RO/NX 内存权限设置
     │   ├─ prepare_coming_module ───► 通知链 + ftrace
     │   ├─ parse_args ──────────────► 参数解析
     │   ├─ mod_sysfs_setup ─────────► /sys/module/xxx/
     │   ├─ free_copy ───────────────► vfree(临时拷贝)
     │   │
     │   └─ do_init_module
     │        ├─ do_mod_ctors ───────► 执行构造函数
     │        ├─ do_one_initcall ────► 调用 module_init()
     │        ├─ kobject_uevent ─────► KOBJ_ADD 事件
     │        └─ schedule_work ──────► 异步释放 init 段
     │
     ▼
  返回 0 (成功)

06 | 总结

insmod加载模块时,调用sys_init_module的过程其实相当于自己买一个高达模型进行拼接的过程,或者说 insmod 把 .ko 当成"带补丁的工程图纸",解析后分配真实运行内存,修正所有地址依赖,最后调用初始化函数

步骤 内核做的事 类比
1 从用户空间读 .ko 组装高达的"图纸包"
2 vmalloc 暂存 把图纸包放在临时工作台上
3 解析 ELF 拆开图纸,区分"一次性工具"和"长期结构"
4 分配 core / init 内存 规划哪里放柱子、哪里放工具
5 拷贝 section 按图纸搭建真正家具
6 重定位 把所有"这里放钉子"的位置,按真实位置打孔
7 执行 init 第一次开灯、测试
8 释放 init 段 收走一次性使用的手套和说明书
相关推荐
hj2862511 小时前
Linux网络基础一
linux·运维
小义_1 小时前
【Linux 1】
linux·运维·云原生·红帽
面向对象World1 小时前
Z8350 Broadcom SDIO网卡调试Ubuntu 22.04 Server版
linux·运维·ubuntu
Irissgwe1 小时前
12、多路转接 select
linux·io多路转接·select
无足鸟ICT2 小时前
【RHCA+】编辑多个文件
linux
fengyehongWorld2 小时前
Linux fd命令
linux
AIMath~2 小时前
hermes agent安装在Linux centos中
linux·运维·服务器
赵民勇2 小时前
如何查看一个二进制程序是否设置了rpath或runpath?
linux·c++
小王师傅662 小时前
深入解析:Docker在Mac上的运行本质与Linux进程管理机制
linux·macos·docker