08 | ELF和靜態(tài)鏈接:為什么程序無法同時在Linux和Windows下運行?

一、編譯、鏈接和裝載:拆解程序執(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 讀取出今天演示程序的重定位表,看看里面又有哪些信息。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容