bboyjing's blog

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

本章节继续学习常量池相关信息

常量池

接上一章节

CONSTANT_Fieldref_info

CONSTANT_Fieldref_info表示字段符号引用,其结构体如下:

1
2
3
4
5
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

class_index和name_and_type_index都是常量池索引,分别指向CONSTANT_Class_info和CONSTANT_NameAndType_info常量。
另外,这里穿插一下CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info。分别表示普通方法符号引用和接口方法符号引用。因为这三种的结构体一样,所以我们实现的时候使用一个统一的结构体。我们就选CONSTANT_Fieldref_info来学习下,其他两个后面就不赘述了。相关代码位于cp_member_ref.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type ConstantFieldrefInfo struct{ ConstantMemberrefInfo }
/*
CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info 的结构一样
所以使用一样的结构体ConstantMemberrefInfo来表示
*/
type ConstantMemberrefInfo struct {
cp ConstantPool
classIndex uint16
nameAndTypeIndex uint16
}
// 读取ConstantMemberrefInfo
func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) {
// 读取2个字节的class_index
self.classIndex = reader.readUint16()
// 读取2个字节的name_and_type_index
self.nameAndTypeIndex = reader.readUint16()
}

下面来看一个CONSTANT_Fieldref_info的class文件:
jvmgo_21
第一个字节tag为0x09,表示CONSTANT_Fieldref_info。后面两个字节是0x0032,转换成十进制为50,最后两个字节是0x0033,转换成十进制为51。
找出常量池第50个、51个:
jvmgo_22
先看下第50个,表示java.lang.System类,第51个正好是上一章节讲的CONSTANT_NameAndType_info例子,这两个组合起来就是System.out
CONSTANT_Fieldref_info位于常量池的第2个,我们就顺便看下哪里指向了这个索引:
jvmgo_23
可以看出,是在方法表中的main()方法指向了常量池的第二个,这样也顺便验证了上一章的疑惑。
最后再完善下constant_pool.go和cp_member_ref.go,添加读取相关值的方法,首先编辑constant_pool.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 读取索引读取ClassName
func (self ConstantPool) getClassName(index uint16) string {
// 通过索引读取CONSTANT_Class_info
classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
// 通过CONSTANT_Class_info的nameIndex读取类的字面值
return self.getUtf8(classInfo.nameIndex)
}
// 读取ConstantNameAndTypeInfo中name_index和descriptor_index对应的字面值
func (self ConstantPool) getNameAndType(index uint16) (string, string) {
// 通过索引读取ConstantNameAndTypeInfo
ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
// 通过ConstantNameAndTypeInfo中nameIndex索引读取字面值
name := self.getUtf8(ntInfo.nameIndex)
// 通过ConstantNameAndTypeInfo中descriptorIndex索引读取字面值
_type := self.getUtf8(ntInfo.descriptorIndex)
return name, _type
}

接下来编辑cp_member_ref.go文件:

1
2
3
4
5
6
7
8
9
// 通过classIndex读取class字面值
func (self *ConstantMemberrefInfo) ClassName() string {
return self.cp.getClassName(self.classIndex)
}
// 通过nameAndTypeIndex读取名称和描述字面值
func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) {
return self.cp.getNameAndType(self.nameAndTypeIndex)
}

剩余常量结构

还有三种常量没有介绍CONSTANT_MothodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info。它们是Java SE 7才添加到class文件中的,目的是支持新增的invokedynamic指令,暂不深究了,相关代码位于cp_invoke_dynamic.go文件中。

至此常量池中的常量结构大致介绍完毕,下面要回溯到第六章节没有讲完的两个地方。

未完的ConstantInfo

第一个地方是要完成ConstantInfo的读取,第六章由于常量结构体还买有讲,所以没办法读取,现在可以了,修改constant_info.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 读取ConstantInfo
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
// 首先读取第一个字节tag
tag := reader.readUint8()
// 根据tag生成常量结构体实现
c := newConstantInfo(tag, cp)
// 读取常量信息
c.readInfo(reader)
return c
}
// 根据tag生成常量结构体实现
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
switch tag {
case CONSTANT_Integer:
return &ConstantIntegerInfo{}
// 省略
...
default:
panic("java.lang.ClassFormatError: constant pool tag!")
}
}

未完的ConstantPool结构体

第二个地方就是没讲完的ConstantPool结构体,在讲ConstantInfo结构体的过程中,对ConstantPool也已经大致了解了,也完善了cp_constant_poll.go大部分代码。常量池有三点需要特别注意:

  1. 表头给出的常量池大小比实际大1。假设表给出的值是n,那么常量池的实际大小时n-1。
  2. 有效的常量池索引是1 ~ n-1
  3. CONSTANT_Long_info和CONSTANT_Double_info各占两个位置。也就是说,如果常量池中存在这两种常量的话,实际的常量数量比n-1还要少,而且1 ~ n-1的某些数也会变成无效索引

完善cp_constant_poll.go中的读取ConstantPool方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 读取常量池
func readConstantPool(reader *ClassReader) ConstantPool {
// 常量池数量(2个字节)
cpCount := int(reader.readUint16())
// 生成长度为cpCount,类型是ConstantInfo的slice,就是常量池
cp := make([]ConstantInfo, cpCount)
// 注意:常量池的数量从1开始!
for i := 1; i < cpCount; i++ {
cp[i] = readConstantInfo(reader, cp)
// ConstantLongInfo和ConstantDoubleInfo占两个位置
switch cp[i].(type) {
case *ConstantLongInfo, *ConstantDoubleInfo:
i++
}
}
return cp
}

常量池到这里总算可以告一段落了,下一章学习如何解析属性表。