DEX文件结构

有一部分问题我没有搞懂,看了大神们的博客也没有很好的理解,总结+问题都贴出来吧,欢迎大神给解疑。

基础性的知识,不能着急赶学习进度。
重要且内容比较多的事情,一定要稳着做,急着性子做这类事情,只会欲速则不达。

一、DEX文件使用的数据类型

类型含义
u1等同于uint8_t,表示1字节的无符号数
u2等同于uint16_t,表示2字节的无符号数
u4等同于uint32_t,表示4字节的无符号数
u8等同于uint64_t,表示8字节的无符号数
sleb128有符号LEB128,可变长度1~5字节
uleb128无符号LEB128,可变长度1~5字节
uleb128p1无符号LEB128值加1,可变长度1~5字节

LEB128由1~5字节组成,所有字节表示一个32位的数据。
一个字节只有7个有效位,如果第一个字节最高位为1,表示会使用第二个字节,以此类推,直到最后一个字节为0为止。

LEB128最多使用5字节,如果读取到5字节后的最高位仍为1,则表示该DEX文件无效。

二、DEX的结构体详解

在这里插入图片描述
DEX文件由DexFile结构体表示,其定义如下:

struct DexFile{
	/* directly-mapped "opt" header*/
	const DexOptHeader* pOptHeader;
	
	/* pointers to directly-mapped structs and arrays in base DEX*/
	const DexHeader* pHeader;
	const DexStringId* pStringsIds;
	const DexTypeId* pTypeIds;
	const DexFieldId* pFiledIds;
	const DexMethodId* pMethodIds;
	const DexClassDef* pClassDefs;
	const DexLink* pLinkData;
}

DexOptHeader是ODEX的头。暂时不深入,DexHeader是DEX文件的头部信息,定义如下:

struct DexHeader{
	u1 magic[8];	//DEX版本标识
	u4 checksum;	//aler32检验
	u1 signature[KSHA1DigestLen];	//SHA-1散列值
	u4 fileSize;	//整个文件的大小
	u4 headerSize;	//DEXHeader结构的大小
	u4 endianTag;	//字节序标记
	u4 linkSize;	//链接段的大小
	u4 linkOff;		//链接段的偏移量
	u4 mapOff;		//DexMapList的文件偏移
	u4 stringIdsSize;//DexStringId的个数
	u4 stringIdsOff;	//DexStringId的文件偏移
	u4 typeIdsSize;	//DexTypeIds的个数
	u4 typeIdsOff;	//DexTypeIds的文件偏移
	u4 protoIdsSize;	//DexProtoId的个数
	u4 protoIdsOff;	//DexProtoId的文件偏移
	u4 fileIdsSize;	//DexFileId的个数
	u4 fileIdsOff;	//DexFileId的文件偏移
	u4 methodIdsSize;//DexMethodId的个数
	u4 methodIdsOff;//DexMethodId的文件偏移
	u4 classDefsSize; //DexClassDef的个数
	u4 classDefsOff;	//DexClassDef的文件偏移
	u4 dataSize;	//数据段的大小
	u4 dataOff;	//数据段的文件偏移
}
  • magic字符串格式固定位为"dex.035",根据版本不同,数据也不同。

  • checksum字段为DEX文件的校验和,用来判断文件是否已经损坏。

  • signature字段用于识别未经dexopt优化的DEX文件。

  • endianTag字段指定DEX运行环境的CPU字节序。

  • 其他结构的大小、个数和文件偏移就不多说了。

2.1 DexMapList

Dalvik虚拟机解析DEX文件的内容,最终将其映射为DexMapList数据结构。DexHeader结构的mapOff字段指明了DexMapList结构在DEX文件中的偏移量,声明如下:

struct DexMapList{
	u4 size;	//DexMapItem结构的个数
	DexMapItem list[1];	//DexMapItem结构
}

size字段表示接下来有多少个DexMapItem结构。DexMapItem的声明如下:

struct DexMapItem{
	u2 type;		//kDexType开头的类型
	u2 unused;		//未使用,用于字节对齐
	u4 size;		//指定类型的个数
	u4 offset; 		//指定类型数据的文件偏移
}

type字段是一个枚举常量,如下所示,通过类型名称很容易知道它的具体类型。

enum{
kDexTypeHeaderItem =0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
......
//省略
}

DexMapItem中的size字段指定了特定类型的个数,它们以特定的类型在DEX文件中连续存放,offset为该类型的起始文件偏移地址。比如Hello.dex,使用010Editor打开,可以看到DexHeader结构的mapOff字段为656,16进制为0x290,从0x290开始,接下来会有13个DexMapItem结构。

对应着结构体

struct DexMapList{
	u4 size;	//13的个数
	DexMapItem list[13];	//DexMapItem结构
}

struct DexMapItem{
	u2 type;		//kDexTypeHeaderItem的类型
	u2 unused;		//未使用,用于字节对齐
	u4 size;		//0的个数,为什么是0?我也不知道
	u4 offset; 		//0x70的文件偏移。
}

在这里插入图片描述

2.2 kDexTypeStringIdItem,string_ids的描述

对应DexHeader的stringIdsSize和stringIdsOff字段。表示在0x70偏移处有连续0x10个DexStringId对象。
在这里插入图片描述
DexStringId的结构声明如下:

struct DexStringId {
	u4 stringDataOff;	//字符串数据偏移
}

这里我有疑问,我看了010edit,发现了不仅有string_data_off字段,还有一个string_data结构体,如图:
在这里插入图片描述
可能是版本更新了?
其中stringDataOff字段指向的字符串并非普通的ASCII字符串,而是MUTF8表示的字符串,这个作为了解就行了。这个0x6代表的是指向文件偏移对应的地址值。

string_ids代表所使用到的字符串

序号偏移量字符串
0x00x1ca< init >
0x10x1d2Hello.java

剩下的就省略了,这里用到的字符串,都是关键字和类名等框架支架名,"Hello World"这用于显示的没有写出来。

那么在反编译成smali文件后,"Hello World"这个是怎么反编译出来的呢?会不会被反编译出来呢?

2.4 kDexTypeProtoIdItem,pro_ids的描述
struct DexProtoId{
	u4 shortyIdx;	//指向DexStringId列表的索引
	u4 returnTypeIdx;//指向DexType列表的索引
	u4 parametersOff;//指向DexTypeList的偏移量
}

shortyIdx为方法声明字符串,returnTypeIdx为方法声明返回类型字符串,parametersOff指向一个DexTypeList结构体,其中存放了方法的参数列表。
DexTypeList结构的声明如下:

struct DexTypeList {
	u4 size; //接下来DexTypeItem结构的个数
	DexTypeItem list[1]; //DexTypeItem结构
}

struct DexTypeItem{
	u2 typeIdx;	//指向DexTypeId列表的索引
}

可看出来,proto_ids包含了函数的声明,还有typeIdx的指向。
在这里插入图片描述

2.4 kDexTypeFieldIdItem, field_ids的描述

kDexTypeFieldIdItem对应于DexHeader中的fieldIdsSize和fieldIdsOff字段,指向的结构体为DexFieldId,声明如下:

struct DexFieldId{
	u2 classIdx;	//类的类型
	u2 typeIdx;		//字段类型
	u4 nameIdx;		//字段名
}

DexFieldId里的数据全部是索引值,指明了方法所在的类,方法的声明和方法名,从0xfc开始,共有一个DexField结构。

在这里插入图片描述

2.5 kDexTypeMethodIdItem, method_ids的描述

kDexTypeMethodIdItem,它对应于DexHeader中的methodIdsSize和methodIdsOff,指向的结构体DexMethodId如下:

struct DexMethodId{
	u2 classIdx; //类的类型,指向DexTypeId列表的索引
	u2 protoIdx; //声明类型,指向DexprotoId列表的索引
	u4 nameIdx; //方法名,指向DexStringId的索引
}

DexMethodId结构中的数据也都是索引值,指明了方法所在的类、方法的声明以及方法名。
在这里插入图片描述

2.6 kDexTypeClassDefItem,class_defs的描述

kDexTypeClassDefItem对应于DexHeader中的classDefsSize和classDefsOff字段,指向的结构体为DexClassDef,声明如下:

struct DexClassDef {
	u4 classIdx;	//类的类型,指向DexTypeIdx的索引
	u4 accessFlags; //访问标志
	u4 superclassIdx;//父类的类型
	u4 interfacesOff;//接口,指向DexTypeList的偏移量
	u4 sourceFileIdx;//源文件名,指向DexStringId列表的索引
	u4 annotationsOff;//注解,指向DexAnnotationsDirectoryItem
	u4 classDataOff;//指向DexClassData结构的偏移量
	u4 staticValuesOff;//指向DexEncodeArray结构的偏移量
}

这里的好多变量看着挺眼熟的,比如,classIdx,指向DexTypeIdx的索引,classIdx,已经出现在了DexMethodId和DexFieldId中。
staticValuesOff指向DexEncodeArray结构,其中记录了类中的静态数据。

struct DexClassData{
	DexClassDataHeader header; //指定字段与方法的个数
	DexField* staticFields;	//静态字段,DexField结构
	DexField* instanceFields;//实例字段,DexField结构
	DexMethod* directMethods;//直接方法,DexMethodId结构
	DexMethod* virualMethods;//虚方法,DexMethod结构
}

DexClassDataHeader结构记录了当前类中的方法和字段数目,它的声明如下:

struct DexClassDataHeader{
    u4 staticFieldsSize;	//静态字段的个数
	u4 instanceFieldsSize;//实例字段的个数
	u4 directMethodsSize;//直接方法的个数
	u4 virualMethodsSize;//虚方法的个数
}
struct DexField{
	u4 fieldIdx; //指向DexFieldId的索引
	u4 accessFlags;//访问标志
}

accessFlags代表此字段的访问权限。

DexMethod结构方法描述了方法的原型、名称、访问标志及代码数据块,他的结构声明如下:

struct DexMethod{
	u4 methodIdx;	//指向DexMethodId的索引
	u4 accessFlags; //访问标志
	u4 codeOff;		//指向DexCode结构的偏移量
}

struct DexCode{
	u2 registersSize //使用的寄存器的个数
	u2 insSize;//参数的个数
	u2 outsSize;//调用其他方法时使用的寄存器的个数
	u2 triesSize;//try/catch语句的个数
	u2 debugInfoOff;//指向调试信息的偏移量
	u2 insnsSize;//指令集的个数,以2字节为单位
	u2 insns[1];//指令集
}

DexCode中的registersSize就是smali语法中的.register指令,就是它的值;

insSize字段制定了方法的参数的个数,对应于smali中的.paramter;

outsSize字段指定了方法在调用外部方法时使用的寄存器的个数;

DexClassDef结构。
在这里插入图片描述
DexClassDataHead结构。
在这里插入图片描述
不含字段,有两个直接方法和一个虚方法。
DexCode,寄存器个数、参数、内部函数使用的寄存器都为1个。
在这里插入图片描述
有4条指令,具体为"7010 0400 0000 0E00"
在这里插入图片描述
在这里插入图片描述
这里就是我不太明白的地方了。
通过"7010 0400 0000 0E00"可以得出指令的具体内容,
70的Opcode为invoke-direct,格式为0x35c,我知道,通过查询这里得知。

在这里插入图片描述
0x35c的指令格式,可以通过这里查到。
在这里插入图片描述
我不明白的是,好多大神说可以这样得到,我不太明白为啥invoke-direct编码就是35c了。我是通过查表才查出来的,而所指的可以推出来的方法是啥?
在这里插入图片描述
[A=5] op {vC, vD, vE, vF, vG}, meth@BBBB
[A=5] op {vC, vD, vE, vF, vG}, site@BBBB
[A=5] op {vC, vD, vE, vF, vG}, type@BBBB
[A=4] op {vC, vD, vE, vF}, kind@BBBB
[A=3] op {vC, vD, vE}, kind@BBBB
[A=2] op {vC, vD}, kind@BBBB
[A=1] op {vC}, kind@BBBB
[A=0] op {}, kind@BBBB

回到正题,0x35c的指令格式为A|G|op BBBB F|E|D|C,而这个指令格式是70得来的,资料中说道在指令“7010”(按小端字节序排列为"1070")中,A为1,G为0,我就又不明白了,也就是说在010edit中是大端显示了?

还有这个指令格式,只是由70得来的,为啥按照"1070"的格式对照A和G的值?
A=1,G=0?资料中说"Vc"代表V0寄存器,指令后面的"BBBB"和"F|E|D|C"都是16位的,这个我能理解,关键是下一句话,"7010"后面的两个16位都是0? "7010"后面是0400,哪里都是0了?BBBB=4且F=E=D=C=0 我都能理解,毕竟0400嘛,能够对应起来。
但是前面的1070你怎么和A=1,G=0对应起来? A|G|op对应1070,难道op是70?

好了,我无法得知,0710 0400 0000 0e00是如何翻译成smali代码的。
这怎么翻译来的?010Edit中DexCode中的insns指令集中的:
7010 0400 0000 0e00
是如何翻译成
invoke-direct {v0}, Ljava/lang/Object;.< init>:()< V>的?

这个问题留着,以后请教,或者慢慢在实践中得知,apktool的源码将dex翻译成smali代码貌似也是基于这个原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值