C++編譯器與連結器工作原理 + Link錯誤
一.Debug版本和Release版本的區別
Debug通常稱為除錯版本,它包含除錯資訊,並且不作任何優化,便於程式設計師除錯程式。Release稱為釋出版本,它往往是進行了各種優化,使得程式在程式碼大小和執行速度上都是最優的,以便使用者很好地使用。
一般而言Debug版本會比Release版本多出*.ilk檔案和*.pdb檔案。
1.*.ilk檔案
ilk字尾全稱為“Incremental Linking”,意即增量連結。
VC6中,“Project Setting->Link(Category:General)”預設勾選“Link incrementally”;相應VC2005中,“專案屬性->配置屬性->連結器->常規->啟用增量連結”預設選項為“是(/INCREMENTAL)”。
當選定漸增型編譯連結時,連結器自動生成ILK檔案,記錄連結資訊,也就是每次重新編譯並不編譯所有的原始檔,只編譯改動過的檔案。而編譯器怎麼知道哪些編譯過哪些未編譯過呢,除了檢查修改時間外,這個ilk檔案也是很重要的資訊。
2.*.pdb檔案
pdb字尾全稱為“Program Debug Database”,意即程式資料庫檔案。
“專案屬性->配置屬性->連結器->除錯”,預設“生成除錯資訊”選項為“是(/DEBUG)”,預設“生成程式資料庫檔案”處填寫“.\Debug\*.pdb”。該選項對應的編譯開關為/PDB。
符號檔案(Symbol Files)是一個數據信息檔案,它包含了應用程式二進位制檔案(比如EXE、DLL等)的除錯資訊,專門用來作除錯之用,最終生成的可執行檔案在執行時並不需要這個符號檔案,但你的程式中所有的變數資訊都記錄在這個檔案中。所以除錯應用程式時,這個檔案是非常重要的。用VC和 WinDbg除錯程式時都要用到這個檔案。
---------------麻將 11.25 斷點進不去。 搞了一下午
表現: F11進入地方與目標位置相錯2行。 從未遇過:同一個函式內部,前一個斷點實心,後一空心如上提示。 解決:程式碼保留一份,全刪除,在copy進來
----
*.ilk檔案 : 編譯具體編譯那個.cpp要靠此檔案 <編譯器本身會:檢查修改時間外>增量連結。
*.pdb檔案: 專門用來作除錯之用 <所有的變數資訊都記錄在這個檔案中>程式資料庫檔案。
.INI:配置檔案。
.OBJ:由編譯器或彙編工具生成的目標檔案,是模組的二進位制中間檔案。
.PCH:預編譯標頭檔案,比較大,由編譯器在建立工程時自動生成, 在以後建立工程時不再重新編譯這些程式碼,以便加快整個編譯過程的速度。
#include "stdafx.h" 是MFC東西. 手機上是肯定不能用的
VC編譯時可以選擇incremental linking(漸增式編譯),也就是每次重新編譯並不編譯所有的原始檔,只編譯改動過的檔案。
C++靜態庫與動態庫 --- http://www.cnblogs.com/skynet/p/3372855.html---------------------------------------------------------------
編譯器編譯原理詳解
這裡並沒不是討論大學課程中所學的《編譯原理》,只是寫一些我自己對C++編譯器及連結器的工作原理的理解和看法吧,以我的水平,還達不到講解編譯原理(這個很複雜,大學時幾乎沒學明白)。
要明白的幾個概念:
1、編譯:編譯器對原始檔進行編譯,就是把原始檔中的文字形式存在的原始碼翻譯成機器語言形式的目標檔案的過程,在這個過程中,編譯器會進行一系列的語法檢查。如果編譯通過,就會把對應的CPP轉換成OBJ檔案。
2、編譯單元:根據C++標準,每一個CPP檔案就是一個編譯單元。每個編譯單元之間是相互獨立並且互相不可知。
3、目標檔案:由編譯所生成的檔案,以機器碼的形式包含了編譯單元裡所有的程式碼和資料,還有一些期他資訊,如未解決符號表,匯出符號表和地址重定向表等。目標檔案是以二進位制的形式存在的。
根據C++標準,一個編譯單元(Translation Unit)是指一個.cpp檔案以及這所include的所有.h檔案,.h檔案裡面的程式碼將會被擴充套件到包含它的.cpp檔案裡,然後編譯器編譯該.cpp檔案為一個.obj檔案,後者擁有PE(Portable Executable,即Windows可執行檔案)檔案格式,並且本身包含的就是二進位制程式碼,但是不一定能執行,因為並不能保證其中一定有main函式。當編譯器將一個工程裡的所有.cpp檔案以分離的方式編譯完畢後,再由連結器進行連結成為一個.exe或.dll檔案。
下面讓我們來分析一下編譯器的工作過程:
我們跳過語法分析,直接來到目標檔案的生成,假設我們有一個A.cpp檔案,如下定義:
int n = 1;
void FunA()
{
++n;
}
它編譯出來的目標檔案A.obj就會有一個區域(或者說是段),包含以上的資料和函式,其中就有n、FunA,以檔案偏移量形式給出可能就是下面這種情況:
偏移量 內容 長度
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(根據編譯器不同而不同)。
所以,
A.obj的匯出符號表為
符號 地址
n 0x0000
_FunA 0x0004
未解決符號為空(因為他沒有引用別的編譯單元裡的東西)。
B.obj的匯出符號表為
符號 地址
_FunB 0x0000
未解決符號表為
符號 地址
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。
總結一下:
目標檔案至少要提供三個表:未解決符號表,匯出符號表和地址重定向表。
未解決符號表:列出了本單元裡有引用但是不在本單元定義的符號及其出現的地址。
匯出符號表:提供了本編譯單元具有定義,並且可以提供給其他編譯單元使用的符號及其在本單元中的地址。
地址重定向表:提供了本編譯單元所有對自身地址的引用記錄。
連結器的工作順序:
當連結器進行連結的時候,首先決定各個目標檔案在最終可執行檔案裡的位置。然後訪問所有目標檔案的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執行檔案上的起始地址)。然後遍歷所有目標檔案的未解決符號表,並且在所有的匯出符號表裡查詢匹配的符號,並在未解決符號表中所記錄的位置上填寫實現地址。最後把所有的目標檔案的內容寫在各自的位置上,再作一些另的工作,就生成一個可執行檔案。
說明:實現連結的時候會更加複雜,一般實現的目標檔案都會把資料,程式碼分成好向個區,重定向按區進行,但原理都是一樣的。
明白了編譯器與連結器的工作原理後,對於一些連結錯誤就容易解決了。
下面再看一看C/C++中提供的一些特性:
extern:這就是告訴編譯器,這個變數或函式在別的編譯單元裡定義了,也就是要把這個符號放到未解決符號表裡面去(外部連結)。
static:如果該關鍵字位於全域性函式或者變數的宣告前面,表明該編譯單元不匯出這個函式或變數,因些這個符號不能在別的編譯單元中使用(內部連結)。如果是static區域性變數,則該變數的儲存方式和全域性變數一樣,但是仍然不匯出符號。
預設連結屬性:對於函式和變數,預設連結是外部連結,對於const變數,預設內部連結。
外部連結的利弊:外部連結的符號在整個程式範圍內都是可以使用的,這就要求其他編譯單元不能匯出相同的符號(不然就會報duplicated external symbols)。
內部連結的利弊:內部連結的符號不能在別的編譯單元中使用。但不同的編譯單元可以擁有同樣的名稱的符號。
為什麼標頭檔案裡一般只可以有宣告不能有定義:標頭檔案可以被多個編譯單元包含,如果標頭檔案裡面有定義的話,那麼每個包含這標頭檔案的編譯單元都會對同一個符號進行定義,如果該符號為外部連結,則會導致duplicated external symbols連結錯誤。
為什麼公共使用的行內函數要定義於標頭檔案裡:因為編譯時編譯單元之間互不知道,如果內聯被定義於.cpp檔案中,編譯其他使用該函式的編譯單元的時候沒有辦法找到函式的定義,因些無法對函式進行展開。所以如果行內函數定義於.cpp裡,那麼就只有這個.cpp檔案能使用它。
-----------------------
函式庫分為靜態庫和動態庫兩種。
靜態庫在程式編譯時會被連線到目的碼中,程式執行時將不再需要該靜態庫。
動態庫在程式編譯時並不會被連線到目的碼中,而是在程式執行是才被載入,因此在程式執行時還需要動態庫存在。
例子: 沒有重新編譯, 如果編譯了 怎麼會再去Link