insmod過程
CSDN的一位不愿意透漏姓氏的底層搬磚人員不用了,后面看看慢慢把文章輸出到簡書上辛润。
模塊加載與卸載
眾所周知胆数,Linux下模塊加載可以用insmod
命令來完成怒允,卸載模塊通過rmmod
命令來完成砰嘁,比如下面,非常簡單:
那么第一個(gè)問題來了市殷,模塊是什么愕撰?
模塊文件
那么接下來,我們就可以通過file
命令來查看一下這個(gè).ko
文件到底是何方神圣醋寝,可以看到搞挣,此ko
文件是一個(gè)ELF 32-bit LSB relocatable
文件,ELF
格式的音羞,32-bit
的囱桨,LSB
小端,relocatable
可重定位文件嗅绰,此文件未stripped
舍肠。
可以看到,file
命令非常好用窘面,直接就知道了ko
模塊文件是ELF relocatable
文件翠语。
話說回來,編譯過程中產(chǎn)生的.o
文件也是ELF relocatable
文件财边,兩個(gè)看起來并沒有太大的差異肌括。
所以模塊文件為什么以.ko
結(jié)尾,就是因?yàn)槟K文件實(shí)際是一個(gè)和.o
文件差不多的中間文件酣难,但又加入了內(nèi)核kernel
特有的一些section
们童,所以特此命名為.ko
。(那么生成.ko
的過程鲸鹦,可以參考一下我的另一篇==《模塊生成過程》==)
話又說回來,ELF
文件是什么文件跷跪?
ELF文件
推薦大家去看
ELF.pdf
這個(gè)文檔馋嗜,我這里就選取部分可能用到的放這里。
首先吵瞻,我們看一下這個(gè)ELF
的全稱是什么:Executable and Linkable Format
葛菇,可執(zhí)行甘磨,可鏈接格式。
那么在模塊文件中眯停,這個(gè)文件的格式就是可鏈接格式济舆;對于普通的可執(zhí)行文件,就是可執(zhí)行格式莺债,比如下面這個(gè)busybox
:
在ELF
文檔中提到滋觉,Object File Format
有兩個(gè)視角,一個(gè)是鏈接過程的視角齐邦,一個(gè)是執(zhí)行過程的視角椎侠。
目標(biāo)文件在鏈接時(shí),就是左邊的鏈接視角措拇;可執(zhí)行程序執(zhí)行時(shí)我纪,是右邊的執(zhí)行視角。鏈接時(shí)需要Section header table
丐吓,執(zhí)行時(shí)則需要Program header table
浅悉。
我們可以通過arm-linux-gnueabi-readelf -S
命令來看一下鏈接過程的視角,即看一下這些section
(在==《模塊生成過程》==中有提到券犁,.ko
只是增加了一些section
术健,比如gnu.linkonce.this_module
,.modinfo
等):
但執(zhí)行過程的視角族操,由于都在內(nèi)存中苛坚,目前就暫時(shí)不管了。
那么接下來就是看一下代碼的過程色难。
busybox的insmod過程
我們這里還是用的busybox
做的根文件系統(tǒng)泼舱,insmod
命令也是由busybox
提供的,那么就直接在busybox
里面找代碼:調(diào)用bb_init_module()
枷莉。
bb_init_module()
也比較簡單娇昙,目前內(nèi)核的版本是linux-4.0
,不走下面的bb_init_module_24()
笤妙,且定義了系統(tǒng)調(diào)用finit_module()
冒掌,成果后直接返回。如果失敗了蹲盘,還有后面的init_module()
系統(tǒng)調(diào)用股毫,這里還需要先將模塊文件映射到內(nèi)存上,結(jié)束后再取消映射整個(gè)模塊文件召衔。
接下來就是要看內(nèi)核系統(tǒng)調(diào)用的時(shí)候了铃诬。
內(nèi)核系統(tǒng)調(diào)用
當(dāng)前用到系統(tǒng)調(diào)用是finit_module()
,系統(tǒng)調(diào)用里傳遞的fd
是在busybox
打開的,這里通過copy_module_from_fd()
將打開的模塊文件讀到申請的內(nèi)核內(nèi)存中趣席,并交由load_module()
處理兵志。
copy_module_from_fd()
這里只需要知道,load_info
結(jié)構(gòu)體的hdr
成員是指向內(nèi)核申請的內(nèi)存宣肚,即整個(gè)模塊文件的內(nèi)存起始地址想罕,len
則是這個(gè)模塊文件的長度。
這里我們用qemu+gdb
調(diào)試一下霉涨,并在load_module()
打上斷點(diǎn)(關(guān)于qemu和gdb網(wǎng)上的教程比較多去參考):
# 網(wǎng)橋配置 -- 僅供參考
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
# 連接設(shè)備
target remote localhost:1234
# 打斷點(diǎn)
b load_module
此時(shí)執(zhí)行模塊加載按价,gdb
那邊就觸發(fā)了斷點(diǎn):
可以看到大小一致:
且。內(nèi)存上的與編譯出來的.ko
一模一樣:
Part1 模型信息校驗(yàn)與模塊重新布局
module_sig_check()
需要開啟CONFIG_MODULE_SIG
才會(huì)起作用嵌纲,這里忽略俘枫。elf_header_check()
主要是檢查一下ELF
文件頭信息,別忘了模塊文件.ko
是ELF relocatable
文件逮走,所以需要檢查一下是否是正確的ELF
文件格式鸠蚪。layout_and_allocate()
獲取模塊的布局,重新申請一次內(nèi)存师溅,并對相關(guān)section
對內(nèi)容進(jìn)行修改茅信。add_unformed_module()
將模塊加入到內(nèi)核的模塊鏈表中。
Part1.1 ELF頭校驗(yàn)
elf_header_check()
是根據(jù)規(guī)范來校驗(yàn)整個(gè)ELF文件頭的信息墓臭,比如ELF Header
的Magic
蘸鲸,必須是 '0x7f' , 'E', 'L', 'F'
;ELF 的文件類型必須是 ET_REL
(模塊文件是可重定位文件窿锉,不必多說了吧)酌摇;不同架構(gòu)芯片的編譯器編譯時(shí)會(huì)把 e_machine
設(shè)置為對應(yīng)的類型,這里是 EM_ARM
嗡载。
)
內(nèi)存上的數(shù)據(jù)如下所示:
可以參觀一下一個(gè)標(biāo)準(zhǔn)的ELF Header
的內(nèi)容:
這里的e_type
必須是ET_REL
可重定位類型窑多,就不用多說了。
Part1.2 模塊內(nèi)存的申請與layout
看章節(jié)小標(biāo)題就大概可以知道洼滚,layout_and_allocate()
函數(shù)所做的工作有:1埂息,記錄模塊信息與重寫模塊數(shù)據(jù);2遥巴,檢查模塊license千康;3,取消percpu section
的內(nèi)存申請標(biāo)記铲掐,后續(xù)特殊處理拾弃;4,統(tǒng)計(jì)和預(yù)留需要layout數(shù)據(jù)空間摆霉;5砸彬,統(tǒng)計(jì)和預(yù)留模塊符號的空間颠毙;6,申請內(nèi)存并拷貝模塊數(shù)據(jù)到layout
砂碉;7,將記錄的模塊信息指向layout
的內(nèi)存刻两。
Part1.2.1 記錄模塊信息與重寫模塊數(shù)據(jù)
setup_load_info()
記錄節(jié)頭表位置增蹭,再根據(jù)節(jié)頭表找到節(jié)名字字符串表在內(nèi)存中位置。然后遍歷所有的節(jié)磅摹,保證相關(guān)節(jié)的偏移+長度不超過整個(gè)ELF文件的大小滋迈,把節(jié)的地址設(shè)在為當(dāng)前內(nèi)核內(nèi)存下的地址。并且要找到模塊特有的節(jié).gnu.linkonce.this_module
户誓,表明是模塊的信息饼灿。并記錄當(dāng)前模塊的.data..percpu
節(jié),方便后續(xù)對每CPU變特殊申請?zhí)幚怼?/p>
rewrite_section_headers()
函數(shù)將內(nèi)存里的每一個(gè)section
的sh_addr
節(jié)地址記錄為當(dāng)前內(nèi)核內(nèi)存上的地址帝美,并找到__versions
節(jié)和.modinfo
節(jié)碍彭,取消這兩個(gè)節(jié)的內(nèi)存申請標(biāo)記:
顯而易見,經(jīng)過rewrite_section_headers()
之后悼潭,內(nèi)存上的數(shù)據(jù)肯定發(fā)生了變化庇忌,用通用的方法,導(dǎo)出內(nèi)存上的數(shù)據(jù):dump memory hello.ko-rewirte-section.dump 0xa0ab0000 0xa0ab0000+31476
比較一波舰褪,除去第0個(gè)section
皆疹,后面每一個(gè)section
的sh_addr
已經(jīng)被修改為當(dāng)前內(nèi)存的地址:
回頭看find_sec()
也比較簡單,因?yàn)榍懊嬖?code>setup_load_info()中已經(jīng)獲取到每個(gè)section
到描述頭以及section
到名字字符串表的位置占拍,那么遍歷一下所有section
略就,比對名字即可:
查找當(dāng)前模塊的.data..percpu
節(jié)也比較簡單:
Part1.2.2 檢查模塊license
check_modinfo()
主要是檢查.modinfo
節(jié)里面的一些包括license
的信息,并記錄此模塊是否存在污染內(nèi)核的可能:
Part1.2.3 統(tǒng)計(jì)和預(yù)留需要layout數(shù)據(jù)空間
layout_sections()
函數(shù)開頭的注釋可以得知晃酒,這里其實(shí)是為后續(xù)執(zhí)行類似ld
的操作做準(zhǔn)備表牢,計(jì)算代碼、只讀數(shù)據(jù)掖疮、小數(shù)據(jù)的大小以及總的大小初茶。對于部分存在架構(gòu)額外內(nèi)存的或者地址對齊限制的,在get_offset()
函數(shù)中體現(xiàn)浊闪。這里主要是分兩個(gè)部分恼布,一開始先統(tǒng)計(jì)模塊的core_layout
部分,然后再統(tǒng)計(jì)init_layout
部分的數(shù)據(jù)搁宾。這里的masks
也注釋得很清楚折汞,0
是AX
可執(zhí)行的非小數(shù)據(jù),1
是A
只需申請內(nèi)存的不可寫的非小數(shù)據(jù)盖腿,3
是小數(shù)據(jù)申請內(nèi)存的爽待。
Part1.2.4 統(tǒng)計(jì)和預(yù)留模塊符號的空間
layout_symtab()
這里一開始在init layout
里面預(yù)留空間损同,用于存放模塊的符號表。緊接著遍歷模塊的符號鸟款,找出哪些屬于內(nèi)核的符號膏燃,然后在core layout
中預(yù)留出內(nèi)核符號的符號表和字符串表空間。最后在init layout
后面預(yù)留模塊字符串表的空間何什。
判斷是否是內(nèi)核的符號组哩,可以根據(jù)符號的st_shndx
是否是為定義的SHN_UNDEF
,未定義的符號肯定不是內(nèi)核符號处渣;符號的名字是否存在等:
Part1.2.5 申請內(nèi)存并拷貝模塊數(shù)據(jù)到layout
move_module()
主要的工作就是把模塊信息移到新申請的可執(zhí)行的內(nèi)存中去伶贰。layout_sections()
和layout_symtab()
統(tǒng)計(jì)了要申請的內(nèi)存大小,設(shè)定了布局罐栈,所以在move_module()
中申請完內(nèi)存后黍衙,就需要將相關(guān)的數(shù)據(jù)拷貝到對應(yīng)的布局中,數(shù)據(jù)來源自然是前面經(jīng)過修正后的模塊信息荠诬,修改完后琅翻,還需要再次修正每個(gè)section
在layout
內(nèi)存中的位置。
申請內(nèi)存的過程調(diào)用的是
vmalloc_exec()
申請?zhí)摂M連續(xù)的可執(zhí)行內(nèi)存浅妆,并更新模塊地址的邊界信息:
這里本來想dump
出當(dāng)前layout
的內(nèi)存數(shù)據(jù)的望迎,但由于內(nèi)核是-O2
編譯的,局部變量已經(jīng)被優(yōu)化了凌外,所以暫時(shí)沒有辦法dump
出內(nèi)存數(shù)據(jù)辩尊。
在Part2.7部分的apply_relocate()
斷點(diǎn)時(shí),發(fā)現(xiàn)mod
參數(shù)未被優(yōu)化康辑,那么就可以直接看一下在apply_relocate()
時(shí)的struct module
內(nèi)存數(shù)據(jù)摄欲,但會(huì)經(jīng)歷Part2.6部分的simplify_symbols()
修改符號地址的。
這里稍微看一下就行了疮薇,后續(xù)如果再研究這個(gè)內(nèi)存的布局的話胸墙,可以再細(xì)看一下:
Part1.3 將模塊加入到內(nèi)核的模塊記錄鏈表中
add_unformed_module()
主要就是將模塊加入到內(nèi)核的模塊鏈表中,保證模塊唯一按咒。
Part2 模塊的符號處理與重定位
percpu_modalloc()
對模塊里面的percpu
進(jìn)行特殊的內(nèi)存申請迟隅,這個(gè)percpu
變量比較特殊,在每個(gè)CPU上都存在一份励七,所以用特殊的申請內(nèi)存函數(shù)去處理智袭。module_unload_init()
為后續(xù)執(zhí)行rmmod
卸載命令時(shí)初始化一些引用變量以及鏈表等。find_module_sections()
找模塊里面的一些section
掠抬,比如參數(shù)的__param
節(jié)吼野,內(nèi)核符號表__ksymtab
節(jié),GPL
范圍協(xié)議的符號__ksymtab_gpl
節(jié)两波,ftrace
增加的_ftrace_events
節(jié)等待瞳步。check_module_license_and_versions()
主要是檢查模塊的license
是否存在污染內(nèi)核的可能闷哆。setup_modinfo()
是對sys
屬性進(jìn)行一些預(yù)先setup
處理。simplify_symbols()
要修復(fù)加載模塊的符號单起,使符號指向內(nèi)核正確的運(yùn)行地址抱怔。apply_relocations()
則是對.rel
節(jié)和.rela
節(jié)進(jìn)行重定位的一個(gè)過程。post_relocation()
對重定位后的percpu
變量重新賦值嘀倒,并將即將加載完成的模塊的符號加入到內(nèi)核模塊的符號鏈表中野蝇,如果成功加載此模塊且內(nèi)核配置了CONFIG_KALLSYMS
,那么在/proc/kallsyms
下可以看到此模塊的符號(我另有《kallsyms 內(nèi)核符號表》可以稍微瞄一眼)括儒。之后flush_module_icache()
執(zhí)行刷新模塊的init_layout
和core_layout
的cache。
Part2.1 申請percpu內(nèi)存
percpu_modalloc()
這里重新為percpu section
的數(shù)據(jù)申請內(nèi)存锐想,是補(bǔ)上Part1部分的去向了percpu
的SHF_ALLOC
:
Part2.2 為后續(xù)模塊卸載做準(zhǔn)備
module_unload_init()
這里主要是初始化了一個(gè)原子變量與兩個(gè)鏈表帮寻,鏈表分別用于存放哪些模塊依賴于我以及我依賴于哪些模塊的信息,主要是卸載掉時(shí)候會(huì)用赠摇。
Part2.3 找模塊的參數(shù)固逗、內(nèi)核符號等節(jié)
find_module_sections()
這里是找到模塊的一些節(jié)的地址,比如模塊參數(shù)__param
藕帜,內(nèi)核符號__ksymtab
烫罩,內(nèi)核GPL符號__ksymtab_gpl
等:
找節(jié)地址的函數(shù)如下,也比較簡單洽故,先獲取節(jié)的序號贝攒,之后再根據(jù)序號獲取地址:
Part2.4 再次檢查GPL協(xié)議?
這里暫時(shí)沒有看懂為什么时甚。
Part2.5 模塊sys參數(shù)setup
setup_modinfo()
遍歷modinfo_attrs
數(shù)組里面的屬性隘弊,如果有setup
回調(diào),嘗試從模塊的.modinfo
中獲取相關(guān)的setup
參數(shù):
好像也沒有看到相關(guān)的參數(shù)出現(xiàn)荒适,所以就此作罷梨熙。
Part2.6 模塊符號處理
simplify_symbols()
是對模塊的符號進(jìn)行處理,模塊節(jié)的獲取在前面已經(jīng)相當(dāng)?shù)亩嗔说段埽@里就不贅述了咽扇。對于SHN_COMMON
符號,可能是模塊內(nèi)部自己定義的一些普通符號陕壹,可以不做處理质欲;SHN_ABS
絕對地址,不需要做處理帐要;而SHN_UNDEF
為定義符號把敞,則需要在當(dāng)前內(nèi)核以及已經(jīng)加載的模塊中找到,否則模塊加載失斦セ荨(鏈接的過程需要解決為定義的符號奋早,否則鏈接失斒Ⅵ;當(dāng)然弱符號另說)耽装;如果有一些類似于軟連接的符號或者perpcu
的符號愤炸,要再進(jìn)行處理。
先看一下ELF的符號定義掉奄,可以看到代表符號的信息都是固定大小的:
resolve_symbol_wait()
這里是一個(gè)等待超時(shí)的過程规个,超時(shí)時(shí)間最大是30s,這里還用了一個(gè)逗號表達(dá)式姓建,差點(diǎn)沒認(rèn)出來诞仓。最主要的函數(shù)還是resolve_symbol()
查找符號的過程。
resolve_symbol()
主要是依賴與find_symbol()
查找符號速兔,找到符號對檢查相關(guān)信息并記錄引用墅拭。
find_symbol()
通過遍歷記錄內(nèi)核符號的地址以及已經(jīng)加載模塊的地址,嘗試找到此符號:
each_symbol_section()
則是遍歷內(nèi)核與已加載模塊的過程:
each_symbol_in_section()
則是遍歷具體地址調(diào)用回調(diào)的過程:
查找與比較函數(shù)是find_symbol_in_section()
涣狗,用的是二分查找谍婉,因?yàn)榫幾g時(shí)的符號已經(jīng)是排序過的了:
Part2.7 節(jié)數(shù)據(jù)的重定位
apply_relocations()
主要是對SHT_REL
和SHT_RELA
類型的節(jié)進(jìn)行重定位操作,通過sh_info
獲取到附加的重定位節(jié)數(shù)據(jù)镀钓,再根據(jù)要重定位的節(jié)的類型去調(diào)用具體的重定位函數(shù):
sh_info
類型穗熬,上面用到下面的SHT_REL
和SHT_RELA
。關(guān)于這兩個(gè)的詳情可以看elf
的文檔丁溅,我這里的理解是唤蔗,SHT_REL
是表明需要重定位,SHT_RELA
是需要重定位且附帶一個(gè)額外的地址偏移唧瘾。
先看一下這里Elf32_Rel
類型和Elf32_Rela
類型措译,
apply_relocate()
是對應(yīng)架構(gòu)下的重定位函數(shù),主要是根據(jù)架構(gòu)提供的aaelf32.pdf
文檔來對具體符號進(jìn)行重定位饰序,類型也比較多领虹,暫時(shí)就不細(xì)看了:
比如打印第一個(gè)需要重定位對符號,r_info
對值是0x162b
,低8bit
的值是0x2b
求豫,即43
塌衰,根據(jù)類型定義可以知道是#define R_ARM_MOVW_ABS_NC 43
:
再去查看aaelf32.pdf
文檔,可以看到這個(gè)就是arm
規(guī)定的重定位類型怎么算:
Part2.8 結(jié)構(gòu)相關(guān)的模塊初始化
前面find_module_sections()
里面有找__ex_table
節(jié)蝠嘉,然后賦給mod->extable
最疆,在post_relocation()
里面就對里面的數(shù)據(jù)進(jìn)行排序,由于當(dāng)前沒有這個(gè)mod->extable
成員蚤告,就略過了努酸。這里主要是拷貝了percpu
數(shù)據(jù)以及拷貝符號到core_layout
中。
add_kallsyms()
這里對符號進(jìn)行一個(gè)字符的字符賦值杜恰,更直接地表明符號的類型获诈;然后拷貝符號數(shù)據(jù)到core_layout
中仍源。
elf_type()
判斷符號的類型,賦值給一個(gè)字符值:
最后是架構(gòu)相關(guān)的初始化舔涎,主要是一個(gè)回溯信息的增加以及SMP初始化節(jié)等:
Part2.9 刷新layout的icache
flush_module_icache()
刷新core_layout
和init_layout
的icache
笼踩。
Part3 模塊init執(zhí)行
strndup_user()
復(fù)制一下模塊的參數(shù);dynamic_debug_setup()
需要開啟內(nèi)核CONFIG_DYNAMIC_DEBUG
才會(huì)啟用亡嫌,這里先略過嚎于;ftrace_module_init()
也是需要開啟相關(guān)的ftrace
配置,略過挟冠。complete_formation()
對模塊的內(nèi)存屬性進(jìn)行修改于购,比如.text
的讀+執(zhí)行,.data
的讀寫屬性知染。parse_args()
是解析模塊參數(shù)价涝,mod_sysfs_setup()
則是對sys
進(jìn)行創(chuàng)建的過程。free_copy()
釋放最初內(nèi)核申請的用于保存模塊原文件信息的內(nèi)存持舆。trace_module_load()
也是trace
相關(guān)的,略過伪窖。do_init_module()
就是最后模塊執(zhí)行init
函數(shù)的過程了逸寓。
Part3.1 模塊符號重名確認(rèn)、內(nèi)存屬性設(shè)置覆山、模塊上線通知
complete_formation()
主要是最后的模塊符號重名確認(rèn)竹伸、內(nèi)存屬性設(shè)置、以及通知鏈通知一下模塊上線了簇宽。
verify_export_symbols()
確認(rèn)一下模塊的符號是否會(huì)和內(nèi)核的符號或者已經(jīng)加載的模塊的符號重名勋篓。查找符號的函數(shù)看Part2.6的find_symbol()
。
Part3.2 模塊init的執(zhí)行
do_init_module()
主要是模塊init的執(zhí)行魏割,然后通知鏈通知一下模塊在線了譬嚣,前面是即將上線,這里是已經(jīng)上線了钞它。
do_one_initcall()
這里就是調(diào)用模塊的初始化函數(shù)了拜银,到這里模塊加載基本已經(jīng)完成了。
Part4 錯(cuò)誤處理
至于最后的這里遭垛,都是上面出錯(cuò)時(shí)的異常處理尼桶,就不多看了。