項目的完整代碼在 C2j-Compiler
前言
第十一篇,終於要進入代碼生成部分了,但是但是在此之前,因為我們要做的是C語言到字節碼的編譯,所以自然要了解一些字節碼,但是由於C語言比較簡單,所以只需要了解一些字節碼基礎
JVM的基本機制
JVM有一個執行環境叫做stack frame
這個環境有兩個基本數據結構
- 執行堆棧:指令的執行,都會圍繞這個堆棧來進行
- 局部變量數組,參數和局部變量就存儲在這個數組。
還有一個PC指針,它指向下一條要執行的指令。
舉一個例子
int f(int a, int b) {
return a+b;
}
f(1,2);
JVM的執行環境是這樣變化的
stack:
localarray:1,2
pc:把a從localarray取出放到stack
stack:1
localarray:2
pc:把b從localarray取出放到stack
stack:1,2
localarray:
pc:把a,b彈出堆棧並且相加壓入堆棧
對於JVM提供的對象
.class public CSourceToJava
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "Hello World!"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
return
.end method
.end class
getstatic、ldc和invokevirtual都相當於JVM提供的指令
getstatic和ldc相當於壓入堆棧操作。invokevirtual則是從堆棧彈出參數,然后調用方法
stack: out "Hello World!"
JVM的基本指令
pusu load store
JVM的運行基本都是圍繞着堆棧來進行,所以指令也都是和堆棧相關,比如進行一個乘法1 * 2:
bipush 1
bipush 2
imul
可以看到JVM的指令操作時帶數據的類型,b代表byte,也就是只能操作-128 ~ 128之間的數,而i代表是整形操作,所以相應也會有sipush等等了
下面加入要把1 * 2打印用prinft打印在控制台上,就需要把out對象壓入堆棧,此時的堆棧:
stack: 2 out
但是調用out的參數需要在堆棧頂部,所以這時候就需要兩個指令iload、istore
istore 0把2放到局部變量隊列,再把out壓入堆棧,再用iload 0把2放入堆棧中
stack: out 2
局部變量和函數參數
局部變量
在字節碼里,局部變量和函數參數都會存儲在隊列上
int func() {
int a;
int b;
a = 1;
b = 2;
return a + b;
}
看一下這個方法執行的時候堆棧的變化情況
// 執行a = 1,把1壓到stack上,再把1放入到隊列里
stack:
array:1
// 執行b = 1,也同理
stack:
array:1, 2
最后的return也有相應的return指令,所以完整的指令如下
sipush 1
istore 0
sipush 2
istore 1
iload 0
iload 1
iadd
ireturn
函數參數
int func(int a, int b, int c, int d){}
在調用這個函數的適合,函數參數就會按照順序被壓入堆棧中,然后拷貝到隊列上
stack: a b c d
array:
stack:
array: d c b a
所以在之后的代碼生成部分就需要一個來找到局部變量的位置的函數
數組
創建數組
下面這段指令的作用是創建一個大小為100的整形數組
sipush 100
newarray int
astore 0
- sipush 100 把元素個數壓入堆棧
- newarray int 創建一個數組,后面是數據類型
- astore 表示把數組對象移入隊列 a表示的是一個對象引用
讀取數組
下面這段指令是讀取數組的第66個元素
aload 0
sipush 66
iaload
- aload 0 把數組對象放到堆棧上
- sipush 放入要讀取的元素下標
- iaload 把讀取的值壓入堆棧
元素賦值
aload 0
sipush 7
sipush 10
iastore
- aload 0 把數組對象加載到堆棧
- sipush 7 把要賦值的值壓入堆棧
- sipush 10 把元素下標壓入堆棧
- iastore 進行賦值
結構體
C語言里的結構體其實就相當於沒有方法只有屬性的類,所以可以把結構體編譯成一個類
創建一個類
new MyClass //創建一個名字為MyClass的類
invokespecial ClassName/<init>() V //調用類的無參構造函數
例子
public class MyClass {
public int a;
public char c;
public MyClass () {
this.a = 0;
this.c = 0;
}
}
public class MyClass生成下面的代碼,都是對應生成一個類的特殊指令
.class public MyClass
.super java/lang/Object
下面的則是對應屬性的聲明
.field public c C
.field public a I
聲明完屬性,就是構造函數了,首先是先把類的實例加載到堆棧,再調用它的父類構造函數,對屬性的賦值:
- 加載類的實例到堆棧上 aload 0
- 壓入值 sipush 0
- 賦值的對應指令 putfield MyClass/c C
aload 0
invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield MyClass/c C
aload 0
sipush 0
putfield MyClass/a I
return
完整的對應的Java字節碼如下:
.class public MyClass
.super java/lang/Object
.field public c C
.field public a I
.method public <init>()V
aload 0
invokespecial java/lang/Object/<init>()V
aload 0
sipush 0
putfield MyClass/c C
aload 0
sipush 0
putfield MyClass/a I
return
.end method
.end class
讀取類的屬性
aload 3 ;假設類實例位於局部變量隊列第3個位置
putfield ClassName/x I
結構體數組
下面的指令創建了10個字符串類型的數組,這時候堆棧上的對象是一個引用,指向heap上一個10個字符串類型的數組
sipush 10
anewarray java/lang/String
下面的指令則是對數組的第一個元素進行賦值
astore 0
aload 0
sipush 0
ldc "hello world"
aastore
所以對於我們自己定義的類也是一樣的
sipush 10
anewarray MyClass
astore 0
下面則是對數組第一個下標生成一個MyClass對象
aload 0
sipush 1
new MyClass
invokespecial CTag/<init>()V
aastore
下面是對數組里的對象的屬性的取值和賦值操作,只是組合了之前的指令而已
aload 0
sipush 1
aaload
sipush 1
putfield MyClass/x I
aload 0
sipush 1
aaload
getfield MyClass/x I
分支語句
JVM指令還有兩個個非常重要的指令就是分支和循環指令,我們先來看分支指令
if (1 < 2) {
a = 1;
} else {
a = 2;
}
上面對應的JVM指令如下:
- 先把1和2壓入堆棧
- if_cmpge指令是大於等於,即如果1大於等於2就去執行else分支
- goto指令是跳轉到相應的標簽,也就是執行完if,就跳出else部分
sipush 1
sipush 2
if_cmpge branch0
sipush 1
astore 0
goto out_branch0
branch0:
sipush 2
istore 0
out_branch0:
sipush 3
istore 0
循環語句
基本的JVM指令只剩循環語句了,邏輯也不困難,基本的JVM指令相對於匯編算是非常簡單了
for (i = 0; i < 3; i++) {
a[i] = i;
}
上面生成的對應字節碼如下(假設現在變量i在隊列的第5個位置,a在隊列的第2個位置):
- 首先對i賦值
- 再把3壓入堆棧和i做比較,判斷i < 3
- 之后就是對數組的操作
- 然后修改i的值
- 返回loop0繼續判斷i < 3
sipush 0
istore 5
loop0:
iload 5
sipush 3
if_icmpge branch0
aload 2 ;加載數組
iload 3 ;加載標i
iload 3 ;加載變量i
iastore ;把i的值存入到a[i]
iload 3 ;加i
sipush 1 ;把1壓入堆棧
iadd ;i++
istore 3 ;把i+1后的值放入到i的隊列上的位置
goto loop0 ;跳轉到循環開頭
branch0:
小結
這一篇主要就是了解一下Java基本的字節碼,因為C語言的語法比較簡單,所以只需要知道一點就足夠生成代碼了。所以相對於匯編來說,是非常簡單的了。這樣下一篇就可以正式進入代碼生成部分
另外,歡迎Star這個項目!