[Golang实现JVM第二篇]解析class文件是万里长征第一步

正确解析class文件是万里长征第一步。本篇我们会全程使用golang完成class文件的解析工作。

数据类型

JVM的class文件完全是二进制文件,最小单位是字节,也有数据类型,但都是字节的整数倍(废话)。规范中class文件一共有两类数据,一种是无符号整数,一种是表。无符号整数一共有u1,u2, u4, u8四种类型,分别表示8bit, 16bit, 32bit, 64bit的无符号整数。表则是无符号整数的集合,class文件中在出现表之前都会先跟着一个u2类型的长度数据,表名后面表的总长度,这样才能正确解析表。

另外还要注意字节序的问题,JVM规范规定class文件统一采用Big Endian字节序,也就是低地址存储高位,高地址存放低位。如果是用C/C++语言写JVM,则程序使用的字节序是跟CPU绑定的,比如intel的x86平台使用Little Endian,PowerPC则是Big Endian。不过幸好我们的主角是Go, Go统一采用大端,这样就不需要操心平台了。假设我们用一个二元素的[]byte数组来存储从class文件中按顺序读到的u16类型数据,那么byte[0]就是u16的高8位,byte[1]就是低8位,组合起来就是:

uint16(b[1]) | uint16(b[0]) << 8

即将高位左移8位,然后跟低位做按位或操作即可还原。

Go读取二进制数据常用函数

我们使用标准库的io.Reader接口从文件中读取字节,然后从字节数组中还原原本的数据类型,例如读取u16类型的数据可以这么写:

func ReadInt16(bufReader io.Reader) (uint16, error) {
    numBuf := make([]byte, 2, 2)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    var num uint16
    err = binary.Read(bytes.NewBuffer(numBuf), binary.BigEndian, &num)
    if nil != err {
        return 0, err
    }

    return num, nil
}

这里我们用了binary包替我们执行的位运算,但是这个方法会涉及类型查询操作和内存分配,所以肯定会比直接手动组装byte要慢一些,但是上篇就已经说了,过早优化是万恶之源,不必在意。

同理,u32的读取可以这么写:

func ReadInt32(bufReader io.Reader) (uint32, error) {
    numBuf := make([]byte, 4, 4)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    var num uint32
    err = binary.Read(bytes.NewBuffer(numBuf), binary.BigEndian, &num)
    if nil != err {
        return 0, err
    }

    return num, nil

如果是读取u8,那直接读一个byte返回就可以了:

func ReadInt8(bufReader io.Reader) (uint8, error) {
    numBuf := make([]byte, 1, 1)
    _, err := bufReader.Read(numBuf)
    if nil != err {
        return 0, err
    }

    return numBuf[0], nil
}

至此,我们已经排除了读取class文件的全部"技术障碍"。

class文件结构

我们先用Go定义出一个class文件的完整结构:

// class文件定义
type DefFile struct {
    MagicNumber uint32

    MinorVersion uint16
    MajorVersion uint16

    // 常量池数量
    ConstPoolCount uint16
    // 常量池
    ConstPool []interface{}

    // 访问标记
    AccessFlag uint16
    // 当前类在常量池的索引
    ThisClass uint16
    // 父类索引
    SuperClass uint16

    // 接口
    InterfacesCount uint16
    Interfaces []uint16

    // 字段
    FieldsCount uint16
    Fields []*FieldInfo

    // 方法
    MethodCount uint16
    Methods []*MethodInfo

    // 属性
    AttrCount uint16
    Attrs []interface{}
}

我们一个个的来看。

  • MagicNumber, MajorVersion, MinorVersion

上来就是一个标识文件类型的魔术数,就是那个有名的“咖啡宝贝” 0xCAFEBABE。然后是主版本号、副版本号。这些没啥好说的。

  • ConstPoolCount, ConstPool

这是整个 class文件最重要的部分,常量池。对是常量池,并不是字节码。先是一个16位无符号整数表示常量池数据项的数量,然后就是常量池数组。 所有的符号引用和字面值(如字符串, 整数)都保存在常量池中,所有其他属性都通过保存常量池数组下标的方式来记录自己引用了哪一条数据。要注意的一点是常量池数组的下标是从1开始填充数据的,下标为0的位置不保存任何数据项,这是为了方便表达"不指向任何一个常量"的含义。比如ConstPoolCount = 10的话,则ConstPool数组有11个元素,下标从1开始,直到11为止。

常量池数据项有十几种种类型,随着JDK版本的增加往往会有新的类型加入。每种类型的结构都不太样,但是都遵循先是一个uint8类型的tag用来表示数据项类型,然后是常量池数据的结构,例如方法引用项(CONSTANT_Methodref):

// 方法引用常量
type MethodRefConstInfo struct {
    Tag uint8
    ClassIndex uint16
    NameAndTypeIndex uint16
}

Tag: 固定为10, 表示这是一条方法引用数据项

ClassIndex: 是一个常量池的数组下标,引用的是一条类引用(CONSTANT_Class)类型的数据项,用来记录方法属于哪个类。

NameAndTypeIndex: 同样是常量池的数组下标,引用的是一个NameAndType类型,用来记录方法名、方面描述符号,而方法描述符中记录了方法的参数类型和返回值类型。

这里就是单拿一个例子来举例,在Java8中完整的常量池类型和结构可以直接参考Oralce的JVM规范在线文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html ,就不再一一列举了。因为非常繁琐,这里列出来解释了也是云里雾里,意义不大,后面在解释字节码引用到常量池的时候再解释含义。要注意的是我们并不需要实现全部常量池类型,只需要实现你的class文件中存在的常用类型即可。具体操作方法在上一篇中提到过,自己写一个简单的java文件,编译,然后用javap -verbose查看。

  • AccessFlag

访问标记,即当前类是public, abstract, 还是final, interface等。注意每个标记不是通过单独的取值存储的,而是通过一个二进制位来标记。例如0x0001表示public, 0x0010表示final,解析的时候需要遍历每一个位,通过判断是否为1来决定是否带有此标记。

完整的标记位取值如下:

const (
    Public = 0x0001
    Private = 0x0002
    Protected = 0x0004
    Static = 0x0008
    Final = 0x0010
    Synchronized = 0x0020
    Bridge = 0x0040
    Varargs = 0x0080
    Native = 0x0100
    Abstarct = 0x0400
    Strict = 0x0800
    Synthetic = 0x1000
)
  • ThisClass, SuperClass

分别表示当前类和父类在常量池中的索引。前者用于确定当前类的全限定性名,后者用于确定父类的全限定性名。在JVM中,给定一个类的全限定性名就可以从classpath中找出这个类的class文件,继而执行加载逻辑。

  • InterfacesCount, Interfaces

因为Java类允许同时实现多个接口,因此这里在记录实现了那些接口时就必须用一个数组来记录了。同样的,先是一个count表示有多少数据项,然后是数据表本身。

  • FieldsCount, Fields

跟接口一样,用于记录当前类级别的字段和实例级别的字段。在Fields的每个数据项中又记录了实例名、类型、修饰符(如private, final)信息。

  • MethodCount, Methods

用于记录方法信息。同理,每一个Methods数据项都会详细记录方法的所有属性。

  • AttrCount, Attrs

属性表集合,用于记录一些附加信息。注意属性表可以出现在class里,也可以在method, field中出现,出现在哪就表名记录的是哪一个层级的属性。属性表跟常量池一样,每个数据项都有不同的类型,而且截至Java12,数据项的类型数量已经高达29种,可以说非常复杂了。每中数据项都遵循着先是一个属性名,再跟一个属性数据的长度(以字节为单位),然后是属性本身。我们常说的字节码,就是保存在Method中的Code属性里的,定义如下:

// code属性
type CodeAttr struct {
  AttrNameIndex uint16
    AttrLength uint32

    MaxStack uint16
    MaxLocals uint16

    // 字节码长度
    CodeLength uint32
    Code []byte

    // 异常表
    ExceptionTableLength uint16
    ExceptionTable []*ExceptionTable

    AttrCount uint16
    Attrs []interface{}
}

注意第一个字段,AttrNameIndex是一个16位的无符号整数,保存的是一个常量池数组下标,而下标所保存的常量池数据项类型就是一个UTF8字符串,在这里就是Code这个固定值。

下面的几项分别保存了操作数栈最大深度、本地变量表最大长度、字节码长度、字节码本身、异常信息,另外最后还有属性信息,套娃。我们以后实现解释器主要就是要找到method中的Code属性的Code字段,然后一条条的解释字节码。

以上就是class文件结构的全部内容了,说实在的,非常复杂,解析的时候也会比较痛苦。但还是那句话,不需要全部都解析出来,只需要解析需要的那部分即可。对于每一个具体的数据类型的含义,在后面实现解释器时用到了再解释,这里不罗列了。笔者已经实现了对class文件的解析逻辑,可以参考下面的地址:https://github.com/wanghongfei/mini-jvm/blob/master/vm/class/jclass.go

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342