ART 虚拟机-Class文件逐字节分析

学习虚拟机的第一步,当然就是分析Class文件啦,直接进入主题,Class文件是经过Java编辑器编译后得到的Java字节码文件,可以看做是Java虚拟机的可执行文件。

1、Class 文件格式

顺序 类型 名称 数量 说明
1 u4 magic 1 魔数,固定为0XCAFEBABE
2 u2 minor_version 1 次版本号
3 u2 major_version 1 主版本号
4 u2 consant_pool_count 1 常量池元素个数
5 cp_info constant_pool constant_pool_count-1 常量池中的元素,索引从1开始
6 u2 access_flags 1 该类的访问权限(public、private等信息)
7 u2 this_class 1 指向常量池的索引,此类的类名
8 u2 super_class 1 指向常量池的索引,父类的类名
9 u2 interfaces_count 1 指向常量池的索引,该类实现了都多少个接口
10 u2 interfaces interfaces_count 指向常量池的索引,该类实现的接口类名
11 u2 fields_count 1 成员变量的数量
12 field_info fields fields_count 成员变量的信息
13 u2 methods_count 1 函数的数量
14 method_info methods methods_count 函数的信息
15 u2 attribute_count 1 该类包含的属性的个数
16 attribute_info attribute attribute_count 该类包含的属性

在Java虚拟机规范里,一个Class文件的结构如上表所示,一共有16项内容,其中u4和u2表示为4个字节长度的无符号整数和2个字节长度的无符号整数。

以一个简单的示例,来挨着解析一下Class文件的结构,测试代码如下,一个类文件,里面只有一个变量和一个方法。

package com.rainbow.pet;

public class ClassFile {

    public int q = 1;

    public int add(int n) {
        int m = 2;
        return m + n;
    }
}
复制代码

使用javac指令,生成对应的Class文件,以及使用javap指令反编译这个Class文件,便于查看对照,生成的结果如下。

image.png

public class com.rainbow.pet.ClassFile
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // com/rainbow/pet/ClassFile
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/rainbow/pet/ClassFile.q:I
   #3 = Class              #17            // com/rainbow/pet/ClassFile
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               q
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               add
  #12 = Utf8               (I)I
  #13 = Utf8               SourceFile
  #14 = Utf8               ClassFile.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // q:I
  #17 = Utf8               com/rainbow/pet/ClassFile
  #18 = Utf8               java/lang/Object
{
  public int q;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC

  public com.rainbow.pet.ClassFile();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field q:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public int add(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_2
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
}
SourceFile: "ClassFile.java"

复制代码

1.1、魔数

按照Class文件的结构,文件的开头是4个字节长度的魔数,每个Class文件开头都是CAFEBABE,没有什么好讲的。

image.png

1.2、次版本号

魔数之后,是两个字节的次版本号 0 。

image.png

1.3、主版本号

次版本号之后是两个字节的主版本号0x37,转成十进制为 55 。 主版本号和次版本号,我们在反编译的Class文件之后,也可以看到minor version: 0 ,major version: 55 。

image.png

public class com.rainbow.pet.ClassFile
  minor version: 0
  major version: 55
复制代码

1.4、常量池

常量池由第四项constant_pool_count(常量池中元素个数)和constant_pool(常量池)组成。常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,其中constant_pool_count是一个u2类型的数据,代表了接着的constant_pool中的元素个数,constant_pool是一个数组(一段连续的空间),数组里面存放的内容为cp_info(常量信息),但是由于这个常量池的容量的计数是从1开始,而不是从0开始,所以实际这个数组里面存放的cp_info的个数为constant_pool_count所表示的数量-1个。设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

可以看到接下来的两个字节表示的是constant_pool_count,显示结果为0x13,转换成十进制为19,按照上述描述,数组计数从1开始,那么应该就一共有18个常量,可以看到一共是有18个常量,接下来就一个一个的挨着看这18个常量是怎么解析出来的。

image.png

Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/rainbow/pet/ClassFile.q:I
   #3 = Class              #17            // com/rainbow/pet/ClassFile
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               q
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               add
  #12 = Utf8               (I)I
  #13 = Utf8               SourceFile
  #14 = Utf8               ClassFile.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // q:I
  #17 = Utf8               com/rainbow/pet/ClassFile
  #18 = Utf8               java/lang/Object
复制代码

常量池中的每一个cp_info都是一个表,这个表到JDK13的时候,一共由17种类型,cp_info的伪代码可以如下理解,具体tag的可能取值也见下表。

cp_info {
    u1 tag; // 每一个常量元素的第一个字节,表示该常量的类型
    ux info; // 根据tag的不同类型,后续的字节长度不定,并表示具体的内容。
}

复制代码

每个常量的第一个字节都是tag,用来确认这个常量具体是什么类型的常量,所以我们继续往下看一个字节,下一个字节是0x0A,根据这个值,查看下表常量类型,可以确认接下来的这个常量类型是CONSTANT_Methodref_info,确认好常量类型之后,再去查找这个类型的常量的具体的数据结构。

image.png

CONSTANT_Methodref_info 类型 说明
tag u1 取值上表所示为 10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类描述符CONSTANT_NameAndType的索引项

可以看到这个类型的常量一共占有5个字节,第一个字节表示类型,后面两个字节都是指向不同类型的索引,继续往下看4个字节,0x0004和0x000F,分别指向常量池的第4项(一个CONSTANT_Class_info类型的常量)和第15项目(一个CONSTANT_NameAndType的常量)。

image.png

可以看到常量池的第一项,确实如此,这样我们就分析完了第一个常量,也知道了常量的解析方法:先根据第一个字节的tag,确认这个常量的类型,再根据常量的类型,确认不同的常量的具体的数据结构,接着分析后续的字节。

#1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
复制代码

所有的常量类型如下

常量类型 标志(tag取值) 说明
CONSTANT_Utf8_info 1(0x01) UFT-8编码的字符串
CONSTANT_Integer_info 3(0x03) 整型常量
CONSTANT_Float_info 4(0x04) 浮点数常量
CONSTANT_Long_info 5(0x05) 长整型常量
CONSTANT_Double_info 6(0x06) 双精度浮点常量
CONSTANT_Class_info 7(0x07) 类或者接口的引用
CONSTANT_String_info 8(0x08) 字符串,并不存储字符串的具体内容,只存了一个索引
CONSTANT_Fieldref_info 9(0x09) 类中成员变量的符号引用
CONSTANT_Methodref_info 10(0x0A) 类中成员函数的符号引用
CONSTANT_InterfaceMethodref_info 11(0x0B) 类中接口函数的符号引用
CONSTANT_NameAndType_info 12(0x0C) 描述类的成员域或成员函数相关的信息
CONSTANT_MethodHandle_info 15(0x0F) 成员句柄,描述MethodHandle的信息,和反射有关
CONSTANT_MethodType_info 16(0x10) 描述成员函数的信息,只包括函数的参数类型和返回值类型
CONSTANT_Dynamic_info 17(0x11) 描述一个动态计算常量
CONSTANT_InvokeDynamic_info 18(0x12) 描述一个动态方法的调用点
CONSTANT_Module_info 19(0x13) 描述一个模块
CONSTANT_Package_info 20(0x14) 描述一个模块中开放或者导出的包

每一种常量的类型,都对应着不同的数据结构

image.png

image.png

image.png

image.png

在上面我们已经了解了具体的常量类型和不同的常量类型的数据结构,接着看下面的剩余的常量

image.png

第二个常量的tag值是0x09,查表得是一个CONSTANT_Methodref_info,一共占有5个字节,后续得两个字节也是索引,分别指向0x0003(第3个常量)和0x0010(第16个常量)

#2 = Fieldref           #3.#16         // com/rainbow/pet/ClassFile.q:I
复制代码

image.png

第三个常量的tag是0x07,查表得是一个CONSTANT_Class_info,一共占有3个字节,后续两个字节也是索引,指向常量池0x0011(第17个常量)

#3 = Class              #17            // com/rainbow/pet/ClassFile
复制代码

image.png

第四个常量的tag是0x07,查表得是一个CONSTANT_Class_info,一共占有3个字节,后续两个字节也是索引,指向常量池0x0012(第18个常量)

 #4 = Class              #18            // java/lang/Object
复制代码

image.png

第五个常量的tag是0x01,查表得是一个CONSTANT_Utf8_info,数据结构的第2、第3个字节0x0001,表示这个utf8字符串的长度,可值这个字符串长度只有一个字节,那么接下来的那个字节则表示的是这个字符串的内容0x71,转换成ascii码为:“q”

#5 = Utf8               q
复制代码

按照第五个常量CONSTANT_Utf8_info的解析方法,第一个字节是tag,第二第三个字节表示字符串的长度,后续的字符串表示具体的内容,我们解析出后续的第6到第14个字符串常量。

image.png

#6 = Utf8               I
复制代码

image.png

 #7 = Utf8               <init>
复制代码

image.png

 #8 = Utf8               ()V
复制代码

image.png

#9 = Utf8               Code
复制代码

image.png

 #10 = Utf8               LineNumberTable
复制代码

image.png

#11 = Utf8               add
复制代码

image.png

 #12 = Utf8               (I)I
复制代码

image.png

 #13 = Utf8               SourceFile
复制代码

image.png

#14 = Utf8               ClassFile.java
复制代码

image.png

第15个常量的tag为0x0C,查表可知是CONSTANT_NameAndType类型的常量,这个常量的数据结构,后面的0x0007和0x0008均为索引,指向常量池的第7项(<init>)和第8项(()V),同理可以得出第16个常量 。

#15 = NameAndType        #7:#8          // "<init>":()V
复制代码

image.png

 #16 = NameAndType        #5:#6          // q:I
复制代码

image.png

第17个常量的tag是0x01,可知是之前分析过的CONSTANT_Utf8_info,第18个常量也是,这里直接给出对应的结果

image.png

#17 = Utf8               com/rainbow/pet/ClassFile
复制代码

image.png

 #18 = Utf8               java/lang/Object
复制代码

到此为止,整个常量池就分析完了,可以看到按照表格的数据,解析出来的结果,和使用javap指令,直接解析出来的结果是一致的。

1.5、访问标志

按照Class文件的结构,在常量池结束之后,为两个字节长度(u2)的访问标志(access_flags),用于标识这个类或者接口的访问信息,多个标志取或得到的结果为这两个字段的结果。

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否为final类型
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,jdk 1.0.2之后,这个标志都必须为真
ACC_INTERFACE 0x0200 是否为一个接口
ACC_ABSTRACT 0x0400 是否为abstact类型,对于接口或者抽象类,此标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

image.png

访问标志为0x0021,查看上表,可以知道是ACC_PUBLIC和ACC_SUPER或起来的结果,ACC_SUPER必定为真 ,可知道这个类的可访问下为pulic。

public class com.rainbow.pet.ClassFile
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // com/rainbow/pet/ClassFile
  super_class: #4                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
复制代码

1.6、类索引、父类索引

在访问标志结束之后,是两个字节长度(u2)的类索引(this_class),类索引指向一个CONSTANT_Class_info的类描述符常量,这个类描述符常量里面的索引值才指向CONSTANT_Utf8_info类型的常量中的具体的字符串。同理,接下来的父类索引跟类索引是一样的数据结构。

image.png

0x0003是当前类的索引,指向第三个常量,而第3个常量,按照之前的分析,指向第17个常量,查看第17个常量,得到当前类的名字“com/rainbow/pet/ClassFile”,同理可得父类名字“java/lang/Object”。

Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         // com/rainbow/pet/ClassFile.q:I
   #3 = Class              #17            // com/rainbow/pet/ClassFile
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               q
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               add
  #12 = Utf8               (I)I
  #13 = Utf8               SourceFile
  #14 = Utf8               ClassFile.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = NameAndType        #5:#6          // q:I
  #17 = Utf8               com/rainbow/pet/ClassFile
  #18 = Utf8               java/lang/Object

复制代码

1.7、接口数、接口索引

在类索引和父类索引之后,为接口数和对应的接口的索引,接口数为这个类实现了多少个接口,如果有N个,那么接下来就由n个长度为u2的接口的索引,接口索引的查找同类索引。从 字节码中可知道,两个字节的接口数为0x0000,及这个类没有实现任何接口,因此接口索引也就没有了。

image.png

1.8、字段数和字段表

接下来的是长度为两个字节长度(u2)字段数量(field_count)和field_count个字段表(field_info)。字段表用于描述类级别的变量,但是不包括方法内部的局部变量。

image.png

类级别的变量数量field_count为0x0001,说明这个类有一个成员变量,类的成员变量的数据结构如下

image.png

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

access_flags用于描述这个类变量前面的修饰符,例如是Public还是Private,是否是static等等,可选项如下表

标志名称 标志值 含义
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

name_index和descriptor_index均是对常量池的引用,分别表示和这个变量的简单名称和描述符。简单名称顾名思义就是定义的字段的名字,描述符是用来描述这个字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值,描述符的含义如下表:

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,例如Ljava/lang/object
[ 数组,例如int[]会被记录成[I, int[][]会被记录成[[I

attributes_count和attributes_info为属性表,表示一些额外的信息,在后续的属性表中介绍。

第1、2个字节为access_flags,为0x0001,查看下表可指标志为ACC_PUBLIC,这个变量是public的,第3、4个字节name_index是名字的索引为0x0005,指向常量池的第5个常量(q),第5、6个字节为descriptor_index是描述的索引为0x0006,指向常量池的第6个常量(I),是一个int类型。第7、8个字节是属性表的数量attributes_count为0x0000,这个变量没有额外的属性,故后续也就没有attributes_info这个字段了。

#5 = Utf8               q
#6 = Utf8               I
复制代码
{
  public int q;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC
}

复制代码

1.9、方法数和方法表

接下来的是长度为两个字节长度(u2)方法数量(method_count)和method_count个方法表(method_info)。方法表的结构基本等同于字段表:

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

因为volatile和transient不能修复方法,故在access_flags里面没有这两个可选项,但是会多出来一个方法特有的可选项,如synchronized、native、strictfp和abstract。

标志名称 标志值 含义
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 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICT 0x0800 方法是否为stirctfp,精确浮点数
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生

方法表中可能会出现编译器自动添加的方法,最常见的便是类构造器<clinit>()方法和实例构造器<init>()方法

image.png

可知道method_count的值为0x0002,说明这个类有两个方法,但是实际我们只写了一个add()方法,还有一个方法是编译器自动添加的构造方法。

接下来分析第一个方法,按照上面method_info的数据接口,第1个u2 access_flags 为0x0001为ACC_PUBLIC,第2个 u2 name_index 0x0007 为索引指向常量池的第7个(<init>),第3个 u2 descriptor_index 0x0008 为索引指向常量池的第8个(()V), 由此我们已经可以知道,第一个方法为这个类的构造方法,可见性为public,返回值为void。

image.png

 #7 = Utf8               <init>
 #8 = Utf8               ()V
{ 
   public com.rainbow.pet.ClassFile();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field q:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4
}
 
复制代码

第4个 u2 attributes_count 为0x0001 说明这个方法带有一个属性,具体的属性的数据结构可以先查看1.2.0。

image.png

第二个方法,按照上面的分析的方法,可以知道第二个方法为定义的add方法,其中第12个常量,表示这个方法接受一个int类型的参数,返回值也是一个int值。

image.png

#11 = Utf8               add
#12 = Utf8               (I)I
{
  public int add(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_2
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
}


复制代码

1.2、属性数和属性表

整个class文件最后两个字段就是属性数(attributes_count)和attributes_count个属性表(attributes_info)了,在字段表和方法表中,最后两个字段也是属性数和属性表,以描述某些场景下的额外的信息。

属性表类似于常量表,每个属性的具体数据结构是不同的,但是所有的属性表的前两个字段是一样的,通用的属性表可以表示成如下结构:

类型 名称 数量 含义
u2 attribute_name_index 1 属性表名字的索引,查找常量池中具体的名字,根据名字确定是哪个属性
u4 attribute_length 1 属性表具体的长度,整个属性表的长度为attribute_length + 6个字节
u1 info attribute_length 具体的某个属性的信息,长度为attribute_length个字节
属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 由final关键字定义的常量值
Deprecated 类,方法,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常 
 EnclosingMethod 类文件 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 
InnerClass 类文件 内部类列表 
 LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系 
 LocalVariableTable Code属性 方法的局部变量描述 
 StackMapTable Code属性 JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 
 Signature 类,方法表,字段表  用于支持泛型情况下的方法签名
 SourceFile 类文件 记录源文件名称 
 SourceDebugExtension 类文件 用于存储额外的调试信息 
 Synthetic 类,方法表,字段表 标志方法或字段为编译器自动生成的  
 LocalVariableTypeTable 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 
 RuntimeVisibleAnnotations 类,方法表,字段表 为动态注解提供支持 
 RuntimeInvisibleAnnotations 表,方法表,字段表 用于指明哪些注解是运行时不可见的 
 RuntimeVisibleParameterAnnotation 方法表 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
 RuntimeInvisibleParameterAnnotation 方法表 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
 AnnotationDefault 方法表 用于记录注解类元素的默认值 
 BootstrapMethods 类文件 用于保存invokeddynamic指令引用的引导方式限定符
 RuntimeVisibleTypeAnnotations 类,方法表,字段表,Code属性 指明哪些类注解是运行时可见的 
 RuntimeInvisibleTypeAnnotations 类,方法表,字段表,Code属性 指明哪些类注解是运行时不可见的 
 MethodParameters 方法表 用于支持将方法名称编译进Class文件中,并可运行时获取 
 Module 用于记录一个Module的名称和相关信息(requires、export、opens、uses、provides) 
 ModulePackages 用于记录一个模块中所有被exports或者opens的包 
 ModuleMainClass 用于指定一个模块的主类 
 NestHost 用于支持嵌套类的反射和访问控制的api,一个内部类通过该属性知道自己的宿主类
 NestMembers 用于支持嵌套类的反射和访问控制的api,一个宿主类通过该属性得知自己有哪些内部类

常用的属性表

以Code属性为例,看一个具体的属性的数据结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

其中attribute_name_index指向常量池中一个CONSTATN_Uft8_info的常量,固定为“Code”,代表了这个属性的名称,接下来是属性值的长度attribute_length,及除了attribute_name_index 和 attribute_length 之后,所有的其他字段加起来是 attribute_length 个字节,整个Code属性表的长度为attribute_length + 6 个字节长。

接下来的max_stack和max_locals表示了操作数栈(Operand Stack)深度的最大值和局部变量所需要的存储空间。code_length和code表示了方法具体的代码翻译成指令之后有多少条,以及具体的指令是什么。

exception_table_length 和 exception_info 是代码里的try{}catch(){}finally{},翻译过来的异常表,在最后这个Code的属性表又带了一个属性表数量和属性表的字段,意思是属性表里面还可以套其他属性表,Code下面可以带其他的属性。

这里只展示了一个具体的属性的数据结构,因为属性太多了,具体分析的时候,可以根据attribute_name_index确认了是哪个属性,然后去查找对应的属性的数据结构,按照Code属性一样的分析方式即可。

属性表的分析跟常量池的分析类似,先根据前两个字节,确认是哪个属性,再去查具体的属性的数据结构,在上一节,两个方法都带有属性表,依次分析一下

image.png

前两个字节0x0009表示attribute_name_index为指向常量池的索引,查找第9个常量为Code,可以知道这个属性为Code,查看上述的Code属性的数据结构,接下来的四个字节表示attribute_length为0x00000026,转成十进制为38,后续就还有38个字节长度。

image.png

#9 = Utf8               Code
复制代码

按照Code属性的数据结构,接下来的2个字节为max_stack 0x0002 表示此方法操作数栈(Operand Stack)深度的最大值为2,接着的2个字节为max_locals 0x0001 表示此方法局部变量所需要的存储空间为1的槽,接下来的4个字节为code_length 0x0000000A 表示此方法的指令码的长度为10个字节,根据文末的虚拟机字节码指令表查找这些指令,分别为0x2A(aload_0) 、0xB7(invokespecial) 0x0001 (#1索引,指向第1个常量)、0x2A(aload_0)、0x04(iconst_1)、0xB5(putfield) 0x0002 (#2索引,指向第2个常量) 、0xB1(return),有些指令码带有操纵,就会多占一些字节。

image.png

接下来的2个字节是exception_table_length异常表的长度为0,及没有异常处理。

image.png

这个Code属性表,最后又套娃了1一个属性表,attributes_count 长度为 1,又按照属性表的属性结构查看后续的两个字节为attribute_name_index 0x000A 指向第10个常量 LineNumberTable,接着又是4个字节长度的attribute_length 0x0000000A 为10个字节长度。

image.png

line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合, line_number_info表包含start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源 码行号。

image.png

image.png

接下来的0x0002为line_number_table_length,表示这个line_number_table里面有两个line_number_info。

image.png

查看第1个line_number_info,0x0000 为 start_pc 字节码行号0,0x0003 为 line_number 为Java源代码行号

image.png

查看第2个line_number_info,0x00004 为 start_pc 字节码行号0,0x0005 为 line_number 为Java源代码行号。

image.png

上述一个方法就分析完成了,可以看到跟下面的代码是匹配一致的。


 #10 = Utf8               LineNumberTable

{
  public int q;
    descriptor: I
    flags: (0x0001) ACC_PUBLIC

  public com.rainbow.pet.ClassFile();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #2                  // Field q:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4
}
复制代码

image.png

{
  public int add(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: iconst_2
         1: istore_2
         2: iload_2
         3: iload_1
         4: iadd
         5: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
}

复制代码

第2个自定义的add()方法,可以按照上述的第1个方法进行分析,不再赘述。

整个这个类的Java的字节码,就剩最后一点点没有分析啦,按照Class文件格式,在方法表结束之后,是这个类的属性表,也就是1.1里面的第15项和第16项,第15项是两个字节长度,也就是0x0001,说明后续只有一个属性了,继续按照属性表的通用格式接下来的两个字节attribute_name_index 0x000D,指向常量池中的第13项为SourceFile,查看SourceFile属性的数据结构如下。


  #13 = Utf8               SourceFile
  #14 = Utf8               ClassFile.java

SourceFile: "ClassFile.java"

复制代码

image.png

接下来的4个字节为attribute_length 0x00000002,表示后续还有两个字节长度的sourcefile_index 0x000E 指向常量池中的第14个常量ClassFile.java。

image.png

第一次完整的挨着一个字节一个字节的分析完了一个Class文件,比较麻烦的就是字段表、方法表、属性表,因为这几个表都可以套娃属性表,就会比较冗长一些。

虽然已经很多人写过了,但是自己去分析的时候,才能更深入的进行一下,但是由于示例很简单,还有很多的属性、字段都还没有分析到,不过掌握了分析的方法,没有遇到的属性、字段等,也可以按照相同的方式方法进行分析即可。

2、虚拟机字节码指令表

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3、参考文档

《深入理解Java虚拟机》

猜你喜欢

转载自juejin.im/post/7216613455192997948