java開發C語言編譯器:把printf編譯成java位元組碼的演算法詳解
上一節,我們探討了jvm函數調用時,參數是如何傳遞的。上節對參數傳遞的方式有個錯誤,這裡先更正一下。在上節,我是這麼說明jvm函數調用時的參數傳遞方式的:
當上面函數運行時,在執行函數lotsOfArguments前,jvm會把輸入參數全部放到堆棧上,當函數被執行時,參數會從堆棧拷貝到局部變數隊列,因此當lotsOfArguments執行前,堆棧上參數如下:
stack:
d
c
b
a
函數執行時堆棧上的參數會依次拷貝到局部變數隊列,情況如下:
local_list: d c b a 。
上面說法錯誤在於,參數從堆棧拷貝到局部隊列的次序說反了,參數拷貝到隊列後的次序應該是:
local_list: a b c d.
由此,當我們的編譯器把C含有傳遞參數的函數調用的代碼編譯成java位元組碼時,需要注意處理上面所說的參數傳遞的次序,上一節,我們使用了一個名為:getLocalVariableIndex(Symbol symbol)的函數來查找函數輸入參數對應在局部變數隊列上的位置,這裡我們需要對其實現根據上面的修改來更正一下,好在需要改正的代碼不多,內容如下:
說到有輸入參數的函數調用,在我們的C語言代碼里,用到最多的是printf,因此有必要就該函數如何編譯成java位元組碼再進行詳細的解析。假設C代碼中有這樣一條語句:
上面的代碼本意是要把整形變數x的值輸出到控制台,但我們實現的編譯器在解析上面代碼時,會先解析變數x, 當解析到一個變數x時,假設變數x在局部隊列中的次序是0,那麼我們當前實現的編譯器一旦解析到變數x時,會先輸出一條java位元組碼指令:
ILOAD 0
解析完變數x後,編譯器才會明白printf不是一個變數,而是一個函數名稱,這時我們的編譯器才會知道,要把printf與java對象System.out對應起來,於是就會把System.out對象壓入到堆棧上,因此就會輸出java位元組碼指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
這樣的話就會把System.out對象壓入到堆棧上,此時堆棧的情況如下:
stack:
System.out
x
這樣一來就有問題了,要想正確列印x變數的值,堆棧上的情況應該是:
stack:
x
System.out
也就是說兩者在堆棧上的位置發生了顛倒。為了處理這個問題,需要修改一下編譯器的編譯流程,當編譯器先解讀了變數x,導致x的值先壓入堆棧,當編譯器接著解讀到printf時,我們必須先把變數x從堆棧上,因為變數x是從局部變數隊列載入到堆棧上的,也就是說上面執行完語句ILOAD 0 後,變數x同時存在在堆棧和隊列上:
stack:
x
local_list: x
此時,我們需要把x從堆棧上挪開,挪開後放到哪呢?我們把它挪開後,放到局部變數隊列的末尾,因此為了把x挪到局部變數隊列的末尾,我們接著執行指令:ISTROE 1, 於是堆棧和隊列的情況在執行指令後如下:
stack:
null
local_list: x , x
也就是說,此時變數x的值在隊列上有兩份,你會疑問直接把x的值從堆棧頂部彈掉,壓入System.out對象後,再次把x從隊列裡面再壓入堆棧不就可以了嗎,亦或者我們把x從堆棧頂部挪開時,直接挪回x原來在隊列中的位置,也就是執行ISTORE 0 不就可以了嗎。這麼做是不可以的,因為如果是這種情況:
此時我們要輸出的是x+1的值,輸出後變數x的值是不變的,編譯器解讀上面代碼時,會先解讀到x+1,於是它會把這個表達式的值壓入堆棧,形成如下情景:
stack:
x+1
local_list: x
因此如果直接把堆棧頂部元素彈開,那麼x+1的值就找不來了,如果直接把堆棧頂部的內容存回到x變數在隊列中的位置,那就變成:
stack:
null
local_list: x+1
這樣的話,變數x的內容就改變了,因此也不行,所以要把堆棧頂部元素的值存到局部變數隊列的末尾,也就是執行指令ISTORE 1:
stack:
null
local_list: x, x+1
接著把System.out壓入堆棧,也就是執行指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
這時候情況如下:
stack:
System.out
local_list: x, x+1
這時候,再把隊列末尾的元素壓到堆棧頂部,也就是執行語句ILOAD 1,於是情況變成:
stack:
x+1
System.out
local_list: x, x+1
此時再調用System.out對象的print(I)V方法,也就是列印一個整形的方法,因此對應的指令就是:
invokevirtual java/io/PrintStream/print(I)V
所以綜合起來說,當編譯器編譯語句:
成為java位元組碼時,編譯後的代碼為:
我們再看複雜一點的情況:
該函數有4個輸入參數,因此函數在jvm執行時,四個輸入參數會放置在局部變數隊列上:
local_list: a, b , c , x, d
變數a處於局部變數隊列的低0處,變數b處於隊列位置為1處,變數c處於隊列位置為2處,變數x處於隊列位置為3處。由於函數還有一個局部變數d,因此變數d在隊列的末尾,也就是處於隊列位置為4處。
在編譯器解讀語句printf(「%d, %d, %d, %d」, a, b, c, x);時,根據我們前面的討論,編譯器會先解讀函數的輸入參數,也就是先解讀變數a,b,c,d,因為每解讀到一個變數是,我們的編譯器會自動執行iload語句,把變數載入到堆棧上,因此就產生了如下指令:
上面指令執行後,jvm堆棧和隊列情況如下:
stack :
x
c
b
a
local_list: a, b , c, x , d
根據前面討論,我們需要先把變數從堆棧上挪到隊列的末尾,因此編譯器要執行指令:
於是堆棧和隊列的情景如下:
stack:
null
local_list: a, b , c, x, d, x, c, b , a
這時,編譯器再把System.out壓入堆棧,也就是執行指令:
接著把處於位置8處的變數a的值再重新放回堆棧,也就是執行指令ILOAD 8,此時運行環境為:
stack:
a
System.out
local_list: a, b, c, x ,d ,x ,c ,b, a
這時調用指令invokevirtual java/io/PrintStream/print(I)V,執行System.out對象的Print函數,把a變數的值輸出到控制台。接著反覆執行這幾個步驟,把剩下幾個變數的值依次列印到控制台上,於是上面代碼中的printf語句編譯成java位元組碼後的內容如下:
大家可以看到,一條C語言語句編譯成java位元組碼時,編出的語句數量是原語句的十幾倍,這也是為何在編譯原理中,代碼優化是極為重要的一環,沒有優化技術,就算編出來的代碼能跑,但是效率也是非常低下的。
實現上面編譯功能的代碼在ClibCall.java中:
最後,我們再看看jvm的運算指令,對整形進行四種基礎運行的jvm指令為:iadd, isub, imul, idiv. 如果要計算 1+2, 那麼分別把數值1和2壓入堆棧,然後執行指令iadd, 那麼堆棧頂部的元素就會被彈出,他們的和也就是3會被壓入到堆棧,其他運算指令的原理相同。如果計算的是浮點數而不是整形,那麼就得使用對應指令,他們分別是fadd, fsub, fmul, fdiv,如果計算的是長整形,那麼對應的指令就是ladd, lsub, lmul, ldiv, 在我們實現的編譯器中,目前暫時只支持對整形的運算指令,相應的代碼實現在BinaryExecutor.java:
完成本節代碼後,我們的編譯器能將下面C代碼編譯成java位元組碼:
上面代碼編譯成java彙編代碼的結果為:
把上面java彙編代碼在編譯成二進位位元組碼運行後結果如下:
更詳細的講解和調試演示過程請參看視頻。
更多技術信息,包括操作系統,編譯器,面試演算法,機器學習,人工智慧,請關照我的公眾號:
※java開發系統內核:應用程序與系統內核的內存隔離
※jvm的return指令以及局部變數的操作
TAG:Coding迪斯尼 |
※Windows 版本的 Chrome 停用微軟的編譯器 改用 Clang
※一統所有AI晶元:Facebook揭秘深度學習編譯器Glow
※棄用微軟 C+編譯器,Win版Chrome 改用 Clang
※Dropbox創建了一種新的Python編譯器:mypyc
※C sharp 翻身?微軟重寫開源的 C sharp 編譯器!
※GameMaker 偏門理論:關於反編譯器
※TVM:Deep Learning模型的優化編譯器
※業界 | 一統所有AI晶元:Facebook揭秘深度學習編譯器Glow
※自製Monkey編程語言編譯器:增加數組操作API和Mapsh數據類型
※助力 Android 抗衡 iOS,華為發布方舟編譯器!
※TVM: Deep Learning模型的優化編譯器(強烈推薦, 附踩坑記錄)
※Unity編譯器測試框架
※藉助 Valve 的新編譯器,Linux 遊戲在 AMD GPU 中獲得了性能提升
※升級Azure、開源Q#編譯器和模擬器,一文看盡微軟Build大會
※Renesas Synergy增加對IAR Systems先進編譯器技術的支持,進一步鞏固該平台在IOT領域的性能優勢
※解讀gcc和g++編譯器分別對c與c++文件影響
※Julia官宣:為機器學習構建一種語言和編譯器
※Valve為AMD GPU開發新的Mesa著色器編譯器,編譯速度更快,遊戲幀數有所提高
※這個改造了Android的華為方舟編譯器,厲害不?
※從GPUTurbo和方舟編譯器可看出,華為開發操作系統很容易