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
文件头信息,别忘了模块文件.ko
是ELF relocatable
文件,所以需要检查一下是否是正确的ELF
文件格式。layout_and_allocate()
获取模块的布局,重新申请一次内存,并对相关section
对内容进行修改。add_unformed_module()
将模块加入到内核的模块链表中。
Part1.1 ELF头校验
elf_header_check()
是根据规范来校验整个ELF文件头的信息,比如ELF Header
的Magic
,必须是 '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()
函数将内存里的每一个section
的sh_addr
节地址记录为当前内核内存上的地址,并找到__versions
节和.modinfo
节,取消这两个节的内存申请标记:
显而易见,经过rewrite_section_headers()
之后,内存上的数据肯定发生了变化,用通用的方法,导出内存上的数据:dump memory hello.ko-rewirte-section.dump 0xa0ab0000 0xa0ab0000+31476
比较一波,除去第0个section
,后面每一个section
的sh_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
也注释得很清楚,0
是AX
可执行的非小数据,1
是A
只需申请内存的不可写的非小数据,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()
中申请完内存后,就需要将相关的数据拷贝到对应的布局中,数据来源自然是前面经过修正后的模块信息,修改完后,还需要再次修正每个section
在layout
内存中的位置。
申请内存的过程调用的是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_layout
和core_layout
的cache。
Part2.1 申请percpu内存
percpu_modalloc()
这里重新为percpu section
的数据申请内存,是补上Part1部分的去向了percpu
的SHF_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_REL
和SHT_RELA
类型的节进行重定位操作,通过sh_info
获取到附加的重定位节数据,再根据要重定位的节的类型去调用具体的重定位函数:
sh_info
类型,上面用到下面的SHT_REL
和SHT_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_layout
和init_layout
的icache
。
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 错误处理
至于最后的这里,都是上面出错时的异常处理,就不多看了。