天天看點

C/C++編譯連結原理

看了c++ primer,寫過一些C++程式後,對其中的編譯連結原理總是不明就裡,想來這也難怪,因為平常都是在VS上,什麼都是封裝好了的,隐藏了太多的細節。本着自己一貫來對底層實作探究的興趣,結合借鑒他人的想法,記下自己對C/C++編譯連結原理的一些了解,要是能給看到此文章的你帶來一丁點幫助就欣慰了。

編譯是把源檔案經過預編譯,優化,彙編翻譯成機器語言的過程,這些機器語言代碼資料以一定的格式COFF(Common Object File Format),OMF(Object Module Format),ELF(Executed linked Format),PE(Portable Executable)等存放于目标檔案中。目标檔案通常包含未解決符号表,導出符号表和位址重定向表。連接配接器具體說明如下:

-----編譯和連結大緻流程-----

C/C++編譯連結原理

編譯器的工作不單單隻有編譯,事實上,它包括了從進階語言到機器語言的完整過程: 

預編譯-》編譯-》彙編-》連結。

  1. 預編譯 

    預編譯過程主要是處理源代碼檔案中以#開始的預編譯指令。主要處理規則如下: 

    1.1.将所有#define删除,并展開所有的宏定義。 

    1.2.處理所有條件編譯指令,比如#ifdef、#else等。 

    1.3.處理#include,遞歸的将被包含的檔案出入到該指令的位置。 

    1.4.删除所有注釋。 

    1.5.添加行号和檔案名辨別,以便于編譯器調試産生的行号消息和警告時顯示的行号。 

    1.6.保留所有#pragma編譯指令。#pragma指令是編譯器參數。經過預編譯之後,産生一個*.i檔案。

  2. 編譯 

    編譯過程就是把預處理完成的*.i檔案進行詞法分析、文法分析、語義分析以及優化之後,産生相應的彙編代碼檔案。 

    2.1.詞法分析利用掃描器和有限狀态機算法,将源代碼字元按照特定的字元辨別分割成一系列記号。這些記号包含了以下幾種分類:關鍵字、辨別符、字面量(包括數字、字元串等)和特殊符号(加減乘除等)。 

    2.2.文法分析産生表達式文法樹,但是不排查這個語句是否合法。 

    2.3.語義分析給文法樹添加類型辨別,并檢查表達式是否合法。 

    2.4.中間代碼生成。 

    2.5.目标代碼生成和優化經過編譯之後,産生一個彙編輸出檔案*.s檔案。

  3. 彙編 

    彙編過程就是将彙編代碼轉變成機器代碼檔案*.o檔案。這是個相對簡單的過程,根據彙編指令和機器指令對照一一翻譯就可以了。

  4. 連結 

    連接配接器将多個*.o檔案彼此關聯拼接到一起,最終産生一個可執行檔案。分為靜态連結和動态連結。

-----編譯和連結大緻流程-----

--------參考自http://blog.csdn.net/success041000/article/details/6714195----------

 源檔案:A.cpp

     int n = 1;

     void FunA() {

         ++n;

     }

  目标檔案:A.obj

     偏移量     内容     長度

     0x0000    n             4

     0x0004    FunA     ??

 注意:這隻是說明,與實際目标檔案的布局可能不一樣,??表示長度未知,目标檔案的各個資料可能不是連續的,也不一定是從0x0000開始。

 FunA函數的内容可能如下:

     0x0004  inc  DWORD  PTR[0x0000]

     0x00??  ret

     這時++n已經被翻譯成inc DWORD PTR[0x0000],也就是說把本單元0x0000位置的一個DWORD(4位元組)加1。

源檔案:B.cpp

     extern int n;

     void FunB() {

        ++n;

     }

     目标檔案:B.obj

     偏移量     内容     長度

     0x0000    FunB     ??

       這裡為什麼沒有n的空間呢,因為n被聲明為extern,這個extern關鍵字就是告訴編譯器n已經在别的編譯單元裡定義了,在這個單元裡就不要定義了。由于編譯單元之間是互不相關的,是以編譯器就不知道n究竟在哪裡,是以在函數FunB就沒有辦法生成n的位址,那麼函數FunB中就是這樣的:

     0x0000 inc DWORD PTR[????]

     0x00?? ret

      那怎麼辦呢?這個工作就隻能由連結器來完成了。

      為了能讓連結器知道哪些地方的位址沒有填好(也就是????),那麼目标檔案中就要有一個表來告訴連結器,這個表就是“未解決符号表”,也就是unresolved symbol table。同樣,提供n的目标檔案也要提供一個“導出符号表”也就是exprot symbol table,來告訴連結器自己可以提供哪些位址。 

      到這裡我們就已經知道,一個目标檔案不僅要提供資料和二進制代碼外,還至少要提供兩個表:未解決符号表和導出符号表,來告訴連結器自己需要什麼和自己能提供些什麼。那麼這兩個表是怎麼建立對應關系的呢?這裡就有一個新的概念:符号。在C/C++中,每一個變量及函數都會有自己的符号,如變量n的符号就是n,函數的符号會更加複雜,假設FunA的符号就是_FunA(C++标準并未定義,這取決于編譯器的具體實作)。

    A.obj的導出符号表

    符号            位址

    n                0x0000

    _FunA       0x0004

    A.obj的未解決符号表

   為空(因為它沒有引用别的編譯單元裡的東西)

    B.obj的導出符号表

    符号             位址

    _FunB        0x0000

    B.obj的未解決符号表

    符号             位址

    n                  0x0001

      這個表告訴連結器,在本編譯單元0x0001位置有一個位址,該位址不明,但符号是n。

      在連結的時候,連結在B.obj中發現了未解決符号,就會在所有的編譯單元中的導出符号表去查找與這個未解決符号相比對的符号名,如果找到,就把這個符号的位址填到B.obj的未解決符号的位址處。如果沒有找到,就會報連結錯誤。在此例中,在A.obj中會找到符号n,就會把n的位址填到B.obj的0x0001處。 

       但是,這裡還會有一個問題,如果是這樣的話,B.obj的函數FunB的内容就會變成inc DWORD PTR[0x000](因為n在A.obj中的位址是0x0000),由于每個編譯單元的位址都是從0x0000開始,那麼最終多個目标檔案連結時就會導緻位址重複。是以連結器在連結時就會對每個目标檔案的位址進行調整。在這個例子中,假如B.obj的0x0000被定位到可執行檔案的0x00001000上,而A.obj的0x0000被定位到可執行檔案的0x00002000上,那麼實作上對連結器來說,A.obj的導出符号地位址都會加上0x00002000,B.obj所有的符号位址也會加上0x00001000。這樣就可以保證位址不會重複。 

       既然n的位址會加上0x00002000,那麼FunA中的inc DWORD PTR[0x0000]就是錯誤的,是以目标檔案還要提供一個表,叫位址重定向表,address redirect table。

目标檔案至少要提供三個表:未解決符号表,導出符号表和位址重定向表。

     (1)未解決符号表:列出了本單元裡有引用但是不在本單元定義的符号及其出現的位址。

     (2)導出符号表:提供了本編譯單元具有定義,并且可以提供給其他編譯單元使用的符号及其在本單元中的位址。

     (3)位址重定向表:提供了本編譯單元所有對自身位址的引用記錄。 

      連結器的工作順序:

      當連結器進行連結的時候,首先決定各個目标檔案在最終可執行檔案裡的位置。然後通路所有目标檔案的位址重定義表,對其中記錄的位址進行重定向(加上一個偏移量,即該編譯單元在可執行檔案上的起始位址)。然後周遊所有目标檔案的未解決符号表,并且在所有的導出符号表裡查找比對的符号,并在未解決符号表中所記錄的位置上填寫實作位址。最後把所有的目标檔案的内容寫在各自的位置上,再作一些另的工作,就生成一個可執行檔案。

      說明:實作連結的時候會更加複雜,一般實作的目标檔案都會把資料,代碼分成好向個區,重定向按區進行,但原理都是一樣的。明白了編譯器與連結器的工作原理後,對于一些連結錯誤就容易解決了。

下面是C/C++中一些相關的特性:

     extern:這就是告訴編譯器,這個變量或函數在别的編譯單元裡定義了,也就是要把這個符号放到未解決符号表裡面去(外部連結)。

     static:如果該關鍵字位于全局函數或者變量的聲明前面,表明該編譯單元不導出這個函數或變量,因些這個符号不能在别的編譯單元中使用(内部連結)。如果是static局部變量,則該變量的存儲方式和全局變量一樣,但是仍然不導出符号。 

     預設連結屬性:對于函數和變量,預設連結是外部連結,對于const變量,預設内部連結。

     外部連結的利弊:外部連結的符号在整個程式範圍内都是可以使用的,這就要求其他編譯單元不能導出相同的符号(不然就會報

duplicated external symbols)。

     内部連結的利弊:内部連結的符号不能在别的編譯單元中使用。但不同的編譯單元可以擁有同樣的名稱的符号。

     為什麼頭檔案裡一般隻可以有聲明不能有定義:頭檔案可以被多個編譯單元包含,如果頭檔案裡面有定義的話,那麼每個包含這頭檔案的編譯單元都會對同一個符号進行定義,如果該符号為外部連結,則會導緻duplicated external symbols連結錯誤。 

     為什麼公共使用的内聯函數要定義于頭檔案裡:因為編譯時編譯單元之間互不知道,如果内聯被定義于.cpp檔案中,編譯其他使用該函數的編譯單元的時候沒有辦法找到函數的定義,因些無法對函數進行展開。是以如果内聯函數定義于.cpp裡,那麼就隻有這個.cpp檔案能使用它。

--------參考自http://blog.csdn.net/success041000/article/details/6714195----------

以上隻是大體概念上的講述,設計具體環境的編譯連結過程細節及檔案輸出格式等再做補充。