1. 程式人生 > >java開發C語言編譯器:jvm的return指令以及區域性變數的操作

java開發C語言編譯器:jvm的return指令以及區域性變數的操作

請結合視訊閱讀本文以便獲得更清晰的理解。
更詳細的講解和程式碼除錯演示過程,請參看視訊
用java開發C語言編譯器

如果你對機器學習感興趣,請參看一下連結:
機器學習:神經網路導論

jvm執行位元組碼時,程式碼的執行必須圍繞兩種資料結構,一種是堆疊,一種是佇列,如果jvm執行某條指令時,該指令需要對資料進行操作,那麼被操作的資料在指令執行前,必須要壓倒堆疊上。如果堆疊上的資料需要暫時保持起來時,它就會被載入到區域性變數佇列上。

java程式碼中,每個方法裡面的區域性變數包括函式的輸入引數都會儲存在佇列上。我們看下面一個方法:

int f() {
    int a;
    int
b; a = 1; b = 2; }

jvm在執行上面程式碼時,首先會分配一個堆疊和一個佇列,一開始堆疊和佇列都為空:

stack: null
list: null

要執行語句 a = 1; 時,首先需要把常量1壓到堆疊上:

stack: 1

區域性變數a對應於佇列的第0個元素,把1賦值給變數a,就相當於把堆疊頂部的數值1轉移到佇列的第0個元素,因此語句a =1;執行後佇列和堆疊的情況如下:
stack: null
list: 1,

執行第二條語句b = 2; 時同理,先把常量2壓到堆疊上:
stack: 2
list: 1

由於變數b是函式的第二個區域性變數,因此它對應佇列的第1個元素,把常量2賦值給變數b,就需要把堆疊頂部的數值2移到堆疊的第1個元素:

stack:
list: 1, 2

由此,當我們把C語言編譯成java位元組碼時,在解析函式時,函式中的區域性變數都需要對應到虛擬機器區域性變數佇列中的對應元素,在一會給出的例子中,我們會通過程式碼看看,在解析C語言函式的區域性變數時,程式是如何把變數和jvm的變數佇列對應起來的。

在此,我們還需要介紹的jvm的return指令, 無論是C程式碼還是java程式碼,一旦函式有返回值時,都需要通過關鍵字return把資料返回給函式的呼叫者,程式碼中的return語句在編譯成java位元組碼後,對應著多條語句。如果return 語句返回的資料型別是整形,那麼該return語句對應的位元組碼指令是ireturn, 前面的i表示整形,同理,如果return 返回的資料型別是浮點數,那麼對應的java位元組碼指令就是freturn, 前面的f表示float,如果return 返回的資料型別是double,對應的位元組碼指令就是dreturn, 前面的d表示double。

需要注意的是return 語句對應的位元組碼指令必須跟函式的返回值宣告相一致,如果函式宣告時返回的資料型別是整形,結果函式編譯後使用的return指令是freturn,也就是要返回一個浮點數,這種不一致性會被jvm檢測到,一旦發現指令的邏輯不一致,虛擬機器就會拒絕執行給定的程式碼。

介紹完理論後,我們看看如何把理論付諸實踐。下面的程式碼將是我們要編譯成java位元組碼的C語言程式碼:

int f() {
    int a;
    int b;
    a = 1;
    b = 2;

    return a+b;
}

void main() {
    int c; 
    c = f();
    printf("result of calling f is :%d", c);
}

函式f的返回值是int,因此在編譯成java位元組碼時,f 裡面的return語句編譯後要對應上jvm的ireturn指令,同時f中含有兩個區域性變數a,b,根據前面講述的理論,在解析到這兩個變數時,編譯器需要把他們對應到java虛擬機器中區域性變數佇列的相應元素。

我們看看,區域性變數是如何對應到虛擬機器佇列的相應元素的,在ProgramGenerator.java中,新增程式碼如下:

public class ProgramGenerator extends CodeGenerator {
    private static ProgramGenerator instance = null;
    private  String funcName = "";
    ....
    public int getLocalVariableIndex(Symbol symbol) {
        TypeSystem typeSys = TypeSystem.getTypeSystem();
        ArrayList<Symbol> list = typeSys.getSymbolsByScope(symbol.getScope());
        Collections.reverse(list);
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i) == symbol) {
                return i;
            }
        }

        return -1;
    }
    ....
}

每個變數都對應著一個符號物件Symbol, 同時每個變數都有給定的作用範圍,getSymbolsByScope把同一作用範圍內的變數全部取出來,形成一個佇列,例如在f中,有兩個具備變數a,b , 他們的作用範圍都是f, 如果把a對應的Symbol物件傳入上面函式後,list會得到一個佇列,該佇列包含兩個Symbol物件,這兩個物件就是變數a和b所對應的Symbol物件。得到這個佇列後,我們通過變數符號物件在佇列中的位置來對應他們在虛擬機器佇列中的位置。

第二處需要改動的程式碼在UnaryNodeExecutor.java中:

public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{
    private Symbol structObjSymbol = null;
    private Symbol monitorSymbol = null;

    @Override
    public Object Execute(ICodeNode root) {
        executeChildren(root);
        ....
        switch (production) {
        ....
        case CGrammarInitializer.Name_TO_Unary:
            symbol = (Symbol)root.getAttribute(ICodeKey.SYMBOL);
            if (symbol != null) {
                root.setAttribute(ICodeKey.VALUE, symbol.getValue());
                root.setAttribute(ICodeKey.TEXT, symbol.getName());

                ICodeNode func = CodeTreeBuilder.getCodeTreeBuilder().getFunctionNodeByName(symbol.getName());
                if (func == null && symbol.getValue() != null) {
                    ProgramGenerator generator = ProgramGenerator.getInstance();
                    int idx = generator.getLocalVariableIndex(symbol);
                    generator.emit(Instruction.ILOAD, "" + idx);    
                }

            }
            break;
            .....
            }
            ....
    }
    .....
}

在解析到某個變數是,我們先看該變數是否已經賦值了,也就是symbol.getValue()返回值不是null, 如果賦值了,那麼通過呼叫getLocalVariableIndex得到該變數在遍歷佇列裡的位置,這個位置將作為它對應在虛擬機器變數佇列裡的位置。假設該變數對應的位置是x, 那麼上面程式碼將輸出指令:
iload x
也就是把變數佇列中第x個元素載入到堆疊頂端。

第三處需要改動的程式碼在FunctDeclExecutor.java:

public class FunctDeclExecutor extends BaseExecutor {
    private ArrayList<Object> argsList = null;
    private ICodeNode currentNode;
    ProgramGenerator generator = ProgramGenerator.getInstance();
    ....
    private String emitArgs(Symbol funSymbol) {
        argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(true);
        String args = "(";
        for (int i = 0; i < argsList.size(); i++) {
            Symbol symbol = (Symbol)argsList.get(i);
            String arg = "";
            if (symbol.getDeclarator(Declarator.ARRAY) != null) {
                arg += "[";
            }

            if (symbol.hasType(Specifier.INT)) {
                arg += "I";
            }

            args += arg;
        }

        if (funSymbol.hasType(Specifier.INT)) {
            args += ")I";
        } else {
            args += ")V";
        }

        return args;
    }
    .....
}

這裡我們要把C語言中的函式宣告編譯成java位元組碼的函式宣告,原來我們一直預設函式返回值都是void型,現在我們函式可以返回整形了,一個函式本質上也是一個變數,因此函式f也對應著一個Symbol物件,我們通過判斷該Symbol物件的型別就可以得知函式的返回值,以例子程式碼為例: int f() 由於f前面有關鍵字int來修飾,因此f對應的Symbol物件它包含一個型別為int的specifier,一旦我們判斷到返回值是整形時,在把函式宣告編譯成位元組碼時,需要在函式引數列表後面加上一個I,用於表明返回值是整形,由此C程式碼中的函式宣告int f()編譯成java位元組碼後對應的程式碼為:
.method public static f()I。

第四處需要修改的程式碼還是在UnaryNodeExecutor.java中:

public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{
....
   public Object Execute(ICodeNode root) {
   ....
   case CGrammarInitializer.Unary_LP_RP_TO_Unary:
   case CGrammarInitializer.Unary_LP_ARGS_RP_TO_Unary:
   ....
   emitReturnInstruction(symbol);
   ....
   }

  private void emitReturnInstruction(Symbol symbol) {
        if (symbol.hasType(Specifier.INT)) {
            ProgramGenerator.getInstance().emit(Instruction.IRETURN);
        } else {
            ProgramGenerator.getInstance().emit(Instruction.RETURN);
        }
    }

前面我們提到過,函式宣告時,指明瞭返回值型別的話,那麼return必須根據返回值型別對應到位元組碼相應的xreturn語句,在這裡我們通過函式的符號物件,獲得函式的返回值型別,如果函式的返回值型別是整形,那麼編譯器就要輸出ireturn語句,如果返回值是void型別,那麼輸出return指令就可以了。

第五處需要修改的是Symbol.java:

public class Symbol implements IValueSetter{
....
public void setValue(Object obj) {
        if (obj != null) {
            System.out.println("Assign Value of " + obj.toString() + " to Variable " + name);   
        }

        this.value = obj;

        if (this.value != null) {
            ProgramGenerator generator = ProgramGenerator.getInstance();
            int idx = generator.getLocalVariableIndex(this);

            generator.emit(Instruction.ISTORE, "" + idx);   
        }

    }
    ....
}

在變數被賦值時,上面的程式碼會被呼叫,前面我們講過,如果給區域性變數賦值,區域性變數對應的是虛擬機器佇列中的某個元素,對其賦值,相當於把堆疊頂部的資料轉移到佇列的對應位置上,假設例子中變數b對應在佇列中的位置為1,上面程式碼執行後,編譯器會輸出指令:
istore 1

也就是把堆疊頂部的整形數值轉移給佇列中的第一個元素。

上面程式碼執行後,我們的編譯器會把給定的C語言程式編譯成如下java彙編程式碼:

.class public CSourceToJava
.super java/lang/Object

.method public static main([Ljava/lang/String;)V
    invokestatic    CSourceToJava/f()I
    istore  0
    iload   0
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    ldc "result of calling f is :"
    invokevirtual   java/io/PrintStream/print(Ljava/lang/String;)V
    istore  2
    getstatic   java/lang/System/out Ljava/io/PrintStream;
    iload   2
    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()I
    sipush  1
    istore  0
    sipush  2
    istore  1
    iload   0
    iload   1
    iadd
    ireturn
.end method

.end class

上面彙編程式碼編譯成位元組碼後執行,其結果如下:
這裡寫圖片描述

通過執行結果可見,我們編譯器對程式碼的編譯結果應該是正確的。由於本節將是內容有點抽象,請結合視訊演示一起來閱讀本文,以便獲得更清晰的理解。

更多技術資訊,包括作業系統,編譯器,面試演算法,機器學習,人工智慧,請關照我的公眾號:
這裡寫圖片描述

相關推薦

java開發C語言編譯器jvm的return指令以及區域性變數操作

請結合視訊閱讀本文以便獲得更清晰的理解。 更詳細的講解和程式碼除錯演示過程,請參看視訊 用java開發C語言編譯器 如果你對機器學習感興趣,請參看一下連結: 機器學習:神經網路導論 jvm執行位元組碼時,程式碼的執行必須圍繞兩種資料結構,一種

java實現C語言編譯器實現有引數的函式呼叫

更詳細的講解和程式碼除錯演示過程,請參看視訊 用java開發C語言編譯器 上一節,我們實現了沒有引數傳遞的函式呼叫,本節,我們看看如何實現有引數傳遞的函式呼叫。 有引數的函式呼叫要比無引數的函式呼叫複雜的多,一個難題在於,我們需要確定引數變數的作用域,例如

嵌入式C語言編譯器GCC

1 GCC相關介紹 GCC與gcc有什麼不同? GCC(GNU Compiler Collection) GNU編譯器集合,包含眾多語言的編譯器:C、C++、Java、D、Objective-C、etc gcc:特指GCC中的C語言編譯器 GCC

【軟體開發底層知識修煉】五 gcc-C語言編譯器

前面的四篇文章終於把處理器系列學完了(點選檢視上一篇文章:快取記憶體與TLB)。收貨很大!!! 接下來就該學習底層軟體部分知識。今天學習gcc的基本概念與簡單用法。 1、GCC與gcc GCC (GNU Compiler Collection) GN

學習較底層程式設計動手寫一個C語言編譯器

本文由 伯樂線上 - 菜鳥浮出水 翻譯。英文出處:Wilfred Hughes。 動手編寫一個編譯器,學習一下較為底層的程式設計方式,是一種學習計算機到底是如何工作的非常有效方法。 編譯器通常被看作是十分複雜的工程。事實上,編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧

手把手教你做一個 C 語言編譯器(8)表示式

這是整個編譯器的最後一部分,解析表示式。什麼是表示式?表示式是將各種語言要素的一個組合,用來求值。例如:函式呼叫、變數賦值、運算子運算等等。 表示式的解析難點有二:一是運算子的優先順序問題,二是如何將表示式編譯成目的碼。我們就來逐一說明。 本系列: 運算子的優先順

手把手教你做一個 C 語言編譯器(7)語句

整個編譯器還剩下最後兩個部分:語句和表示式的解析。它們的內容比較多,主要涉及如何將語句和表示式編譯成彙編程式碼。這章講解語句的解析,相對於表示式來說它還是較為容易的。 本系列: 語句 C 語言區分“語句”(statement)和“表示式”(expression)兩

手把手教你做一個 C 語言編譯器(9)總結

恭喜你完成了自己的 C 語言編譯器,本章中我們發一發牢騷,說一說編寫編譯器值得注意的一些問題;編寫編譯器時遇到的一些難題。 本系列: 虛擬機器與目的碼 整個系列的一開始,我們就著手虛擬機器的實現。不知道你是否有同感,這部分對於整個編譯器的編寫其實是十分重要的。我認

較底層程式設計自己動手寫一個C語言編譯器

  今天呢,我們就來自己動手編寫一個編譯器,學習一下較為底層的程式設計方式,是一種學習計算機到底是如何工作的非常有效方法。 編譯器通常被看作是十分複雜的工程。事實上,編寫一個產品級的編譯器也確實是一個龐大的任務。但是寫一個小巧可用的編譯器卻不是這麼困難。 祕訣就是首先去找到一個

手把手教你做一個 C 語言編譯器(2)虛擬機器

本章是“手把手教你構建 C 語言編譯器”系列的第三篇,本章我們要構建一臺虛擬的電腦,設計我們自己的指令集,執行我們的指令集,說得通俗一點就是自己實現一套匯編語言。它們將作為我們的編譯器最終輸出的目的碼。 本系列: 計算機的內部工作原理 我們關心計算機的三個基本部件

手把手教你做一個 C 語言編譯器(6)函式定義

由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變數定義、函式定義、表示式。本章講解函式定義相關的內容。 本系列: EBNF 表示 這是上一章的 EBNF 方法中與函式定義相關的內容。 C

手把手教你做一個 C 語言編譯器(4)遞迴下降

本章我們將講解遞迴下降的方法,並用它完成一個基本的四則運算的語法分析器。 本系列: 什麼是遞迴下降 傳統上,編寫語法分析器有兩種方法,一種是自頂向下,一種是自底自上。自頂向下是從起始非終結符開始,不斷地對非終結符進行分解,直到匹配輸入的終結符;自底向上是不斷地將終

手把手教你做一個 C 語言編譯器(3)詞法分析器

本章我們要講解如何構建詞法分析器。 本系列: 什麼是詞法分析器 簡而言之,詞法分析器用於對原始碼字串做預處理,以減少語法分析器的複雜程度。 詞法分析器以原始碼字串為輸入,輸出為標記流(token stream),即一連串的標記,每個標記通常包括: (token,

手把手教你做一個 C 語言編譯器(0)前言

“手把手教你構建 C 語言編譯器” 這一系列教程將帶你從頭編寫一個 C 語言的編譯器。希望通過這個系列,我們能對編譯器的構建有一定的瞭解,同時,我們也將構建出一個能用的 C 語言編譯器,儘管有許多語法並不支援。 在開始進入正題之前,本篇是一些閒聊,談談這個系列的初衷

手把手教你做一個 C 語言編譯器(1)設計

本章是“手把手教你構建 C 語言編譯器”系列的第二篇,我們要從整體上講解如何設計我們的 C 語言編譯器。 本系列: 首先要說明的是,雖然標題是編譯器,但實際上我們構建的是 C 語言的直譯器,這意味著我們可以像執行指令碼一樣去執行 C 語言的原始碼檔案。這麼做的理由

手把手教你做一個 C 語言編譯器(5)變數定義

本章中我們用 EBNF 來大致描述我們實現的 C 語言的文法,並實現其中解析變數定義部分。 由於語法分析本身比較複雜,所以我們將它拆分成 3 個部分進行講解,分別是:變數定義、函式定義、表示式。 本系列: EBNF 表示 EBNF 是對前一章提到的 BNF 的擴充

C語言基礎遞歸函數,全局(局)變量

否則 fib 語言 factorial 必須 不起作用 聲明 遞歸函數 tor #include <stdio.h>int factorial(int a); int Fibonacci(a);long Hanoi(a); void main(){ } 函

第5課 嵌入式C語言編譯器

進行 1.5 編譯器 編譯過程 結構 java 頭文件路徑 color bject 1. GCC與gcc (1)GCC:(GNU Compiler Collection)   GNU編譯器集合,包含眾多語言的編譯器,如C、C++、Java、D、Objective-C等 (2

Ubuntu下用glade和GTK+開發C語言界面程序(一)

命令行 簡單的 暑假 all 位置 相同 write 3.0 面向對象的思想 前言:對於大學中計算機系的每年暑假的課設有太多想說的,能從中學到非常多東西,當然不排除打醬油的,這些能夠掠過哦,凡事都打醬油。人生也是打醬油的吧。2333。 對於大三曾經的課設一般的要求

C語言考題Find the key in the picture,good luck..

int c語言 bsp pict fin find print str1 bin str1="Find the key in the picture,good luck.." for i in range(256): for j in range(39):