Dalvik 由 Google 成员 Dan Bornstein 带领编写,其名称来源于他的祖先曾居住过的小渔村,冰岛埃亚峡湾达尔维克(Dalvik)

Android 程序的运行环境不是标准的 Java 虚拟机,而是特有的 Android Runtime(ART),为保证兼容性,Android Runtime 与旧版 Android 所采用 Dalvik 都可执行由 Java Class 文件转换所得的 Dex 文件。

Dex File 即 Dalvik Executable File,Dalvik 可执行文件。

相比基于堆栈的标准 Java 虚拟机,Dalvik 更好地利用了 ARM CPU 中的大量寄存器,因此更适用于 Android。

此外,Dalvik 采用的开源协议为 Apache License 2.0,Standard Java Virtual Machine 采用的开源协议为 GPL v2。Google 声明在 Dalvik 的开发中没有使用任何 Oracle 的标准 Java 虚拟机代码,协议或许也是 Android 采用的 Dalvik 的原因之一,同时这也导致了 Google 与 Oracle 长久的互撕

一杯浓缩的咖啡

Android 执行的之所以是 Dex 文件而非标准 Java 虚拟机所采用的 Class 文件,原因在 Dex 文件相比 Class 文件有包括但不限于以下差异:

  • 一个 Class 文件对应一个 Java 源代码文件,而一个 Dex 文件可对应多个 Java 源代码文件,多个文件中的常量数据可以共享。

    实际上在低版本的 minSdkVersion 中,中小型的应用所有的代码最终都会整合进单一的 classes.dex 文件中,但因 Dex 文件中方法数这一字段的存储范围限制,如果应用及其引用库的方法数超过 65536,应用需要编译为多个 Dex 文件,这也被称为 64K 限制。

    当 minSdkVersion 设置为 21 或更高时,默认情况下启用 Multidex,此时编译应用时编译工具会根据需要构造主要 Dex 文件(classes.dex)及多个辅助 Dex 文件(classes2.dex 等)。

  • Dex 文件中采用了许多与 Class 文件不同的,降低存储空间的数据格式。

  • Dex 文件在描述方法的参数与返回值规则上采用 ShortyDescriptor 语法,使引用类型全限定名可以复用。

  • Dex 文件默认采用 little-endian(低字节在前)的存储顺序。

可以看到,在对于存储空间的利用上,Dex 文件远比 Class 文件要抠门得多,这一精简紧凑的可执行文件更适应于移动设备。

准备

因为官方文档提供对 Dex 文件的详细解析,因此本文仅用于官方文档的补充与协助理解。

官方文档:Dalvik 可执行文件格式

在这里我们采用一种较为另类的方法来协助理解 Dex 文件格式。

Google 现已推出 AOSP 原代码查询工具:Android Code Search ,可以更方便地查看和搜索源代码,而不需要为此下载占用大量空间的源码库。

在 Android Open Source Project 中存在用于读写内存或文件中 Dex 格式数据的相关类,通过对这部分源代码进行解读有利于我们更好地理解 Dex 文件的格式。这一部分源代码位于 dalvik/dex/src 中的 com.android.dex 包中。

概念说明

  • 偏移量(offset)

    在 Dex 文件格式中存在大量记录偏移量的字段,偏移量用于记录数据在文件中的位置,实际上也是另一种形式的索引,它们常以 xx_off 形式命名。

    大部分偏移量所指向的数据都位于数据区,但除特殊情况(比如 handler_off),偏移量均相对于文件开头偏移,而不是数据区开头。当需要用偏移量索引的数据实际并不存在时(如列表条目数为 0),偏移量被设为 0。

  • 数组索引

    与 Class 文件不同,所有数组的索引均从 0 开始。

    一些以 xx_idx_off 形式命名、用于存放数组索引的字段为所在列表前一项对应字段的差值,这可以减少存储空间的占用,可见 Dex 文件在存储空间利用上是真的抠

  • LEB128(Little-Endian Base 128)

    表示任意有符号或无符号整数的可变长度编码,该格式借鉴了 DWARF3 规范,在 Dex 文件中仅用于对 32 位数字进行编码。每一个 LEB128 编码由 1-5 个字节组成,每一个字节的最高位用于表示这是否为结尾字节(为 0 则表示这是结尾字节),其它 7 个位为有效负荷,记录实际数据,因为实际应用中很少会存储大整数,因此采用 LEB128 存储整数可以有效减少存储空间占用。

    LEB128 在 Dex 文件中有三种存储类型:sleb128(有符号整数,结尾字节有效负荷的最高位表示符号)、uleb128(无符号整数)、uleb128p1(无符号整数加一,这使得 -1 这一值可以用单个字节表示,对于一些正常不为负值但需要负值作为标志的情况非常实用)。

  • MUTF-8

    Dex 文件中存储字符串采用的格式是变种的 UTF-8 格式(即 Modified UTF-8),采用单字节、双字节或三字节进行编码,对于常用符号或字母(编码值于 0 到 127 之间),可直接用单个字节表示(基本相当于 ASCII)。

    MUTF-8 采用 UTF-16 编码。

  • magic

    其格式为 {‘d’, ‘e’, ‘x’, ‘\n’, version, ‘\0’},其中 version 是 Dex 文件的格式版本,为三个十进制数,每一个十进制数占一位,如 039 版的 Dex 文件的文件魔数为 “dex\n039\0”。

    在《深入理解 Android:Java 虚拟机 ART》及网络中部分博文中都曾提及:Dex 文件的魔数必须为字符串 “dex\n035\0”,这一说法其实并不正确,随着 Android Oreo 更新,这一规则不再适用。

    事实上,格式版本会随着 Dex 文件格式的变更而单调递增,035 版是因兼容性最广泛使用的 Dex 文件格式版本,在编译 Android 应用时 Dex 文件版本与 minSdkVersion 相关,如当 minSdkVersion 为 29 时生成的是 039 版的 Dex 文件。

  • checksum 与 signature

    checksum 用于检测文件的损坏情况,而 signature 一般用于对文件进行唯一标识。

    校验和与哈希值有什么区别?

    两者实际为包含关系,不是所有哈希值都可作为良好的校验和。校验和旨在检测数据中是否存在常见错误,而不是用于确保数据不发生任何更改(比如 CRC 校验和算法,做到篡改内容而 CRC 校验和不变实际并不困难),因此注重于运算速度而非安全性。

  • endian tag

    标准 Dex 文件的字节序为 little-endian,这一值为 ENDIAN_CONSTANT(0x12345678),但在具体中文件内容的字节序可能为为 big-endian,此时这一值为 REVERSE_ENDIAN_CONSTANT(0x78563412)。

    这一值采用 little-endian 的存储顺序。

代码解析

加载文件

com.android.dex.Dex 开始代码解析之旅,先来看看 Dex 文件是如何被加载的:

// com.android.dex.Dex

/**
 * Creates a new dex buffer from the dex file {@code file}.
 */
public Dex(File file) throws IOException {

    // 当传入的文件为 apk、jar、zip 时,解析压缩文件内的 classes.dex 文件

    if (FileUtils.hasArchiveSuffix(file.getName())) {
        ZipFile zipFile = new ZipFile(file);
        ZipEntry entry = zipFile.getEntry(DexFormat.DEX_IN_JAR_NAME);
        if (entry != null) {
            try (InputStream inputStream = zipFile.getInputStream(entry)) {
                loadFrom(inputStream);
            }
            zipFile.close();
        } else {
            throw new DexException("Expected " + DexFormat.DEX_IN_JAR_NAME + " in " + file);
        }
    }
    
    // 当传入的文件为 Dex 文件时,直接解析该文件

    else if (file.getName().endsWith(".dex")) {
        try (InputStream inputStream = new FileInputStream(file)) {
            loadFrom(inputStream);
        }
    }
    
    // 未知文件格式

    else {
        throw new DexException("unknown output extension: " + file);
    }
}

若文件格式正确,将会调用 loadFrom() 方法对文件的输入流进行操作。

// com.android.dex.Dex

/**
 * It is the caller's responsibility to close {@code in}.
 */
private void loadFrom(InputStream in) throws IOException {

    // 读取 Dex 文件的二进制数据
    
    ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
    byte[] buffer = new byte[8192];

    int count;
    while ((count = in.read(buffer)) != -1) {
        bytesOut.write(buffer, 0, count);
    }

    // data 成员变量保存被封装为 ByteBuffer 类型的 Dex 文件二进制数据

    this.data = ByteBuffer.wrap(bytesOut.toByteArray());
    
    // 将 data 转为 little-endian 字节序

    this.data.order(ByteOrder.LITTLE_ENDIAN);

    // 解析 tableOfContents

    this.tableOfContents.readFrom(this);
}

com.android.dex.TableOfContents 实例用于保存 Dex 文件数据的索引、偏移量等相关信息,本身不保存解析得到的数据,当我们需要获取数据时,Getter 方法会根据 tableOfContents 中的信息进行解析。

这一种方法虽然牺牲了获取数据时的速度,但是避免空间占用,只有当需要数据的时候再进行加载、解析。

首先需要了解 TableOfContents 的一个内部类,TableOfContents.Section

// com.android.dex.TableOfContents

/*
 * 这一内部类用于记录各个成员的大小与与偏移量
 */

public static class Section implements Comparable<Section> {
    public final short type;
    public int size = 0;
    public int off = -1;
    public int byteCount = 0;

    public Section(int type) {
        this.type = (short) type;
    }

    public boolean exists() {
        return size > 0;
    }

    @Override
    public int compareTo(Section section) {
        if (off != section.off) {
            return off < section.off ? -1 : 1;
        }
        return 0;
    }

    @Override
    public String toString() {
        return String.format("Section[type=%#x,off=%#x,size=%#x]", type, off, size);
    }
}

/*
    TableOfContents 的一系列成员变量
 */

public final Section header = new Section(0x0000);
public final Section stringIds = new Section(0x0001);
public final Section typeIds = new Section(0x0002);
public final Section protoIds = new Section(0x0003);
public final Section fieldIds = new Section(0x0004);
public final Section methodIds = new Section(0x0005);
public final Section classDefs = new Section(0x0006);
public final Section callSiteIds = new Section(0x0007);
public final Section methodHandles = new Section(0x0008);
public final Section mapList = new Section(0x1000);
public final Section typeLists = new Section(0x1001);
public final Section annotationSetRefLists = new Section(0x1002);
public final Section annotationSets = new Section(0x1003);
public final Section classDatas = new Section(0x2000);
public final Section codes = new Section(0x2001);
public final Section stringDatas = new Section(0x2002);
public final Section debugInfos = new Section(0x2003);
public final Section annotations = new Section(0x2004);
public final Section encodedArrays = new Section(0x2005);
public final Section annotationsDirectories = new Section(0x2006);
public final Section[] sections = {
    header, stringIds, typeIds, protoIds, fieldIds, methodIds, classDefs, mapList, callSiteIds,
    methodHandles, typeLists, annotationSetRefLists, annotationSets, classDatas, codes,
    stringDatas, debugInfos, annotations, encodedArrays, annotationsDirectories
};

com.android.dex.Dex 中同样存在一个同名的内部类,Dex.Section,用于记录指定偏移量的一个 ByteBuffer 拷贝。

// com.android.dex.Dex

public final class Section implements ByteInput, ByteOutput {
    private final String name;
    private final ByteBuffer data;
    private final int initialPosition;

    private Section(String name, ByteBuffer data) {
        this.name = name;
        this.data = data;
        this.initialPosition = data.position();
    }

    // ..
}

再来看看 com.android.dex.Dex 中一个重要的方法 open(),这一方法将返回指定偏移量的 Dex.Section 实例。

// com.android.dex.Dex

public Section open(int position) {

    // 检验偏移量是否在文件大小的范围内
    
    if (position < 0 || position >= data.capacity()) {
        throw new IllegalArgumentException("position=" + position
                + " length=" + data.capacity());
    }
    
    // 生成共享二进制数据的一个 ByteBuffer 拷贝
    
    ByteBuffer sectionData = data.duplicate();
    sectionData.order(ByteOrder.LITTLE_ENDIAN); // necessary?
    sectionData.position(position);
    sectionData.limit(data.capacity());
    return new Section("section", sectionData);
}

readFrom() 方法着手解析 TableOfContents 是如何从二进制数据中读取相关信息的。

// com.android.dex.TableOfContents

public void readFrom(Dex dex) throws IOException {

    // 读取文件头

    readHeader(dex.open(0));

    // 读取文件各部分的索引

    readMap(dex.open(mapList.off));

    // 计算文件各成员二进制数据占用空间

    computeSizesFromOffsets();
}



/*
    读取头文件内的数据
 */

private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException {

    // 读取 magic

    byte[] magic = headerIn.readByteArray(8);

    // 检验 magic 的合法性
    
    if (!DexFormat.isSupportedDexMagic(magic)) {
        String msg =
                String.format("Unexpected magic: [0x%02x, 0x%02x, 0x%02x, 0x%02x, "
                              + "0x%02x, 0x%02x, 0x%02x, 0x%02x]",
                              magic[0], magic[1], magic[2], magic[3],
                              magic[4], magic[5], magic[6], magic[7]);
        throw new DexException(msg);
    }

    // 根据 magic 中所保存的 Dex 格式版本获取 API 版本
    // 这一值对应 minSdkVersion

    apiLevel = DexFormat.magicToApi(magic);

    // 获取文件校验和

    checksum = headerIn.readInt();

    // 获取文件签名

    signature = headerIn.readByteArray(20);

    // 获取文件大小

    fileSize = headerIn.readInt();

    // 获取 header 大小

    int headerSize = headerIn.readInt();

    // 检验 header 大小是否为 0x70

    if (headerSize != SizeOf.HEADER_ITEM) {
        throw new DexException("Unexpected header: 0x" + Integer.toHexString(headerSize));
    }

    // 获取文件字节序标志

    int endianTag = headerIn.readInt();

    // 检验文件字节序标志

    if (endianTag != DexFormat.ENDIAN_TAG) {
        throw new DexException("Unexpected endian tag: 0x" + Integer.toHexString(endianTag));
    }

    // 读取 link_data 部分的大小与偏移量

    linkSize = headerIn.readInt();
    linkOff = headerIn.readInt();

    // 读取 map_list 部分的偏移量

    mapList.off = headerIn.readInt();
    if (mapList.off == 0) {
        throw new DexException("Cannot merge dex files that do not contain a map");
    }

    // 读取 string_ids 部分的大小与偏移量

    stringIds.size = headerIn.readInt();
    stringIds.off = headerIn.readInt();

    // 读取 type_ids 部分的大小与偏移量

    typeIds.size = headerIn.readInt();
    typeIds.off = headerIn.readInt();

    // 读取 proto_ids 部分的大小与偏移量

    protoIds.size = headerIn.readInt();
    protoIds.off = headerIn.readInt();

    // 读取 field_ids 部分的大小与偏移量

    fieldIds.size = headerIn.readInt();
    fieldIds.off = headerIn.readInt();

    // 读取 method_ids 部分的大小与偏移量

    methodIds.size = headerIn.readInt();
    methodIds.off = headerIn.readInt();

    // 读取 class_defs 部分的大小与偏移量

    classDefs.size = headerIn.readInt();
    classDefs.off = headerIn.readInt();

    // 读取 data 部分的大小与偏移量

    dataSize = headerIn.readInt();
    dataOff = headerIn.readInt();
}



/*
    读取 map_list 部分中的数据(各成员数据的大小与偏移量)
    并与 readHeader() 中所获取的各成员数据的大小与偏移量进行比对
 */

private void readMap(Dex.Section in) throws IOException {

    // 获取 map_list 条目数

    int mapSize = in.readInt();

    // 记录循环中的上一个 Section 实例,以确保 map_list 有效排序

    Section previous = null;

    // 读取 map_list 条目,并进行比对

    for (int i = 0; i < mapSize; i++) {
        short type = in.readShort();
        in.readShort(); // unused

        // 获取 sections 数组中对应的 Section 实例

        Section section = getSection(type);

        // 获取大小与偏移量

        int size = in.readInt();
        int offset = in.readInt();

        // 进行比对,当出错时抛出异常

        if ((section.size != 0 && section.size != size)
                || (section.off != -1 && section.off != offset)) {
            throw new DexException("Unexpected map value for 0x" + Integer.toHexString(type));
        }

        // 设置大小与偏移量(部分 map_list 中的数据在 header 中并没有冗余,如 type_list 等)

        section.size = size;
        section.off = offset;

        // 检验 map_list 是否有效排序

        if (previous != null && previous.off > section.off) {
            throw new DexException("Map is unsorted at " + previous + ", " + section);
        }

        previous = section;
    }

    // 根据偏移量排序 sections 数组

    Arrays.sort(sections);
}



/*
    计算各成员二进制数据占用空间
 */

public void computeSizesFromOffsets() {

    // 计算 data 成员的结尾位置

    int end = dataOff + dataSize;

    // 计算每一个成员二进制数据占用空间

    for (int i = sections.length - 1; i >= 0; i--) {
        Section section = sections[i];
        if (section.off == -1) {
            continue;
        }
        if (section.off > end) {
            throw new DexException("Map is unsorted at " + section);
        }
        section.byteCount = end - section.off;
        end = section.off;
    }
}

读取数据

当数据加载完后,如何读取其中的数据?

实际上,对于大部分成员,com.android.dex.Dex 提供了一个对应的内部类作为数据的 Getter,作为外部读取数据的接口。以读取字符串为例,对应的内部类为 Dex.StringIdTable

// com.android.dex.Dex

private final class StringTable extends AbstractList<String> implements RandomAccess {

    @Override
    public String get(int index) {

        // 检测索引是否越界

        checkBounds(index, tableOfContents.stringIds.size);

        // 返回指定索引的字符串

        return open(tableOfContents.stringIds.off + (index * SizeOf.STRING_ID_ITEM))
                .readString();
    }

    @Override
    public int size() {
        return tableOfContents.stringIds.size;
    }
}

// com.android.dex.Dex.Section

public String readString() {

    // 获取 string_data_item 所在位置的偏移量

    int offset = readInt();

    // 索引到 string_data_item 所在位置

    int savedPosition = data.position();
    int savedLimit = data.limit();
    data.position(offset);
    data.limit(data.capacity());

    // 解析 string_data_item
    
    try {

        // 获取字符串实际长度

        int expectedLength = readUleb128();

        // 解析 MUTF-8 格式的字符串

        String result = Mutf8.decode(this, new char[expectedLength]);

        // 检验长度是否一致

        if (result.length() != expectedLength) {
            throw new DexException("Declared length " + expectedLength
                    + " doesn't match decoded length of " + result.length());
        }
        return result;

    } catch (UTFDataFormatException e) {
        throw new DexException(e);
    } finally {
        data.position(savedPosition);
        data.limit(savedLimit);
    }
}

尝试解析一个 Dex 文件

以一个简单的 Java 类为例:

public class Test {

    private String privateField = "private field";
    public int publicField = 10;

    public String publicMethod(int value) {
        return "public method with value " + value;
    }

    private boolean privateMethod(double origin) {
        return origin > 0;
    }
}

通过 javac 编译生成 Class 文件。

javac Test.java --release 8

dx 无法转换高版本的字节码,因笔者采用 Java 13,需要通过 “–release " 指定生成 Class 文件的目标版本,在此为 Java 8。

然后使用 Android SDK build tools 中的 dx 工具转换为 Dex 文件。

dx --dex --output=test.dex Test.class

通过一段简单的代码对这一 Dex 文件中的数据进行遍历。

// Kotlin Code

package moe.aoramd.decode

import com.android.dex.Dex
import java.io.File

fun main() {

    val dexFile = File("test.dex")

    val dex = Dex(dexFile)

    val typeIds = dex.typeIds()
    val protoIds = dex.protoIds()

    // 遍历字符串
    
    println("--- String ---")
    val strings = dex.strings()
    strings.forEachIndexed { index, string ->
        println("$index : $string")
    }
    println()

    // 遍历类
    
    println("--- Class ---")
    val classDefs = dex.classDefs()
    classDefs.forEachIndexed { index, classDef ->
        val typeIndex = dex.typeIndexFromClassDefIndex(index)
        val typeString = strings[typeIds[typeIndex]]
        print("Name : $typeString | ")
        print("Access Flag : 0x${Integer.toHexString(classDef.accessFlags)}")
        println()
    }
    println()

    // 遍历成员变量
    println("--- Field ---")
    val fieldIds = dex.fieldIds()
    fieldIds.forEachIndexed { index, fieldId ->
        val typeIndex = dex.typeIndexFromFieldIndex(index)
        val typeString = strings[typeIds[typeIndex]]
        print("Class : ${strings[typeIds[fieldId.declaringClassIndex]]} | ")
        print("Type : $typeString | ")
        print("Name : ${strings[fieldId.nameIndex]}")
        println()
    }
    println()

    // 遍历方法
    
    println("--- Method ---")
    val methodIds = dex.methodIds()
    methodIds.forEach {
        val proto = protoIds[it.protoIndex]
        print("Class : ${strings[typeIds[it.declaringClassIndex]]} | ")
        print("Proto : ${strings[proto.shortyIndex]} | ")
        print("Name : ${strings[it.nameIndex]}")
        println()
        println("    > Parameters")
        dex.readTypeList(proto.parametersOffset).types.forEach { type ->
            println("      ${strings[typeIds[type.toInt()]]}")
        }
        println("    > Return")
        println("      ${strings[typeIds[proto.returnTypeIndex]]}")
    }
    println()
}

com.android.dex 中的代码可以利用 Maven 进行依赖:

https://mvnrepository.com/artifact/com.android/dex?repo=jboss-public

运行结果如下:

--- String ---
0 : <init>
1 : D
2 : I
3 : L
4 : LI
5 : LL
6 : LTest;
7 : Ljava/lang/Object;
8 : Ljava/lang/String;
9 : Ljava/lang/StringBuilder;
10 : Test.java
11 : V
12 : Z
13 : ZD
14 : append
15 : private field
16 : privateField
17 : privateMethod
18 : public method with value 
19 : publicField
20 : publicMethod
21 : toString

--- Class ---
Name : LTest; | Access Flag : 0x1

--- Field ---
Class : LTest; | Type : Ljava/lang/String; | Name : privateField
Class : LTest; | Type : I | Name : publicField

--- Method ---
Class : LTest; | Proto : V | Name : <init>
    > Parameters
    > Return
      V
Class : LTest; | Proto : ZD | Name : privateMethod
    > Parameters
      D
    > Return
      Z
Class : LTest; | Proto : LI | Name : publicMethod
    > Parameters
      I
    > Return
      Ljava/lang/String;
Class : Ljava/lang/Object; | Proto : V | Name : <init>
    > Parameters
    > Return
      V
Class : Ljava/lang/StringBuilder; | Proto : V | Name : <init>
    > Parameters
    > Return
      V
Class : Ljava/lang/StringBuilder; | Proto : LI | Name : append
    > Parameters
      I
    > Return
      Ljava/lang/StringBuilder;
Class : Ljava/lang/StringBuilder; | Proto : LL | Name : append
    > Parameters
      Ljava/lang/String;
    > Return
      Ljava/lang/StringBuilder;
Class : Ljava/lang/StringBuilder; | Proto : L | Name : toString
    > Parameters
    > Return
      Ljava/lang/String;

参考链接