热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

golang数组组合成最小的整数_[Golang实现JVM第四篇]整数加法和条件判断指令的实现...

在上一篇中我们实现了一个能跑的解释器,支持了一些基本的栈操作指令。现在我们就可以开始实现有点用的数学运算和条件判断了。局部变量表、程序计数器由于JVM字节码是基于

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

局部变量表、程序计数器

由于JVM字节码是基于栈的指令集,因此一切操作都是以栈为基础的,也就是说计算1+1,那需要先在栈中压入两个1然后进行计算,如果是对象方法调用,那么对象的引用、方法参数都会事先被压入栈中。除栈外还有一个跟执行相关的重要结构就是局部变量表(Local Variable Table),用来保存当前执行环境(如当前方法)下的局部变量,在JVM中用一个数组来表示,有专门的字节码指令用于向局部变量表的指定下标存取数据,例如storeX和loadX指令。值得注意的是这个数组大小是固定的,不需要动态扩容,因为在编译期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_是代表一组指令,格式为if_ byte1 byte2,也就是指令后面跟着两个字节,用来组成一个16位的有符号整数,此整数表示当条件(栈顶元素跟数字0做比较)成立时的要跳转到的目标字节码的offset, 注意这个offset是以当前if_指令的位置为基准的。例如,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举例&#xff0c;他表示将栈顶元素value出栈并且跟0作比较&#xff0c;当value <&#61; 0时条件成立。go代码如下&#xff1a;

case bcode.Ifle:

// 当栈顶int型数值小于等于0时跳转 err :&#61; i.bcodeIfCompZero(frame, codeAttr, func(op1 int, op2 int) bool {

return op1 <&#61; op2

})

if nil !&#61; err {

return fmt.Errorf("failed to execute &#39;ifle&#39;: %w", err)

}

bcodeIfCompZero()函数实现如下&#xff1a;

func (i *InterpretedExecutionEngine) bcodeIfCompZero(frame *MethodStackFrame, codeAttr *class.CodeAttr, gotoJudgeFunc func(int, int) bool) error {

// 当栈顶int型数值小于0时跳转 // 跳转的偏移量 twoByteNum :&#61; codeAttr.Code[frame.pc &#43; 1 : frame.pc &#43; 1 &#43; 2]

var offset int16

err :&#61; binary.Read(bytes.NewBuffer(twoByteNum), binary.BigEndian, &offset)

if nil !&#61; err {

return fmt.Errorf("failed to read offset for if_icmpgt: %w", err)

}

op, _ :&#61; frame.opStack.PopInt()

if gotoJudgeFunc(op, 0) {

frame.pc &#61; frame.pc &#43; int(offset) - 1

} else {

frame.pc &#43;&#61; 2

}

return nil

}

有些这些指令我们就可以解释一些类似于&#xff1a;

int sum &#61; 0;

if (sum > 0) {

sum &#61; 100;

}

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

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



推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
  • 纠正网上的错误:自定义一个类叫java.lang.System/String的方法
    本文纠正了网上关于自定义一个类叫java.lang.System/String的错误答案,并详细解释了为什么这种方法是错误的。作者指出,虽然双亲委托机制确实可以阻止自定义的System类被加载,但通过自定义一个特殊的类加载器,可以绕过双亲委托机制,达到自定义System类的目的。作者呼吁读者对网上的内容持怀疑态度,并带着问题来阅读文章。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 判断数组是否全为0_连续子数组的最大和的解题思路及代码方法一_动态规划
    本文介绍了判断数组是否全为0以及求解连续子数组的最大和的解题思路及代码方法一,即动态规划。通过动态规划的方法,可以找出连续子数组的最大和,具体思路是尽量选择正数的部分,遇到负数则不选择进去,遇到正数则保留并继续考察。本文给出了状态定义和状态转移方程,并提供了具体的代码实现。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • HDFS2.x新特性
    一、集群间数据拷贝scp实现两个远程主机之间的文件复制scp-rhello.txtroothadoop103:useratguiguhello.txt推pushscp-rr ... [详细]
  • WebSocket与Socket.io的理解
    WebSocketprotocol是HTML5一种新的协议。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送 ... [详细]
  • Redis底层数据结构之压缩列表的介绍及实现原理
    本文介绍了Redis底层数据结构之压缩列表的概念、实现原理以及使用场景。压缩列表是Redis为了节约内存而开发的一种顺序数据结构,由特殊编码的连续内存块组成。文章详细解释了压缩列表的构成和各个属性的含义,以及如何通过指针来计算表尾节点的地址。压缩列表适用于列表键和哈希键中只包含少量小整数值和短字符串的情况。通过使用压缩列表,可以有效减少内存占用,提升Redis的性能。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
author-avatar
神秘人-2012
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有