elf文件格式與動態連接庫

機器執行的是機器指令,而機器指令就是一堆二進制的數字。高級語言編寫的程序之因此能夠在不一樣的機器上移植就由於有爲不一樣機器設計的編譯器的存在。高級語言的編譯器就是把高級語言寫的程序轉換成某個機器能直接執行的二進制代碼。以上的知識在咱們學習CS(Computer Science)的初期,老師都會這麼對咱們講。可是我就產生疑問了:既然機器都是執行的二進制代碼,那麼是否是說只要硬件相互兼容,不一樣操做系統下的可執行文件能夠互相運行呢?答案確定是不行。這就要談到可執行文件的格式問題。linux

每一個操做系統都會有本身的可執行文件的格式,好比之前的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去掉符號表的話,會致使鏈接器沒法鏈接。例如: 代碼:小程序

$:gcc -c hello.c 
$:ls hello.c hello.o

用gcc把hello.c編譯成目標文件hello.o 代碼:安全

$:strip hello.o

用strip去掉hello.o中的符號信息。 代碼:函數

$: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 代碼:性能

$: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就是: 代碼:優化

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的動態鏈接)ui

要了解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文件中到底有些什麼信息: 代碼:

$:cat hello.c main() { printf("Hello World\n"); } 
$:gcc-elf -c hello.c

仍是這個簡單的程序,用gcc把它編譯成目標文件hello.o。而後用readelf工具來探測一下elf文件的內容。(readelf是在 binutils軟件包裏的一個工具,大多數Linux發行版都包含它) 代碼:

$: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選項列出段的頭信息: 代碼:

$: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: 代碼:

$: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處。

下面咱們能夠看一下鏈接事後的可執行文件中的內容: 代碼:

$:gcc hello.o 
$:readelf -S a.out 
There are 32 section headers, starting at offset 0xbc4: 

Section Headers: 
    [Nr] Name Type Addr Off Size ES Flg Lk Inf Al 
    [ 0] NULL 00000000 000000 000000 00 0 0 0 
    [ 1] .interp PROGBITS 08048134 000134 000013 00 A 0 0 1 
    [ 2] .note.ABI-tag NOTE 08048148 000148 000020 00 A 0 0 4 
    [ 3] .hash HASH 08048168 000168 00002c 04 A 4 0 4 
    [ 4] .dynsym DYNSYM 08048194 000194 000060 10 A 5 1 4 
    [ 5] .dynstr STRTAB 080481f4 0001f4 000060 00 A 0 0 1 
    [ 6] .gnu.version VERSYM 08048254 000254 00000c 02 A 4 0 2 
    [ 7] .gnu.version_r VERNEED 08048260 000260 000020 00 A 5 1 4 
    [ 8] .rel.dyn REL 08048280 000280 000008 08 A 4 0 4 
    [ 9] .rel.plt REL 08048288 000288 000010 08 A 4 11 4 
    [10] .init PROGBITS 08048298 000298 000017 00 AX 0 0 4 
    [11] .plt PROGBITS 080482b0 0002b0 000030 04 AX 0 0 4 
    [12] .text PROGBITS 080482e0 0002e0 0001b4 00 AX 0 0 16 
    [13] .fini PROGBITS 08048494 000494 00001a 00 AX 0 0 4 
    [14] .rodata PROGBITS 080484b0 0004b0 000016 00 A 0 0 4 
    [15] .eh_frame PROGBITS 080484c8 0004c8 000004 00 A 0 0 4 
    [16] .ctors PROGBITS 080494cc 0004cc 000008 00 WA 0 0 4 
    [17] .dtors PROGBITS 080494d4 0004d4 000008 00 WA 0 0 4 
    [18] .jcr PROGBITS 080494dc 0004dc 000004 00 WA 0 0 4 
    [19] .dynamic DYNAMIC 080494e0 0004e0 0000c8 08 WA 5 0 4 
    [20] .got PROGBITS 080495a8 0005a8 000004 04 WA 0 0 4 
    [21] .got.plt PROGBITS 080495ac 0005ac 000014 04 WA 0 0 4 
    [22] .data PROGBITS 080495c0 0005c0 00000c 00 WA 0 0 4 
    [23] .bss NOBITS 080495cc 0005cc 000004 00 WA 0 0 4 
    [24] .comment PROGBITS 00000000 0005cc 0001b2 00 0 0 1 
    [25] .debug_aranges PROGBITS 00000000 000780 000058 00 0 0 8 
    [26] .debug_info PROGBITS 00000000 0007d8 000164 00 0 0 1 
    [27] .debug_abbrev PROGBITS 00000000 00093c 000020 00 0 0 1 
    [28] .debug_line PROGBITS 00000000 00095c 00015a 00 0 0 1 
    [29] .shstrtab STRTAB 00000000 000ab6 00010c 00 0 0 1 
    [30] .symtab SYMTAB 00000000 0010c4 000510 10 31 56 4 
    [31] .strtab STRTAB 00000000 0015d4 000322 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)

這裏的段比目標文件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文件格式很方便運用動態鏈接技術,下面我就寫一個就簡單的動態鏈接庫的例子: 代碼:

$:cat Dyn_hello.c 
    int main(void) { hi(); } 
$:cat hi.c 
    #include <stdio.h> hi() { printf("Hello world\n"); }

兩個簡單的文件,在mian函數中調用hi()函數,下面並非把兩個文件一塊兒編譯,而是把hi.c編譯成動態鏈接庫。(注意Dyn_hello.c中並無包含任何頭文件。) 代碼:

$:gcc -fPIC -c hi.c 
$:gcc -shared -o libhi.so hi.o

如今在當前目錄下有一個名字爲libhi.so的文件,這就就是僅含有一個函數的動態鏈接庫。 代碼:

$:gcc -c Dyn_hello.c 
$:gcc -o Dyn_hello Dyn_hello.o -L. -lhi

在當前目錄下有了一個Dyn_hello可執行文件,如今就能夠執行它了。 代碼:

$:./Dyn_hello 
./Dyn_hello: error while loading shared libraries: libhi.so: cannot open shared object file: No such file or directory

執行不成功,這就代表了這是一個動態鏈接的程序,鏈接器找不到libhi.so這個動態鏈接庫。在命令行加上 LD_LIBRARY_PATH=...就好了。像這樣運行: 代碼:

$:D_LIBRARY_PATH=. ./Dyn_hello Hello world

指出當前目錄是鏈接器的搜索目錄,就能夠了。

Elf可執行文件還有一個a.out很難實現的特色,就是對dlopen()函數的支持,這個函數能夠在程序中控制動態的加載動態鏈接庫,看下面的一個小程序: 代碼:

$:cat Dl_hello.c 
    #include <dlfcn.h>
     int main (int argc, char *argv[]) 
    { 
        void (*hi) (); 
        void *m; 
        if (argc > 2) exit (0); 
        m = dlopen (argv[1], RTLD_LAZY); 
        if (!m) exit (0); 
        hi = dlsym (m, "hi"); 
        if (hi) { (*hi) (); } 
        dlclose (m); 
    }

用一下命令編譯: 代碼:

$:gcc -c Dl_hello.c 
$:gcc -o Dl_hello Dl_hello.o -ldl

運行Dl_hello程序加上動態鏈接庫。 代碼:

$:./Dl_hello ./libhi.so Hello world

命令行成功的打印出了Hello world說明咱們的動態鏈接庫運用成功了。

在這篇文章中只是討論了elf可執行文件的執行原理,還有不少方面沒有涉及到,要深刻了解elf你也許須要對動態鏈接器hack一下,也要hack一下內核加載程序的loader。可是我想對大多數人來講,這篇文章對elf的介紹已經足夠讓你能夠本身對elf在進行比較深刻的研究了。

相關文章
相關標籤/搜索