动手实现自己的 JVM——Go!(ch03)

发布于:2025-02-18 ⋅ 阅读:(114) ⋅ 点赞:(0)

动手实现自己的 JVM——Go!(ch03)

参考张秀宏老师的《自己动手写java虚拟机》

本章节我们将实现对 Class 文件的解析。Class 文件的基本单位是字节,但直接操作字节流非常不方便。因此,我们通过一个辅助类 ClassReader 来简化读取操作。

代码地址: https://github.com/9lucifer/go_jvm.git


(一)ClassReader

1. ClassReader 结构体

ClassReader 是用于读取字节数据的辅助类,封装了字节切片并提供了一系列读取方法。

package classfile

import "encoding/binary"

// 用于读取数据
type ClassReader struct {
    data []byte
}
  • 字段
    • data []byte:存储待读取的字节数据。
  • 作用
    • 通过方法逐步读取 data 中的数据,并更新 data 的起始位置。

2. 读取方法

ClassReader 提供了多种读取方法,支持从字节切片中读取不同长度的数据。

2.1 读取 uint8
func (self *ClassReader) readUint8() uint8 {
    val := self.data[0]
    self.data = self.data[1:]
    return val
}
  • 功能:读取 1 个字节(uint8)。
  • 实现
    • data 的第一个字节读取值。
    • 更新 data,去掉已读取的字节。
  • 返回值uint8 类型的值。

2.2 读取 uint16
func (self *ClassReader) readUint16() uint16 {
    val := binary.BigEndian.Uint16(self.data)
    self.data = self.data[2:]
    return val
}
  • 功能:读取 2 个字节(uint16)。
  • 实现
    • 使用 binary.BigEndian.Uint16 将字节数据解析为大端序的 uint16
    • 更新 data,去掉已读取的 2 个字节。
  • 返回值uint16 类型的值。

2.3 读取 uint32
func (self *ClassReader) readUint32() uint32 {
    val := binary.BigEndian.Uint32(self.data)
    self.data = self.data[4:]
    return val
}
  • 功能:读取 4 个字节(uint32)。
  • 实现
    • 使用 binary.BigEndian.Uint32 将字节数据解析为大端序的 uint32
    • 更新 data,去掉已读取的 4 个字节。
  • 返回值uint32 类型的值。

2.4 读取 uint64
func (self *ClassReader) readUint64() uint64 {
    val := binary.BigEndian.Uint64(self.data)
    self.data = self.data[8:]
    return val
}
  • 功能:读取 8 个字节(uint64)。
  • 实现
    • 使用 binary.BigEndian.Uint64 将字节数据解析为大端序的 uint64
    • 更新 data,去掉已读取的 8 个字节。
  • 返回值uint64 类型的值。

2.5 读取 uint16 数组
func (self *ClassReader) readUint16s() []uint16 {
    n := self.readUint16()
    s := make([]uint16, n)
    for i := range s {
        s[i] = self.readUint16()
    }
    return s
}
  • 功能:读取一个 uint16 数组。
  • 实现
    • 首先读取数组长度 nuint16)。
    • 创建一个长度为 nuint16 切片。
    • 循环读取 nuint16 值并存入切片。
  • 返回值[]uint16 类型的数组。

2.6 读取字节数组
func (self *ClassReader) readBytes(n uint32) []byte {
    bytes := self.data[:n]
    self.data = self.data[n:]
    return bytes
}
  • 功能:读取指定长度的字节数组。
  • 实现
    • data 中截取前 n 个字节。
    • 更新 data,去掉已读取的 n 个字节。
  • 返回值[]byte 类型的字节数组。

3. 关键点解析
3.1 大端序(Big Endian)
  • 定义:数据的高位字节存储在低地址,低位字节存储在高地址。
  • 使用binary.BigEndian 提供了大端序的解析方法。
  • 示例
    • 字节 [0x12, 0x34] 解析为 uint16 时,值为 0x1234
3.2 数据更新
  • 每次读取数据后,data 的起始位置会更新,去掉已读取的部分。
  • 例如,读取 uint16 后,data 会去掉前 2 个字节。

以下是以 ## (二)ClassFile 开头的整理和润色内容:


(二)ClassFile

ClassFile 结构体用于表示 Java 类文件在 JVM 中的描述。它包含了类文件的各个部分,如魔数、版本号、常量池、访问标志、类信息、字段、方法和属性等。

image-20250217004900238


1. ClassFile 结构体定义

ClassFile 结构体定义了类文件的各个字段,具体如下:

type ClassFile struct {
    minorVersion uint16        // 次版本号
    majorVersion uint16        // 主版本号
    constantPool ConstantPool  // 常量池
    accessFlags  uint16        // 访问标志
    thisClass    uint16        // 当前类的索引
    superClass   uint16        // 父类的索引
    interfaces   []uint16      // 接口索引表
    fields       []*MemberInfo // 字段表
    methods      []*MemberInfo // 方法表
    attributes   []AttributeInfo // 属性表
}
  • 字段说明
    • minorVersionmajorVersion:类文件的次版本号和主版本号。
    • constantPool:常量池,存储类文件中的常量信息。
    • accessFlags:类的访问标志(如 publicfinal 等)。
    • thisClasssuperClass:当前类和父类在常量池中的索引。
    • interfaces:实现的接口在常量池中的索引表。
    • fieldsmethods:字段表和方法表,存储类的字段和方法信息。
    • attributes:属性表,存储类的附加信息(如源码文件名、行号表等)。

2. 解析类文件

Parse 函数用于将字节数据解析为 ClassFile 结构体。

func Parse(classData []byte) (cf *ClassFile, err error) {
    defer func() {
        if r := recover(); r != nil {
            var ok bool
            err, ok = r.(error)
            if !ok {
                err = fmt.Errorf("%v", r)
            }
        }
    }()

    cr := &ClassReader{classData}
    cf = &ClassFile{}
    cf.read(cr)
    return
}
  • 功能:将字节数据解析为 ClassFile
  • 实现
    • 使用 ClassReader 读取字节数据。
    • 调用 cf.read(cr) 方法逐步解析类文件的各个部分。
    • 通过 defer 捕获可能的异常并返回错误。

3. 读取类文件内容

read 方法用于从 ClassReader 中读取类文件的各个部分。

func (self *ClassFile) read(reader *ClassReader) {
    self.readAndCheckMagic(reader)      // 读取并检查魔数
    self.readAndCheckVersion(reader)    // 读取并检查版本号
    self.constantPool = readConstantPool(reader) // 读取常量池
    self.accessFlags = reader.readUint16()       // 读取访问标志
    self.thisClass = reader.readUint16()         // 读取当前类索引
    self.superClass = reader.readUint16()        // 读取父类索引
    self.interfaces = reader.readUint16s()       // 读取接口索引表
    self.fields = readMembers(reader, self.constantPool) // 读取字段表
    self.methods = readMembers(reader, self.constantPool) // 读取方法表
    self.attributes = readAttributes(reader, self.constantPool) // 读取属性表
}
  • 功能:从 ClassReader 中读取类文件的各个部分并填充 ClassFile 结构体。
  • 步骤
    1. 读取并检查魔数。
    2. 读取并检查版本号。
    3. 读取常量池。
    4. 读取访问标志。
    5. 读取当前类和父类索引。
    6. 读取接口索引表。
    7. 读取字段表和方法表。
    8. 读取属性表。

4. 检查魔数和版本号
4.1 检查魔数
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
    magic := reader.readUint32()
    if magic != 0xCAFEBABE {
        panic("java.lang.ClassFormatError: magic!")
    }
}
  • 功能:读取并检查魔数。
  • 魔数:类文件的前 4 个字节必须是 0xCAFEBABE,否则抛出异常。
4.2 检查版本号
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {
    self.minorVersion = reader.readUint16()
    self.majorVersion = reader.readUint16()
    switch self.majorVersion {
    case 45:
        return
    case 46, 47, 48, 49, 50, 51, 52:
        if self.minorVersion == 0 {
            return
        }
    }
    panic("java.lang.UnsupportedClassVersionError!")
}
  • 功能:读取并检查版本号。
  • 支持版本
    • 主版本号为 45。
    • 主版本号为 46 到 52,且次版本号为 0。
  • 异常:如果版本号不支持,抛出 UnsupportedClassVersionError

5. 获取类信息

ClassFile 提供了多个方法用于获取类的详细信息。

5.1 获取类名
func (self *ClassFile) ClassName() string {
    return self.constantPool.getClassName(self.thisClass)
}
  • 功能:获取当前类的名称。
  • 实现:通过 thisClass 索引从常量池中获取类名。
5.2 获取父类名
func (self *ClassFile) SuperClassName() string {
    if self.superClass > 0 {
        return self.constantPool.getClassName(self.superClass)
    }
    return ""
}
  • 功能:获取父类的名称。
  • 实现:通过 superClass 索引从常量池中获取父类名。如果 superClass 为 0,表示没有父类。
5.3 获取接口名
func (self *ClassFile) InterfaceNames() []string {
    interfaceNames := make([]string, len(self.interfaces))
    for i, cpIndex := range self.interfaces {
        interfaceNames[i] = self.constantPool.getClassName(cpIndex)
    }
    return interfaceNames
}
  • 功能:获取实现的接口名称列表。
  • 实现:遍历 interfaces 索引表,从常量池中获取每个接口的名称。

6. 其他方法

ClassFile 还提供了多个 get 方法,用于获取类文件的各个字段。

func (self *ClassFile) MinorVersion() uint16 {
    return self.minorVersion
}
func (self *ClassFile) MajorVersion() uint16 {
    return self.majorVersion
}
func (self *ClassFile) ConstantPool() ConstantPool {
    return self.constantPool
}
func (self *ClassFile) AccessFlags() uint16 {
    return self.accessFlags
}
func (self *ClassFile) Fields() []*MemberInfo {
    return self.fields
}
func (self *ClassFile) Methods() []*MemberInfo {
    return self.methods
}
  • 功能:分别获取次版本号、主版本号、常量池、访问标志、字段表和方法表。

(三)常量池

常量池(Constant Pool)是 Class 文件中非常重要的一部分,它存储了类文件中的常量信息,如字符串、类名、字段名、方法名等。常量池是一个表结构,索引从 1 开始。(不是0)


1. 常量池定义

常量池是一个 ConstantInfo 类型的切片,每个元素表示一个常量项。

type ConstantPool []ConstantInfo
  • ConstantInfo:常量项的接口,具体实现包括 ConstantClassInfoConstantUtf8InfoConstantNameAndTypeInfo 等。

2. 读取常量池

readConstantPool 函数用于从 ClassReader 中读取常量池。

func readConstantPool(reader *ClassReader) ConstantPool {
    cpCount := int(reader.readUint16())
    cp := make([]ConstantInfo, cpCount)

    // 常量池索引从 1 开始
    for i := 1; i < cpCount; i++ {
        cp[i] = readConstantInfo(reader, cp)
        // 处理 8 字节常量(Long 和 Double)
        switch cp[i].(type) {
        case *ConstantLongInfo, *ConstantDoubleInfo:
            i++ // 跳过下一个索引
        }
    }

    return cp
}
  • 功能:读取常量池并返回 ConstantPool
  • 步骤
    1. 读取常量池大小 cpCount
    2. 创建一个大小为 cpCountConstantInfo 切片。
    3. 从索引 1 开始遍历常量池,读取每个常量项。
    4. 如果常量项是 ConstantLongInfoConstantDoubleInfo,则跳过下一个索引(因为这些常量占用两个索引)。
  • 注意:常量池的索引从 1 开始,索引 0 是无效的。

3. 获取常量信息

ConstantPool 提供了多个方法用于获取常量池中的信息。

3.1 获取常量项
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
    if cpInfo := self[index]; cpInfo != nil {
        return cpInfo
    }
    panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}
  • 功能:根据索引获取常量项。
  • 实现
    • 如果索引有效,返回对应的常量项。
    • 如果索引无效,抛出异常。
3.2 获取名称和类型
func (self ConstantPool) getNameAndType(index uint16) (string, string) {
    ntInfo := self.getConstantInfo(index).(*ConstantNameAndTypeInfo)
    name := self.getUtf8(ntInfo.nameIndex)
    _type := self.getUtf8(ntInfo.descriptorIndex)
    return name, _type
}
  • 功能:获取名称和类型描述符。
  • 实现
    • 根据索引获取 ConstantNameAndTypeInfo
    • 从常量池中获取名称和类型描述符的字符串。
3.3 获取类名
func (self ConstantPool) getClassName(index uint16) string {
    classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
    return self.getUtf8(classInfo.nameIndex)
}
  • 功能:获取类名。
  • 实现
    • 根据索引获取 ConstantClassInfo
    • 从常量池中获取类名的字符串。
3.4 获取 UTF-8 字符串
func (self ConstantPool) getUtf8(index uint16) string {
    utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
    return utf8Info.str
}
  • 功能:获取 UTF-8 编码的字符串。
  • 实现
    • 根据索引获取 ConstantUtf8Info
    • 返回字符串内容。

4. 常量池的作用

常量池在 Class 文件中扮演了重要角色,主要用于存储以下信息:

  • 字符串常量:如类名、字段名、方法名等。
  • 类和接口的全限定名
  • 字段和方法的名称和描述符
  • 方法句柄和调用点信息

通过常量池,Class 文件可以高效地引用这些信息,而不需要重复存储。


5. 注意点
  • 功能:常量池是 Class 文件的核心部分,存储了类文件中的常量信息。

  • 关键点

    • 常量池索引从 1 开始。
    • 8 字节常量(如 LongDouble)占用两个索引。
    • 提供了多种方法获取常量池中的信息,如类名、字符串、名称和类型等。

(四)解析 Class 文件的核心逻辑

解析 Class 文件的核心逻辑是按照 Class 文件的结构逐步读取和解析字节数据。以下是按照“字符串解析”的逻辑进行匹配的详细说明。


1. Class 文件结构

Class 文件的结构如下:

部分 说明
魔数(Magic) 标识 Class 文件格式,固定为 0xCAFEBABE
版本号(Version) 包括主版本号和次版本号。
常量池(Constant Pool) 存储常量信息,如字符串、类名、字段名等。
访问标志(Access Flags) 类的访问权限和属性,如 publicfinal 等。
类索引(This Class) 当前类在常量池中的索引。
父类索引(Super Class) 父类在常量池中的索引。
接口索引表(Interfaces) 实现的接口在常量池中的索引表。
字段表(Fields) 类的字段信息。
方法表(Methods) 类的方法信息。
属性表(Attributes) 类的附加信息,如源码文件名、行号表等。

2. 解析逻辑

解析 Class 文件的核心逻辑是按照上述结构逐步读取字节数据,并将其解析为对应的数据结构。

2.1 读取魔数
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
    magic := reader.readUint32()
    if magic != 0xCAFEBABE {
        panic("java.lang.ClassFormatError: magic!")
    }
}
  • 功能:读取并检查魔数。
  • 逻辑
    • 读取前 4 个字节,检查是否为 0xCAFEBABE
    • 如果不是,抛出 ClassFormatError 异常。
2.2 读取版本号
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {
    self.minorVersion = reader.readUint16()
    self.majorVersion = reader.readUint16()
    switch self.majorVersion {
    case 45:
        return
    case 46, 47, 48, 49, 50, 51, 52:
        if self.minorVersion == 0 {
            return
        }
    }
    panic("java.lang.UnsupportedClassVersionError!")
}
  • 功能:读取并检查版本号。
  • 逻辑
    • 读取次版本号和主版本号。
    • 检查版本号是否支持,如果不支持,抛出 UnsupportedClassVersionError 异常。
2.3 读取常量池
func readConstantPool(reader *ClassReader) ConstantPool {
    cpCount := int(reader.readUint16())
    cp := make([]ConstantInfo, cpCount)

    for i := 1; i < cpCount; i++ {
        cp[i] = readConstantInfo(reader, cp)
        switch cp[i].(type) {
        case *ConstantLongInfo, *ConstantDoubleInfo:
            i++ // 跳过下一个索引
        }
    }

    return cp
}
  • 功能:读取常量池。
  • 逻辑
    • 读取常量池大小 cpCount
    • 遍历常量池,读取每个常量项。
    • 如果常量项是 LongDouble,则跳过下一个索引。
2.4 读取访问标志
self.accessFlags = reader.readUint16()
  • 功能:读取访问标志。
  • 逻辑
    • 读取 2 个字节,表示类的访问权限和属性。
2.5 读取类和父类索引
self.thisClass = reader.readUint16()
self.superClass = reader.readUint16()
  • 功能:读取当前类和父类在常量池中的索引。
  • 逻辑
    • 读取 2 个字节,表示当前类的索引。
    • 读取 2 个字节,表示父类的索引。
2.6 读取接口索引表
self.interfaces = reader.readUint16s()
  • 功能:读取实现的接口在常量池中的索引表。
  • 逻辑
    • 读取接口数量,然后读取每个接口的索引。
2.7 读取字段表和方法表
self.fields = readMembers(reader, self.constantPool)
self.methods = readMembers(reader, self.constantPool)
  • 功能:读取字段表和方法表。
  • 逻辑
    • 调用 readMembers 函数,读取字段和方法信息。
2.8 读取属性表
self.attributes = readAttributes(reader, self.constantPool)
  • 功能:读取属性表。
  • 逻辑
    • 调用 readAttributes 函数,读取类的附加信息。

3. 重点
  • 功能:解析 Class 文件的核心逻辑是按照 Class 文件的结构逐步读取字节数据,并将其解析为对应的数据结构。
  • 关键点
    • 按照魔数、版本号、常量池、访问标志、类索引、父类索引、接口索引表、字段表、方法表和属性表的顺序解析。
    • 字符串解析是解析过程中的重要环节,用于获取类名、字段名、方法名等信息。
  • 适用场景:解析 Class 文件,用于 JVM 实现或类文件分析工具。