1. 前言
class文件
作为 JVM 的可执行文件,在可读性方面比 C语言
等直接编译成平台可执行文件的语言强太多,反编译class文件往往能够得到不错的效果。而一个类无论代码的多少,在结构上都大同小异。在源码级,类的结构由上而下大致为:当前类的包名路径、引用类的包名路径、当前类的信息(类名、父类、接口)、变量、方法、内部类/内部接口/枚举/注解等。Javac编译器
也按照该顺序来编译源码。
同时将代码中所使用到的引用类包名路径、变量命名和赋予的值,方法名等信息,均都存储在常量池中。当需要使用时,以索引下标的方式指向常量池中的位置。分析class文件没有任何的技术难度,明白各个数据项的结构,分析起来将会很轻松。
本文以下列源码为例,带大家一起来分析 class 文件 :
public class Person implements Serializable{
private static final long serialVersionUID = 1L;
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
编译为 class 文件后:
Person_class.png
2. 魔数和Class编译版本
-
魔数,CA FE BA BE
固定值,标识文件是否是 class 文件。
-
版本,00 00 00 34
编译JDK版本,minor_version 和 major_version 一起确定class文件格式的版本,前2个字节小版本,后两个字节大版本。
jdk_version.png
3. 常量池
常量池中存储 class文件
中的各种符号引用和字面量,是Class文件的资源仓库,有多全面呢?源码一眼望去,除了各种的关键字和方法体中的字节码指令,其余全部存储在常量池当中,当需要使用时,通过索引下标的方式来引用。常量类型:
Tag | 类型 | 描述 |
---|---|---|
ox1 | CONSTANT_Uft8 | Utf-8编码的字符串 |
ox3 | CONSTANT_Integer | 整型字面量 |
ox4 | CONSTANT_Float | 浮点型字面量 |
ox5 | CONSTANT_Long | 长整型字面量 |
ox6 | CONSTANT_Double | 双精度字面量 |
ox7 | CONSTANT_Class | 类或接口的符号引用 |
ox8 | CONSTANT_String | 字符串类型字面量 |
ox9 | CONSTANT_Fieldref | 字段的符号应用 |
ox10 | CONSTANT_Methodref | 类中方法的符号引用 |
ox11 | CONSTANT_InterfaceMathodref | 接口中方法的符号引用 |
ox12 | CONSTANT_NameAndType | 字段或方法的部分符号引用 |
ox15 | CONSTANT_MethodHand | 表示方法句柄 |
ox16 | CONSTANT_MethType | 标识方法类型 |
ox18 | CONSTANT_InvokedDaynamic | 表示一个动态方法调用点 |
微信截图_20190729095157.png
前两个字节 0x001F
为常量池大小,共有 31 项常量。第 1 项常量 tag 为 07 查上图得知为 class_info 类型,class_info 结构如下:
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
长度为 u2 大小的 name_index 必须指向一个 CONSTANT_Utf8_info 类型的常量。此处 02 指向第二项常量。第 2 项常量 tag 为 01 查表得知为 CONSTANT_Utf8_info 类型常量,结构如下:
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
对应的字节为:01 00 10 + 后面依次16个字节(linked/TestClass )
点击查看更多类型,使用 JDK 自带的 javap 工具,可查看常量池信息:
4. 类信息
4.1 访问标志
常量池结束后,紧接着就是类或接口的信息。前两个字节代表类或接口的访问标志,被哪些关键字修饰:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否声明为public |
ACC_FINAL | 0x0010 | 是否声明为final |
ACC_SUPER | 0x0020 | 是否使用1.2版本编译器(支持使用新的invokespecial指令) |
ACC_INTERFACE | 0x0200 | 接口类型 |
ACC_ABSTRACT | 0x0400 | 抽象类_类型 |
ACC_SYNTHETIC | 0x0100 | 编译器合成,并非用户代码编写 |
ACC_ANNOTATION | 0x2000 | 注解类型 |
ACC_ENUM | 0x4000 | 枚举类型 |
通过ACC_INTERFACE
标志来区分 Class 是类还是接口。如果设置 ACC_INTERFACE ,则 ACC_ABSTRACT、ACC_SUPER、ACC_ENUM 不能设置。
acc_super标志是否使用invokespecial指令,在调用构造方法或使用super.xxx()显示调用父类方法时,会使用该指令。1.2之前invokespecial对方法的调用都是静态绑定的。java1.2 的时候增加了动态绑定的功能。
当前类被声明为 public类型
,使用1.2以上编译器。ACC_PUBLIC
和ACC_SUPER
为真,0x0001 | 0x0020 = 0x0021。
4.2 类索引、父类索引和接口索引
接下来分别是类、父类、接口信息,直接引用常量池中的符号引用,类的全限命名。结构上类信息、父类信息都用一个u2
大小字节引用常量池中的索引。由于接口是可以多实现的,所以先用u2
字节大小记录实现了多少个接口,再依次在后面数几个u2
大小字节。
类型 | 索引 | 常量池 |
---|---|---|
类索引 | 0x0001 | linked/Person |
父类索引 | 0x0003 | java/lang/Object |
Person 类实现了 Serializable 接口,所以 interface_count 等于 1,紧接着 0005
代表引用常量池索引为 5 的常量。得知为 java/io/Serializable
5. 字段表
字段表值包含成员变量,常量会存在常量池当中。
字段表结构:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 修饰符/访问标志 |
u2 | name_index | 1 | 变量命名 |
u2 | descriptor | 1 | 数据类型 |
u2 | attributes_count | 1 | 属性表长度 |
attribute_info | attributes | attributes_count | 属性表 |
修饰符/访问标志:
标志名称 | 标志值 | 含义 | 标志名称 | 标志值 | 含义 |
---|---|---|---|---|---|
ACC_PUBLIC | 0X0001 | 字段是否public | ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_PRIVATE | 0x0002 | 字段是否private | ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_PROTECTED | 0x0004 | 字段是否protected | ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_STATIC | 0x0008 | 字段是否static | ACC_ENUM | 0x4000 | 字段是否enum |
ACC_FINAL | 0x0010 | 字段是否final |
字段类型:
除开 八大基本类型 和 引用类型,还有数组类型,方法还有 Void 类型。
描述符 | 类型 | 描述符 | 类型 |
---|---|---|---|
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 引用类型 |
[ | 数组纬度 |
0x0002 字段表数量,只有二个字段
第一个字段:
- 访问标志 0x001A :private static final 0x02 | 0x08 | 0x10 等于 0x1A
- 变量名 0x0007 :
serialVersionUID
- 变量类型 0x0008 :对应常量池中的
J
,转换为 long 类型。 - 一个属性 0x0001 :详见* 7.1ConstantValue属性分析
第二个字段:
- 访问标志 0x0002:private
- 变量名 0x000C:
name
- 变量类型 0x000D:
Ljava/lang/String;
- 没有属性 0x0000;
6. 方法表
方法的结构有各种修饰符、返回值类型、方法名、参数列表、再就是方法体内的代码。结构如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
微信截图_20190729104127.png
- 0x0003 3个方法
第一个方法:默认构造函数
- 0x0001 访问标志:public
- 0x000E 方法名:init
- 0x000F 返回值类型:V()
- 0x0001 一个属性:00 10 在常量池中
Code属性
,详见 7.2Code属性分析
7. 属性表
字段和方法表中都可以携带自己的属性,JVM 的属性表类型较多,本篇文章只泛泛而谈出现的两种(更多属性):
7.1 ConstantValue属性
同时被 static
、final
关键字修饰的字段,会以 ConstantValue 属性表的结构存储在Class文件种,也就是平时大家说的常量。并且做为类或接口的一部分,存储在运行时常量池当中。格式如下:
ConstantValue_attribute {
u2 attribute_name_index; //属性表名称位于常量池中的位置
u4 attribute_length; //属性表长度大小
u2 constantvalue_index; //常量值位于常量池中的位置
}
接着上面的Class文件分析:
- 00 09 对应常量池中的
ConstantValue
, - 00 00 00 02 :两个字节长度
- 00 0A :1L
7.2 Code属性
方法体中的代码,经Javac编译器处理后存放在方法的Code属性表中。抽象方法和接口不存在Code属性。Code属性结构如下∶
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
}
exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
对应的字节码为:
微信截图_20190729111806.png
使用 javap 命令得出的:
微信截图_20190729104624.png
- 0x0010,
Code
- 0x0001,操作数栈大小
- 0x0001,局部变量表大小
- 0x2A,从表中查询对应的指令为aload_0,该指令将局部变量表的第一个(索引为0)变量推送至栈顶。
- 0xB7,从表中查询对应的指令为invokespecial,方法调用指令,必须声明一个方法命名,可以是构造方法、父类的构造方法、私有方法。后面u2大小,指向常量池中一个CONSTANT_Methodref_info类型的项。
- 0x0011,invokespecial 所执行的方法,对应常量池中的
<init>
方法的符号引用。 - 0x00B1,从表中查询对应的指令为return,当前方法返回 void。
- 0x0000,不存在异常表
code_legth 虽然是一个u4大小的值,理论上可以存储0xffffffff(40多亿)大小的字节码指令,但是虚拟机规范中明确规定限制了一个方法不超过65535条字节码指令,实际只使用了u2(0xffff)大小。
网友评论