DEX详解:
Dex文件分为dex和odex。odex是优化版的dex。先介绍dex:
DEX使用的数据结构为
U1:表示一位无符号位。
U2:表示两位无符号位。
U4:表示四位无符号位。
U8:表示八位无符号位。
sleb128:有符号,长度1~5字节。
uleb128:无符号,长度1~5符号。
uleb128p1:无符号uleb128值加一,长度1~5。
其中u1~u4没问题,但是后三个需要解释一下。
上图是两个字节的leb128,在实际使用的时候系统会去读取第一个字节的最高位,如果是1,表示这个长度不够需要在读取第二个字节,如果还是一则读取第三个,直到有一个的最高位是0则表示读取结束。但是最多读取五个字节,如果这五个都是一则表示apk出错,会停止安装。
下面实际解析两个,比如字符序列为C0 83 92 25为例子:
C0: 1 1000000 83: 1 0000011 92: 1 0010010 25: 0 0100101
可以看到前三个的最高位都为1,读到25最高位为0则停止读取。
在真正读取的时候把最高位去掉,倒叙连起来就是0100101 0010010 0000011 1000000就是4A481C0。所以起uleb128就是0x4A481C0,uleb128p1就是uleb128+1=0x4A481C1。Sleb128把其左移四位,最高位就是符号位,这里是0,就是正的。
再比如字符序列 d1 c2b3 40。计算leb为11010001 11000010 10110011 01000000
整理就是1000000 0110011 1000010 1010001=80CE151。左移四位最高位是1,为负数,就是0xF80CE151。
数据结构 |
字段名称 |
偏移值 |
长度 |
描述 |
U1 |
Magic[8] |
0x0 |
8 |
'Magic'值,即魔数字段,格式如”dex/n035/0”,其中的035表示结构的版本。 |
U4 |
checksum |
0x8 |
4 |
校验码。 |
U1 |
signature |
0xC |
20 |
SHA-1签名。 |
U4 |
file_size |
0x20 |
4 |
Dex文件的总长度。 |
U4 |
header_size |
0x24 |
4 |
文件头长度,009版本=0x5C,035版本=0x70。 |
U4 |
endian_tag |
0x28 |
4 |
标识字节顺序的常量,根据这个常量可以判断文件是否交换了字节顺序,缺省情况下=0x78563412。 |
U4 |
link_size |
0x2C |
4 |
连接段的大小,如果为0就表示是静态连接。 |
U4 |
link_off |
0x30 |
4 |
连接段的开始位置,从本文件头开始算起。如果连接段的大小为0,这里也是0。 |
U4 |
map_off |
0x34 |
4 |
map数据基地址。 |
U4 |
string_ids_size |
0x38 |
4 |
字符串列表的字符串个数。 |
U4 |
string_ids_off |
0x3C |
4 |
字符串列表表基地址。 |
U4 |
type_ids_size |
0x40 |
4 |
类型列表里类型个数。 |
U4 |
type_ids_off |
0x44 |
4 |
类型列表基地址。 |
U4 |
proto_ids_size |
0x48 |
4 |
原型列表里原型个数。 |
U4 |
proto_ids_off |
0x4C |
4 |
原型列表基地址。 |
U4 |
field_ids_size |
0x50 |
4 |
字段列表里字段个数。 |
U4 |
field_ids_off |
0x54 |
4 |
字段列表基地址。 |
U4 |
method_ids_size |
0x58 |
4 |
方法列表里方法个数。 |
U4 |
method_ids_off |
0x5C |
4 |
方法列表基地址。 |
U4 |
class_defs_size |
0x60 |
4 |
类定义类表中类的个数。 |
U4 |
class_defs_off |
0x64 |
4 |
类定义列表基地址。 |
U4 |
data_size |
0x68 |
4 |
数据段的大小,必须以4字节对齐。 |
U4 |
data_off |
0x6C |
4 |
数据段基地址 |
上图和上表就是dex的文件头的结构和各个位置的意思。其中最开始的64 65 78 0A 30 33 3500(dex.035.)表示这是按照dex解析的。
string_ids_size和string_ids_off
这两个字段表示dex中用到的所有的字符串内容的大小和偏移值,我们需要解析完这部分,然后用一个字符串池存起来,后面有其他的数据结构会用索引值来访问字符串,这个池子也是非常重要的。
type_ids_size和type_ids_off
这两个字段表示dex中的类型数据结构的大小和偏移值,比如类类型,基本类型等信息
proto_ids_size和type_ids_off
这两个字段表示dex中的元数据信息数据结构的大小和偏移值,描述方法的元数据信息,比如方法的返回类型,参数类型等信息
field_ids_size和field_ids_off
这两个字段表示dex中的字段信息数据结构的大小和偏移值
method_ids_size和method_ids_off
这两个字段表示dex中的方法信息数据结构的大小和偏移值
class_defs_size和class_defs_off
这两个字段表示dex中的类信息数据结构的大小和偏移值,这个数据结构是整个dex中最复杂的数据结构,他内部层次很深,包含了很多其他的数据结构,所以解析起来也很麻烦。
data_size和data_off
这两个字段表示dex中数据区域的结构信息的大小和偏移值,这个结构中存放的是数据区域,比如我们定义的常量值等信息。
DEX虚拟机解析dex文件的内容,最终都将其映射成DexMapList数据结构。DexHeader结构的mapoff字段指明了DexMapList结构在dex文件中的偏移。
其中DexMapList的结构如下:
typedef structDexMapList {
u4 size; //表明接下来有多少个DexMapItem结构
DexMapItem list[1]; // DexMapItem结构
}DexMapList;
DexMapItem结构如下:
typedefstruct DexMapItem {
u2 type; //KDexType开头类型,枚举以下结构
u2 unused; //暂时未使用
u4 size; //制定类型个数
u4 offset; //指定类型数据的文件偏移
}DexMapItem;
DexMapItem结构定义如下:
enum{
kDexTypeHeaderItem = 0x0000, //头文件
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem =0x0005, //方法
kDexTypeClassDefItem = 0x0006, //类
kDexTypeMapList = 0x1000, //Map
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem =0x2000,
kDexTypeCodeItem =0x2001,
kDexTypeStringDataItem = 0x2002, //字符
kDexTypeDebugInfoItem =0x2003,
kDexTypeAnnotationItem =0x2004,
kDexTypeEncodedArrayItem =0x2005,
kDexTypeAnnotationsDirectoryItem =0x2006,
};
以下新建一个安卓工程,自动生成helloworld为例:放在010中找到mapoff。
map_off |
00 01 87 00 |
string_ids_size |
00 00 15 66 |
string_ids_off |
00 00 00 70 |
type_ids_size |
00 00 02 F2 |
type_ids_off |
00 00 56 08 |
proto_ids_size |
00 00 03 F2 |
proto_ids_off |
00 00 61 D0 |
field_ids_size |
00 00 04 A9 |
field_ids_off |
00 00 91 28 |
method_ids_size |
00 00 12 D4 |
method_ids_off |
00 00 B6 70 |
class_defs_size |
00 00 01 C7 |
class_defs_off |
00 01 4D 10 |
data_size |
00 07 F4 44 |
data_off |
00 01 87 00 |
可以看到mapoff=00018700。跳到00018700看到下图:
对照数据结构可以看到个数为0x11=17。则表示有17个DexMapItem。之后观察DexMapItem结构可以发现其占用三个字节。第一个表示其type,第二个表示个数,第三个表示偏移地址。所以可以得到下表:
Type |
个数 |
偏移地址 |
kDexTypeHeaderItem |
1 |
0x00000000 |
kDexTypeStringIdItem |
5478 |
0x00000070 |
kDexTypeTypeIdItem |
754 |
0x00005608 |
kDexTypeProtoIdItem |
1010 |
0x000061D0 |
kDexTypeFieldIdItem |
1193 |
0x00009128 |
kDexTypeMethodIdItem |
4820 |
0x0000B670 |
kDexTypeClassDefItem |
455 |
0x00014D10 |
kDexTypeMapList |
1 |
0X00018700 |
kDexTypeTypeList |
611 |
0X000187DC |
kDexTypeAnnotationSetItem |
486 |
0X00019EC8 |
kDexTypeClassDataItem |
444 |
0X0001B2DC |
kDexTypeCodeItem |
3083 |
0X00022CDC |
kDexTypeStringDataItem |
5478 |
0X0005BD74 |
kDexTypeDebugInfoItem |
3083 |
0X000786A4 |
kDexTypeAnnotationItem |
559 |
0X00091824 |
kDexTypeEncodedArrayItem |
65 |
0X000955D4 |
kDexTypeAnnotationsDirectoryItem |
373 |
0X00095D44 |
两个表比较发现有些地址是相同的,这里为什么要重复的去保存相同的地址,是为了校验,如果比对发现不相同就停止安装。
下面开始逐步解析这个表,其结构虽然不复杂,但是调用有点乱。
kDexTypeHeaderItem就是之前的文件头,占0x70个字节。描述了整个DEX文件结构。
kDexTypeStringIdItem对应了string_ids_off和string_ids_size。在0x70的位置往下有5478个字符串地址如下图:
在对应的位置可以看到下图:
可以看到这一片往下保存的都是字符串。这里使用的解析方式是MUTF-8,其解析方式如下
最高位表示个数,之后按照最高位规定的个数读取,最后结尾为00。例如下:
我们随便找两个连续偏移为0005BD9F 0005BDC7
可以看到0005BD9F地址是0x26,往后读取38位到0005BDC6为00。下一个字符串的偏移为0005BDC7刚好在前一个字符串00后面。MUTF-8最开始的大小不包括最后的结束符。
kDexTypeTypeIdItem对应type_ids_size和type_ids_off。在0x00005608偏移的位置0x00005608,如图:
kDexTypeTypeIdItem结构指向的结构是DexTypeId,表示应用程序代码中使用到的具体类型。结构如下
struct DexTypeId{
u4 descriptorIdx; //指向DexStringId列表索引
};
这里需要注意:这里的不是地址,而是指向stringID的列表索引,说白了这里的0000 01 98,不是地址00000198,而是指kDexTypeStringIdItem的第0x198的那个字符串。因为在编译的时候已经将字符串统一存在了一个池子中,需要的时候按照ID去字符串池中取。
这里比如前三个:00000198,000001B6,00000350:对应的十进制为:408,438,848.
00000198对应的字符串地址为:,对应的字符串如下:
为:B
000001B6对应的字符串地址为:,对应的字符串如下:
为:C
00000350对应的字符串地址为:,对应的字符串如下:
为:Landroid/app/Activity;
以上三个大致可以发现这就是smali中的原始类型B,C和类Landroid/app/Activity。
kDexTypeProtoIdItem对应proto_ids_size和proto_ids_off字段,指向DexProtoId,表示方法声明的结构体,结构如下:
struct DexProtoId {
u4 shortyIdx; //指向DexStringId列表的索引
u4 returnTypeIdx; //指向DexTypeId列表的索引
u4 parametersOff; //指向DexTypeList的偏移
};
并且其中shortyIdx为方法声明字符串,returnTypeIdx为方法返回类型字符串,parametersOff为指向一个DexTypeList结构体,存放了方法的参数列表。声明如下:
StructDexTypeList{
U4 size; //DexTypeItrm的个数
DexTypeItrm list[1]; //DexTypeItrm结构
};
DexTypeItrm结构如下:
StructDexTypeItrm{
U2 typeIdx; //指向DexTypeId列表的索引
};
由上可知kDexTypeProtoIdItem的地址为 ,并且有1010个。下图就是指向的DexProtoId结构
在去DexTypeId索引的时候,起始位置是0,不是1,fuck!
可以看到每三个字节为一个结构体:我们随机提取两个:
BF 01 00 00 0100 00 00 4C 88 01 00;对应就是1BF,1,1884C
所以shortyIdx表示DexStringId的第447个,找到其地址为00 05E1 8C。在这个地址下找到字符串为:CI
returnTypeIdx 指向DexTypeId的第1个就是上面找到的C
parametersOff偏移地址为1884C,对应数据如图
Size=1,DexTypeItrm=0x4.指向DexTypeId的第四个地址是:BF 0200 00。找到对应的字符串地址为:4C FB 05 00。找到对应字符串为I
所以可以知道这个的方法有一个参数并且类型是I,返回值类型为C,方法声明为CI
5D 02 00 00 0300 00 00 EC 87 01 00;对应就是25D,3,187EC
所以shortyIdx表示DexStringId的第605个,找到其地址为00 05F2 63。在这个地址下找到字符串为:FF
returnTypeIdx 指向DexTypeId的第3个对应的字符串ID是593,对应的字符串地址为86F1 05 00,对应字符串为:F
parametersOff偏移地址为187EC,对应数据如图
Size=1,DexTypeItrm=0x3.指向DexTypeId的第三个地址找到对应的字符串地址为:86 F1 05 00。找到对应字符串为:F
所以可以知道这个的方法有一个参数并且类型是F,返回值类型为F, 方法声明为FF
kDexTypeFieldIdItem对应着field_ids_size和field_ids_off字段指向的结构为DexFieldId:表示代码中的字段。结构如下:
struct DexFieldId {
u2 classIdx; //指向DexTypeId列表索引,表示字段所属的类
u2 typeIdx; //指向DexTypeId列表索引,表示字段类型
u4 nameIdx; //指向DexStringId列表索引,表示字段名
};
找到偏移为0x00009128的DexFieldId结构体位置,有1193个结构体,如图:
这里一定要注意读取顺序,两位和四位的读取方式一定要注意,比如读取classldx和typeldx,是两位两位读取,如果按照四位如nameidx方式读取理解,就会把参数位置弄混。
根据结构体可以知道每两个字节代表一个结构体,我们随机选取几个来分析:
00 10 00 04 00 00 09 57:classIdx=16;typeIdx=4;nameIdx=0x957=2391
typeIdx地址为BF 02 00 00,对应string地址为4C FB 0500,字符串为:I
classIdx地址为57 03 00 00,对应string地址为B0 03 0600,字符串为:Landroid/app/Notification;
nameIdx字符串地址为42 D3 06 00,对应字符串为audioStreamType。
我们在smali中可以看到如下图:
看到上图就可以知道这个结构体包含的是什么了。
就是 classIdx->nameIdx: typeIdx.
kDexTypeMethodIdItem对应method_ids_size和method_ids_off,指向DexMethodId结构体,表示代码中使用的方法,结构体如下:
struct DexMethodId {
u2 classIdx; //指向DexTypeId列表索引,表示方法所属的类
u2 protoIdx; //指向DexProtoId列表索引,表示方法原型
u4 nameIdx; //指向DexStringId列表索引,表示方法名;
};
在偏移0x0000B670位置有4820 DexMethodId结构体个如下图:
选取第一个00 06 03 7E 00 00 0B DF为例:classIdx=0x6, protoIdx=0x37E,nameIdx=00000BDF
classIdx:对应的type地址为4D 03 0000,字符串地址为4E 02 06 00,字符串为Landroid/accessibilityservice/AccessibilityServiceInfo;
protoIdx:对应DexProtoId的第0x37E=894个,地址和内容如图。
根据DexProtoId结构解析:shortyIdx=0x857;returnTypeIdx=0x2D8;parametersOff=0
shortyIdx对应字符串为23 C306 00:Z
returnTypeIdx字符串为EB C3 0600:[B
parametersOff不存在
nameIdx:对应地址为E8 F6 06 00字符串为:getCanRetrieveWindowContent
对应smali可以看到下图
可以发现,实现了getCanRetrieveWindowContent方法。这里都是实现的各种方法。
kDexTypeClassDefItem对应着class_defs_size和class_defs_off字段。其指向的结构体为
typedefstruct DexClassDef {
u4 classIdx; //类的类型,指向DexTypeId列表
u4 accessFlags; //访问标志
u4 superclassIdx; //父类类型,指向DexTypeId列表
u4 interfacesOff; //接口,指向DexTypeList偏移
u4 sourceFileIdx; //源文件名,指向DexStringId列表索引
u4 annotationsOff; //注解,指向DexAnnotationsDirectoryItem结构
u4 classDataOff; //指向DexclassData结构
u4 staticValuesOff; //指向DexEncodedArray结构的偏移
}DexClassDef;
accessFlags字段是类的访问标志,是以ACC_开头的一个枚举值。如果存在接口interfacesOff就会指向一个DexTypeList结构,否则为0.
DexclassData结构如下:
typedefstruct DexClassData {
DexClassDataHeader header; //指定字段与方法的个数
DexField* staticFields; //静态字段,DexField结构
DexField* instanceFields; //实例字段,DexField结构
DexMethod* directMethods; //直接方法,DexMethod结构
DexMethod* virtualMethods; //虚方法,DexMethod结构
} DexClassData;
其中DexClassDataHeader结构记录了当前类中字段与方法的数目,声明如下:
typedefstruct DexClassDataHeader {
u4 staticFieldsSize; //静态字段个数
u4 instanceFieldsSize; //实例字段个数
u4 directMethodsSize; //直接方法个数
u4 virtualMethodsSize; //虚方法个数
} DexClassDataHeader;
DexField结构描述了字段的类型和访问标志,结构如下:
typedefstruct DexField {
u4 fieldIdx; //指向DexFieldId的索引
u4 accessFlags; //访问标志
} DexField;
DexMethod结构描述了方法的原型,名称,访问标志以及代码数据块
typedef structDexMethod {
u4 methodIdx; //指向DexMethodId索引
u4 accessFlags; //访问标志
u4 codeOff; //指向DexCode结构的偏移
} DexMethod;
DexCode结构详细的描述了方法的信息以及方法指令的内容
typedefstruct DexCode {
u2 registersSize; //使用的寄存器个数
u2 insSize; //参数个数
u2 outsSize; //调用其它方法时使用的寄存器个数
u2 triesSize; //Try/Catch个数
u4 debugInfoOff; //指向调试信息的偏移
u4 insnsSize; //指令集个数,以2字节为单位
u2 insns[1]; //指令集
//2字节对齐
//try_item[triesSize] DexTry结构
//Try/Catch中handler个数
//catch_handler_item[handlersSize],DexCatchHandler结构
}DexCode;
在0x00014D10偏移的位置找到DexClassDef结构起始位置如图:
这里我们选取如下的位置:
classIdx; =0000 00 71 DexTypeId=BB 03 00 00 stringoff=80 14 06 00 A9 14 06 00
string=Landroid/support/v4/app/DialogFragment;
Landroid/support/v4/app/Fragment$1
accessFlags; =00 00 00 10 对应下表:不生子类
superclassIdx; =00 00 02 A6 DexTypeId=18 06 00 00 stringoff= 36 92 06 00 4C 9206 00
string=Ljava/lang/Runnable;
Ljava/lang/RuntimeException;
interfacesOff; =00 01 90 58 DexTypeList=0100 00 00 52 00 00 00 size=1 typeid=0x52
DexTypeId=9C 03 00 00 stringoff= FF 0C 06 00
String=Landroid/os/Parcelable;
sourceFileIdx; =00 00 01 A0 stringoff=94 DE 05 00
string=BackStackRecord.java
annotationsOff; =00 09 5F 3C
classDataOff; =00 01 B5 57 header=01 09 03 03 这里采用的是uleb128的取值方式,所以对应的 staticFieldsSize; 0x1 静态字段个数
instanceFieldsSize; 0x9 实例字段个数
directMethodsSize; 0x3 直接方法个数
virtualMethodsSize; 0x3 虚方法个数
首先是DexField结构:fieldIdx =0x67 从0开始第103个 内容:71 00 04 00 84 0E00 00
classIdx=0x71 BB030000 Landroid/support/v4/app/BackStackState;
typeIdx=0x4 string=I
nameIdx=0XE84 string= mBreadCrumbShortTitleRes
accessFlags = 19 ACC_PUBLIC| ACC_STATIC|ACC_FINAL
staticValuesOff; =00 00 00 00
Name |
Value |
For Classes (and InnerClass annotations) |
For Fields |
For Methods |
ACC_PUBLIC |
0x1 |
public: visible everywhere |
public: visible everywhere |
public: visible everywhere |
ACC_PRIVATE |
0x2 |
* private: only visible to defining class |
private: only visible to defining class |
private: only visible to defining class |
ACC_PROTECTED |
0x4 |
* protected: visible to package and subclasses |
protected: visible to package and subclasses |
protected: visible to package and subclasses |
ACC_STATIC |
0x8 |
* static: is not constructed with an outer this reference |
static: global to defining class |
static: does not take a this argument |
ACC_FINAL |
0x10 |
final: not subclassable |
final: immutable after construction |
final: not overridable |
ACC_SYNCHRONIZED |
0x20 |
synchronized: associated lock automatically acquired around call to this method.Note: This is only valid to set when ACC_NATIVE is also set. |
||
ACC_BRIDGE |
0x40 |
bridge method, added automatically by compiler as a type-safe bridge |
||
ACC_VOLATILE |
0x40 |
volatile: special access rules to help with thread safety |
||
ACC_TRANSIENT |
0x80 |
transient: not to be saved by default serialization |
||
ACC_VARARGS |
0x80 |
last argument should be treated as a “rest” argument by compiler |
||
ACC_NATIVE |
0x100 |
native: implemented in native code |
||
ACC_INTERFACE |
0x200 |
interface: multiply-implementable abstract class |
||
ACC_ABSTRACT |
0x400 |
abstract: not directly instantiable |
abstract: unimplemented by this class |
|
ACC_STRICT |
0x800 |
strictfp: strict rules for floating-point arithmetic |
||
ACC_SYNTHETIC |
0x1000 |
not directly defined in source code |
not directly defined in source code |
not directly defined in source code |
ACC_ANNOTATION |
0x2000 |
declared as an annotation class |
||
ACC_ENUM |
0x4000 |
declared as an enumerated type |
declared as an enumerated value |
|
(unused) |
0x8000 |
|||
ACC_CONSTRUCTOR |
0x10000 |
constructor method (class or instance initializer) |
||
ACC_DECLARED_SYNCHRONIZED |
0x20000 |
declared synchronized. Note: This has no effect on execution (other than in reflection of this flag, per se). |
1.
2.
3.
4.
依次解释为:
1. android:label表示Activity的标题。android:name指定了Activity具体的类
2.
3.当包含"android.intent.action.MAIN"的时候,这个表示上面的android:name=中保存的类是Activity的主类。
4.当包含"android.intent.category.LAUNCHER"的时候表示可以通过LAUNCHER来启动。
如果没有发现"android.intent.action.MAIN"和"android.intent.category.LAUNCHER"表示这个程序安装成功后不存在界面和图标,是隐藏的。
这个时候我们可以去Activity的主类中找到OnCreate()。这个位置就是程序的起始位置