上一節(jié),我們探討了jvm函數(shù)調(diào)用時,參數(shù)是如何傳遞的。上節(jié)對參數(shù)傳遞的方式有個錯誤,這里先更正一下。在上節(jié),我是這么說明jvm函數(shù)調(diào)用時的參數(shù)傳遞方式的:
static double lotsOfArguments(int a, long b, float c, String[][] d) {
....
}
當上面函數(shù)運行時,在執(zhí)行函數(shù)lotsOfArguments前,jvm會把輸入?yún)?shù)全部放到堆棧上,當函數(shù)被執(zhí)行時,參數(shù)會從堆??截惖骄植孔兞筷犃?,因此當lotsOfArguments執(zhí)行前,堆棧上參數(shù)如下:
stack:
d
c
b
a
函數(shù)執(zhí)行時堆棧上的參數(shù)會依次拷貝到局部變量隊列,情況如下:
local_list: d c b a 。
上面說法錯誤在于,參數(shù)從堆??截惖骄植筷犃械拇涡蛘f反了,參數(shù)拷貝到隊列后的次序應(yīng)該是:
local_list: a b c d.
由此,當我們的編譯器把C含有傳遞參數(shù)的函數(shù)調(diào)用的代碼編譯成java字節(jié)碼時,需要注意處理上面所說的參數(shù)傳遞的次序,上一節(jié),我們使用了一個名為:getLocalVariableIndex(Symbol symbol)的函數(shù)來查找函數(shù)輸入?yún)?shù)對應(yīng)在局部變量隊列上的位置,這里我們需要對其實現(xiàn)根據(jù)上面的修改來更正一下,好在需要改正的代碼不多,內(nèi)容如下:
public int getLocalVariableIndex(Symbol symbol) {
TypeSystem typeSys = TypeSystem.getTypeSystem();
String funcName = nameStack.peek();
Symbol funcSym = typeSys.getSymbolByText(funcName, 0, "main");
ArrayList<Symbol> localVariables = new ArrayList<Symbol>();
Symbol s = funcSym.getArgList();
while (s != null) {
localVariables.add(s);
s = s.getNextSymbol();
}
Collections.reverse(localVariables); //相比上節(jié)代碼,我們只需添加這行就能改正上面所說的錯誤
ArrayList<Symbol> list = typeSys.getSymbolsByScope(symbol.getScope());
for (int i = 0; i < list.size(); i++) {
if (localVariables.contains(list.get(i)) == false) {
localVariables.add(list.get(i));
}
}
for (int i = 0; i < localVariables.size(); i++) {
if (localVariables.get(i) == symbol) {
return i;
}
}
return -1;
}
說到有輸入?yún)?shù)的函數(shù)調(diào)用,在我們的C語言代碼里,用到最多的是printf,因此有必要就該函數(shù)如何編譯成java字節(jié)碼再進行詳細的解析。假設(shè)C代碼中有這樣一條語句:
printf("the value of a, b , c is: %d, %d, %d", a, b, c);
我們知道,在Java中對應(yīng)printf函數(shù)的是System.out.print,我要在jvm中實現(xiàn)上面printf函數(shù)的功能,我們需要先把對象System.out壓入到堆棧,然后再把要打印到控制臺上的變量內(nèi)容壓入到堆棧上。我們先看一個簡單點的版本:
printf("%d", x);
上面的代碼本意是要把整形變量x的值輸出到控制臺,但我們實現(xiàn)的編譯器在解析上面代碼時,會先解析變量x, 當解析到一個變量x時,假設(shè)變量x在局部隊列中的次序是0,那么我們當前實現(xiàn)的編譯器一旦解析到變量x時,會先輸出一條java字節(jié)碼指令:
ILOAD 0
解析完變量x后,編譯器才會明白printf不是一個變量,而是一個函數(shù)名稱,這時我們的編譯器才會知道,要把printf與java對象System.out對應(yīng)起來,于是就會把System.out對象壓入到堆棧上,因此就會輸出java字節(jié)碼指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
這樣的話就會把System.out對象壓入到堆棧上,此時堆棧的情況如下:
stack:
System.out
x
這樣一來就有問題了,要想正確打印x變量的值,堆棧上的情況應(yīng)該是:
stack:
x
System.out
也就是說兩者在堆棧上的位置發(fā)生了顛倒。為了處理這個問題,需要修改一下編譯器的編譯流程,當編譯器先解讀了變量x,導(dǎo)致x的值先壓入堆棧,當編譯器接著解讀到printf時,我們必須先把變量x從堆棧上,因為變量x是從局部變量隊列加載到堆棧上的,也就是說上面執(zhí)行完語句ILOAD 0 后,變量x同時存在在堆棧和隊列上:
stack:
x
local_list: x
此時,我們需要把x從堆棧上挪開,挪開后放到哪呢?我們把它挪開后,放到局部變量隊列的末尾,因此為了把x挪到局部變量隊列的末尾,我們接著執(zhí)行指令:ISTROE 1, 于是堆棧和隊列的情況在執(zhí)行指令后如下:
stack:
null
local_list: x , x
也就是說,此時變量x的值在隊列上有兩份,你會疑問直接把x的值從堆棧頂部彈掉,壓入System.out對象后,再次把x從隊列里面再壓入堆棧不就可以了嗎,亦或者我們把x從堆棧頂部挪開時,直接挪回x原來在隊列中的位置,也就是執(zhí)行ISTORE 0 不就可以了嗎。這么做是不可以的,因為如果是這種情況:
printf("%d", x+1);
此時我們要輸出的是x+1的值,輸出后變量x的值是不變的,編譯器解讀上面代碼時,會先解讀到x+1,于是它會把這個表達式的值壓入堆棧,形成如下情景:
stack:
x+1
local_list: x
因此如果直接把堆棧頂部元素彈開,那么x+1的值就找不來了,如果直接把堆棧頂部的內(nèi)容存回到x變量在隊列中的位置,那就好變成:
stack:
null
local_list: x+1
這樣的話,變量x的內(nèi)容就改變了,因此也不行,所以要把堆棧頂部元素的值存到局部變量隊列的末尾,也就是執(zhí)行指令I(lǐng)STORE 1:
stack:
null
local_list: x, x+1
接著把System.out壓入堆棧,也就是執(zhí)行指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
這時候情況如下:
stack:
System.out
local_list: x, x+1
這時候,再把隊列末尾的元素壓到堆棧頂部,也就是執(zhí)行語句ILOAD 1,于是情況變成:
stack:
x+1
System.out
local_list: x, x+1
此時再調(diào)用System.out對象的print(I)V方法,也就是打印一個整形的方法,因此對應(yīng)的指令就是:
invokevirtual java/io/PrintStream/print(I)V
所以綜合起來說,當編譯器編譯語句:
printf("%d", x);
成為java字節(jié)碼時,編譯后的代碼為:
iload 0
istore 1
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 1
invokevirtual java/io/PrintStream/print(I)V
我們再看復(fù)雜一點的情況:
int f(int a, int b, int c, int x) {
printf("%d, %d, %d, $d", a, b ,c , x);
int d = x;
return d;
}
該函數(shù)有4個輸入?yún)?shù),因此函數(shù)在jvm執(zhí)行時,四個輸入?yún)?shù)會放置在局部變量隊列上:
local_list: a, b , c , x, d
變量a處于局部變量隊列的低0處,變量b處于隊列位置為1處,變量c處于隊列位置為2處,變量x處于隊列位置為3處。由于函數(shù)還有一個局部變量d,因此變量d在隊列的末尾,也就是處于隊列位置為4處。
在編譯器解讀語句printf("%d, %d, %d, %d", a, b, c, x);時,根據(jù)我們前面的討論,編譯器會先解讀函數(shù)的輸入?yún)?shù),也就是先解讀變量a,b,c,d,因為每解讀到一個變量是,我們的編譯器會自動執(zhí)行iload語句,把變量加載到堆棧上,因此就產(chǎn)生了如下指令:
iload 0 ;解析變量a
iload 1 ;解析變量b
iload 2 ;解析變量c
iload 3 ;解析變量x
上面指令執(zhí)行后,jvm堆棧和隊列情況如下:
stack :
x
c
b
a
local_list: a, b , c, x , d
根據(jù)前面討論,我們需要先把變量從堆棧上挪到隊列的末尾,因此編譯器要執(zhí)行指令:
iload 5 ;把x放到隊列位置為5處
iload 6 ;把c放到隊列位置為6處
iload 7 ;把b放到隊列位置為7處
iload 8 ;把a放到隊列位置為8處
于是堆棧和隊列的情景如下:
stack:
null
local_list: a, b , c, x, d, x, c, b , a
這時,編譯器再把System.out壓入堆棧,也就是執(zhí)行指令:
getstatic java/lang/System/out Ljava/io/PrintStream;
接著把處于位置8處的變量a的值再重新放回堆棧,也就是執(zhí)行指令I(lǐng)LOAD 8,此時運行環(huán)境為:
stack:
a
System.out
local_list: a, b, c, x ,d ,x ,c ,b, a
這時調(diào)用指令invokevirtual java/io/PrintStream/print(I)V,執(zhí)行System.out對象的Print函數(shù),把a變量的值輸出到控制臺。接著反復(fù)執(zhí)行這幾個步驟,把剩下幾個變量的值依次打印到控制臺上,于是上面代碼中的printf語句編譯成java字節(jié)碼后的內(nèi)容如下:
iload 0
iload 1
iload 2
iload 3
istore 5
istore 6
istore 7
istore 8
getstatic java/lang/System/out Ljava/io/PrintStream;
ilod 8
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ilod 7
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ilod 6
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ilod 5
invokevirtual java/io/PrintStream/print(I)V
大家可以看到,一條C語言語句編譯成java字節(jié)碼時,編出的語句數(shù)量是原語句的十幾倍,這也是為何在編譯原理中,代碼優(yōu)化是極為重要的一環(huán),沒有優(yōu)化技術(shù),就算編出來的代碼能跑,但是效率也是非常低下的。
實現(xiàn)上面編譯功能的代碼在ClibCall.java中:
private Object handlePrintfCall() {
ArrayList<Object> argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(false);
String argStr = (String)argsList.get(0);
String formatStr = "";
int i = 0;
int argCount = 1;
String str = "";
while (i < argStr.length()) {
if (argStr.charAt(i) == '%' && i+1 < argStr.length() &&
argStr.charAt(i+1) == 'd') {
i += 2;
//generateJavaAssemblyForPrintf(str);
str = "";
formatStr += argsList.get(argCount);
argCount++;
//printInteger();
} else {
str += argStr.charAt(i);
formatStr += argStr.charAt(i);
i++;
}
}
System.out.println(formatStr);
generateJavaAssemblyForPrintf(argStr, argCount - 1);
return null;
}
private void generateJavaAssemblyForPrintf(String argStr, int argCount) {
ProgramGenerator generator = ProgramGenerator.getInstance();
String funcName = generator.getCurrentFuncName();
TypeSystem typeSystem = TypeSystem.getTypeSystem();
ArrayList<Symbol> list = typeSystem.getSymbolsByScope(funcName);
int localVariableNum = list.size();
int count = 0;
while (count < argCount) {
generator.emit(Instruction.ISTORE, "" + (localVariableNum + count));
count++;
}
int i = 0;
String str = "";
count = argCount - 1;
while (i < argStr.length()) {
if (argStr.charAt(i) == '%' && i+1 < argStr.length() &&
argStr.charAt(i+1) == 'd') {
i += 2;
printString(str);
str = "";
printInteger(localVariableNum + count);
count--;
} else {
str += argStr.charAt(i);
i++;
}
}
printString("\n");
}
最后,我們再看看jvm的運算指令,對整形進行四種基礎(chǔ)運行的jvm指令為:iadd, isub, imul, idiv. 如果要計算 1+2, 那么分別把數(shù)值1和2壓入堆棧,然后執(zhí)行指令iadd, 那么堆棧頂部的元素就會被彈出,他們的和也就是3會被壓入到堆棧,其他運算指令的原理相同。如果計算的是浮點數(shù)而不是整形,那么就得使用對應(yīng)指令,他們分別是fadd, fsub, fmul, fdiv,如果計算的是長整形,那么對應(yīng)的指令就是ladd, lsub, lmul, ldiv, 在我們實現(xiàn)的編譯器中,目前暫時只支持對整形的運算指令,相應(yīng)的代碼實現(xiàn)在BinaryExecutor.java:
public class BinaryExecutor extends BaseExecutor{
@Override
public Object Execute(ICodeNode root) {
....
case CGrammarInitializer.Binary_Plus_Binary_TO_Binary:
case CGrammarInitializer.Binary_DivOp_Binary_TO_Binary:
case CGrammarInitializer.Binary_Minus_Binary_TO_Binary:
case CGrammarInitializer.Binary_Start_Binary_TO_Binary:
//先假設(shè)是整形數(shù)相加
int val1 = (Integer)root.getChildren().get(0).getAttribute(ICodeKey.VALUE);
int val2 = (Integer)root.getChildren().get(1).getAttribute(ICodeKey.VALUE);
if (production == CGrammarInitializer.Binary_Plus_Binary_TO_Binary) {
String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " plus " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
root.setAttribute(ICodeKey.VALUE, val1 + val2);
root.setAttribute(ICodeKey.TEXT, text);
System.out.println(text + " is " + (val1+val2) );
ProgramGenerator.getInstance().emit(Instruction.IADD);
} else if (production == CGrammarInitializer.Binary_Minus_Binary_TO_Binary) {
String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " minus " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
root.setAttribute(ICodeKey.VALUE, val1 - val2);
root.setAttribute(ICodeKey.TEXT, text);
System.out.println(text + " is " + (val1-val2) );
ProgramGenerator.getInstance().emit(Instruction.ISUB);
} else if (production == CGrammarInitializer.Binary_Start_Binary_TO_Binary) {
String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " * " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);
root.setAttribute(ICodeKey.VALUE, val1 * val2);
root.setAttribute(ICodeKey.TEXT, text);
System.out.println(text + " is " + (val1 * val2) );
ProgramGenerator.getInstance().emit(Instruction.IMUL);
}
else {
root.setAttribute(ICodeKey.VALUE, val1 / val2);
System.out.println( root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " is divided by "
+ root.getChildren().get(1).getAttribute(ICodeKey.TEXT) + " and result is " + (val1/val2) );
ProgramGenerator.getInstance().emit(Instruction.IDIV);
}
break;
....
}
}
完成本節(jié)代碼后,我們的編譯器能將下面C代碼編譯成java字節(jié)碼:
int f(int a, int b, int c, int x) {
printf("value of a, b ,c ,x is: %d, %d, %d, %d", a, b, c, x);
int d;
d = (a*x*x) + (b*x);
int e;
int h;
e = 6;
h = 3;
printf("e divided by h is : %d", e/h);
return d;
}
void main() {
int c;
c = f(1, 2, 3, 4);
printf("result of calling f is :%d", c);
}
上面代碼編譯成java匯編代碼的結(jié)果為:
.class public CSourceToJava
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
sipush 1
sipush 2
sipush 3
sipush 4
invokestatic CSourceToJava/f(IIII)I
istore 0
iload 0
istore 1
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "result of calling f is :"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 1
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "
"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
return
.end method
.method public static f(IIII)I
iload 0
iload 1
iload 2
iload 3
istore 7
istore 8
istore 9
istore 10
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "value of a, b ,c ,x is: "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 10
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc ", "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 9
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc ", "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 8
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc ", "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 7
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "
"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
iload 0
iload 3
imul
iload 3
imul
iload 1
iload 3
imul
iadd
istore 4
sipush 6
istore 5
sipush 3
istore 6
iload 5
iload 6
idiv
istore 7
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "e divided by h is : "
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
iload 7
invokevirtual java/io/PrintStream/print(I)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "
"
invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V
iload 4
ireturn
.end method
.end class
把上面java匯編代碼在編譯成二進制字節(jié)碼運行后結(jié)果如下:
更詳細的講解和調(diào)試演示過程請參看視頻。
更多技術(shù)信息,包括操作系統(tǒng),編譯器,面試算法,機器學(xué)習(xí),人工智能,請關(guān)照我的公眾號: