目标
这里的 dex 文件,就是 android 中 dalvik 虚拟机运行的程序格式文件;art 虚拟机,也是基于 dex 格式再创作的.
我们的目标,就是直观的了解下,这个 dex 到底是什么东东.
知识点
在开始之前,先熟悉 python 的两个知识点:
. mmap
. struct
这两个是 python 中的内置模块,分别是映射文件和结构化操作.
mmap
了解 linux 的是不是感觉很亲切?是的,功能是类似的,把一个文件映射到一段内存,然后操作内存就像直接读写文件一样.
创建的格式:
mmap 函数原型
例如:
mmap 调用示例
使用更简单,可以直接操作数组:
直接读写
更详细介绍参考:
http://blog.csdn.net/zdy0_2004/article/details/53200250
对 linux 的 mmap 感兴趣的可以参考:
http://ju.outofmemory.cn/entry/224106
struct
struct 是对二进制流进行操作的一个模块,直译就是数据结构,很像 c 语言中的结构体 struct 声明.它提供了从二进制流读取数据 (unpack),和把数据写入二进制流的方法 (pack).
使用示例:
静态方法调用
其中'
格式对应表如下:
struct 格式对应表
还可以指定字节顺序:
struct 字节序
除此外,还可以指定缓冲区,这里不介绍,参考:
https://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
DEX 文件格式
言归正传,我们的目标是 dex 文件.
文件结构:
dex 文件结构
文件头结构
文件头固定 0x70 长度,也就是 112 个字节.
其中 signature 计算的范围是整个文件除去 magic, checksum 和 signature 三部分的其余所有部分.
checksum 计算的范围是除去 magic 和 checksum 两部分的其余所有部分.
magic 是 dex 文件标识,固定为'dex\n035'
其余大部分字段,都是在划分区域,通过 (size, off) 的方式,就是圈定文件的 [off, off+size] 部分为相关段的数据;后续的索引,也是建立在这个基础上的.
dex 文件头
索引区结构
索引区有五大区域,分别是字符串区域,类名区域,函数原型区域,类属性区域,方法区域.其中每个区域的范围,可以通过头部的信息获取到,[off, size].
这五个区域,是有相互关系的,一个区域记录的往往是另一个区域的索引.
字符串区域
从下图看,就是每四个字节是一项,这四个字节代表的是真实字符串的偏移值.
字符串区域结构
如果把这个数值作为文件的偏移值,相关的内存是这样的:
其中 size 是字符串的长度,str 是 size 大小的一串字符.
字符串真实值
类名区域
这个区域存储的是所有的类名索引,结构如下,每一项用 4 个字节存储,表示字符串区域的索引;这里就已经有两重索引了,一重似乎类索引包含的字符串索引,一重是字符串本身的索引.
类名索引结构
函数原型区域
这个区域存储的是所有函数的原型汇总,每个原型由三部分组成,(名称,返回类型,参数类型),当然它们存储的都是索引数值.
其中名称的索引是字符串区域的索引,返回类型是类名区域的索引,而参数类型则导向另一个地方,它存储的是一个偏移数值.
函数原型结构
这个偏移数值最终导向的结构,是一个数组列表结构,由 [size, itemlist] 格式组成,其中 size 是参数的总个数,itemlist 就是 size 大小的参数列表,列表的每一项又是一个索引数,指向类名区域的索引.
也就是说,这个结构其实就是 size 大小的一个类名数组,也就是我们要找寻的参数类型.
函数原型导向结构
方法区域
方法结构相比原型要简单很多,也是三部分组成,(类名,函数原型,方法名),当然也都是存的索引数值.
其中类名是指向类名区域的索引,函数原型是指向函数原型区域的索引,方法名是指向字符串区域的索引.
方法结构
类属性区域
类的属性和类的方法是相似的,由三部分组成(类名,字段类型,名称),也都是存储的索引数值;
其中类名是指向类名区域的索引,字段类型是指向类名区域的索引,名称则是指向字符串区域的索引.
类属性结构
结构先分析到这里为止,接下来我要把这些结构对应的数据都打印出来.
DEX 解析库
首先分享一个现有的 python 库:
https://github.com/bunseokbot/dexparser
这个库是两年前写的,非常简洁明了.
其实关于 dex 解析的库还是很多的,我比较喜欢这个库的直观性,说到底是解析 Dex 的文件格式,这种 c 风格的写法,非常浅显易懂,直接触摸到文件结构.也因为它的简单,在它的基础之上,我们可以继续做扩充.
这个 dexparser 库利用了 mmap 和 struct 模块,对 dex 文件做解析.
画风是这样的:
初始化和解析文件头
这样的:
文件头数据赋值
还有这样的:
读取字符串列表
这里用 struct,直接从 dex 文件的映射区域 self.mmap 中,通过索引读出数据,然后根据结构说明继续转索引或者直接使用.
上面的 string_list 代码,首先锁定了 string 索引的区域,[string_ids_size, string_ids_off],也就是索引的总数是 string_ids_size 大小;
然后根据这个大小遍历,分别从区域相关位置中取出某一个字符串的索引 off;
接着,通过这个偏移数值定位到字符串真实数据的区域 self.mmap[off],并根据格式 [size, char] 进行读取;
最后就形成了一个字符串的队列.
dexparser 模块中的其它函数也是类似的.
解析和打印
现在用这个库,我要把前面介绍的文件头和索引区域的五大区域都打印出来.
构建一个 Dexparser 对象
(自己解压一个 Apk,拿出其中的 dex 作为测试用)
初始化
打印文件头
打印文件头(代码)
画风是这样的:
打印文件头(结果)
打印字符串列表
打印字符串(代码)
画风是这样的:
打印字符串(结果)
打印类名列表
打印类名列表(代码)
画风是这样的:
打印类名列表(结果)
打印函数原型列表
如下图所示,Dexparser 中提供的列表其实只有索引,我在这里进一步做了补充,根据函数原型的结构,将参数原型的描述也解析了出来.
打印函数原型(代码)
画风是这样的:
其中,函数名的部分我用了'%s'来替代,是为了后面解析方法区域时用的.
打印函数原型(结果)
打印方法列表
下图中就用到了上面函数原型列表中的'%s',把函数名称填写进去.
打印方法列表(代码)
画风是这样的:
打印方法列表(结果)
打印类属性列表
打印类属性列表(代码)
画风是这样的:
打印类属性列表(结果)
保存信息
最后,把打印出来的五大索引区域的列表信息,都保存到文件中.
保存到文件
[未完待续]
来源: http://www.jianshu.com/p/20c0fddd44d2