上篇的鏈接在這里:
函數(shù),從編輯到編譯 (上) --帶你了解預(yù)編譯做了什么
下面繼續(xù):
2. 編譯
所謂編譯過程,就是把預(yù)處理完的文件進(jìn)行一系列詞法分析,語法分析,語義分析及優(yōu)化后生產(chǎn)相應(yīng)的匯編代碼文件。這一步是整個(gè)程序構(gòu)建的核心部分,也是最容易出錯(cuò)的一部分。
從現(xiàn)在開始,步驟就變得十分復(fù)雜了。
對(duì)函數(shù)來說,這一階段是最繁瑣也是最為危險(xiǎn)的:稍有不慎,輕則 warning 重則 error 。
我見過許多出錯(cuò)的函數(shù),他們連著行號(hào)被編譯器帶到窗口,當(dāng)街示眾。
也有些函數(shù)和 #pragma 關(guān)系比較好,小錯(cuò)誤被遮掩過去,免去了示眾的命運(yùn)。
2.1 掃描
我們要先經(jīng)過一臺(tái)掃描器 (Scanner),這機(jī)器如此龐大,以至于我根本看不出內(nèi)部的細(xì)節(jié)。
我對(duì)大型機(jī)器充滿好奇,編譯器給了我一本手冊(cè)——《編譯寶典》,他說里面有講掃描器的實(shí)現(xiàn)。
可我看不懂。
編譯器告訴我,想要參透這本寶典,需要付出代價(jià)。
“代價(jià)?像岳不群那樣?”
“你想哪兒去了!你說的那是《葵花寶典》,我說的代價(jià)是時(shí)間和精力!編譯器這種龐大的工程,需要一個(gè)團(tuán)隊(duì)來合作完成,除非你是打算寫寫玩具編譯器。”
所以我放棄了造出這些機(jī)器的想法,因?yàn)楹瘮?shù)的一生太短了,希望你能實(shí)現(xiàn)我的愿望。
在掃描器里的體驗(yàn)不太舒服,它像一臺(tái) X 光機(jī),把我的身體里里外外看了個(gè)遍,給我的感覺很不妙。
出了機(jī)器,會(huì)收到一個(gè)檢查報(bào)告,像這樣(篇幅有限,只拿一個(gè)表達(dá)式舉例子):

拿著這份報(bào)告,就該去進(jìn)行語法分析了。
2.2 語法分析
語法分析器(Grammar Parser)就不需要我整個(gè)躺進(jìn)去,只用把掃描器生成的檢查報(bào)告交給他。
分析好之后,我拿到了一個(gè)盆栽 新的報(bào)告 —— 一棵樹,或者,準(zhǔn)確一點(diǎn),一棵語法樹(Syntax Tree)。

樹的枝葉一切正常,表示我的表達(dá)式是合法的。毫無疑問,我再次通過了檢查。
但有的函數(shù)就不這么幸運(yùn)了,他們會(huì)在這一步檢查出問題,比如括號(hào)不匹配,表達(dá)式中缺少操作符等等,這些錯(cuò)誤會(huì)上報(bào)編譯器,最后報(bào)告給程序員?!麄兠媾R著整改的命運(yùn)。
2.3 語義分析
剛剛的語法分析器,顧名思義,只完成了語法層面的分析,但他不了解表達(dá)式是不是真的有意義。
比如讓兩個(gè)指針做乘法,在語法上是合法的,但這是沒有意義的。語義分析器(Semantic Analyzer)就能夠檢查出這個(gè)錯(cuò)誤。
但語義分析也不是萬能的,它也有局限性——語義分析僅僅能分析靜態(tài)語句。
你問我什么是靜態(tài)語義?
我不知道,因?yàn)槲抑皇且粋€(gè)函數(shù)。
所謂靜態(tài)語義,是能在編譯期間可以確定的語義,與之相對(duì)的動(dòng)態(tài)語義,就是只有在運(yùn)行期才能確定的語義。
int a = 6 / 0;
從靜態(tài)語義上看,這句話是合法的,編譯期間不會(huì)報(bào)錯(cuò),但等到程序運(yùn)行到這句時(shí),就會(huì)報(bào)出 devided by 0 的錯(cuò)誤,造成程序異常退出。
2.4 代碼生成與優(yōu)化
走到這,編譯部分也算快結(jié)束了。
剩下的兩臺(tái)機(jī)器,一臺(tái)叫代碼生成器(Code Generator),一臺(tái)叫目標(biāo)代碼優(yōu)化器(Target Code Optimizer)。
目標(biāo)代碼優(yōu)化器總是嫌棄代碼生成器,因?yàn)榇a生成器生成的代碼效率低,還需要他花大功夫來優(yōu)化。
用優(yōu)化器的話講:“生成器那家伙,每次生成一堆低效率的代碼,我還得從頭讀到尾,進(jìn)行基于數(shù)據(jù)流分析(data-flow analyse)技術(shù)的全局優(yōu)化,太累了?!?/p>
其實(shí)代碼生成器有做優(yōu)化,叫做局部代碼優(yōu)化,只是優(yōu)化程度遠(yuǎn)遠(yuǎn)不及優(yōu)化器,所以他不好意思反駁優(yōu)化器。
不過這不代表代碼生成器結(jié)構(gòu)就簡單了,它生成代碼的過程十分依賴于目標(biāo)機(jī)器——這意味著它要適配許許多多的機(jī)器,不同的機(jī)器有著不同的字長、寄存器、整數(shù)數(shù)據(jù)類型和浮點(diǎn)數(shù)數(shù)據(jù)類型等,它要考慮的事情太多了。
經(jīng)過生成器,表達(dá)式的樣子發(fā)生了巨大的變化(這里以 x86 的匯編語言來表示):
movl index, %ecx ; value of index to ecx
addl $4, %ecx ; ecx = ecx + 4
mull $8, %ecx ; ecx = ecx * 8
movl index, %eax ; value of index to eax
movl %ecx, array(,eax,4) ; array[index] = ecx
優(yōu)化器對(duì)上面的代碼又做了一番深層次的優(yōu)化,包括選擇尋址方式,刪除多余指令等。(代碼比較短,所以優(yōu)化效果并不明顯。)
movl index, %edx
leal 32(,%edx,8), %eax
movl %eax, array(,%edx,4)
每次走過這些流程,我都不得不感嘆于編譯器復(fù)雜的結(jié)構(gòu),也只有優(yōu)秀的程序員們,才能夠完成這么偉大的工程吧。
函數(shù)的編譯,就是這么繁瑣,且枯燥。
今天令我驚訝的是,所有函數(shù)都完美的通過了編譯階段。
“Nice~ 這次可以早點(diǎn)休息了!”不止是我,其他函數(shù)也是這么想的吧。
我們有說有笑,悠然等待著鏈接程序來做最后的收尾工作。
但萬萬沒想到,危機(jī)竟出現(xiàn)在鏈接階段。
3. 鏈接
我聽長輩們說,鏈接器,擁有比編譯器更為悠久的歷史。
每當(dāng)我把這個(gè)事實(shí)告訴新來的函數(shù)時(shí),他們總是一臉不可思議:
“我們都是先編譯,再鏈接的,怎么會(huì)先有鏈接器,再有編譯器?這又不是先有雞還是先有蛋的哲學(xué)問題?!?/p>
我第一次聽說的時(shí)候,也有這樣的疑惑。
“鏈接是在匯編語言時(shí)代就出現(xiàn)了的概念。在那之前,是機(jī)器語言的時(shí)代。但是想要對(duì)機(jī)器語言進(jìn)行修改,那就太困難了,因?yàn)闄C(jī)器指令的修改經(jīng)常造成具體指令地址的改變,牽一發(fā)而動(dòng)全身。所以匯編語言產(chǎn)生了,用符號(hào)來標(biāo)記位置,而符號(hào)與實(shí)際地址的映射工作,就是鏈接器來做的?!蔽蚁蛩忉尩?。
“我明白了,因?yàn)楦呒?jí)語言出現(xiàn)在后面,所以從高級(jí)語言到匯編語言的步驟——編譯,要比鏈接來的晚一些?!?/p>
是啊,編程語言的發(fā)展,從機(jī)器語言,到匯編語言,再到現(xiàn)在的高級(jí)語言,經(jīng)過了幾十年的時(shí)間。但盡管是現(xiàn)代,我們編譯型高級(jí)語言,想要運(yùn)行,還是得回到匯編語言,再被翻譯成機(jī)器語言,看起來是繞了一個(gè)大圈,但人類程序員的生產(chǎn)力,卻得到了質(zhì)的飛躍。
人類總是能想出各種辦法來減輕他們的工作量。
...
鏈接過程主要包括了地址和空間分配(Address and Storage Allocation)、符號(hào)決議(Symbol Resolution)和重定位(Relocation)等這些步驟。
看起來挺高大上,其實(shí)鏈接器做的和早期程序員人工調(diào)整地址沒什么兩樣,只是更加復(fù)雜而已——你不要指望現(xiàn)在的語言特性比早期簡單。
但從本質(zhì)上說,就是把指令對(duì)其他符號(hào)地址的引用加以修正。鏈接的重點(diǎn)就是兩個(gè)不同的目標(biāo)文件。
這一階段本來是很容易通過的,但今天,居然出現(xiàn)了大錯(cuò)誤。
問題出在 main.c 中。
出乎所有函數(shù)的意料,包括 main。
4. 尾聲
回到編輯器,我們檢查遍了 main 函數(shù)內(nèi)部的所有函數(shù),從他們的聲明,再到他們的實(shí)現(xiàn),全都沒有問題。
“會(huì)不會(huì)是 #include 的時(shí)候出了什么問題?”有函數(shù)提出了自己的看法。
我們決定分頭行動(dòng),一部分和其他文件協(xié)作檢查函數(shù)聲明,剩下一部分負(fù)責(zé)排查有沒有出現(xiàn)循環(huán) #include 問題。
不知過了多少 CPU 周期,大家回來了,一無所獲,兩種問題都沒有出現(xiàn)。
我們一籌莫展。
“ main.c ,鏈接出錯(cuò)...”我滿腦子都在想可能原因,“不會(huì)是 main 函數(shù)本身出了問題吧!”
“快,去看看宏定義有沒有異常!”
宏定義?雖然大家有些疑惑,但還是照做了。
果然,發(fā)現(xiàn)了異常:
...
...
#define main mian
...
...
我心里怒罵“誰這么缺德,干這種事情?!”
好在刪掉這條“間諜”指令后,一切恢復(fù)正常,完美通過編譯鏈接。
我們終于可以休息了。
PS:危險(xiǎn)指令,請(qǐng)勿模仿。除非,,,你想挨一頓毒打。
PPS:函數(shù)的運(yùn)行以后也會(huì)寫到。
PPPS:如果大家對(duì)文章有什么看法和意見,歡迎提出來~ 如果覺得文章有意思,點(diǎn)個(gè)贊再走吧
文中插圖來自《程序員的自我修養(yǎng)》。
聲明:原創(chuàng)文章,未經(jīng)授權(quán),禁止轉(zhuǎn)載