對(duì)于平常的應(yīng)用程序開發(fā)夏块,我們很少需要關(guān)注編譯和鏈接過程羞福,因?yàn)橥ǔ5拈_發(fā)環(huán)境都是流行的集成開發(fā)環(huán)境(IDE)伺通,這樣的IDE一般都將編譯和鏈接的過程一步完成唆迁,通常將這種編譯和鏈接合并到一起的過程稱為構(gòu)建(Build)。
一后频、那些被隱藏了的過程
當(dāng)我們運(yùn)行一個(gè)程序的之前梳庆,通常要經(jīng)過4個(gè)步驟,分別是:
1卑惜、預(yù)處理(Prepressing)
2膏执、編譯(Compilation)
3、匯編(Assembly)
4露久、鏈接(Linking)
(一)預(yù)處理
預(yù)處理又稱預(yù)編譯更米,預(yù)編譯過程主要處理那些源代碼文件中的以"#"
開始的預(yù)編譯指令。比如#include
毫痕、#define
等征峦,主要處理規(guī)則如下:
- 將所有的
#define
刪除,并且展開所有宏定義消请。 - 處理所有條件預(yù)編譯指令栏笆,比如
#if
、#ifdef
臊泰、#elif
蛉加、#else
、#endif
缸逃。 - 處理
#include
預(yù)編譯指令针饥,將被包含的文件插入到該預(yù)編譯指令的位置。注意需频,這個(gè)過程是遞歸進(jìn)行的丁眼,也就是說被包含的文件可能還包含其他文件。 - 刪除所有的注釋贺辰。
- 添加行號(hào)和文件名標(biāo)識(shí)户盯,以便于編譯時(shí)編譯期產(chǎn)生調(diào)試用的行號(hào)信息及用于編譯時(shí)產(chǎn)生編譯錯(cuò)誤或警告時(shí)能夠顯示行號(hào)嵌施。
- 保留所有的
#pragma
編譯器指令,因?yàn)榫幾g器須要使用它們莽鸭。
經(jīng)過預(yù)編譯后的文件(.i文件)不包含任何宏定義吗伤,因?yàn)樗械暮暌呀?jīng)被展開,并且包含的文件也已經(jīng)插入到.i
文件中硫眨,所以當(dāng)我們無法判斷宏定義是否正確或頭文件包含是否正確時(shí)足淆,可以查看預(yù)編譯后的文件來確定問題。
(二)編譯
編譯過程就是把預(yù)處理完的文件進(jìn)行一系列詞法分析礁阁、語(yǔ)法分析巧号、語(yǔ)義分析及優(yōu)化后生產(chǎn)相應(yīng)的匯編代碼文件,這個(gè)過程往往是我們所說的整個(gè)程序構(gòu)建的核心部分姥闭。
(三)匯編
匯編器是將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令丹鸿,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令。所以匯編器的匯編過程相對(duì)于編譯器來說比較簡(jiǎn)單棚品,它沒有復(fù)雜的語(yǔ)法靠欢,也沒有語(yǔ)義,也不需要做指令優(yōu)化铜跑,只是根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就可以了门怪。
(四)鏈接
鏈接通常是一個(gè)讓人比較費(fèi)解的過程,為什么匯編器不直接輸出可執(zhí)行文件而是輸出一個(gè)目標(biāo)文件呢锅纺?鏈接過程到底包含了什么內(nèi)容掷空?為什么要鏈接?這是很多人的疑惑囤锉,所以我們會(huì)在本篇文章具體的分析靜態(tài)鏈接坦弟。
二、編譯器做了什么
從最直觀的角度來講嚼锄,編譯器就是將高級(jí)語(yǔ)言翻譯成機(jī)器語(yǔ)言的一個(gè)工具减拭。編譯過程一般可以分為6步:掃描、語(yǔ)法分析区丑、語(yǔ)義分析、源代碼優(yōu)化修陡、代碼生成和目標(biāo)代碼優(yōu)化沧侥。
我們將結(jié)合這個(gè)過程來簡(jiǎn)單描述從源代碼到最終目標(biāo)代碼的過程,以一段很簡(jiǎn)單的C語(yǔ)言的代碼為例子來講述這個(gè)過程魄鸦,比如我們有一行C語(yǔ)言的源代碼如下:
array[index] = (index + 4) * (2 + 6)
(一)詞法分析
首先宴杀,源代碼程序被輸入到掃描器,通過特定算法輕松地將源代碼的字符序列分割成一系列的記號(hào)拾因。比如上面的那行代碼旺罢,總共包含了28個(gè)非空字符旷余,經(jīng)過掃描以后,產(chǎn)生了16個(gè)記號(hào)扁达,比如:
記號(hào) | 類型 |
---|---|
array | 標(biāo)識(shí)符 |
[ | 左方括號(hào) |
index | 標(biāo)識(shí)符 |
] | 右方括號(hào) |
(二)語(yǔ)法分析
接下來的語(yǔ)法分析器將對(duì)由掃描器產(chǎn)生的記號(hào)進(jìn)行語(yǔ)法分析正卧,從而產(chǎn)生語(yǔ)法樹。簡(jiǎn)單的講跪解,由語(yǔ)法分析器生成的語(yǔ)法樹就是以表達(dá)式為結(jié)點(diǎn)的樹炉旷,我們知道。C語(yǔ)言的一個(gè)語(yǔ)句是一個(gè)表達(dá)式叉讥,而復(fù)雜的語(yǔ)句是很多表達(dá)式的組合窘行。上面例子中的語(yǔ)句就是一個(gè)由賦值表達(dá)式、加法表達(dá)式图仓、乘法表達(dá)式罐盔、數(shù)組表達(dá)式、括號(hào)表達(dá)式組成的復(fù)雜語(yǔ)句救崔。
(三)語(yǔ)義分析
(四)中間語(yǔ)言生成
現(xiàn)代的編譯期有著很多層次的優(yōu)化惶看,往往在源代碼級(jí)別會(huì)有一個(gè)優(yōu)化過程。我們這里所描述的源碼級(jí)優(yōu)化器在不同編譯器中可能會(huì)有不同的定義活有一些其他的差異帚豪。源代碼級(jí)優(yōu)化器會(huì)在源代碼級(jí)別進(jìn)行優(yōu)化碳竟,在上面那段代碼中,(2+6)
這個(gè)表達(dá)式可以被優(yōu)化掉狸臣,因?yàn)樗闹翟诰幾g期就可以被確定莹桅。
(五)目標(biāo)代碼生成與優(yōu)化
源代碼優(yōu)化器產(chǎn)生中間代碼標(biāo)志著下面的過程都屬于編譯器后端。編譯器后端主要包括代碼生成器和目標(biāo)代碼優(yōu)化器烛亦。
代碼生成器:將中間代碼轉(zhuǎn)換成目標(biāo)機(jī)器代碼诈泼。
目標(biāo)代碼優(yōu)化器:對(duì)上述的目標(biāo)代碼進(jìn)行優(yōu)化,比如選擇合適的尋址方式煤禽、使用位移來代替乘法運(yùn)算铐达、刪除多余的指令等。
經(jīng)過這些掃描檬果、語(yǔ)法分析瓮孙、語(yǔ)義分析、源代碼優(yōu)化选脊、代碼生成和目標(biāo)代碼優(yōu)化杭抠,編譯器忙活了這么多個(gè)步驟以后,源代碼終于被編譯成了目標(biāo)代碼恳啥,但是這個(gè)目標(biāo)代碼中有一個(gè)問題是:index和array的地址還沒有確定偏灿。如果我們要把目標(biāo)代碼使用匯編器編譯成真正能夠在機(jī)器上執(zhí)行的指令,那么index和array的地址應(yīng)該從哪得到呢钝的?如果index和array定義在跟上面的源代碼同一個(gè)編譯單元里面翁垂,那么編譯器可以為index和array分配空間铆遭,確定他們的地址,那如果是定義在其他的程序模塊呢沿猜?
這個(gè)問題就涉及到了鏈接的過程枚荣。
三、鏈接器年齡比編譯期長(zhǎng)
我們都知道邢疙,上古時(shí)期沒有高級(jí)語(yǔ)言棍弄,用的都是機(jī)器語(yǔ)言,甚至連匯編語(yǔ)言都沒有疟游,當(dāng)程序需要被運(yùn)行時(shí)呼畸,程序員人工將他所寫的程序?qū)懭氲酱鎯?chǔ)設(shè)備上,最原始的存儲(chǔ)設(shè)備就是紙帶颁虐,即在紙帶上打孔蛮原。
假設(shè)有一種計(jì)算機(jī),它的每條指令是1個(gè)字節(jié)另绩,也就是8位儒陨,我們假設(shè)有一種跳轉(zhuǎn)指令,它的高4位是0001笋籽,表示這是一條跳轉(zhuǎn)指令蹦漠,低4位存放的是跳轉(zhuǎn)目的地的絕對(duì)地址。當(dāng)程序修改的時(shí)候笛园,這些位置都要重新計(jì)算,絕對(duì)地址都會(huì)改變侍芝,重新計(jì)算的過程十分繁瑣又耗時(shí)研铆,并且很容易出錯(cuò),這種重新計(jì)算各個(gè)目標(biāo)的地址過程被叫做重定位州叠。
四棵红、模塊拼裝 - 靜態(tài)鏈接
程序設(shè)計(jì)的模塊化是人們一直在追求的目標(biāo),因?yàn)楫?dāng)一個(gè)系統(tǒng)十分復(fù)雜的時(shí)候咧栗,我們不得不將一個(gè)復(fù)雜的系統(tǒng)逐步分割成小的系統(tǒng)以達(dá)到各個(gè)突破的目的逆甜。一個(gè)復(fù)雜的軟件也如此。人們把每個(gè)源代碼模塊獨(dú)立地編譯致板,然后按照須要將他們“組裝”起來忆绰,這個(gè)組裝模塊的過程就是鏈接。鏈接的主要內(nèi)容就是把各個(gè)模塊之間相互引用的部分都處理好可岂,使得各個(gè)模塊之間能夠正確的銜接。鏈接器所要做的工作其實(shí)跟前面所描述的“程序員人工調(diào)整地址”本質(zhì)上沒什么兩樣翰灾,只不過現(xiàn)代的高級(jí)語(yǔ)言的諸多特性和功能缕粹,使得編譯器稚茅、鏈接器更為復(fù)雜,功能更為強(qiáng)大平斩,但從原理上講亚享,它的工作無非就是把一些指令對(duì)其他符號(hào)地址的引用加以修正。鏈接過程主要包括了地址和空間分配绘面、符號(hào)決議和重定位等這些步驟欺税。
對(duì)于最基本的靜態(tài)鏈接過程,每個(gè)模塊的源代碼文件經(jīng)過編譯器編譯成目標(biāo)文件(Object File揭璃,一般擴(kuò)展名為.o活.obj)晚凿,目標(biāo)文件和庫(kù)(Library)一起鏈接形成最終可執(zhí)行文件。而最常見的庫(kù)就是運(yùn)行時(shí)庫(kù)(Runtime Library)瘦馍,它是支持程序運(yùn)行的基本函數(shù)的集合歼秽。庫(kù)其實(shí)是一組目標(biāo)文件的包,就是一些最常用的代碼編譯成目標(biāo)文件后打包存放情组。
比如我們?cè)诔绦蚰Kmain.c中使用另外一個(gè)func.c中的函數(shù)foo()燥筷,我們?cè)趍ain.c模塊中每一處調(diào)用foo的時(shí)候都必須確切知道foo這個(gè)函數(shù)的地址,但是由于每個(gè)模塊都是單獨(dú)編譯的院崇,在編譯器編譯main.c的時(shí)候它并不知道foo函數(shù)的地址肆氓,所以它暫時(shí)把這些調(diào)用foo的指令的目標(biāo)地址擱置,等待最后鏈接的時(shí)候由鏈接器去將這些指令的目標(biāo)地址修正底瓣,這個(gè)地址修正的過程也被叫做重定位谢揪,每個(gè)要被修正的地方叫一個(gè)重定位入口,重定位所做的就是給程序中每個(gè)這樣的絕對(duì)地址引用的位置“打補(bǔ)丁”濒持,使他們指向正確的地址键耕。