二探·编译与连接
直觀上來說,編譯器就是將高級語言翻譯成機器語言的一種工具。因為使用機器指令或匯編語言編寫程序是一件十分費事,并且相當乏味的一件事。這使得程序開發效率變得相當低下。此外,使用機器語言或匯編語言編寫的程序十分依賴特定的機器,一個為某種CPU編寫的程序在另外一種CPU下完全無法運行。所以研究人員期盼這樣一種語言,我們可以采用類似于自然語言的語言來描述一個程序。高級語言由此誕生,它能使得程序員更加關注程序邏輯的本身,而盡量少考慮計算機本身的限制,這些交給編譯器做就好了。
1.編譯器做了什么?
編譯過程一般可以分為六個步驟:掃描、語法分析、語義分析、源代碼優化、代碼生成和目標代碼優化。整個過程如圖所示:以下面一段C語言代碼為例,來詳細分析這個過程: array[ index ] = ( index + 4 ) * ( 2 + 6 );掃描與詞法分析:
首先,源代碼程序被輸入到掃描器,掃描器的任務相當的簡單,他只是進行簡單的詞法分析,運用一種類似于有限狀態機(Finite State Mchine)的算法可以很輕松的將源代碼的字符序列分割成一系列的記號。比如,上面那個程序包含28個非空字符,經過掃描以后,生成16個記號,如下表所示:
| 記號 | 類型 |
| array | 標識符 |
| [ | 左方括號 |
| index | 標識符 |
| ] | 右方括號 |
| = | 賦值 |
| ( | 左圓括號 |
| index | 標識符 |
| + | 加號 |
| 4 | 數字 |
| ) | 右圓括號 |
| * | 乘號 |
| ( | 左圓括號 |
| 2 | 數字 |
| + | 加號 |
| 6 | 數字 |
| ) | 右圓括號 |
從上圖我們可以看出,整個語句被看作是一個賦值表達式:賦值表達式坐標是一個數組表達式,他的右邊是一個乘法表達式。符號和數字是最小的表達式,他們不是由其他的表達式來組成的,所以他們通常作為整個語法樹的葉節點。對于不同的編程語言,編譯器開發者只需要改變語法規則,而無需為每一個編譯器編寫一個語法分析器,所以它也被稱為“編譯器編譯器”。 語義分析: 語義分析僅僅是完成對表達式的語法層面的分析,但是他并不是了解這個語句是否整整有意義。編譯器所能分析的語義是靜態語義,所謂的靜態語義是指:在編譯期可以確定的語義;與之對應的是動態語義,就是只有在運行期才能確定的語義。 靜態語義通常包括聲明和類型的匹配、類型的轉換。比如當一個浮點型的表達式賦值給一個整型的表達式時,其中隱含了一個浮點型到整型轉換的過程,語義分析過程中,需要完成這個步驟。再例如,將一個浮點型賦值給一個指針的時候,語義分析程序會發現這個類型不匹配,編譯器將會報錯。動態語義一般值在運行期間出現的語義相關問題,比如將0作為除數是一個運行期間語義錯誤。 經過語義分析階段后,整個語法樹的表達式都被標記了類型,如果有些類型需要做隱式轉換,語義分析程序會在語法樹中插入相應的轉換節點。具體如下所示:
從上圖可以看到,每個表達式(包括符號和數字)都被標示了類型。 中間語言生成: 現代的編譯器有著很多層次的優化,往往在源代碼級別會有一個優化過程。源代碼級優化器會在源代碼級別進行優化,對于上面的程序,如果細心我們會發現,(2+6)這個表達式可能被優化掉,因為他的值在編譯期間就可以被確定。經過優化的語法樹如下圖所示:
雖然這看似是一個理所當然的操作,其實直接在語法樹上做優化比較困難,所以源代碼優化器往往將整個語法樹轉換成中間代碼,他是語法樹的順序表示,其實這已經非常接近目標代碼了。但是他一般跟目標機器和運行時的環境是無關的,比如它不包含數據的尺寸、變量地址和寄存器的名字等。中間代碼有很多種類型,在不同的編譯器中有著不同的形式,比較常見的有:三地址碼;P-代碼。 中間代碼使得編譯器可以被分為前端和后端。編譯器前端負責產生機器無關的中間代碼,編譯器后端將中間代碼轉換為目標機器代碼。這樣對于一些可以跨平臺的編譯器而言,他們可以針對不同的平臺使用同一個前端和針對不同機器平臺的數個后端。 目標代碼生成與優化: 源代碼級優化器產生中間代碼標志著下面的過程都屬于編譯器后端。編譯器后端主要包括代碼生成器和目標代碼優化器。 代碼生成器將中間代碼生成目標機器代碼,這個過程十分依賴于目標機器,因為不同的機器有著不同的字長、寄存器、整數數據類型和浮點數數據類型等。對于上面的代碼,代碼生成器可能會生成下面的代碼序列: 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最后目標代碼優化器對上述的目標代碼進行優化,比如選擇合適的尋址方式;使用位移來代替乘法運算、刪除多余的指令。在上面的例子中,乘法是由一條相對復雜的基址比例變址尋址的lea來完成。
2.總結
現代的編譯器有著異常復雜的結構,這是因為現代高級編程語言本身非常地復雜,比如C++語言的定義就極為復雜,至今沒有一個編譯器能夠完整支持C++語言標準所規定的所有語言特性。另外現代的計算機CPU相當地復雜,CPU本身采用了諸如流水線、多發射、超標量等諸多復雜特性,為了支持這些特性,編譯器的機器指令優化過程也變得十分復雜。使得編譯過程更為復雜的是有些編譯器支持多種硬件平臺,即允許編譯器編譯出多種目標CPU的代碼。比如著名的GCC編譯器就支持幾乎所有CPU平臺,這也導致編譯器的指令生成過程更為復雜。 經過掃描、語法分析、語義分析、源代碼優化、代碼生成和目標代碼優化,編譯器忙活了這么多個步驟之后,源代碼終于被編譯成了目標代碼。但是這個目標代碼中有一個問題:index和array的地址還沒有確定。如果我們要把目標代碼使用匯編器會變成真正的能夠在機器上執行的指令,那么index和array的地址應該從哪里得到呢?如果index和array定義在跟上面的源代碼同一個編譯單元里面,那么編譯器可以為index和array分配空間,確定他們的地址:那么我們又該如何定義在其他的程序模塊呢? 這其中會有很多問題值得討論,比如目標代碼代碼中有變量定義在其他模塊中怎么辦?事實上,定義其他模塊的全局變量和函數在最終運行時的絕對地址都要在最終鏈接的時候才能確定。所以現代的編譯器可以將一個源代碼文件編譯成一個未鏈接的目標文件,然后由鏈接器最終將這些目標文件鏈接起來形成可執行文件。總結
- 上一篇: ipmsg飞鸽传书系统即时通讯
- 下一篇: 三探·模块拼装思想(静态链接)