跳转至

ELF与.ko文件结构深度解析

为什么要写这一章

说实话,当我刚开始学习内核模块开发的时候,对于.ko文件的认识就停留在"编译出来就能insmod加载"这个层面。只要模块能加载运行,我才不管它里面是什么结构呢。

但是,随着学习的深入,我发现这种浅尝辄止的理解是不够的。当你遇到"disagrees about version of symbol"这样的错误时,当你需要理解为什么模块参数会出现在sysfs中时,当你想要搞清楚模块签名是怎么工作的时——如果你不懂ELF和.ko的内部结构,就真的会抓瞎。

这一章的目标是让你对.ko文件有一个深入的、从源码层面的理解。我会结合linux-imx的实际源码,带你看看.ko文件里面到底都有些什么东西,这些东西是怎么被内核模块加载器识别和处理的。

ELF文件格式回顾

在深入.ko文件之前,我们先快速回顾一下ELF(Executable and Linkable Format)文件格式。这是Linux系统中目标文件、可执行文件、共享库和内核模块的通用格式。

ELF的基本结构

一个ELF文件由以下四个主要部分组成:

  1. ELF Header(ELF头):描述整个文件的组织结构
  2. Program Header Table(程序头表):告诉系统如何创建进程映像(用于可执行文件)
  3. Section Header Table(节头表):描述文件的各个section
  4. Sections(节):存放各种类型的数据(代码、数据、符号表等)

对于内核模块(.ko文件),最重要的是ELF Header和Section Header Table,因为.ko不是可执行文件,不需要Program Header Table。

ELF Header结构

让我们看看内核源码中ELF Header的定义。在third_party/linux-imx/include/uapi/linux/elf.h中:

typedef struct elf64_hdr {
    unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
    Elf64_Half e_type;                /* 文件类型 */
    Elf64_Half e_machine;             /* 机器架构 */
    Elf64_Word e_version;             /* 版本 */
    Elf64_Addr e_entry;               /* 入口点虚拟地址 */
    Elf64_Off e_phoff;                /* 程序头表偏移 */
    Elf64_Off e_shoff;                /* 节头表偏移 */
    Elf64_Word e_flags;               /* 处理器特定标志 */
    Elf64_Half e_ehsize;              /* ELF头大小 */
    Elf64_Half e_phentsize;           /* 程序头表条目大小 */
    Elf64_Half e_phnum;               /* 程序头表条目数量 */
    Elf64_Half e_shentsize;           /* 节头表条目大小 */
    Elf64_Half e_shnum;               /* 节头表条目数量 */
    Elf64_Half e_shstrndx;            /* 节名字符串表索引 */
} Elf64_Ehdr;

关键字段说明:

  • e_ident[0]:应该是0x7f,接下来三个字节是'E'、'L'、'F'(这就是ELF的magic number)
  • e_type:文件类型。对于.ko文件,这个值是ET_REL(可重定位文件),值为1
  • e_machine:机器架构。对于i.MX6ULL(ARM),这个值是EM_ARM(40)
  • e_shoff:Section Header Table在文件中的偏移量
  • e_shnum:有多少个section

Section Header结构

每个section的描述由Section Header给出:

typedef struct elf64_shdr {
    Elf64_Word sh_name;       /* Section名称(在字符串表中的偏移) */
    Elf64_Word sh_type;       /* Section类型 */
    Elf64_Xword sh_flags;     /* Section标志 */
    Elf64_Addr sh_addr;       /* 执行时的虚拟地址 */
    Elf64_Off sh_offset;      /* 在文件中的偏移 */
    Elf64_Xword sh_size;      /* Section大小 */
    Elf64_Word sh_link;       /* 到其他section的链接 */
    Elf64_Word sh_info;       /* 额外的信息 */
    Elf64_Xword sh_addralign; /* 对齐要求 */
    Elf64_Xword sh_entsize;   /* 条目大小(如果section包含数组) */
} Elf64_Shdr;

常见的sh_type值包括: - SHT_NULL(0):无效section - SHT_PROGBITS(1):程序定义的信息 - SHT_SYMTAB(2):符号表 - SHT_STRTAB(3):字符串表 - SHT_RELA(4):重定位条目(带显式加数)

常见的Section

一个典型的ELF文件包含以下section:

Section名称 类型 说明
.text SHT_PROGBITS 可执行代码
.data SHT_PROGBITS 初始化的数据
.bss SHT_NOBITS 未初始化的数据(不占文件空间)
.rodata SHT_PROGBITS 只读数据
.symtab SHT_SYMTAB 符号表
.strtab SHT_STRTAB 字符串表(用于符号表)
.shstrtab SHT_STRTAB Section名称字符串表

.ko文件与普通ELF的差异

现在我们来到重点:.ko文件和普通的ELF可重定位文件有什么区别?

1. .ko是ET_REL类型

.ko文件本质上是一个ELF可重定位文件(ET_REL),这意味着它: - 没有程序头表(Program Header Table) - 不能直接执行 - 需要链接到内核后才能使用

你可以用readelf -h命令查看:

$ readelf -h hello.ko
ELF Header:
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           ARM
  ...

2. 内核特有的Section

.ko文件包含许多内核特有的section,这些section是普通ELF文件没有的。我们马上会详细讲解这些section。

3. 符号引用的特殊性

.ko文件中的符号引用指向内核空间,而不是用户空间。这意味着: - 符号的解析必须在模块加载时完成 - 符号版本必须与内核匹配(CRC校验) - 某些符号只能在GPL模块中使用(EXPORT_SYMBOL_GPL

.ko中特有的Section详解

这是本章的核心内容。让我们逐一看看.ko文件中那些特有的section。

.modinfo:模块元信息

.modinfo section存储了模块的元数据,这些信息通过MODULE_*宏定义。在third_party/linux-imx/include/linux/module.h中可以看到相关定义:

/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)

#define __MODULE_INFO(tag, name, info)                      \
    static const char __UNIQUE_ID(name)[]                   \
        __used __section(".modinfo") __aligned(1)           \
        = __MODULE_INFO_PREFIX __stringify(tag) "=" info

当你使用这些宏时:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple driver");
MODULE_VERSION("1.0");

编译器会把它们放入.modinfo section。你可以用modinfo命令查看:

$ modinfo hello.ko
filename:       hello.ko
version:        1.0
description:    A simple driver
author:         Your Name
license:        GPL
...

__versions:符号版本校验表

当内核配置了CONFIG_MODVERSIONS时,__versions section包含了模块使用的所有内核符号的CRC校验值。这用于确保模块与内核的ABI兼容性。

third_party/linux-imx/include/linux/module.h中定义:

struct modversion_info {
    unsigned long crc;
    char name[MODULE_NAME_LEN];
};

在模块加载时,内核会比较模块中记录的CRC值与内核中导出符号的CRC值。如果不匹配,加载会失败并报错:

hello: disagrees about version of symbol module_put

__ksymtab / __ksymtab_strings:导出符号表

如果模块导出符号(使用EXPORT_SYMBOLEXPORT_SYMBOL_GPL),这些符号的信息会存放在: - __ksymtab:符号表(包含符号的值和名称偏移) - __ksymtab_strings:符号名称字符串 - __ksymtab_gpl:GPL符号的单独表

third_party/linux-imx/include/linux/export.h中定义:

#define __EXPORT_SYMBOL(sym, sec, ns)                          \
    extern typeof(sym) sym;                                    \
    __CRC_SYMBOL(sym, sec)                                     \
    static const char __kstrtab_##sym[]                        \
    __attribute__((section("__ksymtab_strings"), aligned(1)))  \
    = #sym;                                                    \
    static const struct kernel_symbol __ksymtab_##sym          \
    __used __section("__ksymtab" sec)                          \
    = { (unsigned long)&sym, __kstrtab_##sym }

.gnu.linkonce.this_module:struct module的原型

这个section包含了一个静态的struct module结构,它是模块在内核中表示的原型。在模块加载时,内核会复制这个结构并初始化。

third_party/linux-imx/include/linux/module.h中:

struct module {
    enum module_state state;
    struct list_head list;
    char name[MODULE_NAME_LEN];
    struct module_kobject mkobj;
    struct module_attribute *modinfo_attrs;
    const char *version;
    const char *srcversion;
    /* ... 更多字段 ... */
    struct module_memory mem[MOD_MEM_NUM_TYPES] __module_memory_align;
    void *args; /* 模块参数 */
    /* ... 更多字段 ... */
};

__param:模块参数描述表

当你使用module_param宏定义模块参数时,参数的描述信息会被放入__param section。在third_party/linux-imx/include/linux/moduleparam.h中:

#define module_param(name, type, perm)                \
    module_param_named(name, name, type, perm)

/* 最终展开后会创建一个kernel_param结构放在__param section */

参数信息包括: - 参数名称 - 参数类型 - 参数权限(用于sysfs中的权限) - 参数值指针

.init.text / .exit.text:初始化/清理代码段

这两个section包含了模块的初始化和清理代码:

  • .init.textmodule_init()指定的函数所在的代码段
  • .exit.textmodule_exit()指定的函数所在的代码段

在模块加载成功后,.init.text段会被释放以节省内存。这就是为什么初始化函数可以声明为__init

static int __init hello_init(void)
{
    pr_info("Hello, world!\n");
    return 0;
}
module_init(hello_init);

其他重要section

Section名称 说明
.text 模块的主要代码
.rodata 只读数据(如字符串常量)
.data 可读写的数据
.bss 未初始化的数据
.comment 编译器版本信息
.note.GNU-stack 栈可执行性标记
.rela.* 重定位信息
.debug_* 调试信息(如果编译时包含-g)

内核模块加载流程分析

现在让我们从内核源码的角度,看看模块是如何被加载的。这部分内容可以帮助你理解上面那些section是如何被使用的。

加载入口

模块加载的系统调用是init_modulefinit_module。在third_party/linux-imx/kernel/module/main.c中:

SYSCALL_DEFINE3(init_module, const char __user *, umod,
        const char __user *, uargs, int, flags)
{
    struct load_info info = { };
    int err;

    /* 复制模块信息到内核空间 */
    err = copy_module_from_user(umod, &info);
    if (err)
        return err;

    /* 执行加载 */
    return load_module(&info, uargs, flags);
}

load_module函数

这是模块加载的核心函数,它会:

  1. 验证ELF格式
  2. 读取并解析section headers
  3. 解析符号表和重定位信息
  4. 分配内存并复制section
  5. 执行重定位
  6. 初始化struct module
  7. 调用模块的init函数

关键代码片段:

/* 查找section */
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];
        /* 检查SHF_ALLOC标志和section名称 */
        if ((shdr->sh_flags & SHF_ALLOC)
            && strcmp(info->secstrings + shdr->sh_name, name) == 0)
            return i;
    }
    return 0;
}

符号解析和重定位

模块使用的内核符号需要在加载时解析。这是通过find_symbol函数完成的:

bool find_symbol(struct find_symbol_arg *fsa)
{
    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 },
    };
    /* ... 搜索符号 ... */
}

实战:分析一个.ko文件

光说不练假把式。让我们实际编译一个模块并逐步分析它。

编译测试模块

首先创建一个简单的测试模块:

// SPDX-License-Identifier: GPL-2.0
/*
 * elf_analysis.c - 用于ELF分析的测试模块
 */

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

/* 模块参数 */
static int debug_enable = 0;
module_param(debug_enable, int, 0644);
MODULE_PARM_DESC(debug_enable, "Enable debug mode");

static char *device_name = "testdev";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name");

/* 导出符号供其他模块使用 */
static int test_function(int value)
{
    return value * 2;
}
EXPORT_SYMBOL(test_function);

/* 模块初始化 */
static int __init elf_analysis_init(void)
{
    pr_info("elf_analysis: module loaded\n");
    pr_info("elf_analysis: debug_enable=%d, device_name=%s\n",
            debug_enable, device_name);
    return 0;
}

/* 模块清理 */
static void __exit elf_analysis_exit(void)
{
    pr_info("elf_analysis: module unloaded\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("IMX-Forge Tutorial");
MODULE_DESCRIPTION("ELF structure analysis demo");
MODULE_VERSION("1.0");

module_init(elf_analysis_init);
module_exit(elf_analysis_exit);

配套的Makefile:

# Makefile for ELF analysis module
obj-m := elf_analysis.o

# 内核源码路径
KDIR := /home/charliechen/imx-forge/third_party/linux-imx

# 当前目录
PWD := $(shell pwd)

# 交叉编译工具链前缀(根据你的环境调整)
CROSS_COMPILE := arm-linux-gnueabihf-

# 编译目标
all:
    $(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

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

编译模块:

$ make
make -C /home/charliechen/imx-forge/third_party/linux-imx M=/path/to/module ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
make[1]: Entering directory '/home/charliechen/imx-forge/third_party/linux-imx'
  CC [M]  /path/to/module/elf_analysis.o
  MODPOST /path/to/module/Module.symvers
  CC [M]  /path/to/module/elf_analysis.mod.o
  LD [M]  /path/to/module/elf_analysis.ko
make[1]: Leaving directory '/home/charliechen/imx-forge/third_party/linux-imx'

readelf -h:查看ELF头

$ arm-linux-gnueabihf-readelf -h elf_analysis.ko
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)  # 注意这是可重定位文件
  Machine:                           AArch64                 # 架构类型
  Version:                           0x1
  Entry point address:               0x0                     # 无入口点(可重定位文件)
  Start of program headers:          0 (bytes into file)    # 无程序头
  Start of section headers:          2648 (bytes into file) # 节头表偏移
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0                       # 无程序头
  Size of section headers:           64 (bytes)
  Number of section headers:         28                      # 28个section
  Section header string table index: 26

readelf -S:查看段表

$ arm-linux-gnueabihf-readelf -S elf_analysis.ko
There are 28 section headers, starting at offset 0xa58:

Section Headers:
  [Nr] Name              Type             Address          Size    EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  0000000000000000  0000000000000000         0     0     0
  [ 1] .text             PROGBITS         0000000000000000  0000000000000054  0000000000000000  AX     0     0     4
  [ 2] .rela.text        RELA             0000000000000000  0000000000000120  0000000000000018      I     24    1     8
  [ 3] .init.text        PROGBITS         0000000000000000  0000000000000034  0000000000000000  AX     0     0     4
  [ 4] .exit.text        PROGBITS         0000000000000000  0000000000000014  0000000000000000  AX     0     0     4
  [ 5] .rodata.str1.8    PROGBITS         0000000000000000  0000000000000048  0000000000000001  AMS   0     0     8
  [ 6] .rodata           PROGBITS         0000000000000000  0000000000000001  0000000000000000   AMS   0     0     4
  [ 7] .rela.rodata      RELA             0000000000000000  0000000000000018  0000000000000018   I     24    6     8
  [ 8] .data             PROGBITS         0000000000000000  0000000000000004  0000000000000000  WA     0     0     4
  [ 9] .bss              NOBITS           0000000000000000  0000000000000008  0000000000000000  WA     0     0     4
  [10] __ksymtab         PROGBITS         0000000000000000  0000000000000018  0000000000000018   A     0     0     8
  [11] .rela.__ksymtab   RELA             0000000000000000  0000000000000030  0000000000000018   I     24   10     8
  [12] __ksymtab_strings PROGBITS         0000000000000000  000000000000000d  0000000000000001  AMS   0     0     1
  [13] __param           PROGBITS         0000000000000000  0000000000000050  0000000000000050   A     0     0     8
  [14] .rela.__param     RELA             0000000000000000  0000000000000078  0000000000000018      I     24   13     8
  [15] __mod_pci_device_table PROGBITS   0000000000000000  0000000000000000  0000000000000000   A     0     0     4
  [16] __modver          PROGBITS         0000000000000000  0000000000000020  0000000000000010   A     0     0     8
  [17] .rela.__modver    RELA             0000000000000000  0000000000000030  0000000000000018   I     24   16     8
  [18] .note.GNU-stack   PROGBITS         0000000000000000  0000000000000000  0000000000000000         0     0     1
  [19] .gnu.linkonce.this_module PROGBITS 0000000000000000 0000000000000a00 0000000000000000  WA     0     0   32
  [20] .rela.gnu.linkonce.this_module RELA 0000000000000000 0000000000000030 0000000000000018   I     24   19     8
  [21] .modinfo          PROGBITS         0000000000000000  00000000000000c5  0000000000000001   A     0     0     1
  [22] .rela.modinfo     RELA             0000000000000000  0000000000000078  0000000000000018      I     24   21     8
  [23] .symtab           SYMTAB           0000000000000000  0000000000000648  0000000000000024     24    58     8
  [24] .strtab           STRTAB           0000000000000000  0000000000000290  0000000000000000   0     0     1
  [25] .shstrtab         STRTAB           0000000000000000  00000000000000f3  0000000000000000   0     0     1
  [26] .debug_frame      PROGBITS         0000000000000000  0000000000000080  0000000000000000         0     0     8
  [27] .rela.debug_frame RELA             0000000000000000  0000000000000018  0000000000000018   I     24   26     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info)
  L (link order), O (extra OS processing required), G (group), T (TLS)
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

重点关注的section: - [1] .text - 主要代码 - [3] .init.text - 初始化代码 - [4] .exit.text - 清理代码 - [10] __ksymtab - 导出的符号表 - [12] __ksymtab_strings - 导出的符号名称 - [13] __param - 模块参数 - [16] __modver - 符号版本(CRC) - [19] .gnu.linkonce.this_module - struct module - [21] .modinfo - 模块信息

readelf -s:查看符号表

$ arm-linux-gnueabihf-readelf -s elf_analysis.ko | head -60

Symbol table '.symtab' contains 58 entries:
   Num:    Value          Size Type    Bind   Vis              Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS elf_analysis.mod.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     ...
    24: 0000000000000000     0 SECTION LOCAL  DEFAULT   13 __param
    25: 0000000000000000     0 SECTION LOCAL  DEFAULT   16 __modver
    26: 0000000000000000     0 SECTION LOCAL  DEFAULT   21 .modinfo
    27: 0000000000000000    10 FUNC    LOCAL  DEFAULT    3 elf_analysis_init
    28: 0000000000000000    20 FUNC    LOCAL  DEFAULT    4 elf_analysis_exit
    29: 0000000000000000    44 FUNC    LOCAL  DEFAULT    1 test_function
    30: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    8 debug_enable
    31: 0000000000000000     8 OBJECT  LOCAL  DEFAULT    9 device_name
    32: 0000000000000000     0 NOTYPE  GLOBAL  DEFAULT  UND printk
    33: 0000000000000000     0 NOTYPE  GLOBAL  DEFAULT  UND __this_module
    34: 0000000000000000     0 NOTYPE  GLOBAL  DEFAULT  UND module_put
    ...
    52: 0000000000000000    44 FUNC    GLOBAL DEFAULT    1 test_function
    ...

注意: - test_function在符号表中出现了两次(LOCAL和GLOBAL),GLOBAL版本是因为我们用了EXPORT_SYMBOL - printkmodule_put等是UNDEFINED(UND),需要从内核解析

modinfo:查看.modinfo内容

$ modinfo elf_analysis.ko
filename:       elf_analysis.ko
version:        1.0
description:    ELF structure analysis demo
author:         IMX-Forge Tutorial
license:        GPL
vermagic:       6.12.49-gbp SMP preempt mod_unload modversions aarch64

objdump -d:反汇编代码段

$ arm-linux-gnueabihf-objdump -d elf_analysis.ko

elf_analysis.ko:     file format elf64-littleaarch64

Disassembly of section .text:

0000000000000000 <test_function>:
   0:   d10043ff        sub     sp, sp, #0x10
   4:   f90003e0        str     x0, [sp, #8]
   8:   f94003e0        ldr     x0, [sp, #8]
   c:   8b000040        add     x0, x0, x0         # x0 = x0 * 2
  10:   910043ff        add     sp, sp, #0x10
  14:   d65f03c0        ret

...

这是ARM64汇编,可以看到test_function的实现就是简单的add x0, x0, x0(加法实现乘2)。

常见错误、调试方法与内核报错解读

在模块开发和加载过程中,你可能会遇到各种错误。下面我们来分析常见错误及其解决方案。

1. 版本不匹配错误

elf_analysis: disagrees about version of symbol module_put

原因:模块编译时使用的内核与运行时内核的符号版本不匹配。

解决方法: - 使用正确的内核源码重新编译模块 - 确保modules_prepare或完整编译内核后编译模块 - 检查Module.symvers文件是否存在且正确

2. 未知符号错误

elf_analysis: Unknown symbol printk (err 0)

原因:模块使用的符号在内核中找不到。

调试方法: 1. 检查符号是否真的存在于内核:

# 在内核源码中搜索
grep -r "EXPORT_SYMBOL.*printk" /home/charliechen/imx-forge/third_party/linux-imx/include/

  1. 使用nm检查模块需要的符号:

    $ arm-linux-gnueabihf-nm elf_analysis.ko | grep U
                     U printk
                     U module_put
    

  2. 检查内核配置:

    # 确保相关功能已启用
    zcat /proc/config.gz | grep PRINTK
    

3. GPL符号使用错误

elf_analysis: symbol 'some_gpl_symbol' is not exported

原因:模块试图使用GPL符号,但模块本身不是GPL许可的。

解决方法

// 确保模块声明为GPL许可
MODULE_LICENSE("GPL");  // 不能是"Proprietary"或其他

4. 内存分配失败

elf_analysis: could not find permanent map for module text

原因:系统内存不足或模块太大。

调试方法: 1. 检查可用内存:

cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable'

  1. 查看内核日志:
    dmesg | tail -20
    

5. 调试模块加载问题

启用模块加载调试

# 启用模块相关的动态调试
echo 'module debug' > /sys/kernel/debug/dynamic_debug/control

# 或者使用动态调试
echo 'file kernel/module/main.c +p' > /sys/kernel/debug/dynamic_debug/control

查看模块加载状态

# 查看已加载的模块
lsmod | grep elf_analysis

# 查看模块详细信息
modinfo elf_analysis

# 查看模块在sysfs中的信息
ls -la /sys/module/elf_analysis/

练习题

练习1:基础Section分析

题目:编译一个简单的内核模块(只有module_initmodule_exit),使用readelf命令: 1. 列出所有的section 2. 找出.init.text section的大小 3. 找出.modinfo section的内容

参考答案

# 编译模块
make

# 列出所有section
arm-linux-gnueabihf-readelf -S simple.ko

# 查看特定section大小
arm-linux-gnueabihf-readelf -S simple.ko | grep '\.init\.text'
arm-linux-gnueabihf-readelf -S simple.ko | grep '\.modinfo'

# 查看.modinfo内容
modinfo simple.ko
# 或者用readelf直接查看
arm-linux-gnueabihf-readelf -x .modinfo simple.ko

练习2:符号表分析

题目: 1. 编译一个导出符号的模块 2. 找出__ksymtab section中有多少个符号 3. 找出__ksymtab_strings section中符号名称的偏移量

参考答案

# 查看导出的符号
arm-linux-gnueabihf-readelf -s export_demo.ko | grep GLOBAL

# 查看__ksymtab section
arm-linux-gnueabihf-readelf -x __ksymtab export_demo.ko

# 查看__ksymtab_strings section
arm-linux-gnueabihf-readelf -p __ksymtab_strings export_demo.ko

练习3:模块参数分析

题目: 1. 创建一个带有多个参数的模块(int、charp、bool类型) 2. 使用readelf找出__param section的内容 3. 解释每个参数描述符的含义

参考答案

# 查看__param section的十六进制内容
arm-linux-gnueabihf-readelf -x __param param_demo.ko

# 查看参数的详细信息
modinfo -p param_demo.ko

# 或者查看模块加载后的参数
insmod param_demo.ko
cat /sys/module/param_demo/parameters/*

__param section中的每个条目是一个struct kernel_param结构:

struct kernel_param {
    const char *name;              /* 参数名 */
    const struct kernel_param_ops *ops;  /* 参数操作 */
    const u16 perm;                /* sysfs权限 */
    /* ... */
};

练习4:重定位分析

题目: 1. 使用readelf查看模块的重定位section(.rela.*) 2. 找出哪些符号需要重定位 3. 解释为什么这些符号需要重定位

参考答案

# 列出所有重定位section
arm-linux-gnueabihf-readelf -S reloc_demo.ko | grep rela

# 查看重定位条目
arm-linux-gnueabihf-readelf -r reloc_demo.ko

# 输出示例:
# Relocation section '.rela.text' at offset 0x2b0 contains 8 entries:
#   Offset          Info           Type           Sym. Value    Sym. Name
#   00000000000008  0000002f00000002 R_AARCH64_ABS64  0000000000000000 printk

需要重定位的符号通常是: - 内核函数调用(如printk) - 全局变量引用 - 字符串常量引用

练习5:完整加载流程追踪

题目: 1. 在内核源码中设置断点(使用ftrace或tracepoints) 2. 加载一个模块并追踪加载流程 3. 列出调用的关键函数及其顺序

参考答案

# 启用模块加载追踪
echo 1 > /sys/kernel/debug/tracing/events/module/enable
cat /sys/kernel/debug/tracing/trace

# 或者使用trace_pipe实时查看
cat /sys/kernel/debug/tracing/trace_pipe

关键函数调用顺序(参考third_party/linux-imx/kernel/module/main.c): 1. SYSCALL_DEFINE3(init_module, ...) - 系统调用入口 2. load_module() - 主加载函数 3. elf_validity_check() - ELF格式验证 4. layout_and_allocate() - 内存布局和分配 5. rewrite_section_headers() - 重写section头 6. simplify_symbols() - 符号简化 7. apply_relocations() - 应用重定位 8. post_relocation() - 重定位后处理 9. complete_formation() - 完成模块形成 10. do_init_module() - 执行初始化函数

实战代码查看

为了更深入地理解,建议查看以下内核源码文件:

1. 模块加载核心代码

  • third_party/linux-imx/kernel/module/main.c - 模块加载器主文件
  • third_party/linux-imx/kernel/module/kallsyms.c - 符号表处理

2. 模块相关数据结构

  • third_party/linux-imx/include/linux/module.h - struct module定义
  • third_party/linux-imx/include/linux/moduleparam.h - 模块参数
  • third_party/linux-imx/include/linux/export.h - 符号导出

3. ELF格式定义

  • third_party/linux-imx/include/uapi/linux/elf.h - ELF格式定义
  • third_party/linux-imx/include/linux/elf.h - 内部ELF支持

4. 模块构建工具

  • third_party/linux-imx/scripts/mod/modpost.c - 模块后处理工具
  • third_party/linux-imx/scripts/module.lds.S - 模块链接脚本

关键函数源码位置

函数名 文件位置 说明
load_module() kernel/module/main.c 模块加载主函数
find_sec() kernel/module/main.c 查找section
find_symbol() kernel/module/main.c 查找导出符号
simplify_symbols() kernel/module/main.c 符号处理
apply_relocations() kernel/module/main.c 应用重定位
do_init_module() kernel/module/main.c 执行init函数

下一章预告

到这里,你应该对.ko文件的内部结构有了深入的理解。你知道了.ko文件的ELF格式、各个section的作用,以及模块是如何被加载到内核中的。

下一章,我们将探索更高级的模块开发话题:

  • 模块间的符号依赖和通信
  • 模块的生命周期管理
  • 模块签名和安全机制
  • 性能优化和最佳实践

准备好了吗?让我们继续深入内核模块开发的世界。


延伸阅读