概述
Write once, run everywhere!,我们都知道这是 Java 著名的宣传口号。不同的操作系统,不同的 CPU 具有不同的指令集,如何做到平台无关性,依靠的就是 Java 虚拟机。计算机永远只能识别 0 和 1组成的二进制文件,虚拟机就是我们编写的代码和计算机之间的桥梁。虚拟机将我们编写的 .java 源程序文件编译为 字节码 格式的 .class 文件,字节码是各种虚拟机与所有平台统一使用的程序存储格式,这是平台无关性的本质,虚拟机在操作系统的应用层实现了平台无关。实际上不仅仅是平台无关,JVM 也是 语言无关 的。常见的 JVM 语言,如 Scala,Groovy,再到最近的 Android 官方开发语言 Kotlin,经过各自的语言编译器最终都会编译为 .class 文件。适当的了解 Class 文件格式,对我们开发,逆向都是大有裨益的。
Class 文件结构
Class文件中有两种数据结构
无序号表
- 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数。
- 无符号数可以用来描述数字、索引引用、数量值、或者按照UTF-8编码构成字符串值
表
- 由多个无符号数或其他表作为数据项构成的复合数据类型
- 所有表都习惯性地以“_info”结尾
- 表用于描述有层次关系的复合结构的数据。
- 整个Class文件本质上就是一张表
Class文件格式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interface_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
例子
package Test;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
1. 魔数与Class文件的版本
-
魔数
- 每个Class文件的头4个字节称为魔数
- 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件
-
Class文件的版本号
- 紧接着魔数的4个字节存储的是Class文件的版本号
- 前两个字节是次版本号,后两个字节是主版本号
2. 常量池
-
常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,还是在Class文件中第一个出现的表类型数据项目。
-
常量池中常量的数量是不固定的,常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
-
这个计数是从1开始的
-
上图中常量池容量为0x0013,十进制为19,这就代表常量池中有有19个常量,索引为1-19.
-
索引值为0 用来满足后面某些指向常量池的索引值的数据在特定的情况下需要表达“不引用任何一个常量池项目”。
-
常量池中主要存放两大类常量
- 字面量
- 比如文本字符串、被声明final的常量值等
- 符号引用
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 字面量
常量池中常见的11种数据类型
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
- 每个数据类型都有自己的数据结构
3.访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类,是否被声明为final,等等。
4.类索引、父类索引与接口索引集合
- 类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由着三项数据来确定这个类的继承关系
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
- Java 语言是不允许多重继承的 所以父类索引只要一个
- 除了 java.lang.Object 之外,所有的Java类都有父类
- 因此 除了 java.lang.Object 之外,所有 Java 类的父类索引都不为0
- 类索引与父类索引用两个u2类型的索引值表示,他们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过CONSTANT_Class_info 类型中的常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。
- 接口索引集合就是用来描述这个类实现了哪些接口
- 这些被实现的接口将按 implement 语句(如果该类就是一个接口,这应该是 extends 语句)后的接口顺序从左到右排列在接口的索引集合中。
- 对于接口索引集合,入口的第一项是一个u2类型的数据为接口计数器,表示索引表的容量。
5 字段表集合
- 字段表(filed_info)用于描述接口或类中声明的变量。
- 字段 包括了类级变量 或 实例级变量,但不包括在方法内部声明的变
字段表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
- 字段修饰符放在 access_flags 项目中
- 紧跟access_flags 标志的是两项索引值:name_index 和 descriptor_index。他们都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符
- 全限定名:”org/fenixsoft/clazz/TestClass“
- 简单名称:值没有类型和参数修饰的方法或字段名称,如类中的inc()方法和m字段的简单名称分别是 inc 和 m
- 字段和方法的描述符
- 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
- 根据描述符规则,基本数据类型及代表无返回值的void类型都会死用一个大写字符来表示,而对象类型则用字符L加对象的全限定名表示
- 对于数组类型,每一维度将使用一个前置的 “[” 字符来描述,如一个定义为 “java.lang.String[] []” 类型的二维数组,将被记录为: “[[Ljava/lang/String;”,一个整形数组 “int[]” 将会被记录为 “[I”
- 用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格书序放在一组小括号()之内。如方法 void inc() 的描述符为 ()V,方法 java.lang.String toString() 的描述符为 ()Ljava/lang/String;
- 字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
6.方法表集合
- 与字段表集合非常相似
方法表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
7.属性表集合
-
能够被虚拟机识别的属性有9项
-
一个符合规则的属性表应该满足
类型 名称 数量 u2 attribute_name_index 1 u2 attribute_lenght 1 u1 info attribute_lenght
7.1 Code属性
-
Java 程序方法体里面的代码经过 Javac 编译器处理之后,最终变为字节码指令存储在 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 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:一项指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为 “Code”
- attribute_length:代表了属性值的长度,属性长度固定为 整个属性表的长度减去6个字节
- max_stack:代表了操作数栈深度(Java虚拟机栈中的栈帧中的操作数栈)的最大值。
- max_locals:代表了局部变量表(Java虚拟机栈中栈帧中的局部变量表)所需的存储空间。
- max_locals 的单位是 Slot, Solt 是虚拟机为局部变量分配内存所使用的最小单位。
- code_length + code:用来存储Java源程序编译后生成的字节码指令
- code_length:代表字节码长度
- code:用于存储字节码指令的一系列字节流。