Dalvik优化和验证使用dexopt

本文代码基于Android 9.0

源码中的Dalvik文档

Android源码中的Dalvik文档路径如下,本文是dexopt.html的中文翻译。

/dalvik/docs/*.html

Dalvik虚拟机专为Android移动平台而设计。目标系统具有很少的RAM,在慢速内部闪存上存储数据,并且通常具有十年历史的桌面系统的性能特征。它们还运行Linux,它提供虚拟内存,进程和线程以及基于UID的安全机制。

特征和限制使我们专注于某些目标:

  • 必须在多个进程之间共享类数据(尤其是字节码),以最大限度地减少系统总内存使用量。
  • 必须最小化启动新应用程序的开销以使设备保持响应。
  • 将类数据存储在单个文件中会导致大量冗余,特别是在字符串方面。为了节省磁盘空间,我们需要考虑到这一点。
  • 解析类数据字段会在类加载期间增加不必要的开销。直接以C类型访问数据值(例如整数和字符串)更好。
  • 字节码验证是必要的,但速度很慢,因此我们希望在应用程序执行之外尽可能地进行验证。
  • 字节码优化(加快指令,方法修剪)对于速度和电池寿命非常重要。
  • 出于安全原因,进程可能无法编辑共享代码。
  • 典型的VM实现从压缩存档中解压缩各个类并将它们存储在堆上。这意味着每个进程中每个类的单独副本,并减慢应用程序启动速度,因为代码必须是未压缩的(或者至少从磁盘中读取许多小块)。另一方面,在本地堆上使用字节码可以在第一次使用时轻松重写指令,从而促进了许多不同的优化。

这些目标促使我们做出一些基本决定:

  • 多个类聚合到一个“DEX”文件中。
  • DEX文件以只读方式映射并在进程之间共享。
  • 调整字节排序和字对齐以适应本地系统。
  • 所有类都必须使用字节码验证,但我们希望尽可能“预先验证”。
  • 需要重写字节码的优化必须提前完成。
  • 以下各节将介绍这些决策的后果。

VM操作

应用程序代码在传递到系统.jar 或.apk文件。这些只是.zip 存档,添加了一些元数据文件。始终调用Dalvik DEX数据文件classes.dex。

字节码不能被内存映射并直接从zip文件执行,因为数据被压缩并且文件的开头不能保证字对齐。这些问题可以通过存储classes.dex而无需压缩和填充zip文件来解决,但这会增加通过数据网络发送的包的大小。

我们需要classes.dex先从zip存档中提取,然后才能使用它。虽然我们有可用的文件,但我们也可以执行前面描述的其他一些操作(重新调整,优化,验证)。然而,这提出了一个新问题:谁负责这样做,我们在哪里保持输出?

制备

创建“准备好的”DEX文件至少有三种不同的方法,有时也称为“ODEX”(对于Optimized DEX):

1.虚拟机“及时”完成它。输出进入一个特殊 dalvik-cache目录。这适用于桌面和仅限工程的设备构建,其中dalvik-cache目录的权限 不受限制。在生产设备上,这是不允许的。
2.首次添加应用程序时,系统安装程序会执行此操作。它具有写入所需的权限dalvik-cache。
3.构建系统提前完成。相关jar / apk文件存在,但classes.dex 被删除。优化的DEX存储在原始zip存档旁边,而不是存储dalvik-cache在系统映像中,并且是系统映像的一部分。

该dalvik-cache目录是更准确 $ANDROID_DATA/data/dalvik-cache。其中的文件具有从源DEX的完整路径派生的名称。在设备上,目录由system/ 拥有,system 并且具有0771权限,存储在其中的优化DEX文件由system应用程序组拥有,具有0644权限。受DRM锁定的应用程序将使用640权限来阻止其他用户应用程序检查它们。最重要的是,您可以阅读自己的DEX文件和大多数其他应用程序,但您无法创建,修改或删除它们。

准备“即时”和“系统安装程序”方法的DEX文件分三步进行:

首先,创建dalvik-cache文件。这必须在具有适当权限的进程中完成,因此对于“系统安装程序”情况,这在内部完成installd,以root身份运行。

其次,该classes.dex条目是从zip存档中提取的。ODEX标头的文件开头留有少量空间。

第三,该文件是内存映射的,以便于访问和调整以在当前系统上使用。这包括字节交换和结构重新排名,但对DEX文件没有有意义的更改。我们还进行一些基本的结构检查,例如确保文件偏移和数据索引在有效范围内。

构建系统使用一个毛茸茸的过程,包括启动模拟器,强制及时优化所有相关的DEX文件,然后从中提取结果dalvik-cache。在解释优化时,执行此操作的原因(而不是使用在桌面上运行的工具)将变得更加明显。

一旦代码被字节交换并对齐,我们就准备好了。我们附加一些预先计算的数据,在文件的开头填写ODEX头,然后开始执行。(标题填写在最后,因此我们不会尝试使用部分文件。)但是,如果我们对验证和优化感兴趣,我们需要在初始准备之后插入一个步骤。

dexopt

我们想要验证和优化DEX文件中的所有类。最简单,最安全的方法是将所有类加载到VM中并运行它们。任何无法加载的内容都无法验证或优化。不幸的是,这可能导致分配一些难以发布的资源(例如加载本机共享库),因此我们不希望在我们运行应用程序的同一个虚拟机中执行此操作。

解决方案是调用一个名为的程序dexopt,它实际上只是VM的后门。它执行缩写的VM初始化,从引导类路径加载零个或多个DEX文件,然后设置验证和优化目标DEX中的任何内容。完成后,该过程退出,释放所有资源。

多个VM可能同时需要相同的DEX文件。文件锁定用于确保dexopt仅运行一次。

验证

字节码验证过程涉及扫描DEX文件中每个类中每个方法的指令。目标是识别非法指令序列,以便我们不必在运行时检查它们。所涉及的许多计算对于“精确”垃圾收集也是必需的。有关详细信息,请参阅 Dalvik Bytecode Verifier Notes。

出于性能原因,优化程序(在下一节中描述)假定验证程序已成功运行,并进行一些可能不安全的假设。默认情况下,Dalvik坚持要验证所有类,并且只优化已验证的类。如果要禁用验证程序,可以使用命令行标志来执行此操作。有关 在Android应用程序框架中控制这些功能的说明,另请参阅 控制嵌入式VM。

报告验证失败是一个棘手的问题。例如,在不同包中的类上调用包范围方法是非法的,并且将由验证程序捕获。我们不一定要在验证期间报告它 - 我们实际上想要在尝试方法调用时抛出异常。检查每个方法调用上的访问标志是很昂贵的。在 Dalvik的字节码校验说明文件解决了这个问题。

已成功验证的类在ODEX中设置了标志。加载时不会重新验证它们。Linux访问权限有望防止篡改; 如果你可以解决这些问题,安装错误的字节码远非最简单的攻击线。ODEX文件具有32位校验和,但主要用于快速检查损坏的数据。

优化

虚拟机解释器通常在第一次使用一段代码时执行某些优化。常量池引用被替换为指向内部数据结构的指针,总是成功或始终以某种方式工作的操作将被更简单的表单替换。其中一些需要信息仅在运行时可用,其他信息可以在做出某些假设时静态推断。

Dalvik优化器执行以下操作:

  • 对于虚方法调用,请使用vtable索引替换方法索引。
  • 例如,字段get / put,用字节偏移替换字段索引。此外,将boolean / byte / char / short变量合并为单个32位格式(解释器中的代码越少意味着CPU I-cache中的空间越大)。
  • 将一些高容量调用(如String.length())替换为“内联”替换。这会跳过通常的方法调用开销,直接从解释器切换到本机实现。
  • 修剪空方法。最简单的例子是 Object.,什么也不做,但必须在分配任何对象时调用。除非连接了调试器,否则该指令将替换为充当no-op的新版本。
  • 附加预先计算的数据。例如,VM希望有一个哈希表来查找类名。我们可以在加载DEX文件时计算它,而不是在加载DEX文件时计算它,从而在加载DEX的每个VM中节省堆空间和计算时间。

所有指令修改都涉及用未经Dalvik规范定义的操作码替换操作码。这使我们可以自由地混合优化和未优化的指令。优化指令集及其精确表示与VM版本紧密相关。

大多数优化都是明显的“胜利”。原始索引和偏移的使用不仅允许我们更快地执行,我们还可以跳过最初的符号解析。预计算会占用磁盘空间,因此必须适度进行。

这些优化有几个潜在的问题来源。首先,如果更新VM,则vtable索引和字节偏移可能会发生变化。其次,如果超类在不同的DEX中,并且更新了其他DEX,我们还需要确保我们的优化索引和偏移量也会更新。当使用用户定义的类加载器时,会出现类似但更微妙的问题:我们实际调用的类可能不是我们期望调用的类。

这些问题通过依赖关系列表和可以优化的内容的一些限制来解决。

依赖性和局限性

优化的DEX文件包括其他DEX文件的依赖关系列表,以及来自原始classes.dexzip文件条目的CRC-32和修改日期 。依赖关系列表包括dalvik-cache文件的完整路径和文件的SHA-1签名。设备上文件的时间戳不可靠且未使用。依赖区域还包括VM版本号。

优化的DEX依赖于引导类路径中的所有DEX文件。作为引导类路径一部分的DEX文件取决于之前出现的DEX文件。要确保从属DEX文件之外没有任何内容可用,dexopt只加载引导类。对其他DEX文件中的类的引用失败,这会导致类加载和/或验证失败,并且具有外部依赖性的类根本不会被优化。

这意味着将代码拆分为许多单独的DEX文件有一个缺点:无法优化非引导DEX文件之间的虚方法调用和实例字段查找。由于验证是通过类粒度传递/失败的,因此可以优化类中不依赖于外部DEX文件中的类的方法。这可能有点笨拙,但这是保证在更新单个部件时没有任何损坏的唯一方法。

另一个负面后果:对引导程序DEX的任何更改都将导致拒绝所有优化的DEX文件。这使得很难保持较小的系统更新。

尽管我们谨慎,但仍有可能由用户定义的类加载器加载的DEX文件中的类可能需要引导类(例如,String)并且被赋予具有相同名称的不同类。如果正在处理的DEX文件中的类与引导程序DEX文件中的类具有相同的名称,则该类将被标记为不明确,并且在验证/优化期间将不会解析对它的引用。VM中的类链接代码会执行额外的检查以插入另一个漏洞; 有关详细信息,请参阅VM源中的详细说明(vm / oo / Class.c)。

如果其中一个依赖项已更新,我们需要重新验证并重新优化DEX文件。如果我们可以进行即时dexopt 调用,这很容易。如果我们必须依赖安装程序守护程序,或者DEX仅在ODEX中运送,则VM必须拒绝DEX。

dexopt对于主机, 输出是字节交换和结构对齐的,并且包含高度特定于VM的索引和偏移(版本方面和平台方式)。因此,编写一个dexopt在桌面上运行的版本但生成适合特定设备的输出是很棘手的。调用它的最安全方法是在目标设备上,或在该设备的模拟器上。

生成DEX

一些语言和框架依赖于生成字节码并执行它的能力。相当繁重的dexopt验证和优化模型并不适用。

我们打算在将来的版本中支持这一点,但确切的方法是确定的。我们可以允许添加单个类或整个DEX文件; 可以在指令中允许Java字节码或Dalvik字节码; 可以执行通常的优化集,或者使用单独的解释器直接对字节码执行首次使用优化(由于它是本地定义的,因此不会被映射为只读)。

Copyright © 2008 The Android Open Source Project