1. 程式人生 > 程式設計 >從零寫一個編譯器(十一):程式碼生成之Java位元組碼基礎

從零寫一個編譯器(十一):程式碼生成之Java位元組碼基礎

專案的完整程式碼在 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
複製程式碼

宣告完屬性,就是建構函式了,首先是先把類的例項載入到堆疊,再呼叫它的父類建構函式,對屬性的賦值:

  1. 載入類的例項到堆疊上 aload 0
  2. 壓入值 sipush 0
  3. 賦值的對應指令 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!