整個過程
- 預處理器:將.c 文件轉化成 .i文件,使用的gcc命令是:gcc –E,對應於預處理命令cpp;
- 編譯器:將.c/.h文件轉換成.s文件,使用的gcc命令是:gcc –S,對應於編譯命令 cc –S;
- 彙編器:將.s 文件轉化成 .o文件,使用的gcc 命令是:gcc –c,對應於彙編命令是 as;
- 連接器:將.o文件轉化成可執行程序,使用的gcc 命令是: gcc,對應於連接命令是 ld;
- 加載器:將可執行程序加載到內存並進行執行,loader和ld-linux.so。
過程詳解
1. 預編譯
在正式的編譯階段以前進行。預處理階段將根據已放置在文件中的預處理指令來修改源文件的內容。linux
- 宏定義指令,如 #define a b 對於這種僞指令,預編譯所要作的是將程序中的全部a用b替換,但做爲字符串常量的 a則不被替換。還有 #undef,則將取消對某個宏的定義,使之後該串的出現再也不被替換。
- 條件編譯指令,如#ifdef,#ifndef,#else,#elif,#endif等。 這些僞指令的引入使得程序員能夠經過定義不一樣的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些沒必要要的代碼過濾掉
- 頭文件包含指令,如#include "FileName"或者#include 等。 該指令將頭文件中的定義通通都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。
- 特殊符號,預編譯程序能夠識別一些特殊的符號。 例如在源程序中出現的LINE標識將被解釋爲當前行號(十進制數),FILE則被解釋爲當前被編譯的C源程序的名稱。預編譯程序對於在源程序中出現的這些串將用合適的值進行替換。
頭文件的目的主要是爲了使某些定義能夠供多個不一樣的C源程序使用,這涉及到頭文件的定位即搜索路徑問題。頭文件搜索規則以下:程序員
- 全部header file的搜尋會從-I開始
- 而後找環境變量 C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH指定的路徑
- 再找默認目錄(/usr/include、/usr/local/include、/usr/lib/gcc-lib/i386-linux/2.95.2/include......)
2. 編譯
經過詞法分析和語法分析,在確認全部的指令都符合語法規則以後,將其翻譯成等價的中間代碼表示或彙編代碼。函數
3. 彙編
彙編器(as)把彙編語言代碼翻譯成目標機器指令(.o)。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。目標文件由段組成。一般一個目標文件中至少有兩個段:工具
- 代碼段:該段中所包含的主要是程序的指令。該段通常是可讀和可執行的,但通常卻不可寫。
- 數據段:主要存放程序中要用到的各類全局變量或靜態的數據。通常數據段都是可讀,可寫,可執行的。
4. 連接
將有關的目標文件彼此相鏈接生成可加載、可執行的目標文件。連接器的核心工做就是符號表解析和重定位。編碼
4.1 連接的時機
- 編譯時,就是源代碼被編譯成機器代碼時(靜態連接器負責);
- 加載時,也就是程序被加載到內存時(加載器負責);
- 運行時,由應用程序來實施(動態連接器負責)。
4.2 連接的做用
- 使得分離編譯成爲可能;
- 動態綁定(binding):使定義、實現、使用分離
4.3 靜態庫搜索路徑
- gcc先從-L尋找;
- 再找環境變量LIBRARY_PATH指定的搜索路徑;
- 再找內定目錄 /lib /usr/lib /usr/local/lib 這是當初compile gcc時寫在程序內的。
4.4 動態庫搜索路徑
- 編譯目標代碼時指定的動態庫搜索路徑-L;
- 環境變量LD_LIBRARY_PATH指定的動態庫搜索路徑;
- 配置文件/etc/ld.so.conf中指定的動態庫搜索路徑;
- 默認的動態庫搜索路徑/lib /usr/lib/ /usr/local/lib
4.5 靜態連接(編譯時)
連接器將函數的代碼從其所在地(目標文件或靜態連接庫中)拷貝到最終的可執行程序中。這樣該程序在被執行時這些代碼將被裝入到該進程的虛擬地址空間中。靜態連接庫其實是一個目標文件的集合,其中的每一個文件含有庫中的一個或者一組相關函數的代碼。
爲建立可執行文件,連接器必需要完成的主要任務:spa
- 符號解析:把目標文件中符號的定義和引用聯繫起來;
- 重定位:把符號定義和內存地址對應起來,而後修改全部對符號的引用。
重定位
讓咱們結合具體的CPU指令來了解這個過程。假設咱們有個全局變量叫作var,它在目標文件A中。咱們在目標文件B裏面要訪問這個全局變量,好比咱們在目標文件B裏面有這麼一條指令: movl $0x2a, var
這條指令就是給這個var變量賦值0x2a,至關於C語言中的語句var = 42。而後咱們編譯目標文件B,獲得這條指令機器碼翻譯
因爲在編譯目標文件B的時候,編譯器並不知道變量var的目標地址,因此編譯器在無法肯定地址的狀況下,將這條mov指令的目標地址設爲0,等待連接器在將目標文件A和B連接起來的時候再將其修正。假設A和B連接後,變量var的地址肯定下來爲0x1000,那麼連接器將會把這個指令的目標地址部分修改爲0x10000。這個地址修正的過程也叫作重定位(Relocation),每一個要被修正的地方叫一個重定位入口(Relocation Entry)。
每一個目標文件除了擁有本身的數據和二進制代碼外,還提供了三個表:未解決符號表、導出符號表、地址重定向表。code
-
未解決符號表提供了全部在該編譯單元裏引用可是定義並非在本編譯單元的符號以及其出現的地址;對象
- 導出符號表提供了本編譯單元具備定義,而且願意提供給其餘單元使用的符號及其地址;
- 地址重定向表提供了本編譯單元全部對自身地址的引用的記錄;
編譯器將extern聲明的變量置入未解決符號表,而不置入導出符號表;----外部連接
編譯器將static聲明的全局變量不置入未解決符號表,也不置入導出符號表,所以其餘單元沒法使用;----內部連接
普通變化及其函數被置入導出符號表;進程
4.6 動態連接(加載、運行時)
在此種方式下,函數的定義在動態連接庫或共享對象的目標文件中。在編譯的連接階段,動態連接庫只提供符號表和其餘少許信息用於保證全部符號引用都有定義,保證編譯順利經過。動態連接器(ld-linux.so)連接程序在運行過程當中根據記錄的共享對象的符號定義來動態加載共享庫,而後完成重定位。在此可執行文件被執行時,動態連接庫的所有內容將被映射到運行時相應進程的虛地址空間。動態連接程序將根據可執行程序中記錄的信息找到相應的函數代碼。
各類文件
ELF Executable
ELF exectuable File |
內容 |
文件頭 |
描述文件屬性, 段表, 重定位表 |
代碼段 .code .text |
源代碼編譯後的機器指令, 程序的指令 |
數據段.data |
已初始化的全局變量,局部靜態變量 |
.bss |
未初始化的全局變量, 局部靜態變量 |
.symtab |
存放在程序中定義和引用的函數和全局變量的信息符號表 |
目標文件
- 可重定位(Relocatable)文件:由編譯器和彙編器生成,能夠與其餘可重定位目標文件合併建立一個可執行或共享的目標文件;
- 共享(Shared)目標文件:一類特殊的可重定位目標文件,能夠在連接(靜態共享庫)時加入目標文件或加載時或運行時(動態共享庫)被動態的加載到內存並執行;
- 可執行(Executable)文件:由連接器生成,能夠直接經過加載器加載到內存中充當進程執行的文件。
靜態庫(Archive FIle)
多個.o文件的集合.Linux中默認後綴是.a, 靜態庫的的.o沒有進行連接,只是.o的集合。
共享目標文件
包含了代碼和數據,能夠在兩種狀況下使用
- 連接器可使用這種文件跟其餘的可重定位文件和共享目標文件連接,產生新的目標文件
- 動態連接器能夠將幾個這種共享目錄文件與可執行文件結合,做爲進程映像的一部分來運行
GNU工具
gnu下提供了不少工具來幫助處理目標文件:
- AR : 建立靜態庫,插入、刪除、列出和提取成員;
- STRINGS : 列出目標文件中全部能夠打印的字符串;
- STRIP : 從目標文件中刪除符號表信息;
- NM : 列出目標文件符號表中定義的符號;
- SIZE : 列出目標文件中節的名字和大小;
- READELF : 顯示一個目標文件的完整結構,包括ELF 頭中編碼的全部信息。
- OBJDUMP : 顯示目標文件的全部信息,最有用的功能是反彙編.text節中的二進制指令。
- LDD : 列出可執行文件在運行時須要的共享庫。