写在前面
由于了好久终于想好动手完成这个项目,对我来说无疑是一个大的挑战,最开始学习共语言的目的是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文件