一、編譯、鏈接和裝載:拆解程序執(zhí)行
C 語言代碼,可編譯成匯編代碼,匯編器變成 CPU 可以理解機器碼,CPU執(zhí)行機器碼。C 語言程序是如何變成可執(zhí)行程序。
通過 gcc 生成的文件和 obj dump 獲取到的匯編指令都有些小小的問題。我們先把前面的 add 函數(shù)示例,拆分成兩個文件 add_lib.c 和link_example.c。


我們通過 gcc 來編譯這兩個文件,然后通過 obj dump 命令看看它們的匯編代碼。
$ gcc -g -c? add_lib.c link_example.c
$ obj dump -d -M? intel -S add_lib.o
$ obj dump -d -M? intel -S link_example.o


既然代碼已經(jīng)“編譯”成了指令,運行一下./link_example.o。文件沒有執(zhí)行權(quán)限Permission denied 錯誤。賦予權(quán)限,./link_example.o仍 cannot execute binary file: Exec format error 的錯誤。
看obj dump 出來代碼,兩個程序的地址都是從 0 開始。如果地址一樣,程序如果通過 call 指令調(diào)用函數(shù)的話,怎么知道應(yīng)該跳轉(zhuǎn)到哪一個文件里呢?
無論運行報錯,還是地址重復(fù),都因為 add_lib.o 以及l(fā)ink_example.o 并不是一個可執(zhí)行文件(Executable Program),而是目標(biāo)文件(Object File)。只有通過鏈接器(Linker)把多個目標(biāo)文件以及調(diào)用的各種函數(shù)庫鏈接起來,我們才能得到一個可執(zhí)行文件。
gcc 的 -o 參數(shù),生成對應(yīng)可執(zhí)行文件,對應(yīng)執(zhí)行之后,得函數(shù)的結(jié)果。
$ gcc -o? link-example add_lib.o link_example.o
$ ./link_example
c = 15
二、“C 語言代碼 - 匯編代碼 - 機器碼” 由兩部分組成:
(1)編譯(Compile)、匯編(Assemble)、鏈接(Link)生成可執(zhí)行文件。
(2)通過裝載器(Loader)把可執(zhí)行文件裝載(Load)到內(nèi)存中。CPU 從內(nèi)存中讀取指令和數(shù)據(jù),真正執(zhí)行程序。

ELF格式和鏈接:理解鏈接過程
程序最終是通過裝載器變成指令和數(shù)據(jù),生成可執(zhí)行代碼不僅是一條條的指令。還是通過 ob jdump 指令,可執(zhí)行文件的內(nèi)容拿出來看:

可執(zhí)行代碼 dump 出來內(nèi)容,之前的目標(biāo)代碼長得差不多,但長了很多。在 Linux 下,可執(zhí)行文件和目標(biāo)文件所使用的都是一種叫ELF(Execuatable? and Linkable File Format)的文件格式,中文名字叫可執(zhí)行與可鏈接文件格式,存匯編指令(編譯成)和別的數(shù)據(jù)。
所有 obj dump 出來的代碼里,可以看到對應(yīng)的函數(shù)名稱,像 add、main 等等,乃至你自己定義的全局可以訪問的變量名稱,都存在 ELF 里。名字和它們對應(yīng)的地址,ELF 里存儲符號表(Symbols Table)里(相當(dāng)于地址簿,關(guān)聯(lián)名字和地址)
add 的跳轉(zhuǎn)地址,不再是下一條指令的地址了,是 add 函數(shù)入口地址,這就是 EFL 格式和鏈接器的功勞。

ELF 把信息,分成一個個 Section 存起來。文件頭(File Header):文件的基本屬性,是否是可執(zhí)行文件,對應(yīng)的 CPU、操作系統(tǒng)等。
1. text Section:代碼段或者指令段(Code Section),保存代碼和指令;
2. data Section:數(shù)據(jù)段(Data Section),初始化數(shù)據(jù)信息
3. rel.text Secion:重定位表(Relocation Table)。當(dāng)前的文件里面,哪些跳轉(zhuǎn)地址其實是我們不知道的。比如link_example.o調(diào)用 add 和 printf,鏈接發(fā)生之前,并不知道該跳轉(zhuǎn)到哪里,這些信息就會存儲在重定位表里;
4.symtab Section:符號表(Symbol Table)。當(dāng)前文件里面定義的函數(shù)名稱和對應(yīng)地址的地址簿。
鏈接器:掃描所有輸入的目標(biāo)文件,收集符號表里信息,構(gòu)成全局符號表。根據(jù)重定位表,把不確定跳轉(zhuǎn)地址代碼,根據(jù)符號表里面存儲的地址修正。把目標(biāo)文件合并,變成可執(zhí)行代碼。這也是為什么,可執(zhí)行文件里面的函數(shù)調(diào)用的地址都是正確的。

裝載器執(zhí)行程序。不需要考慮地址跳轉(zhuǎn)的問題,只需解析 ELF 文件,把對應(yīng)的指令和數(shù)據(jù),加載到內(nèi)存里面CPU 執(zhí)行。
總結(jié)延伸
為什么同樣一個程序, Linux 下可執(zhí)行 Windows 不能。格式不一樣(可執(zhí)行文件)。
(1)Linux 下ELF 文件格式, Windows 可執(zhí)行文件格式叫作PE(Portable Executable Format)。Linux 下的裝載器只能解析 ELF 格式不能解析 PE。
能解析 PE 格式裝載器,就可 Linux 下運行 Windows 程序。例:Wine(Linux 下)兼容 PE 格式的裝載器,Windows 里也提供了 WSL(Windows? Subsystem for Linux) ,可解析加載 ELF 格式的文件。
(2)我們不僅是把代碼放在文件里編譯執(zhí)行,可拆分成不同函數(shù)庫,通過一個靜態(tài)鏈接的機制,文件之間:有分工,有合作(通過靜態(tài)鏈接),變成可執(zhí)行程序。
?ELF 文件,為了能夠?qū)崿F(xiàn)靜態(tài)鏈接的機制,羅列程序所需要執(zhí)行指令,包括鏈接所需要的重定位表和符號表。
課后思考
想要更深入了解程序的鏈接過程和 ELF 格式,我推薦你閱讀《程序員的自我修養(yǎng)——鏈接、裝載和庫》的 1~4 章。
通過 readelf 讀取出今天演示程序的符號表,看看符號表里都有哪些信息;然后通過 objdump 讀取出今天演示程序的重定位表,看看里面又有哪些信息。