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文件由以下四个主要部分组成:
- ELF Header(ELF头):描述整个文件的组织结构
- Program Header Table(程序头表):告诉系统如何创建进程映像(用于可执行文件)
- Section Header Table(节头表):描述文件的各个section
- 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(可重定位文件),值为1e_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中定义:
在模块加载时,内核会比较模块中记录的CRC值与内核中导出符号的CRC值。如果不匹配,加载会失败并报错:
__ksymtab / __ksymtab_strings:导出符号表¶
如果模块导出符号(使用EXPORT_SYMBOL或EXPORT_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.text:module_init()指定的函数所在的代码段.exit.text:module_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_module和finit_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函数¶
这是模块加载的核心函数,它会:
- 验证ELF格式
- 读取并解析section headers
- 解析符号表和重定位信息
- 分配内存并复制section
- 执行重定位
- 初始化
struct module - 调用模块的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
- printk、module_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. 版本不匹配错误¶
原因:模块编译时使用的内核与运行时内核的符号版本不匹配。
解决方法:
- 使用正确的内核源码重新编译模块
- 确保modules_prepare或完整编译内核后编译模块
- 检查Module.symvers文件是否存在且正确
2. 未知符号错误¶
原因:模块使用的符号在内核中找不到。
调试方法: 1. 检查符号是否真的存在于内核:
# 在内核源码中搜索
grep -r "EXPORT_SYMBOL.*printk" /home/charliechen/imx-forge/third_party/linux-imx/include/
-
使用
nm检查模块需要的符号: -
检查内核配置:
3. GPL符号使用错误¶
原因:模块试图使用GPL符号,但模块本身不是GPL许可的。
解决方法:
4. 内存分配失败¶
原因:系统内存不足或模块太大。
调试方法: 1. 检查可用内存:
- 查看内核日志:
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_init和module_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的作用,以及模块是如何被加载到内核中的。
下一章,我们将探索更高级的模块开发话题:
- 模块间的符号依赖和通信
- 模块的生命周期管理
- 模块签名和安全机制
- 性能优化和最佳实践
准备好了吗?让我们继续深入内核模块开发的世界。
延伸阅读
- ELF格式规范 - TIS ELF Specification
- Linux内核模块编程指南 - The Linux Kernel Module Programming Guide
- 内核文档:kbuild/modules.rst - 外部模块构建文档