您的位置:首页 > 移动开发 > Android开发

Dex文件内容解析APK相关信息

2017-12-07 16:01 369 查看

Dex文件格式

我们都知道Android项目在构建的时候,会将class文件的jar包通过dx工具将其转化成dex文件,目的是将所有的class文件整合到一个dex文件中,这样的目的是降低冗余,因为每个class的数据格式都相同,dex通过将相同的内容方法一起,使文件结构更加紧凑。



通过上图可以看出,dex文件将方法信息、字段信息、类型信息等都相同的信息都放到了一起,从而省下很多空间。

Dex文件组成(这里只介绍了重要部分):

官网查看全部结构

名称说明
headerdex文件头部,记录了整个dex文件相关信息的起点,很重要
string_ids存储dex信息相关内容的字符串在数据区的索引位置
type_ids存储的是类型(各种数据类型,比如类的类型,方法返回值类型等)
proto_ids方法原型的描述,记录了方法的返回值类型,参数列表等
field_ids字段数据,记录了字段所属类型,类型等
method_ids记录方法信息,包括方法名,所属类名,方法原型等
class_defs存储的所有类的索引位置
data数据区,上面的所有数据都是里面获取的,以及存储dex的数据

数据类型

上面的数据类型是dex中的数据存储的相关数据的所有的数据类型,主要是其对应的字节长度,因为读取相关dex中对应的相关信息是通过其内容的数据类型,进入读取指定的长度的数据,也就是我们要的内容了。

上面唯一没有特殊的数据类型是LEB128,它没有固定的长度,他是1~5个字节可变的数据类型,主要是针对存储的数据是不定长度的。这种数据类型原理:首先判断第一个字节的最高位是否是1,如果是1,说明当前数据包括下一个字节;继续监测第二个字节的最高位是否为1,以此类推直到下一个字节的最高位是0,表示当前数据完毕。

官方文档

这种数据类型的数据读取方式,Android系统读取方式

源码路径: /dalvik/libdex/Leb128.h

DEX_INLINE int readUnsignedLeb128(const u1** pStream) {
const u1* ptr = *pStream;
int result = *(ptr++); //取第一个字节
if (result > 0x7f) {  //如果第1个字节大于0x7f,表示第一个字节最高位为1
int cur = *(ptr++);  //第2个字节
result = (result & 0x7f) | ((cur & 0x7f) << 7); // &0x7f表示去掉这个这个字节的第8位。 合并第一位和第二位的数据
//下面依次读取第3,4,5位的数据
if (cur > 0x7f) {
cur = *(ptr++);
result |= (cur & 0x7f) << 14;
if (cur > 0x7f) {
cur = *(ptr++);
result |= (cur & 0x7f) << 21;
if (cur > 0x7f) {
/*
* Note: We don't check to see if cur is out of
* range here, meaning we tolerate garbage in the
* high four-order bits.
*/
cur = *(ptr++);
result |= cur << 28;
}
}
}
}
*pStream = ptr;
return result;
}


while循环读取,js代码:

jDataView.prototype.readStringUtf = function () {
var len = this.readUnsignedLeb128();
return this.getString(len); // 默认采用unicode读取每个字节的数据(单个字节就和ASCII编码的方式相同了,ASCII最早出现,也是为英文出现的)
};
// ulb128 是dex特有的类型,长度是1-5的可变长度,如果当前字节最高位为1,那么下一个字节也在当前数据中;下面的读取方式:不会有5此限制
// 获取去除leb128类型的数据,
jDataView.prototype.readUnsignedLeb128 = function () {
var result = 0;
do {
var b = this.getUint8(); // 读取一个字节,return unsigned 8-bit integer; 转换成数字的时候会当成有符号数来转
result = (result << 7) | (b & 0x7f); // 将每个字节最高位去掉,合并出最终的数据
} while (b < 0); // 第一位如果是1(也就是负数,最高位为1),那么高位还有数据
return result;
};


Dex header的结构

字段长度(byte)说明
magic8魔法值。dex文件的唯一标识, 值为dex\n035\0,现在android8已经支持dex\n039\0
checksum4除去magic和checksum剩余内容的adler32校验和;用于检测当前文件损坏情况
signature20除了上面三个字段文件剩余内容的SHA-1值,文件的唯一标识
file_size4dex文件的大小,单位:byte
header_size4header的大小
endian_tag4字节序标记,两种,关于这个可以自行查阅
link_size4链接区大小
link_off4从文件开头到链接区的偏移量
map_off4从文件开头到映射项的偏移量
string_ids_size4string_ids的数组长度
string_ids_off4string_ids的偏移量
type_ids_size4type_ids的长度
type_ids_off4type_ids的偏移量
proto_ids_size4proto_ids的长度
proto_ids_off4proto_ids的偏移量
field_ids_size4field_ids的长度
field_ids_off4field_ids的偏移量
method_ids_size4method_ids的长度
method_ids_off4method_ids的偏移量
class_defs_size4class_defs的长度
class_defs_off4class_defs的偏移量
data_size4data数据区的大小,单位:byte
data_off4data的偏移量
我们能看到,通过header可以获取到dex一些基本信息,比如方法数,class数量,field数量等;至于详细信息需要通过header中给与的对应内容的偏移量然后到dex中去读取即可。

通过上面结构解析数据(method信息为例)

首先从header中解析出,读取所有string_ids的字符串信息

DexFileReader.prototype.loadStrings = function (dv) {
var curStrings = [];
var offsets = [];
// 字节流移动到string_ids的偏移位置
dv.seek(this.header.stringIdsOff);// header中的数据是通过上面header结构从dex字节流中读取的
for (var i = 0; i < this.header.stringIdsSize; i++) {
offsets[i] = dv.getInt32(); // 通过上面得知string_ids每个item的长度是4字节
}
// 从dex数据中读取string内容
dv.seek(offsets[0]);
for (var i = 0; i < this.header.stringIdsSize; i++) {
dv.seek(offsets[i]);
curStrings[i] = dv.readStringUtf(); // 这里从string_ids中读取数据是通过上面定义的读取leb128数据类型方式读取的
}
this.strings = this.strings.concat(curStrings);
};


然后是读取所有的method_ids中的数据

DexFileReader.prototype.loadMethods = function (dv) {
var curMethods = [];
dv.seek(this.header.methodIdsOff);
for (var i = 0; i < this.header.methodIdsSize; i++) {
var classIdx = dv.getInt16() & 0xffff; // 对应的class_defs的索引位置和type_ids的索引位置
var protoIdx = dv.getInt16() & 0xffff; // 对应的proto_ids中的索引位置
var nameIdx = dv.getInt32(); // 对应的string_ids中索引位置
curMethods[i] = new DexFormat.MethodDef(this, classIdx, protoIdx, nameIdx);
}
this.methods = this.methods.concat(curMethods);
};


上面读取的数据的根据是method_ids的格式:



读取class_defs对应的信息

var curClasses = [];
dv.seek(this.header.classDefsOff);
for (var i = 0; i < this.header.classDefsSize; i++) {
var classIdx = dv.getInt32(); // 对应的type_ids的索引
var accessFlags = dv.getInt32();
var superclassIdx = dv.getInt32();
var interfacesOff = dv.getInt32();
var sourceFileIdx = dv.getInt32();
var annotationsOff = dv.getInt32();
var classDataOff = dv.getInt32();
var staticValuesOff = dv.getInt32();
curClasses[i] = new DexFormat.ClassDef(this, classIdx, accessFlags, superclassIdx, interfacesOff, sourceFileIdx, annotationsOff, classDataOff, staticValuesOff);
}


class_defs每隔class_def的格式如下:



读取type_ids中的数据

var curTypes = [];
dv.seek(this.header.typeIdsOff);
for (var i = 0; i < this.header.typeIdsSize; i++) {
var descriptorIdx = dv.getInt32(); // 只存取了类的描述符对应的string_idx中的索引位置,并且该字符串存储数据类型是TypeDescriptor,详细查看官方文档
curTypes[i] = new DexFormat.TypeDef(this, descriptorIdx);
}


type_id的数据格式中:只含有一个descriptionIdx存储的当前类型的字符串描述对应的索引位置。

读取到的description,对应的格式如下:



上面的含义是:TypeDescription可以 ‘V’ 或者 (‘[’ (0~255个)) NonArrayFieldTypeDescriptor; 其中[ 表示当前类型是几维数组,在字段的类型或者方法的返回值类型等会用到;NonArrayFieldTypeDescriptor就是下面的集中数据类型,前面的字母都是代表基本数据类型,L代表是引用类型;FullClassName的格式是 类的全类名的点替换成/.

上面大写字母对应的数据类型如下:



下面给出集中例子:

[Lcom/meizu/cloud/pushsdk/notification/model/styleenum/InnerStyleLayout; 表示:com.meizu.cloud.pushsdk.notification.model.styleenum.InnerStyleLayout

Z 表示 boolean

[[D 表示 double[][]

读取methods相关的报名信息,解析如下:

var classData = methodRef.getClassData();
var classType = methodRef.getClassType(); //通过上面存储的classIds获取到对应的Type信息
// 通过type获取对应的描述信息
var classDescriptor = classType.getDescriptor();
// 通过描述信息解析出包名,通过上面给出Desciption可以解析出对应的包名
var packageName = DexFileReader.getPackageNameOnly(classDescriptor);

// 详细过程如下
DexFileReader.getPackageNameOnly = function (typeName) {
var dotted = DexFileReader.descriptorToDot(typeName); // 将dex中存储的类型描述符的字符串 转化成 java代码中数据类型格式
var end = dotted.lastIndexOf(".");
if (end < 0) {
return "";
}
else {
return dotted.substring(0, end);
}
};
// 将dex中存储的类型描述符的字符串 转化成 java代码中数据类型格式
DexFileReader.descriptorToDot = function (descr) {
var targetLen = descr.length;
var offset = 0;
var arrayDepth = 0; // 用于记录数组的维度

// 格式: ('[' 0~255个)'L'FullClassName';' (其中[ : 表示数组的维度, L:表示是类, FullClassName:类的全名,格式:全名称但是用'/'分隔)

/* strip leading [s; will be added to end */
// 先取出去除表示数组维度的[
while (targetLen > 1 && descr.charAt(offset) == '[') {
offset++;
targetLen--;
}
arrayDepth = offset;
if (targetLen == 1) { // 后面的字符串长度只有1,那么就是指包含类的类型标识符,所以是基本数据类型
descr = DexFileReader.getPrimitiveType(descr.charAt(offset)); // 获取基本数据类型对应的全称字符串
offset = 0;
targetLen = descr.length;
}
else {
/* account for leading 'L' and trailing ';' */
if (targetLen >= 2 && descr.charAt(offset) == 'L' &&
descr.charAt(offset + targetLen - 1) == ';') { // L表示自定义的类
targetLen -= 2; /* two fewer chars to copy */
offset++; /* skip the 'L' */
}
}
var buf = [];
/* copy class name over */
var i;
// 将 / 替换成 .
for (i = 0; i < targetLen; i++) {
var ch = descr.charAt(offset + i);
buf[i] = (ch == '/') ? '.' : ch;
}
/* add the appopriate number of brackets for arrays */
// 根据数组的维度,添加指定个数的[] 字符串
while (arrayDepth-- > 0) {
buf[i++] = '[';
buf[i++] = ']';
}
return buf.join("");
};


总结

我们想得到当前apk的相关信息,比如方法数,就可以通过分析dex文件的结构来获取到,市面上的开源软件都是这样做的。关于一些其他信息的获取可以自行通过其结构信息来获取。

参考文献

Dex文件格式(官网)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android dex apk信息