05.從0實現一個JVM語言之目標平臺代碼生成-CodeGenerator

從0實現JVM語言之目標平臺代碼生成-CodeGenerator

源碼github倉庫, 若是這個系列文章對你有幫助, 但願得到你的一個star

本節相關代碼生成package地址

系列導讀 00.一個JVM語言的誕生

階段性的告別

很是感謝您能看到這裏, 您的陪伴是我這幾天堅持寫做整合的動力! 可是全部的故事都有畫上句號
的那一天, 咱們這個系列也不例外, 今天就是一個給你們的階段性停更, 由於咱們代碼優化部分以及一些
還沒實現可是應該實現的部分(數組, printf等等)還會繼續實現, 還有代碼中有的編譯優化可能也要隔
一段時間才能獻上了, 主要緣由是目前因爲我的要準備春招實習了, 要暫時跟你們說一聲告別, 今天這篇
結束之後可能會停更一段時間, 春招完後會繼續更新這個系列以及其餘系列
   
    以後這個系列還會頻率更低地更新, 將來我打算出一個手寫TCP/IP協議簇系列, 再出一個手寫簡單的
操做系統系列, 手寫JVM系列, 若是時間足夠, 應該還會出手寫Tomcat系列, 哈哈這些太多了, 可是之前
沒有深刻了解很遺憾, 在這以前應有一個Android系列, Android應該就是寫個博客園/github客戶端, 安
卓真的是我一直想求而不得, 學校選課移動開發(Android), 我連選三個學期, 前兩個學期直接課程衝了, 
第三個學期我能選的那個時間課程只有5我的選, 被撤銷了, 我已經哭了, 學分已經修滿了,之後大機率是
不會再選這課的, 由於時間該實習了 , 後面Android系列打算用Kotlin實現, 一些很前衛的巨頭就是Kotlin 
first(聽說字節就是), Kotlin的函數式, 協程以及空安全是更先進更舒服的, 固然我的也簡單瞭解過一點
flutter, 因此到時候傾向於用Kotlin或者Flutter去完成這個項目

    更新的系列可能與複習同步, 當作複習練手, 但也可能暫時不會更新了, 要看我我的的複習進度, 我以爲
大機率是鴿, 不過你們放心, 過了春招這段時間我會繼續堅持分享我的一些有趣的, 比較精心打磨, 完成度較好
的項目的

    很是感謝一些老讀者的陪伴(雖然應該只有個位數的幾位朋友), 是大家的堅持閱讀才讓我堅持寫完了這個系列,
 也督促我不能天天都起碼更個新, 不水你們

    也很是感謝您能忍受我一直以來本身都以爲醜的文筆和敘述, 哈哈, 您有什麼寶貴意見均可以留言,
我會盡我所能改正或者提供支持
    
    因此項目就將時中止更新了, 讓咱們有緣再會!
致親愛的讀者:

    我的的文字組織和寫文章的功底屬實通常, 寫的也比較趕時間, 因此係列文章的文字可能比較粗糙,
不免有詞不達意或者寫的很迷惑抽象的地方 

    若是您看了有疑問或者以爲我寫的實在亂七八糟, 這個很抱歉, 確實是個人問題, 您若是有不懂的地方
的地方或者發現個人錯誤(文字錯誤, 邏輯錯誤或者知識點錯誤都有可能), 能夠直接留言, 我看到都會回覆您!

系列食用方法建議

因爲時間緣由, 目前測試並不完善, 因此推薦以下方式根據您的目的進行閱讀

    若是您是學習用, 建議您先將整個項目clone到本地, 而後把感興趣的章節刪除, 本身重寫對照着重寫
    書寫完每一步測試一下可否正常運行(在指定的路徑去讀取源碼測試可否編譯成功並在命令行執行

    java Application(類名)

嘗試可否輸出指望結果, 我沒有研究Junit對編譯器輸出class文件進行測試, 因此目前可能須要您手動測試)

    按照以上步驟, 等您將全部模塊重寫一遍, 大概也對這個系列的脈絡有深入理解了! 若是您重頭開始重寫, 
每每可能因爲出現某些低級錯誤致使長時間debug才找獲得錯誤, 因此對於初學者, 推薦採用本身補寫替換模塊的
方式

    對於但願貢獻代碼的朋友或者對Cva感興趣的朋友, 歡迎貢獻您的源碼與看法, 或者對於該系列一些錯誤/
bug願意提出指正的朋友, 您能夠留言或者在github發issue, 我看到後必定及時處理!

本節提綱

  1. 引言-編譯器前端的尾聲html

  2. BST(Backend Abstract Syntax Tree)前端

  3. Translatorjava

  4. JVM指令git

    4.1. 咱們使用的指令介紹github

  5. Jasmin 彙編器生成 .class File算法

引言-編譯器前端的尾聲

在該步以前還應有編譯期優化, 可是因爲時間緣由沒有完善, debug時也一直沒有開優化, 因此這部分之後有緣再
補充shell

代碼生成是編譯器前端的最後一步了, Javac編譯器的編譯工做到這一步, 即是將Java源碼編譯成JVM .class
字節碼文件, 而咱們這個階段也是如此, 其實當咱們拿到抽象語法樹, 能夠玩一點花的, 能夠將Cva字節碼編譯到
Java, Csharp, C語言甚至js等等express

這是由於咱們拿到的抽象語法樹已是一棵可執行的樹了, 咱們獲得的源碼文本轉換獲得的Program POJO是一個
有靈魂的POJO, 他能作的事就是你寫代碼時所表達的那些事, 固然, 咱們也能夠爲其開發一個解釋器, 執行這課
語法樹(當這個解釋器的構造與計算機相似, 拓展了許多與操做系統相似的功能如垃圾回收/JIT時, 其也能夠叫作
虛擬機), 固然, 這裏咱們並不教你們如何去作這些工做, 若是你有興趣能夠嘗試作一下, 咱們這裏就作一些你們
喜聞樂見的, 常規的編譯器作法, 生成可執行的指令編程

若是是直接面向機器的編譯器, 那麼這個時候每每會生成指定平臺的彙編(8086彙編, x86彙編)再進行接下來一步
的生成, 咱們這裏並非直接生成JVM字節碼(JVM平臺 .class文件), 而是先生成JVM平臺中間語言(Intermediate
Language)JVM彙編指令, 再由彙編器(由中間語言/彙編語言生成機器碼, 這個過程基本就是映射關係, 因此彙編指
令也叫助記符), 咱們這裏其實也是面向機器, 不過這臺機器是一個特殊的JVM虛擬機, JVM虛擬機其實也有着本身的
一套匯編指令集及規範後端

固然, 在編譯器後端, 有着更爲豐富精彩的世界, 好比運行時的編譯, JIT, AOT等等, 還有着無底洞般的優化, 但願
之後有機會能給你們展示這些東西

同時, 這一節因爲時間緣由, 和屢次重構的歷史緣由, 代碼結構相對有一點亂, 我的將梳理講解值得一些注意的部分

BST(Backend Abstract Syntax Tree)

cn.misection.cva.ast 是表示咱們前端Cva源程序的抽象語法樹, 可是這個東西表示的畢竟是咱們本身定義的
Cva程序, 而不是咱們的目標平臺JVM能夠識別的程序, 或者說, 跟咱們目標有差別, 咱們不可能直接將這棵語法樹
用來在JVM平臺執行或者編譯到JVM上(固然咱們能夠針對這種程序結構開發解釋器, 讓其在咱們的解釋器/虛擬機上運行)

後端抽象語法樹, 說是後端語法樹, 其實過程仍是屬於前端, 只不過這裏更接近虛擬機後端, 並且咱們的
語法樹由前端的樹形被咱們翻譯成了後端的線性指令形式, 語法結構劇變, 因此咱們須要一個 新的BST做爲從AST
到指令的中間表示. BST主要關注點在Statement和Expression方面

Translator

說道 Translator, 顧名思義, 你們應該也能理解它的做用, 這是一個翻譯官, 能將咱們前端的ast翻譯成後端的bst,
將樹狀的Cva程序翻譯成線性的JVM指令, 他接受一個前端傳入的CvaProgram, 生成一個後端的TargetProgram, 然
後交由IntermLangGenerator寫入.il中間語言文件, 最後再交由Cvac 編譯器的main方法靜態調用jasmin的主方法
生成咱們的.class字節碼, 咱們的編譯器工做就完成啦!

其實現是impl後端的visitor, 這裏使用到了(僞)訪問者模式, 是一個設計的不成功的訪問者模式, 你們看看就好,
我之後有更好的方案會重構他

JVM指令

因爲JVM是基於棧式計算機概念的虛擬機, 以JVM爲目標平臺的代碼生成較爲簡單, 沒必要考慮寄存器分配等問題; 同時JVM
還有着豐富的指令

咱們的語言因爲支持的類型頗有限, 支持的基本類型只有intboolean(其實這倆都是int), 其餘類型暫時沒有過多
處理, 以及涉及到基本類型之間互相轉換的操做指令能夠不用考慮,咱們支持的比較也比較少, 所以跳轉指令能夠只考慮部分,
因爲不存在一些特殊類型或者操做符, 好比interface abstract instanceof等等複雜的指令

此外, JVM規範中有:

Java Virtual Machine Specification - 2.3.4 The boolean type

Although the Java Virtual Machine defines a boolean type, it only provides
very limited support for it. There are no Java Virtual Machine instructions solely
dedicated to operations on boolean values. Instead, expressions in the Java
programming language that operate on boolean values are compiled to use values
of the Java Virtual Machine int data type.

其實後端是沒有boolean的, 直接用int 型 0 1 表示便可, 這也是爲啥C語言只有0和非0的緣由了,
感受咱們彷佛愈來愈接近世界的真相了, 哈哈

咱們使用的指令介紹

按照JVM規範, JVM支持共計大約150個指令, 咱們用到的指令僅僅是至關小的一個子集, 先給出咱們使用的指令

aload
areturn
astore
getfield
goto <label>

iinc

iadd
isub
imul
idiv
iand
ior
ixor
irem
ishl
ishr
iushr

if_icmplt <label>
iload
invokespecial
invokevirtual
ireturn
istore
ldc
new
putfield

// 將來須要支持的
anewarray(數組操做)

簡單地解釋一下這些指令

咱們能夠把一條指令分紅兩部分看, 好比 iadd, 其實在JVM指令的語義中, 這條指令反映了兩個信息 i 和add,
i 其實就是int的意思, add 是指將棧頂的兩個操做數相加, 因此這條指令的意思就是將棧頂的兩個 int 型操做
數相加, 理解了這條指令, 其餘指令也就不難了, 他們都只是這樣一個個簡單的容易理解的映射關係

其餘的數據類型如byte, 在虛擬機指令層面就是b(做爲操做數操做層面), 都在下面的EnumOperandType中
固然, 這些指令是怎麼獲得的呢, 能夠選擇看書, 若是沒有書籍或者課程資源怎麼辦呢, 除了去網上搗鼓電子書
以外, 咱們也能夠利用現成的JDK提供給咱們的工具javap

授人以魚不如授人以漁, 咱們這裏提供一個理解底層操做的思路

好比說, 你想知道long型的左移操做底層是使用哪條指令
能夠寫一個LongLS.java

/**
* LongLS.java
*/
class LongLS
{
   public static void main(String[] args)
   {
       // 也能夠放在方法中;
       // 一次多放幾條, 能事半功倍查看原理
       long aLong = 1;
       long anotherLong = aLong << 1;
   }
}

而後在命令行執行

javac LongLS.java

而後

javap -c LongLS.class

能夠獲得

Compiled from "LongLS.java"
class LongLS {
  LongLS();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: lconst_1
       1: lstore_1
       2: lload_1
       3: iconst_1
       4: lshl
       5: lstore_3
       6: return
}

若是但願獲得更詳細的信息, 可使用-v參數

javap -v LongLS.class

獲得

Classfile /***/batTest/javap/demo/LongLS.class
  Last modified 2021-3-4; size 271 bytes
  MD5 checksum cc920428a4ba6c860e039dfd79252c53
  Compiled from "LongLS.java"
class LongLS
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // LongLS
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               SourceFile
  #11 = Utf8               LongLS.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               LongLS
  #14 = Utf8               java/lang/Object
{
  LongLS();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=5, args_size=1
         0: lconst_1
         1: lstore_1
         2: lload_1
         3: iconst_1
         4: lshl
         5: lstore_3
         6: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 6
}
SourceFile: "LongLS.java"

這樣咱們就獲得了long型左移操做指令應該是 lshl, long型進行操做時的助記符應該爲 l

同理, 咱們但願得到基本類型在方法簽名中的表示時, 或者但願深刻研究查看某個方法的具體細節,
看某些操做是不是原子性的時候, 看有些操做是否線程安全的時候, 也可使用這個辦法去反彙編
class文件

下面即是使用橋接模式橋接操做數和操做符的兩個枚舉(固然, 咱們這個橋接模式並無用實體類將其組合起來(由於
這些命令是不會有變數的的, 建立對象會浪費資源), 直接在translator中壓入命令隊列(用的是list))

package cn.misection.cvac.codegen.bst.instructor.operand;

import cn.misection.cvac.codegen.bst.instructor.IInstructor;
import cn.misection.cvac.codegen.bst.instructor.Instructable;

/**
 * @author Military Intelligence 6 root
 * @version 1.0.0
 * @ClassName EnumTargetOperand
 * @Description 橋接模式底層操做數;
 * @CreateTime 2021年02月21日 22:23:00
 */
public enum EnumOperandType implements IInstructor, Instructable
{
    /**
     * 底層操做數類型;
     */
    VOID(""),

    BYTE("b"),

    SHORT("s"),

    CHAR("c"),

    INT("i"),

    LONG("l"),

    FLOAT("f"),

    DOUBLE("d"),

    REFERENCE("a"),
    ;

    private final String typeInst;

    EnumOperandType(String typeInst)
    {
        this.typeInst = typeInst;
    }


    @Override
    public String toInst()
    {
        return typeInst;
    }
}
package cn.misection.cvac.codegen.bst.instructor.operand;

import cn.misection.cvac.codegen.bst.instructor.IInstructor;
import cn.misection.cvac.codegen.bst.instructor.Instructable;

/**
 * @author Military Intelligence 6 root
 * @version 1.0.0
 * @ClassName EnumOperator
 * @Description 橋接模式底層操做符;
 * @CreateTime 2021年02月21日 22:24:00
 */
public enum EnumOperator implements IInstructor, Instructable
{
    /**
     * 底層操做符;
     */

    ADD("add"),

    SUB("sub"),

    MUL("mul"),

    DIV("div"),

    /**
     * 求餘;
     */
    /*
     * neg 其實不該該出現, 其是一元的;
     */
    BIT_NEG("neg"),

    REM("rem"),

    BIT_AND("and"),

    BIT_OR("or"),

    BIT_XOR("xor"),

    LEFT_SHIFT("shl"),

    RIGHT_SHIFT("shr"),

    /**
     * 無符號右移;
     */
    UNSIGNED_RIGHT_SHIFT("ushr"),

    RETURN("return"),
    ;
    
    private final String opInst;

    EnumOperator(String opInst)
    {
        this.opInst = opInst;
    }

    @Override
    public String toInst()
    {
        return opInst;
    }
}

他們都實現接口 Instructable, 表示能將該元素轉換成JVM彙編指令

@FunctionalInterface
public interface Instructable
{
    /**
     * 得到該類型指令;
     * @return instruct;
     */
    String toInst();
}

同時他們實現 IInstructor 接口
這個接口是爲了能在假的訪問者模式(這個訪問者模式一個失敗的訪問者模式)有一個統一的傳入父接口

咱們使用到的指令是至關精簡的. 可是, 如下兩條擴展"指令"值得咱們注意.

label

這條"指令"的存在, 是交由下文提到的Jasmin處理

// 不是寫入il文件的write, 而是標準輸出流打印指令;
write

這條"指令"是咱們純粹爲了編程簡單而擴展的. 考慮到咱們的語言的特色, 將他們做爲指令對待, 可以大大簡化咱們編程處理的複雜度, 並且也不會形成任何的反作用, 由於以前幾個階段的分析已經保證了源程序的合法. 這樣用不會有問題, 由於後面咱們會將擴展的指令翻譯成jvm指令.

Code Translation

有了上文的"完善"的指令集(對於咱們這個程序來講), 咱們接下來的工做即是將樹狀的AST轉換成線性的指令. 這裏的轉換主要關注的方法中的真正"幹活"的代碼, 就是方法體內部的工做代碼. 下面經過一個例子展現具體工做.

class Recursor
{
    int compute(int num)
    {
        int total;
        if ( num < 1)
        {
            total = 1;
        }
        else
        {
            total = num * (this.compute(num - 1));
        }
        return total;
    }
}

/**
 * This is the entry point of the program
 */
int main(string[] args)
{
    // fib(10);
    println new Recursor().compute(10);   
    return 0;
}

觀察以上一段代碼, 咱們主要關注compute方法編譯出的指令, 並且給出了較詳細的註釋.

.method public Compute(I)I ; 方法簽名傳入參int, 返回int;
.limit stack 4096 ; 棧調用深度, 因爲目前尚未實現該算法, 所以編譯結果給出默認值 4096
.limit locals 4 ; 本地變量的編號一共4個, 後面的iload 1 每每是 iload var1而不是常量1
    ; num < 1 對於if語句中的判別式進行計算
    iload 1     ; 從本地變量表中加載變量1的值(num)到棧頂
    ldc 1       ; ldc 即 load const將整型數字1壓入棧
                ; 事實上, 咱們須要完成的一個TODO就是優化這些常數的加載
                ; JVM對於常量的加載
                ; 取值 -1~5 採用 iconst 指令, -1是iconst_m1, 其餘則如iconst_1;
                ; 取值 -128~127 採用 bipush 指令(byte取值區間, 下同
                ; byte num >= Math.pow(2, 8) && num < Math.pow(2, 8));
                ; 取值 -32768~32767 採用 sipush指令;
                ; 取值 -2147483648~2147483647 採用 ldc 指令;

    if_icmplt Label_2 ;比較兩個值, 若是第一個值(num)小於整數1, 跳轉至Label_2
    ldc 0       ; 將整數0壓入棧(用於表示比較結果爲false)
    goto Label_3 ; 不然跳轉到label_3
Label_2:
    ldc 1       ; 將整數1壓入棧(用於表示比較結果爲真)
Label_3:        ; 判別式計算完成
    ldc 1
    if_icmplt Label_0 ; 對於求值真假進行計算
    ldc 1       ; 將整數1壓入棧
    istore 2    ; 將棧上的數字存入本地變量2
    goto Label_1
Label_0:
    iload 1     ; 從本地變量表中加載變量1的值 (num)
    aload 0     ; 從本地變量表中加載變量0的值 (this)
    iload 1
    ldc 1
    isub
    invokevirtual FibCalcer/Compute(I)I ; 調用實例方法(在指令參數處指出了方法的從屬及簽名)
    imul        ; 棧頂兩個int型相乘, 並將結果壓入棧頂
    istore 2
Label_1:
    iload 2     ; 從本地變量表中加贊變量2的值
    ireturn     ; 從方法返回. 
.end method

; main 方法
.class public Application
.super java/lang/Object
.method public static main([Ljava/lang/String;)V ; main方法返回V, void, Cva中的int只是致敬C語言;
.limit stack 4096
.limit locals 2
    ldc "fib(10) is " ; 加載字符串常量;
    getstatic java/lang/System/out Ljava/io/PrintStream; ; 打印的System.out 放到棧頂
    swap ; 因爲打印必需要被打印者在棧頂, PrintStream在其下, 因此交換, 之後考慮優化;
    invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V ; 調用虛方法
    ; 對於 private 方法和構造方法<init> 都是invokespecial調用
    ; invokevirtual 調用實例方法, 包括父類方法, 抽象類的抽象方法實現;
    ; invokeinterfacre 調用接口的抽象方法
    ; invokestatic 調用類方法
    ; invokedynamic Java7以後纔有的動態方法調用指令
    new Recursor ; 建立對象;
    dup ; dup指令爲複製操做數棧頂值,並將其壓入棧頂,也就是說此時操做數棧上有連續相同的兩個對象地址;
        ; 這是由於一會有出棧操做, 保留一個副本;
    invokespecial Recursor/<init>()V
    ldc 10
    invokevirtual Recursor/compute(I)I
    getstatic java/lang/System/out Ljava/io/PrintStream;
    swap
    invokevirtual java/io/PrintStream/println(I)V
    return ; 至關於void return, 這個指令只能用於void
           ; 若是要返回 int, 像上面ireturn
           ; 若是是引用類型 則是areturn;
.end method

Jasmin 彙編器生成 .class File

最後的工做就是從字符形式的指令, 由彙編器轉換成二進制形式的.class文件, 用於jvm的運行,
這個彙編器是一個現成的工具, 這個工具其實也是一個命令行應用, 若是使用腳本去粘合咱們將面臨
性能問題和很多通訊上的問題, 咱們的方法是在cvac的main方法中靜態調用這個彙編器的main方法
直接傳入路徑做爲其命令行參數
Jasmin.

相關文章
相關標籤/搜索