编译器前端基本设计已经完成(详见我的自制编译器系列博文),但在考虑编译器后端之前,首先要考虑代码的运行环境,因此考虑再三我便把制作Runtime的过程写成博文,记录从中所学到的知识。
Runtime的设计以JVM为原型,加以部分的简化来去掉复杂的功能,并使得简单的Runtime可以在不多的代码行数内加以实现。
总的来说我们的Runtime需要实现以下几个功能:
1、运行特定的程序指令
2、以控制台的方式进行输出
3、可检查出运行时错误(如数组越界,空指针错误等)
4、有垃圾收集机制
一、Runtime结构设计
(1)帧栈
因为运行时暂且不考虑多线程并发或伪并发,所以整个Runtime具有一个帧栈,帧栈中存放栈帧。帧栈用于函数调用,每个调用函数都有着自己独立的栈帧,并在调用时将此帧压栈,返回时将栈帧弹出。
一个栈帧具有以下结构:
1、方法信息
记录当前调用的方法的签名
2、IP
指令寄存器,记录下一条将要执行的指令的位置,根据此之前的方法信息和IP从静态方法区获取指令。值得注意的是,每个方法具有自己独享的IP,这样可以简化方法退出时的恢复现场的工作。
3、局部变量表
局部变量表存放局部变量的信息,非static调用时this指针压在变量表的第一个位置。如果局部变量是一个基本数据类型(char,int ,double),局部变量表存放局部变量的值,否则存放一个对象的句柄。同时函数调用时参数均是通过局部变量表进行传递的。
4、操作数栈
虚拟执行环境设计成基于栈的执行环境,因此每个方法都有自己的操作数栈,方法调用时给其栈分配空间,返回时释放栈的空间。值得注意的是,每个方法都有专属于自己的栈所带来的好处是简化了方法进入和退出时的清理堆栈的工作,但需要额外的机制来进行返回值的传递。在设计中我们首先通过帧栈找到其调用者的栈帧,然后将返回值压入其调用者的操作数栈中。
(2)堆
所有对象的数据部分存储在堆上,一个堆可以理解为一段连续的地址空间,并且当堆的大小不够时可以进行动态的扩展。垃圾收集工作只在堆上进行。
(3)对象表
一个对象表由若干表项构成,一个表项为一个三元组,包括对象句柄、堆中位置及额外信息。使用对象表来存储程序运行过程中所有的对象的信息。
所谓对象句柄为一个32为整数,是某一个对象在Runtime的唯一标识,指令中所有对对象的操作均通过对象句柄来完成。
堆中位置描述一个对象在堆中的存储位置,并通过方法区类的相应接口来解析出该对象中的数据。此位置不固定,每次GC之后堆中位置或许会发生改变。
信息包含该对象的基本信息,包括但不仅限于一个指向该对象从属类的指针、是否是静态变量标识、数组长度、访问权限等。
(4)静态方法区
Runtime加载类之后将类的信息存储于静态方法区,这其中包括类的字段、类的方法签名、类的方法的指令序列。
堆中的内容将通过静态方法去类字段的内存布局来解析,同时执行引擎通过查找特定类的方法指令序列来执行指令。
二、类文件格式设计
为了使解析、阅读方便,在类文件格式设计中,信息以“行”的形式进行存放,每行之间通过\r\n进行分割。
类文件开头第一行:类名
第二行:字段数n
接下来的n行为字段
字段格式如下:
开头是一个0--4的整数,其中第一位代表是否是public,第二位代表是否是static;其后是字段类型,最后是字段名。三个信息以空格进行分割。
接下来一行是方法数m
接下来有m个方法体。
每个方法体开头为0--4的整数,其中第一位代表是否是public,第二位代表是否是static。接着是返回值类型,之后是方法名,之后是参数数量p,之后是p个参数类型。各信息之间以空格进行分割。
之后一行为{ ,代表方法的指令码开始。
之后跟若干行指令,指令以如下格式存放:
地址:指令码 操作数1,操作数2
其中地址为从1开始递增的整数,指令码为0--256的整数,详细参加之后定义,操作数可为立即数、字符串或没有操作数。
之后跟若干行为长量表,格式为#i const,目前主要用来存储常量字符串。
例如: #0 hello,world
#1 Runtime
之类的
三、指令码设计
基本参考Java字节码,并在此基础上加以简化。
字节码 助记符和指令格式 含义
0x00 nop 什么都不做
0x01 iconst X 整形常量X压入堆栈
0x02 dconst X 浮点常量压入堆栈
0x03 load X 将本地变量表的第X位置的值压入堆栈
0x04 aload 将栈顶元素所指向的数组的第X个元素压入堆栈,X是栈自顶向下第二个元素
0x05 store X 将栈顶元素存入本地变量表的X位置
0x06 astore Y 将栈顶元素指向的数组的第X个元素存入本地变量表的Y位置,X是栈自顶向下第二个元素
0x07 iadd 将栈顶两元素作为int相加并将结果压入栈
0x08 isub 将栈顶两元素作为int相减并将结果压入堆栈
0x09 imul 将栈顶两元素作为int相乘并将结果压入堆栈
0x0A idiv 将栈顶两元素作为int相除并将结果压入堆栈
0x0B dadd 将栈顶两元素作为double相加并将结果压入栈
0x0C dsub 将栈顶两元素作为double相减并将结果压入堆栈
0x0D dmul 将栈顶两元素作为double相乘并将结果压入堆栈
0x0E ddiv 将栈顶两元素作为double相除并将结果压入堆栈
0x0F ishl 将栈顶元素作为int左移X位并将结果压入堆栈,X是第二个元素0x10 ishr 将栈顶元素作为int右移X位并将结果压入堆栈,X是第二个元素
0x11 iand 栈顶两元素按位与,结果压入堆栈
0x12 ior 栈顶两元素按位或,结果压入堆栈
0x13 ixor 栈顶两元素按位异或,结果压入堆栈
0x14 i2d 栈顶int转double ,结果压入堆栈
0x15 d2i 栈顶double转int,结果压入堆栈
0x16 icmp 栈顶两元素当作int比较,结果压入堆栈(-1,0,1)
0x17 dcmp 栈顶两元素作为double比较,结果压入堆栈
0x18 ifz X 如果栈顶元素是0,则跳转到X处执行
0x19 ret 函数返回
0x1A invoke X 调用栈顶对象的非静态函数。
0x1B invokestatic X 调用静态函数,X为函数全限定名,如Class.function
0x1C new 新建对象,并将对象句柄压入堆栈
0x1D newarray X 新建类型X数组,并将数组句柄压入堆栈
0x1E arraylength 将栈顶指向数组的长度压入堆栈
0x1F putfield X 将栈顶元素赋值给栈第二个元素指定对象的X域中
0x20 getfield X 栈顶元素所指向的对象的X域的值压入堆栈
0x21 putstatic X 栈顶元素赋值给X
0x22 getstatic X X所指向的静态变量压入堆栈
0x23 pop 栈顶元素弹出
0x24 NAND 栈顶元素取非
0x25 ldc X 将常量表下标为X的字符串对象句柄压栈
0x26 astore_1 栈顶元素赋值给栈第二个元素指向的数组的下标为栈第三个元素的值