前篇 :编译与反编译,让字节码说人话
什么是字节码?
字节码是一种包含执行程序,由一系列 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 20
和 0X 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高级特性与最佳实践》