注:本文为 “Linux 内核模块 | 管理 / 分析 / 参数说明” 相关文章合辑。
未整理去重。
【Linux 驱动】Linux 内核模块
沧海一笑 - dj 于 2024-04-26 16:16:24 发布
1. 概述
在 Linux 系统中,设备驱动会以内核模块的形式出现,学习 Linux 内核模块编程是驱动开发的先决条件。第一次接触 Linux 内核模块,我们将围绕着“Linux 内核模块是什么”“Linux 内核模块的工作原理”以及“我们该怎么使用 Linux 内核模块”这样的思路一起走进 Linux 内核世界。大致分为四个部分:
- 内核模块的概念:内核模块是什么东西?为什么引入内核模块这一机制?
- 内核模块的原理:内核模块在内核中的加载、卸载过程,深入剖析内核模块如何导出符号。
- helloworld 实验:理解内核模块的代码框架和原理,写一个属于自己的模块,以及模块的使用方法等。
- 内核模块传参与符号共享实验:理解内核模块的参数模式、符号共享,验证内核模块的运行机制。
2. 什么是内核
内核是一个操作系统的核心,是基于硬件的第一层软件扩充,提供操作系统的最基本功能,是操作系统工作的基础,决定着整个操作系统的性能和稳定性。
内核按照体系结构分为两类:微内核(Micro Kernel)和宏内核(Monolithic Kernel)。在微内核架构中,内核只提供操作系统核心功能,如实现进程管理、存储器管理、进程间通信、I/O 设备管理等,而其他的应用层 IPC、文件系统功能、设备驱动模块则不被包含到内核功能中,属于微内核之外的模块,所以针对这些模块的修改不会影响到微内核的核心功能。微内核具有动态扩展性强的优点。Windows 操作系统、华为的鸿蒙操作系统就属于这类微内核架构。
而宏内核架构是将上述包括微内核以及微内核之外的应用层 IPC、文件系统功能、设备驱动模块都编译成一个整体。其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。Linux 操作系统正是采用了宏内核结构。为了解决这一缺点,Linux 中引入了内核模块这一机制。
3. 内核模块机制引入
3.1 内核模块引入原因
Linux 是一个跨平台的操作系统,支持众多的设备,在 Linux 内核源码中有超过 50% 的代码都与设备驱动相关。Linux 为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。内核模块就是实现了某个功能的一段内核代码,在内核运行过程中,可以加载这部分代码到内核中,从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动,只需要编译相关的驱动代码即可,无需对整个内核进行编译。
3.2 内核模块引入好处
内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。在设备驱动的开发过程中,我们可以随意将正在测试的驱动程序添加到内核中或者从内核中移除,每次修改内核模块的代码不需要重新启动内核。在开发板上,我们也不需要将内核模块程序,或者说设备驱动程序的 ELF 文件存放在开发板中,免去占用不必要的存储空间。当需要加载内核模块的时候,可以通过挂载 NFS 服务器,将存放在其他设备中的内核模块,加载到开发板上。在某些特定的场合,我们可以按照需要加载 / 卸载系统的内核模块,从而更好地为当前环境提供服务。
3.3 内核模块的定义和特点
了解了内核模块引入以及带来的诸多好处,我们可以在头脑中建立起对内核模块的初步认识,下面让我们给出内核模块的具体定义:内核模块全称 Loadable Kernel Module (LKM),是一种在内核运行时加载一组目标代码来实现某个特定功能的机制。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行,在运行时它被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不一样的。模块由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序和其他内核上层功能。因此内核模块具备如下特点:
- 模块本身不被编译入内核映像,这控制了内核的大小。
- 模块一旦被加载,它就和内核中的其他部分完全一样。
有了内核模块的概念,下面我们一起深入了解内核模块的工作机制吧。
4. 内核模块的工作机制
我们编写的内核模块,经过编译,最终形成以 .ko
为后缀的 ELF 文件。我们可以使用 file
命令来查看它。
deng@local:~/kernel/bak/5th/1module$ file test.ko
test.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID [sha1]=ccde1e3a7d801e3320877ae06d126f2145e3fe46, with debug_info, not stripped
deng@local:~/kernel/bak/5th/1module$
那么这样的文件是如何被内核一步一步拿到并且很好地工作的呢?为了便于我们更好地理解内核模块的加载 / 卸载过程,可以先跟我一起学习 ELF 文件格式,了解 .ko
究竟是怎么一回事儿。再一同去看看内核源码,探究内核模块加载 / 卸载,以及符号导出的经过。
4.1 内核模块详细加载 / 卸载过程
ko 文件的文件格式
ko 文件在数据组织形式上是 ELF (Executable And Linking Format) 格式,是一种普通的可重定位目标文件。这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。
ELF 文件格式的可能布局如下图。
文件开始处是一个 ELF 头部 (ELF Header),用来描述整个文件的组织,这些信息独立于处理器,也独立于文件中的其余内容。我们可以使用 readelf
工具查看 ELF 文件的头部详细信息。
deng@local:~/kernel/bak/5th/1module$ readelf -h test.ko
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: AArch64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 175504 (bytes into file)
标志: 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: 30
Section header string table index: 27
deng@local:~/kernel/bak/5th/1module$
程序头部表 (Program Header Table) 是一个数组结构,它的每一个元素的数据结构如下:
- 每个数组元素表示一个“段”:包含一个或者多个“节区”,程序头部仅对于可执行文件和共享目标文件有意义。
- 其他信息:系统准备程序执行所必需的其他信息。
节区头部表 / 段表 (Section Header Table):ELF 文件中有很多各种各样的段,这个段表 (Section Header Table) 就是保存这些段的基本属性的结构,ELF 文件的段结构就是由段表决定的,编译器、链接器、装载器都是依靠段表来定位和访问各个段的属性的,包含了描述文件节区的信息。
ELF 头部中:
e_shoff
:给出从文件头到节区头部表格的偏移字节数。e_shnum
:给出表格中条目数目。e_shentsize
:给出每个项目的字节数。
从这些信息中可以确切地定位节区的具体位置、长度。和程序头部表一样,每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间,每一项元素为一结构体(思考这节开头的那张节区和节区头部的示意图)。
我们可以加上 -S
参数读取 ELF 文件的节区头部表的详细信息。
deng@local:~/kernel/bak/5th/1module$ readelf -S test.ko
There are 30 section headers, starting at offset 0x2ad90:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.gnu.bu [...] NOTE 0000000000000000 00000040
0000000000000024 0000000000000000 A 0 0 4
[ 2] .text PROGBITS 0000000000000000 00000064
0000000000000000 0000000000000000 AX 0 0 1
[ 3] .init.text PROGBITS 0000000000000000 00000068
0000000000000028 0000000000000000 AX 0 0 8
[ 4] .rela.init.text RELA 0000000000000000 000171a0
0000000000000030 0000000000000018 I 28 3 8
[ 5] .exit.text PROGBITS 0000000000000000 00000090
0000000000000020 0000000000000000 AX 0 0 8
[ 6] .rela.exit.text RELA 0000000000000000 000171d0
0000000000000030 0000000000000018 I 28 5 8
[ 7] .modinfo PROGBITS 0000000000000000 000000b0
0000000000000059 0000000000000000 A 0 0 8
[ 8] .rodata.str1.1 PROGBITS 0000000000000000 00000109
0000000000000021 0000000000000001 AMS 0 0 1
[ 9] .data PROGBITS 0000000000000000 0000012a
0000000000000000 0000000000000000 WA 0 0 1
[10] .gnu.linkonc [...] PROGBITS 0000000000000000 00000140
0000000000000300 0000000000000000 WA 0 0 64
[11] .rela.gnu.li [...] RELA 0000000000000000 00017200
0000000000000030 0000000000000018 I 28 10 8
[12] .bss NOBITS 0000000000000000 00000440
0000000000000000 0000000000000000 WA 0 0 1
[13] .debug_info PROGBITS 0000000000000000 00000440
000000000000c57a 0000000000000000 0 0 1
[14] .rela.debug_info RELA 0000000000000000 00017230
00000000000138f0 0000000000000018 I 28 13 8
[15] .debug_abbrev PROGBITS 0000000000000000 0000c9ba
00000000000006d5 0000000000000000 0 0 1
[16] .debug_aranges PROGBITS 0000000000000000 0000d08f
0000000000000060 0000000000000000 0 0 1
[17] .rela.debug_[...] RELA 0000000000000000 0002ab20
0000000000000060 0000000000000018 I 28 16 8
[18] .debug_ranges PROGBITS 0000000000000000 0000d0ef
0000000000000030 0000000000000000 0 0 1
[19] .rela.debug_[...] RELA 0000000000000000 0002ab80
0000000000000060 0000000000000018 I 28 18 8
[20] .debug_line PROGBITS 0000000000000000 0000d11f
0000000000000e34 0000000000000000 0 0 1
[21] .rela.debug_line RELA 0000000000000000 0002abe0
0000000000000030 0000000000000018 I 28 20 8
[22] .debug_str PROGBITS 0000000000000000 0000df53
0000000000008d1f 0000000000000001 MS 0 0 1
[23] .comment PROGBITS 0000000000000000 00016c72
000000000000005c 0000000000000001 MS 0 0 1
[24] .note.GNU-stack PROGBITS 0000000000000000 00016cce
0000000000000000 0000000000000000 0 0 1
[25] .debug_frame PROGBITS 0000000000000000 00016cd0
0000000000000060 0000000000000000 0 0 8
[26] .rela.debug_frame RELA 0000000000000000 0002ac10
0000000000000060 0000000000000018 I 28 25 8
[27] .shstrtab STRTAB 0000000000000000 0002ac70
000000000000011d 0000000000000000 0 0 1
[28] .symtab SYMTAB 0000000000000000 00016d30
00000000000003a8 0000000000000018 29 35 8
[29] .strtab STRTAB 0000000000000000 000170d8
00000000000000c1 0000000000000000 0 0 1
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),
D (mbind), p (processor specific)
deng@local:~/kernel/bak/5th/1module$
节区头部表中又包含了很多子表的信息,我们简单地来看两个。
重定位表
重定位表(.rel.text
)位于段表之后,它的类型为(sh_type
)为“SHT_REL
”,即重定位表(Relocation Table)。链接器在处理目标文件时,必须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位信息都记录在 ELF 文件的重定位表里面。对于每个需要重定位的代码段或者数据段,都会有一个相应的重定位表。一个重定位表同时也是 ELF 的一个段,这个段的类型(sh_type
)就是“SHT_REL
”。
读取重定位表。
deng@local:~/kernel/bak/5th/1module$ readelf -r test.ko
重定位节 '.rela.init.text' at offset 0x171a0 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000000c 00260000011b R_AARCH64_CALL26 0000000000000000 printk + 0
000000000020 000600000101 R_AARCH64_ABS64 0000000000000000 .rodata.str1.1 + 0
重定位节 '.rela.exit.text' at offset 0x171d0 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
00000000000c 00260000011b R_AARCH64_CALL26 0000000000000000 printk + 0
000000000018 000600000101 R_AARCH64_ABS64 0000000000000000 .rodata.str1.1 + 10
重定位节 '.rela.gnu.linkonce.this_module' at offset 0x17200 contains 2 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000170 002500000101 R_AARCH64_ABS64 0000000000000000 init_module + 0
0000000002e8 002400000101 R_AARCH64_ABS64 0000000000000000 cleanup_module + 0
重定位节 '.rela.debug_info' at offset 0x17230 contains 3338 entries:
偏移量 信息 类型 符号值 符号名称 + 加数
000000000006 000b00000102 R_AARCH64_ABS32 0000000000000000 .debug_abbrev + 0
00000000000c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 172c
000000000011 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 45a4
000000000015 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 330d
000000000019 000d00000102 R_AARCH64_ABS32 0000000000000000 .debug_ranges + 0
000000000025 000e00000102 R_AARCH64_ABS32 0000000000000000 .debug_line + 0
00000000002c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 39b9
000000000033 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 23e3
00000000003a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3070
00000000003f 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 43bb
00000000004c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2049
000000000051 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e7a
000000000063 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 44e5
000000000070 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 465a
00000000007a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 4032
000000000087 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 16
00000000008c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 24
000000000099 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3bb9
0000000000f0 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + dc
000000000111 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 353a
000000000123 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3ea4
000000000138 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e1a
000000000145 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3293
00000000014f 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 413d
00000000015a 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 259f
000000000165 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3be1
000000000170 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 257
00000000017b 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3005
000000000186 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3e69
0000000001a1 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2971
0000000001ac 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2dcb
0000000001b7 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 35fe
0000000001c2 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 16d3
0000000001cd 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 24a8
0000000001de 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2d2
0000000001e9 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3b95
0000000001f4 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 390b
0000000001ff 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 886
00000000020c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 1a56
000000000211 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2add
00000000021c 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 2043
000000000227 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + 3dd
000000000232 000f00000102 R_AARCH64_ABS32 0000000000000000 .debug_str + a82
字符串表
ELF 文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的,所以用固定的结构来表示比较困难,一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在 ELF 文件中也以段的形式保存,常见的段名为 .strtab
(String Table 字符串表)或者 .shstrtab
(Section Header String Table 段字符串表)。
读取节区字符串表。
deng@local:~/kernel/bak/5th/1module$ readelf -p 28 test.ko
String dump of section '.symtab':
[ 218]
[ 270] "
[ 288] 6
[ 2a0] J
[ 2d0] _
[ 308] (
[ 318