Touch and Parse Java Class File


 

Touch and Parse Java Class File

Java字节码结构及解析过程

0x00. TOC

0x01. 概述

为了实现在不同的操作系统和硬件体系结构下的平台无关性和语言无关性,Java 语言设计者定义了一套程序存储结构的格式规范,即把源程序编译成具有一定格式的字节码文件,各个平台上的虚拟机再按照相应的规范把该字节码文件解析成其平台下的运行代码。

img

这样不仅实现了一处编写,到处运行(Write Once, Run Anywhere)的平台无关性,还为在 Java 虚拟机上运行非 Java 语言代码提供了可能。时至今日,已经发展出有很多其他能够在 Java 虚拟机上运行的语言,如Clojure、Groovy、JRuby、Jython、Scala等。

independence

0x02. 字节码文件结构

Class 文件是一组以8位字节为单位的二进制流,各个数据项严格按照顺序无间隙地紧凑排列在 Class 文件中。当遇到需要占用8位字节以上的空间的数据项时,则会将其按照大端(Big-Endian)方式分割成若干个8位字节进行存储(大端方式指数据的最高位字节存储在地址的最低位,最低位字节存储在地址的最高位)。

根据 JVM 规范,Class 文件格式采用包含无符号数的类结构体存储数据,无符号数以u1u2u4u8分别代表1、2、4、8个字节的无符号数,用来描述数字、索引引用、UTF-8字符串值等;表是由多个无符号数或其他表作为数据项构成的符合数据类型,用于描述有层次关系的符合结构的数据,一般以_info结尾。另外,除了表和无符号数外,当需要表示一个长度不定的数据时,往往需要前置一个容量计数器。Class 文件格式具体如下图所示:

class

1. 魔数

如 gif 和 jpeg 等文件的存储标准一样,为了标识和验证其文件的身份,每个 Class 文件使用前四个字节作为魔数(Magic Number),用于确定该文件是否为一个能被虚拟机接受的文件,其值固定为oxCAFEBABE

2. 版本号

第5、6字节表示次版本号(Minor Version),第7、8和字节表示朱版本号(Major Veriosn)。Java的主版本号是从45开始 的,JDK 1.1之后的每个 JDK 大版本发布主版本号向上加1 (JDK 1.0 ~ 1.1 使用了 45.0〜45.3 的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。例如, JDK 1.1 能支持版本号为 45.0〜45.65535 的 Class 文件,无法执行版本号为 46.0 以上的 Class 文件,而 JDK 1.2 则能支持 4 5 .0〜46.65 535 的 Class 文件,

version

3. 常量池

由于常量池中常量的数量是不固定的,所以在常量池的人口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与 Java 中语言习惯不一样的是,这个容量计数是从1而不是0开始的,

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用則属于编译原理方面的概念,

代码在使用 Javac 进行编译时,采用动态链接方式,即在 Class 文件中不会保存各个方法、宇段的最终内存布局信息,因此这些宇段、方法的符号引用不经过运行期转换的话无法得到真正的内存人口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

常量池中每一项常量都是一个表,在 JDK 1 .7 之前共有14种结构各不相同的表结构,这14种表开始的第一位都是一个ul类型的标志位,代表当前这个常量属于哪种常量类型。这14种常置类型所代表的具体含义见下表:

常量 项目 类型 描述

 

CONSTANT_Utf8

tag

u1

值为 1

length

u2

UTF-8 编码的字符串占用的字节数

bytes

u1

长度为 length 的 UTF-8 编码的字符串

 

CONSTANT_Integer

tag

u1

值为 3

bytes

u4

按照高位在前存储的 int 值

 

CONSTANT_Float

tag

u1

值为 4

bytes

u4

按照高位在前存储的 float 值

 

CONSTANT_Long

tag

u1

值为 5

bytes

u8

按照高位在前存储的 long 值

 

CONSTANT_Double

tag

u1

值为 6

bytes

u8

按照高位在前存储的 double 值

 

CONSTANT_Class

tag

u1

值为 7

name_index

u2

指向全限定名常量项的索引

 

CONSTANT_String

tag

u1

值为 8

string_index

u2

指向字符串字面量的索引

 

CONSTANT_Fieldref

tag

u1

值为 9

class_index

u2

指向声明字段的类或接口描述符 CONSTANT_Class_info 的索引项

name_and_type_index

u2

指向字段名称及类型描述符 CONSTANT_NameAndType_info 的索引项

 

CONSTANT_Methodref

tag

u1

值为 10

class_index

u2

指向声明方法的类描述符 CONSTANT_Class_info 的索引项

name_and_type_index

u2

指向方法名称及类型描述符 CONSTANT_NameAndType_info 的索引项

 

CONSTANT_InrerfaceMethodref

tag

u1

值为 11

class_index

u2

指向声明方法的接口描述符 CONSTANT_Class_info 的索引项

name_and_type_index

u2

指向方法名称及类型描述符 CONSTANT_NameAndType_info 的索引项

 

CONSTANT_NameAndType

tag

u1

值为 12

name_index

u2

指向字段或方法名称常量项目的索引

descriptor_index

u2

指向该字段或方法描述符常量项的索引

4. 访问标志

两个字节大小,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体的标志位以及标志的含义见下表

acess_flags

5. 类索引、父类索引与接口索引集合

类索引(this_class)和父类索引(super_class)都是—个 u2 类型的数据,而接口索引集合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类时全限定名。

由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Objcct 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引不为0。

接口索引集合用于描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合中。

类索引、父类索引和接口索引集合都按順序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常童,通过C0NSTANT_Class_info类型的常量中的索引值可找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

对于接口索引集合,人口的第一项是 u2 类型的接口计数器(interfaces_count),表示索引表的容最。如果该类没有实现任何接口,则该计数器值为 0,后面接口的索引表不再占用任何字节。

6. 字段表集合

宇段表(field_jnfo)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。一个字段可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、宇段名称。

上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。下表列出了字段表的最终格式:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

字段修饰符(access_flags)与类修饰符类似,其标志值与含义的对应关系如下表所示

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否 public
ACC_PRIVATE 0x0002 字段是否 private
ACC_PROTECTED 0x0004 字段是否 protected
ACC_STATIC 0x0008 字段是否 static
ACC_FINAL 0x0010 字段是否 final
ACC_VOLATILE 0x0040 字段是否 volatile
ACC_TRANSIENT 0x0080 字段是否 transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生
ACC_ENUM 0x4000 字段是否 enum

描述符标识字符及其含义的对照关系如下表所示

标识字符 含义 标识字符 含义
B 基本类型 byte J 基本类型 long
C 基本类型 char S 基本类型 short
D 基本类型 double Z 基本类型 boolean
F 基本类型 int V 特殊类型 void
I 基本类型 int L 对象类型

7. 方法表集合

结构与属性表一样,也包括access_flags、name_index、descriptor_index、attributes_count和attributes,差异主要在访问标志,方法的访问标志及其含义如下表所示

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否 public
ACC_PRIVATE 0x0002 方法是否 private
ACC_PROTECTED 0x0004 方法是否 protected
ACC_STATIC 0x0008 方法是否 static
ACC_FINAL 0x0010 方法是否 final
ACC_SYNCHRONIZED 0x0020 方法是否 synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0180 方法是否为 native
ACC_ABSTRACT 0x0400 是否抽象方法
ACC_STRICTFP 0x0800 方法是否为 strictfp
ACC_SYNTHETIC 0x1000 方法是否由编译器自动产生

除了上述对于方法的描述外,方法中的代码,经过编译器编译成字节码指令后,存放在方法属性表集合的一个名为Code的属性中。

与字段表集合相对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编泽器自动添加的方法,最典型的是类构造器<cinit>方法和实例构造器<init>方法。

不同于语法层面的重载,Class 文件中允许具有相同的方法名和特征签名但返回值不同的两个方法共存。

8. 属性表集合

在JDK 1.7 的 JVM 规范中预定义了 20 种属性项,详情参照 JVM Specification

8.1. Code

Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。Code 属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,比如接口或者抽象类中的方法就不存在 Code 属性,Code 属性结构将如下表所示:

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池 utf-8 型常量的索引,常量值固定为Code
u4 attribute_length 1 属性值长度
u2 max_stack 1 操作数栈最大深度,VM根据这个值分配栈帧中的操作数深度
u2 max_locals 1 代表当前属性表示的方法存储局部变量表所需的空间大小,单位是Slot,当代码的执行超出一个局部的作用域时,该变量的内存单元(Slot)会被其他局部变量占用,Javac 编译器会根据变量的作用域来分配 Slot 给每个变量使用,然后算出max_locals
u4 code_length 1 方法体中的代码编译后的字节码长度,因为 JVMS 明确限制一个方法不允许超过 65535 条指令,即该值实际只使用了 16 位,若超过该限制,Javac 会拒绝编译
u1 code code_length 不同于字节码文件中的其他数据项用于描述元数据,Code用于描述代码,一个字节最多可表示的指令数为 28
u2 exception_table_length 1 异常表的长度,由于异常表对于 Code 非必须,故此项可为 0,exception_table为空
exception_info exception_table exception_table_length 异常表内容
u2 attributes_count 1 代码块中的属性个数
attribute_info attributes attributes_count 属性集合

异常表(exception_table)的结构及其说明如下表所示

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1
  • 当程序计数器(PC)的值在 [start_pc, end_pc) 范围内,异常处理器将被激活,即当在[start_pc, end_pc) 间出现类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引)时,将转向第handler_pc行继续处理,当catch_type为 0 时,代表任意异常情况都需要转向到handler_pc处进行处理。
  • end_pc设置为不包含的范围被证实是JVMS设计的一个历史错误。因为如果一个方法的 JVM 机器码刚好是 216 - 1 = 65535 字节长,并且以一个 1 字节的指令结束,那么该指令不能被一个异常处理器保护。编译器作者可以通过将任何方法、实例初始化方法或静态初始化器(任何代码数组的大小)生成的 JVM 代码的最大大小限制为 65534 字节来解决这个问题。
  • handler_pc项的值指示异常处理器的开始,其值必须是Code项目数组中的有效索引,并且必须是指令的操作码的索引。

Code 实例:编写inc函数,查看其编译后的字节码序列及异常表,查询字节码与指令的对照表可以得出大体的执行流程如下所示,其中只有一个变量x,因为第一个本地变量 Slot_0 用来存放 this,所以 x 值的存取涉及的都是操作栈中的第二个操作单元 Slot_1,从而使用的操作栈机器指令是以 1 结尾的。如istore_1iload_1等等。TOC

code

8.2. Exceptions

不同于Code中的异常表,该属性项用于列出方法中可能会抛出的受检查异常(Check Exception),即方法描述时在throws关键字后面的列举的异常。其结构如下表所示:

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池的CONSTANT_Utf8_info项的索引,常量值固定为Exception
u4 attribute_length 1 表示属性长度,不包括初始的 6 个字节。
u2 number_of_exceptions 1 exception_index_table的个数
u2 exception_index_table number_of_exceptions 指向CONSTANT_Class_info的型常量的索引

方法只有在满足以下三个条件中的至少一个时才会抛出异常:

  • 异常是RuntimeException或它的一个子类的一个实例。
  • 异常是Error或其子类之一的实例。
  • 这个异常是刚才描述的exception_index_table中指定的一个异常类或其子类之一的实例。

这些需求在 JVM 中没有强制执行,它们仅在编译时强制执行。

8.3. LineNumberTable

用于描述源码的行号与字节码行号(字节码偏移量)之间的关系它并不是运行时必需的属性,但酞认会生成到 Class 文件中,可在 Javac 中分别使用 -g:none-g:lines选项来取消或要求生成这项信息。如果选择不生成该属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无`法按照源码行来设置断点。其结构如下所示:TOC

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池的CONSTANT_Utf8_info项的索引,常量值固定为LineNumberTable
u4 attribute_length 1 表示属性长度,不包括初始的 6 个字节。
u2 line_number_table_length 1 line_number_table个数
line_number_info line_number_table line_number_table_length line_number_info包括start_pcline_number两个 u2 类型的数据项,分别表示字节码行号和 Java 源码行号。
8.4. LocalVariableTable

用于描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,不是运行时必需的属性,但默认会生成到 Class 文件之中,可以在 Javac 中分别使用-g:none-g:vars选项来取消或要求生成这项信息。如果没有生成这项厲性,最大的影响就是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE 将会使用诸如 arg0、arg1 之类的占位符代替原有的参数名,这对程序运行没有影响,但是会对代码编写和阅读带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值。该属性结构如下表所示:TOC

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池的CONSTANT_Utf8_info项的索引,常量值固定为LocalVariableTable
u4 attribute_length 1 表示属性长度,不包括初始的 6 个字节。
u2 local_variable_table_length 1 local_variable_table个数
local_variable_info local_variable_table local_variable_table_length 本地变量表集合

local_variable_info表示一个栈帧与源码中的局部变量的关系,其结构如下表所示:

类型 名称 数量 说明
u2 start_pc 1 代表该局部变量的生命周期开始的字节码偏移量
u2 length 1 代表该局部变量的作用域覆盖的范围
u2 name_index 1 指向常量池的CONSTANT_Utf8_info项的索引,常量值为变量名
u2 descriptor_index 1 指向常量池的CONSTANT_Utf8_info项的索引,常量值为描述符
u2 index 1 该局部变量在栈帧局部变量表中 Slot 的位置,当该变量为 double 或 long 的 64 位类型的数据时,它占用的 Slot 为 index 和 index + 1 两个
8.5. LocalVariableTypeTable

结构与LocalVariableTable非常相似,仅是把记录的字段描述符descriptor_index换成了signature_index,后者为用于表示字段的特征签名,在引入泛型后,由于描述符中泛型的参数化类型会被擦除,描述符不能准确描述泛型类型,所以才引入该属性。

LocalVariableTypeTable_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 local_variable_type_table_length;
    {   u2 start_pc;
        u2 length;
        u2 name_index;
        u2 signature_index;
        u2 index;
    } local_variable_type_table[local_variable_type_table_length];
}
8.6. SourceFile

定长的可选属性,用于记录生成该字节码文件的源码文件名称,可分别用 Javac 的-g:none-g:source选项来关闭或要求生成这项信息。在 Java 中,对于大多数的类来说,类名和文件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成该属性,抛出异常时,堆找中将不会显示出错代码所属的文件名。该属性结构如下表所示:

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池中的CONSTANT_Utf8_info项的索引,常量值固定为SourceFile
u4 attribute_length 1 属性长度,固定为 2
u2 sourcefile_index 1 指向常量池中的CONSTANT_Utf8_info项的索引,常量值为其源码文件名。
8.7. ConstantValue

用于通知虚拟机自动为静态变量赋值,只有被static修饰的变量才能使用该属性。其结构如下表所示

类型 名称 数量 说明
u2 attribute_name_index 1 指向常量池中的CONSTANT_Utf8_info项的索引,常量值固定为ConstantValue
u1 attribute_length 1 属性长度,固定为 2
u2 constantvalue_index 1 指向常量池中一个字面量常量的索引,,根据变量类型的不同,可为CONSTANT_Long_infoCONSTANT_Long_infoCONSTANT_Long_infoCONSTANT_Integer_infoCONSTANT_Float_infoCONSTANT_Double_info、、CONSTANT_String_info

对于非静态的变量的赋值是在实例构造器<cinit>中进行的,而类变量的赋值可以在类构造方法<cinit>方法中进行或者使用ConstantValue属性。

0x03. 解析

国庆之际,闲得无聊,想着自己折腾下解析字节码文件,因为我对官方性的东西常抱有质疑的想法,官方说得越不容怀疑,我反而越不愿相信,除非自己动手得到结果,听别人的传授固然可取,但没有实践过心里总不踏实,特别是当我发现周志明那本《深入理解Java虚拟机》中作者煞有其事地贴出一段错误的代码样例的时候(无意黑,书整体很好)。TOC

回到解析字节码文件这件事上,如果没有进行编码的设置,用记事本打开字节码文件肯定会出现乱码的,可以用WinHexSublime等工具查看格式化的十六进制数据,当然我们也可以通过编程实现数据的格式化并进行输出打印:

/**
 * format binary array of a class file into
 * hexadecimal string and print it
 */
public void printHexArr() {
    System.out.println("hexadecimal code:");
    String[] strings = ByteUnitReader.binaryToHexString(classFileBytes);
    for (int i = 1; i <= strings.length;i++) {
        String symbol = (i % 28 == 0 || i == strings.length) ? "\n" : ", ";
        System.out.print(strings[i - 1] + symbol);
    }
}

public static String[] binaryToHexString(byte[] bytes){
    String hexStr =  "0123456789ABCDEF", hex;
    String[] hexStrings = new String[bytes.length];
    for (int i = 0; i < bytes.length; i++) {
        // 字节高4位
        hex = String.valueOf(hexStr.charAt((bytes[i] & 0xF0) >> 4));
        // 字节低4位
        hex += String.valueOf(hexStr.charAt(bytes[i] & 0x0F));
        hexStrings[i] = hex;
    }
    return hexStrings;
}

其中在ByteUnitReader.binaryToHexString(byte[] bytes)中分别获取一个字节的高低四位,将其值分别作为hexStr的索引获取相应的十六进制表示,为了利用博客的空间,printHexArr()中则每 28 个字节进行换行。基于以下测试程序的字节码进行解析,只做简单的验证用:

package decompile;
import java.util.Iterator;
public class IterableClass implements Iterable<String>{
    private String[] arr;
    public IterableClass(String[] arr) {
        this.arr = arr;
    }
    @Override
    public Iterator<String> iterator() {
        return new Iterator<String>() { 
            private int index = 0;
            @Override
            public boolean hasNext() {
                return index < arr.length;
            }
            @Override
            public String next() {
                return arr[index++];
            }
            @Override
            public void remove() {
               //TODO
            }
        };
    }
}

格式化字节码后得到的十六进制数据序列如下所示:

CA, FE, BA, BE, 00, 00, 00, 34, 00, 25, 09, 00, 05, 00, 1C, 0A, 00, 06, 00, 1D, 07, 00, 1E, 0A, 00, 03, 00, 1F
07, 00, 20, 07, 00, 21, 07, 00, 22, 01, 00, 0C, 49, 6E, 6E, 65, 72, 43, 6C, 61, 73, 73, 65, 73, 01, 00, 03, 61
72, 72, 01, 00, 13, 5B, 4C, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 53, 74, 72, 69, 6E, 67, 3B, 01, 00, 06, 3C
69, 6E, 69, 74, 3E, 01, 00, 16, 28, 5B, 4C, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 53, 74, 72, 69, 6E, 67, 3B
29, 56, 01, 00, 04, 43, 6F, 64, 65, 01, 00, 0F, 4C, 69, 6E, 65, 4E, 75, 6D, 62, 65, 72, 54, 61, 62, 6C, 65, 01
00, 12, 4C, 6F, 63, 61, 6C, 56, 61, 72, 69, 61, 62, 6C, 65, 54, 61, 62, 6C, 65, 01, 00, 04, 74, 68, 69, 73, 01
00, 19, 4C, 64, 65, 63, 6F, 6D, 70, 69, 6C, 65, 2F, 49, 74, 65, 72, 61, 62, 6C, 65, 43, 6C, 61, 73, 73, 3B, 01
00, 08, 69, 74, 65, 72, 61, 74, 6F, 72, 01, 00, 16, 28, 29, 4C, 6A, 61, 76, 61, 2F, 75, 74, 69, 6C, 2F, 49, 74
65, 72, 61, 74, 6F, 72, 3B, 01, 00, 09, 53, 69, 67, 6E, 61, 74, 75, 72, 65, 01, 00, 2A, 28, 29, 4C, 6A, 61, 76
61, 2F, 75, 74, 69, 6C, 2F, 49, 74, 65, 72, 61, 74, 6F, 72, 3C, 4C, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 53
74, 72, 69, 6E, 67, 3B, 3E, 3B, 01, 00, 0A, 61, 63, 63, 65, 73, 73, 24, 30, 30, 30, 01, 00, 2E, 28, 4C, 64, 65
63, 6F, 6D, 70, 69, 6C, 65, 2F, 49, 74, 65, 72, 61, 62, 6C, 65, 43, 6C, 61, 73, 73, 3B, 29, 5B, 4C, 6A, 61, 76
61, 2F, 6C, 61, 6E, 67, 2F, 53, 74, 72, 69, 6E, 67, 3B, 01, 00, 02, 78, 30, 01, 00, 3A, 4C, 6A, 61, 76, 61, 2F
6C, 61, 6E, 67, 2F, 4F, 62, 6A, 65, 63, 74, 3B, 4C, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 49, 74, 65, 72, 61
62, 6C, 65, 3C, 4C, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 53, 74, 72, 69, 6E, 67, 3B, 3E, 3B, 01, 00, 0A, 53
6F, 75, 72, 63, 65, 46, 69, 6C, 65, 01, 00, 12, 49, 74, 65, 72, 61, 62, 6C, 65, 43, 6C, 61, 73, 73, 2E, 6A, 61
76, 61, 0C, 00, 09, 00, 0A, 0C, 00, 0B, 00, 23, 01, 00, 19, 64, 65, 63, 6F, 6D, 70, 69, 6C, 65, 2F, 49, 74, 65
72, 61, 62, 6C, 65, 43, 6C, 61, 73, 73, 24, 31, 0C, 00, 0B, 00, 24, 01, 00, 17, 64, 65, 63, 6F, 6D, 70, 69, 6C
65, 2F, 49, 74, 65, 72, 61, 62, 6C, 65, 43, 6C, 61, 73, 73, 01, 00, 10, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F
4F, 62, 6A, 65, 63, 74, 01, 00, 12, 6A, 61, 76, 61, 2F, 6C, 61, 6E, 67, 2F, 49, 74, 65, 72, 61, 62, 6C, 65, 01
00, 03, 28, 29, 56, 01, 00, 1C, 28, 4C, 64, 65, 63, 6F, 6D, 70, 69, 6C, 65, 2F, 49, 74, 65, 72, 61, 62, 6C, 65
43, 6C, 61, 73, 73, 3B, 29, 56, 00, 21, 00, 05, 00, 06, 00, 01, 00, 07, 00, 01, 00, 02, 00, 09, 00, 0A, 00, 00
00, 03, 00, 01, 00, 0B, 00, 0C, 00, 01, 00, 0D, 00, 00, 00, 46, 00, 02, 00, 02, 00, 00, 00, 0A, 2A, B7, 00, 02
2A, 2B, B5, 00, 01, B1, 00, 00, 00, 02, 00, 0E, 00, 00, 00, 0E, 00, 03, 00, 00, 00, 09, 00, 04, 00, 0A, 00, 09
00, 0B, 00, 0F, 00, 00, 00, 16, 00, 02, 00, 00, 00, 0A, 00, 10, 00, 11, 00, 00, 00, 00, 00, 0A, 00, 09, 00, 0A
00, 01, 00, 01, 00, 12, 00, 13, 00, 02, 00, 0D, 00, 00, 00, 33, 00, 03, 00, 01, 00, 00, 00, 09, BB, 00, 03, 59
2A, B7, 00, 04, B0, 00, 00, 00, 02, 00, 0E, 00, 00, 00, 06, 00, 01, 00, 00, 00, 0F, 00, 0F, 00, 00, 00, 0C, 00
01, 00, 00, 00, 09, 00, 10, 00, 11, 00, 00, 00, 14, 00, 00, 00, 02, 00, 15, 10, 08, 00, 16, 00, 17, 00, 01, 00
0D, 00, 00, 00, 2F, 00, 01, 00, 01, 00, 00, 00, 05, 2A, B4, 00, 01, B0, 00, 00, 00, 02, 00, 0E, 00, 00, 00, 06
00, 01, 00, 00, 00, 05, 00, 0F, 00, 00, 00, 0C, 00, 01, 00, 00, 00, 05, 00, 18, 00, 11, 00, 00, 00, 03, 00, 14
00, 00, 00, 02, 00, 19, 00, 1A, 00, 00, 00, 02, 00, 1B, 00, 08, 00, 00, 00, 0A, 00, 01, 00, 03, 00, 00, 00, 00
00, 00

接下来是解析的过程,使用ClassFile类记录解析到的字节码文件的信息,主要的解析逻辑封装在ClassReader类中,其中的核心方法是公开的parse()方法:

  • 获取字节码文件的输入流,设置最大输入限制为INPUT_STREAM_DATA_CHUNK_SIZE大小,将输入流转为待处理的字节数组classFileBytes
  • 输出十六进制字节码信息。
  • 设置私有变量byteCursor记录当前解析到的字节数组的索引。
  • 顺序读取魔数、版本号、常量池表项数。
  • 根据常量池表项数读取相应个表项,使用双向链表结构暂存解析到的常量池信息,当找到的常量池表项指向另一个常量池表项时,采用二分查找的思想,从当前表项索引向某一个方向开始查询,直到找到目标索引对应的表项,输出的常量池信息参考javap -v的结构。
  • 定义AccessFlags类记录解析到访问标志信息,并在类内部封装格式化打印方法。
  • 解析本类、父类、接口、域信息和方法数目。
magic                         cafebabe                                                              
minorVersion                  0                                                                     
majorVersion                  52                                                                    
constantPoolCount             37                                                                    

constantPoolItems:
#1 = Filedref                 #5, #28   		//decompile/IterableClass, arr:[Ljava/lang/String;        
#2 = Methodref                #6, #29   		// java/lang/Object, <init>:()V                           
#3 = Class                    #30       		//decompile/IterableClass$1                               
#4 = Methodref                #3, #31   		// decompile/IterableClass$1, <init>:(Ldecompile/IterableClass;)V
#5 = Class                    #32       		//decompile/IterableClass                                 
#6 = Class                    #33       		//java/lang/Object                                        
#7 = Class                    #34       		//java/lang/Iterable                                      
#8 = Utf8                     InnerClasses                                                          
#9 = Utf8                     arr                                                                   
#10 = Utf8                    [Ljava/lang/String;                                                   
#11 = Utf8                    <init>                                                                
#12 = Utf8                    ([Ljava/lang/String;)V                                                
#13 = Utf8                    Code                                                                  
#14 = Utf8                    LineNumberTable                                                       
#15 = Utf8                    LocalVariableTable                                                    
#16 = Utf8                    this                                                                  
#17 = Utf8                    Ldecompile/IterableClass;                                             
#18 = Utf8                    iterator                                                              
#19 = Utf8                    ()Ljava/util/Iterator;                                                
#20 = Utf8                    Signature                                                             
#21 = Utf8                    ()Ljava/util/Iterator<Ljava/lang/String;>;                            
#22 = Utf8                    access$000                                                            
#23 = Utf8                    (Ldecompile/IterableClass;)[Ljava/lang/String;                        
#24 = Utf8                    x0                                                                    
#25 = Utf8                    Ljava/lang/Object;Ljava/lang/Iterable<Ljava/lang/String;>;            
#26 = Utf8                    SourceFile                                                            
#27 = Utf8                    IterableClass.java                                                    
#28 = NameAndType             #9, #10   		// arr:[Ljava/lang/String;                                
#29 = NameAndType             #11, #35  		// <init>:()V                                             
#30 = Utf8                    decompile/IterableClass$1                                             
#31 = NameAndType             #11, #36  		// <init>:(Ldecompile/IterableClass;)V                    
#32 = Utf8                    decompile/IterableClass                                               
#33 = Utf8                    java/lang/Object                                                      
#34 = Utf8                    java/lang/Iterable                                                    
#35 = Utf8                    ()V                                                                   
#36 = Utf8                    (Ldecompile/IterableClass;)V                                          

accessFlags                   ACC_PUBLIC, ACC_SUPER                                                 
thisClass                     #5		// decompile/IterableClass                                        
superClass                    #6		// java/lang/Object                                               
interfacesCount               1                                                                     
interfaces[0]                 #7		// java.lang.Iterable                                             

fieldsCount                   1                                                                     
fields[0]                     {flags=[ACC_PRIVATE]}, nameIndex=9, descriptorIndex=10, attrCount=0, fieldName='arr', descriptorName='[Ljava/lang/String;'}

methodsCount                  3 

暂时解析到这里吧,属性表和方法表结构的解析实在太复杂,而实际上最难的部分也在这里,除了处理字节游标的位置,确保解析长度刚好覆盖一个结构外,当前解析的结构(如Code属性)中还可能嵌套着多层的结构,容我暂且知难而退,浅尝辄止一回,有机会再回来折腾,代码先放到Github仓库中。

另外,实际上肯定有更好的解析方法,比如说应用层面的ASM,JVM具体实现、字节码可视化工具JavaClassViewer以及我在Github上发现的几个实验性的工程(如classpyClassAnalyzer)等等。TOC

0x04. 参考

  • 《深入理解Java虚拟机》(第二版,周志明著,机械工业出版社)
  • Oracle: The class File Format