bboyjing's blog

自己动手写JVM十【解析class文件(八)】

本章节继续学习属性相关信息

解析属性表

Code属性

Code时变长属性,只存在于method_info结构中。Code属性中存放字节码等方法相关信息,其结构定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

max_stack给出操作数栈的最大深度,max_locals给出局部变量表大小。接着是字节码,存在u1表中。最后时异常处理表和属性表。这些信息后面都会涉及到,这里就先一笔带过。相关代码位于attr_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
type CodeAttribute struct {
cp ConstantPool
maxStack uint16
maxLocals uint16
code []byte
exceptionTable []*ExceptionTableEntry
attributes []AttributeInfo
}
type ExceptionTableEntry struct {
startPc uint16
endPc uint16
handlerPc uint16
catchType uint16
}
// 读取Code属性
func (self *CodeAttribute) readInfo(reader *ClassReader) {
// 读取2个字节的maxStack
self.maxStack = reader.readUint16()
// 读取2个字节的maxLocals
self.maxLocals = reader.readUint16()
// 读取4个字节的code长度
codeLength := reader.readUint32()
// 读取指定字节数的code
self.code = reader.readBytes(codeLength)
// 读取异常处理表
self.exceptionTable = readExceptionTable(reader)
self.attributes = readAttributes(reader, self.cp)
}
...

ClassFile.class的Code的class文件如下:
jvmgo_27

Exception属性

Exception是变长属性,记录方法抛出的异常表,其结构如下:

1
2
3
4
5
6
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}

Exception属性也比较简单,不做过多解释了,相关代码位于attr_exceptions.go文件中,可自行查看。

LineNumberTable和LocalVariableTable属性

LineNumberTable属性表存放方法的行号信息,LocalVariableTable存放方法的局部变量信息。这两种属性和前面介绍的SourceFIle属性都属于调试信息,都不是运行时必需的。代码也不贴出来了,分别位于attr_line_number_table.go和attr_local_variable_table.go文件中。

完善AttributeInfo接口

8中属性已经讲完了,下面我们来完善AttributeInfo接口,在attribute_info.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
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {
// 读取2个字节的属性长度
attributesCount := reader.readUint16()
// 读取所有属性
attributes := make([]AttributeInfo, attributesCount)
for i := range attributes {
attributes[i] = readAttribute(reader, cp)
}
return attributes
}
// 读取属性
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {
// 读取2个字节的attribute_name_index
attrNameIndex := reader.readUint16()
// 通过attrNameIndex从常量池中获取utf8字面值
attrName := cp.getUtf8(attrNameIndex)
// 读取固定4个字节的attribute_length
attrLen := reader.readUint32()
// 读取属性
attrInfo := newAttributeInfo(attrName, attrLen, cp)
attrInfo.readInfo(reader)
return attrInfo
}
// 通过属性名字返回属性实现
func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {
switch attrName {
case "Code":
return &CodeAttribute{cp: cp}
...
default:
// 读取未实现的属性
return &UnparsedAttribute{attrName, attrLen, nil}
}
}

完善ClassFile

class文件中的内容大致都理了一遍,现在可以来完善class_file.go文件中的读取方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (self *ClassFile) read(reader *ClassReader) {
// 读取魔数
self.readAndCheckMagic(reader)
// 读取版本号
self.readAndCheckVersion(reader)
// 读取常量池
self.constantPool = readConstantPool(reader)
// 读取类访问标志
self.readAccessFlags(reader)
// 读取类索引
self.readThisClass(reader)
// 读取超类索引
self.readSuperClass(reader);
// 读取接口索引表
self.readInterface(reader)
// 读取字段表
self.readFields(reader)
// 读取方法表
self.readMethods(reader)
// 读取属性
self.attributes = readAttributes(reader, self.constantPool)
}

最后将member_info.go文件中的readMember方法的读取属性方法放开。

测试

在搜索class文件的测试中,把class加载到了内存中,并且看到了一堆看似杂杂乱无章的数字打印到控制台,现在就来修改下测试代码,把命令行工具临时打造成一个简化版的javap。
在class_file.go和member_info.go文件中添加对应的读取方法,方法很简单,就不列出来了。
修改测试入口main.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
37
38
39
40
41
42
43
44
45
46
47
// 模拟启动JVM
func startJVM(cmd *cmd.Cmd) {
// 获取Classpath
cp := classpath.Parse(cmd.XjreOption, cmd.CpOption)
fmt.Printf("classpath:%v class:%v args:%v\n",
cp, cmd.Class, cmd.Args)
// 将.替换成/(java.lang.String -> java/lang/String)
className := strings.Replace(cmd.Class, ".", "/", -1)
// 读取class
cf := loadClass(className, cp)
fmt.Println(cmd.Class)
printClassInfo(cf)
}
func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile {
// 读取class
classData, _, err := cp.ReadClass(className)
if err != nil {
panic(err)
}
cf, err := classfile.Parse(classData)
if err != nil {
panic(err)
}
return cf
}
// 打印出class文件的重要信息
func printClassInfo(cf *classfile.ClassFile) {
fmt.Printf("version: %v.%v\n", cf.MajorVersion(), cf.MinorVersion())
fmt.Printf("constants count: %v\n", len(cf.ConstantPool()))
fmt.Printf("access flags: 0x%x\n", cf.AccessFlags())
fmt.Printf("this class: %v\n", cf.ClassName())
fmt.Printf("super class: %v\n", cf.SuperClassName())
fmt.Printf("interfaces: %v\n", cf.InterfaceNames())
fmt.Printf("fields count: %v\n", len(cf.Fields()))
for _, f := range cf.Fields() {
fmt.Printf(" %s\n", f.Name())
}
fmt.Printf("methods count: %v\n", len(cf.Methods()))
for _, m := range cf.Methods() {
fmt.Printf(" %s\n", m.Name())
}
}

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
cd /home/zhangjing/IdeaProjects/jvmgo/go
go install cn.didadu/jvmgo
#指定-classpath
./bin/jvmgo -classpath /home/zhangjing/IdeaProjects/jvmgo/java/target/classes cn.didadu.ClassFile
#输出结果
classpath:/home/zhangjing/IdeaProjects/jvmgo/java/target/classes class:cn.didadu.ClassFile args:[]
cn.didadu.ClassFile
version: 52.0
constants count: 64
access flags: 0x21
this class: cn/didadu/ClassFile
super class: java/lang/Object
interfaces: []
fields count: 8
FLAG
BYTE
X
SHORT
INT
LONG
PI
E
methods count: 2
<init>
main

解析class文件到这儿终于结束了。