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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Elf

發布時間:2023/12/10 编程问答 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Elf 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
機器執行的是機器指令,而機器指令就是一堆二進制的數字。高級語言編寫的程序之所以可以在不同的機器上移植就因為有為不同機器設計的編譯器的存在。高級語言的編譯器就是把高級語言寫的程序轉換成某個機器能直接執行的二進制代碼。以上的知識在我們學習CS(Computer Science)的初期,老師都會這么對我們講。但是我就產生疑問了:既然機器都是執行的二進制代碼,那么是不是說只要硬件相互兼容,不同操作系統下的可執行文件可以互相運行呢?答案肯定是不行。這就要談到可執行文件的格式問題。

每個操作系統都會有自己的可執行文件的格式,比如以前的Unix?是用a.out格式的,現代的Unix?類系統使用elf格式, WindowsNT?是使用基于COFF格式的可執行文件。那么最簡單的格式應該是DOS的可執行格式,嚴格來說DOS的可執行文件沒有什么格式可言,就是把二進制代碼安順序放在文件里,運行時DOS操作系統就把所有控制計算機的權力都給了這個程序。這種方式的不足之處是顯而易見的,所以現代的操作系統都有一種更好的方式來定義可執行文件的格式。一種常見的方法就是為可執行文件分段,一般來說把程序指令的內容放在.text段中,把程序中的數據內容放在.data段中,把程序中未初始化的數據放在.bss段中。這種做法的好處有很多,可以讓操作系統內核來檢查程序防止有嚴重錯誤的程序破壞整個運行環境。比如:某個程序想要修改.text段中的內容,那么操作系統就會認為這段程序有誤而立即終止它的運行,因為系統會把.text段的內存標記為只讀。在.bss段中的數據還沒有初始化,就沒有必要在可執行文件中浪費儲存空間。在.bss中只是表明某個變量要使用多少的內存空間,等到程序加載的時候在由內核把這段未初始化的內存空間初始化為0。這些就是分段儲存可執行文件的內容的好處。

下面談一下Unix系統里的兩種重要的格式:a.out和elf(Executable and Linking Format)。這兩種格式中都有符號表(symbol table),其中包括所有的符號(程序的入口點還有變量的地址等等)。在elf格式中符號表的內容會比a.out格式的豐富的多。但是這些符號表可以用 strip工具去除,這樣的話這個文件就無法讓debug程序跟蹤了,但是會生成比較小的可執行文件。a.out文件中的符號表可以被完全去除,但是 elf中的在加載運行是起著重要的作用,所以用strip永遠不可能完全去除elf格式文件中的符號表。但是用strip命令不是完全安全的,比如對未連接的目標文件來說如果用strip去掉符號表的話,會導致連接器無法連接。例如:
Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???

  • 用gcc把hello.c編譯成目標文件hello.o
    Shell代碼
  • $:strip?hello.o??

  • 用strip去掉hello.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連接時,連接器ld報錯。說明在目標文件中的符號起著很重要的作用,如果要發布二進制的程序的話,在debug后為了減小可執行文件的大小,可以用strip來除去符號信息但是在程序的調試階段還是不要用strip為好。

    在接下去討論以前,我們還要來講講relocations的概念:首先有個簡單的程序hello.c
    Shell代碼
  • $:cat?hello.c??
  • main(?)??
  • {??
  • printf("Hello?World\n");??
  • }???

  • 當我們把hello.c編譯為目標文件時,我們并沒有在源文件中定義printf這個函數,所以匯編器也不知道printf這個函數的具體的地址,所以在目標文件中就會留下printf這個符號。以下的工作就交給連接器了,連接器會找到這個函數的入口地址然后傳遞給這個文件最終形成可執行文件。這個過程就叫做relocations。a.out格式的可執行文件是沒有這種relocation的功能的,內核不會執行其中還有未知函數的入口地址的可執行文件的。在目標文件中當然可以relocation,只不過連接器需要把未知函數的入口地址完全找到,生成可執行文件才行。這樣就有一個很尷尬的問題,在 a.out格式中極其難以實現動態連接技術。要知道為什么現在的Unix幾乎都是用的elf格式的可執行文件就要了解a.out格式的短處。

    a.out的符號是極其有限的,在/usr/include/linux/asm/a.out.h中定義了一個結構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?*/??
  • };??

  • 在這個結構中更本沒有指示每個段在文件中的開始位置,內核加載器具有一些非正式的方法來加載可執行文件的。明顯的,a.out 是不支持動態連接的。(在內部不支持動態連接,用某些技術也是可以實現a.out的動態連接)

    要了解elf可執行文件的運行方式,我們有必要討論一下動態連接技術。很多人對動態連接技術十分熟悉,但是很少有人真正了解動態連接的內部工作方式。回想沒有動態連接的日子,程序員寫程序時不用什么都從頭開始,他們可以調用定義的很好的函數,然后再用連接器與函數庫連接。這樣的話使得程序員更加有效率,但是一個十分重要的問題出現了:這樣產生的可執行文件就會很大。因為連接器把程序需要用的所有函數的代碼都復制到了可執行文件中去了。這種連接方式就是所謂的靜態連接,與之相對的就是動態連接。連接器在可執行文件中標記出程序調用外部函數的位置,并不把代碼復制進去,只是標出函數在動態連接庫中的位置。用這樣的方式生成的特殊可執行文件就是動態連接的。在運行這種動態程序時,系統在運行時把該程序調用的外部函數地址映射到程序地址,這就是所謂的動態連接,系統就有一個程序叫做動態連接器,在動態連接的程序執行前都要先把地址映射好。很顯然的,必須有一種機制保證動態連接的程序中的函數地址正確地指向了動態連接庫的某個函數地址。這就需要討論一下elf可執行文件格式處理動態連接的機制了。

    elf的動態連接庫是內存位置無關的,就是說你可以把這個庫加載到內存的任何位置都沒有影響。這就叫做position independent。而a.out的動態連接庫是內存位置有關的,它一定要被加載到規定的內存地址才能工作。在編譯內存位置無關的動態連接庫時,要給編譯器加上 -fpic選項,讓編譯器產生的目標文件是內存位置無關的還會盡量減少對變量引用時使用絕對地址。把庫編譯成內存位置無關會帶來一些花費,編譯器會保留一個寄存器來指向全局偏移量表(global offset table (or GOT for short)),這就會導致編譯器在優化代碼時少了一個寄存器可以使用,但是在最壞的情況下這種性能的減少只有3%,在其他情況下是大大小于3%的。

    Elf的另一個特點是它的動態連接庫是在運行時處理符號的,這是通過用符號表和再布置(relocation)表來實現的。在載入文件時并不能立即執行,要在處理完符號表把所有的地址都relocation完后才可以執行。這個聽起來有點復雜而且可能導致文件運行慢,不過對elf做了很大的優化后,這種減慢已經是微不足道的了。理論上說不是用-fpic選項編譯出來的目標文件也可以用作動態連接庫,但是在運行時會需要做數目極大的 relocation,這是對運行速度有極大影響的。這樣的程序性能是很差的,幾乎沒有可用性。

    當從動態連接庫中讀一個全局變量時與從非-fpic編譯的目標文件讀是不同的。讀動態連接的庫中的變量是通過GOT來尋找到目標變量的,GOT已經由某一個寄存器指向了。GOT本生就是一個指針列表,找到GOT中的某一個指針就可以讀到所要的全局變量了,有了GOT我們要讀出一個變量只要做一次relocation。

    下面我們來看看elf文件中到底有些什么信息:
    Shell代碼
  • $:cat?hello.c??
  • main()??
  • {??
  • ????????printf("Hello?World\n");??
  • }??
  • $:gcc-elf?-c?hello.c??

  • 還是這個簡單的程序,用gcc把它編譯成目標文件hello.o。然后用readelf工具來探測一下elf文件的內容。(readelf是在 binutils軟件包里的一個工具,大多數Linux發行版都包含它)
    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選項是列出elf文件的頭信息。Magic:字段是一個標識符,只要Magic字段是7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00的文件都是elf文件。Class:字段是表示elf的版本,這是一個32位的elf。Machine:字段是指出目標文件的平臺信息,這里是 I386兼容平臺。其他的字段可以從其字面上看出它的意義,這里就不一一解釋了。

    下面用-S選項列出段的頭信息:
    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字段顯示的是各個段的名字,Type顯示段的屬性,Addr是每個段載入虛擬內存的位置,Off是每個段在目標文件中的偏移位置,Size是每個段的大小,后面的一些字段是表示段的可寫,可讀,或者可執行。

    用-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段中有兩個relocation,其中之一就是printf函數的relcation。Offset指出當relocation時要把 printf函數的入口地址貼到離.text段開頭00000024處。

    下面我們可以看一下連接過后的可執行文件中的內容:
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 0
    這里的段比目標文件hello.o的段要多的多,這是因為這個程序需要elf的一個動態連接庫libc.so.1。在這里需要簡單的介紹一下內核加載 elf可執行文件。內核先是把整個文件加載到用戶的虛擬內存空間,如果程序是與動態連接庫連接的,則程序中就會包含動態連接器的名稱,可能是 /lib/elf/ld-linux.so.1。(動態連接器本身也是一個動態連接庫)

    在文件的尾部的一些段的Addr值是00000000,因為這些都是符號表,動態連接器并不把這些段的內容加載到內存中。. interp段中只是儲存這一個ASCII的字符串,它就是動態連接器的名字(路徑)。.hash, .dynsym, .dynstr這三個段是用于動態連接器執行relocation時的符號表。.hash是一個哈希表,可以讓我們很快的從.dynsym中找到所需的符號。

    .plt段中儲存著我們調用動態連接庫中的函數入口地址,在默認狀態下,程序初始化時,.plt中的指針并不是指向正確的函數入口地址的而是指向動態連接器本身,當你在程序中調用某個動態連接庫中的函數時,連接器會找到那個函數在動態連接庫中的位置,再把這個位置連接到.plt段中。這樣做的好處是如果在程序中調用了很多動態連接庫中的函數,會花費掉連接器很長時間把每個函數的地址連接到.plt段中。所以就可以采用連接器只是把要用的函數地址連接進去,以后要用的再連接。但是也可以設置環境變量LD_BIND_NOW=1讓連接器在程序執行前把所有的函數地址都連接好,這主要是方便調試程序。

    readelf工具還有很多選項,具體內容可以查看man手冊。在文章的開頭就說elf文件格式很方便運用動態連接技術,下面我就寫一個就簡單的動態連接庫的例子:
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 1
    兩個簡單的文件,在mian函數中調用hi()函數,下面并不是把兩個文件一起編譯,而是把hi.c編譯成動態連接庫。(注意Dyn_hello.c中并沒有包含任何頭文件。)
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 2
    現在在當前目錄下有一個名字為libhi.so的文件,這就就是僅含有一個函數的動態連接庫。
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 3
    在當前目錄下有了一個Dyn_hello可執行文件,現在就可以執行它了。
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 4
    執行不成功,這就表明了這是一個動態連接的程序,連接器找不到libhi.so這個動態連接庫。在命令行加上 LD_LIBRARY_PATH=...就行了。像這樣運行:
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 5
    指出當前目錄是連接器的搜索目錄,就可以了。

    Elf可執行文件還有一個a.out很難實現的特點,就是對dlopen()函數的支持,這個函數可以在程序中控制動態的加載動態連接庫,看下面的一個小程序:
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 6
    用一下命令編譯:
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 7
    運行Dl_hello程序加上動態連接庫。
    Shell代碼
  • $:gcc?-c?hello.c??
  • $:ls??
  • hello.c??????hello.o???
  • 8
    命令行成功的打印出了Hello world說明我們的動態連接庫運用成功了。

    在這篇文章中只是討論了elf可執行文件的執行原理,還有很多方面沒有涉及到,要深入了解elf你也許需要對動態連接器hack一下,也要hack一下內核加載程序的loader。但是我想對大多數人來說,這篇文章對elf的介紹已經足夠讓你可以自己對elf在進行比較深入的研究了。

    總結

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

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。