ODEX简介
ODEX,全名Optimized DEX
,即优化过的DEX
。
有至少3种方法去创建一个“准备好的”DEX文件,即ODEX:
虚拟机JIT输出会跑到一个特殊的
dalvik-cache
目录。这只在一些特殊的桌面和工程机的设备上使用(这些机器的build中,dalvik-cache目录的权限不是严格的)。在生产机器上这是不被允许的。系统的安装器在程序首次安装时候执行,它有写dalvik-cache的权限。
构建(build)系统预先执行。相关的 jar / apk 文件还在,但classes.dex被剥离出来了。ODEX和原来的zip包保存在一起,不在dalvik-cache,而是系统镜像的一部分。
dalvik-cache
目录更准确地说是$ANDROID_DATA/data/dalvik-cache
。里面的文件的名字来源于源DEX的完整路径。在设备上该目录被system所拥有,而system拥有0771权限,保存在那里的ODEX被系统和应用的组所拥有,权限为0644。数字权限保护的应用会使用640权限来防止其他应用去检测它们。底线是你可以读取自己的与其他大部分应用的DEX文件,但你不能创建、修改,或删除它们。
前两种方法的执行分为以下三个步骤:
首先,dalvik-cache文件被创建。这必须在一个有恰当权限的进程进行,所以在“系统安装器”的场景,是在运行为root的installd
进程执行的。
接着,classes.dex从zip包中解压出来。文件头部留出一小块空间给ODEX header。
最后,文件被内存映射以便访问,并被为当前系统使用进行调整。这包括了字节交换(byte-swapping),结构重新排列(structure realigning),但并没有对DEX文件做有意义的改变。还做了一些其他的基本结构检查,比如确保文件偏移量和数据索引落在有效范围内。
构建系统不在桌面上运行工具,而宁愿去启动模拟器,强制所有相关DEX文件的即时优化,然后从dalvik-cache把结果提取出来。这样做的原因,在解释完优化后会变得更显而易见。
一旦代码被字节替换和对齐,我们就可以继续了。我们添加了一些预计算的数据,在文件头填写ODEX header,然后开始执行。然而,如果我们对验证和优化有兴趣,就需要在初始准备后再插入一个步骤。
dexopt的魔法
在Android 2.3版本以前,系统源码中提供了生成odex的工具dexopt-wrapper
,位于Android 2.2系统源码的 build/tools/dexpreopt/dexopt-wrapper/
目录下,查看DexOptWrapper.cpp文件会发现实际调用的是 /system/bin/dexopt
程序。在5.0及以上版本的设备上,你可能已经再也找不到dexopt了,取而代之的是dex2oat。
我们想要验证和优化DEX文件里的所有类。最简单和安全的方法就是把所有类加载到虚拟机,然后跑一遍。任何加载失败的就是验证/优化失败的。不幸的是,这可能导致一些资源的分配难以释放(比如native共享库的加载),所以我们不想执行在应用运行的虚拟机里。
解决方案就是起一个叫做dexopt的程序(事实上就是虚拟机的后门)。它会执行一个简短的虚拟机初始化,从引导的类路径加载0个或多个DEX文件,然后开始做一切从目标DEX可以做的验证和优化。结束后,进程退出,释放所有资源。
因为多个虚拟机可能同时需求同一个DEX文件,文件锁被用来确保dexopt仅被执行一次。
优化
虚拟机解释器通常会在一段代码被首次使用的时候执行某些优化。常量池引用被指向内部数据结构的指针所替代,总是成功的操作或是那些总会以某种方式工作的,会被更简单的形式所替代。这些的一部分需要仅在运行时可用的信息,另一部分在某些特定假设下可以被静态推论出。
Dalvik优化器做了这些:
- 对于虚方法调用,把方法索引替换为
vtable
索引。 - 对于实例变量(field)的
get/put
,把变量索引替换为字节偏移。另外,把boolean / byte / char / short
基本变量(variants)合并到单个的32位形式(解释器里更少的代码意味着CPU I-cache里更少的空间)。 - 替换一些高频次调用,例如把 String.length() 替换成
内联的
。这可以跳过一些常见的方法调用消耗,直接从解释器切换到native实现。 - 删除空方法。最简单的例子就是
Object.
什么都没干,却必须在任何对象被分配的时候执行。指令会被替换为一个新版本的空指令(no-op)形式,除非调试器被attach上去了。 - 附加预计算数据。例如,虚拟机想要一个类名的哈希表以便查找。不同于在加载DEX文件时候去计算这个,我们可以先计算,以节省堆(heap)空间和所有加载该DEX文件的虚拟机的计算时间。
生成ODEX文件
使用dexopt-wrapper
可以将dex转换为odex。dexopt-wrapper
在安卓2.3以前的源码中可以找到。将dex-wrapper编译后放到手机中。
adb push dexopt-wrapper /data/local
adb shell chmod 777 /data/local/dexopt-wrapper
从apk文件中提取一个dex文件,将其改名为classex.dex
,zip将其压缩后改名为HelloDex.zip
adb push HelloDex.zip /data/local
adb shell
cd /data/local
./dexopt-wrapper HelloDex.zip HelloDex.odex
将ODEX文件转换为DEX文件
经过优化的ODEX
文件中包含与设备相关的依赖库列表Dependences
结构信息,不同的Android设备的底层bootClassPath
环境变量中存放的系统加载库列表也不尽相同。因此,将ODEX文件转换成DEX文件的过程是设备相关的。
为了将ODEX文件还原成DEX文件,需要先将ODEX文件反编译成smali文件,再将smali文件编译成DEX文件。我们将这个过程称为deodex
。反编译与编译smali文件使用的工具都是smali
。在使用baksmali命令反编译ODEX文件时,需要加入参数-d
,以指定与ODEX相关的设备的framework
目录。
因为依赖库都来源于Android设备的/system/framework
目录,所以,
第一步操作是将设备上的framework
目录pull到本地。
adb pull /system/framework ./
(注意,执行以上命令需要拥有设备的root权限)
接下来,执行如下命令完成反编译
baksmali -a 19 -x crack.odex -d ./framework -o ./outdex
其中,-a
参数时Android设备的版本号,19
表示当前设备是4.4版本的。-x
参数用于指定要操作的ODEX文件,-d
参数用于指定framework目录,-o
参数指定输出smali文件的目录。完成该命令后,会生成一系列smali文件,接下来,只要执行smali
命令将smali文件生成DEX文件即可。
smali ./outdex -o outdex.dex
(当然,网上也有odex2dex的自动脚本)