Golang实现JAVA虚拟机-解析class文件
原文链接:https://gaoyubo.cn/blogs/de1bedad.html
前言
所需前置知识为:JAVA语言、JVM知识、Go笔记
对应项目:jvmgo
一、准备环境
操作系统:Windows 11
1.1 JDK版本
openjdk version "1.8.0_382"
1.2 Go版本
go version go1.21.0 windows/amd64
1.3 配置Go工作空间
1.4 java命令指示
Java虚拟机的工作是运行Java应用程序。和其他类型的应用程序一样,Java应用程序也需要一个入口点,这个入口点就是我们熟知的main()
方法。最简单的Java程序是 只有一个main()
方法的类,如著名的HelloWorld程序。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
JVM如何知道从哪个类启动呢,虚拟机规范并没有明确,而是需要虚拟机实现。比如Oracle的JVM就是通过java
命令启动的,主类名由命令行参数决定。
java命令有如下4种形式:
java [-options] class [args]
java [-options] -jar jarfile [args]
javaw [-options] class [args]
javaw [-options] -jar jarfile [args]
可以向java
命令传递三组参数:选项、主类名(或者JAR文件名) 和main()
方法参数。选项由减号(–)开头。通常,第一个非选项参数 给出主类的完全限定名(fully qualified class name)。但是如果用户提供了–jar选项,则第一个非选项参数表示JAR文件名,java
命令必须从这个JAR文件中寻找主类。javaw
命令和java命令几乎一样,唯 一的差别在于,javaw
命令不显示命令行窗口,因此特别适合用于启 动GUI(图形用户界面)应用程序。
选项可以分为两类:标准选项和非标准选项。标准选项比较稳定,不会轻易变动。非标准选项以-X开头,
选项 | 用途 |
---|---|
-version | 输出版本信息,然后退出 |
-? / -help | 输出帮助信息,然后退出 |
-cp / -classpath | 指定用户类路径 |
-Dproperty=value | 设置Java系统属性 |
-Xms<size> | 设置初始堆空间 大小 |
-Xmx<size> | 设置最大堆空间 大小 |
-Xss<size> | 设置线程栈空间 大小 |
二、编写命令行工具
环境准备完毕,接下来实现java
命令的的第一种用法。
2.1 创建目录
创建cmd.go
Go源文件一般以.go作为后缀,文件名全部小写,多个单词之间使用下划线分隔。Go语言规范要求Go源文件必须使用UTF-8编码,详见https://golang.org/ref/spec
2.2 结构体存储cmd选项
在文件中定义cmd中java命令需要的选项和参数
package ch01
// author:郜宇博
type Cmd struct {
// 标注是否为 --help
helpFlag bool
//标注是否为 --version
versionFlag bool
//选项
cpOption string
//主类名,或者是jar文件
class string
//参数
args []string
}
Go语言标准库包
由于要处理的命令行,因此将使用到flag()
函数,此函数为Go的标准库包之一。
Go语言的标准库以包的方式提供支持,下表列出了Go语言标准库中常见的包及其功能。
Go语言标准库包名 | 功 能 |
---|---|
bufio | 带缓冲的 I/O 操作 |
bytes | 实现字节操作 |
container | 封装堆、列表和环形列表等容器 |
crypto | 加密算法 |
database | 数据库驱动和接口 |
debug | 各种调试文件格式访问及调试功能 |
encoding | 常见算法如 JSON、XML、Base64 等 |
flag | 命令行解析 |
fmt | 格式化操作 |
go | Go语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改 |
html | HTML 转义及模板系统 |
image | 常见图形格式的访问及生成 |
io | 实现 I/O 原始访问接口及访问封装 |
math | 数学库 |
net | 网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等 |
os | 操作系统平台不依赖平台操作封装 |
path | 兼容各操作系统的路径操作实用函数 |
plugin | Go 1.7 加入的插件系统。支持将代码编译为插件,按需加载 |
reflect | 语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值 |
regexp | 正则表达式封装 |
runtime | 运行时接口 |
sort | 排序接口 |
strings | 字符串转换、解析及实用函数 |
time | 时间接口 |
text | 文本模板及 Token 词法器 |
flag()函数
[]: https://studygolang.com/pkgdoc
eg:flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
var name string
var age int
var married bool
var delay time.Duration
flag.StringVar(&name, "name", "张三", "姓名")
flag.IntVar(&age, "age", 18, "年龄")
flag.BoolVar(&married, "married", false, "婚否")
flag.DurationVar(&delay, "d", 0, "时间间隔")
2.3 接收处理用户输入的命令行指令
创建parseCmd()函数,实现接受处理用户输入的命令行指令
func parseCmd() *Cmd {
cmd := &Cmd{}
flag.Usage = printUsage
flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
flag.Parse()
args := flag.Args()
if len(args) > 0 {
cmd.class = args[0]
cmd.args = args[1:]
}
return cmd
}
func printUsage() {
fmt.Printf("Usage: %s [-options] class [args...]\n", os.Args[0])
//flag.PrintDefaults()
}
首先设置flag.Usage
变量,把printUsage()
函数赋值给它;
然后调 用flag包提供的各种Var函数设置需要解析的选项;
接着调用 Parse()
函数解析选项。
如果Parse()
函数解析失败,它就调用 printUsage()
函数把命令的用法打印到控制台。
如果解析成功,调用flag.Args()
函数可以捕获其他没有被解析 的参数。其中第一个参数就是主类名,剩下的是要传递给主类的参数。
2.4 测试
与cmd.go文件一样,main.go文件的包名也是main。在Go 语言中,main是一个特殊的包,这个包所在的目录(可以叫作任何 名字)会被编译为可执行文件。Go程序的入口也是main()函数,但 是不接收任何参数,也不能有返回值。
测试代码如下:
package main
import "fmt"
func main() {
cmd := parseCmd()
if cmd.versionFlag {
//模拟输出版本
fmt.Println("version 0.0.1")
} else if cmd.helpFlag || cmd.class == "" {
printUsage()
} else {
startJVM(cmd)
}
}
// 模拟启动JVM
func startJVM(cmd *Cmd) {
fmt.Printf("classpath:%s class:%s args:%v\n",
cmd.cpOption, cmd.class, cmd.args)
}
main()
函数先调用ParseCommand()
函数解析命令行参数,如 果一切正常,则调用startJVM()
函数启动Java虚拟机。如果解析出现错误,或者用户输入了-help选项,则调用PrintUsage()
函数打印出帮助信息。如果用户输入了-version
选项,则输版本信息。因为我们还没有真正开始编写Java虚拟机,所以startJVM()
函数暂时只是打印一些信息而已。
在终端:
go install jvmgo\ch0
此时在工作空间的bin目录中会生成ch01.exe的文件,运行:结果如下
三、获取类路径
已经完成了JAVA应用程序如何启动:命令行启动,并获取到了启动时需要的选项和参数。
但是,如果要启动一个最简单的“Hello World”程序(如下),也需要加载很多所需的类进入JVM
中
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
加载HelloWorld类之前,需要加载该类的父类(超类),也就是java.lang.Object
,main函数的参数为String[]
类型,因此也需要将java.lang.String
类和java.lang.String[]
加载,输出字符串又需要加载java.lang.System
类,等等。接下来就来解决如何获取这些类的路径。
3.1类路径介绍
Java虚拟机规范并没有规定虚拟机应该从哪里寻找类,因此不同的虚拟机实现可以采用不同的方法。
Oracle的Java虚拟机实现根据类路径(class path)来搜索类。
按照搜索的先后顺序,类路径可以 分为以下3个部分:
- 启动类路径(bootstrap classpath)
- 扩展类路径(extension classpath)
- 用户类路径(user classpath)
启动类路径默认对应jre\lib
目录,Java标准库(大部分在rt.jar
里) 位于该路径。
扩展类路径默认对应jre\lib\ext
目录,使用Java扩展机制的类位于这个路径。
用户类路径为自己实现的类,以及第三方类库的路径。可以通过-Xbootclasspath
选项修改启动类路径,不过一般不需要这样做。
用户类路径的默认值是当前目录,也就是.
。可以设置 CLASSPATH环境变量来修改用户类路径,但是这样做不够灵活,所以不推荐使用。
更好的办法是给java命令传递-classpath
(或简写为-cp
)选项。-classpath/-cp
选项的优先级更高,可以覆盖CLASSPATH环境变量
设置。如下:
java -cp path\to\classes ...
java -cp path\to\lib1.jar ...
java -cp path\to\lib2.zip ...
3.2解析用户类路径
该功能建立在命令行工具上,因此复制上次的代码,并创建classpath子目录。
Java虚拟机将使用JDK的启动类路径来寻找和加载Java 标准库中的类,因此需要某种方式指定jre目录的位置。
命令行选项可以获取,所以增加一个非标准选项-Xjre。
修改Cmd结构体,添加XjreOption字段;parseCmd()函数也要相应修改:
type Cmd struct {
// 标注是否为 --help
helpFlag bool
//标注是否为 --version
versionFlag bool
//选项
cpOption string
//主类名,或者是jar文件
class string
//参数
args []string
// jre路径
XjreOption string
}
func parseCmd() *Cmd {
cmd := &Cmd{}
flag.Usage = printUsage
flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre")
flag.Parse()
args := flag.Args()
if len(args) > 0 {
//第一个参数是主类名
cmd.class = args[0]
cmd.args = args[1:]
}
return cmd
}
3.3获取用户类路径
可以把类路径想象成一个大的整体,它由启动类路径、扩展类路径和用户类路径三个小路径构成。
三个小路径又分别由更小的路径构成。是不是很像组合模式(composite pattern)
?
接下来将使用组合模式来设计和实现类路径。
1.Entry接口
定义一个Entry
接口,作为所有类的基准。
package classpath
import "os"
// :(linux/unix) or ;(windows)
const pathListSeparator = string(os.PathListSeparator)
type Entry interface {
// className: fully/qualified/ClassName.class
readClass(classpath string) ([]byte, Entry, error)
String() string
}
常量pathListSeparator是string类型,存放路径分隔符,后面会用到。
Entry接口中有个两方法。
-
readClass()方法:负责寻找和加载class 文件。
参数是class文件的相对路径,路径之间用斜线/
分隔,文件名有.class
后缀。比如要读取java.lang.Object
类,传 入的参数应该是java/lang/Object.class
。返回值是读取到的字节数据、最终定位到class文件的Entry,以及错误信息。 -
String()方法:作用相当于Java中的
toString()
,用于返回变量 的字符串表示。
Go的函数或方法允许返回多个值,按照惯例,可以使用最后一个返回值作为错误信息。
还需要一个类似于JAVA构造函数的函数,但在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命令,表示"构造函数"
newEntry()函数根据参数创建不同类型的Entry实例,代码如下:
func newEntry(path string) Entry {
////如果路径包含分隔符 表示有多个文件
if strings.Contains(path, pathListSeparator) {
return newCompositeEntry(path)
}
//包含*,则说明要将相应目录下的所有class文件加载
if strings.HasSuffix(path, "*") {
return newWildcardEntry(path)
}
//包含.jar,则说明是jar文件,通过zip方式加载
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {
return newZipEntry(path)
}
return newDirEntry(path)
}
2.实现类
存在四种类路径指定方式:
- 普通路径形式:gyb/gyb
- jar/zip形式:/gyb/gyb.jar
- 通配符形式:gyb/*
- 多个路径形式:gyb/1:/gyb/2
DirEntry(普通形式)
创建entry_dir.go
,定义DirEntry结构体:
package classpath
import "io/ioutil"
import "path/filepath"
type DirEntry struct {
absDir string
}
func newDirEntry(path string) *DirEntry {
//转化为绝对路径
absDir, err := filepath.Abs(path)
if err != nil {
panic(err)
}
return &DirEntry{absDir}
}
func (self *DirEntry) readClass(className string) ([]byte, Entry, error) {
//拼接类文件目录 和 类文件名
// '/gyb/xxx/' + 'helloworld.class' = '/gyb/xxx/helloworld.class'
fileName := filepath.Join(self.absDir, className)
data, err := ioutil.ReadFile(fileName)
return data, self, err
}
func (self *DirEntry) String() string {
return self.absDir
}
DirEntry
只有一个字段,用于存放目录的绝对路径。
和Java语言不同,Go结构体不需要显示实现接口,只要方法匹配即可。
ZipEntry(jar/zip形式)
package classpath
import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"
type ZipEntry struct {
absPath string
}
func newZipEntry(path string) *ZipEntry {
absPath, err := filepath.Abs(path)
if err != nil {
panic(err)
}
return &ZipEntry{absPath}
}
func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
r, err := zip.OpenReader(self.absPath)
if err != nil {
return nil, nil, err
}
defer r.Close()
for _, f := range r.File {
if f.Name == className {
rc, err := f.Open()
if err != nil {
return nil, nil, err
}
defer rc.Close()
data, err := ioutil.ReadAll(rc)
if err != nil {
return nil, nil, err
}
return data, self, nil
}
}
return nil, nil, errors.New("class not found: " + className)
}
func (self *ZipEntry) String() string {
return self.absPath
}
首先打开ZIP文件,如果这一步出错的话,直接返回。然后遍历 ZIP压缩包里的文件,看能否找到class文件。如果能找到,则打开 class文件,把内容读取出来,并返回。如果找不到,或者出现其他错 误,则返回错误信息。有两处使用了defer语句来确保打开的文件得 以关闭。
CompositeEntry(多路径形式)
CompositeEntry
由更小的Entry
组成,正好可以表示成[]Entry。
在Go语言中,数组属于比较低层的数据结构,很少直接使用。大部分情况下,使用更便利的slice类型。
构造函数把参数(路径列表)按分隔符分成小路径,然后把每个小路径都转换成具体的 Entry实例。
package classpath
import "errors"
import "strings"
type CompositeEntry []Entry
func newCompositeEntry(pathList string) CompositeEntry {
compositeEntry := []Entry{}
for _, path := range strings.Split(pathList, pathListSeparator) {
//去判断 path 属于哪其他三种哪一种情况 生成对应的 ClassDirEntry类目录对象
entry := newEntry(path)
compositeEntry = append(compositeEntry, entry)
}
return compositeEntry
}
func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) {
//遍历切片 中的 类目录对象
for _, entry := range self {
//如果找到了 对应的 类 直接返回
data, from, err := entry.readClass(className)
if err == nil {
return data, from, nil
}
}
//没找到 返回错误
return nil, nil, errors.New("class not found: " + className)
}
func (self CompositeEntry) String() string {
strs := make([]string, len(self))
for i, entry := range self {
strs[i] = entry.String()
}
return strings.Join(strs, pathListSeparator)
}
WildcardEntry(通配符形式)
WildcardEntry
实际上也是CompositeEntry
,所以就不再定义新的类型了。
首先把路径末尾的星号去掉,得到baseDir,然后调用filepath
包的Walk()
函数遍历baseDir创建ZipEntry
。Walk()
函数的第二个参数 也是一个函数。
在walkFn
中,根据后缀名选出JAR文件
,并且返回SkipDir跳过子目录(通配符类路径不能递归匹配子目录下的JAR文件)。
package classpath
import "os"
import "path/filepath"
import "strings"
func newWildcardEntry(path string) CompositeEntry {
//截取通用匹配符 /gyb/* 截取掉 *
baseDir := path[:len(path)-1] // remove *
//多个 类目录对象
compositeEntry := []Entry{}
walkFn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
//如果为空
if info.IsDir() && path != baseDir {
return filepath.SkipDir
}
//如果是 .jar 或者 .JAR 结尾的文件
if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
jarEntry := newZipEntry(path)
compositeEntry = append(compositeEntry, jarEntry)
}
return nil
}
//遍历 目录下所有 .jar .JAR 文件 生成ZipEntry目录对象 放在切片中返回
//walFn为函数
filepath.Walk(baseDir, walkFn)
return compositeEntry
}
3.4实现类目录
前面提到了 java 虚拟机
默认 会先从启动路径--->扩展类路径 --->用户类路径
按顺序依次去寻找,加载类。
那么就会有3个类目录对象,所以就要定义一个结构体去存放它。
type Classpath struct {
BootClasspath Entry
ExtClasspath Entry
UserClasspath Entry
}
启动类路径
启动路径,其实对应Jre
目录下``lib` 也就是运行java 程序必须可少的基本运行库。
通过 -Xjre
指定 如果不指定 会在当前路径下寻找jre 如果找不到 就会从我们在装java是配置的JAVA_HOME
环境变量 中去寻找。
所以获取验证环境变量的方法如下:
func getJreDir(jreOption string) string {
//如果 从cmd -Xjre 获取到目录 并且存在
if jreOption != "" && exists(jreOption) {
//返回目录
return jreOption
}
//如果 当前路径下 有 jre 返回目录
if exists("./jre") {
return "./jre"
}
//如果 上面都找不到 到系统环境 变量中寻找
if jh := os.Getenv("JAVA_HOME"); jh != "" {
//存在 就返回
return filepath.Join(jh, "jre")
}
//都找不到 就报错
panic("Can not find jre folder!")
}
//判断 目录是否存在
func exists(path string) bool {
if _, err := os.Stat(path); err != nil {
if os.IsNotExist(err) { return false
} }
return true }
扩展类路径
扩展类 路径一般 在启动路径 的子目录下 jre/lib/ext
func (self *Classpath) parseBootAndExtClasspath(jreOption string) {
jreDir := getJreDir(jreOption)
// 拼接成jre 的路径
// jre/lib/*
jreLibPath := filepath.Join(jreDir, "lib", "*")
//加载 所有底下的 jar包
self.BootClasspath = newWildcardEntry(jreLibPath)
// 拼接 扩展类 的路径
// jre/lib/ext/*
jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
//加载 所有底下的jar包
self.ExtClasspath = newWildcardEntry(jreExtPath)
}
用户类路径
用户类路径通过前面提到的 -classpath 来指定 ,如果没有指定 就默认为当前路径就好
func (self *Classpath) parseUserClasspath(cpOption string) {
//如果没有指定
if cpOption == "" {
// . 作为当前路径
cpOption = "."
}
//创建 类目录对象
self.UserClasspath = newEntry(cpOption)
}
实现类的加载
对于指定文件类名取查找 我们是按前面提到的(启动路径--->扩展类路径 --->用户类路径
按顺序依次去寻找,加载类),没找到就挨个查找下去。
如果用户没有提供-classpath/-cp
选项,则使用当前目录作为用 户类路径。ReadClass()
方法依次从启动类路径、扩展类路径和用户 类路径中搜索class文件,
//根据类名 分别从 bootClasspath,extClasspath,userClasspath 依次加载类目录
func (self *Classpath) ReadClass(className string) ([]byte, ClassDirEntry, error) {
className = className + ".class"
if data, entry, err := self.BootClasspath.readClass(className); err == nil{ return data, entry, err }
if data, entry, err := self.ExtClasspath.readClass(className); err == nil { return data, entry, err }
return self.UserClasspath.readClass(className)
}
初始化类加载目录
定义一个初始化函数,来作为初始函数,执行后生成一个 Classpath对象。
//jreOption 为启动类目录 cpOption 为 用户指定类目录 从cmd 命令行 中解析获取
func InitClassPath(jreOption, cpOption string) *Classpath {
cp := &Classpath{}
//初始化 启动类目录
cp.parseBootAndExtClasspath(jreOption)
//初始化 用户类目录
cp.parseUserClasspath(cpOption)
return cp
}
注意,传递给ReadClass()方法的类名不包含“.class”后缀。
3.5总结
3.6测试
成功获取到class文件!
四、解析Class文件
4.1 class文件介绍
作为类/接口信息的载体,每一个class文件
都完整的定义了一个类,为了使Java程序可以实现“编写一次,处处运行”,java虚拟机对class文件的格式进行了严格的规范。
但是对于从哪里加载class文件
,给予了高度自由空间:第三节中说过,可以从文件系统读取jar/zip文件
中的class文件
,除此之外,也可以从网络下载,甚至是直接在运行中生成class文件
。
构成class文件
的基本数据单位是字节,可以把整个class文件当 成一个字节流来处理。稍大一些的数据由连续多个字节构成,这些数据在class文件中以大端(big-endian)
方式存储。
为了描述class文件格式,Java虚拟机规范定义了u1
、u2
和u4
三种数据类型来表示1、 2和4字节无符号整数,分别对应Go语言的uint8
、uint16
和uint32
类型。
相同类型的多条数据一般按表(table)
的形式存储在class文件中。表由表头
和表项(item)
构成,表头是u2或u4整数。假设表头是 n,后面就紧跟着n个表项数据。
Java虚拟机规范使用一种类似C语言的结构体语法来描述class 文件格式。整个class文件被描述为一个ClassFile结构,代码如下:
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
!表示大小不定。
4.2解析class文件
Go语言内置了丰富的数据类型,非常适合处理class文件。
如下为Go和Java语言基本数据类型对照关系:
Go语言类型 | Java语言类型 | 说明 |
---|---|---|
int8 | byte | 8比特有符号整数 |
uint8(别名byte) | N/A | 8比特无符号整数 |
int16 | short | 16比特有符号整数 |
uint16 | char | 16比特无符号整数 |
int32(别名rune) | int | 32比特有符号整数 |
uint32 | N/A | 32比特无符号整数 |
int64 | long | 64比特有符号整数 |
uint64 | N/A | 64比特无符号整数 |
float32 | float | 32比特IEEE-754浮点数 |
float64 | double | 64比特IEEE-754浮点数 |
4.2.1读取数据
解析class文件
的第一步是从里面读取数据。虽然可以把class文件
当成字节流来处理,但是直接操作字节很不方便,所以先定义一个结构体ClassReader
来帮助读取数据,创建class_reader.go。
package classfile
import "encoding/binary"
type ClassReader struct {
data []byte
}
func (self *ClassReader) readUint8() uint8 {...} // u1
func (self *ClassReader) readUint16() uint16 {...} // u2
func (self *ClassReader) readUint32() uint32 {...} // u4
func (self *ClassReader) readUint64() uint64 {...}
func (self *ClassReader) readUint16s() []uint16 {...}
func (self *ClassReader) readBytes(length uint32) []byte {...}
ClassReader
只是[]byte
类型的包装而已。readUint8()
读取u1
类型数据。
ClassReader
并没有使用索引记录数据位置,而是使用Go 语言的reslice语法
跳过已经读取的数据
实现代码如下:
// u1
func (self *ClassReader) readUint8() uint8 {
val := self.data[0]
self.data = self.data[1:]
return val
}
// u2
func (self *ClassReader) readUint16() uint16 {
val := binary.BigEndian.Uint16(self.data)
self.data = self.data[2:]
return val
}
// u4
func (self *ClassReader) readUint32() uint32 {
val := binary.BigEndian.Uint32(self.data)
self.data = self.data[4:]
return val
}
func (self *ClassReader) readUint64() uint64 {
val := binary.BigEndian.Uint64(self.data)
self.data = self.data[8:]
return val
}
func (self *ClassReader) readUint16s() []uint16 {
n := self.readUint16()
s := make([]uint16, n)
for i := range s {
s[i] = self.readUint16()
}
return s
}
func (self *ClassReader) readBytes(n uint32) []byte {
bytes := self.data[:n]
self.data = self.data[n:]
return bytes
}
Go标准库encoding/binary包中定义了一个变量BigEndian
,可以从[]byte
中解码多字节数据。
4.2.2解析整体结构
有了ClassReader,可以开始解析class文件了。创建class_file.go文件,在其中定义ClassFile结构体
,与4.1中的class文件中字段对应。
package classfile
import "fmt"
type ClassFile struct {
//magic uint32
minorVersion uint16
majorVersion uint16
constantPool ConstantPool
accessFlags uint16
thisClass uint16
superClass uint16
interfaces []uint16
fields []*MemberInfo
methods []*MemberInfo
attributes []AttributeInfo
}
在class_file.go文件中实现一系列函数和方法。
func Parse(classData []byte) (cf *ClassFile, err error) {...}
func (self *ClassFile) read(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {...}
func (self *ClassFile) readAndCheckVersion(reader *ClassReader) {...}
func (self *ClassFile) MinorVersion() uint16 {...} // getter
func (self *ClassFile) MajorVersion() uint16 {...} // getter
func (self *ClassFile) ConstantPool() ConstantPool {...} // getter
func (self *ClassFile) AccessFlags() uint16 {...} // getter
func (self *ClassFile) Fields() []*MemberInfo {...} // getter
func (self *ClassFile) Methods() []*MemberInfo {...} // getter
func (self *ClassFile) ClassName() string {...}
func (self *ClassFile) SuperClassName() string {...}
func (self *ClassFile) InterfaceNames() []string {...}
相比Java语言,Go的访问控制非常简单:只有公开和私有两种。
所有首字母大写的类型、结构体、字段、变量、函数、方法等都是公开的,可供其他包使用。
首字母小写则是私有的,只能在包内部使用。
解析[]byte
Parse()函数把[]byte解析成ClassFile结构体。
func Parse(classData []byte) (cf *ClassFile, err error) {
defer func() {
//尝试捕获 panic,并将其存储在变量 r 中。如果没有发生 panic,r 将为 nil。
if r := recover(); r != nil {
var ok bool
//判断 r 是否是一个 error 类型
err, ok = r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
}
}()
cr := &ClassReader{classData}
cf = &ClassFile{}
cf.read(cr)
return
}
顺序解析
read() 方法依次调用其他方法解析class文件,顺序一定要保证正确,与class文件相对应。
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)
}
self.readAndCheckMagic(reader)
: 这是一个ClassFile
结构的方法,用于读取并检查类文件的魔数。魔数是类文件的标识符,用于确定文件是否为有效的类文件。self.readAndCheckVersion(reader)
: 这个方法用于读取并检查类文件的版本号。Java类文件具有版本号,标识了它们的Java编译器版本。这里会对版本号进行检查。self.constantPool = readConstantPool(reader)
: 这一行代码调用readConstantPool
函数来读取常量池部分的数据,并将其存储在ClassFile
结构的constantPool
字段中。常量池是一个包含各种常量信息的表格,用于支持类文件中的各种符号引用。self.accessFlags = reader.readUint16()
: 这一行代码读取类的访问标志,它标识类的访问权限,例如public
、private
等。self.thisClass = reader.readUint16()
: 这行代码读取指向当前类在常量池中的索引,表示当前类的类名。self.superClass = reader.readUint16()
: 这行代码读取指向父类在常量池中的索引,表示当前类的父类名。self.interfaces = reader.readUint16s()
: 这行代码读取接口表的数据,表示当前类实现的接口。self.fields = readMembers(reader, self.constantPool)
: 这行代码调用readMembers
函数,以读取类的字段信息,并将它们存储在fields
字段中。字段包括类的成员变量。self.methods = readMembers(reader, self.constantPool)
: 这行代码类似于上一行,但它读取类的方法信息,并将它们存储在methods
字段中。self.attributes = readAttributes(reader, self.constantPool)
: 最后,这行代码调用readAttributes
函数,以读取类的属性信息,并将它们存储在attributes
字段中。属性包括类级别的注解、源码文件等信息。
以下均为类似于Java的getter方法,以后将不再赘述。
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
}
ClassName从常量池中获取,SuperClassName同理,常量池还未实现。
所有类的超类(父类),Object是java中唯一没有父类的类,一个类可以不是Object的直接子类,但一定是继承于Object并拓展于Object。
func (self *ClassFile) ClassName() string {
return self.constantPool.getClassName(self.thisClass)
}
func (self *ClassFile) SuperClassName() string {
if self.superClass > 0 {
return self.constantPool.getClassName(self.superClass)
}
//Object类
return ""
}
Java的类是单继承,多实现的,因此获取接口应该使用循环,也从常量池中获取。
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
}
解析魔数
很多文件格式都会规定满足该格式的文件必须以某几个固定字节开头,这几个字节主要起标识作用,叫作魔数(magic number)
。
- PDF文件以4字节“%PDF”(0x25、0x50、0x44、0x46)开头
- ZIP 文件以2字节“PK”(0x50、0x4B)开头
- class文件的魔数 是“0xCAFEBABE” 。
因此readAndCheckMagic()方法的代码如下。
func (self *ClassFile) readAndCheckMagic(reader *ClassReader) {
magic := reader.readUint32()
if magic != 0xCAFEBABE {
panic("java.lang.ClassFormatError: magic!")
}
}
Java虚拟机规范规定,如果加载的class文件不符合要求的格式,Java虚拟机实现就抛出java.lang.ClassFormatError异常。
但是因为我们才刚刚开始编写虚拟机,还无法抛出异常,所以暂时先调用 panic()方法终止程序执行。
版本号
解析版本号
魔数之后是class文件的次版本号和主版本号,都是u2类型
。
假设某class文件的主版本号是M,次版本号是m,那么完整的版本号 可以表示成M.m
的形式。
次版本号只在J2SE 1.2
之前用过,从1.2 开始基本上就没什么用了(都是0)。
主版本号在J2SE 1.2之前是45, 从1.2开始,每次有大的Java版本发布,都会加1。
Java 版本 | 类文件版本号 |
---|---|
Java 1.1 | 45.3 |
Java 1.2 | 46.0 |
Java 1.3 | 47.0 |
Java 1.4 | 48.0 |
Java 5 | 49.0 |
Java 6 | 50.0 |
Java 7 | 51.0 |
Java 8 | 52.0 |
特定的Java虚拟机实现只能支持版本号在某个范围内的class文 件。
Oracle的实现是完全向后兼容的,比如Java SE 8
支持版本号为 45.0~52.0的class文件。
如果版本号不在支持的范围内,Java虚拟机 实现就抛出java.lang.UnsupportedClassVersionError
异常。参考 Java 8,支持版本号为45.0~52.0的class文件。如果遇到其他版本号, 调用panic()
方法终止程序执行。
如下为检查版本号代码:
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!")
}
解析类访问标识
版本号之后是常量池,但是由于常量池比较复杂,所以放到4.3 节介绍。
常量池之后是类访问标志,这是一个16位的bitmask
,指出class文件定义的是类还是接口,访问级别是public
还是private
,等等。
本章只对class文件进行初步解析,并不做完整验证,所以只是读取类访问标志以备后用。
ClassFileTest的类访问标志为:0X21
:
解析类和父类索引
类访问标志之后是两个u2类型的常量池索引,分别给出类名和超类名。
class文件存储的类名类似完全限定名,但是把点换成了 斜线,Java语言规范把这种名字叫作二进制名binary names
。
因为每个类都有名字,所以thisClass
必须是有效的常量池索引。
除 java.lang.Object
之外,其他类都有超类,所以superClass
只在 Object.class
中是0,在其他class文件中必须是有效的常量池索引。如下,ClassFileTest的类索引是5,超类索引是6。
解析接口索引表
类和超类索引后面是接口索引表,表中存放的也是常量池索引,给出该类实现的所有接口的名字。ClassFileTest没有实现接口, 所以接口表是空的
解析字段和方法表
接口索引表之后是字段表和方法表,分别存储字段和方法信息。
字段和方法的基本结构大致相同,差别仅在于属性表。
下面是 Java虚拟机规范给出的字段结构定义
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
和类一样,字段和方法也有自己的访问标志。访问标志之后是一个常量池索引,给出字段名或方法名,然后又是一个常量池索引,给出字段或方法的描述符,最后是属性表。
为了避免重复代 码,用一个结构体统一表示字段和方法。
package classfile
type MemberInfo struct {
cp ConstantPool
accessFlags uint16
nameIndex uint16
descriptorIndex uint16
attributes []AttributeInfo
}
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {...}
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {...}
func (self *MemberInfo) AccessFlags() uint16 {...} // getter
func (self *MemberInfo) Name() string {...}
func (self *MemberInfo) Descriptor() string {...}
cp字段保存常量池指针,后面会用到它。readMembers()读取字段表或方法表,代码如下:
func readMembers(reader *ClassReader, cp ConstantPool) []*MemberInfo {
memberCount := reader.readUint16()
members := make([]*MemberInfo, memberCount)
for i := range members {
members[i] = readMember(reader, cp)
}
return members
}
readMember()函数读取字段或方法数据。
func readMember(reader *ClassReader, cp ConstantPool) *MemberInfo {
return &MemberInfo{
cp: cp,
accessFlags: reader.readUint16(),
nameIndex: reader.readUint16(),
descriptorIndex: reader.readUint16(),
attributes: readAttributes(reader, cp),
}
}
Name()从常量 池查找字段或方法名,Descriptor()从常量池查找字段或方法描述 符
func (self *MemberInfo) Name() string {
return self.cp.getUtf8(self.nameIndex)
}
func (self *MemberInfo) Descriptor() string {
return self.cp.getUtf8(self.descriptorIndex)
}
4.2.3解析常量池
常量池占据了class文件很大一部分数据,里面存放着各式各样的常量信息,包括数字和字符串常量、类和接口名、字段和方法名,等等
创建constant_pool.go
文件,里面定义 ConstantPool类型
。
package classfile
type ConstantPool []ConstantInfo
func readConstantPool(reader *ClassReader) ConstantPool {...}
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {...}
func (self ConstantPool) getNameAndType(index uint16) (string, string) {...}
func (self ConstantPool) getClassName(index uint16) string {...}
func (self ConstantPool) getUtf8(index uint16) string {...}
常量池实际上也是一个表,但是有三点需要特别注意。
表头给出的常量池大小比实际大1。假设表头给出的值是n,那么常量池的实际大小是n–1。
有效的常量池索引是1~n–1。0是无效索引,表示不指向任何常量。
CONSTANT_Long_info
和 CONSTANT_Double_info
各占两个位置。也就是说,如果常量池中存在这两种常量,实际的常量数量比n–1还要少,而且1~n–1的某些 数也会变成无效索引。
常量池由readConstantPool()
函数读取,代码如下:
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)
switch cp[i].(type) {
//占两个位置
case *ConstantLongInfo, *ConstantDoubleInfo:
i++
}
}
return cp
}
getConstantInfo()
方法按索引查找常量
func (self ConstantPool) getConstantInfo(index uint16) ConstantInfo {
if cpInfo := self[index]; cpInfo != nil {
return cpInfo
}
panic(fmt.Errorf("Invalid constant pool index: %v!", index))
}
getNameAndType()
方法从常量池查找字段或方法的名字和描述符
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
}
getClassName()
方法从常量池查找类名
func (self ConstantPool) getClassName(index uint16) string {
classInfo := self.getConstantInfo(index).(*ConstantClassInfo)
return self.getUtf8(classInfo.nameIndex)
}
getUtf8()
方法从常量池查找UTF-8字符串
func (self ConstantPool) getUtf8(index uint16) string {
utf8Info := self.getConstantInfo(index).(*ConstantUtf8Info)
return utf8Info.str
}
ConstPool接口
由于常量池中存放的信息各不相同,所以每种常量的格式也不同。
常量数据的第一字节是tag,用来区分常量类型。
下面是Java 虚拟机规范给出的常量结构
cp_info {
u1 tag;
u1 info[];
}
Java虚拟机规范一共定义了14种常量。创建constant_info.go
文件,在其中定义tag常量值,代码如下:
package classfile
// Constant pool tags
const (
CONSTANT_Class = 7
CONSTANT_Fieldref = 9
CONSTANT_Methodref = 10
CONSTANT_InterfaceMethodref = 11
CONSTANT_String = 8
CONSTANT_Integer = 3
CONSTANT_Float = 4
CONSTANT_Long = 5
CONSTANT_Double = 6
CONSTANT_NameAndType = 12
CONSTANT_Utf8 = 1
CONSTANT_MethodHandle = 15
CONSTANT_MethodType = 16
CONSTANT_InvokeDynamic = 18
)
定义ConstantInfo接口来表示常量信息
type ConstantInfo interface {
readInfo(reader *ClassReader)
}
//读取常量信息
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {...}
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {...}
readInfo()方法读取常量信息,需要由具体的常量结构体实现。 readConstantInfo()函数先读出tag值,然后调用newConstantInfo()函数创建具体的常量,最后调用常量的readInfo()方法读取常量信息, 代码如下:
func readConstantInfo(reader *ClassReader, cp ConstantPool) ConstantInfo {
tag := reader.readUint8()
c := newConstantInfo(tag, cp)
c.readInfo(reader)
return c
}
newConstantInfo()根据tag值创建具体的常量,代码如下:
func newConstantInfo(tag uint8, cp ConstantPool) ConstantInfo {
switch tag {
case CONSTANT_Integer:
return &ConstantIntegerInfo{}
case CONSTANT_Float:
return &ConstantFloatInfo{}
case CONSTANT_Long:
return &ConstantLongInfo{}
case CONSTANT_Double:
return &ConstantDoubleInfo{}
case CONSTANT_Utf8:
return &ConstantUtf8Info{}
case CONSTANT_String:
return &ConstantStringInfo{cp: cp}
case CONSTANT_Class:
return &ConstantClassInfo{cp: cp}
case CONSTANT_Fieldref:
return &ConstantFieldrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_Methodref:
return &ConstantMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_InterfaceMethodref:
return &ConstantInterfaceMethodrefInfo{ConstantMemberrefInfo{cp: cp}}
case CONSTANT_NameAndType:
return &ConstantNameAndTypeInfo{}
case CONSTANT_MethodType:
return &ConstantMethodTypeInfo{}
case CONSTANT_MethodHandle:
return &ConstantMethodHandleInfo{}
case CONSTANT_InvokeDynamic:
return &ConstantInvokeDynamicInfo{}
default:
panic("java.lang.ClassFormatError: constant pool tag!")
}
}
CONSTANT_Integer_info
CONSTANT_Integer_info
使用4字节存储整数常量,其JVM结构定义如下:
CONSTANT_Integer_info {
u1 tag;
u4 bytes;
}
CONSTANT_Integer_info
和后面将要介绍的其他三种数字常量无论是结构,还是实现,都非常相似,所以把它们定义在同一个文件中。创建cp_numeric.go
文件,在其中定义 ConstantIntegerInfo结构体
,代码如下:
package classfile
import "math"
type ConstantIntegerInfo struct {
val int32
}
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {...}
readInfo()先读取一个uint32
数据,然后把它转型成int32
类型, 代码如下
func (self *ConstantIntegerInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = int32(bytes)
}
CONSTANT_Float_info
CONSTANT_Float_info
使用4字节存储IEEE754单精度浮点数
常量,JVM结构如下:
CONSTANT_Float_info {
u1 tag;
u4 bytes;
}
在cp_numeric.go
文件中定义ConstantFloatInfo结构体
,代码如下:
type ConstantFloatInfo struct {
val float32
}
func (self *ConstantFloatInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint32()
self.val = math.Float32frombits(bytes)
}
CONSTANT_Long_info
CONSTANT_Long_info
使用8字节存储整数常量,结构如下:
CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
在cp_numeric.go
文件中定义ConstantLongInfo结构体
,代码如下:
type ConstantLongInfo struct {
val int64
}
func (self *ConstantLongInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = int64(bytes)
}
CONSTANT_Double_info
最后一个数字常量是CONSTANT_Double_info
,使用8字节存储IEEE754双精度浮点数
,结构如下:
CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
在cp_numeric.go
文件中定义ConstantDoubleInfo
结构体,代码如下:
type ConstantDoubleInfo struct {
val float64
}
func (self *ConstantDoubleInfo) readInfo(reader *ClassReader) {
bytes := reader.readUint64()
self.val = math.Float64frombits(bytes)
}
CONSTANT_Utf8_info
CONSTANT_Utf8_info常量里放的是MUTF-8编码的字符串, 结构如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
Java类文件中使用MUTF-8(Modified UTF-8)编码而不是标准的UTF-8,是因为MUTF-8在某些方面更适合于在Java虚拟机内部处理字符串。以下是一些原因:
- 空字符的表示: 在标准的UTF-8编码中,空字符(U+0000)会使用单个字节0x00表示,这与C字符串中的字符串终止符相同,可能引起混淆。在MUTF-8中,空字符会使用0xC0 0x80来表示,避免了混淆。
- 编码长度: MUTF-8编码中的每个字符都使用1至3个字节来表示,这与UTF-8编码相比更紧凑。对于大多数常见的字符集,这可以减少存储和传输开销。
- 字符的编码范围: MUTF-8编码对字符的范围进行了限制,只包含Unicode BMP(基本多文种平面)范围内的字符。这些字符通常足够用于表示Java标识符和字符串文字。
- 兼容性: 早期版本的Java使用的是MUTF-8编码,因此为了保持与早期版本的兼容性,后续版本也继续使用MUTF-8。这有助于确保Java类文件的可互操作性。
创建cp_utf8.go
文件,在其中定义 ConstantUtf8Info结构体
,代码如下:
type ConstantUtf8Info struct {
str string
}
func (self *ConstantUtf8Info) readInfo(reader *ClassReader) {
length := uint32(reader.readUint16())
bytes := reader.readBytes(length)
self.str = decodeMUTF8(bytes)
}
Java序列化机制也使用了MUTF-8编码。
java.io.DataInput和 java.io.DataOutput接口分别定义了
readUTF()
和writeUTF()
方法,可以读写MUTF-8编码的字符串。
如下为简化版的java.io.DataInputStream.readUTF()
// mutf8 -> utf16 -> utf32 -> string
func decodeMUTF8(bytearr []byte) string {
utflen := len(bytearr)
chararr := make([]uint16, utflen)
var c, char2, char3 uint16
count := 0
chararr_count := 0
for count < utflen {
c = uint16(bytearr[count])
if c > 127 {
break
}
count++
chararr[chararr_count] = c
chararr_count++
}
for count < utflen {
c = uint16(bytearr[count])
switch c >> 4 {
case 0, 1, 2, 3, 4, 5, 6, 7:
/* 0xxxxxxx*/
count++
chararr[chararr_count] = c
chararr_count++
case 12, 13:
/* 110x xxxx 10xx xxxx*/
count += 2
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", count))
}
chararr[chararr_count] = c&0x1F<<6 | char2&0x3F
chararr_count++
case 14:
/* 1110 xxxx 10xx xxxx 10xx xxxx*/
count += 3
if count > utflen {
panic("malformed input: partial character at end")
}
char2 = uint16(bytearr[count-2])
char3 = uint16(bytearr[count-1])
if char2&0xC0 != 0x80 || char3&0xC0 != 0x80 {
panic(fmt.Errorf("malformed input around byte %v", (count - 1)))
}
chararr[chararr_count] = c&0x0F<<12 | char2&0x3F<<6 | char3&0x3F<<0
chararr_count++
default:
/* 10xx xxxx, 1111 xxxx */
panic(fmt.Errorf("malformed input around byte %v", count))
}
}
// The number of chars produced may be less than utflen
chararr = chararr[0:chararr_count]
runes := utf16.Decode(chararr)
return string(runes)
}
- 初始化
chararr
数组,用于存储UTF-16字符。 - 遍历MUTF-8字节数组中的字节,根据字节的值来判断字符的编码方式。
- 如果字节值小于128,表示ASCII字符,直接转换为UTF-16并存储。
- 如果字节值在特定范围内,表示多字节字符,需要根据UTF-8编码规则进行解码。
- 如果遇到不符合规则的字节,抛出异常来处理错误情况。
- 最后,将解码后的UTF-16字符转换为Go字符串。
CONSTANT_String_info
CONSTANT_String_info
常量表示java.lang.String
字面量,结构如下:
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
可以看到,CONSTANT_String_info
本身并不存放字符串数据
只存了常量池索引
,这个索引指向一个CONSTANT_Utf8_info常量
。
下创建cp_string.go
文件,在其中定义 ConstantStringInfo结构体
type ConstantStringInfo struct {
cp ConstantPool
stringIndex uint16
}
func (self *ConstantStringInfo) readInfo(reader *ClassReader) {
self.stringIndex = reader.readUint16()
}
String()
方法按索引从常量池中查找字符串:
func (self *ConstantStringInfo) String() string {
return self.cp.getUtf8(self.stringIndex)
}
CONSTANT_Class_info
CONSTANT_Class_info
常量表示类或者接口的符号引用
他是对类或者接口的符号引用。它描述的可以是当前类型的信息,也可以描述对当前类的引用,还可以描述对其他类的引用。JVM结构如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
和CONSTANT_String_info
类似,name_index
是常量池索引,指向CONSTANT_Utf8_info
常量。
创建 cp_class.go
文件,定义ConstantClassInfo结构体
type ConstantClassInfo struct {
cp ConstantPool
nameIndex uint16
}
func (self *ConstantClassInfo) readInfo(reader *ClassReader) {
self.nameIndex = reader.readUint16()
}
func (self *ConstantClassInfo) Name() string {
return self.cp.getUtf8(self.nameIndex)
}
CONSTANT_NameAndType_info
CONSTANT_NameAndType_info
给出字段或方法的名称和描述符。
CONSTANT_Class_info
和CONSTANT_NameAndType_info
加在 一起可以唯一确定一个字段或者方法。其结构如下:
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
字段或方法名由name_index
给出,字段或方法的描述符由 descriptor_index
给出。
name_index
和descriptor_index
都是常量池索引,指向CONSTANT_Utf8_info常量
。
Java虚拟机规范定义了一种简单的语法来描述字段和方法,可以根据下面的规则生成描述符。
一、类型描述符
- 基本类型byte、short、char、int、long、float和double的描述符是单个字母,分别对应B、S、C、I、J、F和D。注意,long的描述符是J 而不是L。
- 引用类型的描述符是L+类的完全限定名+分号。
- 数组类型的描述符是[+数组元素类型描述符
二、字段描述符
字段类型的描述符
三、方法描述符
分号分隔的参数类型描述符+返回值类型描述符,其中void返回值由单个字母V表示。
Java语言支持方法重载(override),不同的方法可 以有相同的名字,只要参数列表不同即可。
这就是为什么 CONSTANT_NameAndType_info结构要同时包含名称和描述符的原因。
创建cp_name_and_type.go
文件,在其中定义ConstantName-AndTypeInfo结构体
,代码如下:
type ConstantNameAndTypeInfo struct {
nameIndex uint16
descriptorIndex uint16
}
func (self *ConstantNameAndTypeInfo) readInfo(reader *ClassReader) {
self.nameIndex = reader.readUint16()
self.descriptorIndex = reader.readUint16()
}
CONSTANT_Fieldref_info、 CONSTANT_Methodref_info和 CONSTANT_InterfaceMethodref_info
CONSTANT_Fieldref_info
表示字段符号引用, CONSTANT_Methodref_info
表示普通(非接口)方法符号引用, CONSTANT_InterfaceMethodref_info
表示接口方法符号引用。这三种常量结构一模一样。
其中CONSTANT_Fieldref_info
的结构如下:
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
常量。
创建cp_member_ref.go
文件,定义一个统一的结构体ConstantMemberrefInfo
来表示这3种常量,然后定义三个结构体“继承”ConstantMemberrefInfo
。
Go语言并没有“继承”这个概念,但是可以通过结构体嵌套来模拟
type ConstantFieldrefInfo struct{ ConstantMemberrefInfo }
type ConstantMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantInterfaceMethodrefInfo struct{ ConstantMemberrefInfo }
type ConstantMemberrefInfo struct {
cp ConstantPool
classIndex uint16
nameAndTypeIndex uint16
}
func (self *ConstantMemberrefInfo) readInfo(reader *ClassReader) {
self.classIndex = reader.readUint16()
self.nameAndTypeIndex = reader.readUint16()
}
func (self *ConstantMemberrefInfo) ClassName() string {
return self.cp.getClassName(self.classIndex)
}
func (self *ConstantMemberrefInfo) NameAndDescriptor() (string, string) {
return self.cp.getNameAndType(self.nameAndTypeIndex)
}
还有三个常量没有介绍:CONSTANT_MethodType_info、 CONSTANT_MethodHandle_info和 CONSTANT_InvokeDynamic_info。它们是Java SE 7才添加到class文件中的,目的是支持新增的invokedynamic指令。本次暂不实现。
总结
可以把常量池中的常量分为两类:字面量(literal)
和符号引用 (symbolic reference)
。
字面量
包括数字常量
和字符串常量
,符号引用
包括类
和接口名
、字段
和方法信息
等。
除了字面量,其他常量都是通过索引直接或间接指向CONSTANT_Utf8_info
常量,以 CONSTANT_Fieldref_info
为例,如下所示。
4.2.4解析属性表
一些重要的信息没有出现,如方法的字节码等。那么这些信息存在哪里呢?答案是属性表。
AttributeInfo接口
和常量池类似,各种属性表达的信息也各不相同,因此无法用统一的结构来定义。不同之处在于,常量是由Java虚拟机规范严格 定义的,共有14种。
但属性是可以扩展的,不同的虚拟机实现可以定义自己的属性类型。
由于这个原因,Java虚拟机规范没有使用tag
,而是使用属性名来区别不同的属性。
属性数据放在属性名之后的u1表
中,这样Java虚拟机实现就可以跳过自己无法识别的属性。 属性的结构定义如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
属性表中存放的属性名实际上并不是编码后的字符串, 而是常量池索引,指向常量池中的CONSTANT_Utf8_info
常量。
创建attribute_info.go
文件,在其中定义 AttributeInfo接口
package classfile
type AttributeInfo interface {
readInfo(reader *ClassReader)
}
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {...}
func readAttribute(reader *ClassReader, cp ConstantPool) AttributeInfo {...}
func newAttributeInfo(attrName string, attrLen uint32,cp ConstantPool) AttributeInfo {...}
和ConstantInfo接口
一样,AttributeInfo接口
也只定义了一个readInfo()
方法,需要由具体的属性实现。readAttributes()
函数读取属性表。
func readAttributes(reader *ClassReader, cp ConstantPool) []AttributeInfo {
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 {
attrNameIndex := reader.readUint16()
attrName := cp.getUtf8(attrNameIndex)
attrLen := reader.readUint32()
attrInfo := newAttributeInfo(attrName, attrLen, cp)
attrInfo.readInfo(reader)
return attrInfo
}
readAttribute()
先读取属性名索引,根据它从常量池
中找到属性名
,然后读取属性长度,接着调用newAttributeInfo()
函数创建具体的属性实例。
Java虚拟机规范预定义了23种属性,先解析其中的8种。newAttributeInfo()
函数的代码如下
func newAttributeInfo(attrName string, attrLen uint32, cp ConstantPool) AttributeInfo {
switch attrName {
case "Code":
return &CodeAttribute{cp: cp}
case "ConstantValue":
return &ConstantValueAttribute{}
case "Deprecated":
return &DeprecatedAttribute{}
case "Exceptions":
return &ExceptionsAttribute{}
case "LineNumberTable":
return &LineNumberTableAttribute{}
case "LocalVariableTable":
return &LocalVariableTableAttribute{}
case "SourceFile":
return &SourceFileAttribute{cp: cp}
case "Synthetic":
return &SyntheticAttribute{}
default:
return &UnparsedAttribute{attrName, attrLen, nil}
}
}
创建attr_unparsed.go文件中,定义UnparsedAttribute结构体
package classfile
/*
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
*/
type UnparsedAttribute struct {
name string
length uint32
info []byte
}
func (self *UnparsedAttribute) readInfo(reader *ClassReader) {
self.info = reader.readBytes(self.length)
}
func (self *UnparsedAttribute) Info() []byte {
return self.info
}
按照用途,23种预定义属性可以分为三组。
- 第一组属性是实现 Java虚拟机所必需的,共有5种;
- 第二组属性是Java类库所必需的,共有12种;
- 第三组属性主要提供给工具使用,共有6种。
第三组属性是可选的,也就是说可以不出现在class文件中。如果class文件中存在第三组属性,Java虚拟机实现或者Java类库也是可以利用它们 的,比如使用LineNumberTable属性在异常堆栈中显示行号。
如下给出了这23 种属性出现的Java版本、分组以及它们在class文件中的位置。
Deprecated和Synthetic属性
Deprecated
和Synthetic
是最简单的两种属性,仅起标记作用,不包含任何数据。
这两种属性都是JDK1.1引入的,可以出现在 ClassFile、field_info和method_info结构中,它们的结构定义如下:
Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length;
}
Synthetic_attribute {
u2 attribute_name_index;
u4 attribute_length;
}
由于不包含任何数据,所以attribute_length
的值必须是0。
Deprecated属性
用于指出类、接口、字段或方法已经不建议使用,编译器等工具可以根据Deprecated属性输出警告信息。
J2SE 5.0
之前 可以使用Javadoc提供的@deprecated标签
指示编译器给类、接口、字段或方法添加Deprecated属性,语法格式如下:
/** @deprecated */
public void oldMethod() {...}
从J2SE 5.0
开始,也可以使用@Deprecated注解
,语法格式如下:
@Deprecated
public void oldMethod() {}
在Java中,编译器可能会生成一些额外的方法、字段或类,用于支持内部的匿名内部类、枚举、泛型等特性。这些生成的元素可能会被标记为 Synthetic
。
创建attr_markers.go
文件,在其中定义 DeprecatedAttribute
和SyntheticAttribute
结构体,代码如下:
package classfile
type DeprecatedAttribute struct { MarkerAttribute }
type SyntheticAttribute struct { MarkerAttribute }
type MarkerAttribute struct{}
func (self *MarkerAttribute) readInfo(reader *ClassReader) {
// read nothing
}
SourceFile属性
SourceFile
属性是Java类文件中的一个属性,它用于指定源文件的名称,即生成该类文件的源代码文件的名称。这个属性并不直接影响类的运行时行为。其结构定义如下:
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
attribute_length
的值必须是2。sourcefile_index
是常量池索引, 指向CONSTANT_Utf8_info常量
创建 attr_source_file.go
文件,在其中定义SourceFileAttribute结构体
,代码如下:
package classfile
type SourceFileAttribute struct {
cp ConstantPool
sourceFileIndex uint16
}
func (self *SourceFileAttribute) readInfo(reader *ClassReader) {
self.sourceFileIndex = reader.readUint16()
}
func (self *SourceFileAttribute) FileName() string {
return self.cp.getUtf8(self.sourceFileIndex)
}
例如,如果有一个名为 MyClass.java
的源代码文件,它包含以下类:
public class MyClass {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
当编译 MyClass.java
文件时,会生成一个名为 MyClass.class
的类文件,并在其中添加一个 SourceFile
属性,将其值设置为 MyClass.java
。
ConstantValue属性
ConstantValue
属性是Java类文件中的一个属性,通常与字段(field)相关联。这个属性的作用是为字段提供一个常量初始值。这意味着,如果您在类中声明一个字段,并为其分配了 ConstantValue
属性,那么该字段的初始值将在类加载时被设置为 ConstantValue
中指定的常量。
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
例如,假设有以下Java代码:
public class MyClass {
public final int myField = 42;
}
在对应的类文件中,将包含一个 ConstantValue
属性,指定了常量值 42
,并与 myField
字段相关联。当类加载时,myField
将被初始化为 42
。
constantvalue_index
是常量池索引,具体指向哪种常量因字段类型而异,如下为对照表
创建attr_constant_value.go
文件,在其中定义ConstantValueAttribute
结构体,代码如下:
package classfile
type ConstantValueAttribute struct {
constantValueIndex uint16
}
func (self *ConstantValueAttribute) readInfo(reader *ClassReader) {
self.constantValueIndex = reader.readUint16()
}
func (self *ConstantValueAttribute) ConstantValueIndex() uint16 {
return self.constantValueIndex
}
Code属性
Code
属性是Java类文件中的一个属性,通常与方法(Method)相关联。
它包含了方法的字节码指令,即实际的可执行代码。Code
属性是Java类文件中最重要的属性之一,因为它包含了方法的实际执行逻辑。
以下是关于 Code
属性的一些重要信息:
- 属性结构:
Code
属性通常包含以下信息:- 最大堆栈深度(
max_stack
):方法执行时所需的最大堆栈深度。 - 局部变量表的大小(
max_locals
):方法内部局部变量表的大小。 - 字节码指令(
code
):实际的字节码指令序列,即方法的执行代码。 - 异常处理器列表(
exception_table
):用于捕获和处理异常的信息。 - 方法属性(
attributes
):其他与方法相关的属性,例如局部变量表、行号映射表等。
- 最大堆栈深度(
- 字节码指令:
Code
属性中的code
部分包含了方法的实际字节码指令,这些指令由Java虚拟机执行。每个指令执行一些特定的操作,例如加载、存储、算术操作、分支、方法调用等。 - 异常处理:
Code
属性中的exception_table
部分包含了异常处理器的信息,指定了哪些字节码范围可以抛出哪些异常,并且指定了如何处理这些异常。 - 局部变量表:
Code
属性中的局部变量表(max_locals
)用于存储方法执行期间的局部变量,例如方法参数和临时变量。 - 属性:
Code
属性中还可以包含其他属性,如局部变量表、行号映射表等,这些属性提供了更多的调试和运行时信息。
Code
属性是Java虚拟机实际执行方法的关键部分,它描述了方法的行为和操作,包括如何处理输入和生成输出。编译器将源代码编译为字节码,然后将字节码填充到 Code
属性中,这使得Java程序可以在虚拟机上执行。
创建attr_code.go
文件,定义CodeAttribute结构体
、ExceptionTableEntry结构体
,代码如下:
type CodeAttribute struct {
cp ConstantPool
maxStack uint16
maxLocals uint16
code []byte
exceptionTable []*ExceptionTableEntry
attributes []AttributeInfo
}
func (self *CodeAttribute) readInfo(reader *ClassReader) {
self.maxStack = reader.readUint16()
self.maxLocals = reader.readUint16()
codeLength := reader.readUint32()
self.code = reader.readBytes(codeLength)
self.exceptionTable = readExceptionTable(reader)
self.attributes = readAttributes(reader, self.cp)
}
type ExceptionTableEntry struct {
startPc uint16
endPc uint16
handlerPc uint16
catchType uint16
}
func readExceptionTable(reader *ClassReader) []*ExceptionTableEntry {
exceptionTableLength := reader.readUint16()
exceptionTable := make([]*ExceptionTableEntry, exceptionTableLength)
for i := range exceptionTable {
exceptionTable[i] = &ExceptionTableEntry{
startPc: reader.readUint16(),
endPc: reader.readUint16(),
handlerPc: reader.readUint16(),
catchType: reader.readUint16(),
}
}
return exceptionTable
}
Exceptions属性
Exceptions属性通常与方法(Method)相关联,用于指定方法可能抛出的受检查异常(checked exceptions)的列表。
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
创建attr_exceptions.go
文件,在其中定义ExceptionsAttribute结构体
type ExceptionsAttribute struct {
exceptionIndexTable []uint16
}
func (self *ExceptionsAttribute) readInfo(reader *ClassReader) {
self.exceptionIndexTable = reader.readUint16s()
}
func (self *ExceptionsAttribute) ExceptionIndexTable() []uint16 {
return self.exceptionIndexTable
}
LineNumberTable和LocalVariableTable属性
LineNumberTable
和 LocalVariableTable
属性是Java类文件中的两个用于调试和运行时跟踪的属性,它们包含了与源代码中行号和局部变量相关的信息。
LineNumberTable 属性:用于建立源代码行号和字节码指令之间的映射。它允许开发工具在调试时将异常栈轨迹映射到源代码的特定行,以便开发者可以更容易地定位和修复代码中的问题。结构如下:
LineNumberTable {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}
LocalVariableTable 属性:用于跟踪局部变量的信息,包括局部变量的名称、数据类型、作用域范围和字节码偏移。
创建attr_line_number_table.go
文件,定义LineNumberTableAttribute结构体
,代码如下:
type LineNumberTableAttribute struct {
lineNumberTable []*LineNumberTableEntry
}
type LineNumberTableEntry struct {
startPc uint16
lineNumber uint16
}
func (self *LineNumberTableAttribute) readInfo(reader *ClassReader) {
lineNumberTableLength := reader.readUint16()
self.lineNumberTable = make([]*LineNumberTableEntry, lineNumberTableLength)
for i := range self.lineNumberTable {
self.lineNumberTable[i] = &LineNumberTableEntry{
startPc: reader.readUint16(),
lineNumber: reader.readUint16(),
}
}
}
func (self *LineNumberTableAttribute) GetLineNumber(pc int) int {
for i := len(self.lineNumberTable) - 1; i >= 0; i-- {
entry := self.lineNumberTable[i]
if pc >= int(entry.startPc) {
return int(entry.lineNumber)
}
}
return -1
}
创建attr_local_variable_table.go
文件,定义LocalVariableTableAttribute
,代码如下:
type LocalVariableTableAttribute struct {
localVariableTable []*LocalVariableTableEntry
}
type LocalVariableTableEntry struct {
startPc uint16
length uint16
nameIndex uint16
descriptorIndex uint16
index uint16
}
func (self *LocalVariableTableAttribute) readInfo(reader *ClassReader) {
localVariableTableLength := reader.readUint16()
self.localVariableTable = make([]*LocalVariableTableEntry, localVariableTableLength)
for i := range self.localVariableTable {
self.localVariableTable[i] = &LocalVariableTableEntry{
startPc: reader.readUint16(),
length: reader.readUint16(),
nameIndex: reader.readUint16(),
descriptorIndex: reader.readUint16(),
index: reader.readUint16(),
}
}
}
4.3测试
打开ch03\main.go文件,修改import
语句和startJVM()
函数,代码如下:
package main
import "fmt"
import "strings"
import "jvmgo/ch03/classfile"
import "jvmgo/ch03/classpath"
func main() {
cmd := parseCmd()
if cmd.versionFlag {
fmt.Println("version 0.0.1")
} else if cmd.helpFlag || cmd.class == "" {
printUsage()
} else {
startJVM(cmd)
}
}
func startJVM(cmd *Cmd) {
cp := classpath.Parse(cmd.XjreOption, cmd.cpOption)
className := strings.Replace(cmd.class, ".", "/", -1)
cf := loadClass(className, cp)
fmt.Println(cmd.class)
printClassInfo(cf)
}
func loadClass(className string, cp *classpath.Classpath) *classfile.ClassFile {
classData, _, err := cp.ReadClass(className)
if err != nil {
panic(err)
}
cf, err := classfile.Parse(classData)
if err != nil {
panic(err)
}
return cf
}
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())
}
}
首先go install jvmgo\ch03 生产ch03.exe
然后执行,并输入命令行语句,得到结果如下:
- version: 52.0:这表示
java.lang.String
类的类文件版本为 52.0。类文件版本号与Java版本号有关,52.0 对应于Java 8。 - constants count: 548:这表示常量池中包含 548 个常量。常量池包含了类的常量、方法、字段等信息。
- access flags: 0x31:这表示类的访问标志,
0x31
是十六进制表示,对应于二进制00110001
。这些标志描述类的访问权限和特性。 - this class: java/lang/String:这表示类的名称,即
java.lang.String
。 - super class: java/lang/Object:这表示
java.lang.String
类继承自java.lang.Object
类。 - interfaces: [java/io/Serializable java/lang/Comparable java/lang/CharSequence]:这表示
java.lang.String
类实现了三个接口,分别是Serializable
、Comparable
和CharSequence
。 - fields count: 5:这表示
java.lang.String
类包含 5 个字段。 - methods count: 94:这表示
java.lang.String
类包含 94 个方法。其中一些是构造方法(<init>
),其他是实例方法。