用go编写java虚拟机(一)class文件

写在前面

由于了好久终于想好动手完成这个项目,对我来说无疑是一个大的挑战,最开始学习共语言的目的是go越来越火想用这个技能来获得一份不错的收入,当我用工作之余完成go的基础学习后,迫不及待地的用go完成了几个小项目,最终我发现go和java还是有较大差距的,从生态上来说用java的小伙伴无疑是最开心的,几乎任何问题都能百度到一两点用来参考,而go就悲剧了,同样用java开发一个web项目那叫一个快,用go的话你可能会遇到百度都百度不到的问题,就excel的解析用go的第三方包bug满天飞不如自己写一个呢,用java几乎用不到自己来写某个东西,都是现成的,就go最擅长的并发和网络编程,我尝试了后对比netty,感觉go不是那么香了,但是我缺一门造轮子和写桌面应用的语言,不选择c++是因为go感觉还是会大火

用go写jvm的目的

本着继续加强go的语法,突破java的瓶颈,选择这个方式来完成,再有就是jvm的内存管理我不想写,go有类似java的内存管理机制自动回收,所以选择这个方案。

初识class文件

class文件是什么?反正就是java编译后的东西,此后系列文章都用我自己的语言解释,有错误的地方可以评论区指出来,毕竟我的水平有限,另外就是我会以我的视角来写,不官方的描述解释一些东西。
现在我们来写一个类

public class Main {
    public static void main(String[] args) {

        System.out.println("Hello world!");
    }
}

编译一下,或者在idea里面运行一下找到对应的class文件

现在想办法看看class文件的内容,在这里要提示各位,我们程序员从来都是byte加16进制,没有其它的,尤其是网络编程物联网等没有字符串就是byte和16进制
写一段程序读取class文件的内容

package main

import (
	"fmt"
	"os"
)

func main() {
	file, err := os.ReadFile("main.class")
	if err != nil {
		panic(err)
	}
	for _, o := range file {
		sprintf := fmt.Sprintf("%02X ", o)
		fmt.Print(sprintf)
		fmt.Print(" ")
	}
}

运行结果

CA  FE  BA  BE  00  00  00  34  00  22  0A  00  06  00  14  09  00  15  00  16  08  00  17  0A  00  18  00  19  07  00  1A  07  00  1B  01  00  06  3C  69  6E  69  74  3E  01  00  03  28  29  56  01  00  04  43  6F  64  65  01  00  0F  4C  69  6E  65  4E  75  6D  62  65  72  54  61  62  6C  65  01  00  12  4C  6F  63  61  6C  56  61  72  69  61  62  6C  65  54  61  62  6C  65  01  00  04  74  68  69  73  01  00  06  4C  4D  61  69  6E  3B  01  00  04  6D  61  69  6E  01  00  16  28  5B  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  29  56  01  00  04  61  72  67  73  01  00  13  5B  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  01  00  0A  53  6F  75  72  63  65  46  69  6C  65  01  00  09  4D  61  69  6E  2E  6A  61  76  61  0C  00  07  00  08  07  00  1C  0C  00  1D  00  1E  01  00  0C  48  65  6C  6C  6F  20  77  6F  72  6C  64  21  07  00  1F  0C  00  20  00  21  01  00  04  4D  61  69  6E  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  4F  62  6A  65  63  74  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  53  79  73  74  65  6D  01  00  03  6F  75  74  01  00  15  4C  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  3B  01  00  13  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  01  00  07  70  72  69  6E  74  6C  6E  01  00  15  28  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  29  56  00  21  00  05  00  06  00  00  00  00  00  02  00  01  00  07  00  08  00  01  00  09  00  00  00  2F  00  01  00  01  00  00  00  05  2A  B7  00  01  B1  00  00  00  02  00  0A  00  00  00  06  00  01  00  00  00  01  00  0B  00  00  00  0C  00  01  00  00  00  05  00  0C  00  0D  00  00  00  09  00  0E  00  0F  00  01  00  09  00  00  00  37  00  02  00  01  00  00  00  09  B2  00  02  12  03  B6  00  04  B1  00  00  00  02  00  0A  00  00  00  0A  00  02  00  00  00  04  00  08  00  05  00  0B  00  00  00  0C  00  01  00  00  00  09  00  10  00  11  00  00  00  01  00  12  00  00  00  02  00  13

代码中用到了go的格式化输出,之所以循环是为了打印加个空格,对这块比较不了解的可以百度“go格式化输出”我们要把这段16进制的内容保存并且分析,我们之后第一目标就是运行这个文件

魔数

注意观察class文件的前四个字节的内容

CA  FE  BA  BE

咖啡杯比,这个东西是class文件的开头,标识这是一个class文件类型的文件,像zip、pdf等文件开头也有这个再往后看 00 00 00 34

00 00 00 34

这代表版本就是jdk的版本java是向下兼容,如果你的虚拟机是针对java8的那么高于这个版本号的都不能运行才对。前两个字节是小版本号,后两个字节是主版本号
接下来是00 22

CA  FE  BA  BE  00  00  00  34  00  22

拿出计算器

22对应的10进制是34 代表实际上常量池有33个常量信息,为什么是33个而不是34个,因为0这个位置是个特殊索引,我们可以理解为一个容量34的数组,下标0被占用了,所以是33个,常量池的下标也从1开始不从0,一直到总数-1
接下来会有33个常量信息我们一个一个分析,遇到一个说一个没有的遇见了再说

CA  FE  BA  BE  00  00  00  34  00  22  0A

常量池的每一项元素开头都有一个标识tag,用来标识是什么类型的常量,通过这个标识对应解析常量信息,

类型
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_InvokeDynamic 18

如上表0A是十进制10对应CONSTANT_Methodref这种类型的数据,第一个常量是一个方法信息
数据结构是

CONSTANT_Methodref_info {

    1字节 tag;//标记

    2字节 class_index;//方法所属的类

    2字节 name_and_type_index;//方法的名称和签名

}

数一下接下来是4字节需要解析加上tag是5字节

0A  00  06  00  14  

00 06是方法所属的类这里是个下标6,从1开始,暂时先不管因为后面还没解析到
00 14是方法的名称也是个下标,暂时先放一放
接下来是09,CONSTANT_Fieldref这个类型,数据结构是

CONSTANT_Fieldref_info {

    1字节 tag;

    2字节 class_index;

    2字节 name_and_type_index;

}

可以看到和方法类似,先放一放

09  00  15  00  16

接下来的第三项的tag是08对应的是CONSTANT_String这个类型,对应的数据结构是

CONSTANT_String_info {

    1字节 tag;

    2字节 string_index;//strin类型常量的下标

}
08  00  17

下面的tag是0a是个方法信息

 0A  00  18  00  19

第5个常量的tag是07CONSTANT_Class类型的数据,是类或者接口的信息,数据结构是

CONSTANT_Class_info {

    1字节 tag;

    2字节 name_index;

}

第6个也是这个类型

 07  00  1A  07  00  1B 

接下来第7个tag 是1是CONSTANT_Utf8这个类型,数据结构是

CONSTANT_Utf8_info {

    1字节 tag;

    2字节 length;

    length字节 bytes[length];

}

这个类型是字符串类型,方法名称类名称什么的都是这个,长度是2字节,理论上类名称可以是FF FF 65535的长度,我没试过。。。。

 01  00  06  3C  69  6E  69  74  3E

运行

	s := "3C696E69743E"
	str, _ := hex.DecodeString(s)
	fmt.Println(string(str))

对应的字符串是
接下来第8、9、10、11、12、13、14、15、16、17、18、19还是字符串

01  00  03  28  29  56  01  00  04  43  6F  64  65  01  00  0F  4C  69  6E  65  4E  75  6D  62  65  72  54  61  62  6C  65  01  00  12  4C  6F  63  61  6C  56  61  72  69  61  62  6C  65  54  61  62  6C  65  01  00  04  74  68  69  73  01  00  06  4C  4D  61  69  6E  3B  01  00  04  6D  61  69  6E  01  00  16  28  5B  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  29  56  01  00  04  61  72  67  73  01  00  13  5B  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  01  00  0A  53  6F  75  72  63  65  46  69  6C  65  01  00  09  4D  61  69  6E  2E  6A  61  76  61 

解析出来是

()VCodeLineNumberTableLocalVariableTablethisLMain;main([Ljava/lang/String;)Vargs[Ljava/lang/String;SourceFileMain.java

接下来的class文件内容

0C  00  07  00  08  07  00  1C  0C  00  1D  00  1E  01  00  0C  48  65  6C  6C  6F  20  77  6F  72  6C  64  21  07  00  1F  0C  00  20  00  21  01  00  04  4D  61  69  6E  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  4F  62  6A  65  63  74  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  53  79  73  74  65  6D  01  00  03  6F  75  74  01  00  15  4C  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  3B  01  00  13  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  01  00  07  70  72  69  6E  74  6C  6E  01  00  15  28  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  29  56  00  21  00  05  00  06  00  00  00  00  00  02  00  01  00  07  00  08  00  01  00  09  00  00  00  2F  00  01  00  01  00  00  00  05  2A  B7  00  01  B1  00  00  00  02  00  0A  00  00  00  06  00  01  00  00  00  01  00  0B  00  00  00  0C  00  01  00  00  00  05  00  0C  00  0D  00  00  00  09  00  0E  00  0F  00  01  00  09  00  00  00  37  00  02  00  01  00  00  00  09  B2  00  02  12  03  B6  00  04  B1  00  00  00  02  00  0A  00  00  00  0A  00  02  00  00  00  04  00  08  00  05  00  0B  00  00  00  0C  00  01  00  00  00  09  00  10  00  11  00  00  00  01  00  12  00  00  00  02  00  13

tag是0c 十进制12为CONSTANT_NameAndType类型,类似中间表,数据结构是

CONSTANT_NameAndType_info {

    1字节 tag;

    2字节 name_index;

    2字节 descriptor_index;

}

这个到现在是第20个常量

0C  00  07  00  08

name_index是07代表第7个常量,解析是这个字符串descriptor_index是第八个常量()V
第21个常量,07CONSTANT_Class类型(上面介绍过数据类型)

07  00  1C

第22个常量CONSTANT_NameAndType类型,

0C  00  1D  00  1E

第23个常量字符串

 01  00  0C  48  65  6C  6C  6F  20  77  6F  72  6C  64  21 

解析出来是Hello world!
第24个常量 类信息

07  00  1F  

第25个常量是CONSTANT_NameAndType类型

0C  00  20  00  21

第26\27\28\29\30\31\32\33个类型是字符串

 01  00  04  4D  61  69  6E  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  4F  62  6A  65  63  74  01  00  10  6A  61  76  61  2F  6C  61  6E  67  2F  53  79  73  74  65  6D  01  00  03  6F  75  74  01  00  15  4C  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  3B  01  00  13  6A  61  76  61  2F  69  6F  2F  50  72  69  6E  74  53  74  72  65  61  6D  01  00  07  70  72  69  6E  74  6C  6E  01  00  15  28  4C  6A  61  76  61  2F  6C  61  6E  67  2F  53  74  72  69  6E  67  3B  29  56

解析出来是

Mainjava/lang/Objectjava/lang/SystemoutLjava/io/PrintStream;java/io/PrintStreamprintln(Ljava/lang/String;)V

现在我们已经把常量池解析完了,现在就可以对照所有常量来组合信息了

常量类型
CONSTANT_Methodref_info 所属类,下标是6,对应第6个常量第6个常量对应的是第26个常量值是Main,方法名称和签名是“<init>”, ()V

下面不再整理其它的了
接下来还剩下面的字节没分析

00  21  00  05  00  06  00  00  00  00  00  02  00  01  00  07  00  08  00  01  00  09  00  00  00  2F  00  01  00  01  00  00  00  05  2A  B7  00  01  B1  00  00  00  02  00  0A  00  00  00  06  00  01  00  00  00  01  00  0B  00  00  00  0C  00  01  00  00  00  05  00  0C  00  0D  00  00  00  09  00  0E  00  0F  00  01  00  09  00  00  00  37  00  02  00  01  00  00  00  09  B2  00  02  12  03  B6  00  04  B1  00  00  00  02  00  0A  00  00  00  0A  00  02  00  00  00  04  00  08  00  05  00  0B  00  00  00  0C  00  01  00  00  00  09  00  10  00  11  00  00  00  01  00  12  00  00  00  02  00  13

接下来介绍常量池后紧跟着的是类的访问控制 00 21,是否是接口?是否私有?是否final等等,在后续详细解释
接着是2字节的 00 05类名 对应的常量池的下标是Main
继续2字节00 06 父类的下标 java/lang/Object,继续2字节接口个数0,继续2字节域个数0,


接着是方法表,注意下面一大堆内容是方法表,方法表和常量池类似也是有特定的结构的 其数据结构是

	method_info {

    2字节 access_flags;//访问标识,后续用到详解

    2字节 name_index;//方法名称详解

    2字节 descriptor_index;//描述符索引

	2字节 attributes_count//属性集合数量

	attribute_info //属性集合

}

先是2字节的方法个数 00 02(理论上可以有65535个方法,我没试过),00 01 访问标识(方法的修饰符这里是public,其它的后续用到详解)00 07方法名索引 对应常量池的<init> ,00 08 对应常量池的()V,方法的描述(无参、无返回值,后续详解)
00 01 属性个数
属性表的结构(再次表示,我会用到什么解释什么没用到的,暂时会先放一放)属性表的结构与字符串的结构有些像

attribute_info  {

    2字节 attribute_name_index;//属性名称,很关键,后续的属性该怎样解析全靠这个名称分类

    4字节 attribute_length;//属性长度

    attribute_length字节 bytes[attribute_length];//属性内容

}
00  09  00  00  00  2F

属性名称下标09 对应Code 长度 2F 对应 47,如此我们知道后面47个字节都是Code这个属性的内容

00  09  00  00  00  2F  00  01  00  01  00  00  00  05  2A  B7  00  01  B1  00  00  00  02  00  0A  00  00  00  06  00  01  00  00  00  01  00  0B  00  00  00  0C  00  01  00  00  00  05  00  0C  00  0D  00  00

对于code这种属性我们应该按照下面的结构来解析

	code	{

    2字节 attribute_name_index;//属性名称

    4字节 attribute_length;//属性长度

    2字节 max_stack;//栈深
    2字节 max_locals;//局部变量的存储空间单位是slot(后续详解)
    4字节 code_length;//代码指令长度,关键部分
    code_length字节 code;//代码,也叫指令,关键部分
    2字节 exception_table_length;//异常表个数
     exception_table_info[exception_table_length];//异常表
    2字节 attribute_length;//属性长度(套娃开始)
    attribute_length字节 bytes[attribute_length];//属性内容(套娃开始)

}

00 09 00 00 00 2F 名称和长度,上面已经解析,00 01栈深 00 01存储空间00 00 00 05指令(代码长度)2A B7 00 01 B1(代码)异常表0个,code属性中的属性 00 02两个
接下来处理code属性中的属性(套娃模式)下面是第一个

00  0A  00  00  00  06  00  01  00  00  00  01

0A对应第10个常量LineNumber,所以要按照LineNumber的类型解析这个属性,大概含义是class文件的行号和源码的对应关系,其数据结构是

	LineNumber {

    2字节 attribute_name_index;//属性名称

    4字节 attribute_length;//属性长度

    2字节 line_number_table_length;//line_number_table的个数
    
    line_number_table[line_number_table_length];//

}

其中line_number_table单个是4字节,前2字节是字节码的行号、后2字节是源码的行号00 01 00 00 00 01代表一个line_number_table,字节码是第一行,后面的0证明源码中不存在。
code中的第二个属性

00  0B  00  00  00  0C  00  01  00  00  00  05  00  0C  00  0D  00  00

00 0b代表第11个常量LocalVariableTable,这种类型的属性大概描述栈桢中局部变量表中的变量与Java源码中定义的变量之间的关系。
后续实现栈桢的时候详解,先放一放
下面是第二个方法的解析先是访问控制00 09(这个后面详解)打开计算器

第1和5位是1其它是0,代表 public and static
00 0e 第14个常量 main([Ljava/lang/String;)V 这个就是main方法的内容了

00  01  00  09  00  00  00  37  00  02  00  01  00  00  00  09  B2  00  02  12  03  B6  00  04  B1  00  00  00  02  00  0A  00  00  00  0A  00  02  00  00  00  04  00  08  00  05  00  0B  00  00  00  0C  00  01  00  00  00  09  00  10  00  11  00  00

00 01 00 09 方法1个属性,属性类型是第9个常量是Code
00 00 00 37属性长度
00 02 栈深
00 01 空间
00 00 00 09 指令长度
B2 00 02 12 03 B6 00 04 B1 指令
00 00 异常个数
00 02 属性个数2
00 0A LineNumber
00 00 00 0A LineNumber的信息长度(上面分析过这个)
00 0B 00 00 00 0C 第二个属性00 0b代表第11个常量LocalVariableTable长度是12
到此方法弄完了最后剩余

  00  01  00  12  00  00  00  02  00  13

00 01 属性个数(这个是类的属性)
00 12 代表 第18个常量其值是SourceFile,这个类型的属性用于记录Class文件的源码文件名称,
00 00 00 02 是长度 00 13 是第13个常量Main.java
到这里正好一个字节不剩的分析完整个class文件

转载请说明出处内容投诉
CSS教程_站长资源网 » 用go编写java虚拟机(一)class文件

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买