這里并沒(méi)不是討論大學(xué)課程中所學(xué)的《編譯原理》,,只是寫(xiě)一些我自己對(duì)C++編譯器及鏈接器的工作原理的理解和看法吧,,以我的水平,還達(dá)不到講解編譯原理(這個(gè)很復(fù)雜,,大學(xué)時(shí)幾乎沒(méi)學(xué)明白),。
要明白的幾個(gè)概念:
1,、編譯:編譯器對(duì)源文件進(jìn)行編譯,就是把源文件中的文本形式存在的源代碼翻譯成機(jī)器語(yǔ)言形式的目標(biāo)文件的過(guò)程,,在這個(gè)過(guò)程中,,編譯器會(huì)進(jìn)行一系列的語(yǔ)法檢查。如果編譯通過(guò),,就會(huì)把對(duì)應(yīng)的CPP轉(zhuǎn)換成OBJ文件,。
2、編譯單元:根據(jù)C++標(biāo)準(zhǔn),,每一個(gè)CPP文件就是一個(gè)編譯單元,。每個(gè)編譯單元之間是相互獨(dú)立并且互相不可知。
3,、目標(biāo)文件:由編譯所生成的文件,,以機(jī)器碼的形式包含了編譯單元里所有的代碼和數(shù)據(jù),還有一些期他信息,,如未解決符號(hào)表,,導(dǎo)出符號(hào)表和地址重定向表等。目標(biāo)文件是以二進(jìn)制的形式存在的,。
根據(jù)C++標(biāo)準(zhǔn),,一個(gè)編譯單元(Translation
Unit)是指一個(gè).cpp文件以及這所include的所有.h文件,.h文件里面的代碼將會(huì)被擴(kuò)展到包含它的.cpp文件里,,然后編譯器編譯該.cpp文件為一個(gè).obj文件,,后者擁有PE(Portable
Executable,即Windows可執(zhí)行文件)文件格式,,并且本身包含的就是二進(jìn)制代碼,,但是不一定能執(zhí)行,因?yàn)椴⒉荒鼙WC其中一定有main函數(shù),。當(dāng)編譯器將一個(gè)工程里的所有.cpp文件以分離的方式編譯完畢后,,再由鏈接器進(jìn)行鏈接成為一個(gè).exe或.dll文件。
下面讓我們來(lái)分析一下編譯器的工作過(guò)程:
我們跳過(guò)語(yǔ)法分析,,直接來(lái)到目標(biāo)文件的生成,,假設(shè)我們有一個(gè)A.cpp文件,如下定義:
int n =
1;
void
FunA()
{
++n;
}
它編譯出來(lái)的目標(biāo)文件A.obj就會(huì)有一個(gè)區(qū)域(或者說(shuō)是段),,包含以上的數(shù)據(jù)和函數(shù),,其中就有n、FunA,,以文件偏移量形式給出可能就是下面這種情況:
偏移量
內(nèi)容 長(zhǎng)度
0x0000 n
4
0x0004
FunA ??
注意:這只是說(shuō)明,,與實(shí)際目標(biāo)文件的布局可能不一樣,??表示長(zhǎng)度未知,目標(biāo)文件的各個(gè)數(shù)據(jù)可能不是連續(xù)的,,也不一定是從0x0000開(kāi)始,。
FunA函數(shù)的內(nèi)容可能如下:
0x0004
inc DWORD PTR[0x0000]
0x00??
ret
這時(shí)++n已經(jīng)被翻譯成inc DWORD PTR[0x0000],也就是說(shuō)把本單元0x0000位置的一個(gè)DWORD(4字節(jié))加1,。
有另外一個(gè)B.cpp文件,,定義如下:
extern
int n;
void
FunB()
{
++n;
}
它對(duì)應(yīng)的B.obj的二進(jìn)制應(yīng)該是:
偏移量
內(nèi)容 長(zhǎng)度
0x0000
FunB ??
這里為什么沒(méi)有n的空間呢,因?yàn)閚被聲明為extern,,這個(gè)extern關(guān)鍵字就是告訴編譯器n已經(jīng)在別的編譯單元里定義了,,在這個(gè)單元里就不要定義了。由于編譯單元之間是互不相關(guān)的,,所以編譯器就不知道n究竟在哪里,,所以在函數(shù)FunB就沒(méi)有辦法生成n的地址,那么函數(shù)FunB中就是這樣的:
0x0000
inc DWORD PTR[????]
0x00??
ret
那怎么辦呢,?這個(gè)工作就只能由鏈接器來(lái)完成了,。
為了能讓鏈接器知道哪些地方的地址沒(méi)有填好(也就是還????),那么目標(biāo)文件中就要有一個(gè)表來(lái)告訴鏈接器,,這個(gè)表就是“未解決符號(hào)表”,,也就是unresolved
symbol table。同樣,,提供n的目標(biāo)文件也要提供一個(gè)“導(dǎo)出符號(hào)表”也就是exprot
symbol table,,來(lái)告訴鏈接器自己可以提供哪些地址。
好,,到這里我們就已經(jīng)知道,,一個(gè)目標(biāo)文件不僅要提供數(shù)據(jù)和二進(jìn)制代碼外,還至少要提供兩個(gè)表:未解決符號(hào)表和導(dǎo)出符號(hào)表,,來(lái)告訴鏈接器自己需要什么和自己能提供些什么。那么這兩個(gè)表是怎么建立對(duì)應(yīng)關(guān)系的呢,?這里就有一個(gè)新的概念:符號(hào),。在C/C++中,每一個(gè)變量及函數(shù)都會(huì)有自己的符號(hào),,如變量n的符號(hào)就是n,,函數(shù)的符號(hào)會(huì)更加復(fù)雜,假設(shè)FunA的符號(hào)就是_FunA(根據(jù)編譯器不同而不同),。
所以,,
A.obj的導(dǎo)出符號(hào)表為
符號(hào) 地址
n
0x0000
_FunA 0x0004
未解決符號(hào)為空(因?yàn)樗麤](méi)有引用別的編譯單元里的東西)。
B.obj的導(dǎo)出符號(hào)表為
符號(hào) 地址
_FunB 0x0000
未解決符號(hào)表為
符號(hào) 地址
n
0x0001
這個(gè)表告訴鏈接器,,在本編譯單元0x0001位置有一個(gè)地址,,該地址不明,但符號(hào)是n。
在鏈接的時(shí)候,,鏈接在B.obj中發(fā)現(xiàn)了未解決符號(hào),,就會(huì)在所有的編譯單元中的導(dǎo)出符號(hào)表去查找與這個(gè)未解決符號(hào)相匹配的符號(hào)名,如果找到,,就把這個(gè)符號(hào)的地址填到B.obj的未解決符號(hào)的地址處,。如果沒(méi)有找到,就會(huì)報(bào)鏈接錯(cuò)誤,。在此例中,,在A.obj中會(huì)找到符號(hào)n,就會(huì)把n的地址填到B.obj的0x0001處,。
但是,,這里還會(huì)有一個(gè)問(wèn)題,如果是這樣的話,,B.obj的函數(shù)FunB的內(nèi)容就會(huì)變成inc DWORD
PTR[0x000](因?yàn)閚在A.obj中的地址是0x0000),由于每個(gè)編譯單元的地址都是從0x0000開(kāi)始,,那么最終多個(gè)目標(biāo)文件鏈接時(shí)就會(huì)導(dǎo)致地址重復(fù)。所以鏈接器在鏈接時(shí)就會(huì)對(duì)每個(gè)目標(biāo)文件的地址進(jìn)行調(diào)整,。在這個(gè)例子中,,假如B.obj的0x0000被定位到可執(zhí)行文件的0x00001000上,而A.obj的0x0000被定位到可執(zhí)行文件的0x00002000上,,那么實(shí)現(xiàn)上對(duì)鏈接器來(lái)說(shuō),,A.obj的導(dǎo)出符號(hào)地地址都會(huì)加上0x00002000,B.obj所有的符號(hào)地址也會(huì)加上0x00001000,。這樣就可以保證地址不會(huì)重復(fù),。
既然n的地址會(huì)加上0x00002000,那么FunA中的inc DWORD
PTR[0x0000]就是錯(cuò)誤的,,所以目標(biāo)文件還要提供一個(gè)表,,叫地址重定向表,address redirect table,。
總結(jié)一下:
目標(biāo)文件至少要提供三個(gè)表:未解決符號(hào)表,,導(dǎo)出符號(hào)表和地址重定向表。
未解決符號(hào)表:列出了本單元里有引用但是不在本單元定義的符號(hào)及其出現(xiàn)的地址,。
導(dǎo)出符號(hào)表:提供了本編譯單元具有定義,,并且可以提供給其他編譯單元使用的符號(hào)及其在本單元中的地址。
地址重定向表:提供了本編譯單元所有對(duì)自身地址的引用記錄,。
鏈接器的工作順序:
當(dāng)鏈接器進(jìn)行鏈接的時(shí)候,,首先決定各個(gè)目標(biāo)文件在最終可執(zhí)行文件里的位置。然后訪問(wèn)所有目標(biāo)文件的地址重定義表,,對(duì)其中記錄的地址進(jìn)行重定向(加上一個(gè)偏移量,,即該編譯單元在可執(zhí)行文件上的起始地址),。然后遍歷所有目標(biāo)文件的未解決符號(hào)表,并且在所有的導(dǎo)出符號(hào)表里查找匹配的符號(hào),,并在未解決符號(hào)表中所記錄的位置上填寫(xiě)實(shí)現(xiàn)地址,。最后把所有的目標(biāo)文件的內(nèi)容寫(xiě)在各自的位置上,再作一些另的工作,,就生成一個(gè)可執(zhí)行文件,。
說(shuō)明:實(shí)現(xiàn)鏈接的時(shí)候會(huì)更加復(fù)雜,一般實(shí)現(xiàn)的目標(biāo)文件都會(huì)把數(shù)據(jù),,代碼分成好向個(gè)區(qū),,重定向按區(qū)進(jìn)行,但原理都是一樣的,。
明白了編譯器與鏈接器的工作原理后,,對(duì)于一些鏈接錯(cuò)誤就容易解決了。
下面再看一看C/C++中提供的一些特性:
extern:這就是告訴編譯器,,這個(gè)變量或函數(shù)在別的編譯單元里定義了,,也就是要把這個(gè)符號(hào)放到未解決符號(hào)表里面去(外部鏈接)。
static:如果該關(guān)鍵字位于全局函數(shù)或者變量的聲明前面,,表明該編譯單元不導(dǎo)出這個(gè)函數(shù)或變量,,因些這個(gè)符號(hào)不能在別的編譯單元中使用(內(nèi)部鏈接)。如果是static局部變量,,則該變量的存儲(chǔ)方式和全局變量一樣,,但是仍然不導(dǎo)出符號(hào)。
默認(rèn)鏈接屬性:對(duì)于函數(shù)和變量,,默認(rèn)鏈接是外部鏈接,,對(duì)于const變量,默認(rèn)內(nèi)部鏈接,。
外部鏈接的利弊:外部鏈接的符號(hào)在整個(gè)程序范圍內(nèi)都是可以使用的,,這就要求其他編譯單元不能導(dǎo)出相同的符號(hào)(不然就會(huì)報(bào)duplicated
external symbols)。
內(nèi)部鏈接的利弊:內(nèi)部鏈接的符號(hào)不能在別的編譯單元中使用,。但不同的編譯單元可以擁有同樣的名稱的符號(hào),。
為什么頭文件里一般只可以有聲明不能有定義:頭文件可以被多個(gè)編譯單元包含,如果頭文件里面有定義的話,,那么每個(gè)包含這頭文件的編譯單元都會(huì)對(duì)同一個(gè)符號(hào)進(jìn)行定義,,如果該符號(hào)為外部鏈接,,則會(huì)導(dǎo)致duplicated
external symbols鏈接錯(cuò)誤,。
為什么公共使用的內(nèi)聯(lián)函數(shù)要定義于頭文件里:因?yàn)榫幾g時(shí)編譯單元之間互不知道,如果內(nèi)聯(lián)被定義于.cpp文件中,,編譯其他使用該函數(shù)的編譯單元的時(shí)候沒(méi)有辦法找到函數(shù)的定義,,因些無(wú)法對(duì)函數(shù)進(jìn)行展開(kāi),。所以如果內(nèi)聯(lián)函數(shù)定義于.cpp里,那么就只有這個(gè).cpp文件能使用它,。
|