Dalvik学习之class dex odex文件结构

文中所有内容均是邓凡平老师的 深入理解Android之Dalvik 和丰生强老师的 Android软件安全与逆向分析 阅读中的笔记

class文件结构

ClassFile{
    //唯一取值:0xCAFEBABE
    u4              magic;
    
    //class文件的版本号,和Java编译器有关
    u2              minor_version;
    u2              major_version;
    
    //常量池,长度为字符串数量加1,constant_pool[0]留给JVM用
    u2              constant_pool_count;
    cp_info         constant_pool[constant_pool_count - 1];
    
    //class的类型和名字,类型有三种 0x0001:ACC_PUBLIC 
    //0x0010:ACC_FINAL 0x0200:ACC_INTERFACE
    u2              access_flag;
    u2              this_class;
    u2              super_class;
    
    //类interface数量,变量数量,方法数量(无论static还是非static),
    //属性数量,名字都作为字符串存在常良池中
    u2              interfaces_count;
    u2              interfaces[interfaces_count - 1];
    u2              fields_count;
    field_info      fields[fields_count - 1];
    u2              methods_count;
    method_info     methods[methods_count - 1];
    u2              attributes_count;
    attribute_info  attributes[attributes_count - 1];
    }

class文件实操,先写一个简单的Java文件:

package com.example;

public class MyClass {
    public static void main(String[] args){
        String s = "hello world";
        System.out.println(s);
    }
}

然后调用javac MyClass.java生成MyClass.class
调用javap -verbose MyClass.class查看

Dalvik学习之class dex odex文件结构_第1张图片
class文件结构.jpg

常量池

tips:constant_pool_count值为常量池数组长度+1,就像上图中常量第一个元素
以#1开头,0默认是给VM用的

常量池的元素类型这样表示:

cp_info { 
    //特别注意,这是介绍的cp_info 是相关元素类型的通用表达。
    u1 tag; //tag 为1 个字节长。不论cp_info 具体是哪种,第一个字节一定代表tag
    u1 info[]; //其他信息,长度随tag 不同而不同
}

tag的取值:

  • tag=7 <==info 代表这个cp_info 是CONSTANT_Class_info 结构体
  • tag=9 <==info 代表CONSTANT_Fieldrefs_info 结构体
  • tag=10 <==info 代表CONSTANT_Methodrefs_info 结构体
  • tag=8 <==info 代表CONSTANT_String_info 结构体
  • tag=1 <==info 代表CONSTANT_Utf8_info 结构体

看几个例子,字符串结构体:

CONSTANT_Utf8_info {
    u1 tag;    //取值为1
    u2 length; //下面就是存储UTF8 字符串的地方了
    u1 bytes[length];
}

类信息结构体

CONSTANT_Class_info {
    u1 tag; //tag 取值为7,代表CONSTANT_Class_info
    u2 name_index; //name_index 表示代表自己类名的字符串信息位于于常量池数组中哪一个,也就是索引
}

dex文件

class文件显然有很多可以优化的地方,比如每一个class文件都有一个常量池,如果有重复字符串就造成了资源浪费,所以Dalvik的dex文件对其进行了优化


Dalvik学习之class dex odex文件结构_第2张图片
dex文件结构

先看看dex文件中的数据结构

下面很多代码定义在Android源码的 DexFile.h 中

类型 含义
u1 等同于uint8_t,一个字节的无符号数
u2 等同于uint16_t,两个字节的无符号数
u4 等同于uint32_t,四个字节的无符号数
u8 等同于uint64_t,八字节的无符号数
sleb128 有符号LEB128,可变长度1~5字节
uleb128 无符号LEB128,可变长度1~5字节
uleb128p1 无符号LEB128值加1,可变长度1~5字节
  • sleb128是dex文件中特有的数据类型,每个字节7个有效位,最高位取值1表示要用到第二个字节,以此类推但最长五个字节,如果读取到
    第五个字节最高位仍为1,表示该dex文件无效,Dalvik虚拟机在验证dex时会失败返回
  • dex文件里采用了变长方式表示字符串长度。一个字符串的长度可能是一个字节(小于256)或者4个字节(1G大小以上)。字符串的长度大多数都是小于 256个字节,因此需要使用一种编码,既可以表示一个字节的长度,也可以表示4个字节的长度,并且1个字节的长度占绝大多数。能满足这种表示的编码方式有 很多,但dex文件里采用的是uleb128方式。leb128编码是一种变长编码,每个字节采用7位来表达原来的数据,最高位用来表示是否有后继字节。
    查看dex方法
  1. class转为dex文件,工具是sdk build_tools下的dx命令。dx --dex --debug --verbose-dump--output=test.dex com/test/TestMain.class
  2. 查看dex文件,利用build-tools 下的dexdump 命令查看,dexdump -d -l plain test.dex

dex文件整体结构

整体结构比较简单,由七个结构体组成:

  • dex header 指定了dex文件的一些属性,并记录其他六个部分在dex文件中的物理偏移
  • string_ids
  • type_ids
  • proto_ids
  • field_ids
  • method_ids
  • class_def
  • data
  • link_data

dexHeader结构体的组成

struct DexHeader {
000 u1 magic[8];                    //dex版本标识
    u4 checksum;                    //adler32检验
    u1 signature[KSHA1DIGESTLEN];   //SHA-1哈希值 长度为20,定义在DexFile.h中
020 u4 fileSize;                    //整个文件大小
    u4 headerSize;                  //DexHeader结构大小 70 00 00 00
    u4 endianTag;                   //字节序标记 预设78 56 34 12 即0x12345678,表示小端little-Endian字节序
    u4 linkSize;                    //链接段大小
030 u4 linkoff;                     //连接段偏移
    u4 mapoff;                      //DexMapList的文件偏移,这里mapoff等于dataOff
    u4 stringIdsSize;               //DexStringId的个数
    u4 stringIDsOff;                //DexStringId的文件偏移
040 u4 typeIdsSize;                 //DexTypeID的个数
    u4 typeIdsOff;                  //DexTypeId的文件偏移
    u4 protoIdsSize;                //DexProtoId的个数
    u4 protoIdsOff;                 //DexProtoId的文件偏移
050 u4 fieldIdsSize;                //DexFieldId的个数
    u4 fieldIdsOff;                 //DexFieldId的文件偏移
    u4 methodIdsSize;               //DexMethodId的个数
    u4 methodIdsOff;                //DexMethonId的文件偏移
060 u4 classDefsSize;               //DexClassDef的个数
    u4 classDefsOff;                //DexClassDef的文件偏移
    u4 dataSize;                    //数据段的大小
    u4 dataOff;                     //数据段的文件偏移
}

tips:
由上面结构体也可以看出来,Android 65K方法数问题的根本原因并不在于Dex文件方法索引长度限制

dex文件结构分析

tips:
这里 书中(Android软件安全与逆向分析)有一点不明白,说Dalvik虚拟机解析dex文件的内容,最终将其映射成DexMapList数据结构
,是说Dex文件生成过程中有Dalvik虚拟机的参与吗。

我分析了一个简单的Android程序,使用十六进制编辑器C32Asm,打开apk解压出的dex文件

Dalvik学习之class dex odex文件结构_第3张图片
dex文件

上图就是完整DexHeader的数据,在注释里写得很清楚了,观察发现,mapOff值为0x00059178,这里要注意小端字节序,找到

Dalvik学习之class dex odex文件结构_第4张图片
DexMapList

红色框画出来的就是每个元素头部,其中第一个0x12,代表有16个DexMapItem结构
DexMapItem结构:

struct DexMapItem{
    u2 type;        //类型,枚举常量
    u2 unused;      //未使用,用于字节对其
    u4 size;        //指定类型的个数
    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,
};

举个香甜的栗子,找到DexMapList中的StringIdItem,个数:0x39EE,偏移:0x0070,去找0x0070中的第一个

0x0070

看一看DexStringId的结构体:

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

偏移量是0x0012c120,找到它

Dalvik学习之class dex odex文件结构_第5张图片
0x0012c120

已经找到字符串了

再找一个复杂些的,找到DexMapList中的MethodIdItem,个数0x000038DD,偏移0x00026E98,找到它

0x00026E98

看一看DexMethodId的结构体:

/*
 * Direct-mapped "method_id_item".
 */
struct DexMethodId {
    u2  classIdx;           /* index into typeIds list for defining class */
    u2  protoIdx;           /* index into protoIds for method prototype */
    u4  nameIdx;            /* index into stringIds for method name */
};

odex文件

odex文件有两种存在方式:

  1. 从Apk文件中提取出来,与Apk文件存放在同一目录下且文件后缀为odex的文件,这种多是Android ROM的系统程序;
  2. dalvik-cache缓存文件,这类odex文件仍然以dex作为后缀,存放在cache/dalvik-cache目录下,保存形式为"apk路径@apk名@classes.dex";

由于Android程序的Apk文件为Zip压缩包格式,Dalvik虚拟机每次加载他们时需要从Apk中读取classes.dex文件,这样会耗费很多CPU时间,而采用odex
方式优化的dex文件已经包含了加载dex必须的依赖库文件列表,Dalvik虚拟机只需检测并加载所需的依赖库即可执行相应的dex文件,这大大缩短了读取dex文件
所需的时间。

odex文件整体结构

  • odex文件头
  • dex文件
  • 依赖库
  • 辅助数据

odex文件的写入和读取并没有像dex文件那样定义了全系列的数据结构,Dalvik虚拟机将dex文件映射到内存中后是DexFile格式,结构如下:

/*
 * Structure representing a DEX file.
 *
 * Code should regard DexFile as opaque, using the API calls provided here
 * to access specific structures.
 */
struct DexFile {
    /* directly-mapped "opt" header */
    const DexOptHeader* pOptHeader;

    /* pointers to directly-mapped structs and arrays in base DEX */
    const DexHeader*    pHeader;
    const DexStringId*  pStringIds;
    const DexTypeId*    pTypeIds;
    const DexFieldId*   pFieldIds;
    const DexMethodId*  pMethodIds;
    const DexProtoId*   pProtoIds;
    const DexClassDef*  pClassDefs;
    const DexLink*      pLinkData;

    /*
     * These are mapped out of the "auxillary" section, and may not be
     * included in the file.
     */
    const DexClassLookup* pClassLookup;
    const void*         pRegisterMapPool;       // RegisterMapClassPool

    /* points to start of DEX file data */
    const u1*           baseAddr;

    /* track memory overhead for auxillary structures */
    int                 overhead;

    /* additional app-specific data structures associated with the DEX */
    //void*               auxData;
};

最前面的DexOptHeader就是odex的头,DexLink一下的部分是"auxillary section",即辅助数据段,记录了文件被优化后添加的一些信息。不过DexFile
机构描述的是加载金内存的数据结构,还有一些数据是不会加载进内存的。丰生强老师将odex文件结构定义整理如下:

struct ODEXFile {
    DexOptHeader            header;     //odex文件头
    DexFile                 DexFile;    //dex文件
    Dependences             deps;       //依赖库列表
    ChunkDexClassLookup     lookup;     //类查询结构
    ChunkRegisterMapPool    mapPool;    //映射池
    ChunkEnd                end;        //结束标识   
}

odex文件结构分析

ODEXFile的文件头DexOptHeader在DexFile.h文件中定义如下:

struct DexOptHeader{
    u1 magic[8];            //odex版本标识 ,目前固定值 64 65 79 0A 30 33 36 00
    u4 dexOffset;           //dex文件头偏移 ,目前0x28 = 40,等于odex文件头大小
    u4 dexLength;           //dex文件总长度
    u4 depsOffset;          //odex依赖库列表偏移
    u4 depsLength;          //依赖库列表总长度
    u4 optOffset;           //辅助数据偏移
    u4 optLength;           //辅助数据总长度
    u4 flags;               //标志,Dalvik虚拟机加载odex时的优化与验证选项
    u4 checksum;            //依赖库与辅助数据的校验和
}

你可能感兴趣的:(Dalvik学习之class dex odex文件结构)