GCC 編譯全過程

現代編譯器常見的編譯過程:
源文件-->預處理-->編譯/優化-->彙編-->鏈接-->可執行文件

對於gcc而言:

第一步 預處理
       命令: gcc -o test.i -E test.c
             或者 cpp -o test.i test.c (這裏cpp不是值c plus plus,而是the C Preprocessor)
       結果:  生成預處理後的文件test.i(可以打開後與預處理前進行比對,當然長度會嚇你一跳)    
       作用:  讀取c源程序,對僞指令和特殊符號進行處理。包括宏,條件編譯,包含的頭文件,以及一些特殊符號。基本上是一個replace的過程。

第二步 編譯及優化
        命令:  gcc -o test.s -S test.i
             或者 /路徑/cc1 -o test.s test.i
        結果: 生成彙編文件test.s(可打開後查看源文件生成的彙編碼)
        作用: 通過詞法和語法分析,確認所有指令符合語法規則(否則報編譯錯),之後翻譯成對應的中間碼,在linux中被稱爲RTL(Register Transfer Language),通常是平臺無關的,這個過程也被稱爲編譯前端。編譯後端對RTL樹進行裁減,優化,得到在目標機上可執行的彙編代碼。gcc採用as 作爲其彙編器,所以彙編碼是AT&T格式的,而不是Intel格式,所以在用gcc編譯嵌入式彙編時,也要採用AT&T格式。
       
第三步 彙編
        命令: gcc -o test.o -c test.s
               或者 as -o test.o test.s
        結果:   生成目標機器指令文件test.o(可用objdump查看)
        作用:  把彙編語言代碼翻譯成目標機器指令, 用file test.o 可以看到test.o是一個relocatable的ELF文件,通常包含.text .rodata代碼段和數據段。可用readelf -r test.o查看需要relocation的部分。
       
第四步 鏈接
        命令: gcc -o test test.o
               或者 ld -o test test.o
        結果:   生成可執行文件test (可用objdump查看)
        作用:  將在一個文件中引用的符號同在另外一個文件中該符號的定義鏈接起來,使得所有的這些目標文件鏈接成爲一個能被操作系統加載到內存的執行體。(如果有不到的符號定義,或者重複定義等,會報鏈接錯)。用file test 可以看到test是一個executable的ELF文件。
       
        當然鏈接的時候還會用到靜態鏈接庫,和動態連接庫。靜態庫和動態庫都是.o目標文件的集合。
        靜態庫:
        命令:ar -v -q test.a test.o
        結果: 生成靜態鏈接庫test.a
        作用: 靜態庫是在鏈接過程中將相關代碼提取出來加入可執行文件的庫(即在鏈接的時候將函數的代碼將從其所在地靜態鏈接庫中被拷貝到最終的可執行程序中),ar只是將一些別的文件集合到一個文件中。可以打包,當然也可以解包。
       
        動態庫: 
        命令:  gcc -shared test.so test.o
             或者/PATH/collect2 -shared test.so test.o (省略若干參數)
        結果:  生成動態連接庫test.so
        作用: 動態庫在鏈接時只創建一些符號表,而在運行的時候纔將有關庫的代碼裝入內存,映射到運行時相應進程的虛地址空間。如果出錯,如找不到對應的.so文件,會在執行的時候報動態連接錯(可用LD_LIBRARY_PATH指定路徑)。用file test.so可以看到test.so是shared object的ELF文件。
       
當然以上各步可以一步或若干步一起完成,如gcc -o test test.c直接得到可執行文件。

附:
    ELF文件格式
    ELF文件格式是ABI(Application Binary Interface)的一部分,被Tool Interface Standards committee作爲在32位Intel架構下可移植的目標文件格式。其格式比較複雜,這裏就不細講了,只說說其類型。
    在specification 1.1中定義了的類型。表示在ELF header中的e_type
    Name        Value  Meaning
    ====        =====  =======
    ET_NONE         0  No file type
    ET_REL          1  Relocatable file
    ET_EXEC         2  Executable file
    ET_DYN          3  Shared object file
    ET_CORE         4  Core file
    ET_LOPROC  0xff00  Processor-specific
    ET_HIPROC  0xffff  Processor-specific
   
    主要的有4種
    1. Relocatable file 保留了代碼和數據,被用來和其他的object file一起創建可執行的文件或者是shared object file. (也就是我們常見的.o文件)
    2. Executable file 保留了用來執行的程序,該文件可以被系統exec()加載用以創建程序進程。(也就是我們常說的可執行文件)
    3. Shared object file 保留了代碼和數據,以在兩種情況下被連接,一是link editor如ld,可以用它與其他的Relocateble或者Shared的object file一起創建另一個object file. 二是與Executable file或者其他的Shared object file動態鏈接成爲一個進程映像。(也就是我們常說的動態鏈接庫,或者.so文件)   
    4. Core file 的內容在規範中沒有指明,目前多用來記錄core dump信息。

=============================================================================

本文對gcc 編譯器如何工作做一個概要描述.   更爲詳細的信息請參考編譯器手冊。 
     當我們進行編譯的時候,要使用一系列的工具,我們稱之爲工具鏈.其中包括:預處理器CPP,編譯器前端gcc/g++,彙編器as,連接器ld. 一個編譯過程包括下面幾個階段:
 (1)預處理.預處理器CPP將對源文件中的宏進行展開。
 (2)編譯.gcc將c文件編譯成彙編文件。
 (3)彙編。as將彙編文件編譯成機器碼。
 (4)連接。ld將目標文件和外部符號進行連接,得到一個可執行二進制文件。 
 
  下面以一個很簡單的test.c來探討這個過程。
 
 #define NUMBER  (1+2)
 int main()
{      
   int x=NUMBER;       
   return 0;  
}
 
(1)預處理:gcc會首先調用CPP進行預處理: CPP test.c >test.i
預處理的輸出爲文件test.i。 我們用cat test.i查看test.i的內容如下:
 int main()
{  
  int x=(1+2);   
   return 0;
 }
 
我們可以看到,文件中宏定義NUMBER出現的位置被(1+2)替換掉了,其它的內容保持不變。
 
 (2)gcc將c文件編譯成彙編文件。
 接下來gcc會執行gcc -S test.i得到的輸出文件爲test.s .
 
 (3)as將彙編文件編譯成機器碼。
as test.s -o test.o得到輸出文件爲test.o.  test.o中爲目標機器上的二進制文件. 用nm查看文件中的符號: nm test.o輸出如下:  
 00000000 b .bss 
 00000000 d .data     
 00000000 t .text   
 U ___main    
 U __alloca 
 00000000 T _main
 
既然已經是二進制目標文件了,能不能執行呢?試一下./test.o,提示cannot execute binary file.原來___main前面的U表示這個符號的地址還沒有定下來,T表示這個符號屬於代碼段。ld連接的時候會爲這些帶U的符號確定地址。
 
  (4)連接。
 連接需要指定庫的位置。通常程序中會有很多的外部符號,因此需要指定的位置就會很多。不過,我們之需要調用gcc即可,ld會自己去找這些庫的位置。
gcc test.o>test就得到了最終的可執行程序了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章