"連接是將各類代碼和數據部分收集起來並組合成爲一個單一文件的過程",說的通俗一點(不許確),連接就是把編譯生成的*.o文件整合成可執行文件的過程。一般編譯器會幫咱們把預編譯、編譯、彙編和連接的過程都給作了,咱們不常常用到連接器,資料也少,下面就將筆者的體會和整理奉上。linux
注:廣義的編譯指的是預編譯、編譯、彙編和連接整個過程,狹義的編譯指*.i文件生成*.o文件的過程。程序員
一個源程序通過預編譯、編譯、彙編和連接成爲可執行文件,相信你們已經很熟悉了,確定有很多讀者用gcc還原過這個過程,下面咱們再還原一遍,重點關注連接這一步。函數
1. 咱們要編譯的文件列表spa
├── func.c
└── main.c3d
2. 文件內容以下orm
main.cblog
void func(); int main_global = 2; static int static_global = 3; int main() { int local = 3; func(); return 0; }
func.c內存
extern int main_global; void func() { }
第二節 分解編譯過程編譯器
看完上面的文件內容和關係,相信你們用一條命令就生成了可執行文件main:it
# gcc -o main main.c func.c 注:此命令會生成可執行文件main
或者多用幾條命令也可生成:
# gcc -c main.c 注:此命令會生成main.o
# gcc -c func.c 注:此命令會生成func.o
# gcc -o main main.o func.o 注:此命令會生成可執行文件main
下面咱們把編譯過程分解下:
第1步. 預編譯(將宏、頭文件等展開)
# gcc -E main.c -o main.i 注:此命令會生成main.i
# gcc -E func.c -o func.i 注:此命令會生成func.i
第2步. 編譯(生成彙編語言)
# gcc -S main.i -o main.s 注:此命令會生成main.s
# gcc -S func.i -o func.s 注:此命令會生成func.s
第3步. 彙編(生成可重定位目標文件)
# gcc -c main.s -o main.o 注:此命令會生成main.o
# gcc -c func.s -o func.o 注:此命令會生成func.o
第4步. 連接(生成可執行文件)
# gcc -o main main.o func.o 注:此命令會生成可執行文件main
好了,至此,咱們經過gcc將源文件編譯成了可執行文件。
讀者可能會問:說好的連接器ld呢?別急,連接器理應出如今第4步。如今,咱們嘗試用ld連接完成第4步。
1. 使用ld
咱們man了下ld,用法以下:
ld files... [options] [-o outputfile]
因而,咱們滿懷信心,不假思索地寫下了下面的語句:
# ld -o main main.o func.o
2. 出錯
可是,結果並不如預期,出現了以下錯誤:
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000e8
3. 錯誤緣由
錯誤提示說的很明確,找不到入口符號_start ,咱們要在連接的時候指明程序入口。
4. 解決
既然如此,咱們用-e選項指明程序入口。
# ld -o main main.o func.o -e main
連接沒報錯,可是當咱們運行main時,提示Segmentation fault,這是由於連接時還缺乏一些參數。
那麼還缺乏什麼參數呢,讓咱們看下上一節中的第4步,即用gcc連接目標文件的命令:
# gcc -o main main.o func.o 注:此命令連接main.o func.o, 生成可執行文件main
不難猜想,上面的命令用到了連接器, 如何驗證呢,其實熟悉gcc的讀者很清楚,給gcc加個-v參數,就能夠打印gcc的執行過程用到的命令。好了,讓咱們加個-v參數,一探究竟吧!
# gcc -v -o main main.o func.o
打印出以下信息:
...這裏省略了一些打印信息...
/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2 --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/../lib64/crt1.o /usr/lib/../lib64/crti.o /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtbegin.o -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../.. main.o func.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtend.o /usr/lib/../lib64/crtn.o
好了,看到/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2了嗎,這就是個連接器,什麼?不是ld嗎?別慌,collect2只是ld的一個別名。看到-l參數了吧,後面跟的就是連接用到的庫,-L參數是查找路徑。咱們用上面的命令完成最後的連接吧(固然,你能夠把/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2換成ld):
/usr/local/libexec/gcc/x86_64-unknown-linux-gnu/4.8.2/collect2 --eh-frame-hdr -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o main /usr/lib/../lib64/crt1.o /usr/lib/../lib64/crti.o /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtbegin.o -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/../../.. main.o func.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/local/lib/gcc/x86_64-unknown-linux-gnu/4.8.2/crtend.o /usr/lib/../lib64/crtn.o
好了,至此,咱們用完成了連接,並生成了可執行文件,下面就讓咱們看看連接是如何工做的。
爲了便於理解,咱們開篇就說了句通俗易懂的話:「連接就是把編譯生成的*.o文件整合成可執行文件的過程」。那麼是如何整合的呢,難道是把文件內容都拷貝到一個文件中嗎,顯然不是,可是咱們能夠說連接是有規則的拷貝,按照什麼規則呢,要想知道,還得先了解下*.o文件的格式。
咱們把*.o叫目標文件,實際上*.o文件只是目標文件的一種,目標文件的格式在Unix系統下被稱爲ELF格式(Executable and Linking Format,可執行和可連接格式),目標文件有三種:
1. 可重定位目標文件
上面咱們產生的*.o文件即main.o和func.o被稱爲可重定位目標文件,它與其餘可重定位目標文件合併後生成可執行目標文件。典型的(可能還有其餘節)ELF可重定位目標文件格式以下:
ELF header、Segment header table和.init等被稱爲節,每一個節的內容經過節的名字不難推測出來,此處再也不贅述。咱們重點來看下.symtab這個節,.symtab是一個符號表,裏面存了一堆符號(變量、函數等),後面會講到符號解析,說白點,就是查找這張表,找出表裏的符號定義在哪裏。每一個可重定位目標文件在.symtab中都有一張符號表,須要注意的是,這張符號表不包含局部變量信息,每一個可重定位目標文件obj都包含三種不一樣的符號:
在obj中定義的,被其餘文件引用的全局符號(如本文的main_global就是在main.c中定義的,能被其餘文件使用的符號);
由其餘文件定義的,被obj使用的全局符號(如本文func函數就是這樣的符號,它在func.c文件中定義,被main.c文件使用);
只被obj文件定義和使用的全局符號(如本文static_global就是main.c中定義和使用的全局符號,其餘文件不能使用)。
好了,讓咱們看一下main.o的符號表,linux下咱們用readelf命令來查看ELF文件格式信息。
命令:
# readelf -s main.o 參數是小寫s,查看符號表信息
輸出:
.symtab中並無包含咱們在main.c中定義的local變量,連接只關心全局的符號信息。下面命令能夠查看ELF文件節的信息。
命令:
# readelf -S main.o 參數是大寫s,查看節信息
輸出:
2. 可執行目標文件
咱們生成的main就是可執行目標文件,它能夠被加載到內存中運行,它的格式和可重定位目標文件相似,以下圖所示,須要注意的是,其頭部包括程序的入口點(entry point),也就是文件被載入內存後要執行的第一條指令的地址。
咱們來看下可執行目標文件中的節:
命令:
# readelf -S main 參數是大寫s,查看節信息
輸出:
咱們能夠看到Addr一列中,已是非0值,說明能夠載入內存了,而可重定位目標文件main.o中的Addr一列爲0.
3. 共享目標文件
一種特殊的可重定位目標文件,能夠在運行時被動態地加載到內存中連接,如一些動態庫.so文件,此處不討論。
連接就是把一些類似的段合併到一塊兒的過程,以下圖所示:
這個合併要分兩步完成,第一步是分析每一個可重定位目標文件中段的屬性、長度和位置,進行地址分配;第二步是重定位,就是把符號引用和符號定義關聯起來。
第一步 分配地址空間:
咱們來看下連接先後段屬性的變化。
命令:
# objdump -h main.o
輸出:
命令:
# objdump -h func.o
輸出:
命令:
# objdump -h main
輸出(省略了不關心的信息):
上面輸出結果中VMA一列表示Virtual Memory Adress,即虛擬地址,咱們看到main.o和func.o的VMA都是0,由於還沒分配地址空間,而在可執行文件main中已經有了值0x0804****(32位從0x08048000開始,64位從0x00400000開始),說明分配了地址空間。
第二步 符號解析和重定位:
咱們來看下main.o中的符號:
咱們看到main.o中的符號func前面有個標誌U,即Undefined未定義的,這是顯然的,由於main.c中用到的函數func是在func.c中定義的,咱們在編譯main.c時,並不知道func在哪裏,那麼我麼是何時知道它們在哪裏呢,答案是連接後。咱們來看下連接後的可執行文件main的符號:
命令:
# nm main
輸出:
D:Global data 符號,T:Global text符號。在連接事後,能夠找到符號了,關於連接的詳細介紹參考本文最後給出的參考文獻。
(未完待續)
參考:
1. Randal E. Bryant..;<<Computer Systems: A Programmer's Perspective>>.
2.《程序員的自我修養:連接、裝載和庫》