上篇的連接在這裏: 函數,從編輯到編譯 (上) --帶你瞭解預編譯作了什麼c++
下面繼續:程序員
所謂編譯過程,就是 把預處理完的文件進行一系列詞法分析,語法分析,語義分析及優化後生產相應的彙編代碼文件。 這一步是整個程序構建的核心部分,也是最容易出錯的一部分。編程
從如今開始,步驟就變得十分複雜了。編程語言
對函數來講,這一階段是最繁瑣也是最爲危險的:稍有不慎,輕則 warning
重則 error
。編輯器
我見過許多出錯的函數,他們連着行號被編譯器帶到窗口,當街示衆。ide
也有些函數和 #pragma
關係比較好,小錯誤被遮掩過去,免去了示衆的命運。函數
咱們要先通過一臺掃描器 (Scanner),這機器如此龐大,以致於我根本看不出內部的細節。優化
我對大型機器充滿好奇,編譯器給了我一本手冊——《編譯寶典》,他說裏面有講掃描器的實現。.net
可我看不懂。翻譯
編譯器告訴我,想要參透這本寶典,須要付出代價。
「代價?像嶽不羣那樣?」
「你想哪兒去了!你說的那是《葵花寶典》,我說的代價是時間和精力!編譯器這種龐大的工程,須要一個團隊來合做完成,除非你是打算寫寫玩具編譯器。」
因此我放棄了造出這些機器的想法,由於函數的一輩子過短了,但願你能實現個人願望。
在掃描器裏的體驗不太舒服,它像一臺 X 光機,把個人身體裏裏外外看了個遍,給個人感受很不妙。
出了機器,會收到一個檢查報告,像這樣(篇幅有限,只拿一個表達式舉例子):
拿着這份報告,就該去進行語法分析了。
語法分析器(Grammar Parser)就不須要我整個躺進去,只用把掃描器生成的檢查報告交給他。
分析好以後,我拿到了一個盆栽 新的報告 —— 一棵樹,或者,準確一點,一棵語法樹(Syntax Tree)。
樹的枝葉一切正常,表示個人表達式是合法的。毫無疑問,我再次經過了檢查。
但有的函數就不這麼幸運了,他們會在這一步檢查出問題,好比括號不匹配,表達式中缺乏操做符等等,這些錯誤會上報編譯器,最後報告給程序員。——他們面臨着整改的命運。
剛剛的語法分析器,顧名思義,只完成了語法層面的分析,但他不瞭解表達式是否是真的有意義。
好比讓兩個指針作乘法,在語法上是合法的,但這是沒有意義的。語義分析器(Semantic Analyzer)就可以檢查出這個錯誤。
但語義分析也不是萬能的,它也有侷限性——語義分析僅僅能分析靜態語句。
你問我什麼是靜態語義?
我不知道,由於我只是一個函數。
所謂靜態語義,是能在編譯期間能夠肯定的語義,與之相對的動態語義,就是隻有在運行期才能肯定的語義。
int a = 6 / 0;
從靜態語義上看,這句話是合法的,編譯期間不會報錯,但等到程序運行到這句時,就會報出 devided by 0
的錯誤,形成程序異常退出。
走到這,編譯部分也算快結束了。
剩下的兩臺機器,一臺叫代碼生成器(Code Generator),一臺叫目標代碼優化器(Target Code Optimizer)。
目標代碼優化器老是嫌棄代碼生成器,由於代碼生成器生成的代碼效率低,還須要他花大功夫來優化。
用優化器的話講:「生成器那傢伙,每次生成一堆低效率的代碼,我還得從頭讀到尾,進行基於數據流分析(data-flow analyse)技術的全局優化,太累了。」
其實代碼生成器有作優化,叫作局部代碼優化,只是優化程度遠遠不及優化器,因此他很差意思反駁優化器。
不過這不表明代碼生成器結構就簡單了,它生成代碼的過程十分依賴於目標機器——這意味着它要適配許許多多的機器,不一樣的機器有着不一樣的字長、寄存器、整數數據類型和浮點數數據類型等,它要考慮的事情太多了。
通過生成器,表達式的樣子發生了巨大的變化(這裏以 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
優化器對上面的代碼又作了一番深層次的優化,包括選擇尋址方式,刪除多餘指令等。(代碼比較短,因此優化效果並不明顯。)
movl index, %edx leal 32(,%edx,8), %eax movl %eax, array(,%edx,4)
每次走過這些流程,我都不得不感嘆於編譯器複雜的結構,也只有優秀的程序員們,纔可以完成這麼偉大的工程吧。
函數的編譯,就是這麼繁瑣,且枯燥。
今天令我驚訝的是,全部函數都完美的經過了編譯階段。
「Nice~ 此次能夠早點休息了!」不止是我,其餘函數也是這麼想的吧。
咱們有說有笑,悠然等待着連接程序來作最後的收尾工做。
但萬萬沒想到,危機竟出如今連接階段。
我聽長輩們說,連接器,擁有比編譯器更爲悠久的歷史。
每當我把這個事實告訴新來的函數時,他們老是一臉難以想象:
「咱們都是先編譯,再連接的,怎麼會先有連接器,再有編譯器?這又不是先有雞仍是先有蛋的哲學問題。」
我第一次據說的時候,也有這樣的疑惑。
「連接是在彙編語言時代就出現了的概念。在那以前,是機器語言的時代。可是想要對機器語言進行修改,那就太困難了,由於機器指令的修改常常形成具體指令地址的改變,牽一髮而動全身。因此彙編語言產生了,用符號來標記位置,而符號與實際地址的映射工做,就是連接器來作的。」我向他解釋道。
「我明白了,由於高級語言出如今後面,因此從高級語言到彙編語言的步驟——編譯,要比連接來的晚一些。」
是啊,編程語言的發展,從機器語言,到彙編語言,再到如今的高級語言,通過了幾十年的時間。但儘管是現代,咱們編譯型高級語言,想要運行,仍是得回到彙編語言,再被翻譯成機器語言,看起來是繞了一個大圈,但人類程序員的生產力,卻獲得了質的飛躍。
人類老是能想出各類辦法來減輕他們的工做量。
...
連接過程主要包括了地址和空間分配(Address and Storage Allocation)、符號決議(Symbol Resolution)和重定位(Relocation)等這些步驟。
看起來挺高大上,其實連接器作的和早期程序員人工調整地址沒什麼兩樣,只是更加複雜而已——你不要期望如今的語言特性比早期簡單。
但從本質上說,就是把指令對其餘符號地址的引用加以修正。連接的重點就是兩個不一樣的目標文件。
這一階段原本是很容易經過的,但今天,竟然出現了大錯誤。
問題出在 main.c 中。
出乎全部函數的意料,包括 main。
<!--由於靜態連接的步驟比較少,因此講的也比較簡略,之後會詳細補充連接器相關內容的-->
回到編輯器,咱們檢查遍了 main 函數內部的全部函數,從他們的聲明,再到他們的實現,全都沒有問題。
「會不會是 #include
的時候出了什麼問題?」有函數提出了本身的見解。
咱們決定分頭行動,一部分和其餘文件協做檢查函數聲明,剩下一部分負責排查有沒有出現循環 #include
問題。
不知過了多少 CPU 週期,你們回來了,一無所得,兩種問題都沒有出現。
咱們束手無策。
「 main.c ,連接出錯...」我滿腦子都在想可能緣由,「不會是 main 函數自己出了問題吧!」
「快,去看看宏定義有沒有異常!」
宏定義?雖然你們有些疑惑,但仍是照作了。
果真,發現了異常:
... ... #define main mian ... ...
我內心怒罵「誰這麼缺德,幹這種事情?!」
好在刪掉這條「間諜」指令後,一切恢復正常,完美經過編譯連接。
咱們終於能夠休息了。
PS:危險指令,請勿模仿。除非,,,你想挨一頓打。
PPS:函數的運行之後也會寫到。
PPPS:若是你們對文章有什麼見解和意見,歡迎提出來~ 若是以爲文章有意思,點個贊再走吧
文中插圖來自《程序員的自我修養》。
聲明:原創文章,未經受權,禁止轉載