insmod过程

insmod过程

模块加载与卸载

众所周知,Linux下模块加载可以用insmod命令来完成,卸载模块通过rmmod命令来完成,比如下面,非常简单:

在这里插入图片描述

那么第一个问题来了,模块是什么?

模块文件

那么接下来,我们就可以通过file命令来查看一下这个.ko文件到底是何方神圣,可以看到,此ko文件是一个ELF 32-bit LSB relocatable文件,ELF格式的,32-bit的,LSB小端,relocatable可重定位文件,此文件未stripped

在这里插入图片描述

可以看到,file命令非常好用,直接就知道了ko模块文件是ELF relocatable文件。

话说回来,编译过程中产生的.o文件也是ELF relocatable文件,两个看起来并没有太大的差异。

在这里插入图片描述

所以模块文件为什么以.ko结尾,就是因为模块文件实际是一个和.o文件差不多的中间文件,但又加入了内核kernel特有的一些section,所以特此命名为.ko。(那么生成.ko的过程,可以参考一下我的另一篇==《模块生成过程》==)

话又说回来,ELF文件是什么文件?

ELF文件

推荐大家去看ELF.pdf这个文档,我这里就选取部分可能用到的放这里。

首先,我们看一下这个ELF的全称是什么:Executable and Linkable Format,可执行,可链接格式。

那么在模块文件中,这个文件的格式就是可链接格式;对于普通的可执行文件,就是可执行格式,比如下面这个busybox

在这里插入图片描述

ELF文档中提到,Object File Format有两个视角,一个是链接过程的视角,一个是执行过程的视角。

在这里插入图片描述

目标文件在链接时,就是左边的链接视角;可执行程序执行时,是右边的执行视角。链接时需要Section header table,执行时则需要Program header table

我们可以通过arm-linux-gnueabi-readelf -S命令来看一下链接过程的视角,即看一下这些section(在==《模块生成过程》==中有提到,.ko只是增加了一些section,比如gnu.linkonce.this_module.modinfo等):

在这里插入图片描述
在这里插入图片描述

但执行过程的视角,由于都在内存中,目前就暂时不管了。

那么接下来就是看一下代码的过程。

busybox的insmod过程

我们这里还是用的busybox做的根文件系统,insmod命令也是由busybox提供的,那么就直接在busybox里面找代码:调用bb_init_module()

在这里插入图片描述

bb_init_module()也比较简单,目前内核的版本是linux-4.0,不走下面的bb_init_module_24(),且定义了系统调用finit_module(),成果后直接返回。如果失败了,还有后面的init_module()系统调用,这里还需要先将模块文件映射到内存上,结束后再取消映射整个模块文件。

在这里插入图片描述
在这里插入图片描述

接下来就是要看内核系统调用的时候了。

内核系统调用

当前用到系统调用是finit_module(),系统调用里传递的fd是在busybox打开的,这里通过copy_module_from_fd()将打开的模块文件读到申请的内核内存中,并交由load_module()处理。

在这里插入图片描述

copy_module_from_fd()这里只需要知道,load_info结构体的hdr成员是指向内核申请的内存,即整个模块文件的内存起始地址,len则是这个模块文件的长度。

这里我们用qemu+gdb调试一下,并在load_module()打上断点(关于qemu和gdb网上的教程比较多去参考):

# 网桥配置 -- 仅供参考
sudo brctl addbr br0
sudo ifconfig enp0s6 down
sudo brctl addif br0 enp0s6
sudo brctl stp br0 off
sudo ifconfig br0 10.37.129.17 netmask 255.255.255.0 promisc up
sudo ifconfig enp0s6 10.37.129.10 netmask 255.255.255.0 promisc up
sudo tunctl -t tap0
sudo ifconfig tap0 10.37.129.18 netmask 255.255.255.0 promisc up
sudo brctl addif br0 tap0

# 起 qemu -- 仅供参考
sudo qemu-system-arm -M vexpress-a9 -m 512M -kernel arch/arm/boot/zImage -nographic -dtb arch/arm/boot/dts/vexpress-v2p-ca9.dtb -net nic -net tap,ifname=tap0,script=no -append "init=/linuxrc root=/dev/nfs rw nfsroot=10.37.129.17:/home/mj/work/rootfs/,proto=tcp,nolock ip=10.37.129.20:10.37.129.17:10.37.129.1:255.255.255.0::eth0:off console=ttyAMA0,38400n" -S -s

# 起 gdb
arm-linux-gnueabi-gdb vmlinux
# 连接设备 
target remote localhost:1234
# 打断点
b load_module

在这里插入图片描述

在这里插入图片描述

此时执行模块加载,gdb那边就触发了断点:

在这里插入图片描述

可以看到大小一致:
在这里插入图片描述

且。内存上的与编译出来的.ko一模一样:

在这里插入图片描述

Part1 模型信息校验与模块重新布局

module_sig_check()需要开启CONFIG_MODULE_SIG才会起作用,这里忽略。elf_header_check()主要是检查一下ELF文件头信息,别忘了模块文件.koELF relocatable文件,所以需要检查一下是否是正确的ELF文件格式。layout_and_allocate()获取模块的布局,重新申请一次内存,并对相关section对内容进行修改。add_unformed_module()将模块加入到内核的模块链表中。

在这里插入图片描述

Part1.1 ELF头校验

elf_header_check()是根据规范来校验整个ELF文件头的信息,比如ELF HeaderMagic,必须是 '0x7f' , 'E', 'L', 'F';ELF 的文件类型必须是 ET_REL (模块文件是可重定位文件,不必多说了吧);不同架构芯片的编译器编译时会把 e_machine 设置为对应的类型,这里是 EM_ARM

在这里插入图片描述

内存上的数据如下所示:

在这里插入图片描述

可以参观一下一个标准的ELF Header的内容:

在这里插入图片描述

这里的e_type必须是ET_REL可重定位类型,就不用多说了。

在这里插入图片描述

Part1.2 模块内存的申请与layout

看章节小标题就大概可以知道,layout_and_allocate()函数所做的工作有:1,记录模块信息与重写模块数据;2,检查模块license;3,取消percpu section的内存申请标记,后续特殊处理;4,统计和预留需要layout数据空间;5,统计和预留模块符号的空间;6,申请内存并拷贝模块数据到layout;7,将记录的模块信息指向layout的内存。

在这里插入图片描述

Part1.2.1 记录模块信息与重写模块数据

setup_load_info()记录节头表位置,再根据节头表找到节名字字符串表在内存中位置。然后遍历所有的节,保证相关节的偏移+长度不超过整个ELF文件的大小,把节的地址设在为当前内核内存下的地址。并且要找到模块特有的节.gnu.linkonce.this_module,表明是模块的信息。并记录当前模块的.data..percpu节,方便后续对每CPU变特殊申请处理。

在这里插入图片描述
在这里插入图片描述

rewrite_section_headers()函数将内存里的每一个sectionsh_addr节地址记录为当前内核内存上的地址,并找到__versions节和.modinfo节,取消这两个节的内存申请标记:

在这里插入图片描述

显而易见,经过rewrite_section_headers()之后,内存上的数据肯定发生了变化,用通用的方法,导出内存上的数据:dump memory hello.ko-rewirte-section.dump 0xa0ab0000 0xa0ab0000+31476

在这里插入图片描述

比较一波,除去第0个section,后面每一个sectionsh_addr已经被修改为当前内存的地址:

在这里插入图片描述

回头看find_sec()也比较简单,因为前面在setup_load_info()中已经获取到每个section到描述头以及section到名字字符串表的位置,那么遍历一下所有section,比对名字即可:

在这里插入图片描述

查找当前模块的.data..percpu节也比较简单:

在这里插入图片描述

Part1.2.2 检查模块license

check_modinfo()主要是检查.modinfo节里面的一些包括license的信息,并记录此模块是否存在污染内核的可能:

在这里插入图片描述

Part1.2.3 统计和预留需要layout数据空间

layout_sections()函数开头的注释可以得知,这里其实是为后续执行类似ld的操作做准备,计算代码、只读数据、小数据的大小以及总的大小。对于部分存在架构额外内存的或者地址对齐限制的,在get_offset()函数中体现。这里主要是分两个部分,一开始先统计模块的core_layout部分,然后再统计init_layout部分的数据。这里的masks也注释得很清楚,0AX可执行的非小数据,1A只需申请内存的不可写的非小数据,3是小数据申请内存的。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Part1.2.4 统计和预留模块符号的空间

layout_symtab()这里一开始在init layout里面预留空间,用于存放模块的符号表。紧接着遍历模块的符号,找出哪些属于内核的符号,然后在core layout中预留出内核符号的符号表和字符串表空间。最后在init layout后面预留模块字符串表的空间。

在这里插入图片描述

判断是否是内核的符号,可以根据符号的st_shndx是否是为定义的SHN_UNDEF,未定义的符号肯定不是内核符号;符号的名字是否存在等:

在这里插入图片描述

Part1.2.5 申请内存并拷贝模块数据到layout

move_module()主要的工作就是把模块信息移到新申请的可执行的内存中去。layout_sections()layout_symtab()统计了要申请的内存大小,设定了布局,所以在move_module()中申请完内存后,就需要将相关的数据拷贝到对应的布局中,数据来源自然是前面经过修正后的模块信息,修改完后,还需要再次修正每个sectionlayout内存中的位置。

在这里插入图片描述
在这里插入图片描述

申请内存的过程调用的是vmalloc_exec()申请虚拟连续的可执行内存,并更新模块地址的边界信息:

在这里插入图片描述

这里本来想dump出当前layout的内存数据的,但由于内核是-O2编译的,局部变量已经被优化了,所以暂时没有办法dump出内存数据。

在Part2.7部分的apply_relocate()断点时,发现mod参数未被优化,那么就可以直接看一下在apply_relocate()时的struct module内存数据,但会经历Part2.6部分的simplify_symbols()修改符号地址的。

在这里插入图片描述

这里稍微看一下就行了,后续如果再研究这个内存的布局的话,可以再细看一下:

在这里插入图片描述
在这里插入图片描述

Part1.3 将模块加入到内核的模块记录链表中

add_unformed_module()主要就是将模块加入到内核的模块链表中,保证模块唯一。

在这里插入图片描述

Part2 模块的符号处理与重定位

percpu_modalloc()对模块里面的percpu进行特殊的内存申请,这个percpu变量比较特殊,在每个CPU上都存在一份,所以用特殊的申请内存函数去处理。module_unload_init()为后续执行rmmod卸载命令时初始化一些引用变量以及链表等。find_module_sections()找模块里面的一些section,比如参数的__param节,内核符号表__ksymtab节,GPL范围协议的符号__ksymtab_gpl节,ftrace增加的_ftrace_events节等待。check_module_license_and_versions()主要是检查模块的license是否存在污染内核的可能。setup_modinfo()是对sys属性进行一些预先setup处理。simplify_symbols()要修复加载模块的符号,使符号指向内核正确的运行地址。apply_relocations()则是对.rel节和.rela节进行重定位的一个过程。post_relocation()对重定位后的percpu变量重新赋值,并将即将加载完成的模块的符号加入到内核模块的符号链表中,如果成功加载此模块且内核配置了CONFIG_KALLSYMS,那么在/proc/kallsyms下可以看到此模块的符号(我另有《kallsyms 内核符号表》可以稍微瞄一眼)。之后flush_module_icache()执行刷新模块的init_layoutcore_layout的cache。

在这里插入图片描述

Part2.1 申请percpu内存

percpu_modalloc()这里重新为percpu section的数据申请内存,是补上Part1部分的去向了percpuSHF_ALLOC

在这里插入图片描述

Part2.2 为后续模块卸载做准备

module_unload_init()这里主要是初始化了一个原子变量与两个链表,链表分别用于存放哪些模块依赖于我以及我依赖于哪些模块的信息,主要是卸载掉时候会用。

在这里插入图片描述
在这里插入图片描述

Part2.3 找模块的参数、内核符号等节

find_module_sections()这里是找到模块的一些节的地址,比如模块参数__param,内核符号__ksymtab,内核GPL符号__ksymtab_gpl等:

找节地址的函数如下,也比较简单,先获取节的序号,之后再根据序号获取地址:

在这里插入图片描述

Part2.4 再次检查GPL协议?

这里暂时没有看懂为什么。

在这里插入图片描述

Part2.5 模块sys参数setup

setup_modinfo()遍历modinfo_attrs数组里面的属性,如果有setup回调,尝试从模块的.modinfo中获取相关的setup参数:

在这里插入图片描述

好像也没有看到相关的参数出现,所以就此作罢。

在这里插入图片描述

Part2.6 模块符号处理

simplify_symbols()是对模块的符号进行处理,模块节的获取在前面已经相当的多了,这里就不赘述了。对于SHN_COMMON符号,可能是模块内部自己定义的一些普通符号,可以不做处理;SHN_ABS绝对地址,不需要做处理;而SHN_UNDEF为定义符号,则需要在当前内核以及已经加载的模块中找到,否则模块加载失败(链接的过程需要解决为定义的符号,否则链接失败;当然弱符号另说);如果有一些类似于软连接的符号或者perpcu的符号,要再进行处理。

先看一下ELF的符号定义,可以看到代表符号的信息都是固定大小的:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

resolve_symbol_wait()这里是一个等待超时的过程,超时时间最大是30s,这里还用了一个逗号表达式,差点没认出来。最主要的函数还是resolve_symbol()查找符号的过程。

在这里插入图片描述

resolve_symbol()主要是依赖与find_symbol()查找符号,找到符号对检查相关信息并记录引用。

在这里插入图片描述

find_symbol()通过遍历记录内核符号的地址以及已经加载模块的地址,尝试找到此符号:

在这里插入图片描述

each_symbol_section()则是遍历内核与已加载模块的过程:

在这里插入图片描述

each_symbol_in_section()则是遍历具体地址调用回调的过程:

在这里插入图片描述

查找与比较函数是find_symbol_in_section(),用的是二分查找,因为编译时的符号已经是排序过的了:

在这里插入图片描述

Part2.7 节数据的重定位

apply_relocations()主要是对SHT_RELSHT_RELA类型的节进行重定位操作,通过sh_info获取到附加的重定位节数据,再根据要重定位的节的类型去调用具体的重定位函数:

在这里插入图片描述

sh_info类型,上面用到下面的SHT_RELSHT_RELA。关于这两个的详情可以看elf的文档,我这里的理解是,SHT_REL是表明需要重定位,SHT_RELA是需要重定位且附带一个额外的地址偏移。

在这里插入图片描述

先看一下这里Elf32_Rel类型和Elf32_Rela类型,

在这里插入图片描述

apply_relocate()是对应架构下的重定位函数,主要是根据架构提供的aaelf32.pdf文档来对具体符号进行重定位,类型也比较多,暂时就不细看了:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

比如打印第一个需要重定位对符号,r_info对值是0x162b,低8bit的值是0x2b,即43,根据类型定义可以知道是#define R_ARM_MOVW_ABS_NC 43

在这里插入图片描述

再去查看aaelf32.pdf文档,可以看到这个就是arm规定的重定位类型怎么算:

在这里插入图片描述

Part2.8 结构相关的模块初始化

前面find_module_sections()里面有找__ex_table节,然后赋给mod->extable,在post_relocation()里面就对里面的数据进行排序,由于当前没有这个mod->extable成员,就略过了。这里主要是拷贝了percpu数据以及拷贝符号到core_layout中。

在这里插入图片描述

add_kallsyms()这里对符号进行一个字符的字符赋值,更直接地表明符号的类型;然后拷贝符号数据到core_layout中。

在这里插入图片描述

elf_type()判断符号的类型,赋值给一个字符值:

在这里插入图片描述

最后是架构相关的初始化,主要是一个回溯信息的增加以及SMP初始化节等:

在这里插入图片描述
在这里插入图片描述

Part2.9 刷新layout的icache

flush_module_icache()刷新core_layoutinit_layouticache

在这里插入图片描述

Part3 模块init执行

strndup_user()复制一下模块的参数;dynamic_debug_setup()需要开启内核CONFIG_DYNAMIC_DEBUG才会启用,这里先略过;ftrace_module_init()也是需要开启相关的ftrace配置,略过。complete_formation()对模块的内存属性进行修改,比如.text的读+执行,.data的读写属性。parse_args()是解析模块参数,mod_sysfs_setup()则是对sys进行创建的过程。free_copy()释放最初内核申请的用于保存模块原文件信息的内存。trace_module_load()也是trace相关的,略过。do_init_module()就是最后模块执行init函数的过程了。

在这里插入图片描述

Part3.1 模块符号重名确认、内存属性设置、模块上线通知

complete_formation()主要是最后的模块符号重名确认、内存属性设置、以及通知链通知一下模块上线了。

在这里插入图片描述

verify_export_symbols()确认一下模块的符号是否会和内核的符号或者已经加载的模块的符号重名。查找符号的函数看Part2.6的find_symbol()

在这里插入图片描述

Part3.2 模块init的执行

do_init_module()主要是模块init的执行,然后通知链通知一下模块在线了,前面是即将上线,这里是已经上线了。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

do_one_initcall()这里就是调用模块的初始化函数了,到这里模块加载基本已经完成了。

在这里插入图片描述

Part4 错误处理

至于最后的这里,都是上面出错时的异常处理,就不多看了。

在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值