bboyjing's blog

自己动手写JVM三【解析class文件(一)】

上一章节已经实现了读取class文件,测试的时候还打印出一堆杂乱无章的数字,本章就来学习下,这些数字到底是什么,如何解析这些class文件。

class文件

作为类信息的载体,每个class文件都完整地定义了一个类,也就是说所有的信息都包含在那一堆数字当中,我们可以通过网络下载、从数据库加载,甚至是在运行中直接生成class文件。所以,class文件并非特指位于磁盘中的.class文件,而是泛指任何格式符合规范的class数据。
为了更好的理解class文件,我们再建一个java项目,和go项目同级,该项目的作用就是在需要的时候测试自己写的JVM。现在,新建一个class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ClassFile {
public static final boolean FLAG = true;
public static final byte BYTE = 123;
public static final char X = 'X';
public static final short SHORT = 12345;
public static final int INT = 123456789;
public static final long LONG = 12345678901L;
public static final float PI = 3.14f;
public static final double E = 2.71828;
public static void main(String[] args) throws RuntimeException {
System.out.println("Hello, World!");
}
}

上述代码很简单,是为了学习编译后的class文件。《自己动手写Java虚拟机》的作者写了classpy的图形化工具,可以方便地查看反编译后的class文件。具体如何使用请参照作者的Github站点。编译ClassFile,用classpy打开ClassFile.class:
jvmgo_3
各种看不懂啊,没有关系,慢慢来。构成class文件的基本数据单位是以16进制表示的一个字节,默认按大端方式存储,可以把整个class文件当成一个字节流来处理。
Java虚拟机规范使用一种类似C语言的结构体来描述class文件,这种伪结构只有两种数据类型:无符号数和表。其中无符号数是基本数据类型以u1、u2和u4来分别代表1、2和4个字节无符号数。表是由多个无符号数或者其他表作为数据项的复合数据类型,所有的表都习惯以_info结尾。下表列出calss文件的构成:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attribute_count 1
attribute_info attributes attribute_count

现在再看看那一串数字,看似杂乱无章,其实是由严格地规定组合而成,我们要做的就是从数字中把规定找出来,那么就可以揭开class文件的面纱了。

读取class文件

下面将一边学习class文件格式,一边编写代码实现class文件解析。Go语言内置了丰富的数据类型,非常适合处理class文件。下表列举了Go和Java基本数据类型对照关系:

Go语言类型 Java语言类型 说明
int8 byte 8比特有符号整数
unit8(别名byte) N/A 8比特无符号整数
int16 short 16比特有符号整数
uint16 char 16比特无符号整数
int32(别名rune) int 32比特有符号整数
uint32 N/A 32比特无符号整数
int64 long 64比特有符号整数
uint64 N/A 64比特无符号整数
float32 float 32比特IEEE-745浮点数
float64 double 64比特IEEE-745浮点数

先不探究class文件的规律,把它读出来,之前说过了可以把class文件当成字节流来处理,我们定义一个结构来存储数据。class_reader.go用于读取字节流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package classfile
import "encoding/binary"
// byte数组存储读取的字节流
type ClassReader struct {
data []byte
}
// 读取u1类型的数据
func (self *ClassReader) readUint8() uint8 {
// 读取第一个字节,8位uint
val := self.data[0]
// 将读取过的字节从字节流中剔除
self.data = self.data[1:]
return val
}
....
// 读取u2表,表的大小由开头的u2数据指出
func (self *ClassReader) readUint16s() []uint16 {
n := self.readUint16()
s := make([]uint16, n)
for i := range s {
s[i] = self.readUint16();
}
return s
}
// 读取指定数量的字节
func (self *ClassReader) readBytes(n uint32) []byte {
bytes := self.data[:n]
self.data = self.data[n:]
return bytes
}

下面定义和Class文件结构数据类型一致的class_file.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package classfile
type ClassFile struct {
magic uint32
minorVersion uint16
majorVersion uint16
constantPool ConstantPool
accessFlags uint16
thisClass uint16
superClass uint16
interfaces []uint16
fields []*MemberInfo
methods []*MemberInfo
attributes []AttributeInfo
}

以上只是class_file.go的部分代码,后面一点点完善。其中的ConstantPool、MemberInfo和AttributeInfo都定义在其他文件中,暂时先不管,可以把文件建起来,数据结构定义好,能够编译通过就可以了。
这一章节其实也就做了一些解析class的准备工作,下面几个章节将对照class文件,详细学习下ClassFile中每一项的含义,同时完善代码。