編譯器如何工作

編譯器一直是我比較喜歡的話題。編譯器是個比較神奇的工具,它可以把原來毫無意義的字符數據轉變成一行一行可以執行的代碼。作爲每一個科班出身的同學來說,編譯原理都是專業學習中必須經歷的一個部分。只是在後來的工作中,真正從事編譯器開發的同學少之又少,但是如果你懂得了編譯原理的相關機理,會給你的工作帶來很大的幫助。關於編譯原理的書很多,網上可以搜一下有阿霍版本的《編譯原理》,有陳火旺院士的《編譯原理》,張素琴版本的《編譯原理》,三本書我都覺得不錯。另外,現在關於編譯原理也有很多的開發工具,比如說lex&yacc,只要你會編寫基本的語法範式,設計自己的編譯器也不是什麼難事。


    其實,現在的編譯器早已經突破了原來的概念。比如說,編譯器最終的代碼不一定在實際機器上運行,可能是虛擬機;編譯器編譯語言時不一定需要生成可執行文件,能解釋就行;編譯器最好並行編譯;編譯器不一定很大,可能十幾個文件就可以,比如說lua等等。不過,我們今天說的編譯器還是比較傳統的c編譯器,有興趣的同學可以看看編譯器是怎麼幫助我們生成可執行文件的。我們按照詞法、語法、語義、優化的順序逐一展開。現在假設有這樣一段代碼,

  1. #include <stdio.h>  
  2.   
  3. #define MAX_VALUE 7  
  4. int test(int value)  
  5. {  
  6.     return MAX_VALUE + 1 + value * 4;  
  7. }  
  8.   
  9. int main(int argc, char* argv)  
  10. {  
  11.     int p;  
  12.     p = test(3);  
  13.     printf("p = %d", p);  
  14.     return 1;  
  15. }  

   

    (1)詞法分析


    詞法分析是整個文件編譯最基本的環節。上面的文件中就存在很多的字符,那我們就需要分別對它們進行處理。比如說,通常的分類很可能是這樣的,

    a)字符是否是數字,例如7,1,4

    b)字符是否是string類型,例如“p = %d”

    c)字符是否是關鍵字,例如define

    d)字符是否是變量,例如value,argc, argv, p

    e)字符是否是運算符, 例如 +

    f)字符是否是圓括號、方括號、花括號等等



    (2)語法分析


    語法分析的目的就是構建一個語法樹,分析當前的文件是否符合編程語言的文法結構,比如說,

    a)整個字符串是否符合表達式要求

    b)字符串是否符合判斷語句要求

    c)字符串是否符合循環語句要求

    d)字符串是否符合函數要求

    e)字符串是否符合include語法要求

    f) 有沒有沒有未聲明就是用的變量等等;



    (3)語義分析


    語義分析有的時候和語法分析是聯繫在一起的。但是,這裏我們把它拆開來單獨成了一部分。所謂的語義分析,其實就是把前面生成的語法樹拆解下來,生成原子語句操作的過程。比如說,上面的文件很可能是這樣的形式,

  1. SET value  
  2. mov temp1[inner], 7  
  3. add temp1[inner], 1  
  4. mul temp2,value[param], 4  
  5. add temp1, temp2  
  6. mov result, temp1  
  7. pop  
  8.   
  9.   
  10. SET argc[param]  
  11. SET argv[param]  
  12. SET 3  
  13. call test  
  14. pop   
  15. get result  
  16. mov p[inner], result  
  17. SET p  
  18. SET string "p = %d"  
  19. pop  
  20. pop  
  21. mov result, 1  
  22. pop  
  23. pop  
    這裏需要解釋一下,語義轉換的結構和形式其實是各個編譯器自己定義的,未必有通用的結構。這裏的語句只是我自己想出來的,可能和實際的形式有很大的出入,但是基本方法應該是一樣的。主要解釋如下,

    a)SET值爲函數參數

    b)call爲函數調用

    c)pop爲堆棧平衡使用

    d)數據[inner],表示當前變量是函數中的臨時變量

    e)數據[param],表示當前變量是入參參數

    f)temp表示編譯器爲了自身計算方便,臨時添加的局部變量

    g)result表示返回值


    (4)代碼優化


    代碼優化是編譯器處理的一個重要環節,代碼優化的目的主要是減少不必要的計算和處理,比如

    a)計算沒有使用價值的臨時變量

    b)除去沒有判斷價值的if語句

    c)對於某些const變量,編譯器提前計算,這裏就可以對temp1提前計算

    d)其他優化措施等等



    (5)生成彙編代碼


    在(3)中生成的代碼只是中間代碼,並不是完全意義上的彙編語言。所以,編譯器還需要把它翻譯成對應的二進制代碼,比如說arm語言、x86語言或者是powerpc語言等等。當然這中間還是存在一些技巧的,比如

    a)對於多參數的函數,某些cpu可以用寄存器代替,有些cpu用堆棧表示

    b)某些cpu需要對字節對齊,某些cpu則不需要

    c)某些cpu有字節序的要求,某些cpu則無所謂,而有的cpu則可選

    d)對於臨時變量,有的cpu可以寄存器表示,而有的cpu只能自己生成一個temp變量等等,

    

    說到這裏,我們也可以自己小試一下身手,看看代碼怎麼生成,熟悉x86代碼的同學也可以自己試試,

  1. push ebp  
  2. mov ebp, esp  
  3. push ebx  
  4. push ecx  
  5. mov ebx, 8  
  6. mov ecx, ebp[8]  
  7. mul ecx, 4  
  8. add ebx, ecx  
  9. mov eax, ebx  
  10. pop ecx  
  11. pop ebx  
  12. mov esp, ebp  
  13. pop ebp  
  14.   
  15.   
  16. push ebp  
  17. mov ebp, esp  
  18. sub esp, 0x4  
  19. push 3  
  20. call test  
  21. add esp 4  
  22. mov ebp[-4], eax  
  23. push ebp[-4]  
  24. push string "p = %d"  
  25. call printf  
  26. add esp, 8  
  27. mov eax, 1  
  28. sub esp, 0x4  
  29. mov esp, ebp  
  30. pop ebp  
     

    (6)彙編級代碼優化


    這裏的優化其實還挺多的,但是功能基本有限,無外乎就是,

    a)乘法轉變成移位

    b)除法轉變成移位

    c)寄存器優化使用

    d)刪除寄存器的重複操作過程

    e)部分函數參數用寄存器代替等等



    (7)鏈接和生成可執行文件


    在編譯過程中,我們常常看到有些代碼編譯通過了,但是鏈接失敗了。這是很正常的事情,因爲在最後生成的文件當中,每一個變量和函數都應該有出處,否則就會鏈接失敗。不管是什麼系統平臺,鏈接都是個大學問。這個時候,做的事情其實還是比較多的,比如

    a)生成執行文件,確定是否帶調試信息

    b)鏈接所有的變量和代碼

    c)生成map文件

    d)確定函數和變量的出處,一旦查找失敗,結束

    e)調整變量和函數代碼的位置,填寫文件結構,生成最終可執行文件



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章