Dalvik可执行格式与字节码规范

1 背景

清明假期的时候终于把论文投给会议了,现在心里的一块大石头终于落地了。(´▽`ʃ♡ƪ)

不过,重新做好系统之后,发现自己的博客已经好久没有更新了,同时把博客部署到GitHub和Gitee挺难搞,但还是难不住我的。(●’◡’●)

话不多说,开始这次关于Dalvik可执行格式与字节码规范的奇妙之旅吧!!!

2 Dalvik虚拟机

虽然Android平台使用Java语言来开发应用程序,但Android程序却不是运行在标准的Java虚拟机上的。

在Android的早期版本中,可能是为了解决移动设备上软件运行效率较低的问题,也可能是为了规避与Oracle公司的版权纠纷,Google专门为Android平台设计了用于运行Android程序的虚拟机,它就是Dalvik虚拟机(Dalvik Virtual Machine)。Android4.4发布以后,为了解决UI卡顿、显示延迟等性能问题,Google对Android底层虚拟机进行了非常大的修改,引入了全新的ART(Android Runtime)虚拟机,将由JIT执行的Dalvik切换为由AOT编译的ART。为了实现向下兼容,ART完整地提供了对Dalvik可执行格式及字节码规范的执行。

2.1 Dalvik虚拟机的特点

Dalvik虚拟机是Android平台的核心组件,其特点如下:

  • 体积小,占用内存空间少。
  • 专有的DEX(Dalvik Executable)可执行文件格式,体积小,执行速度快。
  • 常量池采用32位索引值,对类方法名、字段名、常量的寻址速度快。
  • 基于寄存器架构,同时拥有一套完整的指令系统。
  • 提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理及垃圾回收等重要功能。
  • 所有Android程序都运行在Android系统进程中,每个进程都与一个Dalvik虚拟机实例对应

2.2 Dalvik虚拟机与Java虚拟机的区别

Dalvik虚拟机与传统的Java虚拟机有诸多显著区别,具体如下(二者亦不兼容)。

2.2.1 运行的字节码不同

Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。传统的Java程序经过编译,生成Java字节码并保存在class文件中,Java虚拟机通过解码class文件的内容来运行程序。而Dalvik虚拟机运行的是Dalvik字节码,所有Dalvik字节码由Java字节码转换而来,并被打包到一个DEX可执行文件中,Dalvik虚拟机通过解释DEX文件来执行这些字节码。

2.2.2 Dalvik可执行文件的体积更小

在Android SDK中,将Java字节码转换为Dalvik字节码的工作是由一个名为dx的工具负责完成的。dx能够重新排列Java类文件,消除在类文件中出现的所有冗余信息,避免虚拟机在初始化时反复加载和解析文件。

Java类文件中通常有多个方法签名,如果其他类文件引用了该类文件中的方法,相应的方法签名也会被复制到其他类文件中。也就是说,如果多个不同的类同时包含相同的方法签名,大量的字符串常量会被多个类文件重复使用。这些冗余信息造成的文件体积增大严重影响了虚拟机解析文件的效率。针对这个问题,dx对所有Java类文件中的常量池进行了分解,消除了其中的冗余信息,然后将它们重新组合形成一个常量池,并让所有类文件共享这个常量池。

使用dx将Java文件转换为DEX文件的过程,如图1所示。基于dx对常量池的压缩,相同的字符串和常量在DEX文件中只会出现一次(文件的体积会随之减小)。

图1 使用dx将Java文件转换为DEX文件

2.2.3 虚拟机架构不同

Java虚拟机是基于栈架构的。当程序运行时,Java虚拟机会频繁地对栈进行读写进行读写数据的操作。在这个过程中,不仅会多次进行指令分派与内存访问,而且会耗费大量的CPU时间。因此,对一些资源有限的设备来说,这是一笔相当大的开销。

Dalvik虚拟机是基于寄存器架构的。数据的访问直接在寄存器之间传递,因此该访问方式比基于栈的访问方式快很多。

2.3 虚拟机的执行方式

2.3.1 即时编译(JIT)

即时编译(Just-in-time Compilation, JIT)又称动态编译,是一种通过在运行时将字节码翻译为机器码使得程序的执行速度加快的技术。JIT技术是在Android2.2的Dalvik虚拟机中引入的。Android官方宣称,采用JIT技术的Dalvik虚拟机的执行速度比以往快了3~6倍。

主流的JIT包括两种字节码编译方式,具体如下:

  • method方式:以函数或方法为单位进行编译。
  • trace方式:以trace为单位进行编译。

method方式很容易理解。那什么是trace方式呢?在函数中,只有少数代码是顺序执行的,多数代码都有好几条执行路径,而其中一些路径在实际运行过程中是很少执行的,这部分路径成为冷路径(执行比较频繁的路径称为热路径)。传统的method方式会编译整个方法的代码,从而在冷路径上耗费很多的编译时间及内存。使用trace方式进行编译,则能快速获取热路径的代码,从而用更短的时间和更少的内存来编译代码。

目前,Dalvik虚拟机默认采用trace方式编译代码,同时支持method方式。

2.3.2 预编译(AOT)

和Dalvik虚拟机一样,Android官方喜欢从第三方的技术突破中寻求技术优化方案。ART来自Google收购的Flexycore公司,其目的是为Android系统提供一个性能更高的虚拟机技术方案。

ART使用AOT(Ahead-of-Time)编译技术,在APK第一次安装或系统升级、重启时,通过调用dex2oat命令将APK中的DEX文件静态编译成OAT文件并存放到Android设备的/data/dalvik-cache或/data/app/package目录下。dex2oat与dexopt不同,dex2oat更像一个编译器,将DEX中的Dalvik字节码编译成Native机器码。经过这样的操作,以后启动程序时,ART就会提高APK的启动速度,从而执行生成的OAT文件而不是APK中的DEX文件。从Android 5.0开始,系统默认将ART作为虚拟机,使用Android设备的用户应该能够明显感觉到程序运行速度的变化(这就是基于AOT的ART虚拟机的强大之处)。AOT有一个缺点,就是它的静态编译操作会影响APK的安装效率,导致在Android 4.4之后版本的设备上安装APK所花费的时间比低版本长。为了进一步提高APK的运行与安装效率,在Android 7.0中增加了JIT(Just-in-Time)编译。我们可以明显感觉到,在Android 7.0及以上版本的系统中安装APK的速度比在Android 6.0和Android5.0中快。新版本的Android使用的是基于JIT on AOT的编译技术。通过Android 2.3为Dalvik引入JITAndroid 4.4引入ARTAndroid 7.0为ART引入JIT的过程可以看出,Android从未放弃让程序运行得更好、更快的目标,性能优化是一项不断更新、完善且具有极高挑战性的工作。

【注】ART是安装时解释dex,Dalvik是运行时解释dex。

3 Android系统架构,分区加载机制与Dalvik虚拟机执行流程

3.1 Android系统架构

Android系统由Linux内核函数库Android运行时应用程序框架应用程序组成,如图2所示。Android系统架构采用分层的思想,其优点包括各层之间的依赖性降低、便于独立分发、容易收敛问题和错误等。Dalvik虚拟机属于Android运行时环境,它与一些核心库一起承担了Android应用程序的运行工作。

图2 Android系统架构图

与图2类似,Android系统另一种架构图如图3所示。

图3 Android系统架构图

3.2 Android分区加载机制

  1. 系统分区:只读分区,包含操作系统内核、系统函数库、实时运行框架、应用框架与系统应用程序等。
  2. Cache分区/目录分区
    • /system/app:系统自带应用程序apk
    • /system/lib:系统库文件
    • /system/bin & /system/xbin:系统管理命令
    • /system/framework:Android系统应用框架的JAR文件
  3. 数据分区:各类用户数据与应用程序
    • /data/data:所有apk程序数据。每个APK在改目录有一个与Package名字一样的目录,APK只能在此目录下操作
    • /data/app:保存用户安装的APK
    • /data/system:packages.xml、packages.list、appwidgets.xml等文件,记录安装软件及widget信息
    • /data/misc:保存WIFI账号及VPN设置等
  4. SD卡分区

3.3 Dalvik虚拟机执行流程

Android系统启动加载内核后,首先执行init进程(进程ID=1):

  • 设备初始化
  • 读取init.rc配置文件并启动重要外部程序Zygote(所有进程的孵化器进程)
    1. 初始化Dalvik虚拟机
    2. 启动system_server并进入Zygote模式,通过socket等候命令
    3. 执行Android程序时,system_server通过Binder IPC方式发送命令给Zygote
    4. Zygote收到命令后通过fork自身创建一个Dalvik虚拟机实例来执行应用程序的入口函数
    5. 进程fork完成后,执行的工作交给Dalvik虚拟机

Android程序的启动过程如图4所示。

图4 Android程序的启动过程

Android程序的启动流程图如图5所示。

图5 Android程序的启动流程

Dalvik虚拟机执行程序的流程也可以如图6所示。

图6 Dalvik虚拟机执行程序的流程

4 DEX反汇编工具

目前主流的DEX文件反汇编工具有Android官方的dexdump第三方的baksmali,两者的反汇编效果都不错,都支持所有Android版本的DEX文件,只是在语法上有些差异。

经过dexdump和baksmali的反汇编代码结构大致相同,方法名、字段类型和代码指令序列一致。不过,对于操作的寄存器,dexdump使用的都是以v开头的寄存器baksmali则同时使用以v和p开头的寄存器。在这里,我建议使用baksmali对DEX文件进行反编译,因为在Dalvik汇编代码较长、使用寄存器较多的情况下,p命名法比v命名法优势更加明显。

假设一个函数使用了M个寄存器,且该函数有N个参数,下表介绍了v命名法和p命名法的表现形式。

v命名法 p命名法 寄存器含义
v0 v0 第1个局部变量寄存器
v1 v1 第2个局部变量寄存器
…… …… 中间的局部变量寄存器递增且名称相同
vM-N p0 第1个参数寄存器)
…… …… 中间的参数寄存器递增
vM-1 pN-1 第N个参数寄存器

5 总结

本博客主要介绍了Android的运行环境Dalvik虚拟机和ART虚拟机,并对比介绍了Dalvik虚拟机与Java虚拟机的差异,然后简单介绍了Android系统架构和常用的DEX反汇编工具。

6 参考文献

[1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019.
[2]https://ctf-wiki.org/android/basic_operating_mechanism/java_layer/smali/smali/


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。

×

喜欢就点赞,疼爱就打赏