bboyjing's blog

自己动手写JVM七【解析class文件(五)】

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

常量池

接上一章节

CONSTANT_String_info

CONSTANT_String_info常量表示java.lang.String字面量,结构如下:

1
2
3
4
CONSTANT_String_info {
u1 tag;
u2 string_index;
}

可以看出,CONSTANT_String_info本身并不存放字符串数据,只存了常量池索引,这个索引指向前面讲的CONSTANT_Utf8_info常量。ConstantStringInfo结构体位于cp_string.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ConstantStringInfo struct {
cp ConstantPool
stringIndex uint16
}
// 读取CONSTANT_String_info
func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
// 读取string_index(2个字节)
self.stringIndex = reader.readUint16()
}
// 读取string字面常量
func (self *ConstantStringInfo) String() string{
//需要通过stringIndex到常量池中读取
return self.cp.getUtf8(self.stringIndex)
}

因为指向stringIndex指向的是常量池索引,所以要获取string字面值,还需要去ConstantPool中读取数据,下面编辑constant_pool.go文件,添加通过索引值读取ConstantUtf8Info的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 通过索引读取常量结构体
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
/*
为什么不是index - 1?
因为常量池的索引是从1开始的!
*/
if cpInfo := self[index]; cpInfo != nil {
return cpInfo
}
panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}
// 通过索引获取ConstantUtf8Info结构体的字面值
func (self ConstantPool)getUtf8(index uint16) string {
// 将ConstantInfo转换为实际类型ConstantUtf8Info
utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
return utf8Info.str
}

ClassFile.class中main()方法中使用的字符串class文件如下:
jvmgo_15
有上图可以看出,tag为0x08,表示CONSTANT_String_info,tag占一个字节。后面两个字节0x0034是常量池索引,转换成十进制为52,那再看一下常量池第52个:
jvmgo_16
再详细看下class文件,第一个字节tag为0x01,表示CONSTANT_Utf8_info。后面两个字节是length:0x000D,转换成十进制为13。最后13个字节为0x48656C6C6F2C20576F726C6421,对应的ASCII码正是Hello, World!。

CONSTANT_Class_info

CONSTANT_Class_info常量表示类或者接口的符号引用,结构如下:

1
2
3
4
5
6
/*
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
*/

和CONSTANT_String_info类似,name_index是常量池索引,指向CONSTANT_Utf8_info常量,相关代码位于cp_class.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type ConstantClassInfo struct {
cp ConstantPool
nameIndex uint16
}
// 读取CONSTANT_Class_info
func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
//读取name_index(2个字节)
self.nameIndex = reader.readUint16()
}
// 读取class字面值
func (self *ConstantClassInfo) Name() string {
// 需要通过nameIndex到常量池读取utf8字面值
return self.cp.getUtf8(self.nameIndex)
}

ClassFile.class类名的class文件如下:
jvmgo_17
有上图可以看出,tag为0x07,表示CONSTANT_Class_info,tag占一个字节。后面两个字节0x0037是常量池索引,转换成十进制为55,再看下常量池第55个:
jvmgo_18
再详细看下class文件,第一个字节tag为0x01,表示CONSTANT_Utf8_info。后面两个字节是length:0x0013,转换成十进制为19。最后19个字节为0x636E2F6469646164752F436C61737346696C65,对应的ASCII码正是cn/didadu/ClassFile。

CONSTANT_NameAndType_info

CONSTANT_NameAndType_info给出字段或方法的名称和描述符。CONSTANT_Class_info和CONSTANT_NameAndType_info加在一起可以唯一确定一个字段或者方法。,其结构如下:

1
2
3
4
5
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}

字段或方法名由name_index给出,字段或方法的描述符由descriptor_index给出。name_index和descriptor_index都是常量池索引,指向CONSTANT_Utf8_info常量。字段和方法名就是代码中出现的字段或者方法的名字,这个很好理解。重点看一下他们的描述符,Java虚拟机规范定义了一种简单的语法来描述字段和方法,具体规则如下:

  1. 类型描述符
    1. 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。要注意的是,long的描述符是J而不是L。
    2. 引用类型的描述符是L+类的完全限定名+分号。
    3. 数组类型的描述符就是[+数组元素类型描述符。
  2. 字段描述符就是字段类型的描述符。
  3. 方法描述符是(分号分割的参数类型描述符)+返回值类型描述符,其中void返回值由单个字母V表示。

看到这里有点疑惑了,这里说的字段、方法的名称和描述与之前讲的字段表、方法表有什么区别。经过一番研究,具体过程就不详述了,参照ClassFile2.class的class文件,会发现端倪。得出一个初步的结论,字段表、方法表表示的是类成员变量和方法的定义,而此处的CONSTANT_NameAndType_info出现在局部方法中,表示局部变量的定义和方法的调用。
相关代码位于cp_name_and_type.go文件中:

1
2
3
4
5
6
7
8
9
10
11
12
type ConstantNameAndTypeInfo struct {
nameIndex uint16
descriptorIndex uint16
}
// 读取CONSTANT_NameAndType_info
func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) {
// 读取2个字节的name_index
self.nameIndex = reader.readUint16()
// 读取2个字节的descriptorIndex
self.descriptorIndex = reader.readUint16()
}

从class文件中挑一个CONSTANT_NameAndType_info看看:
jvmgo_19
看下class文件,第一个字节tag为0x12,表示CONSTANT_NameAndType_info。后面两个字节是0x003B,转换成十进制为59(name_index),再后面两个字节是0x003c(descriptor_index),转换成十进制是60。
找出常量池第59个、60个:
jvmgo_20
就不分析了,最终可以看出该字段的名称和描述分别为out、Ljava/io/PrintStream,表示变量的名称为out,类型是java.io.PrintStream,对应ClassFile.java中System.out的使用。

下节继续。。。