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 段 收走一次性使用的手套和说明书
相关推荐
groundhappy18 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕18 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫19 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生20 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1121 小时前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei371 天前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源
笑锝没心没肺1 天前
yum update 报错:GPG key retrieval failed: [Errno 14] HTTPS Error 404 - Not Found
linux·运维·服务器