Class类文件结构分析 -- 听懂字节码的语言

891 阅读9分钟

前篇 :编译与反编译,让字节码说人话

什么是字节码?

字节码是一种包含执行程序,由一系列 op 代码 / 数据对 组成的二进制文件,是一种中间码。

如何查看字节码?

Java 中的字节码文件,即 .class 文件,直接打开是打不开的,强行查看时会出现这样的乱码 :

所以我们需要借助一点工具。这里是使用了 GHex,直接将 .class 文件拖入其中就可以打开了 :

字节码里都写了点啥?

这就需要从 Class 文件的结构讲起啦。

一个典型的 class 文件分为 魔数、版本号、常量池、访问标志、类索引、父索引、接口索引、字段表、方法表、属性表 这十个部分,用一个数据结构可以表示如下:

类型 名称 干嘛的
u4 magic 魔数
u2 minor_version 次版本号
u2 major_version 主版本号
u2 constant_pool_count 常量池入口,该值代表常量池容量计数值
cp_info constaont_pool 常量池,其中的每一个常量都是一个表
u2 access_flags 访问标志
u2 this_class 类索引
u2 super_class 父索引
u2 interfaces_count 接口索引入口,该值代表索引表的容量,下同
u2 interfaces 接口索引集合,若该类没有实现接口,则不占用任何字节
u2 fileds_count 字段表入口
field_info fileds 字段表集合,用于描述接口或者类中声明的变量
u2 methods_count 方法表入口
method_info methods 方法表集合
u2 attributes_count 属性表入口
attribute_info attributes 属性表集合

javap -v 输出详细附加信息看一下反编译出的代码 :

那就试试看如果自己跟着字节码走一遍,能不能得出相同的结果吧!

首先需要了解两个概念 :

  • 无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1、2、4、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的符合数据结构,所有表都习惯以 " _info " 结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。

简言之,如果将诸如 "CA"、"FE" 的部分称为 "一个字节",当我们对着 Class 的文件结构看字节码文件的时候,当看见类型为 ux,就将 x 个字节视为一个整体;当看见类型为 xxx_info,就需要另外去 xxx_info 这张表中查看具体的对应关系。

魔数

根据 u4,取最前面四个字节,0X CA FE BA BE

直译过来叫“咖啡宝贝”的这个东西,就是我们所谓的“魔数”啦。它相当于一个文件类型标识,代表这个文件的类型是 class 文件。

版本号

根据 u2,向后取两个字节,0X 00 00,代表次版本号;
根据 u2,向后取两个字节,0X 00 34,代表主版本号。

查表可得,这表示当前编译器版本为 JDK1.8.0

常量池

根据 u2,向后取两个字节,0X 00 21,代表常量池容量计数值。

需要注意的是,这个容量计数是从 1 而不是 0 开始的。比如这里的值为 0X21,对应的十进制为 33,那么常量池中的常量总数其实是 33 - 1 = 32 个的。

根据 cp_info,接下来我们需要去查另一张表了!首先看到紧跟其后的第一个字节为 0X 0A,对应十进制为 10,对应表中类型为 CONSTANT\_Methodref\_info,即类中方法的符号引用。

结合之前javap -v得到的结果,可以看出常量池中的第一个常量的确是 Methodref 类型,和我们推导的一致。

通过查询常量池中的14中常量项的结构总表,找到 CONSTANT\_Methodref\_info 对应的结构组成

然后继续向下分析就好啦。

PS :这里要特别提一下 UTF-8 类型的,如下图。其中,0X 01 表示类型为 CONSTANT\_Utf8\_info

查表知,其后紧跟的 0X 00 06 为字符长度,也就是说这个常量的 length 为 6,结合最后一行,向后数 length 个 u1,得到的 OX 3C 69 6E 69 74 3E 即为使用了 UTF-8 缩略编码的这个字符串。

Access_Flag 访问标志

在常量池结束之后,紧接着的两个字节代表访问标志,用来识别一些类或者接口层次的访问信息。包括 :这个 Class 是类还是接口,是否定义为 public 类型,是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等待。

如图,这里我们的访问标志为 0X 00 21,代表 0X 00 200X 00 01 的并集。

类索引、父类索引

类索引 :0X 00 05,父索引 :0X 00 06。它们分别引用两个 u2 类型的索引值,各自指向一个类型为 CONSTANT\_Class\_info 的类描述符常量,而这个常量是存在常量池中的。

回顾一下javap -v得到的代码,可以清晰看到其对应的常量 :这个类为 Demo,而它的父类是 Object。

接口索引集合

对于接口索引集合,入口的第一项 -- u2 类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器为 0(比如我们这个就是 0 ),后面接口的索引表不再占用任何字节。

字段表集合

字段表用来描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内声明的局部变量。

这里我们需要换一个例子来分析,因为字段表这块还蛮重要的,但这个示例里面没有字段,O.O

// 括起来表示这个新示例的作用域只有这么点,后面分析依然是用的之前那个例子

{

临时示例代码 :

package demo;

public class Demo {
    private int a;
}

编译所得结果 :

对应的字节码,红框框起来的是字段表入口 :

好!开始了!首先让我们来看一下字段表结构 :

再结合具体的字节码 :

  • 访问标志值 :0X 00 02,查表得,对应 ACC_PRIVATE,表示该字段为私有属性
  • 字段的简单名称 :0X 00 04,对应常量池中第四个量,a
  • 字段的方法描述符 :0X 00 05,对应常量池中第五个量,I
  • 属性表入口,值为属性个数,永安里存储一些额外的信息,这里是 0X 00 00,说明没有额外信息
  • 属性表,这里为空

其中,方法描述符里的 I 表示 基本类型int。详细的描述符标识字符含义如下表 :

对于数组类型,每一个维度将使用一个前置的[字符来描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为[[Ljava/lang/String;一个整型数组int[]将被记录为[I

}

方法表集合

让我们回到最开始的那个例子。首先是方法表入口: 0X 00 02,说明有两个方法。

接下来的参数分别为 :

  • 访问标志值 :0X 00 01,查表得,对应 ACC_PUBLIC,表示该方法为公有方法
  • 名称索引 :0X 00 07,对应常量池中第七个量 <init>,实质是编译器给类添加的那个默认构造器
  • 描述符索引 :OX 00 08,对应常量池中第八个量 ()V
  • 属性个数 :0X 00 01,表示该方法的属性表集合中有一项属性
  • 属性名称索引 :0X 00 09,对应常量池中第九个量 Code

与字段表集合相对应的,如果父类方法在子类中没有被重写,方法列表集合中就不会出现来自父类的方法信息;但同样,可能会出现由编译器自动添加的方法。

属性表集合

一个符合规范的属性表应该长这个样子:

类型 名称 数量 干嘛的
u2 attribute_name_index 1 属性名称索引
u4 attribute_length 1 属性值的长度;值为整个属性表长度减去 6 个( 指 u2 + u4 个 ) 字节
u1 info attribute_length 属性值

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

类型 名称 数量 干嘛的
u2 attribute_name_index 1 属性名称
u4 attribute_length 1 属性值的长度
u2 max_stack 1 操作数栈深度的最大值
u2 max_locals 1 局部变量表所需的存储空间
u4 code_length 1 存储 Java 源程序编译后生成的字节码指令
u1 code code_length 用于描述方法体里的 Java 代码
u2 exception_table_length 1 异常表入口,该值表示异常表的容量
exception_info exception_table exception_table_length 异常表,允许为空
u2 attribute_count 1 属性表入口,该值表示属性表的容量
attribute_info attributes attribute_count 属性表

这里需要注意的是表中最后三行提到的“异常表”和“属性表”,这里我们还是框起来看一下 :

  • 异常表 :0X 00 00,该表为空
  • 属性表入口 :0X 00 01,指 Code 属性后面还跟了一个属性
  • 属性表名称 :0X 00 0A,对应常量池中第 10 个常量 LineNumberTable

然后接下来再去 LineNumberTable 属性结构表 中查对应的结构就好啦,其他属性值也是同理。

写在最后

虽然乍一看感觉非常复杂的样子,但其实自己跟着走一遍就差不多可以理清啦。有什么地方有问题的,欢迎批评指正与交流呀 (*/ω\*)


参考 :

  1. www.jianshu.com/p/252f381a6…
  2. 《深入理解Java虚拟机 -- JVM高级特性与最佳实践》