學(xué)習(xí)ELF文件,除了要學(xué)習(xí)其文件格式本身,不可避免要了解其可執(zhí)行文件的鏈接過程。這樣可以為后續(xù)學(xué)習(xí)Linux/Android的hook打下基礎(chǔ)。
這個系列文章,是筆者自己學(xué)習(xí)ELF的筆記的重新整理。
另外,這里推薦幾本相關(guān)書籍,是筆者在學(xué)習(xí)ELF相關(guān)內(nèi)容時讀過的:
- 《計算機(jī)系統(tǒng)基礎(chǔ)》作者:袁春風(fēng) --> 配套視頻 文章中用到的很多圖來源于課程的PPT
- 《程序員的自我修養(yǎng)》作者:俞甲子
好,下面進(jìn)入正題:
可執(zhí)行文件的生成
我們先通過一個簡單C程序,回顧一下可執(zhí)行文件的生成過程。

可執(zhí)行文件的生成過程如下圖:

如圖,可執(zhí)行文件的生成需要經(jīng)過預(yù)處理、編譯、匯編和鏈接這4個過程。其中:
- 預(yù)處理的工作:
- 刪除 #define 并展開宏定義
- 處理所有的條件預(yù)編譯指令,如 "#if","#ifdef","#endif"等
- 插入頭文件到 "#include" 處,可以遞歸方式進(jìn)行處理
- 刪除所有的注釋
- 添加行號和文件名標(biāo)識,以便編譯時編譯器產(chǎn)生調(diào)試用的行號信息
- 保留所有 #pragma 編譯指令(編譯器需要用)
命令示例如下:- gcc -E hello.c -o hello.i
經(jīng)過預(yù)編譯處理后,得到的是預(yù)處理文件(如,hello.i),它還是一個可讀的文本文件,但不包含任何宏定義。
- 編譯的工作
編譯過程就是將預(yù)處理后得到的預(yù)處理文件(如hello.i)進(jìn)行詞法分析、語法分析、語義分析、優(yōu)化后,生成匯編代碼文件。
經(jīng)過編譯后,得到的匯編代碼文件(如,hello.S)還是一個可讀的文本文件。
命令示例如下:
- gcc -S hello.i -o hello.s
- gcc -S hello.c -o hello.s
- 匯編的工作
匯編器將編譯得到的匯編代碼文件轉(zhuǎn)換成機(jī)器指令序列。
匯編的結(jié)果是一個可重定位目標(biāo)文件(如,hello.o)其中包含的是不可讀的二進(jìn)制代碼。
命令示例如下:
- gcc -c hello.s -o hello.o
- gcc -c hello.c -o hello.o
- as hello.s -o hello.o
- 鏈接的工作
鏈接過程將多個可重定位目標(biāo)文件合并以生成可執(zhí)行目標(biāo)文件。
命令示例如下:
- gcc -static -o myproc main.o test.o
- ld -static -o myproc main.o test.o
鏈接的好處
學(xué)習(xí)鏈接之前,可能會有疑問,鏈接有什么好處?
其實鏈接的概念可以追溯到最早程序員用機(jī)器語言編寫程序的時期。
來看下圖,假設(shè)穿孔表示0,未穿孔為1,且 0010代表跳轉(zhuǎn)jmp

如果要在第5條指令前加入指令,則程序員就得重新計算jmp指令的目標(biāo)地址(重定位),然后重新打孔。由此可以看到,這樣很繁瑣。
后來匯編語言的出現(xiàn)后,程序員用助記符表示操作碼,用符號表示位置,用助記符表示寄存器,如下圖:

如圖,可見,如果jmp L0 和 sub C之間加入了新的指令,則只要重新確定sub C指令的地址,再填入L0即可。而這個重定位的工作就是在鏈接的過程中完成的。
此外,鏈接還有如下好處:
- 模塊化
- 一個程序可以分成很多源程序文件;
- 可構(gòu)建公共函數(shù)庫,如數(shù)學(xué)庫,標(biāo)準(zhǔn)C庫等。以便代碼重用,提高開發(fā)效率。
- 效率高
- 時間上,可分開編譯:只需要重新編譯修改的源程序文件,然后重新鏈接;
- 空間上,無需包含共享庫所有代碼:源文件中無需包含共享庫函數(shù)的源碼,只要直接調(diào)用即可(如,只要直接調(diào)用printf() 函數(shù),無需包含其源碼),另外,可執(zhí)行文件和運(yùn)行時的內(nèi)存中只需包含所調(diào)用函數(shù)的代碼,而不需要包含整個共享庫。
鏈接的步驟
再來看個C程序的例子:

通過命令生成可執(zhí)行程序:
gcc -O2 -g -o p main.c swap.c
其生成過程如下圖:

其中,鏈接的步驟如下
- Step-1:符號解析
程序中有定義和引用的符號(包括變量和函數(shù)等)
void swap() {...} /* 定義符號swap */ swap(); /* 引用符號swap */ int *xp = &x; /* 定義符號xp,引用符號x */編譯器將定義的符號存放在符號表中。
符號解析就是將符號的引用和符號的定義建立關(guān)聯(lián)
- Step-2:重定位
- 將多個代碼段與數(shù)據(jù)段分別合并為一個單獨的代碼段和數(shù)據(jù)段
- 計算每個定義的符號在虛擬地址空間中的絕對地址
- 將可執(zhí)行文件中的符號引用處的地址修改為重定位后的地址信息
這個步驟可用下圖來簡單描述:
