DEX文件结构与解析

DEX文件结构与解析

Dex是Dalvik虚拟机的执行文件,对于每一个开发者来说,他的结构对于开发这至关重要,也是我们优化的一个方面,虽然很多工作是Android Stuido和其对应的工具进行的,但是我们需要知道他的基础结构和工作原理.由于近期的工作原因,特意研究了一下dex的完整结构,并且用kotlin写了完整的结构解析代码,除了dex具体的代码解析以外的其他结构解析都已经完成.特此总结一下学到的东西,希望与大家一起进步.

前期准备工作

1.我们在探索Dex文件结构时候需要使用一些工具,我个人推荐使用010Editor,加上Dex模板,这样更好的方便我们分析Dex的结构

2.分析的Dex文件是我自己用Timber(一个开元音乐播放器)代码编译的。如果需要可以自己找个合适的分析对象

正式开始

将dex从apk中解压出来,拖入010editor中,此时因为dex文件比较大,010Editor会提示是否继续进行分析

分析完成以后会在下面产生一个结构列表,我们分析就靠这个结构列表和对应的数据了。

dexStruct

Dex头部

首先我们用鼠标选中结构的第一行,可以看到上面文本去默认会被高亮,这第一段里面的就是Dex文件的头部

MagicNumber

Dex文件头的前8个byte是用来识别dex文件的MagicNumber,内容是Dex.035,并以00结尾,十六进制值为 64 65 78 0A 30 33 35 00

CheckSum

Dex文件的adler32校验和,长度4个字节,校验文件除去 maigc、checksum 外余下的所有文件区域

SHA1 Signature

接下来的20个字节是SHA1的签名

File Size 文件长度

文件长度4个字节,所以单个Dex文件大小不可能超过4GB,但是因为可以拆分分多个Dex,所以使用4字节也是足够了

Header Size

长度4字节,标记整个头部的长度,这里的值是0x00 00 00 70,十进制是112,如果我们选中010Editor下面的结构框的第一个名字叫 struct header_item dex_header 的item ,我们就能发现,上面选中的结构大小刚好是0H到6FH,共计70H个数据,这个值一般是固定的

EndianTag

长度4个字节,用来标记大小端,默认值是0x12 34 56 78,指定cpu的运行环境是大端还是小端,默认的intel使用的是小端,所以在010Editor上面看到的是78 56 34 12,这个请注意!!!

LinkSize & LinkOff 字段

这两个字段指定连接段的大小和对应的文件偏移地址,通常情况都为0,linksize为0表示静态链接

mapOff

指定了DexMapList的结构在Dex文件中的位置偏移,这个DexMapList结构是其他结构的一个数据大纲,里面记录了这些结构的一些信息。

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

struct DexMapItem {   
    u2 type;      /* kDexType开头的类型 */
    u2 unused;  /* 未使用,用于字节对齐 */
    u4 size;    /* type指定类型的个数,它们在dex文件中连续存放 */
    u4 offset;  /* 指定类型数据的文件偏移 */
};

/* type字段为一个枚举常量,通过类型名称很容易判断它的具体类型。 */
/* map item type codes */
enum {
    kDexTypeHeaderItem               = 0x0000,
    kDexTypeStringIdItem             = 0x0001,
    kDexTypeTypeIdItem               = 0x0002,
    kDexTypeProtoIdItem              = 0x0003,
    kDexTypeFieldIdItem              = 0x0004,
    kDexTypeMethodIdItem             = 0x0005,
    kDexTypeClassDefItem             = 0x0006,
    kDexTypeMapList                  = 0x1000,
    kDexTypeTypeList                 = 0x1001,
    kDexTypeAnnotationSetRefList     = 0x1002,
    kDexTypeAnnotationSetItem        = 0x1003,
    kDexTypeClassDataItem            = 0x2000,
    kDexTypeCodeItem                 = 0x2001,
    kDexTypeStringDataItem           = 0x2002,
    kDexTypeDebugInfoItem            = 0x2003,
    kDexTypeAnnotationItem           = 0x2004,
    kDexTypeEncodedArrayItem         = 0x2005,
    kDexTypeAnnotationsDirectoryItem = 0x2006,
};

DexMapListStruct

这个dex_map_list 结构保存在DEX文件的最末尾处,并且第一个字节就是保存的结构数量,也就是map_item的数量

map_item结构如下图所示,单一结构长度位12个字节,其中前两个字节是描述对应的段类型,紧跟着的2个字节是对齐字节,无意义。接下来的4个字节是对应的段大小,最后的4个字节是对应的段偏移,以上就是关于mapoff对应的内容的解释

stringIdsSize && stringIdsOff字段

这两个字段是用来标记所有字符串的,stringIdsSize标记字符串的数量,stringIdsOff标记字符串偏移的首地址,知道这两个数据以后,我们就可以进行字符串的解析了.

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

我们先看一下stringIdsOff 这个值,截图如下

string_ids_off

我们可以看到这个数值位 70 00 00 00,这个是不是很熟悉?我们去看一下,dex的header值是多少? 是70H,因为我们这个数值存放的问题,实际这个偏移值就是70H。

也就是说,我们的Dex头部的后面紧跟着就是字符串的相关数据了。我们接着说字符串数据的解析

我们把结构选择到010Editor的第二个结构体,也就是struct string_id_list dex_string_ids这里,选中这个结构体我们可以看到这个结构的全部内容。如下

string_id_list

这里的展开的每一个item就是一个字符串的索引,这里强调一下是索引。不是真正的字符串。这个值对应的是真正的字符串的偏移地址,我们后面需要用到的字符串,会通过这个索引进行查表找到对应的字符串。

typeIdsSize & typeIdsOff

类型区的大小和对应的类型名字的偏移索引。

这个索引的起始地址就是typeIdsOff对应的值,这个值在我的dex文件上面是0x00 02 DD 9C,然后这个起始地址开始有6393个数据,我们现在跳转到对应的地址看一下。

struct DexTypeId {
    u4 descriptorIdx;    /* 指向 DexStringId列表的索引 */
};

type_ids_offset

想要精准的跳到对应的地址,我们可以直接选择结构列表里面的struct type_id_list dex_type_ids,010Editor会自动帮我们定位到对应的位置,这个时候我们再来看一下这个结构的全部数据,已经被选中了.如果我们展开这个结构,下面是每一个item的索引和item的内部结构,我们可以数一下索引的值,我这里的是从0-6392,一共是6393个结构。所以这个刚好跟我们头部的数据是对应的。

接下来我们看一些type_id_item的值,大家可以在010Editor里面进行查看。里面能够看到很多的类型,例如byte,float,还有这个dex中的类对应的类型

type_id

protoIdsSize & protoIdsOff

protoIdsOff指向了函数原型的偏移地址,主要的标记内容 方法声明=返回类型 + 参数列表,protoIds标记对应的数量.从对应的value里面,我们就能看到函数返回值,参数类型等函数原型的标记

zstruct DexProtoId {
    u4 shortyIdx;   /* 指向DexStringId列表的索引 */
    u4 returnTypeIdx;   /* 指向DexTypeId列表的索引 */
    u4 parametersOff;   /* 指向DexTypeList的偏移 */
}

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

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

proto_ids

field_ids_size & field_ids_off

field_ids_off偏移地址指向的结构数据全部是索引值,指明了字段所在的类、字段的类型以及字段名

struct DexFieldId {
    u2 classIdx;   /* 类的类型,指向DexTypeId列表的索引 */
    u2 typeIdx;    /* 字段类型,指向DexTypeId列表的索引 */
    u4 nameIdx;    /* 字段名,指向DexStringId列表的索引 */
};

具体请看下图,class_idx 这个field所属的类的索引值,type_idx是这个field对应的类型索引值,name_idx是这个字段的名称对应的索引值

field_id

method_ids_size & method_ids_off

method_ids_off 指定了函数方法的偏移位置,具体的字段如下代码所示

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

classIdx 指向这个方法所在的类对象的索引,通过索引可以获取到类对象的名称

protoIdx指向的是这个方法声明的原型字符串的索引

nameIdx获取这个方法名称的字符串索引

有了这三个索引对应的字符串,我们就可以获取到正确的类,方法名,参数和返回值,还是使用我们的010Editor,我们打开method_id_list dex_method_ids这个结构下面的item,我们就可以看到这些数据获取以后,我们能知道的一些东西,具体看下图

method_idx

我们可以在结构体的最上面看到整个的方法原型,这个是由下面的三个字段对应的字符串值拼接成的,可以完整的看到方法的声明细节。

class_defs_size & class_defs_off

class_defs_off指向的是类定义的偏移地址,这里的类定义结构比较复杂,里面嵌套了很多层,我们先来看一下010Editor的结构

class_struct_item

在这个结构里面,我们能看到一些基本的信息,比如class_idx索引等等,访问的标志等等,

class_def_item的结构如下

uint   32-bit unsigned int, little-endian
struct class_def_item
{
    uint class_idx;         //描述具体的 class 类型,值是 type_ids 的一个 index 。值必须是一个 class 类型,不能是数组类型或者基本类型。   
    uint access_flags;        //描述 class 的访问类型,诸如 public , final , static 等。在 dex-format.html 里 “access_flags Definitions” 有具体的描述 
    uint superclass_idx;    //描述 supperclass 的类型,值的形式跟 class_idx 一样 
    uint interface_off;     //值为偏移地址,指向 class 的 interfaces,被指向的数据结构为 type_list 。class 若没有 interfaces 值为 0
    uint source_file_idx;    //表示源代码文件的信息,值是 string_ids 的一个 index。若此项信息缺失,此项值赋值为 NO_INDEX=0xffff ffff
    uint annotations_off;    //值是一个偏移地址,指向的内容是该 class 的注释,位置在 data 区,格式为 annotations_direcotry_item。若没有此项内容,值为 0 
    uint class_data_off;    //值是一个偏移地址,指向的内容是该 class 的使用到的数据,位置在 data 区,格式为 class_data_item。若没有此项内容值为 0。该结构里有很多内容,详细描述该 class 的 field、method, method 里的执行代码等信息,后面会介绍 class_data_item
    uint static_value_off;    //值是一个偏移地址 ,指向 data 区里的一个列表 (list),格式为 encoded_array_item。若没有此项内容值为 0
}

如果仔细对比你就会发现,010Editor会将annotations_off指向的数据放在他的后面,class_data_off,static_values_off也是一样的处理方式,其实这些数据都在dex结构的其他地方,但是010Editor为了让你查看方便,把他们都放在了一起,实际的class_def_item结构都是指向和偏移,真正的解析都在对应的地址位置而不在这个类定义的结构里面

class_def_item 中的 annotations_off指向的内容

annotations指向的是annotation 相关的数据描述,这个描述的结构具体如下

uint   32-bit unsigned int, little-endian
struct annotation_directory_item
{
    uint class_annotations_off;        //从文件开头到直接在该类上所做的注释的偏移量;如果该类没有任何直接注释,则该值为 0。该偏移量(如果为非零值)应该是到 data 区段中某个位置的偏移量。数据格式由下文的“annotation_set_item”指定。
    uint fields_size;//此项所注释的字段数量
    uint annotated_methods_size;//此项所注释的方法数量
    uint annotated_parameters_size;//此项所注释的方法参数列表的数量

    field_annotation field_annotations[fields_size];//(可选)    所关联字段的注释列表。该列表中的元素必须按 field_idx 以升序进行排序。
    method_annotation method_annotations[annotated_methods_size];//(可选)    所关联方法的注释列表。该列表中的元素必须按 method_idx 以升序进行排序。
    parameter_annotation parameter_annotations[annotated_parameters_size];//(可选)所关联方法参数的注释列表。该列表中的元素必须按 method_idx 以升序进行排序。
}
struct field_annotation
{
    uint field_idx;//字段(带注释)标识的 field_ids 列表中的索引
    uint annotations_off;    //字段(带注释)标识的 field_ids 列表中的索引
}
struct method_annotation
{
    uint method_idx;//方法(带注释)标识的 method_ids 列表中的索引
    uint annotations_off;    //从文件开头到该方法注释列表的偏移量。偏移量应该是到 data 区段中某个位置的偏移量。数据格式由下文的“annotation_set_item”指定。
}
struct parameter_annotation
{
    uint method_idx;//方法(其参数带注释)标识的 method_ids 列表中的索引
    uint annotations_off;    //从文件开头到该方法参数的注释列表的偏移量。偏移量应该是到 data 区段中某个位置的偏移量。数据格式由下文的“annotation_set_ref_list”指定。
}

struct annotation_set_ref_list
{
    unit size;//列表的大小(以条目数表示)
    annotation_set_ref_item[size] list;//列表的元素
}
struct annotation_set_ref_item
{
    unit annotations_off; //从文件开头到所引用注释集的偏移量;如果此元素没有任何注释,则该值为 0。该偏移量(如果为非零值)应该是到 data 区段中某个位置的偏移量。数据格式由下文的“annotation_set_item”指定。
}
struct annotation_set_item
{
    unit size; //该集合的大小(以条目数表示)
    annotation_off_item[size] entries; //该集合的元素。这些元素必须按 type_idx 以升序进行排序。
}

struct annotation_off_item
{
    unit annotation_off; //从文件开头到注释的偏移量。该偏移量应该是到 data 区段中某个位置的偏移量,且该位置的数据格式由下文的“annotation_item”指定。
}

struct annotation_item
{
    ubyte visibility; //此注释的预期可见性(见下文)
   encoded_annotation annotation; //已编码的注释内容,采用上文的“encoded_value 编码”下的“encoded_annotation 格式”所述的格式。
}

//可见值
// VISIBILITY_BUILD            0x00    预计仅在构建时(例如,在编译其他代码期间)可见
//VISIBILITY_RUNTIME    0x01    预计在运行时可见
//VISIBILITY_SYSTEM          0x02      预计在运行时可见,但仅对基本系统(而不是常规用户代码)可见
struct encoded_array_item
{
    encoded_array value; //用于表示编码数组值的字节,采用上文的“encoded_value 编码”下的“encoded_array 格式”指定的格式。
}


struct hiddenapi_class_data_item
{
    unit size; //该区段的总大小
    unit[] offsets; //由 class_idx 编入索引的偏移量数组。索引 class_idx 中的零数组意味着此 class_idx 没有任何数据,或者所有隐藏 API 标记均为零。否则,数组条目为非零值,并且包含从该区段开头到此 class_idx 的隐藏 API 标记数组的偏移量。
    bleb128[] flags; //每个类的隐藏 API 标记的级联数组。可能的标记值如下表所述。标记按照字段的相同编码顺序进行编码,且方法在类数据中编码。
}

限制标记类型:
名称                      值            说明
whitelist                0            此列表中的接口已在 Android 框架软件包索引中正式记录,它们是受支持的接口,您可以自由使用。
greylist                1    包含可以使用的非 SDK 接口的列表(无论应用的目标 API 级别是什么)。
blacklist                2    包含不能使用的非 SDK 接口的列表(无论应用的目标 API 级别是什么)。访问其中任何一个接口都会导致运行时错误。
greylist‑max‑o    3    包含可用于 Android 8.x 及下文的非 SDK 接口列表(除非这些接口受到限制)。
greylist‑max‑p    4    包含可用于 Android 9.x 的非 SDK 接口列表(除非这些接口受到限制)。
greylist‑max‑q    5    包含可用于 Android 10.x 的非 SDK 接口列表(除非这些接口受到限制)。

如果需要可以参考google的官方文档(需要翻墙)

https://source.android.com/devices/tech/dalvik/dex-format.html

class_def_item中的class_data_off

指向 data 区里的 class_data_item 结构,class_data_item 里存放着本 class 使用到的各种数据,下面是 class_data_item 的结构

uleb128 unsigned little-endian base 128 
struct class_data_item
{
    uleb128 static_fields_size; //静态成员变量的个数
    uleb128 instance_fields_size; //实例成员变量个数
    uleb128 direct_methods_size; //直接函数个数
    uleb128 virtual_methods_size; // 虚函数个数
    encoded_field  static_fields[static_fields_size];
    encoded_field  instance_fields[instance_fields_size];
    encoded_method direct_methods[direct_methods_size];
    encoded_method virtual_methods[virtual_methods_size];
}
struct encoded_field
{
    uleb128 filed_idx_diff; 
    uleb128 access_flags;  
}
struct encoded_method
{
    uleb128 method_idx_diff; //前缀 methd_idx 表示它的值是 method_ids 的一个 index ,后缀 _diff 表示它是于另 外一个 method_idx 的一个差值 ,就是相对于 encodeed_method [] 数组里上一个元素的 method_idx 的差值 。 其实 encoded_filed - > field_idx_diff 表示的也是相同的意思 ,只是编译出来的 Hello.dex 文件里没有使用到 class filed 所以没有仔细讲 ,详细的参考 https://source.android.com/devices/tech/dalvik/dex-format 官网文档。
    uleb128 access_flags; //访问权限,比如 public、private、static、final 等
    uleb128 code_off;//一个指向 data 区的偏移地址,目标是本 method 的代码实现。被指向的结构是code_item,有近 10 项元素
}

struct code_item 
{
    ushort                         registers_size; //本段代码使用到的寄存器数目
    ushort                         ins_size; //method 传入参数的数目
    ushort                         outs_size; //本段代码调用其它 method 时需要的参数个数
    ushort                         tries_size;//try_item 结构的个数
    uint                         debug_info_off;//偏移地址,指向本段代码的 debug 信息存放位置,是一个 debug_info_item 结构
    uint                         insns_size;
    ushort                         insns [insns_size]; 
    ushort                         paddding;             // optional
    try_item                     tries [tyies_size]; // optional
    encoded_catch_handler_list  handlers;             // optional
}


struct debug_info_off
{
    uleb128 line_start;//状态机的 line 寄存器的初始值。不表示实际的位置条目
    uleb128 parameters_size;//已编码的参数名称的数量。每个方法参数都应该有一个名称,但不包括实例方法的 this(如果有)
    uleb128p1[parameters_size] paramer_names;//方法参数名称的字符串索引。NO_INDEX 的编码值表示该关联参数没有可用的名称。该类型描述符和签名隐含在方法描述符和签名中
}
class_def_item中的static_value_off
uleb128  unsigned LEB128, valriable length
struct encoded_array_item
{
    encoded_array value; //表示encoded_value 个数
}
struct encoded_array
{    
    uleb128 size;
    encoded_value values[size];
}
map_list

我们在前面已经提到过这个map_list,他里面的内容是保存header中对应的描述,里面描述的更加全面,具体的结构如下

map_list 里先用一个 uint 描述后面有 size 个 map_item,后续就是对应的 size 个 map_item 描述。 map_item 结构有 4 个元素: type 表示该 map_item 的类型,Dalvik Executable Format 里 Type Code 的定义; size 表示再细分此 item,该类型的个数;offset 是第一个元素的针对文件初始位置的偏移量; unuse 是用对齐字节的,无实际用处。

ushort 16-bit unsigned int, little-endian
uint   32-bit unsigned int, little-endian
struct map_list 
{
    uint     size;
    map_item list [size]; 
}
struct map_item 
{
    ushort type; 
    ushort unuse; 
    uint   size; 
    uint   offset;
} 
leb128编码

Dalvik使用readUnsignedLeb128函数来尝试读取一个leb128编码的数值(代码位于dalvik\libdex\Leb128.h中),那么什么是uleb128呢?

LEB128即”Little-Endian Base 128”,基于128的小端序编码格式,是对任意有符号或者无符号整型数的可变长度的编码。用LEB128编码的正数,会根据数字的大小改变所占字节数。在android的.dex文件中,他只用来编码32bits的整型数。

img

例子

LEB128编码的0x02b0 ---> 转换后的数字0x0130
转换过程:
0x02b0 ---> 0000 0010 1011 0000 -->去除最高位--> 000 0010 011 0000 -->按4bits重排 --> 00 0001 0011 0000 --> 0x130
/*
 * Reads an unsigned LEB128 value, updating the given pointer to point
 * just past the end of the read value. This function tolerates
 * non-zero high-order bits in the fifth encoded byte.
 */
DEX_INLINE int readSignedLeb128(const u1** pStream) {
    const u1* ptr = *pStream;
    int result = *(ptr++);

    if (result <= 0x7f) {
        result = (result << 25) >> 25;
    } else {
        int cur = *(ptr++);
        result = (result & 0x7f) | ((cur & 0x7f) << 7);
        if (cur <= 0x7f) {
            result = (result << 18) >> 18;
        } else {
            cur = *(ptr++);
            result |= (cur & 0x7f) << 14;
            if (cur <= 0x7f) {
                result = (result << 11) >> 11;
            } else {
                cur = *(ptr++);
                result |= (cur & 0x7f) << 21;
                if (cur <= 0x7f) {
                    result = (result << 4) >> 4;
                } else {
                    cur = *(ptr++);
                    result |= cur << 28;
                }
            }
        }
    }
    *pStream = ptr;
    return result;
}

对应的Kotlin代码,我自己写的功能,请大家参考

private fun decodeUleb128(data: ByteArray): Long {
    var result: Long = 0L
    parserByte@
    for (index in data.indices) {
        val cur = data[index].toUInt()
        when (index) {
            0 -> {
                result = cur.toLong()
                if (cur <= 127u) {
                    //就一个byte,直接赋值,跳出循环
                    break@parserByte
                }
                //最高位为1,继续保存值到result里面,继续下一次循环
            }
            1 -> {
                //拼接数据,这里对result的值进行and操作,去掉高位数据
                val lowVal = (result and 0x7f).toInt()
                val hiVal = (cur and 127u).toInt() shl 7
                result = (lowVal or hiVal).toLong()
                if (cur <= 127u) {
                    //如果最高位不是0,没有数据了,直接返回
                    break@parserByte
                }
                //什么都不做,等着下一次循环,继续操作
            }
            2 -> {
                val hiVal = ((cur and 127u).toInt() shl 14)
                result = result or hiVal.toLong()
                if (cur <= 127u) {
                    break@parserByte
                }
            }
            3 -> {
                val hiVal = (cur and 127u).toInt() shl 21
                result = result or hiVal.toLong()
                if (cur <= 127u) {
                    break@parserByte
                }
            }
            4 -> {
                val hiVal = cur.toInt() shl 28
                result = result or hiVal.toLong()
                if (cur <= 127u) {
                    break@parserByte
                }
            }
        }
    }
    return result
}

文章作者: 孙老师
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 孙老师 !
 上一篇
无需权限获取应用程序列表 无需权限获取应用程序列表
无需权限获取应用程序列表最近在研究反编译的时候发现了系统的一种漏洞方式获取应用程序信息,这个可以无需任何权限,用户无任何感知的获取全部应用程序列表,这对于一些特别需要知道用户手机内是否安装了某些特定应用的的人来说真的是太好了. 经过我自己的
2019-12-29 孙老师
下一篇 
Android 代码优化与混淆 Android 代码优化与混淆
Android 代码优化与混淆1.android代码优化andriod gradle 插件3.4.0版本以上,不在使用ProGurad执行编译代码优化工作,转而使用R8编译器一起处理代码 1) 压缩代码,检测依赖库,安全的移除未使用的类,字
2019-12-14 孙老师
  目录