閱讀博客的朋友可以到我的網易雲課堂中,通過視頻的方式查看代碼的調試和執行過程:
http://study.163.com/course/courseMain.htm?courseId=1002830012
在通常情況下,編譯器會將目標語言轉換成某種中間語言格式,而不是直接將源代碼轉換成二進制機器指令。不少C語言編譯器,都會將代碼編譯成匯編語言,然后再通過匯編編譯器將匯編代碼轉換成目標機器可執行的二進制代碼,這么說來,匯編語言其實也是一種中間語言。
編譯成中間語言有很多優勢,一是可以優化,先把中間語言進行高度優化后,再將其轉換成機器指令,那么程序的速度可以成倍的提高。其二是可以實現跨平台,針對同一種中間語言,不同平台的編譯器可以將其轉換成與該平台兼容的二進制指令,從而使得一種源程序代碼可以運行到不同的硬件平台上。
還有一種好處,就是可以通過虛擬機來運行中間語言,從而突破硬件平台對語言的限制,例如java字節碼顯然就是一種中間語言,運行到java虛擬機上。我們本章或許會將C語言轉義成某種字節碼,然后開發一個虛擬機來運行生成的字節碼。由此,接下來的重點,我們將聚焦到指令集的格式,以及虛擬機的架構設計上。
中間語言的格式:三元組,四元組,逆向波蘭格式
中間語言的指令格式,一般如標題所提及的一樣,對大多數匯編語言來說,采取的就是三元組形式,這種格式的指令一般包含三部分:操作符,數據源,結果目標。例如指令:
ADD D0, D1
意思是將D0寄存器的數值與D1相加,並把相加后的結果存放到寄存器D1中。其實C語言也有等價功能的代碼表示:
d += s;
上面的語句用數學表示法如下:
(+=, d, s)
三元組指令格式又可以稱為兩地址指令,因為大多數指令都由源地址,目標地址,以及操作符構成。
四元組一般由四部分組成,兩個數據源地址,一個操作符,一個目標地址,例如:
d = s1 + s2;
數學化的表現形式如下:
(+, d, s1, s2)
有時候四元組指令並非都包含四部分,例如賦值語句:
(=, d, s, -)
第四部分的 -, 不是減號,而是橫桿,表示這一部分為空。第一部分表示操作,不能為空,所以上面指令的意思是:
d = s;
無論是三元組還是四元組,有時候目標地址無需明確的包含在指令中,例如下面兩條三元組指令:
(LESS_THAN, a, b)
(GOTO, target, -)
第一條指令比較兩個數的大小,並且將比較結果存放在某個地方,第二條指令的執行將依賴第一條指令的結果,如果第一條指令結果為true, 那么第二條指令將使得程序流跳轉到target指定的地址。
有時候,算術運算的指令也不會涉及到目標地址,例如下面兩條三元組語句將執行A = B + C:
(+, B, C)
(=, A, .-1)
第一條語句執行完加法運算后,把結果存儲到一個內部寄存器叫”加法寄存器”
第二條三元組語句,第三部分的”.”, 表示當前語句所在的地址,那么 “.-1”, 表示的就是上一條語句的地址,因此,第二條語句的作用是把上一條語句的運算結果賦值給A.
三元組相對於四元組有一個優勢,就是它與大多數匯編語言的格式很接近。我們本章將代碼編譯后,所形成的中間語言將采用三元組格式。但四元組也有三元組無法企及的好處,一是簡練,例如(+, d, s1, s2), 就需要兩條三元組來完成同等功能:
(=, d, s1)
(+=, d, s2)
此外,四元組相比於三元組,更容易進行優化,例如上面的兩條三元組語句,在優化時,需要將他們當做一個整體對待,代碼挪動時需要兩條語句一起挪動,而四元組只要挪動一條語句就可以了。
第三種常用的中間語言格式是逆向波蘭格式,PostScript, HP計算器,使用的中間代碼就是這種格式。這種格式的語句比較容易解析,同時語句解析時不需要分配臨時變量。例如表達式:
( 1 + 2 ) * (3 + 4)
對應的逆向波蘭格式為:
1 2 + 3 4 + *
逆向波蘭表達式的解析需要一個堆棧, 例如上面語句的解析過程如下:
stack input action
empty 1 2 + 3 4 + * push 1
1 2 + 3 4 + * push 2
1 2 + 3 4 + * 將棧頂兩元素出棧相加,
然后將相加的結果壓入
堆棧
3 3 4 + * push 3
3 3 4 + * push 4
3 3 4 + * 將棧頂兩元素出棧相加然
后將相加的結果壓入堆棧
3 7 * 將棧頂兩元素出棧相乘,
然后將相加的結果壓入堆棧
21 棧頂元素就是計算結果