日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Elf

發(fā)布時(shí)間:2023/12/10 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Elf 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
機(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)行比較深入的研究了。

    總結(jié)

    以上是生活随笔為你收集整理的Elf的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。