写点什么

浅谈 Android Dex 文件

  • 2020-03-11
  • 本文字数:5559 字

    阅读完需:约 18 分钟

浅谈 Android Dex 文件

概述

为什么要了解 Dex 文件

了解了 Dex 文件以后,对日常开发中遇到一些问题能有更深的理解。如:APK 的瘦身、热修复、插件化、应用加固、Android 逆向工程、64K 方法数限制。

什么是 Dex 文件

在明白什么是 Dex 文件之前,要先了解一下 JVM,Dalvik 和 ART。JVM 是 JAVA 虚拟机,用来运行 JAVA 字节码程序。Dalvik 是 Google 设计的用于 Android 平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART 即 Android Runtime,是 Google 为了替换 Dalvik 设计的新 Android 运行时环境,在 Android4.4 推出。ART 比 Dalvik 的性能更好。Android 程序一般使用 Java 语言开发,但是 Dalvik 虚拟机并不支持直接执行 JAVA 字节码,所以会对编译生成的 .class 文件进行翻译、重构、解释、压缩等处理,这个处理过程是由 dx 进行处理,处理完成后生成的产物会以 .dex 结尾,称为 Dex 文件。Dex 文件格式是专为 Dalvik 设计的一种压缩格式。所以可以简单的理解为:Dex 文件是很多 .class 文件处理后的产物,最终可以在 Android 运行时环境执行。

Dex 文件是怎么生成的

Java 代码转化为 Dex 文件的流程如图所示,当然真的处理流程不会这么简单,这里只是一个形象的显示:



注:图片来源于网络


现在来通过一个简单的例子实现 Java 代码到 Dex 文件的转化。

从.java 到.class

先来创建一个 Hello.java 文件,为了便于分析,这里写一些简单的代码。代码如下:


public class Hello {  private String helloString = "hello! youzan";
public static void main(String[] args) { Hello hello = new Hello(); hello.fun(hello.helloString); }
public void fun(String a) { System.out.println(a); }}
复制代码


在该文件的同级目录下面使用 JDK 的 javac 编译这个 java 文件。


javac Hello
复制代码


javac 命令执行后会在当前目录生成 Hello.class 文件,Hello.class 文件已经可以直接在 JVM 虚拟机上直接执行。这里使用使用命令执行该文件。


java Hello
复制代码


执行后应该会在控制台打印出“hello! youzan”


这里也可以对 Hello.class 文件执行 javap 命令,进行反汇编。


javap -c Hello
复制代码


执行结果如下:


public class Hello { public Hello();  Code:    0: aload_0    1: invokespecial #1         // Method java/lang/Object."<init>":()V    4: aload_0    5: ldc      #2         // String hello! youzan    7: putfield   #3         // Field helloString:Ljava/lang/String;   10: return
public static void main(java.lang.String[]); Code: 0: new #4 // class Hello 3: dup 4: invokespecial #5 // Method "<init>":()V 7: astore_1 8: aload_1 9: aload_1 10: getfield #3 // Field helloString:Ljava/lang/String; 13: invokevirtual #6 // Method fun:(Ljava/lang/String;)V 16: return
public void fun(java.lang.String); Code: 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: aload_1 4: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 7: return}
复制代码


其中 Code 之后都是具体的指令,供 JVM 虚拟机执行。指令的具体含义可以参考 JAVA 官方文档。

从.class 到.dex

上面生成的 .class 文件虽然已经可以在 JVM 环境中运行,但是如果要在 Android 运行时环境中执行还需要特殊的处理,那就是 dx 处理,它会对 .class 文件进行翻译、重构、解释、压缩等操作。


dx 处理会使用到一个工具 dx.jar,这个文件位于 SDK 中,具体的目录大致为 你的 SDK 根目录/build-tools/任意版本 里面。使用 dx 工具处理上面生成的 Hello.class 文件,在 Hello.class 的目录下使用下面的命令:


dx --dex --output=Hello.dex Hello.class
复制代码


执行完成后,会在当前目录下生成一个 Hello.dex 文件。这个 .dex 文件就可以直接在 Android 运行时环境执行,一般可以通过 PathClassLoader 去加载 dex 文件。现在在当前目录下执行 dexdump 命名来反编译:


dexdump -d Hello.dex
复制代码


执行结果如下(部分区域的含义已经在下面描述):


Processing 'Hello.dex'...Opened 'Hello.dex', DEX version '035'
------ 这里是编写的Hello.java的类的信息 ------Class #0 - Class descriptor : 'LHello;' Access flags : 0x0001 (PUBLIC) Superclass : 'Ljava/lang/Object;' Interfaces - Static fields - Instance fields - #0 : (in LHello;) name : 'helloString' type : 'Ljava/lang/String;' access : 0x0002 (PRIVATE)
------ 下面区域描述的是构造方法的信息。7010 0400 0100 1a00 0b00 之类的数字就是方法中的代码翻译成的指令。Dalvik使用的是16位代码单元,所以这里就是4个数字为一组,每个数字是16进制。invoke-direct 这些是前面指令对应的助记符,也代表着这些指令的真正操作。如果对这些指令转化感兴趣可以去https://source.android.com/devices/tech/dalvik/instruction-formats 查看 ------ Direct methods - #0 : (in LHello;) name : '<init>' --- 方法名称:这个很明显就是构造方法 --- type : '()V' --- 方法原型,()里面表示入参,()后面表示返回值,V代表void--- access : 0x10001 (PUBLIC CONSTRUCTOR) --- 方法访问类型 --- code - registers : 2 --- 方法使用的寄存器数量 --- ins : 1 --- 方法入参,方法除了我们定义的参数以外,系统还会默认带一个特殊参数 --- outs : 1 insns size : 8 16-bit code units --- 指令大小 ---000148: |[000148] Hello.<init>:()V000158: 7010 0400 0100 |0000: invoke-direct {v1}, Ljava/lang/Object;.<init>:()V // method@000400015e: 1a00 0b00 |0003: const-string v0, "hello! youzan" // string@000b000162: 5b10 0000 |0005: iput-object v0, v1, LHello;.helloString:Ljava/lang/String; // field@0000000166: 0e00 |0007: return-void catches : (none) positions : 0x0000 line=1 0x0003 line=2 locals : 0x0000 - 0x0008 reg=1 this LHello;
#1 : (in LHello;) name : 'main' type : '([Ljava/lang/String;)V' access : 0x0009 (PUBLIC STATIC) code - registers : 3 ins : 1 outs : 2 insns size : 11 16-bit code units000168: |[000168] Hello.main:([Ljava/lang/String;)V000178: 2200 0000 |0000: new-instance v0, LHello; // type@000000017c: 7010 0000 0000 |0002: invoke-direct {v0}, LHello;.<init>:()V // method@0000000182: 5401 0000 |0005: iget-object v1, v0, LHello;.helloString:Ljava/lang/String; // field@0000000186: 6e20 0100 1000 |0007: invoke-virtual {v0, v1}, LHello;.fun:(Ljava/lang/String;)V // method@000100018c: 0e00 |000a: return-void catches : (none) positions : 0x0000 line=5 0x0005 line=6 0x000a line=7 locals :
Virtual methods - #0 : (in LHello;) name : 'fun' type : '(Ljava/lang/String;)V' access : 0x0001 (PUBLIC) code - registers : 3 ins : 2 outs : 2 insns size : 6 16-bit code units000190: |[000190] Hello.fun:(Ljava/lang/String;)V0001a0: 6200 0100 |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@00010001a4: 6e20 0300 2000 |0002: invoke-virtual {v0, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@00030001aa: 0e00 |0005: return-void catches : (none) positions : 0x0000 line=10 0x0005 line=11 locals : 0x0000 - 0x0006 reg=1 this LHello;
source_file_idx : 1 (Hello.java)
复制代码


到此为止,已经完成了将 Java 代码转变成 Dalvik 可执行的文件,即 dex。

Dex 文件的具体格式

现在来分析一下 Dex 文件的具体格式,就像 MP3,MP4,JPG,PNG 文件一样,Dex 文件也有它自己的格式,只有遵守了这些格式,才能被 Android 运行时环境正确识别。


Dex 文件整体布局如下图所示:



这些区域的数据互相关联,互相引用。由于篇幅原因,这里只是显示部分区域的关联,完整的请去官网自行查看相关数据整理。下图中的各字段都在后面的各区域的详细介绍中有具体介绍。



下面将分别对文件头、索引区、类定义区域进行简单的介绍。其它区域可以去 Android 官网了解。

文件头

文件头区域决定了该怎样来读取这个文件。具体的格式如下表(在文件中排列的顺序就是下面表格中的顺序):


id 区

id 区存储着字符串,type,prototype,field, method 资源的真正数据在文件中的偏移量,我们可以根据 id 区的偏移量去找到该 id 对应的真实数据。

字符串 id 区域

这个区块是一个偏移量列表,每个偏移量对应了一个真正的字符串资源,每个偏移量占 32 位。我们可以通过偏移量找到对应的实际字符串数据。具体格式如下:



最终这个偏移的位置应该是落在数据区的。找到这个偏移量的位置后,根据下面的格式就可以读取出这个字符串资源的具体数据:


类型 id 区

这个区块是一个索引列表,索引的值对应字符串 id 区域偏移量列表中的某一项。数据格式如下:



如果我们要找到某个类型的值,需要先根据类型 id 列表中的索引值去字符串 id 列表中找到对应的项,这一项存储的偏移量对应的字符串资源就是这个类型的字符串描述。

方法原型 id 区

这个区块是一个方法原型 id 列表,数据格式为:


成员 id 区

这个区块存储着原型 id 列表,数据格式为:


方法 id 区

这个区块存储着方法 id 列表,数据格式为: 这个区块存储着原型 id 列表,数据格式为:


类定义区

这个区域存储的是类定义的列表,具体的数据结构如下:


解析 dex 文件的工具

这里推荐一个可以解析 dex 文件的工具 010 Editor。它可以通过预置的模板让我们更清晰的了解 dex 文件的格式。


Dex 文件在 Android Tinker 热修复中的应用

在目前的主流的 Android 热修复方案中,Tinker 有免费、开源、用户量大等优点,因此在有赞也是基于 Tinker 搭建 Android 热修复服务。Tinker 热修复的主要原理就是通过对比旧 APK 的 dex 文件与新 APK 的 dex 文件,生成补丁包,然后在 APP 中通过补丁包与旧 APK 的 dex 文件合成新的 dex 文件。流程如下图所示:



注:图片来源于 Tinker 官网

补丁包的生成

Tinker 官方使用自研一套合成方案,就是 DexDiff。它基于 Dex 文件格式的特性,具有补丁包小,消耗内存小等优点。在 DexDiff 算法中,会根据 Dex 文件的格式,将 Dex 文件划分为不同的区块类,如下图:



这些区块有一个统一的数据结构,主要的数据有区块对应的实际数据类型及在文件中的偏移量。如下图:



有了区块数据中的实际数据类型与偏移量,再根据实际数据类型对应的数据结构就可以从文件中读出这个区块包含的实际数据。这里以 header 区域为例,读取代码如下(删除了部分无关代码,代码可以参照上面的 Dex 文件格式的文件头的介绍):


private void readHeader(Dex.Section headerIn) throws UnsupportedEncodingException { byte[] magic = headerIn.readByteArray(8);  int apiTarget = DexFormat.magicToApi(magic); checksum = headerIn.readInt();  signature = headerIn.readByteArray(20); fileSize = headerIn.readInt(); int headerSize = headerIn.readInt(); int endianTag = headerIn.readInt(); linkSize = headerIn.readInt(); linkOff = headerIn.readInt(); mapList.off = headerIn.readInt(); stringIds.size = headerIn.readInt(); stringIds.off = headerIn.readInt(); typeIds.size = headerIn.readInt(); typeIds.off = headerIn.readInt(); protoIds.size = headerIn.readInt(); protoIds.off = headerIn.readInt(); fieldIds.size = headerIn.readInt(); fieldIds.off = headerIn.readInt(); methodIds.size = headerIn.readInt(); methodIds.off = headerIn.readInt(); classDefs.size = headerIn.readInt(); classDefs.off = headerIn.readInt(); dataSize = headerIn.readInt(); dataOff = headerIn.readInt();}
复制代码


从文件中读取到新旧 Dex 文件各区块的具体的数据后,就可以进行对比生成补丁包了。因为各区块的数据结构不一致,因此各区块有着相应的 diff 算法来处理各区块补丁生成与合成。算法列表如图:



这些算法会对比新旧 Dex 文件转化成数据结构以后数据的差异,然后生成相关的操作指令,存储到补丁文件,下发到客户端。

补丁的合成

客户端收到补丁文件后,会使用相同的读取方式,将旧 Dex 文件转换为相关的数据结构,然后使用补丁包中的操作指令,对旧 Dex 数据进行修改,生成新 Dex 数据,最后数据写入文件,生成新 Dex 文件,这样就完成了补丁的合成。

写在最后

本文并没有写什么特别深入的东西,对 dex 的文件格式也没有完全描述完全。主要是给大家分享一个 dex 文件的大致结构,还有一些在实际中的应用。让大家在以后遇到相关问题的时候,可以有一些方向去了解 dex 文件,然后解决问题。最后,如果大家有任何的建议或意见,欢迎反馈。

参考资源


Dalvik 和 Java 字节码的对比(http://www.importnew.com/596.html


2020-03-11 22:191496

评论

发布
暂无评论
发现更多内容

百度Apollo参编首批汽车信息安全国家标准,跻身车联网信息安全第一梯队

百度开发者中心

Apollo

看完这篇SpringBoot让我在阿里成功涨薪40%,感谢

Java 编程 程序员 程序人生 springboot

万字长文解密数据异构最佳实践(含完整代码实现)!!

冰河

MySQL 数据库 canal 数据同步 数据异构

操作系统——计算机硬件简介

思想者杰克

顶会VLDB‘22论文解读:CAE-ENSEMBLE算法

华为云开发者联盟

算法 时间序列 华为云数据库 深度神经网络

百度智能云人脸采集SDK通过CFCA权威安全测评

百度开发者中心

安全 sdk

极客时间【架构实战营】第二期 模块七作业

Geek_91606e

架构实战营

TDengine在浙商银行微服务监控中的实践

TDengine

tdengine 后端 时序数据库

牛掰!“基础-中级-高级”Java程序员面试集结,看完献出我的膝盖

Java spring 程序员 JVM hashmap

linux lsquic 编译

webrtc developer

《黑客之道》kali Linux之NMAP高级使用技巧和漏洞扫描发现

学神来啦

Linux 渗透 kali nmap

网课录屏用什么软件好?Camtasia极简操作,课程重点一目了然

淋雨

Camtaisa

四种 AI 技术方案,教你拥有自己的 Avatar 形象

阿里云CloudImagine

人工智能 阿里云 计算机视觉 视频云 元宇宙

你需要知道的 19 个 console 实用调试技巧

CRMEB

CSS JavaScript DOM console crmeb

行业白皮书发布!百度智慧城市助力城市“双碳”目标达成

百度开发者中心

人工智能 智慧城市

ReplacingMergeTree:实现Clickhouse数据更新

华为云开发者联盟

数据 事务 Clickhouse 数据更新 OLAP数据库

【高并发】开篇:线程与多线程

冰河

Java 并发编程 多线程 高并发 异步编程

百度鸿鹄芯片落地首款量产车吉利博越X 智能车机体验惊艳成都车展

百度开发者中心

百度 车联网 鸿鹄芯片

Github上线仅六天,收获Star超55K+,这套笔记能拿下90%以上面试

Java redis spring 程序员 架构

GitHub远程免密连接详解,还顺手解决了RPC失败HTTP413

老表

GitHub RPC HTTP 11月日更

架构实战营-总结

哈希

恒源云(GPUSHARE)_CAN: 借助数据分布提升分类性能

恒源云

深度学习

百度智能云与雅量商业智能携手,加速零售行业智能化升级

百度开发者中心

百度智能云 零售行业

阿里云视频云,用技术普惠打造平民化“虚拟人”

阿里云CloudImagine

人工智能 阿里云 视频云 数字人 虚拟人

常用的Nmap脚本及使用实例

喀拉峻

网络安全 信息安全 渗透测试 脚本 nmap

端开发技术——5个高效的Flutter开发工具

思想者杰克

“智慧粮仓”守卫中国饭碗

ThingJS数字孪生引擎

大前端 物联网 可视化

接口文档工具yapi的安装

小鲍侃java

11月日更

科大讯飞联袂伯俊科技进军3C零售,构建发展新格局

科技热闻

Hexo个人博客快速部署到Gitee&Coding详细教程

老表

Hexo gitee CODING 博客配置 11月日更

科技热点周刊|ClickHouse 融资 2.5 亿美元、个人信息保护法正式实施、Facebook 改名 Meta

青云技术社区

云计算 facebook 云原生

浅谈 Android Dex 文件_文化 & 方法_有赞技术_InfoQ精选文章