Class文件与Dex文件

837 阅读16分钟

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文件结构

image-20190820235430306.png

以上是使用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 数据段偏移字节

表中字段值部分处理成了十进制,处理过程涉及十六进制转换和字节序的概念。以下举例分析:

image-20190823073139443.png

这就是小尾方式,即低位字节在前,高位字节在后,78 56 34 12 实际上是12 34 56 78 但在一个字节内部依旧是高位在前低位在后,注:一个字节8位,一个十六进制数是4位,所以用两个十六进制数代表一个字节。

image-20190823073829586.png

再比如数据区域大小,在dex文件中为 90 02 00 00 调整顺序后为 0x00000290 ,转换为十进制为0+9*16+2*16*16=656

5.2 String_ids

字符串索引区记录了dex文件中每个字符串在数据区中的位置,每个索引占据4个字节,如下图所示:

image-20190823075416554.png

虽然索引区只保存了索引,但010editor为了方便显示,将该字符串在数据区中的真实值也拿了过来。

比如第3个字符串Hello, Android! 在索引区的值为57 02 00 00,调整顺序后为00 00 02 57 说明该字符串在dex文件0x0257 这个位置,我们来验证一下

image-20190823080151081.png

实际数据从0025h行7列开始,符合我们的预期。

image-20190823080958461.png

在此顺便解析一下该字符串在数据区中存储方式,Dalvik中使用uleb128、uleb128p1、sleb128 三种编码表示32位整形数值。因为在java中通常使用32位表示一个整数,宽度不可变,不论实际数值多大都要分配32位空间。但在手机中存储空间比较有限,使用可变宽度的数据存储方式能更有效利用空间。这里我们不做详细讨论,先了解uleb128的两个特征,

一是其占用的字节数是不确定的,长度有可能在1到5个字节之间变化,

二是以字节中最高位是否为0来表示字节流有没有结束的。

Center.png

上图中字符都是英文字符,都使用1个字节进行描述。

String在数据区中的存储

image-20190830082534174.png

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 类型列表偏移字节

image-20190906080609568.png

这里type_id_item中包含了descriptor_idx 指向字符串列表的索引。

5.4 Proto_ids

这块区域存储了Dex中方法原型的声明列表,方法原型就是入参和返回类型 这里每个方法原型的结构如下:

image-20190906083201562.png

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中的成员字段,结构如下图:

image-20190906084817166.png

每个field_id_item 大小固定,由以下三部分组成。

class_idx u2 字段所在的Class,指向Type_ids 列表。

type_idx u2 字段类型,指向Type_ids 列表。

name_idx u4 字段名称,指向String列表。

5.6 Method_ids

这块区域存储了方法声明列表,

image-20190906085449404.png

method_id_item 分为三部分:

class_idx u2 该方法所在的类,指向Type列表。

proto_idx u2 该方法的方法原型,指向Prototype列表。

name_idx u4 该方法的名称,指向String列表。

5.7 Class_def_items

类相关数据

image-20190923224356565.png

这里只有一个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 这是这部分比较复杂的一部分,展开如下:

image-20190923231922894.png

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 部分是方法实际内容部分,具体内容还是蛮多的

image-20190923233823924.png

5.8 Dex_map_list

image-20190923235253408.png