Elf
機(jī)器執(zhí)行的是機(jī)器指令,而機(jī)器指令就是一堆二進(jìn)制的數(shù)字。高級(jí)語言編寫的程序之所以可以在不同的機(jī)器上移植就因?yàn)橛袨椴煌瑱C(jī)器設(shè)計(jì)的編譯器的存在。高級(jí)語言的編譯器就是把高級(jí)語言寫的程序轉(zhuǎn)換成某個(gè)機(jī)器能直接執(zhí)行的二進(jìn)制代碼。以上的知識(shí)在我們學(xué)習(xí)CS(Computer Science)的初期,老師都會(huì)這么對(duì)我們講。但是我就產(chǎn)生疑問了:既然機(jī)器都是執(zhí)行的二進(jìn)制代碼,那么是不是說只要硬件相互兼容,不同操作系統(tǒng)下的可執(zhí)行文件可以互相運(yùn)行呢?答案肯定是不行。這就要談到可執(zhí)行文件的格式問題。
每個(gè)操作系統(tǒng)都會(huì)有自己的可執(zhí)行文件的格式,比如以前的Unix?是用a.out格式的,現(xiàn)代的Unix?類系統(tǒng)使用elf格式, WindowsNT?是使用基于COFF格式的可執(zhí)行文件。那么最簡單的格式應(yīng)該是DOS的可執(zhí)行格式,嚴(yán)格來說DOS的可執(zhí)行文件沒有什么格式可言,就是把二進(jìn)制代碼安順序放在文件里,運(yùn)行時(shí)DOS操作系統(tǒng)就把所有控制計(jì)算機(jī)的權(quán)力都給了這個(gè)程序。這種方式的不足之處是顯而易見的,所以現(xiàn)代的操作系統(tǒng)都有一種更好的方式來定義可執(zhí)行文件的格式。一種常見的方法就是為可執(zhí)行文件分段,一般來說把程序指令的內(nèi)容放在.text段中,把程序中的數(shù)據(jù)內(nèi)容放在.data段中,把程序中未初始化的數(shù)據(jù)放在.bss段中。這種做法的好處有很多,可以讓操作系統(tǒng)內(nèi)核來檢查程序防止有嚴(yán)重錯(cuò)誤的程序破壞整個(gè)運(yùn)行環(huán)境。比如:某個(gè)程序想要修改.text段中的內(nèi)容,那么操作系統(tǒng)就會(huì)認(rèn)為這段程序有誤而立即終止它的運(yùn)行,因?yàn)橄到y(tǒng)會(huì)把.text段的內(nèi)存標(biāo)記為只讀。在.bss段中的數(shù)據(jù)還沒有初始化,就沒有必要在可執(zhí)行文件中浪費(fèi)儲(chǔ)存空間。在.bss中只是表明某個(gè)變量要使用多少的內(nèi)存空間,等到程序加載的時(shí)候在由內(nèi)核把這段未初始化的內(nèi)存空間初始化為0。這些就是分段儲(chǔ)存可執(zhí)行文件的內(nèi)容的好處。
下面談一下Unix系統(tǒng)里的兩種重要的格式:a.out和elf(Executable and Linking Format)。這兩種格式中都有符號(hào)表(symbol table),其中包括所有的符號(hào)(程序的入口點(diǎn)還有變量的地址等等)。在elf格式中符號(hào)表的內(nèi)容會(huì)比a.out格式的豐富的多。但是這些符號(hào)表可以用 strip工具去除,這樣的話這個(gè)文件就無法讓debug程序跟蹤了,但是會(huì)生成比較小的可執(zhí)行文件。a.out文件中的符號(hào)表可以被完全去除,但是 elf中的在加載運(yùn)行是起著重要的作用,所以用strip永遠(yuǎn)不可能完全去除elf格式文件中的符號(hào)表。但是用strip命令不是完全安全的,比如對(duì)未連接的目標(biāo)文件來說如果用strip去掉符號(hào)表的話,會(huì)導(dǎo)致連接器無法連接。例如:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
用gcc把hello.c編譯成目標(biāo)文件hello.o
Shell代碼$:strip?hello.o??
用strip去掉hello.o中的符號(hào)信息。
Shell代碼$:gcc?hello.o?? /usr/lib/gcc/i686-pc-linux-gnu/3.4.5/../../../crt1.o:?In?function?`_start':?? init.c:??(.text+0x18)??:?undefined?reference?to?`main'?collect2:?ld?returned?1?exit?status??
再用gcc連接時(shí),連接器ld報(bào)錯(cuò)。說明在目標(biāo)文件中的符號(hào)起著很重要的作用,如果要發(fā)布二進(jìn)制的程序的話,在debug后為了減小可執(zhí)行文件的大小,可以用strip來除去符號(hào)信息但是在程序的調(diào)試階段還是不要用strip為好。
在接下去討論以前,我們還要來講講relocations的概念:首先有個(gè)簡單的程序hello.c
Shell代碼$:cat?hello.c?? main(?)?? {?? printf("Hello?World\n");?? }???
當(dāng)我們把hello.c編譯為目標(biāo)文件時(shí),我們并沒有在源文件中定義printf這個(gè)函數(shù),所以匯編器也不知道printf這個(gè)函數(shù)的具體的地址,所以在目標(biāo)文件中就會(huì)留下printf這個(gè)符號(hào)。以下的工作就交給連接器了,連接器會(huì)找到這個(gè)函數(shù)的入口地址然后傳遞給這個(gè)文件最終形成可執(zhí)行文件。這個(gè)過程就叫做relocations。a.out格式的可執(zhí)行文件是沒有這種relocation的功能的,內(nèi)核不會(huì)執(zhí)行其中還有未知函數(shù)的入口地址的可執(zhí)行文件的。在目標(biāo)文件中當(dāng)然可以relocation,只不過連接器需要把未知函數(shù)的入口地址完全找到,生成可執(zhí)行文件才行。這樣就有一個(gè)很尷尬的問題,在 a.out格式中極其難以實(shí)現(xiàn)動(dòng)態(tài)連接技術(shù)。要知道為什么現(xiàn)在的Unix幾乎都是用的elf格式的可執(zhí)行文件就要了解a.out格式的短處。
a.out的符號(hào)是極其有限的,在/usr/include/linux/asm/a.out.h中定義了一個(gè)結(jié)構(gòu)exec就是:
Shell代碼struct?exec?? {??? ????unsigned?long?a_info;???????/*Use?macros?N_MAGIC,?etc?for?access?*/?? ????unsigned?a_text;????????/*?length?of?text,?in?bytes?*/?? ????unsigned?a_data;????????/*?length?of?data,?in?bytes?*/?? ????unsigned?a_bss;?????????/*?length?of?uninitialized?data?area?for?file,?in?bytes*/?? ????unsigned?a_syms;????????/*?length?of?symbol?table?data?in?file,?in?bytes?*/?? ????unsigned?a_entry;???????/*?start?address?*/?? ????unsigned?a_trsize;??????/*length?of?relocation?info?for?text,?in?bytes?*/?? ????unsigned?a_drsize;???????/*length?of?relocation?info?for?data,?in?bytes?*/?? };??
在這個(gè)結(jié)構(gòu)中更本沒有指示每個(gè)段在文件中的開始位置,內(nèi)核加載器具有一些非正式的方法來加載可執(zhí)行文件的。明顯的,a.out 是不支持動(dòng)態(tài)連接的。(在內(nèi)部不支持動(dòng)態(tài)連接,用某些技術(shù)也是可以實(shí)現(xiàn)a.out的動(dòng)態(tài)連接)
要了解elf可執(zhí)行文件的運(yùn)行方式,我們有必要討論一下動(dòng)態(tài)連接技術(shù)。很多人對(duì)動(dòng)態(tài)連接技術(shù)十分熟悉,但是很少有人真正了解動(dòng)態(tài)連接的內(nèi)部工作方式。回想沒有動(dòng)態(tài)連接的日子,程序員寫程序時(shí)不用什么都從頭開始,他們可以調(diào)用定義的很好的函數(shù),然后再用連接器與函數(shù)庫連接。這樣的話使得程序員更加有效率,但是一個(gè)十分重要的問題出現(xiàn)了:這樣產(chǎn)生的可執(zhí)行文件就會(huì)很大。因?yàn)檫B接器把程序需要用的所有函數(shù)的代碼都復(fù)制到了可執(zhí)行文件中去了。這種連接方式就是所謂的靜態(tài)連接,與之相對(duì)的就是動(dòng)態(tài)連接。連接器在可執(zhí)行文件中標(biāo)記出程序調(diào)用外部函數(shù)的位置,并不把代碼復(fù)制進(jìn)去,只是標(biāo)出函數(shù)在動(dòng)態(tài)連接庫中的位置。用這樣的方式生成的特殊可執(zhí)行文件就是動(dòng)態(tài)連接的。在運(yùn)行這種動(dòng)態(tài)程序時(shí),系統(tǒng)在運(yùn)行時(shí)把該程序調(diào)用的外部函數(shù)地址映射到程序地址,這就是所謂的動(dòng)態(tài)連接,系統(tǒng)就有一個(gè)程序叫做動(dòng)態(tài)連接器,在動(dòng)態(tài)連接的程序執(zhí)行前都要先把地址映射好。很顯然的,必須有一種機(jī)制保證動(dòng)態(tài)連接的程序中的函數(shù)地址正確地指向了動(dòng)態(tài)連接庫的某個(gè)函數(shù)地址。這就需要討論一下elf可執(zhí)行文件格式處理動(dòng)態(tài)連接的機(jī)制了。
elf的動(dòng)態(tài)連接庫是內(nèi)存位置無關(guān)的,就是說你可以把這個(gè)庫加載到內(nèi)存的任何位置都沒有影響。這就叫做position independent。而a.out的動(dòng)態(tài)連接庫是內(nèi)存位置有關(guān)的,它一定要被加載到規(guī)定的內(nèi)存地址才能工作。在編譯內(nèi)存位置無關(guān)的動(dòng)態(tài)連接庫時(shí),要給編譯器加上 -fpic選項(xiàng),讓編譯器產(chǎn)生的目標(biāo)文件是內(nèi)存位置無關(guān)的還會(huì)盡量減少對(duì)變量引用時(shí)使用絕對(duì)地址。把庫編譯成內(nèi)存位置無關(guān)會(huì)帶來一些花費(fèi),編譯器會(huì)保留一個(gè)寄存器來指向全局偏移量表(global offset table (or GOT for short)),這就會(huì)導(dǎo)致編譯器在優(yōu)化代碼時(shí)少了一個(gè)寄存器可以使用,但是在最壞的情況下這種性能的減少只有3%,在其他情況下是大大小于3%的。
Elf的另一個(gè)特點(diǎn)是它的動(dòng)態(tài)連接庫是在運(yùn)行時(shí)處理符號(hào)的,這是通過用符號(hào)表和再布置(relocation)表來實(shí)現(xiàn)的。在載入文件時(shí)并不能立即執(zhí)行,要在處理完符號(hào)表把所有的地址都relocation完后才可以執(zhí)行。這個(gè)聽起來有點(diǎn)復(fù)雜而且可能導(dǎo)致文件運(yùn)行慢,不過對(duì)elf做了很大的優(yōu)化后,這種減慢已經(jīng)是微不足道的了。理論上說不是用-fpic選項(xiàng)編譯出來的目標(biāo)文件也可以用作動(dòng)態(tài)連接庫,但是在運(yùn)行時(shí)會(huì)需要做數(shù)目極大的 relocation,這是對(duì)運(yùn)行速度有極大影響的。這樣的程序性能是很差的,幾乎沒有可用性。
當(dāng)從動(dòng)態(tài)連接庫中讀一個(gè)全局變量時(shí)與從非-fpic編譯的目標(biāo)文件讀是不同的。讀動(dòng)態(tài)連接的庫中的變量是通過GOT來尋找到目標(biāo)變量的,GOT已經(jīng)由某一個(gè)寄存器指向了。GOT本生就是一個(gè)指針列表,找到GOT中的某一個(gè)指針就可以讀到所要的全局變量了,有了GOT我們要讀出一個(gè)變量只要做一次relocation。
下面我們來看看elf文件中到底有些什么信息:
Shell代碼$:cat?hello.c?? main()?? {?? ????????printf("Hello?World\n");?? }?? $:gcc-elf?-c?hello.c??
還是這個(gè)簡單的程序,用gcc把它編譯成目標(biāo)文件hello.o。然后用readelf工具來探測(cè)一下elf文件的內(nèi)容。(readelf是在 binutils軟件包里的一個(gè)工具,大多數(shù)Linux發(fā)行版都包含它)
Shell代碼$:readelf?-h?hello.o?? ??ELF?Header:?? ??Magic:???7f?45?4c?46?01?01?01?00?00?00?00?00?00?00?00?00?? ??Class:?????????????????????????????ELF32?? ??Data:??????????????????????????????2's?complement,?little?endian?? ??Version:???????????????????????????1?(current)?? ??OS/ABI:????????????????????????????UNIX?-?System?V?? ??ABI?Version:???????????????????????0?? ??Type:??????????????????????????????REL?(Relocatable?file)?? ??Machine:???????????????????????????Intel?80386?? ??Version:???????????????????????????0x1?? ??Entry?point?address:???????????????0x0?? ??Start?of?program?headers:??????????0?(bytes?into?file)?? ??Start?of?section?headers:??????????256?(bytes?into?file)?? ??Flags:?????????????????????????????0x0?? ??Size?of?this?header:???????????????52?(bytes)?? ??Size?of?program?headers:???????????0?(bytes)?? ??Number?of?program?headers:?????????0?? ??Size?of?section?headers:???????????40?(bytes)?? ??Number?of?section?headers:?????????11?? ??Section?header?string?table?index:?8??
-h選項(xiàng)是列出elf文件的頭信息。Magic:字段是一個(gè)標(biāo)識(shí)符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,這是一個(gè)32位的elf。Machine:字段是指出目標(biāo)文件的平臺(tái)信息,這里是 I386兼容平臺(tái)。其他的字段可以從其字面上看出它的意義,這里就不一一解釋了。
下面用-S選項(xiàng)列出段的頭信息:
Shell代碼$:readelf?-S?hello.o?? There?are?11?section?headers,?starting?at?offset?0x100:?? ?? Section?Headers:?? ??[Nr]?Name??????????????????Type????????????????Addr?????????????Off????????????Size???ES???Flg?????Lk?Inf?Al?? ??[?0]?????????????????????????????NULL????????????????00000000?000000?000000?????00??????????0???0??0?? ??[?1]?.text?????????????????????PROGBITS????????00000000?000034?00002a?????00??AX????0???0??4?? ??[?2]?.rel.text????????????????REL?????????????????00000000?000370?000010??????08???????????9???1??4?? ??[?3]?.data????????????????????PROGBITS????????00000000?000060?000000?????00??WA????0???0??4?? ??[?4]?.bss?????????????????????NOBITS??????????00000000?000060?000000????????00??WA???0???0??4?? ??[?5]?.rodata????????????????PROGBITS????????00000000?000060?00000e??????00???A??????0???0??1?? ??[?6]?.note.GNU-stack??PROGBITS????????00000000?00006e?000000?????00???????????0???0??1?? ??[?7]?.comment????????????PROGBITS????????00000000?00006e?00003e?????00????????????0???0??1?? ??[?8]?.shstrtab?????????????STRTAB??????????00000000?0000ac?000051????????00???????????0???0??1?? ??[?9]?.symtab???????????????SYMTAB??????????00000000?0002b8?0000a0??????10????????????10???8??4?? ??[10]?.strtab????????????????STRTAB??????????00000000?000358?000015???????00???????????0???0??1?? Key?to?Flags:?? ??W?(write),?A?(alloc),?X?(execute),?M?(merge),?S?(strings)?? ??I?(info),?L?(link?order),?G?(group),?x?(unknown)?? ??O?(extra?OS?processing?required)?o?(OS?specific),?p?(processor?specific)??
Name字段顯示的是各個(gè)段的名字,Type顯示段的屬性,Addr是每個(gè)段載入虛擬內(nèi)存的位置,Off是每個(gè)段在目標(biāo)文件中的偏移位置,Size是每個(gè)段的大小,后面的一些字段是表示段的可寫,可讀,或者可執(zhí)行。
用-r可以列出elf文件中的relocation:
Shell代碼$:readelf?-r?hello.o?? ?? Relocation?section?'.rel.text'?at?offset?0x370?contains?2?entries:?? ?Offset?????Info????Type????????????Sym.Value??Sym.?Name?? 0000001f??00000501?R_386_32??????????00000000???.rodata?? 00000024??00000902?R_386_PC32????????00000000???printf??
在.text段中有兩個(gè)relocation,其中之一就是printf函數(shù)的relcation。Offset指出當(dāng)relocation時(shí)要把 printf函數(shù)的入口地址貼到離.text段開頭00000024處。
下面我們可以看一下連接過后的可執(zhí)行文件中的內(nèi)容:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
0
這里的段比目標(biāo)文件hello.o的段要多的多,這是因?yàn)檫@個(gè)程序需要elf的一個(gè)動(dòng)態(tài)連接庫libc.so.1。在這里需要簡單的介紹一下內(nèi)核加載 elf可執(zhí)行文件。內(nèi)核先是把整個(gè)文件加載到用戶的虛擬內(nèi)存空間,如果程序是與動(dòng)態(tài)連接庫連接的,則程序中就會(huì)包含動(dòng)態(tài)連接器的名稱,可能是 /lib/elf/ld-linux.so.1。(動(dòng)態(tài)連接器本身也是一個(gè)動(dòng)態(tài)連接庫)
在文件的尾部的一些段的Addr值是00000000,因?yàn)檫@些都是符號(hào)表,動(dòng)態(tài)連接器并不把這些段的內(nèi)容加載到內(nèi)存中。. interp段中只是儲(chǔ)存這一個(gè)ASCII的字符串,它就是動(dòng)態(tài)連接器的名字(路徑)。.hash, .dynsym, .dynstr這三個(gè)段是用于動(dòng)態(tài)連接器執(zhí)行relocation時(shí)的符號(hào)表。.hash是一個(gè)哈希表,可以讓我們很快的從.dynsym中找到所需的符號(hào)。
.plt段中儲(chǔ)存著我們調(diào)用動(dòng)態(tài)連接庫中的函數(shù)入口地址,在默認(rèn)狀態(tài)下,程序初始化時(shí),.plt中的指針并不是指向正確的函數(shù)入口地址的而是指向動(dòng)態(tài)連接器本身,當(dāng)你在程序中調(diào)用某個(gè)動(dòng)態(tài)連接庫中的函數(shù)時(shí),連接器會(huì)找到那個(gè)函數(shù)在動(dòng)態(tài)連接庫中的位置,再把這個(gè)位置連接到.plt段中。這樣做的好處是如果在程序中調(diào)用了很多動(dòng)態(tài)連接庫中的函數(shù),會(huì)花費(fèi)掉連接器很長時(shí)間把每個(gè)函數(shù)的地址連接到.plt段中。所以就可以采用連接器只是把要用的函數(shù)地址連接進(jìn)去,以后要用的再連接。但是也可以設(shè)置環(huán)境變量LD_BIND_NOW=1讓連接器在程序執(zhí)行前把所有的函數(shù)地址都連接好,這主要是方便調(diào)試程序。
readelf工具還有很多選項(xiàng),具體內(nèi)容可以查看man手冊(cè)。在文章的開頭就說elf文件格式很方便運(yùn)用動(dòng)態(tài)連接技術(shù),下面我就寫一個(gè)就簡單的動(dòng)態(tài)連接庫的例子:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
1
兩個(gè)簡單的文件,在mian函數(shù)中調(diào)用hi()函數(shù),下面并不是把兩個(gè)文件一起編譯,而是把hi.c編譯成動(dòng)態(tài)連接庫。(注意Dyn_hello.c中并沒有包含任何頭文件。)
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
2
現(xiàn)在在當(dāng)前目錄下有一個(gè)名字為libhi.so的文件,這就就是僅含有一個(gè)函數(shù)的動(dòng)態(tài)連接庫。
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
3
在當(dāng)前目錄下有了一個(gè)Dyn_hello可執(zhí)行文件,現(xiàn)在就可以執(zhí)行它了。
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
4
執(zhí)行不成功,這就表明了這是一個(gè)動(dòng)態(tài)連接的程序,連接器找不到libhi.so這個(gè)動(dòng)態(tài)連接庫。在命令行加上 LD_LIBRARY_PATH=...就行了。像這樣運(yùn)行:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
5
指出當(dāng)前目錄是連接器的搜索目錄,就可以了。
Elf可執(zhí)行文件還有一個(gè)a.out很難實(shí)現(xiàn)的特點(diǎn),就是對(duì)dlopen()函數(shù)的支持,這個(gè)函數(shù)可以在程序中控制動(dòng)態(tài)的加載動(dòng)態(tài)連接庫,看下面的一個(gè)小程序:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
6
用一下命令編譯:
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
7
運(yùn)行Dl_hello程序加上動(dòng)態(tài)連接庫。
Shell代碼$:gcc?-c?hello.c?? $:ls?? hello.c??????hello.o???
8
命令行成功的打印出了Hello world說明我們的動(dòng)態(tài)連接庫運(yùn)用成功了。
在這篇文章中只是討論了elf可執(zhí)行文件的執(zhí)行原理,還有很多方面沒有涉及到,要深入了解elf你也許需要對(duì)動(dòng)態(tài)連接器hack一下,也要hack一下內(nèi)核加載程序的loader。但是我想對(duì)大多數(shù)人來說,這篇文章對(duì)elf的介紹已經(jīng)足夠讓你可以自己對(duì)elf在進(jìn)行比較深入的研究了。
每個(gè)操作系統(tǒng)都會(huì)有自己的可執(zhí)行文件的格式,比如以前的Unix?是用a.out格式的,現(xiàn)代的Unix?類系統(tǒng)使用elf格式, WindowsNT?是使用基于COFF格式的可執(zhí)行文件。那么最簡單的格式應(yīng)該是DOS的可執(zhí)行格式,嚴(yán)格來說DOS的可執(zhí)行文件沒有什么格式可言,就是把二進(jìn)制代碼安順序放在文件里,運(yùn)行時(shí)DOS操作系統(tǒng)就把所有控制計(jì)算機(jī)的權(quán)力都給了這個(gè)程序。這種方式的不足之處是顯而易見的,所以現(xiàn)代的操作系統(tǒng)都有一種更好的方式來定義可執(zhí)行文件的格式。一種常見的方法就是為可執(zhí)行文件分段,一般來說把程序指令的內(nèi)容放在.text段中,把程序中的數(shù)據(jù)內(nèi)容放在.data段中,把程序中未初始化的數(shù)據(jù)放在.bss段中。這種做法的好處有很多,可以讓操作系統(tǒng)內(nèi)核來檢查程序防止有嚴(yán)重錯(cuò)誤的程序破壞整個(gè)運(yùn)行環(huán)境。比如:某個(gè)程序想要修改.text段中的內(nèi)容,那么操作系統(tǒng)就會(huì)認(rèn)為這段程序有誤而立即終止它的運(yùn)行,因?yàn)橄到y(tǒng)會(huì)把.text段的內(nèi)存標(biāo)記為只讀。在.bss段中的數(shù)據(jù)還沒有初始化,就沒有必要在可執(zhí)行文件中浪費(fèi)儲(chǔ)存空間。在.bss中只是表明某個(gè)變量要使用多少的內(nèi)存空間,等到程序加載的時(shí)候在由內(nèi)核把這段未初始化的內(nèi)存空間初始化為0。這些就是分段儲(chǔ)存可執(zhí)行文件的內(nèi)容的好處。
下面談一下Unix系統(tǒng)里的兩種重要的格式:a.out和elf(Executable and Linking Format)。這兩種格式中都有符號(hào)表(symbol table),其中包括所有的符號(hào)(程序的入口點(diǎn)還有變量的地址等等)。在elf格式中符號(hào)表的內(nèi)容會(huì)比a.out格式的豐富的多。但是這些符號(hào)表可以用 strip工具去除,這樣的話這個(gè)文件就無法讓debug程序跟蹤了,但是會(huì)生成比較小的可執(zhí)行文件。a.out文件中的符號(hào)表可以被完全去除,但是 elf中的在加載運(yùn)行是起著重要的作用,所以用strip永遠(yuǎn)不可能完全去除elf格式文件中的符號(hào)表。但是用strip命令不是完全安全的,比如對(duì)未連接的目標(biāo)文件來說如果用strip去掉符號(hào)表的話,會(huì)導(dǎo)致連接器無法連接。例如:
Shell代碼
用gcc把hello.c編譯成目標(biāo)文件hello.o
Shell代碼
用strip去掉hello.o中的符號(hào)信息。
Shell代碼
再用gcc連接時(shí),連接器ld報(bào)錯(cuò)。說明在目標(biāo)文件中的符號(hào)起著很重要的作用,如果要發(fā)布二進(jìn)制的程序的話,在debug后為了減小可執(zhí)行文件的大小,可以用strip來除去符號(hào)信息但是在程序的調(diào)試階段還是不要用strip為好。
在接下去討論以前,我們還要來講講relocations的概念:首先有個(gè)簡單的程序hello.c
Shell代碼
當(dāng)我們把hello.c編譯為目標(biāo)文件時(shí),我們并沒有在源文件中定義printf這個(gè)函數(shù),所以匯編器也不知道printf這個(gè)函數(shù)的具體的地址,所以在目標(biāo)文件中就會(huì)留下printf這個(gè)符號(hào)。以下的工作就交給連接器了,連接器會(huì)找到這個(gè)函數(shù)的入口地址然后傳遞給這個(gè)文件最終形成可執(zhí)行文件。這個(gè)過程就叫做relocations。a.out格式的可執(zhí)行文件是沒有這種relocation的功能的,內(nèi)核不會(huì)執(zhí)行其中還有未知函數(shù)的入口地址的可執(zhí)行文件的。在目標(biāo)文件中當(dāng)然可以relocation,只不過連接器需要把未知函數(shù)的入口地址完全找到,生成可執(zhí)行文件才行。這樣就有一個(gè)很尷尬的問題,在 a.out格式中極其難以實(shí)現(xiàn)動(dòng)態(tài)連接技術(shù)。要知道為什么現(xiàn)在的Unix幾乎都是用的elf格式的可執(zhí)行文件就要了解a.out格式的短處。
a.out的符號(hào)是極其有限的,在/usr/include/linux/asm/a.out.h中定義了一個(gè)結(jié)構(gòu)exec就是:
Shell代碼
在這個(gè)結(jié)構(gòu)中更本沒有指示每個(gè)段在文件中的開始位置,內(nèi)核加載器具有一些非正式的方法來加載可執(zhí)行文件的。明顯的,a.out 是不支持動(dòng)態(tài)連接的。(在內(nèi)部不支持動(dòng)態(tài)連接,用某些技術(shù)也是可以實(shí)現(xiàn)a.out的動(dòng)態(tài)連接)
要了解elf可執(zhí)行文件的運(yùn)行方式,我們有必要討論一下動(dòng)態(tài)連接技術(shù)。很多人對(duì)動(dòng)態(tài)連接技術(shù)十分熟悉,但是很少有人真正了解動(dòng)態(tài)連接的內(nèi)部工作方式。回想沒有動(dòng)態(tài)連接的日子,程序員寫程序時(shí)不用什么都從頭開始,他們可以調(diào)用定義的很好的函數(shù),然后再用連接器與函數(shù)庫連接。這樣的話使得程序員更加有效率,但是一個(gè)十分重要的問題出現(xiàn)了:這樣產(chǎn)生的可執(zhí)行文件就會(huì)很大。因?yàn)檫B接器把程序需要用的所有函數(shù)的代碼都復(fù)制到了可執(zhí)行文件中去了。這種連接方式就是所謂的靜態(tài)連接,與之相對(duì)的就是動(dòng)態(tài)連接。連接器在可執(zhí)行文件中標(biāo)記出程序調(diào)用外部函數(shù)的位置,并不把代碼復(fù)制進(jìn)去,只是標(biāo)出函數(shù)在動(dòng)態(tài)連接庫中的位置。用這樣的方式生成的特殊可執(zhí)行文件就是動(dòng)態(tài)連接的。在運(yùn)行這種動(dòng)態(tài)程序時(shí),系統(tǒng)在運(yùn)行時(shí)把該程序調(diào)用的外部函數(shù)地址映射到程序地址,這就是所謂的動(dòng)態(tài)連接,系統(tǒng)就有一個(gè)程序叫做動(dòng)態(tài)連接器,在動(dòng)態(tài)連接的程序執(zhí)行前都要先把地址映射好。很顯然的,必須有一種機(jī)制保證動(dòng)態(tài)連接的程序中的函數(shù)地址正確地指向了動(dòng)態(tài)連接庫的某個(gè)函數(shù)地址。這就需要討論一下elf可執(zhí)行文件格式處理動(dòng)態(tài)連接的機(jī)制了。
elf的動(dòng)態(tài)連接庫是內(nèi)存位置無關(guān)的,就是說你可以把這個(gè)庫加載到內(nèi)存的任何位置都沒有影響。這就叫做position independent。而a.out的動(dòng)態(tài)連接庫是內(nèi)存位置有關(guān)的,它一定要被加載到規(guī)定的內(nèi)存地址才能工作。在編譯內(nèi)存位置無關(guān)的動(dòng)態(tài)連接庫時(shí),要給編譯器加上 -fpic選項(xiàng),讓編譯器產(chǎn)生的目標(biāo)文件是內(nèi)存位置無關(guān)的還會(huì)盡量減少對(duì)變量引用時(shí)使用絕對(duì)地址。把庫編譯成內(nèi)存位置無關(guān)會(huì)帶來一些花費(fèi),編譯器會(huì)保留一個(gè)寄存器來指向全局偏移量表(global offset table (or GOT for short)),這就會(huì)導(dǎo)致編譯器在優(yōu)化代碼時(shí)少了一個(gè)寄存器可以使用,但是在最壞的情況下這種性能的減少只有3%,在其他情況下是大大小于3%的。
Elf的另一個(gè)特點(diǎn)是它的動(dòng)態(tài)連接庫是在運(yùn)行時(shí)處理符號(hào)的,這是通過用符號(hào)表和再布置(relocation)表來實(shí)現(xiàn)的。在載入文件時(shí)并不能立即執(zhí)行,要在處理完符號(hào)表把所有的地址都relocation完后才可以執(zhí)行。這個(gè)聽起來有點(diǎn)復(fù)雜而且可能導(dǎo)致文件運(yùn)行慢,不過對(duì)elf做了很大的優(yōu)化后,這種減慢已經(jīng)是微不足道的了。理論上說不是用-fpic選項(xiàng)編譯出來的目標(biāo)文件也可以用作動(dòng)態(tài)連接庫,但是在運(yùn)行時(shí)會(huì)需要做數(shù)目極大的 relocation,這是對(duì)運(yùn)行速度有極大影響的。這樣的程序性能是很差的,幾乎沒有可用性。
當(dāng)從動(dòng)態(tài)連接庫中讀一個(gè)全局變量時(shí)與從非-fpic編譯的目標(biāo)文件讀是不同的。讀動(dòng)態(tài)連接的庫中的變量是通過GOT來尋找到目標(biāo)變量的,GOT已經(jīng)由某一個(gè)寄存器指向了。GOT本生就是一個(gè)指針列表,找到GOT中的某一個(gè)指針就可以讀到所要的全局變量了,有了GOT我們要讀出一個(gè)變量只要做一次relocation。
下面我們來看看elf文件中到底有些什么信息:
Shell代碼
還是這個(gè)簡單的程序,用gcc把它編譯成目標(biāo)文件hello.o。然后用readelf工具來探測(cè)一下elf文件的內(nèi)容。(readelf是在 binutils軟件包里的一個(gè)工具,大多數(shù)Linux發(fā)行版都包含它)
Shell代碼
-h選項(xiàng)是列出elf文件的頭信息。Magic:字段是一個(gè)標(biāo)識(shí)符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,這是一個(gè)32位的elf。Machine:字段是指出目標(biāo)文件的平臺(tái)信息,這里是 I386兼容平臺(tái)。其他的字段可以從其字面上看出它的意義,這里就不一一解釋了。
下面用-S選項(xiàng)列出段的頭信息:
Shell代碼
Name字段顯示的是各個(gè)段的名字,Type顯示段的屬性,Addr是每個(gè)段載入虛擬內(nèi)存的位置,Off是每個(gè)段在目標(biāo)文件中的偏移位置,Size是每個(gè)段的大小,后面的一些字段是表示段的可寫,可讀,或者可執(zhí)行。
用-r可以列出elf文件中的relocation:
Shell代碼
在.text段中有兩個(gè)relocation,其中之一就是printf函數(shù)的relcation。Offset指出當(dāng)relocation時(shí)要把 printf函數(shù)的入口地址貼到離.text段開頭00000024處。
下面我們可以看一下連接過后的可執(zhí)行文件中的內(nèi)容:
Shell代碼
這里的段比目標(biāo)文件hello.o的段要多的多,這是因?yàn)檫@個(gè)程序需要elf的一個(gè)動(dòng)態(tài)連接庫libc.so.1。在這里需要簡單的介紹一下內(nèi)核加載 elf可執(zhí)行文件。內(nèi)核先是把整個(gè)文件加載到用戶的虛擬內(nèi)存空間,如果程序是與動(dòng)態(tài)連接庫連接的,則程序中就會(huì)包含動(dòng)態(tài)連接器的名稱,可能是 /lib/elf/ld-linux.so.1。(動(dòng)態(tài)連接器本身也是一個(gè)動(dòng)態(tài)連接庫)
在文件的尾部的一些段的Addr值是00000000,因?yàn)檫@些都是符號(hào)表,動(dòng)態(tài)連接器并不把這些段的內(nèi)容加載到內(nèi)存中。. interp段中只是儲(chǔ)存這一個(gè)ASCII的字符串,它就是動(dòng)態(tài)連接器的名字(路徑)。.hash, .dynsym, .dynstr這三個(gè)段是用于動(dòng)態(tài)連接器執(zhí)行relocation時(shí)的符號(hào)表。.hash是一個(gè)哈希表,可以讓我們很快的從.dynsym中找到所需的符號(hào)。
.plt段中儲(chǔ)存著我們調(diào)用動(dòng)態(tài)連接庫中的函數(shù)入口地址,在默認(rèn)狀態(tài)下,程序初始化時(shí),.plt中的指針并不是指向正確的函數(shù)入口地址的而是指向動(dòng)態(tài)連接器本身,當(dāng)你在程序中調(diào)用某個(gè)動(dòng)態(tài)連接庫中的函數(shù)時(shí),連接器會(huì)找到那個(gè)函數(shù)在動(dòng)態(tài)連接庫中的位置,再把這個(gè)位置連接到.plt段中。這樣做的好處是如果在程序中調(diào)用了很多動(dòng)態(tài)連接庫中的函數(shù),會(huì)花費(fèi)掉連接器很長時(shí)間把每個(gè)函數(shù)的地址連接到.plt段中。所以就可以采用連接器只是把要用的函數(shù)地址連接進(jìn)去,以后要用的再連接。但是也可以設(shè)置環(huán)境變量LD_BIND_NOW=1讓連接器在程序執(zhí)行前把所有的函數(shù)地址都連接好,這主要是方便調(diào)試程序。
readelf工具還有很多選項(xiàng),具體內(nèi)容可以查看man手冊(cè)。在文章的開頭就說elf文件格式很方便運(yùn)用動(dòng)態(tài)連接技術(shù),下面我就寫一個(gè)就簡單的動(dòng)態(tài)連接庫的例子:
Shell代碼
兩個(gè)簡單的文件,在mian函數(shù)中調(diào)用hi()函數(shù),下面并不是把兩個(gè)文件一起編譯,而是把hi.c編譯成動(dòng)態(tài)連接庫。(注意Dyn_hello.c中并沒有包含任何頭文件。)
Shell代碼
現(xiàn)在在當(dāng)前目錄下有一個(gè)名字為libhi.so的文件,這就就是僅含有一個(gè)函數(shù)的動(dòng)態(tài)連接庫。
Shell代碼
在當(dāng)前目錄下有了一個(gè)Dyn_hello可執(zhí)行文件,現(xiàn)在就可以執(zhí)行它了。
Shell代碼
執(zhí)行不成功,這就表明了這是一個(gè)動(dòng)態(tài)連接的程序,連接器找不到libhi.so這個(gè)動(dòng)態(tài)連接庫。在命令行加上 LD_LIBRARY_PATH=...就行了。像這樣運(yùn)行:
Shell代碼
指出當(dāng)前目錄是連接器的搜索目錄,就可以了。
Elf可執(zhí)行文件還有一個(gè)a.out很難實(shí)現(xiàn)的特點(diǎn),就是對(duì)dlopen()函數(shù)的支持,這個(gè)函數(shù)可以在程序中控制動(dòng)態(tài)的加載動(dòng)態(tài)連接庫,看下面的一個(gè)小程序:
Shell代碼
用一下命令編譯:
Shell代碼
運(yùn)行Dl_hello程序加上動(dòng)態(tài)連接庫。
Shell代碼
命令行成功的打印出了Hello world說明我們的動(dòng)態(tài)連接庫運(yùn)用成功了。
在這篇文章中只是討論了elf可執(zhí)行文件的執(zhí)行原理,還有很多方面沒有涉及到,要深入了解elf你也許需要對(duì)動(dòng)態(tài)連接器hack一下,也要hack一下內(nèi)核加載程序的loader。但是我想對(duì)大多數(shù)人來說,這篇文章對(duì)elf的介紹已經(jīng)足夠讓你可以自己對(duì)elf在進(jìn)行比較深入的研究了。
總結(jié)
- 上一篇: Meld安装
- 下一篇: win32 ipv6 bind 100