函數(shù),從編輯到編譯 (下) -- 一文帶你了解編譯 鏈接

上篇的鏈接在這里:
函數(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á)式舉例子):


file

拿著這份報(bào)告,就該去進(jìn)行語法分析了。

2.2 語法分析

語法分析器(Grammar Parser)就不需要我整個(gè)躺進(jìn)去,只用把掃描器生成的檢查報(bào)告交給他。

分析好之后,我拿到了一個(gè)盆栽 新的報(bào)告 —— 一棵樹,或者,準(zhǔn)確一點(diǎn),一棵語法樹(Syntax Tree)。

file

樹的枝葉一切正常,表示我的表達(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)載

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容