当前位置: 首页 > article >正文

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

动手实现自己的 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 实现或类文件分析工具。

http://www.kler.cn/a/551251.html

相关文章:

  • 烧烤炉出口亚马逊欧盟站CE认证EN1860安全标准
  • Golang的并发编程案例详解
  • LeetCode1287
  • 【SpringBoot苍穹外卖】debugDay04
  • TikTok 多账号管理与自动化运营:矩阵系统功能全解析
  • 国产编辑器EverEdit - 括号匹配检查
  • Unity DeepSeek API 聊天接入教程(0基础教学)
  • 国产编辑器EverEdit - “切换文件类型”的使用场景
  • 使用DeepSeek+本地知识库,尝试从0到1搭建高度定制化工作流(数据分析篇)
  • PiscTrace:让计算机视觉变得简单而高效
  • Oracle视图(基本使用)
  • 解决 `pip install open-webui` 时的编译错误:Microsoft Visual C++ 14.0 或更高版本缺失
  • PHP关键字入门指南:分类与功能全解析
  • Linux /dev/null
  • Java 并发编程知识点
  • Avalonia-wpf介绍
  • 汽车迷你Fakra连接器市场报告:未来几年年复合增长率CAGR为21.3%
  • 零基础学QT、C++(二)QT连接数据库
  • [Windows] Win7也能控制安卓手机屏幕(手机镜像投屏):scrcpy
  • 程序人生-Hello’s P2P