[Golang实现JVM第四篇] 整数加法和条件判断指令的实现

在上一篇中我们实现了一个能跑的解释器,支持了一些基本的栈操作指令。现在我们就可以开始实现"有点用"的数学运算和条件判断了。

github: https://github.com/wanghongfei/mini-jvm

局部变量表、程序计数器

由于JVM字节码是基于栈的指令集,因此一切操作都是以栈为基础的,也就是说计算1+1,那需要先在栈中压入两个1然后进行计算,如果是对象方法调用,那么对象的引用、方法参数都会事先被压入栈中。除栈外还有一个跟执行相关的重要结构就是局部变量表(Local Variable Table),用来保存当前执行环境(如当前方法)下的局部变量,在JVM中用一个数组来表示,有专门的字节码指令用于向局部变量表的指定下标存取数据,例如storeXloadX指令。值得注意的是这个数组大小是固定的,不需要动态扩容,因为在编译期javac就能够确定一个方法需要用大的局部变量表,然后会把这个数字写入到class文件的code属性的max_locals字段中。我们在解释字节码的时候可以直接用它来创建数组。

程序计数器比较简单,就是一个整数类型,永远指向下一条将要执行的字节码,具体到实现就是指向下一条字节码数组的下标。

现在我们的方法栈就有三个元素了,操作数栈、局部变量表、程序计数器:

// 方法栈的栈帧
type MethodStackFrame struct {
    // 本地变量表
    localVariablesTable []interface{}

    // 操作数栈
    opStack *OpStack

    // 程序计数器
    pc int
}

因为本篇暂不涉及方法调用,因此栈帧创建的问题可以先忽略,假定全局只有一个MethodStackFrame

iload_n, istore_n, iconst_n指令

iload是一组指令,包含iload, iload0, iload1 ... ... iload3。开头的i表示操作数必须是整数,后边的数字表示需要将局部变量表的哪个槽位中的整数压到栈顶,即数组下标。而对于不带数字的iload指令,指令后面会紧跟着一个byte, 表示数组下标,例如要把下标为5的槽位中的整数压栈那么指令就会是iload 5两个字节。了解这些以后就很容易实现了:

        case bcode.Iload:
            // Load int from local variable
            // ilaod index
            index := codeAttr.Code[frame.pc + 1]
            frame.pc++  // (1)
            frame.opStack.Push(frame.localVariablesTable[index])
        case bcode.Iload0:
            // 将第1个slot中的值压栈
            frame.opStack.Push(frame.localVariablesTable[0])
        case bcode.Iload1:
            frame.opStack.Push(frame.localVariablesTable[1])
        case bcode.Iload2:
            frame.opStack.Push(frame.localVariablesTable[2])
        case bcode.Iload3:
            frame.opStack.Push(frame.localVariablesTable[3])

注意(1)的位置,因为不带数字的iload指令后面会跟着一个表示数组下标的字节,因此我们在取出这个下标后需要将程序计数器+1, 否则下次循环后就会取到数组下标而不是字节码了。此外在操作数组的时候其实并不需要检查下标是否越界,javac会保证生成的指令不会操作越界的下标。

istore指令也是类似的,表示将栈顶元素出栈,然后保存到局部变量表的指定槽位中,例如:

        case bcode.Istore1:
            // 将栈顶int型数值存入第二个本地变量
            top, _ := frame.opStack.PopInt()
            frame.localVariablesTable[1] = top
        case bcode.Istore2:
            // 将栈顶int型数值存入第3个本地变量
            top, _ := frame.opStack.PopInt()
            frame.localVariablesTable[2] = top

iconst指令有点不太一样,虽然也是将整数压栈,但是他不跟局部变量表交互,压栈的值直接在指令中体现,例如iconst_1就是把1压栈,iconst_2就是压入2,实现起来也非常简单:

case bcode.Iconst1:
            frame.opStack.Push(1)

iadd指令

iadd表示连续做两次出栈操作,然后将得到的两个整数相加,最后再把结果压回栈中。实现起来也非常简单:

case bcode.Iadd:
            // 取出栈顶2元素,相加,入栈
            op1, _ := frame.opStack.PopInt()
            op2, _ := frame.opStack.PopInt()
            sum := op1 + op2
            frame.opStack.Push(sum)

还是那句话,不需要在pop()前检查栈是否为空,因为编译器会保证不会非法操作栈,除非是我们的go代码出了问题,如果是后者的话就直接让程序崩溃方便及时发现问题。

有了iload, istore, iadd后,我们终于能计算1+1了,然而尴尬的是,计算后的结果是保存在局部变量表里的,看不见摸不着,不过可以在debug调试过程中看到这个值。下一篇会介绍如何实现方法调用,到时候就可以实现控制台输出的功能了。

ifeq, iflt, ifeq等判断指令

if_<cond>是代表一组指令,格式为if_<cond> byte1 byte2,也就是指令后面跟着两个字节,用来组成一个16位的有符号整数,此整数表示当条件(栈顶元素跟数字0做比较)成立时的要跳转到的目标字节码的offset, 注意这个offset是以当前if_<cond>指令的位置为基准的。例如,if_lt所在的offset是10,byte1 byte2组合后是5, 那么目标字节码的位置就是 10 + 5 = 15。这里如果用javap反编译的话输出结果会有一点误导人,javap会输出这条指令计算好偏移量后的数值而不是byte1 byte2本身的值,例如:

3: ifle          9

右侧的9表示 3 + 6 = 9,即ifle后面跟着的16位数字其实是6,而不是9。

我们拿ifle举例,他表示将栈顶元素value出栈并且跟0作比较,当value <= 0时条件成立。go代码如下:

case bcode.Ifle:
            // 当栈顶int型数值小于等于0时跳转
            err := i.bcodeIfCompZero(frame, codeAttr, func(op1 int, op2 int) bool {
                return op1 <= op2
            })

            if nil != err {
                return fmt.Errorf("failed to execute 'ifle': %w", err)
            }

bcodeIfCompZero()函数实现如下:

func (i *InterpretedExecutionEngine) bcodeIfCompZero(frame *MethodStackFrame, codeAttr *class.CodeAttr, gotoJudgeFunc func(int, int) bool) error {
    // 当栈顶int型数值小于0时跳转
    // 跳转的偏移量
    twoByteNum := codeAttr.Code[frame.pc + 1 : frame.pc + 1 + 2]
    var offset int16
    err := binary.Read(bytes.NewBuffer(twoByteNum), binary.BigEndian, &offset)
    if nil != err {
        return fmt.Errorf("failed to read offset for if_icmpgt: %w", err)
    }

    op, _ := frame.opStack.PopInt()
    if gotoJudgeFunc(op, 0) {
        frame.pc = frame.pc + int(offset) - 1

    } else {
        frame.pc += 2
    }

    return nil
}

有些这些指令我们就可以解释一些类似于:

int sum = 0;
if (sum > 0) {
  sum = 100;
}

编译后的字节码了。我们可以照葫芦画瓢,先写一段java代码编译一下,然后javap -verbose看看有没有不认识的指令,如果有就查规范,看看应该如何解释,就能够实现很多简单指令了。这里要注意有很多指令后面会携带一个_w后缀,例如整数自增指令iinc_w,表示对字节码后面跟着的字节进行加宽处理,例如原本的iinc byte1 byte2变成了iinc_w byte1 byte2 byte3,诸如此类,只要对照规范看清楚就OK了。

下一篇会介绍方法调用相关指令的实现。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342