Skip to content

Latest commit

 

History

History
2234 lines (1740 loc) · 80.2 KB

File metadata and controls

2234 lines (1740 loc) · 80.2 KB

Linux 内核模块系统深度解析

基于 Linux Kernel 源码深度分析 关键文件:include/linux/module.h | include/linux/moduleparam.h | include/linux/init.h | kernel/module/main.c | kernel/module/signing.c


目录

  1. 内核模块概述
  2. .ko ELF 文件格式深度解析
  3. 模块加载流程(load_module)
  4. 符号导出与解析机制
  5. 模块参数系统
  6. 模块卸载机制
  7. 模块签名验证
  8. initcall 机制与优先级体系
  9. 模块内存保护
  10. 常见模块开发模式
  11. 符号解析详细流程剖析
  12. 模块内核符号命名空间
  13. 模块的 livepatch 支持
  14. 模块系统的 RCU 并发模型
  15. 模块构建系统(Kbuild)
  16. 深入案例:xt_RATEEST 模块
  17. 模块调试技术
  18. 模块与 BPF 的交互
  19. 关键数据结构关系图
  20. 工具链与调试命令速查

1. 内核模块概述

1.1 为什么需要模块

Linux 内核面临一个根本矛盾:硬件种类繁多、驱动数量庞大,但系统内存有限,不可能把所有驱动代码都常驻内存。内核模块(Loadable Kernel Module,LKM)是这一矛盾的工程化解法。

模块本质是一段可被动态链接进内核地址空间的 ELF 目标文件(.ko),它运行在内核空间,与内核共享同一套虚拟地址,具备与内核代码完全相同的权限。这是模块与用户态动态库(.so)的本质区别——.so 通过 MMU 地址映射运行在用户空间,.ko 直接映射进内核地址空间。

1.2 静态内核 vs 模块化内核的权衡

                静态编译(built-in)          模块化(loadable)
              +-------------------------+  +------------------------+
  启动速度    | 快,无需加载过程          |  | 需运行时 insmod/modprobe |
  内存占用    | 始终驻留,不可释放        |  | 按需加载,用后可卸载      |
  初始化顺序  | 严格按 initcall 级别      |  | insmod 时即运行 init()   |
  符号可见性  | 内核全局可见              |  | 需显式 EXPORT_SYMBOL     |
  调试能力    | 难以热更新                |  | 可重复加载(开发期方便)   |
  安全性      | 无法被用户态任意注入      |  | 需签名验证等机制          |
  适用场景    | 核心调度器、内存管理      |  | 驱动、文件系统、netfilter  |
              +-------------------------+  +------------------------+

通常的实践是:核心调度器、内存管理、VFS 框架静态编译;设备驱动、网络过滤器、文件系统做成模块。make menuconfig 中每个选项的 m(module)选项正是这一机制的体现。

1.3 模块与内核的关系

  用户空间
  ┌──────────────────────────────────────────┐
  │  insmod / modprobe / rmmod               │
  │     ↓ syscall (init_module / finit_module)│
  └──────────────────────────────────────────┘
          │
          ▼
  内核空间(同一虚拟地址空间)
  ┌──────────────────────────────────────────────────────┐
  │                                                      │
  │  vmalloc 区域                                        │
  │  ┌──────────┐  ┌──────────┐  ┌──────────┐           │
  │  │ module A │  │ module B │  │ module C │  ...       │
  │  │ .text    │  │ .text    │  │ .text    │           │
  │  │ .data    │  │ .data    │  │ .data    │           │
  │  └──────────┘  └──────────┘  └──────────┘           │
  │                                                      │
  │  内核核心代码区(.text/.data)                        │
  │  ┌──────────────────────────────┐                   │
  │  │ vmlinux(内核主体)           │                   │
  │  │ __ksymtab(导出符号表)       │                   │
  │  └──────────────────────────────┘                   │
  └──────────────────────────────────────────────────────┘

2. .ko ELF 文件格式深度解析

2.1 .ko 是标准 ELF 可重定位文件

.ko 文件是 ELF Type = ET_REL(可重定位目标文件),与普通 .o 文件相同,但包含了内核模块特有的 section。链接器没有对 .ko 做最终链接,重定位表项还保留着,由内核在加载时完成最终重定位。

.ko 文件布局(ELF ET_REL):

  +--------------------+  偏移 0
  | ELF Header (64B)   |  e_type=ET_REL, e_machine=EM_X86_64
  |                    |  e_shoff → 节头表偏移, e_shnum → 节数量
  +--------------------+
  |                    |
  | Section Data       |  各节的实际内容(字节流)
  |                    |
  | .text              |  机器指令
  | .init.text         |  __init 函数(初始化后可释放)
  | .exit.text         |  __exit 函数
  | .data              |  已初始化全局变量
  | .bss               |  未初始化全局变量(仅记录大小)
  | .rodata            |  只读数据(字符串常量等)
  | .modinfo           |  MODULE_INFO() 生成的键值对
  | .gnu.linkonce.this_module  │ struct module 原型
  | __param            |  模块参数描述符(kernel_param 数组)
  | __ksymtab          |  EXPORT_SYMBOL() 导出的符号表
  | __ksymtab_gpl      |  EXPORT_SYMBOL_GPL() 导出的符号表
  | __kcrctab          |  CRC 校验表(CONFIG_MODVERSIONS)
  | .symtab            |  ELF 符号表
  | .strtab            |  符号名字符串表
  | .rela.text         |  .text 节的重定位表
  | .rela__param       |  参数节重定位表
  |                    |
  +--------------------+
  | Section Header Table|  节头表:每个 Elf64_Shdr 64 字节
  +--------------------+

2.2 关键 ELF 节详解

2.2.1 .text 节

包含模块的可执行代码。.init.text 子节包含所有用 __init 标记的函数,这些函数在模块初始化完成后会被释放。include/linux/init.h:45 中的宏定义:

// include/linux/init.h:45
#define __init   __section(".init.text") __cold __latent_entropy \
                            __no_kstack_erase
#define __initdata   __section(".init.data")
#define __initconst  __section(".init.rodata")
#define __exitdata   __section(".exit.data")
#define __exit_call  __used __section(".exitcall.exit")

__cold 属性告诉编译器这是冷路径代码,生成时会放到函数末尾,减少 icache 压力。__latent_entropy 用于内核熵池初始化。__no_kstack_erase 禁用该函数的栈擦除(节省初始化阶段的性能开销)。

2.2.2 .data / .bss 节

  • .data:已初始化的全局/静态变量(读写)
  • .bss:未初始化的全局/静态变量,ELF 中仅记录大小,不存储实际数据,由内核加载器在分配内存后 memset 为零
  • .rodata:只读数据,加载后映射为只读页
  • .data..ro_after_init:加载时可写,init() 完成后变为只读(对应 mod_mem_type 中的 MOD_RO_AFTER_INIT,见 include/linux/module.h:323
  • .data..read_mostly:主要被读取的数据,编译器/链接器将其聚合在同一缓存行,减少写入时的缓存失效

2.2.3 .modinfo 节——模块元信息

.modinfo 节由 MODULE_INFO() 宏填充,存储键值对字符串,每个字符串以 \0 结尾,格式为 key=valueinclude/linux/moduleparam.h:33-39):

// include/linux/moduleparam.h:33
#define MODULE_INFO(tag, info)                            \
    static_assert(                                        \
        sizeof(info) - 1 == __builtin_strlen(info),       \
        "MODULE_INFO(" #tag ", ...) contains embedded NUL byte"); \
    static const char __UNIQUE_ID(modinfo)[]              \
        __used __section(".modinfo") __aligned(1)         \
        = __MODULE_INFO_PREFIX __stringify(tag) "=" info

MODULE_LICENSE() 正是通过这个宏实现(include/linux/module.h:230):

// include/linux/module.h:230
#define MODULE_LICENSE(_license) MODULE_FILE MODULE_INFO(license, _license)

MODULE_ALIAS() 同理(include/linux/module.h:163):

// include/linux/module.h:163
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)

.modinfo 节的内容可通过 modinfo <module.ko> 命令查看,内核加载时用这些信息进行各种检查(如 vermagic 版本魔数校验)。

modinfo 常见字段示例(以当前工作区 net/netfilter/xt_DSCP.c 为例):

license=GPL
author=Harald Welte <laforge@netfilter.org>
description=Xtables: DSCP/TOS field match
alias=ipt_dscp
alias=ip6t_dscp
alias=ipt_tos
alias=ip6t_tos
vermagic=6.x.x SMP preempt mod_unload ...

这对应 net/netfilter/xt_DSCP.c:16-22 的声明:

// net/netfilter/xt_DSCP.c:16
MODULE_AUTHOR("Harald Welte <laforge@netfilter.org>");
MODULE_DESCRIPTION("Xtables: DSCP/TOS field match");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_dscp");
MODULE_ALIAS("ip6t_dscp");

2.2.4 .gnu.linkonce.this_module 节——struct module 原型

这是模块中最特殊的节,它包含模块自身的 struct module 实例(__this_module)。内核加载时将该节的数据复制到内核管理的内存中,并填充运行时字段。

include/linux/init.h:173-174 中:

// include/linux/init.h:173
#ifdef MODULE
extern struct module __this_module;
#define THIS_MODULE (&__this_module)
#else
#define THIS_MODULE ((struct module *)0)
#endif

模块代码通过 THIS_MODULE 指针引用自身的 struct module,用于引用计数、注册时标识所属模块等。

2.2.5 __param 节——模块参数

module_param() 最终通过 __module_param_call 宏将一个 struct kernel_param 变量放入 __param 节(include/linux/moduleparam.h:299-306):

// include/linux/moduleparam.h:299
#define __module_param_call(prefix, name, ops, arg, perm, level, flags) \
    static_assert(sizeof(""prefix) - 1 <= __MODULE_NAME_LEN);   \
    static const char __param_str_##name[] = prefix #name;      \
    static struct kernel_param __moduleparam_const __param_##name \
    __used __section("__param")                                  \
    __aligned(__alignof__(struct kernel_param))                  \
    = { __param_str_##name, THIS_MODULE, ops,                    \
        VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }

2.3 struct module 在 ELF 中的位置

struct module 是内核中描述一个已加载模块的核心数据结构,定义在 include/linux/module.h:397-585。其关键字段如下:

// include/linux/module.h:397
struct module {
    enum module_state state;          // 模块状态:LIVE/COMING/GOING/UNFORMED

    struct list_head list;            // 全局 modules 链表节点
    char name[MODULE_NAME_LEN];       // 模块名(最长 56 字节)

    struct module_kobject mkobj;      // sysfs kobject
    struct module_attribute *modinfo_attrs;
    const char *version;
    const char *srcversion;
    struct kobject *holders_dir;

    /* 导出符号 */
    const struct kernel_symbol *syms; // EXPORT_SYMBOL 符号表
    const u32 *crcs;                  // 符号 CRC(MODVERSIONS)
    unsigned int num_syms;

    /* GPL-only 符号 */
    unsigned int num_gpl_syms;
    const struct kernel_symbol *gpl_syms;
    const u32 *gpl_crcs;
    bool using_gplonly_symbols;

#ifdef CONFIG_MODULE_SIG
    bool sig_ok;                      // 签名验证结果
#endif

    /* 异常表 */
    unsigned int num_exentries;
    struct exception_table_entry *extable;

    /* 初始化/清理函数指针 */
    int (*init)(void);

    /* 内存区域:MOD_TEXT/MOD_DATA/MOD_RODATA/... */
    struct module_memory mem[MOD_MEM_NUM_TYPES] __module_memory_align;

    struct mod_arch_specific arch;    // 架构特定数据
    unsigned long taints;             // 污点标志

    /* KALLSYMS */
    struct mod_kallsyms __rcu *kallsyms;
    struct mod_kallsyms core_kallsyms;

    char *args;                       // 模块参数字符串

#ifdef CONFIG_MODULE_UNLOAD
    struct list_head source_list;     // 依赖本模块的模块列表
    struct list_head target_list;     // 本模块依赖的模块列表
    void (*exit)(void);               // 清理函数指针
    atomic_t refcnt;                  // 引用计数
#endif
} ____cacheline_aligned __randomize_layout;

__randomize_layout 标记表示在开启 CONFIG_RANDSTRUCT 时,结构体字段顺序会被随机化以对抗内核堆喷攻击。

enum mod_mem_type 定义了模块内存的七种类型(include/linux/module.h:318-329):

// include/linux/module.h:318
enum mod_mem_type {
    MOD_TEXT = 0,       // 代码段(可执行)
    MOD_DATA,           // 数据段(读写)
    MOD_RODATA,         // 只读数据
    MOD_RO_AFTER_INIT,  // init 后变为只读
    MOD_INIT_TEXT,      // __init 代码(初始化后释放)
    MOD_INIT_DATA,      // __initdata(初始化后释放)
    MOD_INIT_RODATA,    // __initconst(初始化后释放)
    MOD_MEM_NUM_TYPES,
};

3. 模块加载流程(load_module)

3.1 系统调用入口

内核提供两个加载模块的系统调用,均定义在 kernel/module/main.c

用户空间工具调用链:

  insmod <module.ko>
      │
      ├─ 旧式:syscall(SYS_init_module, buf, len, args)
      │         ↓
      │    SYSCALL_DEFINE3(init_module) [main.c:3570]
      │         → copy_module_from_user() 将 .ko 复制到内核
      │         → load_module(&info, uargs, 0)
      │
      └─ 新式:syscall(SYS_finit_module, fd, args, flags)
                ↓
           SYSCALL_DEFINE3(finit_module) [main.c:3735]
                → idempotent_init_module()
                → init_module_from_file()
                → load_module(&info, uargs, flags)

init_module 系统调用实现(kernel/module/main.c:3570):

// kernel/module/main.c:3570
SYSCALL_DEFINE3(init_module, void __user *, umod,
        unsigned long, len, const char __user *, uargs)
{
    int err;
    struct load_info info = { };

    err = may_init_module();   // 检查 CAP_SYS_MODULE 权限
    if (err)
        return err;

    err = copy_module_from_user(umod, len, &info);  // 复制 ELF 到内核
    if (err)
        return err;

    return load_module(&info, uargs, 0);
}

finit_modulekernel/module/main.c:3735)是更现代的接口,接受文件描述符而非内存指针,支持通过文件签名做幂等性检查,避免同一模块被并发加载两次。

3.2 load_module() 主流程

load_module() 是整个加载过程的核心函数(kernel/module/main.c:3358),其完整流程如下:

load_module(info, uargs, flags)
│
├─ 1. module_sig_check()          签名校验(最早执行)
│
├─ 2. elf_validity_cache_copy()   ELF 头合法性校验 + 缓存节头表
│
├─ 3. early_mod_check()           早期模块检查(vermagic、许可证等)
│
├─ 4. layout_and_allocate()       计算内存布局 + module_alloc() 分配
│     └─ 为 7 种 mod_mem_type 分别分配 vmalloc 区域
│
├─ 5. add_unformed_module()       加入 modules 全局链表(状态=UNFORMED)
│
├─ 6. percpu_modalloc()           分配 per-CPU 数据
│
├─ 7. module_unload_init()        初始化引用计数和依赖链表
│
├─ 8. find_module_sections()      定位各特殊节(__param、__ksymtab 等)
│
├─ 9. check_export_symbol_versions()  检查 CRC 版本兼容性
│
├─ 10. setup_modinfo()            从 .modinfo 节提取 version/srcversion
│
├─ 11. simplify_symbols()         符号解析:对 .symtab 中的未定义符号
│       └─ resolve_symbol() → find_symbol() → 查内核符号表或其他模块
│
├─ 12. apply_relocations()        应用重定位表(.rela.text 等)
│
├─ 13. post_relocation()          重定位后的修正工作
│
├─ 14. flush_module_icache()      刷新指令缓存
│
├─ 15. complete_formation()       将模块状态升为 COMING
│
├─ 16. prepare_coming_module()    通知链(MODULE_STATE_COMING)
│
├─ 17. parse_args()               解析命令行参数到 module->kp
│
├─ 18. mod_sysfs_setup()          创建 /sys/module/<name>/ 目录
│
├─ 19. free_copy()                释放临时内核缓冲区(ELF 文件副本)
│
└─ 20. do_init_module()           调用 mod->init(),释放 __init 内存

3.3 ELF 解析:节头表与符号表

elf_validity_cache_copy() 完成基本合法性检查后,内核通过以下辅助函数操作 ELF 结构(kernel/module/main.c:236-313):

// kernel/module/main.c:236
static unsigned int find_sec(const struct load_info *info, const char *name)
{
    unsigned int i;
    for (i = 1; i < info->hdr->e_shnum; i++) {
        Elf_Shdr *shdr = &info->sechdrs[i];
        if ((shdr->sh_flags & SHF_ALLOC)
            && strcmp(info->secstrings + shdr->sh_name, name) == 0)
            return i;
    }
    return 0;
}

load_info 结构体缓存了 ELF 头指针、节头表数组、节字符串表等信息,避免重复解析。每个节通过 Elf_Shdr(即 Elf64_Shdr)描述,关键字段:

sh_name      节名字符串在 .shstrtab 中的偏移
sh_type      SHT_PROGBITS(代码/数据)/ SHT_RELA(重定位)/ SHT_SYMTAB 等
sh_flags     SHF_ALLOC(需分配内存)/ SHF_EXECINSTR(可执行)/ SHF_WRITE
sh_addr      运行时地址(加载前为 0,加载后填入虚拟地址)
sh_offset    节在文件中的字节偏移
sh_size      节的字节大小
sh_link      关联节索引(符号表 → 字符串表)
sh_addralign 对齐要求
sh_entsize   条目大小(用于符号表、重定位表等固定大小条目的节)

3.4 内存分配:module_alloc 与 vmalloc 区域

layout_and_allocate() 负责计算各内存区域的布局并调用 execmem_alloc()(底层为 vmalloc 区域)分配内存:

内核虚拟地址空间(64位 x86_64):

  ffffffff80000000  ← 内核核心代码(vmlinux .text)
  ...
  ffffffffa0000000  ← 模块区域开始(MODULES_VADDR)
  │
  │  ┌──────────────────┐
  │  │ xt_DSCP .text    │ MOD_TEXT(可执行,只读)
  │  │ xt_DSCP .rodata  │ MOD_RODATA(不可执行,只读)
  │  │ xt_DSCP .data    │ MOD_DATA(读写)
  │  │ xt_DSCP .init.* │ MOD_INIT_TEXT/DATA(临时,用后释放)
  │  └──────────────────┘
  │  ┌──────────────────┐
  │  │ xt_HL .text      │
  │  ...
  │
  ffffffffc0000000  ← 模块区域结束(MODULES_END)

每种 mod_mem_type 都由独立的 module_memory_alloc() 处理(kernel/module/main.c:1312):

// kernel/module/main.c:1312
static int module_memory_alloc(struct module *mod, enum mod_mem_type type)
{
    unsigned int size = PAGE_ALIGN(mod->mem[type].size);
    enum execmem_type execmem_type;
    void *ptr;

    mod->mem[type].size = size;

    // 代码和数据使用不同的分配器
    if (mod_mem_type_is_data(type))
        execmem_type = EXECMEM_MODULE_DATA;
    else
        execmem_type = EXECMEM_MODULE_TEXT;

    ptr = execmem_alloc_rw(execmem_type, size);
    if (!ptr)
        return -ENOMEM;

    mod->mem[type].is_rox = execmem_is_rox(execmem_type);

    if (!mod->mem[type].is_rox)
        kmemleak_not_leak(ptr);

    memset(ptr, 0, size);    // 清零(.bss 依赖此操作)
    mod->mem[type].base = ptr;
    return 0;
}

3.5 符号解析:resolve_symbol() 与 find_symbol()

加载过程中,.symtabst_shndx == SHN_UNDEF 的符号(即外部引用)必须在内核或其他模块中找到对应的实现。simplify_symbols() 遍历所有符号,对未定义的符号调用 resolve_symbol()

find_symbol() 函数在两个层面查找(kernel/module/main.c:388-424):

// kernel/module/main.c:388
bool find_symbol(struct find_symbol_arg *fsa)
{
    static const struct symsearch arr[] = {
        // 内核核心导出的普通符号
        { __start___ksymtab, __stop___ksymtab,
          __start___kcrctab, NOT_GPL_ONLY },
        // 内核核心导出的 GPL-only 符号
        { __start___ksymtab_gpl, __stop___ksymtab_gpl,
          __start___kcrctab_gpl, GPL_ONLY },
    };
    struct module *mod;
    unsigned int i;

    // 先在内核核心符号表中查找
    for (i = 0; i < ARRAY_SIZE(arr); i++)
        if (find_exported_symbol_in_section(&arr[i], NULL, fsa))
            return true;

    // 再在所有已加载模块的符号表中查找
    list_for_each_entry_rcu(mod, &modules, list, ...) {
        struct symsearch arr[] = {
            { mod->syms, mod->syms + mod->num_syms,
              mod->crcs, NOT_GPL_ONLY },
            { mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
              mod->gpl_crcs, GPL_ONLY },
        };
        ...
    }
    return false;
}

find_exported_symbol_in_section() 使用 bsearch() 在排序好的 kernel_symbol 数组中二分查找(kernel/module/main.c:362-382),因此导出符号表必须在构建时排序(由链接脚本保证)。

GPL-only 符号检查在此处进行:如果模块没有 GPL 兼容许可证(即 fsa->gplok == false),则不允许访问 GPL-only 符号(kernel/module/main.c:368):

// kernel/module/main.c:368
if (!fsa->gplok && syms->license == GPL_ONLY)
    return false;

3.6 重定位应用:apply_relocations()

重定位是将符号引用转换为实际内存地址的过程。对于 x86_64,重定位类型主要有:

R_X86_64_PC32    32 位 PC 相对寻址(call/jmp 指令)
R_X86_64_PLT32   PLT 相对(函数调用)
R_X86_64_64      64 位绝对地址
R_X86_64_32S     32 位符号扩展绝对地址

重定位表项(Elf64_Rela)包含:

  • r_offset:需要修改的位置(相对节起始的偏移)
  • r_info:高 32 位为符号表索引,低 32 位为重定位类型
  • r_addend:加数

内核的 apply_relocate_add() 函数(架构特定,如 arch/x86/kernel/module.c)对每条重定位项:

  1. 从符号表找到符号的运行时地址(经过 simplify_symbols() 已填好)
  2. 根据重定位类型计算最终值(如 PC 相对 = 目标地址 - 当前指令地址 - 4)
  3. 将计算结果写入 r_offset 指向的位置

3.7 __init 节的独立映射与释放

do_init_module() 是真正执行 init() 函数并释放 __init 内存的函数(kernel/module/main.c:3017):

// kernel/module/main.c:3017
static noinline int do_init_module(struct module *mod)
{
    struct mod_initfree *freeinit;

    // 记录 init 区域地址,供后续释放
    freeinit->init_text   = mod->mem[MOD_INIT_TEXT].base;
    freeinit->init_data   = mod->mem[MOD_INIT_DATA].base;
    freeinit->init_rodata = mod->mem[MOD_INIT_RODATA].base;

    do_mod_ctors(mod);      // 调用 C++ 构造函数(如有)

    // 执行模块的 init() 函数
    if (mod->init != NULL)
        ret = do_one_initcall(mod->init);

    // init 成功:将模块状态设为 LIVE
    mod->state = MODULE_STATE_LIVE;

    // 通知链:MODULE_STATE_LIVE
    blocking_notifier_call_chain(&module_notify_list,
                                 MODULE_STATE_LIVE, mod);

    // 等待异步探测完成
    if (!mod->async_probe_requested)
        async_synchronize_full();

    // 释放 ftrace 追踪的 init_text 条目
    ftrace_free_mem(mod, mod->mem[MOD_INIT_TEXT].base, ...);

    mutex_lock(&module_mutex);
    module_put(mod);         // 释放初始化期间持有的引用

    // 切换 kallsyms 到只含核心符号的版本
    rcu_assign_pointer(mod->kallsyms, &mod->core_kallsyms);

    // 将 ro_after_init 区域设为只读
    module_enable_rodata_ro_after_init(mod);

    // 从 mod_tree 中删除 init 节点
    mod_tree_remove_init(mod);

    // 异步通过工作队列释放 init 内存
    llist_add(&freeinit->node, &init_free_list);
    schedule_work(&init_free_wq);
}

__init 内存之所以要异步释放(通过工作队列),是因为 do_init_module() 本身就运行在 init() 函数的调用链中,不能在返回之前释放 init_text(否则会释放仍在使用的栈帧)。

do_free_init() 是工作队列处理函数(kernel/module/main.c:2982):

// kernel/module/main.c:2982
static void do_free_init(struct work_struct *w)
{
    struct llist_node *pos, *n, *list;
    struct mod_initfree *initfree;

    list = llist_del_all(&init_free_list);

    // 等待 RCU 宽限期:确保没有读者还在访问 init 区域符号
    synchronize_rcu();

    llist_for_each_safe(pos, n, list) {
        initfree = container_of(pos, struct mod_initfree, node);
        execmem_free(initfree->init_text);
        execmem_free(initfree->init_data);
        execmem_free(initfree->init_rodata);
        kfree(initfree);
    }
}

4. 符号导出与解析机制

4.1 EXPORT_SYMBOL() 宏的展开

EXPORT_SYMBOL()EXPORT_SYMBOL_GPL() 定义在 include/linux/export.h:89-90

// include/linux/export.h:89
#define EXPORT_SYMBOL(sym)      _EXPORT_SYMBOL(sym, "")
#define EXPORT_SYMBOL_GPL(sym)  _EXPORT_SYMBOL(sym, "GPL")

两者最终展开为 ___EXPORT_SYMBOL() 内联汇编(include/linux/export.h:33):

// include/linux/export.h:33
#define ___EXPORT_SYMBOL(sym, license, ns...)    \
    .section ".export_symbol","a"    ASM_NL      \
    __export_symbol_##sym:           ASM_NL      \
        .asciz license               ASM_NL      \
        .ascii ns "\0"               ASM_NL      \
        __EXPORT_SYMBOL_REF(sym)     ASM_NL      \
    .previous

在 C 代码中使用时(include/linux/export.h:75-79):

// include/linux/export.h:75
#define __EXPORT_SYMBOL(sym, license, ns)          \
    extern typeof(sym) sym;                        \
    __ADDRESSABLE(sym)                             \
    __GENDWARFKSYMS_EXPORT(sym)                    \
    asm(__stringify(___EXPORT_SYMBOL(sym, license, ns)))

这会在 .export_symbol 节中生成一个条目,包含:许可证字符串 + 命名空间字符串 + 符号地址引用。构建工具链(scripts/mod/modpost.c)在链接期间将 .export_symbol 处理成最终的 __ksymtab__ksymtab_strings

4.2 __ksymtab 与 __ksymtab_strings 节

最终生成的内核符号表布局如下:

__ksymtab(kernel_symbol 数组,按符号名排序):
  ┌─────────────────────────────────────────┐
  │ struct kernel_symbol {                  │
  │   int   value_offset;  // 相对偏移→符号地址 │
  │   int   name_offset;   // 相对偏移→符号名    │
  │   int   namespace_offset; // 命名空间         │
  │ }                                       │
  ├─────────────────────────────────────────┤
  │ struct kernel_symbol { ... }            │  (排序后)
  │ ...                                     │
  └─────────────────────────────────────────┘

__ksymtab_strings(符号名字符串池):
  "alloc_pages\0blk_mq_init_queue\0copy_from_user\0..."

CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 开启时使用 PC 相对 32 位偏移(而非绝对地址),这样符号表本身可以放在只读节中,减少运行时重定位需求。

内核核心符号表的边界通过链接器符号暴露(kernel/module/main.c:390-396):

// kernel/module/main.c:390
static const struct symsearch arr[] = {
    { __start___ksymtab, __stop___ksymtab,
      __start___kcrctab, NOT_GPL_ONLY },
    { __start___ksymtab_gpl, __stop___ksymtab_gpl,
      __start___kcrctab_gpl, GPL_ONLY },
};

4.3 EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL 的本质差异

                EXPORT_SYMBOL          EXPORT_SYMBOL_GPL
              +--------------------+  +--------------------+
  许可证标记  | license=""(空)    |  | license="GPL"      |
  查找许可    | NOT_GPL_ONLY        |  | GPL_ONLY           |
  可用模块    | 任何模块            |  | 仅 GPL 兼容许可模块  |
  拒绝条件    | 无                  |  | !fsa->gplok 时拒绝  |
  典型用途    | 通用 API            |  | 内核内部接口        |
              +--------------------+  +--------------------+

gplok 标志在 resolve_symbol() 时由模块许可证决定。许可证字符串解析在 kernel/module/main.c 中通过 license_is_gpl_compatible() 函数完成,判断 .modinfolicense= 字段是否包含 "GPL" 变体。

4.4 resolve_symbol() 详细逻辑

resolve_symbol() 是符号解析的核心函数(kernel/module/main.c:1231),其执行顺序如下:

// kernel/module/main.c:1231
static const struct kernel_symbol *resolve_symbol(struct module *mod,
                          const struct load_info *info,
                          const char *name,
                          char ownername[])
{
    struct find_symbol_arg fsa = {
        .name  = name,
        // GPL 检查:专有模块不允许使用 GPL-only 符号
        .gplok = !(mod->taints & (1 << TAINT_PROPRIETARY_MODULE)),
        .warn  = true,
    };

    mutex_lock(&module_mutex);
    if (!find_symbol(&fsa))
        goto unlock;

    // 记录是否使用了 GPL-only 符号(供后续报告)
    if (fsa.license == GPL_ONLY)
        mod->using_gplonly_symbols = true;

    // 污点继承:被依赖的模块如果 tainted,依赖方也继承污点
    if (!inherit_taint(mod, fsa.owner, name)) {
        fsa.sym = NULL;
        goto getname;
    }

    // CRC 版本检查(CONFIG_MODVERSIONS)
    if (!check_version(info, name, mod, fsa.crc)) {
        fsa.sym = ERR_PTR(-EINVAL);
        goto getname;
    }

    // 命名空间检查
    err = verify_namespace_is_imported(info, fsa.sym, mod);

    // 建立模块依赖关系(增加被依赖模块的引用计数)
    err = ref_module(mod, fsa.owner);
    ...
}

resolve_symbol_wait()kernel/module/main.c:1287)在此基础上增加了等待机制:若目标符号所在模块正在初始化(MODULE_STATE_COMING),则等待最多 30 秒:

// kernel/module/main.c:1294
if (wait_event_interruptible_timeout(module_wq,
        !IS_ERR(ksym = resolve_symbol(mod, info, name, owner))
        || PTR_ERR(ksym) != -EBUSY,
                     30 * HZ) <= 0) {
    pr_warn("%s: gave up waiting for init of module %s.\n",
        mod->name, owner);
}

4.5 kallsyms_lookup_name() 动态查找

kallsyms_lookup_name() 提供了在运行时按名称查找内核符号地址的能力:

kallsyms_lookup_name("schedule")
    │
    ├─ 在 kallsyms_names 表中二分查找
    │   (该表在编译时由 scripts/kallsyms.c 生成)
    │
    └─ 返回符号的运行时地址(unsigned long)

注意:自 Linux 5.7 起,kallsyms_lookup_name() 不再对内核外部模块暴露(未被 EXPORT_SYMBOL),模块必须使用 __symbol_get() 或其他接口。

4.6 MODVERSIONS 与 CRC 校验

开启 CONFIG_MODVERSIONS 时,每个导出符号都有一个 32 位 CRC,在构建时由 genksyms 工具根据符号的类型签名计算(kernel/module/main.c:331-335):

// kernel/module/main.c:331
#ifndef CONFIG_MODVERSIONS
#define symversion(base, idx) NULL
#else
#define symversion(base, idx) ((base != NULL) ? ((base) + (idx)) : NULL)
#endif

CRC 不匹配时,insmod 默认拒绝加载(MODULE_INIT_IGNORE_MODVERSIONS 标志可以跳过此检查,但会导致内核 taint)。


5. 模块参数系统

5.1 module_param() 宏展开链

module_param() 是模块参数的主要入口宏,定义在 include/linux/moduleparam.h:139-140

// include/linux/moduleparam.h:139
#define module_param(name, type, perm)   \
    module_param_named(name, name, type, perm)

展开链:

module_param(debug, int, 0644)
    ↓
module_param_named(debug, debug, int, 0644)
    ↓  include/linux/moduleparam.h:162
param_check_int(debug, &debug);               // 类型安全检查
module_param_cb(debug, &param_ops_int, &debug, 0644);
__MODULE_PARM_TYPE(debug, "int");             // 写入 .modinfo
    ↓  include/linux/moduleparam.h:188
__module_param_call(MODULE_PARAM_PREFIX, debug, &param_ops_int, &debug, 0644, -1, 0)
    ↓  include/linux/moduleparam.h:299
// 生成静态变量,放入 __param 节:
static const char __param_str_debug[] = "debug";
static struct kernel_param const __param_debug
    __used __section("__param")
    __aligned(__alignof__(struct kernel_param))
    = { "debug", THIS_MODULE, &param_ops_int,
        0644, -1, 0, { &debug } }

5.2 struct kernel_param 结构体

struct kernel_param 是每个模块参数的描述符(include/linux/moduleparam.h:82-94):

// include/linux/moduleparam.h:82
struct kernel_param {
    const char *name;                    // 参数名(如 "debug")
    struct module *mod;                  // 所属模块
    const struct kernel_param_ops *ops;  // set/get 操作集
    const u16 perm;                      // sysfs 权限(如 0644)
    s8 level;                            // initcall 级别(-1 = 任意时机)
    u8 flags;                            // KERNEL_PARAM_FL_UNSAFE 等
    union {
        void *arg;                       // 变量地址(普通类型)
        const struct kparam_string *str; // 字符串参数
        const struct kparam_array *arr;  // 数组参数
    };
};

struct kernel_param_ops 定义了参数的操作接口(include/linux/moduleparam.h:60-69):

// include/linux/moduleparam.h:60
struct kernel_param_ops {
    unsigned int flags;           // KERNEL_PARAM_OPS_FL_NOARG
    int (*set)(const char *val, const struct kernel_param *kp); // 写参数
    int (*get)(char *buffer, const struct kernel_param *kp);    // 读参数
    void (*free)(void *arg);      // 模块卸载时释放(用于 charp)
};

5.3 类型安全机制

param_check_##type 宏在编译期做类型检查。以 int 类型为例:

// 内核预定义(概念等效)
#define param_check_int(name, p) __param_check(name, p, int)
#define __param_check(name, p, type) \
    static inline type *__check_##name(void) { return(p); }

这通过返回类型不兼容在编译时产生错误,防止开发者传入错误类型的变量地址。

支持的内置类型(include/linux/moduleparam.h:130-137):

byte, hexint, short, ushort, int, uint, long, ulong
charp    字符指针(内核自动 kstrdup/kfree)
bool     布尔值(0/1, y/n, Y/N)
invbool  反向布尔(N=true)

数组参数通过 module_param_array() 宏支持,底层使用 struct kparam_array

// include/linux/moduleparam.h:105
struct kparam_array {
    unsigned int max;          // 最大元素数
    unsigned int elemsize;     // 单个元素大小
    unsigned int *num;         // 实际元素数指针
    const struct kernel_param_ops *ops; // 元素操作集
    void *elem;                // 数组基地址
};

5.4 /sys/module//parameters/ sysfs 接口

模块加载后,mod_sysfs_setup() 在 sysfs 下创建:

/sys/module/<modname>/
├── parameters/          ← 各模块参数
│   ├── debug            ← 对应 module_param(debug, ...)
│   ├── timeout
│   └── ...
├── refcnt               ← 引用计数
├── initstate            ← live / coming / going
├── srcversion           ← 源码 MD5
└── sections/            ← ELF 节地址(需 CAP_SYS_ADMIN)
    ├── .text
    ├── .data
    └── ...

读取参数值时,sysfs 调用 kernel_param_ops->get();写入时调用 set()。写操作受 param_lock 互斥锁保护(include/linux/module.h:430)。

如果参数 perm == 0,则不在 sysfs 中创建对应文件(仅支持内核命令行设置)。

5.5 命令行参数传递

模块加载时,insmod module.ko param1=value1 param2=value2 中的参数字符串经由 load_module() 传入,最终由 parse_args() 解析(kernel/module/main.c:3485):

// kernel/module/main.c:3485
after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp,
                          -32768, 32767, mod,
                          unknown_module_param_cb);

parse_args() 逐个解析 key=value 对,通过名称匹配找到对应的 kernel_param,然后调用其 ops->set() 函数设置变量值。


6. 模块卸载机制

6.1 delete_module() 系统调用

模块卸载由 SYSCALL_DEFINE2(delete_module, ...) 处理,定义在 kernel/module/main.c:776

// kernel/module/main.c:776
SYSCALL_DEFINE2(delete_module, const char __user *, name_user,
        unsigned int, flags)
{
    struct module *mod;
    char name[MODULE_NAME_LEN];
    int ret, forced = 0;

    // 1. 权限检查:需要 CAP_SYS_MODULE
    if (!capable(CAP_SYS_MODULE) || modules_disabled)
        return -EPERM;

    mutex_lock(&module_mutex);

    // 2. 在 modules 链表中查找
    mod = find_module(name);

    // 3. 检查是否有其他模块依赖本模块
    if (!list_empty(&mod->source_list)) {
        ret = -EWOULDBLOCK;
        goto out;
    }

    // 4. 检查模块状态
    if (mod->state != MODULE_STATE_LIVE) {
        ret = -EBUSY;
        goto out;
    }

    // 5. 检查 init 有但 exit 无的情况
    if (mod->init && !mod->exit) {
        forced = try_force_unload(flags);
        if (!forced) {
            ret = -EBUSY;
            goto out;
        }
    }

    // 6. 尝试停止模块(减引用计数)
    ret = try_stop_module(mod, flags, &forced);

    mutex_unlock(&module_mutex);

    // 7. 调用 exit() 函数
    if (mod->exit != NULL)
        mod->exit();

    // 8. 通知链
    blocking_notifier_call_chain(&module_notify_list,
                                 MODULE_STATE_GOING, mod);

    // 9. 释放模块资源
    free_module(mod);
}

6.2 引用计数机制

模块引用计数通过 atomic_t refcnt 字段(include/linux/module.h:569)和三个函数管理:

// kernel/module/main.c:907
bool try_module_get(struct module *module)
{
    bool ret = true;
    if (module) {
        if (likely(module_is_live(module) &&
                   atomic_inc_not_zero(&module->refcnt) != 0))
            trace_module_get(module, _RET_IP_);
        else
            ret = false;  // 模块正在卸载,获取失败
    }
    return ret;
}
EXPORT_SYMBOL(try_module_get);

// kernel/module/main.c:923
void module_put(struct module *module)
{
    if (module) {
        ret = atomic_dec_if_positive(&module->refcnt);
        WARN_ON(ret < 0);
        trace_module_put(module, _RET_IP_);
    }
}
EXPORT_SYMBOL(module_put);

引用计数使用规则:

try_module_get(mod)   ← 使用模块前调用(失败说明模块正在卸载)
    │
    ↓ 使用模块功能...
    │
module_put(mod)       ← 使用完毕后调用

MODULE_REF_BASE(值为 1)是模块加载时的基础引用计数。卸载时先减去 MODULE_REF_BASE,如果结果为 0 说明没有其他用户在使用(kernel/module/main.c:730):

// kernel/module/main.c:730
static int try_release_module_ref(struct module *mod)
{
    int ret;
    ret = atomic_sub_return(MODULE_REF_BASE, &mod->refcnt);
    BUG_ON(ret < 0);
    if (ret)
        ret = atomic_add_unless(&mod->refcnt, MODULE_REF_BASE, 0);
    return ret;
}

6.3 依赖关系管理

模块间依赖通过双向链表维护(kernel/module/main.c:660-674):

模块 A(依赖方)              模块 B(被依赖方)
┌──────────────┐            ┌──────────────┐
│ target_list  │←──────────→│ source_list  │
│  (我依赖谁)   │  module_use │  (谁依赖我)  │
└──────────────┘            └──────────────┘

struct module_use {
    struct list_head source_list;  // 挂在 B->source_list
    struct list_head target_list;  // 挂在 A->target_list
    struct module *source;         // = A
    struct module *target;         // = B
};

当模块 A 首次引用模块 B 的符号时,ref_module(A, B) 被调用,创建 module_use 节点并插入两侧链表。卸载 A 时,module_unload_free() 遍历 A->target_list,对每个目标模块调用 module_put()

尝试卸载 B 时,如果 B->source_list 非空(说明仍有模块依赖 B),直接返回 -EWOULDBLOCK,用户会看到 rmmod: ERROR: Module b is in use by: a

6.4 module_exit() 宏展开

module_exit() 宏将清理函数注册为 cleanup_module 的别名(include/linux/module.h:139-144):

// include/linux/module.h:139(MODULE 宏定义版本)
#define module_exit(exitfn)                               \
    static inline exitcall_t __maybe_unused __exittest(void) \
    { return exitfn; }                                    \
    void cleanup_module(void) __copy(exitfn)              \
        __attribute__((alias(#exitfn)));                  \
    ___ADDRESSABLE(cleanup_module, __exitdata);

cleanup_module 是传统的 ABI 接口名,内核通过 mod->exit 函数指针调用,该指针在 find_module_sections() 时通过查找 cleanup_module 符号地址来初始化。

__exit 标记的函数(include/linux/init.h:79):

// include/linux/init.h:79
#define __exit  __section(".exit.text") __exitused __cold notrace

如果模块不支持卸载(未定义 module_exit()),则 mod->exit 为 NULL,此时 delete_module() 会在 mod->init && !mod->exit 处返回 -EBUSY

6.5 卸载保护:正在使用中的模块

当有代码正在执行模块内的函数时(引用计数 > MODULE_REF_BASE),卸载会被拒绝。但有两种绕过机制:

  1. 强制卸载CONFIG_MODULE_FORCE_UNLOAD):使用 rmmod -f 传入 O_TRUNC 标志,try_force_unload() 强行将模块状态设为 GOING,并给内核打 TAINT_FORCED_RMMOD 污点标志(kernel/module/main.c:714-720)。

  2. __module_put_and_kthread_exit():工作在内核线程中运行模块代码时,线程退出前调用此函数同时释放引用并退出线程(kernel/module/main.c:228-232):

// kernel/module/main.c:228
void __noreturn __module_put_and_kthread_exit(struct module *mod, long code)
{
    module_put(mod);
    kthread_exit(code);
}
EXPORT_SYMBOL(__module_put_and_kthread_exit);

7. 模块签名验证

7.1 CONFIG_MODULE_SIG 编译选项

内核提供三个签名相关配置选项:

CONFIG_MODULE_SIG         启用签名检查基础设施(但不强制要求签名)
CONFIG_MODULE_SIG_FORCE   强制要求所有模块必须有有效签名
CONFIG_MODULE_SIG_SHA256  使用 SHA-256 摘要算法(可选 SHA-384/512)

签名强制执行状态在运行时通过 sig_enforce 变量控制(kernel/module/signing.c:22-23):

// kernel/module/signing.c:22
static bool sig_enforce = IS_ENABLED(CONFIG_MODULE_SIG_FORCE);
module_param(sig_enforce, bool_enable_only, 0644);

bool_enable_only 类型确保该参数只能从 false 改为 true,不可逆。这允许系统运行时动态开启强制签名模式(如进入 lockdown 状态时)。

7.2 .ko 文件末尾的签名结构

签名附加在 .ko 文件末尾,格式如下:

.ko 文件布局(带签名):

  ┌──────────────────────────────────┐
  │  ELF 内容(原始 .ko 数据)         │  ← modlen 字节
  ├──────────────────────────────────┤
  │  PKCS#7 签名数据(DER 编码)       │  ← sig_len 字节
  ├──────────────────────────────────┤
  │  struct module_signature (16B)   │  ← 固定结尾
  │    algo: 签名算法                 │
  │    hash: 哈希类型                 │
  │    id_type: PKEY_ID_PKCS7        │
  │    signer_len: 签名者名称长度     │
  │    key_id_len: 密钥 ID 长度       │
  │    sig_len: PKCS#7 数据长度(BE)  │
  ├──────────────────────────────────┤
  │  "~Module signature appended~\n" │  ← MODULE_SIG_STRING 标记
  └──────────────────────────────────┘

7.3 mod_verify_sig() 流程

签名验证核心函数(kernel/module/signing.c:43-68):

// kernel/module/signing.c:43
int mod_verify_sig(const void *mod, struct load_info *info)
{
    struct module_signature ms;
    size_t sig_len, modlen = info->len;

    // 从文件末尾读取 module_signature 结构
    memcpy(&ms, mod + (modlen - sizeof(ms)), sizeof(ms));

    // 检查结构合法性(算法、标志等)
    ret = mod_check_sig(&ms, modlen, "module");
    if (ret)
        return ret;

    // 计算实际签名数据长度和 ELF 内容长度
    sig_len = be32_to_cpu(ms.sig_len);
    modlen -= sig_len + sizeof(ms);
    info->len = modlen;  // 告知调用者实际 ELF 长度

    // 通过 PKCS#7 验证签名
    return verify_pkcs7_signature(mod, modlen,
                                  mod + modlen, sig_len,
                                  VERIFY_USE_SECONDARY_KEYRING,
                                  VERIFYING_MODULE_SIGNATURE,
                                  NULL, NULL);
}

7.4 module_sig_check() 的完整决策树

module_sig_check() 处理各种签名状态(kernel/module/signing.c:70-125):

module_sig_check(info, flags)
│
├─ 1. 检查文件末尾是否有 MODULE_SIG_STRING 标记
│      │
│      ├─ 有标记 → 调用 mod_verify_sig()
│      │           │
│      │           ├─ 验证成功:info->sig_ok=true,返回 0
│      │           └─ 验证失败:err = -EKEYREJECTED 等致命错误
│      │
│      └─ 无标记 → err = -ENODATA("unsigned module")
│
├─ 2. 处理非致命错误(-ENODATA / -ENOPKG / -ENOKEY):
│      │
│      ├─ is_module_sig_enforced()?
│      │    ├─ 是:pr_notice("rejected") → 返回 -EKEYREJECTED
│      │    └─ 否:调用 security_locked_down()
│      │              └─ lockdown 模式下也会拒绝
│      │
│      └─ 非 lockdown、非强制:允许加载(但 sig_ok=false)
│
└─ 3. 致命错误(内存不足、签名格式错误等):直接返回错误码

7.5 内置证书与密钥环

签名验证使用的公钥来自内核内置的证书(VERIFY_USE_SECONDARY_KEYRING):

内核密钥环体系:

  .builtin_trusted_keys(内置信任密钥环)
      │ CONFIG_MODULE_SIG_KEY 指定的密钥(构建时嵌入)
      │ CONFIG_SYSTEM_TRUSTED_KEYS 指定的额外证书
      │
  .secondary_trusted_keys(次级信任密钥环)
      │ 可以在运行时添加(需 CAP_SYS_ADMIN)
      │
  模块签名验证使用 VERIFY_USE_SECONDARY_KEYRING,
  即在两个密钥环中都查找匹配的证书

scripts/sign-file 工具使用 CONFIG_MODULE_SIG_KEY 指定的私钥(默认 certs/signing_key.pem,构建时自动生成)对模块进行签名,生成的公钥证书被编译进内核的 .builtin_trusted_keys 密钥环。


8. initcall 机制与优先级体系

8.1 initcall 的设计目标

内核启动需要按照严格的依赖顺序初始化各个子系统:先初始化总线控制器,再初始化挂在总线上的设备;先初始化文件系统,再挂载根文件系统。initcall 机制通过编译时分类、链接时排序来实现这一需求,无需手动维护复杂的初始化调用图。

8.2 8 个优先级级别

内核定义了 8 个(加上 early 和 rootfs,共 10 个)初始化优先级(include/linux/init.h:284-316):

// include/linux/init.h:284
#define early_initcall(fn)         __define_initcall(fn, early)

// 级别 0(pure):纯粹的变量初始化,无依赖
#define pure_initcall(fn)          __define_initcall(fn, 0)

// 级别 1(core):核心子系统(kmem_cache、workqueue等)
#define core_initcall(fn)          __define_initcall(fn, 1)
#define core_initcall_sync(fn)     __define_initcall(fn, 1s)

// 级别 2(postcore):总线子系统初始化后
#define postcore_initcall(fn)      __define_initcall(fn, 2)

// 级别 3(arch):体系结构相关初始化
#define arch_initcall(fn)          __define_initcall(fn, 3)

// 级别 4(subsys):子系统初始化(PCI、USB等总线)
#define subsys_initcall(fn)        __define_initcall(fn, 4)

// 级别 5(fs):文件系统注册
#define fs_initcall(fn)            __define_initcall(fn, 5)
#define rootfs_initcall(fn)        __define_initcall(fn, rootfs)

// 级别 6(device):设备驱动初始化(最常用)
#define device_initcall(fn)        __define_initcall(fn, 6)

// 级别 7(late):延迟初始化(系统基本就绪后)
#define late_initcall(fn)          __define_initcall(fn, 7)

// __initcall 等价于 device_initcall(级别 6)
#define __initcall(fn) device_initcall(fn)

include/linux/init.h:152-161 中定义了各级别的边界符号:

// include/linux/init.h:152
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];

这些符号在链接脚本(include/asm-generic/vmlinux.lds.h)中定义,将各 .initcallN.init 节聚合并标记边界。

8.3 __define_initcall 宏展开

subsys_initcall(pci_driver_init) 为例展开(非 LTO、非 PREL32 架构,include/linux/init.h:263-265):

// include/linux/init.h:263
#define ____define_initcall(fn, __unused, __name, __sec)    \
    static initcall_t __name __used                         \
        __attribute__((__section__(__sec))) = fn;

// 展开结果:
static initcall_t __initcall_pci_driver_init4
    __used
    __attribute__((__section__(".initcall4.init")))
    = pci_driver_init;

LTO 场景下,include/linux/init.h:234-235 需要生成全局 stub 函数:

// include/linux/init.h:237
#define __define_initcall_stub(__stub, fn)            \
    int __init __stub(void);                          \
    int __init __stub(void)                           \
    {                                                 \
        return fn();                                  \
    }                                                 \
    __ADDRESSABLE(__stub)

对于支持 CONFIG_HAVE_ARCH_PREL32_RELOCATIONS 的架构(如 ARM64),使用 PC 相对 32 位偏移(include/linux/init.h:255-261):

// include/linux/init.h:255(PREL32 版本)
#define ____define_initcall(fn, __stub, __name, __sec)    \
    __define_initcall_stub(__stub, fn)                    \
    asm(".section \"" __sec "\", \"a\"        \n"         \
        __stringify(__name) ":                \n"         \
        ".long " __stringify(__stub) " - .    \n"         \
        ".previous                            \n");

8.4 do_initcalls() 遍历机制

内核启动时(init/main.c 中的 kernel_init_freeable())调用 do_initcalls()

// init/main.c(概念)
static initcall_level_t initcall_levels[] = {
    __initcall0_start,  // pure
    __initcall1_start,  // core
    __initcall2_start,  // postcore
    __initcall3_start,  // arch
    __initcall4_start,  // subsys
    __initcall5_start,  // fs
    __initcall6_start,  // device(最多)
    __initcall7_start,  // late
    __initcall_end,
};

static void do_initcalls(void)
{
    int level;
    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level, command_line);
}

do_one_initcall() 是执行单个 initcall 的函数(include/linux/init.h:136),它处理返回值检查、调试打印(CONFIG_INIT_CALL_DEBUG)和异常捕获。

8.5 module_init() 与 __initcall() 的关系

关键区别:module_init() 的行为随编译方式不同而不同(include/linux/module.h:80-145):

module_init(fn)
    │
    ├─ 静态编译(!MODULE):等价于 __initcall(fn)
    │   即 device_initcall(fn),放入 .initcall6.init 节
    │   在内核启动时由 do_initcalls() 调用
    │   include/linux/module.h:89:
    │   #define module_init(x)  __initcall(x);
    │
    └─ 模块化(MODULE):
        生成 init_module 别名(指向 fn)
        在 insmod 时由 do_init_module() 中的
        do_one_initcall(mod->init) 调用
        include/linux/module.h:131:
        #define module_init(initfn)                     \
            static inline initcall_t __inittest(void)   \
            { return initfn; }                          \
            int init_module(void) __copy(initfn)        \
                __attribute__((alias(#initfn)));

include/linux/module.h:112-126 展示了当以模块形式编译时,early_initcall()late_initcall() 等所有级别宏都等价于 module_init()

// include/linux/module.h:112
#define early_initcall(fn)       module_init(fn)
#define core_initcall(fn)        module_init(fn)
#define postcore_initcall(fn)    module_init(fn)
#define arch_initcall(fn)        module_init(fn)
#define subsys_initcall(fn)      module_init(fn)
#define fs_initcall(fn)          module_init(fn)
#define device_initcall(fn)      module_init(fn)
#define late_initcall(fn)        module_init(fn)

9. 模块内存保护

9.1 内存权限分区

现代内核实现了严格的模块内存权限分区,防止代码注入和数据执行攻击(CONFIG_STRICT_MODULE_RWX):

模块内存区域权限矩阵:

  内存类型            读    写    执行   对应 mod_mem_type
  ─────────────────────────────────────────────────────
  MOD_TEXT           ✓     ✗     ✓      代码段(机器指令)
  MOD_RODATA         ✓     ✗     ✗      只读数据
  MOD_RO_AFTER_INIT  ✓    ✗→✓    ✗     init 后变为只读
  MOD_DATA           ✓     ✓     ✗      读写数据
  MOD_INIT_TEXT      ✓     ✗     ✓      __init 代码(临时)
  MOD_INIT_DATA      ✓     ✓     ✗      __initdata(临时)
  MOD_INIT_RODATA    ✓     ✗     ✗      __initconst(临时)

权限设置通过 set_memory_ro()set_memory_nx() 等函数实现。module_memory 结构体中的 is_rox 字段标记该区域是否已经处于只读+可执行状态(include/linux/module.h:357-364):

// include/linux/module.h:356
struct module_memory {
    void *base;
    bool is_rox;        // read-only + executable
    unsigned int size;
#ifdef CONFIG_MODULES_TREE_LOOKUP
    struct mod_tree_node mtn;
#endif
};

9.2 RODATA 节只读映射

complete_formation() 调用 module_enable_rodata_ro()MOD_RODATA 节设置为只读,调用 module_enable_nx() 禁止 MOD_DATA 的执行权限。这些操作在模块加载完成、符号重定位全部完成之后进行——重定位期间需要临时的写权限。

do_init_module() 完成后,进一步调用 module_enable_rodata_ro_after_init() 处理 MOD_RO_AFTER_INIT 区域(kernel/module/main.c:3087)。

9.3 jump_label 静态 Key 优化

jump_label(又名静态 key)是内核中一个精妙的性能优化机制,与模块系统深度集成。

// 典型用法(内核追踪点开关):
DEFINE_STATIC_KEY_FALSE(tracepoint_enabled);

if (static_branch_unlikely(&tracepoint_enabled)) {
    do_expensive_trace_work();
}

tracepoint_enabled 为 false 时,if 条件被编译为一条 nop 指令(5 字节,x86_64),不产生任何分支预测开销。当状态改变时(static_branch_enable()),内核通过 text_poke_bp() 动态修改该指令为 jmp,实现零开销的条件检查。

模块加载时,jump_entry_update_module() 负责初始化模块内所有 jump_label 条目(struct module 中的 jump_entries 字段,include/linux/module.h:512-514):

// include/linux/module.h:511
#ifdef CONFIG_JUMP_LABEL
    struct jump_entry *jump_entries;
    unsigned int num_jump_entries;
#endif

模块卸载时,必须撤销所有 jump_label 的修改,否则内核代码区会残留指向已卸载模块的 jmp 指令,导致内核崩溃。

9.4 模块地址空间随机化(KASLR)

开启 CONFIG_RANDOMIZE_BASE(KASLR)时,模块加载地址也会随机化(CONFIG_RANDOMIZE_MODULE_REGION_FULL)。module_alloc() 使用 get_random_u32() 在允许范围内随机选择分配起始地址。

struct module 结构体本身也用 __randomize_layout 标记(include/linux/module.h:585),在开启 CONFIG_RANDSTRUCT 时字段顺序随机化,防止攻击者通过已知偏移访问关键字段。

9.5 CFI(Control Flow Integrity)

开启 CONFIG_CFI_CLANG 时,模块的函数指针调用会被 CFI 保护。struct module 中的 kcfi_traps 字段记录 CFI 陷阱表(include/linux/module.h:424-426):

// include/linux/module.h:423
#ifdef CONFIG_ARCH_USES_CFI_TRAPS
    s32 *kcfi_traps;
    s32 *kcfi_traps_end;
#endif

CFI 为每个间接调用目标(函数指针目标)生成类型签名校验,如果调用目标类型不匹配(如利用漏洞劫持函数指针后调用了错误类型的函数),内核会主动崩溃而不是执行攻击者控制的代码。


10. 常见模块开发模式

10.1 最简模块骨架

#include <linux/module.h>
#include <linux/init.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author <email>");
MODULE_DESCRIPTION("Example module");

static int __init my_init(void)
{
    pr_info("my_module: loaded\n");
    return 0;
}

static void __exit my_exit(void)
{
    pr_info("my_module: unloaded\n");
}

module_init(my_init);
module_exit(my_exit);

关键点

  • __init 标记 my_init:模块初始化后,该函数占用的内存会被释放
  • __exit 标记 my_exit:若模块静态编译,该函数永不被调用,链接器可以丢弃
  • module_init() / module_exit() 展开为 init_module / cleanup_module 别名

10.2 platform_driver 模块

platform_driver 是最常用的设备驱动框架,通过 module_platform_driver() 宏简化注册:

#include <linux/module.h>
#include <linux/platform_device.h>

static int my_driver_probe(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "probe called\n");
    return 0;
}

static void my_driver_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove called\n");
}

static const struct of_device_id my_driver_dt_ids[] = {
    { .compatible = "vendor,my-device" },
    { }
};
MODULE_DEVICE_TABLE(of, my_driver_dt_ids);

static struct platform_driver my_driver = {
    .probe  = my_driver_probe,
    .remove = my_driver_remove,
    .driver = {
        .name           = "my-device",
        .of_match_table = my_driver_dt_ids,
    },
};

module_platform_driver(my_driver);
MODULE_LICENSE("GPL");

module_platform_driver(drv) 展开为:

#define module_platform_driver(__platform_driver)   \
    module_driver(__platform_driver,                \
                  platform_driver_register,          \
                  platform_driver_unregister)

最终等效于在 init 中调用 platform_driver_register(),在 exit 中调用 platform_driver_unregister()

10.3 字符设备模块

#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>

static dev_t dev_num;
static struct cdev my_cdev;

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,   // 引用计数保护
    .read    = my_read,
    .write   = my_write,
    .open    = my_open,
    .release = my_release,
};

static int __init my_init(void)
{
    int ret;

    ret = alloc_chrdev_region(&dev_num, 0, 1, "my_device");
    if (ret < 0)
        return ret;

    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;

    ret = cdev_add(&my_cdev, dev_num, 1);
    if (ret) {
        unregister_chrdev_region(dev_num, 1);
        return ret;
    }
    return 0;
}

static void __exit my_exit(void)
{
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

.owner = THIS_MODULE 是防止卸载竞争的关键——VFS 在调用 fops 中的函数之前会调用 try_module_get(file->f_op->owner),使引用计数增加,确保模块在 fops 函数执行期间不被卸载。

10.4 netfilter 模块(以当前工作区 xt_DSCP 为例)

当前工作区中的 net/netfilter/xt_DSCP.c 是一个典型的 netfilter 匹配器模块,展示了完整的模块开发模式(net/netfilter/xt_DSCP.c:64-110):

// net/netfilter/xt_DSCP.c:64
static struct xt_match dscp_mt_reg[] __read_mostly = {
    {
        .name      = "dscp",
        .family    = NFPROTO_IPV4,
        .checkentry = dscp_mt_check,
        .match     = dscp_mt,
        .matchsize = sizeof(struct xt_dscp_info),
        .me        = THIS_MODULE,   // 持有模块引用
    },
    {
        .name      = "dscp",
        .family    = NFPROTO_IPV6,
        .checkentry = dscp_mt_check,
        .match     = dscp_mt6,
        .matchsize = sizeof(struct xt_dscp_info),
        .me        = THIS_MODULE,
    },
    // ... tos 匹配器
};

static int __init dscp_mt_init(void)
{
    return xt_register_matches(dscp_mt_reg, ARRAY_SIZE(dscp_mt_reg));
}

static void __exit dscp_mt_exit(void)
{
    xt_unregister_matches(dscp_mt_reg, ARRAY_SIZE(dscp_mt_reg));
}

module_init(dscp_mt_init);
module_exit(dscp_mt_exit);

__read_mostly 属性(展开为 __section(".data..read_mostly"))将该数组放入只读优化的数据节,这些数据主要被读取,很少修改,可以被多个 CPU 缓存在同一缓存行而不产生伪共享问题。

.me = THIS_MODULE 在每个 xt_match 中保存模块指针,当 netfilter 子系统使用这个匹配器时会调用 try_module_get(match->me),防止在匹配规则仍被引用时卸载模块。

其余工作区中修改的 netfilter 模块(xt_HL.cxt_RATEEST.cxt_TCPMSS.c)同样遵循这一模式:通过 xt_register_target() / xt_register_match() 注册,通过 .me = THIS_MODULE 管理引用计数。


11. 符号解析详细流程剖析

11.1 并发加载的等待机制

resolve_symbol_wait()kernel/module/main.c:1287)解决了并发加载场景:

场景:modprobe 同时加载模块 A 和 B,A 依赖 B 的符号

  CPU 0                           CPU 1
  ──────                          ──────
  load_module(A)                  load_module(B)
  simplify_symbols(A)             add_unformed_module(B)
  resolve_symbol("B::foo")           → B 状态 = UNFORMED
      │
      ├─ find_symbol() 找到 B::foo
      │   但 B->state == MODULE_STATE_UNFORMED
      │   → 返回 -EBUSY
      │
      └─ wait_event_interruptible_timeout(module_wq, ...)
              等待最多 30 秒...
                                  complete_formation(B)
                                      → B 状态 = COMING
                                      → wake_up_all(&module_wq)
              │
              ├─ 重新调用 resolve_symbol("B::foo")
              │   B 状态 = COMING → strong_try_module_get(B) 返回 -EBUSY
              └─ 继续等待...
                                  do_init_module(B)
                                      → B 状态 = LIVE
                                      → wake_up_all(&module_wq)
              │
              └─ resolve_symbol("B::foo") 成功

11.2 模块污点(Taint)传播

mod->taints 字段记录模块的污点标志(include/linux/module.h:460)。当模块被加载时,可能触发以下污点:

TAINT_PROPRIETARY_MODULE  (P) - 加载了非 GPL 许可证模块
TAINT_FORCED_MODULE       (F) - insmod -f 强制加载版本不匹配的模块
TAINT_OUT_OF_TREE         (O) - 加载了非内核树中的模块
TAINT_UNSIGNED_MODULE     (E) - 加载了未签名模块
TAINT_LIVEPATCH           (K) - 加载了 livepatch 模块
TAINT_FORCED_RMMOD        (R) - 强制卸载了模块

污点通过 inherit_taint() 传播:如果模块 A 依赖一个带有 TAINT_PROPRIETARY_MODULE 的模块 B,则 A 也会继承这个污点,相应地 A 就不能再访问内核的 GPL-only 符号(kernel/module/main.c:1256)。


12. 模块内核符号命名空间

12.1 命名空间的引入背景

Linux 5.4 引入了内核符号命名空间。在此之前,EXPORT_SYMBOL_GPL() 的粒度太粗——任何 GPL 兼容模块都可以使用所有 GPL-only 符号,没有进一步的访问控制。命名空间提供了更细粒度的符号可见性控制。

12.2 命名空间导出与导入

导出带命名空间的符号使用 EXPORT_SYMBOL_NS_GPL()include/linux/export.h:92):

// include/linux/export.h:91
#define EXPORT_SYMBOL_NS(sym, ns)     __EXPORT_SYMBOL(sym, "", ns)
#define EXPORT_SYMBOL_NS_GPL(sym, ns) __EXPORT_SYMBOL(sym, "GPL", ns)

模块要使用这些符号,必须显式导入该命名空间(include/linux/module.h:302):

// include/linux/module.h:302
#define MODULE_IMPORT_NS(ns)   MODULE_INFO(import_ns, ns)

// 使用示例:
MODULE_IMPORT_NS(USB);

verify_namespace_is_imported()resolve_symbol() 中检查模块的 .modinfo 中是否声明了对应命名空间(kernel/module/main.c:1266)。

12.3 EXPORT_SYMBOL_FOR_MODULES

include/linux/export.h:94 中的特殊导出宏,将符号限定为只能被特定模块使用:

// include/linux/export.h:94
#define EXPORT_SYMBOL_FOR_MODULES(sym, mods) __EXPORT_SYMBOL(sym, "GPL", "module:" mods)

13. 模块的 livepatch 支持

13.1 livepatch 模块的特殊性

Linux livepatch(内核热补丁,CONFIG_LIVEPATCH)允许在不重启系统的情况下修复内核代码缺陷。struct module 中的 livepatch 相关字段(include/linux/module.h:547-553):

// include/linux/module.h:547
#ifdef CONFIG_LIVEPATCH
    bool klp;           // 这是一个 livepatch 模块
    bool klp_alive;     // livepatch 仍然激活
    struct klp_modinfo *klp_info;  // 保留的 ELF 信息
#endif

普通模块加载完成后,内核会释放 ELF 解析用的临时数据。但 livepatch 模块需要这些数据来支持函数替换时的重定位计算,因此 load_module() 在检测到 is_livepatch_module(mod) 时会调用 copy_module_elf() 保留 ELF 信息(kernel/module/main.c:3501):

// kernel/module/main.c:3501
if (is_livepatch_module(mod)) {
    err = copy_module_elf(mod, info);
    if (err < 0)
        goto sysfs_cleanup;
}

13.2 livepatch 的函数替换机制

insmod livepatch_fix.ko
    │
klp_enable_patch()
    │
对每个需要替换的函数:
klp_patch_func()
    │
    ├─ 找到原始内核函数地址
    ├─ 使用 ftrace 在函数入口安装 trampoline
    └─ trampoline 跳转到 livepatch 模块中的新函数

等待所有线程从旧函数返回(consistency model)
    │
替换完成:系统继续运行,使用新函数实现

卸载 livepatch 模块时,klp_module_going() 负责撤销所有函数替换(kernel/module/main.c:838)。


14. 模块系统的 RCU 并发模型

14.1 modules 链表的并发访问

全局 modules 链表(kernel/module/main.c:76)受到两种同步机制保护:

访问类型          同步机制
─────────────────────────────────────────
写操作(加/删模块)  module_mutex(互斥锁)
读操作(遍历链表)   RCU(读-拷贝-更新)

list_add_rcu() / list_del_rcu() 确保链表修改对 RCU 读者是安全的。find_symbol() 函数(在符号查找热路径上)只需要 RCU 读锁,不需要 module_mutex,从而避免了符号解析的锁竞争(kernel/module/main.c:404):

// kernel/module/main.c:404
list_for_each_entry_rcu(mod, &modules, list,
                        lockdep_is_held(&module_mutex)) {
    ...
}

14.2 模块地址查找树:mod_tree

mod_tree 是一个基于 latch-tree 的区间树,用于快速将内存地址映射到所属模块(kernel/module/main.c:83):

// kernel/module/main.c:83
struct mod_tree_root mod_tree __cacheline_aligned = {
    .addr_min = -1UL,  // 初始为最大值,表示无模块
};

__module_address() 函数通过此树将任意地址(如崩溃时的 PC 值)映射到对应模块,时间复杂度 O(log n),相比线性扫描 modules 链表快得多。latch-tree 是一种 RCU 友好平衡二叉树,维护两份树结构交替使用,读者无需锁即可安全遍历。

__mod_update_bounds() 维护模块地址范围的最小/最大值缓存(kernel/module/main.c:97-116),用于快速排除明显不在模块区域内的地址。


15. 模块构建系统(Kbuild)

15.1 模块 Makefile 结构

内核树内的模块 Makefile 格式:

# 内核树内模块示例
obj-$(CONFIG_E1000) += e1000.o
e1000-objs := e1000_main.o e1000_hw.o e1000_ethtool.o

obj-$(CONFIG_E1000)y 为静态编译,m 为编译为模块(.ko),空则不编译。多个 .o 文件会被链接器以 ld -r(可重定位合并)方式合并成一个 .ko

15.2 Kbuild 构建 .ko 的流程

源文件(.c)
    │
    ▼ gcc -c -DMODULE -D__KERNEL__ ...
    │
中间目标(.o)
    │
    ▼ ld -r(可重定位合并多个 .o)
    │
module.o
    │
    ▼ scripts/mod/modpost(生成 module.mod.c)
    │   - 处理 .export_symbol 节 → __ksymtab
    │   - 生成 .modinfo 中的 vermagic
    │   - 检查节不匹配(section mismatch)
    │
module.mod.o
    │
    ▼ ld -r module.o module.mod.o
    │
module.ko
    │
    ▼(可选)scripts/sign-file
    │
module.ko(带签名)

scripts/mod/modpost 是 Kbuild 中非常关键的工具:

  1. 解析所有 .o 中的 .export_symbol 节,生成最终的 __ksymtab 和 CRC 表
  2. 生成 module.mod.c,其中包含模块的 vermagic 字符串和依赖信息
  3. 检查节不匹配(如在非 init 函数中引用了 __init 数据),在构建时产生警告

15.3 树外模块(Out-of-tree)构建

KDIR ?= /lib/modules/$(shell uname -r)/build

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

运行 make 时,会切换到内核构建目录,以内核的 Kbuild 基础设施处理 M=$(PWD) 指定的模块目录。树外模块加载时会被打上 TAINT_OUT_OF_TREE 污点(O 标志)。


16. 深入案例:xt_RATEEST 模块

当前工作区修改了 net/netfilter/xt_RATEEST.c,该模块实现了 netfilter 的流量速率估计匹配器,展示了复杂模块的两级资源管理设计。

16.1 模块结构

xt_RATEEST 模块注册了两个组件:一个 target(用于设置速率估计器)和一个 match(用于匹配速率条件):

netfilter 规则链
    │
    ├─ RATEEST target(接收数据包时更新速率计数器)
    │   └─ xt_rateest_tg():记录字节数和包数,更新 EWMA 统计
    │
    └─ rateest match(匹配时检查速率)
        └─ xt_rateest_mt():与阈值比较,决定是否匹配

速率估计器(struct xt_rateest)通过哈希表管理,多个 target 规则可以引用同一个估计器实例。

16.2 两级引用计数设计

级别 1(模块引用计数):
    try_module_get(match->me) ← netfilter 在使用匹配规则时调用
    module_put(match->me)     ← 规则被删除时调用

级别 2(估计器引用计数):
    xt_rateest_get(name)      ← match 规则引用同一估计器时调用
    xt_rateest_put(est)       ← match 规则被删除时调用

模块必须等到所有级别 2 的引用都释放(即没有规则引用任何估计器),才能安全卸载。这是内核模块开发中常见的"资源对象 + 模块引用"双层引用计数模式。


17. 模块调试技术

17.1 使用 GDB 调试加载的模块

do_init_module() 被故意声明为 noinlinekernel/module/main.c:3017),就是为了给 GDB 脚本 lx-symbols 提供可靠的断点目标:

# 获取模块各节的加载地址
cat /sys/module/<modname>/sections/.text
cat /sys/module/<modname>/sections/.data

# 在 GDB 中加载符号
(gdb) add-symbol-file <module.ko> \
    -s .text  <text_address>  \
    -s .data  <data_address>  \
    -s .bss   <bss_address>

17.2 /proc/modules 格式解读

cat /proc/modules
# 输出格式:名称 大小 使用计数 依赖模块 状态 地址
xt_dscp 16384 2 - Live 0xffffffffc0a00000
xt_hl 16384 4 xt_dscp Live 0xffffffffc0b00000

各字段含义:

  • 16384:模块占用内存字节数(四舍五入到页面大小)
  • 2:当前引用计数减去 MODULE_REF_BASEmodule_refcount() 返回值)
  • xt_dscp:依赖本模块的模块列表
  • Live:模块状态(Live/Loading/Unloading)
  • 0xffffffffc0a00000:模块代码段基地址(mod->mem[MOD_TEXT].base

17.3 内核 oops 中的模块信息

当发生内核 oops 时,调用栈中会标注涉及模块的函数:

[<ffffffffc0a01234>] dscp_mt+0x14/0x30 [xt_dscp]
[<ffffffffc0b05678>] xt_check_match+0x48/0x120 [x_tables]

括号内的模块名由 __module_text_address() 解析得到:给定内存地址,在 mod_tree 中查找包含该地址的模块。TAINT 信息也会在 oops 中显示(如 Tainted: P O),告知内核社区系统中加载了专有模块(P)或树外模块(O)。

17.4 动态调试(dyndbg)

内核的动态调试基础设施与模块系统深度集成。struct module 中包含 dyndbg_info 字段(include/linux/module.h:582-584):

// include/linux/module.h:582
#ifdef CONFIG_DYNAMIC_DEBUG_CORE
    struct _ddebug_info dyndbg_info;
#endif

模块加载时,ddebug_add_module() 将模块中所有 pr_debug() / dev_dbg() 调用点注册到动态调试数据库。用户可以在运行时选择性地开启/关闭特定模块的调试输出:

# 开启 xt_dscp 模块的所有 pr_debug 输出
echo "module xt_dscp +p" > /sys/kernel/debug/dynamic_debug/control

模块卸载时,ddebug_remove_module() 清理注册信息,避免悬挂指针。


18. 模块与 BPF 的交互

18.1 BPF raw tracepoints 与模块

struct module 中的 bpf_raw_events 字段(include/linux/module.h:502-504)允许模块注册 BPF 可访问的原始事件追踪点:

// include/linux/module.h:501
#ifdef CONFIG_BPF_EVENTS
    unsigned int num_bpf_raw_events;
    struct bpf_raw_event_map *bpf_raw_events;
#endif

18.2 BTF(BPF Type Format)与模块

开启 CONFIG_DEBUG_INFO_BTF_MODULES 时,每个模块都会嵌入 BTF 类型信息(include/linux/module.h:505-510):

// include/linux/module.h:505
#ifdef CONFIG_DEBUG_INFO_BTF_MODULES
    unsigned int btf_data_size;
    unsigned int btf_base_data_size;
    void *btf_data;      // 模块自身的 BTF 数据
    void *btf_base_data; // 引用的内核基础 BTF 数据
#endif

BTF 使 BPF 验证器能够理解模块中数据结构的布局,实现类型安全的内核访问。bpftool 工具可以利用 BTF 提供模块级别的类型信息。


19. 关键数据结构关系图

struct module(include/linux/module.h:397)
│
├─ list_head list          → modules 全局链表(module_mutex 保护)
│
├─ module_kobject mkobj    → /sys/module/<name>/ 目录
│   └─ module_param_attrs → /sys/module/<name>/parameters/
│
├─ kernel_symbol *syms    → __ksymtab 节(NOT_GPL_ONLY 符号)
├─ kernel_symbol *gpl_syms → __ksymtab_gpl 节(GPL_ONLY 符号)
│
├─ kernel_param *kp        → __param 节(模块参数数组)
│
├─ module_memory mem[]     → 7 种内存区域(MOD_TEXT ... MOD_INIT_RODATA)
│   └─ mod_tree_node mtn  → 按地址索引的红黑树(加速 __module_address())
│
├─ mod_kallsyms *kallsyms  → 模块符号表(RCU 保护)
│
├─ int (*init)(void)       → module_init() 注册的初始化函数
├─ void (*exit)(void)      → module_exit() 注册的清理函数
│
├─ list_head source_list   → 依赖本模块的模块链表
├─ list_head target_list   → 本模块依赖的模块链表
│
└─ atomic_t refcnt         → 引用计数(try_module_get/module_put)

模块加载完整状态机:

                        insmod / modprobe
                               │
                               ▼
                    ┌─────────────────┐
                    │  MODULE_STATE_  │
                    │    UNFORMED     │  ← 正在解析/分配内存
                    └────────┬────────┘
                             │ add_unformed_module()
                             ▼
                    ┌─────────────────┐
                    │  MODULE_STATE_  │
                    │    COMING       │  ← 正在执行 init()
                    └────────┬────────┘
                             │ do_init_module() 成功
                             ▼
                    ┌─────────────────┐
                    │  MODULE_STATE_  │◄──── 正常运行
                    │     LIVE        │
                    └────────┬────────┘
                             │ rmmod(引用计数降为0)
                             ▼
                    ┌─────────────────┐
                    │  MODULE_STATE_  │
                    │    GOING        │  ← 正在执行 exit()
                    └────────┬────────┘
                             │ free_module()
                             ▼
                       (已从内存删除)

20. 工具链与调试命令速查

# 查看模块元信息
modinfo <module.ko>

# 加载模块(带参数)
insmod <module.ko> param1=value1

# 通过 modprobe 加载(自动处理依赖)
modprobe <modname> param1=value1

# 卸载模块
rmmod <modname>
rmmod -f <modname>  # 强制卸载(需 CONFIG_MODULE_FORCE_UNLOAD)

# 查看已加载模块
lsmod              # 等效 cat /proc/modules
cat /sys/module/<name>/refcnt

# 查看模块符号
cat /proc/kallsyms | grep <modname>
nm <module.ko>

# 读取/设置模块参数
cat /sys/module/<name>/parameters/<param>
echo value > /sys/module/<name>/parameters/<param>

# 查看 ELF 节信息
readelf -S <module.ko>
readelf -r <module.ko>  # 重定位表
readelf -s <module.ko>  # 符号表

# 查看模块签名
scripts/extract-module-sig.pl -d <module.ko>
openssl pkcs7 -inform der -print_certs -in sig.p7

# 对模块签名
scripts/sign-file sha256 certs/signing_key.pem certs/signing_key.x509 <module.ko>

# 动态调试
echo "module <name> +p" > /sys/kernel/debug/dynamic_debug/control

# 内核 taint 标志查看
cat /proc/sys/kernel/tainted

由 Claude Code 分析生成