# 裝載概述
# 裝載理論篇
## 創(chuàng)建虛擬地址空間
## 讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系
## 將CPU指令寄存器設(shè)置成可執(zhí)行文件入口坏挠,啟動(dòng)運(yùn)行
# Mach-O文件的裝載
# * Linux ELF文件的裝載(了解)
先附上源碼地址:結(jié)合 XNU 源碼(應(yīng)該不是最新的冲茸,且不怎么全,不過(guò)用來(lái)分析學(xué)習(xí)也差不多了)固灵,來(lái)看加載器的流程捅伤,效果更好。重要的兩個(gè)類(lèi):
-
bsd/kern/kern_exec.c
:進(jìn)程執(zhí)行的相關(guān)操作:線(xiàn)程創(chuàng)建巫玻、數(shù)據(jù)初始化等丛忆。 -
bsd/kern/mach_loader.c
:Mach-O文件解析加載相關(guān)。第二節(jié)中提到的Mach-O文件中的內(nèi)核加載器負(fù)責(zé)處理的load command 對(duì)應(yīng)的內(nèi)核中處理的函數(shù)都在該文件中仍秤,比如處理LC_SEGMET
命令的load_segment
函數(shù)熄诡、處理LC_LOAD_DYLINKER
命令的load_dylinker
函數(shù)(負(fù)責(zé)調(diào)用命令指定的動(dòng)態(tài)鏈接器)。
# 裝載概述
在鏈接完成之后诗力,應(yīng)用開(kāi)始運(yùn)行之前粮彤,有一段裝載過(guò)程,我們都知道程序執(zhí)行時(shí)所需要的指令和數(shù)據(jù)必須在內(nèi)存中才能夠被正常運(yùn)行。
最簡(jiǎn)單的辦法就是將程序運(yùn)行所需要的指令和數(shù)據(jù)全都裝入內(nèi)存中导坟,這樣程序就可以順利運(yùn)行屿良,這就是最簡(jiǎn)單的靜態(tài)裝入
的辦法。
但是很多情況下程序所需要的內(nèi)存數(shù)量大于物理內(nèi)存的數(shù)量惫周,當(dāng)內(nèi)存的數(shù)量不夠時(shí)尘惧,根本的解決辦法就是添加內(nèi)存。相對(duì)于磁盤(pán)來(lái)說(shuō)递递,內(nèi)存是昂貴且稀有的喷橙,這種情況自計(jì)算機(jī)磁盤(pán)誕生以來(lái)一直如此。所以人們想盡各種辦法登舞,希望能夠在不添加內(nèi)存的情況下讓更多的程序運(yùn)行起來(lái)贰逾,盡可能有效地利用內(nèi)存。后來(lái)研究發(fā)現(xiàn)菠秒,程序運(yùn)行時(shí)是有局部性原理
的疙剑,所以我們可以將程序最常用的部分駐留在內(nèi)存中,而將一些不太常用的數(shù)據(jù)存放在磁盤(pán)里面践叠,這就是動(dòng)態(tài)裝入
的基本原理言缤。(這也是虛擬地址空間
機(jī)制要解決的問(wèn)題,這里不再贅述禁灼,大學(xué)都學(xué)過(guò))
覆蓋裝入(Overlay)和頁(yè)映射(Paging)是兩種很典型的動(dòng)態(tài)裝載方法管挟,它們所采用的思想都差不多,原則上都是利用了程序的局部性原理弄捕。動(dòng)態(tài)裝入的思想是程序用到哪個(gè)模塊,就將哪個(gè)模塊裝入內(nèi)存守谓,如果不用就暫時(shí)不裝入皮璧,存放在磁盤(pán)中。
# 裝載理論篇
在虛擬存儲(chǔ)中分飞,現(xiàn)代的硬件MMU都提供地址轉(zhuǎn)換的功能悴务。有了硬件的地址轉(zhuǎn)換和頁(yè)映射機(jī)制,操作系統(tǒng)動(dòng)態(tài)加載可執(zhí)行文件的方式跟靜態(tài)加載有了很大的區(qū)別譬猫。
事實(shí)上讯檐,從操作系統(tǒng)的角度來(lái)看,一個(gè)進(jìn)程最關(guān)鍵的特征是它擁有獨(dú)立的虛擬地址空間染服,這使得它有別于其他進(jìn)程别洪。很多時(shí)候一個(gè)程序被執(zhí)行同時(shí)都伴隨著一個(gè)新的進(jìn)程的創(chuàng)建,那么我們就來(lái)看看這種最通常的情形:創(chuàng)建一個(gè)進(jìn)程柳刮,然后裝載相應(yīng)的可執(zhí)行文件并且執(zhí)行挖垛。在有虛擬存儲(chǔ)的情況下痒钝,上述過(guò)程最開(kāi)始只需要做三件事情:
- 創(chuàng)建一個(gè)獨(dú)立的虛擬地址空間。
- 讀取可執(zhí)行文件頭痢毒,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系送矩。
- 將CPU的指令寄存器設(shè)置成可執(zhí)行文件的入口地址,啟動(dòng)運(yùn)行哪替。
首先是創(chuàng)建虛擬地址空間栋荸。一個(gè)虛擬空間由一組頁(yè)映射函數(shù)
將虛擬空間的各個(gè)頁(yè)
映射至相應(yīng)的物理空間
,所以創(chuàng)建一個(gè)虛擬空間實(shí)際上并不是創(chuàng)建空間而是創(chuàng)建映射函數(shù)所需要的相應(yīng)的數(shù)據(jù)結(jié)構(gòu)
凭舶,在i386 的Linux下晌块,創(chuàng)建虛擬地址空間實(shí)際上只是分配一個(gè)頁(yè)目錄(Page Directory)就可以了,甚至不設(shè)置頁(yè)映射關(guān)系帅霜,這些映射關(guān)系等到后面程序發(fā)生頁(yè)錯(cuò)誤的時(shí)候再進(jìn)行設(shè)置匆背。
讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系身冀。上面那一步的頁(yè)映射關(guān)系函數(shù)是虛擬空間到物理內(nèi)存的映射關(guān)系
钝尸,這一步所做的是虛擬空間與可執(zhí)行文件的映射關(guān)系
。我們知道闽铐,當(dāng)程序執(zhí)行發(fā)生頁(yè)錯(cuò)誤時(shí)蝶怔,操作系統(tǒng)將從物理內(nèi)存中分配一個(gè)物理頁(yè)奶浦,然后將該“缺頁(yè)”從磁盤(pán)中讀取到內(nèi)存中兄墅,再設(shè)置缺頁(yè)的虛擬頁(yè)和物理頁(yè)的映射關(guān)系,這樣程序才得以正常運(yùn)行澳叉。
但是很明顯的一點(diǎn)是隙咸,當(dāng)操作系統(tǒng)捕獲到缺頁(yè)錯(cuò)誤時(shí),它應(yīng)知道程序當(dāng)前所需要的頁(yè)在可執(zhí)行文件中的哪一個(gè)位置成洗。這就是虛擬空間與可執(zhí)行文件之間的映射關(guān)系五督。從某種角度來(lái)看,這一步是整個(gè)裝載過(guò)程中最重要的一步瓶殃,也是傳統(tǒng)意義上“裝載”的過(guò)程充包。
由于可執(zhí)行文件在裝載時(shí)實(shí)際上是被映射的虛擬空間,所以可執(zhí)行文件很多時(shí)候又被叫做映像文件(Image)遥椿。
很明顯基矮,這種映射關(guān)系只是保存在操作系統(tǒng)內(nèi)部的一個(gè)數(shù)據(jù)結(jié)構(gòu)。Linux中將進(jìn)程虛擬空間中的一個(gè)段叫做虛擬內(nèi)存區(qū)域(VMA, Virtual Memory Area)冠场;在Windows中將這個(gè)叫做虛擬段(Virtual Section)家浇,其實(shí)它們都是同一個(gè)概念。
VMA是一個(gè)很重要的概念碴裙,它對(duì)于我們理解程序的裝載執(zhí)行和操作系統(tǒng)如何管理進(jìn)程的虛擬空間有非常重要的幫助钢悲。
操作系統(tǒng)在內(nèi)部保存這種結(jié)構(gòu)点额,很明顯是因?yàn)楫?dāng)程序執(zhí)行發(fā)生段錯(cuò)誤時(shí),它可以通過(guò)查找這樣的一個(gè)數(shù)據(jù)結(jié)構(gòu)來(lái)定位錯(cuò)誤頁(yè)在可執(zhí)行文件中的位置
莺琳。
將CPU指令寄存器設(shè)置成可執(zhí)行文件入口还棱,啟動(dòng)運(yùn)行。第三步其實(shí)也是最簡(jiǎn)單的一步芦昔,操作系統(tǒng)通過(guò)設(shè)置CPU的指令寄存器將控制權(quán)轉(zhuǎn)交給進(jìn)程诱贿,由此進(jìn)程開(kāi)始執(zhí)行。這一步看似簡(jiǎn)單咕缎,實(shí)際上在操作系統(tǒng)層面上比較復(fù)雜珠十,它涉及內(nèi)核堆棧和用戶(hù)堆棧的切換、CPU運(yùn)行權(quán)限的切換凭豪。不過(guò)從進(jìn)程的角度看這一步可以簡(jiǎn)單地認(rèn)為操作系統(tǒng)執(zhí)行了一條跳轉(zhuǎn)指令焙蹭,直接跳轉(zhuǎn)到可執(zhí)行文件的入口地址(通常是text區(qū)的地址)。
- ELF文件頭中嫂伞,有
e_entry
字段保存入口地址 - Mach-O文件中的
LC_MAIN
加載指令作用就是設(shè)置程序主程序的入口點(diǎn)地址和棧大小)
# Mach-O文件的裝載
(二) Mach-O 文件結(jié)構(gòu) 介紹 Mach Heade
中的 Load Command
加載命令孔厉,結(jié)合其用途,就可以簡(jiǎn)單看出可執(zhí)行文件的裝載流程:
首先帖努,是由內(nèi)核加載器(定義在
bsd/kern/mach_loader.c
文件中)來(lái)處理一些需要由內(nèi)核加載器直接使用的加載命令撰豺。內(nèi)核的部分(內(nèi)核加載器)負(fù)責(zé)新進(jìn)程的基本設(shè)置——分配虛擬內(nèi)存,創(chuàng)建主線(xiàn)程拼余,以及處理任何可能的代碼簽名/加密的工作污桦。(這也是本篇內(nèi)容主要講的)接著,對(duì)于需要?jiǎng)討B(tài)鏈接(使用了動(dòng)態(tài)庫(kù))的可執(zhí)行文件(大部分可執(zhí)行文件都是動(dòng)態(tài)鏈接的)來(lái)說(shuō)匙监,控制權(quán)會(huì)轉(zhuǎn)交給鏈接器凡橱,鏈接器進(jìn)而接著處理文件頭中的其他加載命令。真正的庫(kù)加載和符號(hào)解析的工作都是通過(guò)
LC_LOAD_DY LINKER
命令指定的動(dòng)態(tài)鏈接器
在用戶(hù)態(tài)完成的亭姥。(下一篇文章再細(xì)講dyld
及動(dòng)態(tài)鏈接
)
下面通過(guò)代碼來(lái)看一下具體的過(guò)程稼钩。下面通過(guò)一個(gè)調(diào)用棧圖來(lái)說(shuō)明, 這里面每個(gè)方法都做了很多事情达罗,這里只注釋了到_dyld_start的關(guān)鍵操作坝撑,很簡(jiǎn)略。有興趣可以詳細(xì)看源碼kern_exec.c
粮揉、mach_loader.c
▼ execve // 用戶(hù)點(diǎn)擊了app巡李,用戶(hù)態(tài)會(huì)發(fā)送一個(gè)系統(tǒng)調(diào)用 execve 到內(nèi)核
▼ __mac_execve // 主要是為加載鏡像進(jìn)行數(shù)據(jù)的初始化,以及資源相關(guān)的操作滔蝉,以及創(chuàng)建線(xiàn)程
▼ exec_activate_image // 拷貝可執(zhí)行文件到內(nèi)存中击儡,并根據(jù)不同的可執(zhí)行文件類(lèi)型選擇不同的加載函數(shù),所有的鏡像的加載要么終止在一個(gè)錯(cuò)誤上蝠引,要么最終完成加載鏡像阳谍。
// 在 encapsulated_binary 這一步會(huì)根據(jù)image的類(lèi)型選擇imgact的方法
/*
* 該方法為Mach-o Binary對(duì)應(yīng)的執(zhí)行方法蛀柴;
* 如果image類(lèi)型為Fat Binary,對(duì)應(yīng)方法為exec_fat_imgact矫夯;
* 如果image類(lèi)型為Interpreter Script鸽疾,對(duì)應(yīng)方法為exec_shell_imgact
*/
▼ exec_mach_imgact
?? // 首先對(duì)Mach-O做檢測(cè),會(huì)檢測(cè)Mach-O頭部训貌,解析其架構(gòu)制肮、檢查imgp等內(nèi)容,判斷魔數(shù)递沪、cputype豺鼻、cpusubtype等信息。如果image無(wú)效款慨,會(huì)直接觸發(fā)assert(exec_failure_reason == OS_REASON_NULL); 退出儒飒。
// 拒絕接受Dylib和Bundle這樣的文件,這些文件會(huì)由dyld負(fù)責(zé)加載檩奠。然后把Mach-O映射到內(nèi)存中去桩了,調(diào)用load_machfile()
▼ load_machfile
?? // load_machfile會(huì)加載Mach-O中的各種load monmand命令。在其內(nèi)部會(huì)禁止數(shù)據(jù)段執(zhí)行埠戳,防止溢出漏洞攻擊井誉,還會(huì)設(shè)置地址空間布局隨機(jī)化(ASLR),還有一些映射的調(diào)整整胃。
// 真正負(fù)責(zé)對(duì)加載命令解析的是parse_machfile()
▼ parse_machfile //解析主二進(jìn)制macho
?? /*
* 首先颗圣,對(duì)image頭中的filetype進(jìn)行分析,可執(zhí)行文件MH_EXECUTE不允許被二次加載(depth = 1)爪模;動(dòng)態(tài)鏈接編輯器MH_DYLINKER必須是被可執(zhí)行文件加載的(depth = 2)
* 然后欠啤,循環(huán)遍歷所有的load command荚藻,分別調(diào)用對(duì)應(yīng)的內(nèi)核函數(shù)進(jìn)行處理
* LC_SEGMET:load_segment函數(shù):對(duì)于每一個(gè)段屋灌,將文件中相應(yīng)的內(nèi)容加載到內(nèi)存中:從偏移量為 fileoff 處加載 filesize 字節(jié)到虛擬內(nèi)存地址 vmaddr 處的 vmsize 字節(jié)。每一個(gè)段的頁(yè)面都根據(jù) initprot 進(jìn)行初始化应狱,initprot 指定了如何通過(guò)讀/寫(xiě)/執(zhí)行位初始化頁(yè)面的保護(hù)級(jí)別共郭。
* LC_UNIXTHREAD:load_unixthread函數(shù),見(jiàn)下文
* LC_MAIN:load_main函數(shù)
* LC_LOAD_DYLINKER:獲取動(dòng)態(tài)鏈接器相關(guān)的信息疾呻,下面load_dylinker會(huì)根據(jù)信息除嘹,啟動(dòng)動(dòng)態(tài)鏈接器
* LC_CODE_SIGNATURE:load_code_signature函數(shù),進(jìn)行驗(yàn)證岸蜗,如果無(wú)效會(huì)退出尉咕。理論部分,回見(jiàn)第二節(jié)load_command `LC_CODE_SIGNATURE `部分璃岳。
* 其他的不再多說(shuō)年缎,有興趣可以自己看源碼
*/
▼ load_dylinker // 解析完 macho后悔捶,根據(jù)macho中的 LC_LOAD_DYLINKER 這個(gè)LoadCommand來(lái)啟動(dòng)這個(gè)二進(jìn)制的加載器,即 /usr/bin/dyld
▼ parse_machfile // 開(kāi)始解析 dyld 這個(gè)mach-o文件
▼ load_unixthread // 解析 dyld 的 LC_UNIXTHREAD 命令单芜,這個(gè)過(guò)程中會(huì)解析出entry_point
▼ load_threadentry // 獲取入口地址
?? thread_entrypoint // 里面只有i386和x86架構(gòu)的蜕该,沒(méi)有arm的,但是原理是一樣的
?? //上一步獲取到地址后洲鸠,會(huì)再加上slide堂淡,ASLR偏移,到此扒腕,就獲取到了dyld的入口地址绢淀,也就是 _dyld_start 函數(shù)的地址
▼ activate_exec_state
?? thread_setentrypoint // 設(shè)置entry_point。直接把entry_point地址寫(xiě)入到用戶(hù)態(tài)的寄存器里面了瘾腰。
//這一步開(kāi)始更啄,_dyld_start就真正開(kāi)始執(zhí)行了。
▼ dyld
▼ __dyld_start // 源碼在dyldStartup.s這個(gè)文件居灯,用匯編實(shí)現(xiàn)
▼ dyldbootstrap::start()
▼ dyld::_main()
▼ //函數(shù)的最后祭务,調(diào)用 getEntryFromLC_MAIN,從 Load Command 讀取LC_MAIN入口怪嫌,如果沒(méi)有LC_MAIN入口义锥,就讀取LC_UNIXTHREAD,然后跳到主程序的入口處執(zhí)行
▼ 這是下篇內(nèi)容
# * Linux ELF文件的裝載(了解)
首先在用戶(hù)層面岩灭,bash進(jìn)程會(huì)調(diào)用fork()系統(tǒng)調(diào)用創(chuàng)建一個(gè)新的進(jìn)程拌倍,然后新的進(jìn)程調(diào)用 execve()
系統(tǒng)調(diào)用執(zhí)行指定的ELF文件,原先的bash進(jìn)程繼續(xù)返回等待剛才啟動(dòng)的新進(jìn)程結(jié)束噪径,然后繼續(xù)等待用戶(hù)輸入命令柱恤。 execve() 系統(tǒng)調(diào)用被定義在unistd.h,它的原型如下:
/*
* 三個(gè)參數(shù)分別是被執(zhí)行的程序文件名找爱、執(zhí)行參數(shù)和環(huán)境變量梗顺。
*/
int execve(const char *filename, char *const argv[], char *const envp[]);
Glibc對(duì)該系統(tǒng)調(diào)用進(jìn)行了包裝,提供了 execl()车摄、execlp()寺谤、execle()、execv()吮播、execvp()等5個(gè)不同形式的exec系列API变屁,它們只是在調(diào)用的參數(shù)形式上有所區(qū)別,但最終都會(huì)調(diào)用到 execve() 這個(gè)系統(tǒng)中意狠。
在進(jìn)入 execve() 系統(tǒng)調(diào)用之后粟关,Linux內(nèi)核就開(kāi)始進(jìn)行真正的裝載工作。
sys_execve()
环戈,在內(nèi)核中闷板,該函數(shù)是execve()系統(tǒng)調(diào)用相應(yīng)的入口获列,定義在arch\i386\kernel\Process.c。 該函數(shù)進(jìn)行一些參數(shù)的檢查復(fù)制之后蛔垢,調(diào)用 do_execve()击孩。-
do_execve()
,該函數(shù)會(huì)首先查找被執(zhí)行的文件鹏漆,如果找到文件巩梢,則讀取文件的前128個(gè)字節(jié)。目的是判斷文件的格式艺玲,每種可執(zhí)行文件的格式的開(kāi)頭幾個(gè)字節(jié)都是很特殊的括蝠,特別是開(kāi)頭4個(gè)字節(jié),常常被稱(chēng)做魔數(shù)
(Magic Number)饭聚,通過(guò)對(duì)魔數(shù)的判斷可以確定文件的格式和類(lèi)型忌警。比如:- ELF的可執(zhí)行文件格式的頭4個(gè)字節(jié)為0x7F、’e’秒梳、’l’法绵、’f’;
- Java的可執(zhí)行文件格式的頭4個(gè)字節(jié)為’c’酪碘、’a’朋譬、’f’、’e’兴垦;
- 如果被執(zhí)行的是Shell腳本或perl徙赢、python等這種解釋型語(yǔ)言的腳本,那么它的第一行往往是 “#!/bin/sh” 或 “#!/usr/bin/perl” 或 “#!/usr/bin/python” 探越,這時(shí)候前兩個(gè)字節(jié)
'#'
和'!'
就構(gòu)成了魔數(shù)狡赐,系統(tǒng)一旦判斷到這兩個(gè)字節(jié),就對(duì)后面的字符串進(jìn)行解析钦幔,以確定具體的解釋程序的路徑枕屉。
當(dāng)do_execve()讀取了這128個(gè)字節(jié)的文件頭部之后,然后調(diào)用search_binary_handle()节槐。
-
search_binary_handle()
搀庶,該函數(shù)會(huì)去搜索和匹配合適的可執(zhí)行文件裝載處理過(guò)程拐纱。Linux中所有被支持的可執(zhí)行文件格式都有相應(yīng)的裝載處理過(guò)程铜异,此函數(shù)會(huì)通過(guò)判斷文件頭部的魔數(shù)確定文件的格式,并且調(diào)用相應(yīng)的裝載處理過(guò)程秸架。比如:- ELF可執(zhí)行文件的裝載處理過(guò)程叫做 load_elf_binary()揍庄;
- a.out可執(zhí)行文件的裝載處理過(guò)程叫做 load_aout_binary();
- 裝載可執(zhí)行腳本程序的處理過(guò)程叫做 load_script()东抹。
-
load_elf_binary()
蚂子,這個(gè)函數(shù)被定義在fs/Binfmt_elf.c沃测,代碼比較長(zhǎng),它的主要步驟是:- 檢查ELF可執(zhí)行文件格式的有效性食茎,比如魔數(shù)蒂破、程序頭表中段(Segment)的數(shù)量。
- 尋找動(dòng)態(tài)鏈接的“.interp”段别渔,設(shè)置動(dòng)態(tài)鏈接器路徑附迷。
- 根據(jù)ELF可執(zhí)行文件的程序頭表的描述,對(duì)ELF文件進(jìn)行映射哎媚,比如代碼喇伯、數(shù)據(jù)、只讀數(shù)據(jù)拨与。
- 初始化ELF進(jìn)程環(huán)境稻据,比如進(jìn)程啟動(dòng)時(shí)EDX寄存器的地址應(yīng)該是 DT_FINI 的地址(動(dòng)態(tài)鏈接相關(guān))。
- 將系統(tǒng)調(diào)用的返回地址修改成ELF可執(zhí)行文件的入口點(diǎn)买喧,這個(gè)入口點(diǎn)取決于程序的鏈接方式捻悯,對(duì)于靜態(tài)鏈接的ELF可執(zhí)行文件,這個(gè)程序入口就是ELF文件的文件頭中
e_entry
所指的地址淤毛;對(duì)于動(dòng)態(tài)鏈接的ELF可執(zhí)行文件秋度,程序入口點(diǎn)是動(dòng)態(tài)鏈接器。
當(dāng) load_elf_binary() 執(zhí)行完畢钱床,返回至 do_execve() 再返回至 sys_execve() 時(shí)荚斯, 上面的第5步中已經(jīng)把系統(tǒng)調(diào)用的返回地址改成了被裝載的ELF程序(或動(dòng)態(tài)鏈接器)的入口地址了。所以當(dāng) sys_execve()
系統(tǒng)調(diào)用從內(nèi)核態(tài)返回到用戶(hù)態(tài)時(shí)查牌,EIP 寄存器直接跳轉(zhuǎn)到了ELF程序的入口地址事期,于是新的程序開(kāi)始執(zhí)行,ELF可執(zhí)行文件裝載完成纸颜。