Class文件与Dex文件
1.class文件基本概念
是一种8位字节的二进制流文件,其中各个数据按照顺序紧密排列,没有间隙;每个类或者接口都单独占据一个class文件。
通过 javac Hello.java
即可生成class文件。
javac -target 1.8 -source 1.8 Hello.java 可以指定Java代码版本和要编译运行的目标版本。
class 文件中只有两种数据类型无符号数和表,
无符号数用于描述数字、索引、数量值、按UTF8编码构成的字符串值。使用u1,u2,u4,u8 分别代表1一个字节,2个字节,4个字节。而一个字节有8位可表示两个十六进制数,u1类型例如"CA" ,u2类型例如:"CA FE",u4类型例如:"CA FE BA BE"。
表用于描述更复杂的结构,比如字段、方法、属性等,通常以"_info"结尾,表中的数据项也是用无符号数表示。
例如: field_info 表由access_flags(u2),name_index(u2),descriptor_index(u2),attributes_count(u2) 四项数据紧密排列构成,相当于一个field_info表占8个字节。
2.class文件结构
一个class文件主要包含以下内容:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool[0] | constant_pool_count-1 |
constant_pool[constant_pool_count-1] | ||
u2 | access_flag | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interface_count | 1 |
u2 | interfaces[1] | interface_count |
interfaces[interface_count] | ||
u2 | fields_count | 1 |
field_info | fields[0] | fields_count-1 |
fields[fields_count-1] | ||
u2 | methods_count | 1 |
method_info | methods[0] | methods_count-1 |
methods[methods_count-1] | ||
u2 | attributes_count | 1 |
attribute_info | attributes | |
2.1 magic
魔数:用于文件类型身份识别表示该文件是否能被虚拟机接收,class文件中的值为:0xCAFEBABE
魔数作用和后缀名类似,但后缀名容易修改,使用魔数进行文件类型识别更安全。
2.2 class文件版本
早期JDK使用 1.主版本.次版本 的方式命名版本,例如 JDK 1.1.4;到Java 1.5开始使用Java SE 5.0 (1.5.0) 这种方式命名
minor_version 表示次版本号,值在0-65535之间。
major_version 表示主版本号,JDK 1.1从45开始,之后每个大版本+1,例如:JDK 1.8 就是52
2.3 常量池
常量池在class文件中相当于实际数据存储部分,而其他表更多的表示了class文件的结构,其各数据项都通过索引指向常量池。
常量池中存储了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
constant_pool_count 代表常量池中常量的容量,该值有偏移,实际计数索引是从1开始的,第一项常量的索引就是1,比如:0x0028 代表十进制40,实际常量池中有39项常量。将第0项空出来用于,在有些表的数据需要表示“不引用任何一个常量池项目”的含义时,可将其数据索引置为0。 在class文件中其他集合类型都是从0开始计数的。
常量池中的内容有14种常量类型,基本结构都是 tag+值 或者 tag+索引 类型,tag是每种类型的标记是固定值。
(1) CONSTANT_Utf8_info
tag | u1 | 1 |
---|---|---|
length | u2 | utf8编码的字符串占用的字节数 |
bytes[length] | u1 | 以utf8编码的字符,每个字符长度为u1 |
(2) CONSTANT_Integer_info
tag | u1 | 3 |
---|---|---|
bytes | u4 | 按照高位在前存储的int值 |
(3)CONSTANT_Float_info
tag | u1 | 4 |
---|---|---|
bytes | u4 | 按高位在前存储的float值 |
(4)CONSTANT_Long_info
tag | u1 | 5 |
---|---|---|
bytes | u8 | 按高位在前存储的Long值 |
(5)CONSTANT_Double_info
tag | u1 | 6 |
---|---|---|
bytes | u8 | 按高位在前存储的Double值 |
(6)CONSTANT_Class_info
tag | u1 | 7 |
---|---|---|
name_index | u2 | 指向全限定名常量的索引 |
(7)CONSTANT_String_info
tag | u1 | 8 |
---|---|---|
string_index | u2 | 指向字符串字面量的索引 |
(8)CONSTANT_Fieldref_info
tag | u1 | 9 |
---|---|---|
class_index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 |
name_and_type_index | u2 | 指向字段描述符CONSTANT_NameAndType_info的索引项 |
(9)CONSTANT_Methodref_info
tag | u1 | 10 |
---|---|---|
class_index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 |
name_and_type_index | u2 | 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项 |
(10)CONSTANT_Interface_Methodref_info
tag | u1 | 11 |
---|---|---|
class_index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 |
name_and_type_index | u2 | 指向方法名称及类型描述符CONSTANT_NameAndType_info的索引项 |
(11)CONSTANT_NameAndType_info
tag | u1 | 12 |
---|---|---|
class_index | u2 | 指向该字段或方法名称常量项的索引 |
descriptor_index | u2 | 指向该字段或方法描述符常量项的索引 |
以上是11种常见的常量类型,下面还有3种
(12)CONSTANT_Method_Handle_info
tag | u1 | 15 |
---|---|---|
reference_kind | u1 | 值在1-9之间,决定了方法句柄的类型,影响方法句柄的字节码行为 |
reference_index | u2 | 对常量池的有效引用 |
(13)CONSTANT_Method_Type_info
tag | u1 | 16 |
---|---|---|
descriptor_index | u2 | 对常量池的有效引用,方法的描述符 |
(14)CONSTANT_Invoke_Dynamic_info
tag | u1 | 18 |
---|---|---|
bootstrap_method_attr_index | u2 | 值必须是对当前Class文件中引导方法表的bootstrap_method[]数组的有效索引 |
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,表示方法名和方法描述符 |
2.4 访问标志符
access_flags 长度为u2,标识该类或者接口的访问信息,0x0000 一共四位,
0x0001:ACC_PUBLIC 代表是否为public类型
0x0010:ACC_FINAL 代表是否为final类型
0x0020:ACC_SUPER 代表是否允许使用invokespecial字节码指令的新语义,在jdk1.0.2之后编译的类该位不能为0。
0x0200:ACC_INTERFACE 代表这是一个接口
0x0400:ACC_ABSTRACT 代表是否是抽象类类型
0x1000:ACC_SYNTHETIC 标志这个类不是由用户代码产生的
0x2000:ACC_ANNOTATION 代表这是一个注解
0x4000:ACC_ENUM 代表这是一个枚举
2.5 类索引
this_class 长度为u2,标志该类的全限定名称。
2.6 父类索引
super_class 长度为u2,标志该类的父类的全限定名称。
2.7 接口索引
接口索引是个数组列表,分为数量+若干item。
interfaces_count长度为u2,标志实现接口数量。
interfaces[0] 每个item长度为u2。
…...
interfaces[interfaces_count-1]
2.8 字段表集合
字段表记录了接口或类中声明的变量,包括类变量和实例变量。
fields_count 长度为u2,标识了字段个数。
field_info fields[0] 代表第1个字段,每个字段由5部分组成。
access_flags 访问控制符,长度为u2,可设置值和具体含义如下:
0x0001:ACC_PUBLIC 代表字段是否为public
0x0002:ACC_PRIVATE 代表字段是否为private
0x0004:ACC_PROTECTED 代表字段是否为protected
0x0008:ACC_FINAL 代表字段是否为final 类变量
0x0040:ACC_VOLATILE 代表字段是否是volatile 并发可见行
0x0080:ACC_TRANSIENT 代表字段是否是transient 是否可序列化
0x1000:ACC_SYNTHETIC 代表字段是否由编译器自动产生
0x4000:ACC_ENUM 代表字段是 enum
这里面public private protected 是互斥的 但可以和 final进行组合,组合值为相加值。
name_index 字段简单名称索引,长度为u2,简单名称即方法名去掉括号的部分或字段不包含修饰符的部分,如get()方法就是get,public String name 字段简单名称就是 name。
descriptor_index 字段描述符索引,长度为u2,描述字段的类型,使用特定标志字符代表类型。
基本类型:byte类型使用 B, char类型使用 C,double类型使用 D,float类型使用 F,int类型使用 I,long类型使用 J,short类型使用 S,boolean类型使用 Z,
对象类型:使用L加对象全限定名称表示 Ljava/lang/Object
数组类型:加[作为前缀,几维数组就加几个[ ,int[] 用[I][][ 表示。
attributes_count 属性长度,u2类型,当值为0时没有后面属性表。
attribute_info attributes 属性表,属性表给予了更多灵活性,一个属性分为以下三部分
(1)attribute_name_index 属性名称索引,指向常量池,长度为u2
(2)attribute_length 该属性占用位数,长度为u4
(3)info 属性具体信息,这部分不同属性长度不一样 如ConstantValue属性,标记该属性是静态变量,即方法变量使用ConstantValue属性进行初始化,而实例变量在构造函数中初始化。
2.9 方法表集合
方法表中记录了类中的方法信息,方法表外层结构和字段表类似,主要由以下几部分组成:
methods_count 标记了该字节码中方法数量,长度为u2
method_info methods[0] 代表第1个方法,每个方法同样有5部分组成:
access_flags u2 方法访问标记符,方法的访问标记和字段的不同,具体如下:
0x0001:ACC_PUBLIC 代表方法是否为public
0x0002:ACC_PRIVATE 代表方法是否为private
0x0004:ACC_PROTECTED 代表方法是否为protected
0x0008:ACC_FINAL 代表方法是否为final 类变量
0x0020:ACC_SYNCHRONIZED 代表方法是否synchronized
0x0040:ACC_BRIDGE 代表方法是否是由编译器产生的桥接方法
0x0080:ACC_VARARGS 代表方法是否接受不定参数
0x0100:ACC_NATIVE 代表方法是否是native
0x0400:ACC_ABSTRACT 代表方法是否是abstract
0x0800:ACC_STRICTFP 代表方法是否为strictfp
0x1000:ACC_SYNTHETIC 代表方法是否是由编译器自动产生的
name_index 字段简单名称索引,长度为u2,简单名称即方法名去掉括号的部分或字段不包含修饰符的部分,如get()方法就是get,public String name 字段简单名称就是 name。
descriptor_index 字段描述符索引,长度为u2,描述方法的签名,先参数列表后返回类型,
String get(int i) 的方法签名为 (I)Ljava/lang/String 参数列表在括号内。
attributes_count 属性长度,u2类型,当值为0时没有后面属性表。
(1)attribute_name_index 属性名称索引,指向常量池,长度为u2
(2)attribute_length 该属性占用位数,长度为u4
(3)info 属性具体信息,
方法的具体逻辑就 存储在Code属性当中。
属性表内容后面补充。
2.10 Class文件属性集合
以上字段表和方法表中都存在属性表,而在class文件最后面也存在一张属性表用于描述一些专有信息。
attributes_count 属性长度,u2类型,当值为0时没有后面属性表。
(1)attribute_name_index 属性名称索引,指向常量池,长度为u2
(2)attribute_length 该属性占用位数,长度为u4
(3)info 属性具体信息
3.class文件弊端
(1)内存占用大,不适用于移动端
(2)堆栈的加载模式,加载速度慢
(3)文件IO操作多,类查找慢
4. Dex文件基本概念
dex是能够被DVM识别,加载并执行的文件格式。dex文件记录了整个工程的所有类信息。
4.1 生成dex文件
我们可以通过as自动帮我们生成dex文件,也可以手动使用dx命令来生成dex文件,并在手机中运行dex文件。
dx命令在sdk/build-tools/29.0.0 下,选择一个较高版本。在用户目录下 .bash_profile 文件中添加环境变量,source .bash_profile 使其生效。
指定生成的目标文件名称为Hx.dex 以及使用的源字节码文件为Hello.class
dx --dex --output Hx.dex Hello.class
4.2 运行dex文件
将生成的dex文件push进模拟器或者手机中,adb push Hx.dex /sdcard
在adb shell 中调用dalvikvm命令,dalvikvm -cp Hx.dex Hello
就可以运行该文件。
5.Dex文件结构
以上是使用010editor展示的Hx.dex文件结构,以下是源代码,我们将对源码是如何存储在dex文件中的做一一分析。
import java.io.Serializable;
public class Hello implements Serializable {
public int b = 123;
public int c = 2343423;
public Integer d = 399394934;
public static final String a = "aaa";
public static void main(String[] sb) {
System.out.println("Hello, Android!\n");
}
private void dealData() {
System.out.println("DealData!!!");
}
}
5.1 Header区域
头部区域包含了dex文件整体结构
字段名称 | 偏移量 | 长度 | 当前dex文件中字段值 | 字段描述 |
---|---|---|---|---|
magic | 0x0 | 0x8 | dex 035 | dex魔术字固定值为dex\n035 |
checksum | 0x8 | 0x4 | E2AE87EA | Alder32算法,是去除magic和checksum字段之外剩余所有内容的校验码 |
signature | 0xC | 0x14 | E0872BBF123351DD2F8D 94FC2EFC8BE7A94C1292 | sha-1签名,去除了magic、checksum和signature之外所有内容的签名。 |
file_size | 0x20 | 0x4 | 1080 | 整个dex文件的字节大小 |
header_size | 0x24 | 0x4 | 112 | header部分的字节大小 |
endian_tag | 0x28 | 0x4 | 0x12345678 | 字节序(大尾方式,小尾方式)默认为小尾方式,即0x12345678 |
link_size | 0x2C | 0x4 | 0 | 链接段大小,默认为0表示静态链接 |
link_off | 0x30 | 0x4 | 0 | 链接段开始偏移 |
map_off | 0x34 | 0x4 | 908 | map_item偏移 |
string_ids_size | 0x38 | 0x4 | 26 | 字符串列表中字符串个数 |
string_ids_off | 0x3C | 0x4 | 112 | 字符串列表偏移字节 |
type_ids_size | 0x40 | 0x4 | 10 | 类型列表中类型个数 |
type_ids_off | 0x44 | 0x4 | 216 | 类型列表偏移字节 |
proto_ids_size | 0x48 | 0x4 | 4 | 方法声明列表中的个数 |
proto_ids_off | 0x4C | 0x4 | 256 | 方法声明列表偏移字节 |
filed_ids_size | 0x50 | 0x4 | 5 | 字段列表中的个数 |
filed_ids_off | 0x54 | 0x4 | 304 | 字段列表偏移字节 |
method_ids_size | 0x58 | 0x4 | 6 | 方法列表中的个数 |
method_ids_off | 0x5C | 0x4 | 344 | 方法列表偏移字节 |
class_defs_size | 0x60 | 0x4 | 1 | 类定义列表中的个数 |
class_defs_off | 0x64 | 0x4 | 392 | 类定义列表偏移字节 |
data_size | 0x68 | 0x4 | 656 | 数据段的大小 |
data_off | 0x6C | 0x4 | 424 | 数据段偏移字节 |
表中字段值部分处理成了十进制,处理过程涉及十六进制转换和字节序的概念。以下举例分析:
这就是小尾方式,即低位字节在前,高位字节在后,78 56 34 12
实际上是12 34 56 78
但在一个字节内部依旧是高位在前低位在后,注:一个字节8位,一个十六进制数是4位,所以用两个十六进制数代表一个字节。
再比如数据区域大小,在dex文件中为 90 02 00 00
调整顺序后为 0x00000290 ,转换为十进制为0+9*16+2*16*16=656
5.2 String_ids
字符串索引区记录了dex文件中每个字符串在数据区中的位置,每个索引占据4个字节,如下图所示:
虽然索引区只保存了索引,但010editor为了方便显示,将该字符串在数据区中的真实值也拿了过来。
比如第3个字符串Hello, Android! 在索引区的值为57 02 00 00
,调整顺序后为00 00 02 57
说明该字符串在dex文件0x0257 这个位置,我们来验证一下
实际数据从0025h行7列开始,符合我们的预期。
在此顺便解析一下该字符串在数据区中存储方式,Dalvik中使用uleb128、uleb128p1、sleb128 三种编码表示32位整形数值。因为在java中通常使用32位表示一个整数,宽度不可变,不论实际数值多大都要分配32位空间。但在手机中存储空间比较有限,使用可变宽度的数据存储方式能更有效利用空间。这里我们不做详细讨论,先了解uleb128的两个特征,
一是其占用的字节数是不确定的,长度有可能在1到5个字节之间变化,
二是以字节中最高位是否为0来表示字节流有没有结束的。
上图中字符都是英文字符,都使用1个字节进行描述。
String在数据区中的存储
string_id 存储了字符串在数据区中的开始位置,比如 F1 02 00 00 就是在 0x02F1 这个位置,数据部分的第一个字节 0x12 代表存储该字符串需要字节个数为 2+1*16=18个 ,但其实下面的data部分有19个,最后一个字节为00 代表结束符。
5.3 Type_ids
这块区域存储在Dex中的类型,其中的值都是索引,指向字符串列表中的位置。同样这块区域的偏移位置和尺寸也都由header中的 字段决定。
type_ids_size 0x40 0x4 10 类型列表中类型个数
type_ids_off 0x44 0x4 216 类型列表偏移字节
这里type_id_item中包含了descriptor_idx 指向字符串列表的索引。
5.4 Proto_ids
这块区域存储了Dex中方法原型的声明列表,方法原型就是入参和返回类型 这里每个方法原型的结构如下:
shorty_idx u4 是该方法原型名称,这个索引值指向String列表中的位置。LI代表返回类型为String 入参为int
return_type_idx u4 是该方法原型的返回类型,这个索引值指向Type列表中的位置。
parameters_off u4 是该方法原型的参数列表在dex文件中的偏移位置,这个变量位置之后,一个proto_id_item 就结束了。
参数列表部分,由参数格式和参数类型构成。
size 代表参数个数。
type_item 代表一个参数类型,通过type_idx 指向具体typelist中的索引。
5.5 Field_ids
这块区域存储了Dex中的成员字段,结构如下图:
每个field_id_item 大小固定,由以下三部分组成。
class_idx u2 字段所在的Class,指向Type_ids 列表。
type_idx u2 字段类型,指向Type_ids 列表。
name_idx u4 字段名称,指向String列表。
5.6 Method_ids
这块区域存储了方法声明列表,
method_id_item 分为三部分:
class_idx u2 该方法所在的类,指向Type列表。
proto_idx u2 该方法的方法原型,指向Prototype列表。
name_idx u4 该方法的名称,指向String列表。
5.7 Class_def_items
类相关数据
这里只有一个class所以只有一个class_def_item_class_def,来分析一下类结构:
字段名称 | 长度 | 字段值 | 字段描述 | |
---|---|---|---|---|
class_idx | u4 | 0x1 | 类型名称的索引,指向type_ids列表 | |
access_flags | u4 | 0x1 | 类型访问标志 | |
superclass_idx | u4 | 0x5 | 父类类型的索引,指向type_ids列表 | |
interface_off | u4 | 0x023C | 接口偏移,该类实现接口的开始位置 | |
source_file_idx | u4 | 0x3 | class源文件的索引,指向String_ids列表 | |
annotations_off | u4 | 0x0 | 注解偏移,指向DexAnnotationsDirectoryItem | |
class_data_off | u4 | 0x03AD | 类数据偏移,指向class_data_item的开始位置 | |
static_values_off | u4 | 0x03AA | 静态变量偏移,指向encoded_array_item的开始位置 | |
图中有三部分没有在表中列出,因为从字节码顺序上这三部分不在块里面,下面单独分析下:
type_item_list
![image-20190923231922894](/Users/wuxiang/Library/Application Support/typora-user-images/image-20190923231922894.png)
首先,第一个字段是 size 4u 值为1 说明只有一个接口,接下来就是接口列表type_item_list 其中只包含一个值
type_idx 2u 值为3 ,指向了上面type_id_item[3]. 即 java.io.Serializable接口。
class_data_item 这是这部分比较复杂的一部分,展开如下:
static_fields_size 说明了静态成员变量个数
instance_fields_size 说明了实例变量个数
direct_method_size 说明了直接方法个数
virtual_method_size 说明了虚方法个数
static_fields 是静态成员变量列表,每个item由field_idx 和 access_flags 构成
instance_fields 是实例变量列表,同样每个item由field_idx 和access_flags构成
direct_methods 是直接方法列表,每个item由method_idx 指向方法表,access_flags 访问标记符,code_off 方法代码的偏移位置构成。
后面code_item 部分是方法实际内容部分,具体内容还是蛮多的