一、ELF簡(jiǎn)介 現(xiàn)在PC平臺(tái)流行的可執(zhí)行文件格式主要是Windows下的PE(portable Executable)和Linux的ELF(Excutable Linkable Format)。 編譯器編譯源代碼后生成的文件叫做目標(biāo)文件,從目標(biāo)文件的結(jié)構(gòu)上講, 它是已經(jīng)編譯后的可執(zhí)行文件格式,只是還沒(méi)有鏈接的過(guò)程,其中可能有些符號(hào)或有些地址還沒(méi)有被調(diào)整。其實(shí)它本身就是按照可執(zhí)行文件格式存儲(chǔ)的,只是跟真正的可執(zhí)行文件在結(jié)構(gòu)上稍有不同。 簡(jiǎn)單的說(shuō),目標(biāo)文件就是源代碼編譯后但未進(jìn)行鏈接的那些中間文件(Winodws的.obj和Linux下的.o) ,它跟可執(zhí)行文件的內(nèi)容結(jié)構(gòu)很相似,所以一般跟可執(zhí)行文件格式一起采用一種格式存儲(chǔ)。從某種意義上,可以把目標(biāo)文件和可執(zhí)行文件看成是一種類型的文件。在Windows下,稱之為PE-COEF文件格式,在Linux下,稱之為ELF文件。 另外,不光是可執(zhí)行文件(.exe、elf)按照可執(zhí)行文件格式存儲(chǔ),動(dòng)態(tài)鏈接庫(kù).dll(windows)、.so(linux)以及靜態(tài)連接庫(kù)(.lib(windows)、.a(linux))文件都按可執(zhí)行文件格式存儲(chǔ)。 二、ELF結(jié)構(gòu) 2.1一般目標(biāo)文件將符號(hào)表、調(diào)試信息、字符串等一些鏈接時(shí)所須要的信息,以“節(jié)”(Section)的形式存儲(chǔ),有時(shí)候也叫“段”(Segment),通常不加區(qū)別。 -代碼段(Code Section):存放源代碼編譯后的機(jī)器指令
-數(shù)據(jù)段(Data Section) : 存放全局變量和局部靜態(tài)變量 - 數(shù)據(jù)段常見的名字:“.data”,".rodata",".comment",".bss" - 未初始化的全部變量和局部變量放在“.bss”里,僅僅作為預(yù)留位置, 沒(méi)有內(nèi)容在文件中也不占據(jù)空間 - 只讀數(shù)據(jù)段(.rodata) 和注釋信息段(.comment) 2.2在ELF文件中實(shí)際存在(占據(jù)空間)的也就是“.text”".data" ".rodata"和“.comment”這4個(gè)段
2.3 調(diào)試工具
三、段表 與ELF文件中段有關(guān)的重要結(jié)構(gòu)就是段表(Section HeaderTable),該表描述了ELF文件包含的所有段的信息,比如每個(gè)段的段名、長(zhǎng)度、偏移、權(quán)限等屬性。 readelf輸出的結(jié)果就是ELF文件段表的內(nèi)容。 Section Table的長(zhǎng)度為0x1b8也就是440個(gè)字節(jié),11個(gè)段描述符(10個(gè)有效+1個(gè)NULL),每個(gè)段描述符為40個(gè)字節(jié)。 四、動(dòng)態(tài)鏈接 4.1 為了解決靜態(tài)鏈接空間浪費(fèi)和更新困難的問(wèn)題,最簡(jiǎn)單的辦法是把程序的模塊相互分割開來(lái),形成獨(dú)立的文件,而不再將它們靜態(tài)地鏈接在一起。 簡(jiǎn)單的說(shuō),就是不對(duì)那些組成程序的目標(biāo)文件進(jìn)行鏈接,等到程序要運(yùn)行的以后才進(jìn)行鏈接。也就是說(shuō),把鏈接這個(gè)過(guò)程推遲到了運(yùn)行時(shí)再進(jìn)行,這就是動(dòng)態(tài)鏈接的基本思想。
4.2 libc簡(jiǎn)介 在Linux中,常用的C語(yǔ)言庫(kù)運(yùn)行庫(kù)glibc動(dòng)態(tài)鏈接形式保存在"/lib"目錄下,文件名叫做“l(fā)ibc.so”,整個(gè)系統(tǒng)只保留一份C語(yǔ)言庫(kù)的動(dòng)態(tài)鏈接文件“l(fā)ibc.so”,而所有的C語(yǔ)言編寫的、動(dòng)態(tài)鏈接的程序都可以在運(yùn)行時(shí)使用它。 當(dāng)程序被裝載時(shí),系統(tǒng)的動(dòng)態(tài)鏈接器 會(huì)將程序所需的動(dòng)態(tài)鏈接庫(kù)裝載到進(jìn)程的地址空間,并且將程序中所有未決議的符號(hào)綁定到相應(yīng)的動(dòng)態(tài)鏈接庫(kù)中,并進(jìn)行重定位工作。
動(dòng)態(tài)鏈接是把鏈接的過(guò)程從本來(lái)的程序被裝載前推遲到了裝載的時(shí)候。 這樣做雖然很靈活,但是性能受到影響。 這就引出了動(dòng)態(tài)鏈接的優(yōu)化方法。 4.3動(dòng)態(tài)鏈接程序運(yùn)行時(shí)地址空間分布
值得注意的是,動(dòng)態(tài)鏈接模塊裝載地址是從地址0x00000000開始的。我們知道這個(gè)地址是無(wú)效地址,而實(shí)際裝載地址是0xb7efc000。從中可以推斷,共享對(duì)象的最終裝載地址在編譯時(shí)是不確定的,而是動(dòng)態(tài)分配的。 原因是共享對(duì)象存在一些對(duì)象地址沖突的問(wèn)題,可能會(huì)包含一些絕對(duì)地址的引用 與此不同的是,可執(zhí)行文件往往是第一個(gè)被加載的文件,它可以選擇一個(gè)固定空間的地址,比如Linux下一般都是0x0804000,windows下一般都是0x0040000. 4.4裝載時(shí)重定位 基本思路是:在鏈接時(shí),對(duì)所有絕對(duì)地址的引用不作重定位,而把這一步推遲到裝載時(shí)再完成。一旦模塊裝載地址確定,即目標(biāo)地址確定,那么系統(tǒng)就對(duì)程序中所有的絕對(duì)地址引用進(jìn)行重定位。 假設(shè)函數(shù)foobar相對(duì)于代碼段的起始地址是0x100,當(dāng)模塊被裝載到0x10000000時(shí),我們假設(shè)代碼段位于模塊的嘴開始,即代碼段的裝載地址也是0x10000000,那么我們就可以確定foobar的地位為0x1000100。這時(shí)候,系統(tǒng)遍歷模塊中的重定位表,把所有對(duì)foobar的地址引用都重定位至0x10000100。 4.5地址無(wú)關(guān)代碼 裝載時(shí)重定位解決了動(dòng)態(tài)模塊中有絕對(duì)地址引用的問(wèn)題,但是又帶了指令部分無(wú)法在多個(gè)進(jìn)程間共享的問(wèn)題。 具體想法就是把程序模塊中共享的指令部分在裝載時(shí)不需要因?yàn)檠b載地址的改變而改變。把指令中那些需要被修改的部分分離出來(lái),跟數(shù)據(jù)部分放在一起,這樣指令部分就可以保持不變,而數(shù)據(jù)部分可以在每個(gè)進(jìn)程中擁有一個(gè)副本。這種方案就是目前的地址無(wú)關(guān)代碼(PIC)技術(shù) 具體方法:先分析模塊中各種類型的地址引用方式,把共享對(duì)象模塊中地址引用按照是否跨模塊分為兩類:模塊內(nèi)部引用和模塊外部引用。
4.6全局偏移表(GOT) 4.6.1對(duì)于類型三,我們需要用到代碼地址無(wú)關(guān)(PIC)技術(shù),基本的思想就是把跟地址相關(guān)部分放到數(shù)據(jù)段里面。 ELF的做法是在數(shù)據(jù)段里建立一個(gè)指向這些變量的指針數(shù)據(jù),稱為全局偏移表(GOT),當(dāng)代碼需要引用該全局變量時(shí),可以通過(guò)GOT中相對(duì)應(yīng)的項(xiàng)間接引用。
如圖,當(dāng)指令需要訪問(wèn)變量b時(shí),程序先會(huì)找到GOT,然后根據(jù)GOT中的變量所對(duì)應(yīng)的項(xiàng)找到變量的目標(biāo)地址。 由于GOT本身是放在數(shù)據(jù)段的,所以它可以在模塊裝載時(shí)被修改,并且每個(gè)進(jìn)程都可以有獨(dú)立的副本,相互不受影響。 可以使用objdump查看GOT的位置,以及GOT中變量的偏移
可以看到GOT在文件中的偏移是0x15d0
可以看到變量b在GOT中的偏移是8,相當(dāng)于是第三項(xiàng)(每4個(gè)字節(jié)一項(xiàng)) 4.6.2 對(duì)于模塊間調(diào)用和跳轉(zhuǎn),GOT中保存的是目標(biāo)函數(shù)的地址,可以借助GOT中的項(xiàng)進(jìn)行間接跳轉(zhuǎn)。 方法:先得到當(dāng)前指令地址PC,然后加上一個(gè)偏移地址得到函數(shù)地址在GOT中的偏移,然后一個(gè)間接調(diào)用
4.7 延遲綁定(PLT) 4.7.1 基本思想 動(dòng)態(tài)鏈接以犧牲一部份性能為代價(jià)。PLT是另一種優(yōu)化動(dòng)態(tài)鏈接性能的方法。
4.7.2 具體做法 -動(dòng)態(tài)鏈接器需要某個(gè)函數(shù)來(lái)完成地址綁定工作,這個(gè)函數(shù)至少要知道這個(gè)地址綁定發(fā)生在哪個(gè)模塊 哪個(gè)函數(shù),如lookup(module,function)。 在glibc中,lookup的函數(shù)真名叫做_dl_runtime_reolve() - 當(dāng)我們調(diào)用某個(gè)外部模塊時(shí),調(diào)用函數(shù)并不直接通過(guò)GOT跳轉(zhuǎn),而是通過(guò)一個(gè)叫做PLT項(xiàng)的結(jié)構(gòu)來(lái)進(jìn)行跳轉(zhuǎn),每個(gè)外部函數(shù)在PLT中都有一個(gè)相應(yīng)的項(xiàng),比如bar()函數(shù)在PLT中的項(xiàng)地址叫做bar@plt,具體實(shí)現(xiàn) bar@plt: jmp *(bar@GOT) push n push moduleID jump _dl_runtime_resolve 第一條指令是一條通過(guò)GOT間接跳轉(zhuǎn)指令,bar@GOT表示GOT中保存bar()這個(gè)函數(shù)的相應(yīng)項(xiàng)。 但是為了實(shí)現(xiàn)延遲綁定,連接器在初始化階段沒(méi)有將bar()地址填入GOT,而是將“push n”的地址填入到bar@GOT中,所以第一條指令的效果是跳轉(zhuǎn)到第二條指令,相當(dāng)于沒(méi)有進(jìn)行任何操作。第二條指令將n壓棧,接著將模塊ID壓棧,跳轉(zhuǎn)到_dl_runtime_resolve。實(shí)際上就是lookup(module,function)的調(diào)用。 _dl_runtime_resolve()在工作完成后將bar()真實(shí)地址填入bar@GOT中。 一旦bar()解析完畢,再次調(diào)用bar@plt時(shí),直接就能跳轉(zhuǎn)到bar()的真實(shí)地址。 4.7.3實(shí)際實(shí)現(xiàn) PLT的真正實(shí)現(xiàn)要更復(fù)雜些,ELF將GOT拆分成兩個(gè)表“.got”和".got.plt",前者用來(lái)保存全局變量引用的地址,后者用來(lái)保存函數(shù)引用的地址。 也就是說(shuō),所有對(duì)于外部函數(shù)的引用被分離出來(lái)放到了“.got.plt”中
- 參考資料:《程序員的自我修養(yǎng)》 |
|