本文簡單介紹了程序的連接原理。學習連接原理有助於程序員理解程序的本質,同時也能夠爲往後的大型軟件的代碼開發打下堅實的基礎。由此可知連接原理的重要性,尤爲是一些程序員被一些莫名其妙的錯誤困擾的時候,更加可以體會到這一點。程序員
鏈接器將多個目標文件連接成一個完整的、可加載、可執行的目標文件。其輸入是一組可重定位的目標文件。連接的兩個主要任務以下:函數
(1) 符號解析,將目標文件內的引用符號和該符號的定義聯繫起來。工具
(2) 將符號定義與存儲器的位置聯繫起來,修改對這些符號的引用。學習
典型的目標文件分爲如下3種形式:ui
(1) 可重定位目標文件this
這種文件包含二進制代碼和數據,這些代碼和數據已經轉換成了機器指令代碼和數據,可是還不能夠直接執行。由於這些指令和數據中每每引用其餘模塊(目標文件)中的符號,這些其餘模塊的符號對於本模塊來講是未知的,這些符號的解析須要連接器將全部模塊進行連接。這種操做稱爲「重定位」,所以,這種目標文件被稱爲「可重定位的目標文件」,後綴名一般爲*.ospa
(2) 可執行目標文件debug
這種文件一樣包含了二進制代碼和數據。所不一樣的是,這種文件已經通過了連接操做,和全部的模塊(目標文件)都產生了聯繫。連接器將全部須要的可重定位目標文件鏈接成一個可執行目標文件。這時,每一個目標文件中引用其餘目標文件中的符號都已經獲得瞭解析和重定位。所以,每一個符號都是已知的了,該文件能夠被機器直接執行。3d
(3) 共享目標文件調試
這是一種特殊的可定位目標文件,能夠在須要它的程序運行或加載時,動態地加載到內存中運行。這種文件的後綴名一般是*.so。共享目標文件一般又被稱爲「動態庫」文件或者「共享庫」文件。
下面的示例演示了可重定位目標文件和可執行目標文件的產生。該程序使用兩個簡單的C語言源程序add.c和main.c文件,其中add.c中定義一個函數add(),實現兩個整數相加;main.c中定義了main函數,在該函數中調用add()函數。
//@file add.c //@brief sum 2 integers int add(int a, int b) { return (a+b); }
//@file main.c //@brief call add() from another file #include <stdio.h> #include <stdlib.h> extern int add(int,int); int main(int argc, char *argv[]) { int a, b; if (argc != 3) { printf("Usage: main a b\n"); exit(-1); } a = atoi(argv[1]); b = atoi(argv[2]); printf("Sum = %d\n", add(a, b)); return 0; }
那麼,咱們使用ld命令連接兩個文件,會提示如下錯誤,我我的以爲是由於代碼中使用到了<stdio>和<stdlib>庫中的函數,可是並無指定對應的目標文件致使。固然,我函數習慣直接使用gcc命令來鏈接這兩個文件,最終運行效果以下:
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ld add.o main.o –o main ld: warning: cannot find entry symbol _start; defaulting to 0000000008048074 main.o: In function `main': main.c:(.text+0x17): undefined reference to `puts' main.c:(.text+0x23): undefined reference to `exit' main.c:(.text+0x33): undefined reference to `atoi' main.c:(.text+0x47): undefined reference to `atoi' main.c:(.text+0x6f): undefined reference to `printf' xiaomanon@xiaomanon-machine:~/Documents/c_code$ gcc add.o main.o -o main
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./main 12 19 Sum = 31
補充:關於ld的用法?
咱們接下來就來解決上面使用ld命令連接可重定位目標文件時出錯的問題。提示信息中,第一個warningd的意思是沒有找到一個函數入口,咱們可使用ld命令的-e選項來指定:
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ls add.c add.o main.c main.o xiaomanon@xiaomanon-machine:~/Documents/c_code$ ld -e main main.o
main.o: In function `main': main.c:(.text+0x29): undefined reference to `add'
這裏,又有一個錯誤提示:沒有定義add,咱們在其中添加對add.o的連接。
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ld -e main main.o add.o xiaomanon@xiaomanon-machine:~/Documents/c_code$ ls add.c add.o a.out main.c main.o
咱們能夠看到,最終生成了a.out文件,運行它:
xiaomanon@xiaomanon-machine:~/Documents/c_code$ ./a.out
Segmentation fault (core dumped)
結果出現了段錯誤,這是問什麼呢?應該怎麼解決?
ELF(Excutable Linkable File)是Linux環境下最經常使用的目標文件格式,在大多數狀況下,不管是可重定位的目標文件仍是可執行的目標文件都可採用這種格式。ELF格式的目標文件中不只包含了二進制的代碼和數據,還包括不少幫助連接器解析符號和解釋目標文件的信息。下圖展現了一個典型的ELF格式的可重定位目標文件的結構。
該目標文件主要由兩部分組成:ELF文件頭和目標文件的段。ELF文件頭的前16個字節構成了一個字節序,描述了生成該文件系統的字長以及字節序。剩下的部分包括了ELF文件的一些其餘信息,其中包括ELF文件頭的大小、目標文件的類型、目標機的類型、段頭部表在目標文件內的文件偏移位置等。在連接和加載ELF格式的程序時,這些信息是很重要的。
除了ELF文件頭以外,剩下的部分由目標文件的段組成。這些段是ELF文件中的核心部分。由如下幾個段組成:
■ .text : 代碼段,存儲二進制的機器指令,這些指令能夠被機器直接執行。
■ .rodata : 只讀數據段,存儲程序中使用的複雜常量,例如字符串等。
■ .data : 數據段,存儲程序中已經被明確初始化的全局數據。包括C語言中的全局變量和靜態變量。若是這些全局數據被初始化爲0,則不存儲在數據段中,而是被存儲在塊存儲段中。C語言局部變量保存在棧上,不出如今數據段中。
■ .bss : 塊存儲段,存儲未被明確初始化的全局數據。 在目標文件中這個段並不佔用實際的空間,而僅僅是一個佔位符,以告知指定位置上應當預留全局數據的空間。塊存儲段存在的緣由是爲了提升磁盤上存儲空間的利用率。
注意:以上的4個段會在程序運行時加入到內存中,是實實在在的程序段。目標文件中還有一些輔助程序進程連接和加載的信息,這些信息並不加載到內存中。實際上,這些信息在生成最終的可執行目標文件時就已經被去掉了。
■ .symtab : 符號表,存儲定義和引用的函數和全局變量。每一個可重定位的目標文件中都要有一個這樣的表。在該表中,全部引用的本模塊內的全局符號(包括函數和全局變量)以及其餘模塊(目標文件)中的全局符號都會有一個登記。連接中的重定位操做就是將這些引用的全局符號的位置肯定。
■ .rel.text : 代碼段須要重定位(relocate)的信息,存儲須要靠重定位操做修改位置的符號的彙總。這些符號在代碼段中,一般是一個函數名和標號。
■ .rel.data : 數據段須要重定位的信息,存儲須要靠重定位操做修改位置的符號的彙總。這些符號在數據段中,是一些全局變量。
■ .debug : 調試信息,存儲一個用於調試的符號表。在編譯程序時使用gcc編譯器的-g選項會生成該段,該表包括源程序中全部符號的引用和定義,有了這個段在使用gdb調試器對程序進行調試的時候才能夠打印並觀察變量的值。
■ .line : 源程序的行號映射,存儲源程序中每個語句的行號。在編譯程序時使用gcc編譯器的-g選項會生成該段,在使用gdb調試器對程序進行調試的時候這個段的做用很大。
■ .strtab : 字符串表,存儲.symtab符號表和.debug符號表中符號的名字,這些名字是一些字符串,而且以‘\0’結尾。
符號解析是連接的主要任務之一。只有在正確解析了符號以後纔可以更改引用符號的位置,從而完成重定位,生成一個能夠被機器直接加載執行的可執行目標文件。每一個可重定位目標文件都有一個符號表,在這個符號表中存儲符號,這些符號分爲3類:
(1) 本模塊中引用的其餘模塊所定義的全局符號
(2) 本模塊中定義的全局符號
(3) 本模塊中定義和引用的局部符號
注意:局部變量和局部符號不是一回事。局部變量存儲在棧中,是一個僅僅在內存中出現的概念;而局部符號包括靜態變量和局部標號,這些內容也可能出如今磁盤文件中。
下面代碼演示了在程序中使用局部符號。該程序聲明瞭一個靜態局部變量和一個局部變量,其中靜態局部變量是一個局部符號。
//@file cnt.c #include <stdio.h> void f(int i) { int static count = 10; int a = 0; count = i; count++; if (count >= 20) goto done; else{ printf("the count is lower than 20\n"); return; } done: printf("the count is higher than 20\n"); a = 20; printf("a is : %d\n", a); return; } int main(void) { int i; scanf("%d", &i); f(i); return 0; }
該程序中局部靜態變量count和標號done都是局部符號,會出如今目標文件的符號表中,而局部變量a存儲在棧上,所以不會出如今符號表中。
而後使用gcc –c cnt.c命令,編譯獲得可重定位目標文件cnt.o,這樣咱們就可使用GNU的readelf工具查看可重定位目標文件內容,該工具能夠讀物目標文件的符號表,從而獲得每個符號的信息。
xiaomanon@xiaomanon-machine:~/Documents/c_code$ readelf -a cnt.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: 500 (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: 13 Section header string table index: 10 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 000095 00 AX 0 0 1 [ 2] .rel.text REL 00000000 000520 000068 08 11 1 4 [ 3] .data PROGBITS 00000000 0000cc 000004 00 WA 0 0 4 [ 4] .bss NOBITS 00000000 0000d0 000000 00 WA 0 0 1 [ 5] .rodata PROGBITS 00000000 0000d0 000045 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 000115 000025 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 00013a 000000 00 0 0 1 [ 8] .eh_frame PROGBITS 00000000 00013c 000058 00 A 0 0 4 [ 9] .rel.eh_frame REL 00000000 000588 000010 08 11 8 4 [10] .shstrtab STRTAB 00000000 000194 00005f 00 0 0 1 [11] .symtab SYMTAB 00000000 0003fc 0000f0 10 12 10 4 [12] .strtab STRTAB 00000000 0004ec 000034 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific) There are no section groups in this file. There are no program headers in this file. Relocation section '.rel.text' at offset 0x520 contains 13 entries: Offset Info Type Sym.Value Sym. Name 00000011 00000301 R_386_32 00000000 .data 00000016 00000301 R_386_32 00000000 .data 0000001e 00000301 R_386_32 00000000 .data 00000023 00000301 R_386_32 00000000 .data 00000030 00000501 R_386_32 00000000 .rodata 00000035 00000b02 R_386_PC32 00000000 puts 0000004a 00000501 R_386_32 00000000 .rodata 0000004f 00000c02 R_386_PC32 00000000 printf 00000059 00000501 R_386_32 00000000 .rodata 0000005e 00000b02 R_386_PC32 00000000 puts 00000079 00000501 R_386_32 00000000 .rodata 0000007e 00000e02 R_386_PC32 00000000 __isoc99_scanf 0000008a 00000a02 R_386_PC32 00000000 f Relocation section '.rel.eh_frame' at offset 0x588 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000020 00000202 R_386_PC32 00000000 .text 00000040 00000202 R_386_PC32 00000000 .text The decoding of unwind sections for machine type Intel 80386 is not currently supported. Symbol table '.symtab' contains 15 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS cnt.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 3 4: 00000000 0 SECTION LOCAL DEFAULT 4 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 4 OBJECT LOCAL DEFAULT 3 count.1826 7: 00000000 0 SECTION LOCAL DEFAULT 7 8: 00000000 0 SECTION LOCAL DEFAULT 8 9: 00000000 0 SECTION LOCAL DEFAULT 6 10: 00000000 101 FUNC GLOBAL DEFAULT 1 f 11: 00000000 0 NOTYPE GLOBAL DEFAULT UND puts 12: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf 13: 00000065 48 FUNC GLOBAL DEFAULT 1 main 14: 00000000 0 NOTYPE GLOBAL DEFAULT UND __isoc99_scanf No version information found in this file.
能夠看到,ELF格式文件輸出信息的最後是一個符號表,這個符號表揭示了cnt.c源文件中符號的信息。符號表的第5列表示符號的做用域類型,LOCAL表示局部符號,而GLOBAL表明的是全局符號。
開始連接的時候,連接器首先完成的任務就是符號解析。因爲符號已經被肯定,連接器所要作的就是尋找全部參與連接的目標文件,查找這些文件中是否認義了本模塊中還沒有能解析的符號。
若是查找到未解析的符號的定義,則準備開始下一步重定位;若是尋找全部參與連接的目標文件後仍然找不到未解析的符號的定義,則認爲該符號未定義,從而將出錯信息輸出給用戶。當所有的符號都被解析以後,就能夠開始連接的第二個任務——重定位。
當符號解析結束以後,每一個符號的定義位置以及大小都是已知的了。重定位操做只須要將這些符號連接起來。在這個步驟中,連接器須要將全部參與連接的目標文件合併,而且爲每個符號分配存儲內容的運行時地址。重定位分爲如下兩步進行:
(1) 重定位段
這一步將全部目標文件中同類型的段合併,生成一個大段。例如,將全部參與連接的目標文件的數據段合併,生成一個大的數據段;全部目標文件的代碼段也被合併,生成一個大的代碼段,以下圖所示。
合併以後,程序中的指令和變量就擁有一個統一的而且惟一的運行時地址了。
(2) 重定位符號引用
因爲目標文件中相同的段已經合併,所以程序中對富豪的引用位置也就都做廢了。這是連接器須要修改這些引用符號的地址,使其指向正確的運行時地址。
當編譯器生成一個目標文件後,其並不知道代碼和變量最終的存儲位置,也不知道定義在其餘文件中的外部符號。所以,編譯器會生成一個重定位表目,裏面存儲着關於每個符號的信息。這個表目告知連接器在合併目標文件時應該如何修改每一個目標文件中對符號的引用。這種重定位表目存儲在.rel.text段和.rel.data段中。該表目能夠理解爲一個結構體,其中存儲着每個符號的重定位信息。
typedef struct { int offset;/*偏移值*/ int symbol;/*所表明的符號*/ int type;/*符號的類型*/ }symbol_rel;
offset表示該符號在存儲的段中的偏移值。symbol表明該符號的名稱,字符串實際存儲在.strtab段中,這裏存儲的是該字符串首地址的下標。type表示重定位類型,連接器只關心兩種類型,一種是與PC相關的重定位引用,另外一種是絕對地址引用。
PC相關的重定位引用表示將當前的PC值(這個值一般是嚇一跳指令的存儲位置)加上該符號的偏移值。絕對地址引用表示將當前指令中已經指定的地址引用直接做爲跳轉的地址,不須要進行任何修改。
有了這些信息,連接器就能夠將符號在存儲段中的偏移值加上該段在重定位後的新地址,這樣就獲得了一個新的引用地址,而這個引用地址就是該符號的最終地址。一樣,在程序中全部引用該地址的部分都要作修改,使用這個新的絕對地址代替舊的偏移地址。當新的符號地址被修改完畢之後,連接器的工做就結束了。