1. 程式人生 > >編譯性語言和解釋性語言

編譯性語言和解釋性語言

我們知道,任何程式語言編寫的程式歸根到底都是由底層機器的機器程式碼(01序列)執行的,無論是編譯型語言還是解釋型語言。而任何高階程式語言程式的原始碼都是一個字元序列,這個字元序列到底層的01序列是通過編譯器或解析器經過多次轉換完成的。

圖1 程式語言的層次結構

        這個層次結構中,從高到低越來越接近於機器硬體。機器程式碼就是01序列,組合語言就是描述本地機器的指令集體系結構,而高階語言就包含相應的資料結構和語法結構,更接近人類的語言習慣。因此,層次越高就越面向於人類。在電腦科學中,CPU被抽象為指令集體系結構,這個指令集描述了CPU所有完成的所有功能。所有的程式都經過編譯或解釋轉化為這個指令集表示的機器程式。在指令集中指令可以按功能劃分為:

        1. 資料傳輸指令,用於讀寫記憶體、暫存器。

        2. 算術與邏輯運算指令,比如:addl執行雙字(32bit)的加法,andl雙字的按位與。

        3. 控制流指令,用於實現高階程式語言中的分支、迴圈等控制結構。

        4. 過程呼叫指令,用於實現函式呼叫,分配、恢復棧幀等操作。

        任何程式都需要被轉換為某個指令集的指令序列,比如下列簡單的求階乘的C程式:

 

[cpp] view plain copy

 print?

  1. int fact_while(int n)  
  2. {  
  3.     int result = 1;  
  4.     while (n > 1) {  
  5.         result *= n;  
  6.         n = n-1;  
  7.     }  
  8.     return result;  
  9. }  


在32bit機器上,經過gcc編譯之後的x86指令序列為:

 

 

[plain] view plain copy

 print?

  1.     movl 8(%ebp), %edx   
  2.     movl $1, %eax   
  3.     cmpl $1, %edx   
  4.     jle .L7   
  5. .L10:   
  6.     imull %edx, %eax   
  7.     subl $1, %edx   
  8.     cmpl $1, %edx   
  9.     jg .L10  
  10. .L7:  

 

        通過觀察C程式的機器程式碼可以發現由C程式轉化為機器程式碼,主要有資料型別和控制結構的轉換。下面以x86指令集說明:

 

        1. 資料型別的轉換:在底層,x86指令對於資料是不區分邏輯型別的,也就是不分int,float,double。所有的資料按照其所佔的位元組數被歸類為字(16個位元組,Word)、雙字(32個位元組,Double Words)、四字(64個位元組,Quad Words)。一個指令操作的資料型別是由這個指令的字尾表示的,比如mov指令,movw操作字,movl操作雙字。也就是說高階語言的程式中的不同資料型別反映到底層指令集上主要體現是指令的不同。比如,將上述C程式中的result型別改為short,在相應的彙編程式碼中的mov指令會由movl轉換為movw。當然,還有一個問題就是C語言中的具體資料型別,在機器程式碼中是如何儲存表示的。這應該是gcc編譯器的職責,比如對於int,首先gcc需要知道底層指令集如何編碼int,採用什麼編碼方式,位元組順序是Big-endian還是Little-endian等。在知道底層的實現方式後gcc才能將表示整型數字的字串編碼為相應的二進位制形式。而對於陣列、struct和union這些資料結構會轉化為相應的記憶體地址加偏移量的形式。

        2. 控制結構的轉換:控制結構就是執行指令的流程。在x86中,所有的指令集都是順序執行。要實現分支、迴圈等結構,必須具備go形式的跳轉指令,以及相應的條件判斷指令。CPU中有一組條件碼暫存器,指示算術或邏輯運算的狀態(計算結果是否溢位、為0或者是負數等)。執行條件運算指令可以測試一個條件,比如"cmpl $1, %edx"比較直接數1與暫存器%edx中存放的數的大小,並將結果存入條件碼暫存器中。接下來執行條件跳轉指令,根據條件碼暫存器中的狀態進行判斷是否進行跳轉。比如“jg .L10”是在前一條的cmpl指令結果返回大於的情況跳轉到L10,否則執行下一條指令。

        當然,在進行函式呼叫時,還要在底層用機器碼對其進行描述。我們知道,電腦科學中用棧來實現函式的呼叫(叫做呼叫棧),棧中存放棧幀。每一次函式呼叫對應一個棧幀,棧幀中包含該方法的區域性變數、儲存的暫存器值等資料。這樣函式的呼叫和返回就對應著棧幀的入棧和出棧。CPU的暫存器組中,有兩個專門用於實現方法呼叫,分別是%esp和%ebp。%esp是棧指標暫存器,存放當前函式棧棧頂的記憶體地址。%ebp是幀指標暫存器,在%esp和%ebp之間的記憶體地址序列就對應於當前函式的棧幀。由於函式呼叫、返回與棧幀的關係很密切,所以可以將以此函式呼叫過程描述為:

        1. 初始化被呼叫函式的棧幀,並將其入棧。也就是呼叫函式過程,通過call指令實現。

        2. 執行被呼叫函式。

        3. 恢復呼叫函式的棧幀,將被呼叫函式的棧幀出棧。也就是函式返回的過程,通過ret指令實現。

        對於初始化、恢復棧幀實際上都是%esp和%ebp的調整,還要包括傳參和返回值的問題,這些都是由編譯器實現的。

        上面介紹了C語言和機器語言的關係,下面看一下其他型別語言的實現機制。首先,我們可以把程式語言分為編譯型語言、解釋型語言和虛擬機器語言。編譯型語言直接被編譯成本地機器程式碼,比如C、C++。解釋型語言是通過直譯器執行,比如JavaScript、shell、Python等。虛擬機器語言執行在虛擬機器上,需要被編譯成虛擬機器程式碼,由虛擬機器執行,比如Java。雖然python也有自己的虛擬機器,但是不需要編譯,所以把它歸類為解釋型語言。

圖2 程式語言實現結構

        通過上文的分析、我們知道對於一門語言最重要的是資料型別、控制結構和語法結構以及系統呼叫。從上圖可以看出,C和C++更接近於底層硬體,但是不能像組合語言一樣可以直接訪問暫存器等硬體。而python和java相對於C和C++的抽象層次又高了一層,它們不能通過指標直接訪問記憶體。從機器語言->組合語言->系統語言(C和C++)->解釋型語言(python)和虛擬機器語言(java),抽象層次越來越高,越貼近於人的思維,不需要考慮那麼多細節;同時,程式設計師的自由度和程式的執行速度越來越低。下面從低向高j討論一下。

        在底層,組合語言會經過彙編器轉換為機器程式碼。比如,通過gcc編譯C程式時,會調用匯編器進行彙編。通過彙編器和組合語言這一層次,可以很好的隔離底層機器硬體的實現細節。不同的處理器具有與之對應的彙編器,將組合語言彙編成該處理器支援的指令集。這樣就是實現了組合語言這一層的移植性。

        在C和C++系統程式語言這一層,會通過編譯器完成語言元素到組合語言的對映。比如前文描述的,資料型別、控制結構、函式呼叫等結構的轉換。

        python是解釋型語言,它通過python直譯器實現向底層語言的對映。我們知道python虛擬機器是由C語言編寫的,所以python程式會轉化為C程式而執行。比如,python中的所有物件都會在C中有對應的PyObject結構體。python的list、dict等資料型別也要在C中有對應的表示。而像生成器、迭代器等語法結構需要相應的支援。

        而虛擬機器是模擬一個指令集的程式,所以它自身有一套獨立於具體硬體、作業系統的指令集。需要通過底層語言實現這套指令集。虛擬機器本身也有自己的資料型別系統、語言結構等。比如,java虛擬機器上支援的資料型別有基本資料型別和引用型別,也支援tableswitch和lookupswitch等實現switch語法結構的位元組碼指令。對於這些語言元素對映到底層語言的實現方式可以不同的方式。首先是直譯器模式轉化為C++,還有就是JIT直接編譯成本地機器程式碼。

        像java這樣的虛擬機器語言會被編譯器編譯成虛擬機器本地的機器程式碼,然後再虛擬機器上執行,這裡就需要向javac編譯器實現java語言的資料型別、語言結構和java虛擬機器上的資料型別、語法結構的對映。

        通過談論,可以看出編譯器和直譯器以及虛擬機器在程式語言中的重要性,它們都是程式語言可以在計算機上執行的基石。一門程式語言的編譯器、直譯器或者虛擬機器可以很大程度上影響這門語言的執行效率。因為它們在進行語言轉換時會進行很多的優化以提高執行效率。這也是為什麼JVM上有那麼多優秀的語言,因為JVM很強大。所以,要深入語言的底層,要學會編譯器、直譯器和虛擬機器的實現,這方面還需要下功夫啊。

--------------------- 本文來自 zhangzker 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/zhangzker/article/details/53171132?utm_source=copy