LV06-02-内核模块-04-内核模块的工作机制
本文主要是内核模块——内核模块的工作机制的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
PC端开发环境 | Windows | Windows11 |
Ubuntu | Ubuntu20.04.2的64位版本 | |
VMware® Workstation 17 Pro | 17.6.0 build-24238078 | |
终端软件 | MobaXterm(Professional Edition v23.0 Build 5042 (license)) | |
Win32DiskImager | Win32DiskImager v1.0 | |
Linux开发板环境 | Linux开发板 | 正点原子 i.MX6ULL Linux 阿尔法开发板 |
uboot | NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04 | |
linux内核 | linux-4.19.71(NXP官方提供) |
点击查看本文参考资料
分类 | 网址 | 说明 |
官方网站 | https://www.arm.com/ | ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档 |
https://www.nxp.com.cn/ | NXP官方网站 | |
https://www.nxpic.org.cn/ | NXP 官方社区 | |
https://u-boot.readthedocs.io/en/latest/ | u-boot官网 | |
https://www.kernel.org/ | linux内核官网 |
点击查看相关文件下载
分类 | 网址 | 说明 |
NXP | https://github.com/nxp-imx | NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库 |
nxp-imx/linux-imx/releases/tag/v4.19.71 | NXP linux内核仓库tags中的v4.19.71 | |
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 | NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0 | |
I.MX6ULL | i.MX 6ULL Applications Processors for Industrial Products | I.MX6ULL 芯片手册(datasheet,可以在线查看) |
i.MX 6ULL Applications ProcessorReference Manual | I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网) | |
Source Code | https://elixir.bootlin.com/linux/latest/source | linux kernel源码 |
https://elixir.bootlin.com/u-boot/latest/source | uboot源码 |
这一节我们来了解一下内核模块的工作机制。
一、ko文件的格式
1. ELF文件格式
ko文件在数据组织形式上是ELF(Excutable And Linking Format)格式,是一种普通的可重定位目标文件。 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类。ELF 文件格式的可能布局如下图:
文件开始处是一个ELF头部(ELF Header),用来描述整个文件的组织,这些信息独立于处理器, 也独立于文件中的其余内容。详细分析可以看这里:ELF文件格式分析 (gitee.com)
2. 头部信息
我们在ubuntu中可使用readelf工具查看elf文件的头部信息:
1 | readelf -h hello_world_demo.ko |
程序头部表(Program Header Table)是个数组结构,它的每一个元素的数据结构如下每个数组元素表示:
- 一个”段”:包含一个或者多个”节区”,程序头部仅对于可执行文件和共享目标文件有意义
- 其他信息:系统准备程序执行所必需的其它信息”
节区头部表/段表(Section Heade Table) ELF文件中有很多各种各样的段,这个段表(Section Header Table)就是保存这些段的基本属性的结构, ELF文件的段结构就是由段表决定的,编译器、链接器、装载器都是依靠段表来定位和访问各个段的属性的 包含了描述文件节区的信息。
ELF头部中:
- e_shoff:给出从文件头到节区头部表格的偏移字节数,
- e_shnum:给出表格中条目数目,
- e_shentsize: 给出每个项目的字节数。
3. 节区
从上面哪些信息中可以确切地定位节区的具体位置、长度和程序头部表一样, 每一项节区在节区头部表格中都存在着一项元素与它对应,因此可知,这个节区头部表格为一连续的空间, 每一项元素为一结构体(思考这节开头的那张节区和节区头部的示意图)。我们可以加上-S参数读取elf文件的节区头部表的详细信息。
1 | readelf -S hello_world_demo.ko |
节区头部表中又包含了很多子表的信息,我们简单的来看两个。
3.1 重定位表
重定位表(“.rel.text”)位于段表之后,它的类型为(sh_type)为”SHT_REL”,即重定位表(Relocation Table) 链接器在处理目标文件时,必须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置, 这些重定位信息都记录在ELF文件的重定位表里面,对于每个须要重定位的代码段或者数据段,都会有一个相应的重定位表 一个重定位表同时也是ELF的一个段,这个段的类型(sh_type)就是”SHT_REL”:
1 | readelf -r hello_world_demo.ko |
3.2 字符串表
ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的, 所以用固定的结构来表示比较困难,一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。 一般字符串表在ELF文件中也以段的形式保存,常见的段名为”.strtab”(String Table 字符串表)或者”.shstrtab”(Section Header String Table 段字符串表)
1 | readelf -p 21 hello_world_demo.ko # 前面.shstrtab在节头中显示是21 |
反正我是没咋看懂,就先这样吧,大概了解一下,详细的就看这篇文档:ELF文件格式分析 (gitee.com)
二、内核模块加载与卸载
在前面我们了解了ko内核模块文件的一些格式内容之后, 我们可以知道内核模块其实也是一段经过特殊加工的代码, 那么既然是加工过的代码,内核就可以利用到加工时留在内核模块里的信息, 对内核模块进行利用。所以我们就可以接着了解内核模块的加载过程和卸载过程了。
1. 加载过程
1.1 sys_init_module()
首先 insmod
会通过文件系统将 .ko
模块读到用户空间的一块内存中, 然后执行系统调用 sys_init_module()
解析模组,这时,内核在vmalloc区分配与ko文件大小相同的内存来暂存ko文件, 暂存好之后解析ko文件,将文件中的各个section分配到init 段和core 段,在modules区为init段和core段分配内存, 并把对应的section copy到modules区最终的运行地址,经过relocate函数地址等操作后,就可以执行ko的init操作了, 这样一个ko的加载流程就结束了。 同时,init段会被释放掉,仅留下core段来运行。
sys_init_module()
函数定义在module.c - kernel/module.c:
1 | SYSCALL_DEFINE3(init_module, void __user *, umod, |
- 第14行:通过vmalloc在vmalloc区分配内存空间,将内核模块copy到此空间,info→hdr 直接指向此空间首地址,也就是ko的elf header 。
- 第18行:然后通过load_module()进行模块加载的核心处理,在这里完成了模块的搬移,重定向等过程。
1.2 load_module()
load_module()函数定义在module.c - kernel/module.c:
1 | /* 分配并加载模块 */ |
- 第9行:setup_load_info()加载struct load_info 和 struct module, rewrite_section_headers,将每个section的sh_addr修改为当前镜像所在的内存地址, section 名称字符串表地址的获取方式是从ELF头中的e_shstrndx获取到节区头部字符串表的标号,找到对应section在ELF文件中的偏移,再加上ELF文件起始地址就得到了字符串表在内存中的地址。
- 第11行:在layout_and_allocate()中,layout_sections() 负责将section 归类为core和init这两大类,为ko的第二次搬移做准备。move_module()把ko搬移到最终的运行地址。内核模块加载代码搬运过程到此就结束了。
2. 卸载过程
卸载过程相对加载比较简单,我们输入指令rmmod,最终在系统内核中需要调用sys_delete_module进行实现。这个函数定义在module.c - kernel/module.c。
具体过程如下:先从用户空间传入需要卸载的模块名称,根据名称找到要卸载的模块指针, 确保我们要卸载的模块没有被其他模块依赖,然后找到模块本身的exit函数实现卸载。
1 | SYSCALL_DEFINE2(delete_module, const char __user *, name_user, |
- 第8行:确保有插入和删除模块不受限制的权利,并且模块没有被禁止插入或删除
- 第11行:获得模块名字。
- 第20行:找到要卸载的模块指针。
- 第26行:有依赖的模块,需要先卸载它们。
- 第41行:检查模块的退出函数。
- 第51行:停止机器,使参考计数不能移动并禁用模块。
- 第59行:告诉通知链module_notify_list上的监听者,模块状态 变为 MODULE_STATE_GOING。
- 第64行:等待所有异步函数调用完成。
三、内核是如何导出符号的
1. 模块层叠
符号是什么东西?我们为什么需要导出符号呢?内核模块如何导出符号呢?其他模块又是如何找到这些符号的呢?
实际上,符号指的就是内核模块中使用EXPORT_SYMBOL 声明的函数和变量。 当模块被装入内核后,它所导出的符号都会记录在公共内核符号表中。 在使用命令insmod加载模块后,模块就被连接到了内核,因此可以访问内核的共用符号。
通常情况下我们无需导出任何符号,但是如果其他模块想要从我们这个模块中获取某些方便的时候, 就可以考虑使用导出符号为其提供服务。这被称为模块层叠技术。 例如msdos文件系统依赖于由fat模块导出的符号;USB输入设备模块层叠在usbcore和input模块之上。 也就是我们可以将模块分为多个层,通过简化每一层来实现复杂的项目。
modprobe是一个处理层叠模块的工具,它的功能相当于多次使用insmod, 除了装入指定模块外还同时装入指定模块所依赖的其他模块。
2. 怎么导出符号?
当我们要导出模块的时候,可以使用下面的宏:
1 | EXPORT_SYMBOL(name) |
符号必须在模块文件的全局部分导出,不能在函数中使用,_GPL使得导出的模块只能被GPL许可的模块使用。 编译我们的模块时,这两个宏会被拓展为一个特殊变量的声明,存放在ELF文件中。 具体也就是存放在ELF文件的符号表中:
- st_name: 是符号名称在符号名称字符串表中的索引值
- st_value: 是符号所在的内存地址
- st_size: 是符号大小
- st_info: 是符号类型和绑定信息
- st_shndx: 表示符号所在section
当ELF的符号表被加载到内核后,会执行simplify_symbols来遍历整个ELF文件符号表。 根据st_shndx找到符号所在的section和st_value中符号在section中的偏移得到真正的内存地址。 并最终将符号内存地址,符号名称指针存储到内核符号表中。simplify_symbols()函数定义在module.c - kernel/module.c :
1 | /* Change all symbols so that st_value encodes the pointer directly. */ |
内核导出的符号表结构有两个字段,一个是符号在内存中的地址,一个是符号名称指针, 符号名称被放在了__ksymtab_strings这个section中, 以EXPORT_SYMBOL举例,符号会被放到名为__ksymtab的section中。 这个结构体我们要注意,它构成的表是导出符号表而不是通常意义上的符号表 。
我们来看一下这个kernel_symbol结构体,它定义在export.h - include/linux/export.h :
1 | struct kernel_symbol { |
其他的内核模块在寻找符号的时候会调用resolve_symbol_wait去内核和其他模块中通过符号名称寻址目标符号,resolve_symbol_wait会调用resolve_symbol,进而调用 find_symbol。 找到了符号之后,把符号的实际地址赋值给符号表 sym[i].st_value = ksym→value。find_symbol()函数定义在module.c - kernel/module.c:
1 | /* Find a symbol and return it, along with, (optional) crc and |
- 第16行:在each_symbol_section中,去查找了两个地方,一个是内核的导出符号表,即我们在将内核符号是如何导出的时候定义的全局变量,一个是遍历已经加载的内核模块,查找动作是在each_symbol_in_section中完成的。
- 第27行:导出符号标志.
至此符号查找完毕,最后将所有section借助ELF文件的重定向表进行重定向,就能使用该符号了。
参考资料