尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android运行时文件详解:DEX、ODEX、OAT、VDEX、ART演进与排查实践

Android运行时文件详解:DEX、ODEX、OAT、VDEX、ART演进与排查实践 我干Android开发和逆向也有好些年了大大小小的坑踩了不少。每次帮人看启动崩溃或者插件化问题十有八九都会撞上这几个文件dex、odex、oat、vdex、art。很多人都知道它们在Android里很重要但真问到它们是什么、互相之间什么关系、为什么不同系统版本上长得不一样能说清楚的人其实不多。这篇文章我就把这些文件按系统演进的顺序讲透顺带把我平时排查问题用到的思路、工具和踩过的坑一并整理出来。无论你是做应用开发、系统定制还是刚开始接触逆向分析这篇都适合你。看完你至少能搞明白一件事你的App安装到手机上之后那些代码到底经历了什么。1. 为什么搞懂这几个文件比背参数更重要1.1 先从一次改包名就崩溃的案例讲起前阵子有个朋友找我说自己把一个App的包名从com.example.old改成com.example.new重新签名后装到机器上一启动就崩Logcat里反复出现和oat、DexFile相关的报错。他第一反应是混淆配置出了问题但检查了半天也没发现异常。我让他去/data/app对应目录下翻了一下发现残留了一份旧包名生成的base.oat和base.vdex。问题其实就出在这里系统在安装新包时发现缓存路径匹配不上或者校验收到的Dex和已有的编译产物不一致加载阶段直接抛异常。这类问题如果你只盯着Java层代码永远找不到原因必须理解底层那套编译缓存机制才能定位。这其实就是我写这篇文章的动机。很多Android开发日常只接触classes.dex打包的时候关心一下MultiDex但对系统在安装、启动过程中偷偷做的那些“加工”完全没概念。可一旦遇到启动优化、崩溃排查、逆向脱壳、插件化改造这些文件就是绕不开的核心。所以这篇不只是带你认文件而是把Android运行时从Dalvik到ART的演进逻辑一起捋一遍理解了逻辑遇到任何奇怪现象你都能自己分析。1.2 先说结论五个文件各管一段路在展开细节之前我先给一个总览。这五个文件并不是并列关系而是Android系统在不同阶段的产物文件类型全称或含义出现时期核心作用DEXDalvik ExecutableAndroid 1.6起App编译后的字节码JVM的class在Android上的等价物ODEXOptimized DEXAndroid 2.2起Dalvik时代对DEX做预优化后的产物OATAhead-Of-Time compiledAndroid 5.0起ART运行时把DEX编译成机器码后的文件VDEXVerified DEXAndroid 8.0起Dex2Oat时生成的已验证DEX副本或校验数据ARTART ImageAndroid 5.0起预加载的类对象堆快照也叫镜像文件稍微记一下这张表后面的内容全是在给这张表“填肉”。你会发现这些文件的变迁史就是Android运行时的进化史。2. DEX所有Android运行的起点2.1 为什么是DEX而不是JAR或者Class大家都知道Android应用是Java/Kotlin写的但为什么编译完不直接跑jar非得转成classes.dex早期Java程序跑在JVM上有多少个类就生成多少.class文件每个文件都有自己的常量池、字段表、方法表有很多重复数据。一个稍大的应用几百个类加载起来光是解析这些元数据就够喝一壶了。而Android设备当年的内存和CPU都非常紧张必须把能省的都省掉。于是DEX就登场了。它把整个工程的多个class文件合并成一个大文件所有类共享同一个常量池。字符串、方法名、类名这些重复信息在DEX里只存在一份空间省了至少三分之一。同时DEX按固定偏移布局运行时可以直接内存映射读取不用像class文件那样逐步解析。所以你看网上很多老教程说“Android的DEX是经过优化的class集合”这个描述虽不严谨但方向是对的。再插一句热搜里那“老版三星dex”其实经常有人搜到我这篇。三星DeX是那个桌面模式扩展坞的名字和这里的DEX文件格式完全是两码事搜索引擎经常把两者混在一起别搞混就行。2.2 DEX文件结构速览与魔数识别搞逆向的人都知道拿到一个未知文件第一步就是看魔数。DEX的前8个字节很有规律dex、\n、0、3、5、\0也就是常见的dex\n035\0。在16进制编辑器里看到类似下面这样基本可以断定是DEX格式00000000 64 65 78 0a 30 33 35 00 64 b9 1b 18 ... # dex\n035\0整个DEX文件分为三大部分文件头Header记录了文件大小、校验和、字符串表偏移、类定义表偏移等关键索引。索引区包括字符串表、类型表、原型表、字段表、方法表相当于一本“通讯录”。数据区class_defs、code_item方法实际字节码、data段等是真正干活的地方。对普通开发者来说不必把每个偏移都背下来但至少要懂一个原理DEX里一切都是靠偏移量互相关联的。解析DEX的过程就是不断的“指针寻址”这也是为什么DEX无法直接做大范围修改改了方法体导致偏移变化可能就得重建整个文件。2.3 修改DEX的两种姿势与避坑心得实际工作中修改DEX的场景很多最常见的就是Hook逻辑、去广告、修bug。操作方式分两种重打包用baksmali把DEX反汇编成smali改完再用smali组回一个新DEX。好处是逻辑清晰坏处是DEX签名会变需要重新签名。直接改字节码用十六进制工具直接Patch DEX的某段字节码比如把一个方法调用改成NOP。好处是尽量保留原结构坏处是偏移计算必须非常小心稍微改错一个偏移就能让你在崩溃日志里泡一整天。过来人的三条经验动手前先做校验和备份DEX文件头里存了SHA-1校验值改完如果没同步更新系统在加载时会直接拒绝。优先用baksmali做“语义级”修改不要上来就十六进制硬改除非你只是替换一个常量。遇到方法数超过65535的老项目要搞清楚主DEX和次DEX的划分改错文件会导致NoClassDefFoundError满天飞。3. ODEXDalvik时代“优化过的DEX”3.1 为什么需要“优化过的DEX”在Android 2.2之前Dalvik虚拟机每次开机都要解析所有框架的DEX解析完才能跑。这个开销有多大呢早期设备开机慢、App启动慢很大一部分时间就耗在这上面了。后来系统引入了DexOpt工具在首次安装或者开机阶段预先对DEX做一遍优化生成ODEX即Optimized DEX。DexOpt干活的本质是把DEX里那些运行时才需要做的事提前完成。比如把类名、方法名、字段名的查找结果直接缓存为偏移把字节码指令做一次线性化处理减少解释器的工作量。这样App跑起来之后虚拟机拿到的不再是原始的DEX而是已经“半处理”过的版本。这也是“odex”这个文件名的来源它不是新格式更像是DEX的预编译缓存。3.2 为什么ODEX不能随便挪动老玩家可能还记得当年刷机圈流行把/system/app里的APK和同目录的.odex一起从手机A拷贝到手机B结果经常失败开机直接卡在白LOGO。原因很简单ODEX优化过程会跟当前的固件、系统库地址、CPU指令集强绑定。它里头记录了一堆依赖系统库方法的内部偏移换一台设备、换一个ROM版本这些偏移全对不上了轻则类加载失败重则系统起不来。所以ODEX的跨设备兼容性几乎为零这也是它后来被淘汰的伏笔之一。3.3 对我们现在的启发很多人觉得ODEX已经进博物馆了实际在现代Android上“odex”这个后缀仍然存在。你去看/system/app里的应用目录经常能看到xxx.odex文件但它的本质已经变成了ART时代的OAT文件只是继续沿用了odex的命名习惯。所以如果你在真机上看到odex文件先别下结论得看看文件头到底是deyDEX优化还是oat开头。还有一点Dalvik和ART早期版本都允许在build.prop里加dalvik.vm.dexopt-flags来控制DexOpt的处理级别比如vn,ov这些老参数。现在虽然不常见了但你在逆向老ROM或者研究兼容性的时候这些东西还是能派上用场的。4. OATART的AOT编译产物别再说它是DEX的改名版4.1 Mixed Mode机制解释执行、JIT、AOT的关系Android 5.0之后ART正式取代Dalvik这是Android运行时的一次大换代。ART最核心的变化是引入了AOTAhead-Of-Time编译应用在安装时就直接把DEX字节码编译成本地机器码运行时直接执行本地码不再需要解释执行。听着很美好但代价也明显安装时间暴涨存储占用翻倍。到了Android 7.0引入了我们熟悉的JITJust-In-Time机制系统不再对DEX做全量AOT而是先让应用在运行时通过解释器JIT执行并收集热点方法再根据Profile引导只把确实热的方法编译成本地码。这就是大家常说的混合编译模式。当前ART实际运行模式是这样组合的解释执行启动初期方法没有被编译先走解释器保证App能快速启动。JIT编译解释器发现某个方法执行的次数够多就会触发JIT把该方法编译成本地码并缓存下次调用直接走本地码。AOT编译安装或系统空闲时跑dex2oat把整个DEX或者Profile标记的热点方法提前编译成OAT文件。Profile引导AOT在后台空闲时系统根据前几次运行收集的Profile只编译“值得编译”的方法兼顾性能和空间。这也是为什么同一个App在不同手机上安装速度、启动速度差异很大取决于设备用的编译策略和Profile成熟程度。4.2 OAT文件的ELF结构数据段里藏着的DEX很多新手第一次用readelf打开OAT文件会被它的结构吓到因为它根本不是一个自创格式而是一个标准的ELF共享库。这一点特别关键意味着你可以用所有传统的ELF工具去分析它。OAT文件的基本布局如下ELF头标准ELF格式可以从这里读入口点、段表。oatdata段真正的精华里面嵌着原始DEX、OAT头、类偏移表、方法偏移表。oatexec段则是那些被编译好的本地机器码。刚才说的加载流程可以用一个伪代码表示boot.oat - ELF header 找到 oatdata - oatdata 解析OAT头拿到DEX数量、偏移 - DEX0, DEX1... 顺着偏移取出每个DEX - DEX的类列表 再找对应方法在oatexec中的native code偏移理解这个结构对逆向特别有用。你拿到一个App的OAT文件如果只想提取里面的DEX完全可以直接跳过DEX解析那一大套直接定位到OAT头后面的DEX段把DEX字段剪切出来就行。这也是很多脱壳工具最初的原理。4.3 通过oatdump查看应用编译情况实操时我在Root设备上最常用的手法是# 找到App对应的oat/vdex adb shell su -c ls /data/app/你的包名/oat/arm64/ # 用oatdump导出oat的详细信息 adb shell su -c oatdump --oat-file/data/app/你的包名/oat/arm64/base.oat --output/sdcard/base_oat.txt adb pull /sdcard/base_oat.txt拉下来的文本里能看到每个被编译方法的地址、大小、所属类还能直接看到DEX的类定义表。oatdump这个工具是AOSP自带的网上也能找到针对各版本的预编译版本。注意不同Android版本选项略有变化先跑oatdump --help看一眼最保险。4.4 编译过滤器与“Speed”那些参数你在cmd package compile相关文档里经常看到verify、speed、speed-profile这些参数它们就是编译过滤器。我建议按性能和空间做成表格过滤器含义使用场景verify仅做DEX校验不编译测试环境space以最小空间为目标编译存储吃紧的老设备balanced平衡空间与性能默认可选值之一speed全量编译成本地码追求极致流畅的重度使用场景speed-profile按Profile编译热点方法Android 7.0默认趋势everything编译时做最大强度优化不推荐用于生产经常有人问我“为什么我的App安装比别人的慢”答案很可能就是它在安装时被speed全量编译了。在生产环境中speed-profile是性价比最高的选择。你可以写一个简单命令做对比# 把某个App改成speed-profile编译 adb shell cmd package compile -m speed-profile -f 你的包名 # 改成全量speed adb shell cmd package compile -m speed -f 你的包名两种模式下冷启动耗时、写入存储占用差异都非常直观建议自己实测一把数据记录下来以后优化的时候心里有底。5. VDEX与OAT形影不离的“校验缓存”5.1 VDEX里到底存了什么Android 8.0之后你在应用数据目录里会看到OAT文件旁边多出一个.vdex它就是VDEX全称Verified DEX。名字已经很直白存的是“已经做过头验证的DEX”。为什么需要它原因很简单ART运行时要执行DEX之前必须做一次类和方法结构的校验避免非法字节码搞崩运行时。以前这个校验在每次启动时都要做比较耗时。现在dex2oat编译时直接把校验信息和DEX副本一起塞进vdex运行时加载时直接读结论省掉了重复计算的过程。VDEX里存放的内容大致有两类DEX副本未参与编译的原始DEX直接放在这里运行时需要时可以快速加载。验证标记每个类、方法经过校验后的状态数据例如这个类是verified还是rejected。5.2 从VDEX提取DEX没OAT也能做逆向这个技巧在逆向圈特别常用。很多加固产品会把原始DEX藏得很深但系统要跑应用就必须让ART拿到DEX所以vdex里经常能找到完整的DEX内容。提取方法不复杂。网上有一个开源工具vdexExtractor可以自动解析vdex头、定位DEX偏移并把DEX以独立文件导出。你也可以自己写一个小脚本按vdex格式手动裁切。核心流程是读出vdex的头部信息找到DEX段数量和起始位置。从DEX段起始位置读取16字节的dex\n035\0魔数确认边界。把DEX段切出来就得到一份可以直接扔进Jadx的class文件了。我在实际操作中还注意过一个小坑部分最新Android系统生成的vdex经过Quickening处理字节码加入了字段方法的快捷偏移直接拖进Jadx可能部分方法显示异常。遇到这种情况需要先对Quickening指令做反处理返回给普通DEX指令。网上叫“dequickening”的脚本不少但每个厂商定制ROM可能存在差异这点要有心理准备。5.3 dex2oat的参数与编译模式对应关系做系统级调试的时候要不要生成vdex、vdex里放不放DEX副本是由dex2oat参数决定的。常见参数有--compiler-filterspeed-profile --generate-debug-info --strip --copy-dex-filestrue--copy-dex-files这个参数直接决定vdex里会不会保留DEX副本。如果为falsevdex里只有校验信息APK里又有原始DEX那么运行时直接从APK里读如果APK里没有DEX比如System App则必须让vdex保留副本否则应用根本跑不起来。这就是为什么你解开一个system应用目录APK里看不见classes.dexDEX全在vdex里的原因。6. ART文件Boot.art不是“艺术文件”是堆快照6.1 预置类对象如何被“冻结”在镜像里每次开机启动Android系统都要加载大量基础类比如java.lang.Object、java.lang.String、android.app.Activity等等。如果每次都现造对象、现解析方法启动时间会非常难看。于是ART想了个办法把这些类和对象的初始化结果直接打包成一个镜像文件以后启动时只要把这块内存快速地映射进来就能拿到一堆已经初始化好的对象。这个文件就是.art通常叫“ART Image”。它本质上是一块堆内存的快照里面存放着预加载类对象、字符串、ArtMethod指针等数据。换句话说它不是给我们开发者直接改的源码或字节码而是ART运行时为了加速启动做的“极速缓存”。6.2 Boot.art和boot.oat的配套关系设备上最常见的是/system/framework/下面的boot.art和boot.oat。它们必须成对出现而且必须严格匹配。BOOT.OAT里存的是框架层的机器码。BOOT.ART里存的是框架层预初始化对象的堆快照。你可以脑补一下组装过程系统先映射boot.art把那些类对象放到内存里然后再映射boot.oat把代码段加载进来ART内部会把类和代码之间的指针关联好。如果你强行替换其中一个而不同时替换另一个最常见的表现就是开机直接进入recovery或者Zygote进程疯狂崩溃。6.3 App也有自己的ART镜像吗这个问题经常有人问。答案是在部分系统版本里每个App在自己的数据目录下也会生成一个base.art它存放的是App首次冷启动后一部分热点类的镜像。但这个机制后来没有得到大规模推广因为收益有限还对存储和兼容性造成压力。现在你在绝大多数真机上App目录下更多看到的是.vdex和.oat.art反而不常见。一旦你理解了ART镜像的原理再去看系统调优参数dalvik.vm.image-dex2oat-filter就能秒懂。它控制的是生成镜像时的编译级别如果设成verify生成镜像的速度很快但启动加速效果相对弱一些如果设成speed镜像生成时间长但启动能感觉到明显变快。7. 实操在真机上快速定位、导出、分析五大文件7.1 不同Android版本的文件路径变化这些文件的存放路径在不同版本里差别挺大我自己的经验是直接记一个“版本对照表”Android版本框架文件位置App编译产物位置Android 4.4及之前/system/framework/*.jar *.odex/data/dalvik-cache/*.odexAndroid 5.x~7.x/system/framework/ /boot.oat、boot.art/data/app/包名/oat/ /base.oatAndroid 8.x~9.x/system/framework/ /boot.oat、boot.art、boot.vdex/data/app/包名/oat/ /base.odex、base.vdexAndroid 10及以上/apex/com.android.art/javalib/ /boot.oat、boot.art/data/app/~~随机字符串/包名/oat/ /base.odex、base.vdex注意Android 10之后ART运行时被打包成APEX模块路径变了很多老脚本写死了旧路径就会找不到文件。排查时先看系统版本再用find命令全局搜索比凭记忆硬找要高效得多。7.2 利用现有API和工具做一次完整分析我给一个从正常设备上分析App编译状态的完整流程不用Root也能完成大部分操作# 1. 查看App当前编译过滤器状态 adb shell dumpsys package dexopt | grep 你的包名 # 2. 如果发现状态不对改成speed-profile并验证 adb shell cmd package compile -m speed-profile -f 你的包名 adb shell dumpsys package dexopt | grep 你的包名 # 3. 有Root的话直接浏览产物文件 adb shell su -c ls -l /data/app/你的包名/oat/arm64/紧接着如果想进一步体验一手现场可以拿到Root权限的设备把整个目录拉出来adb shell su -c cp -r /data/app/你的包名/ /sdcard/app_artifacts/ adb pull /sdcard/app_artifacts/拉下来之后用二进制工具分别打开base.odex和base.vdex看一眼文件头。base.odex的魔数应是oat\nbase.vdex的魔数应是vdex\n。你会发现这里的base.odex果然是个“披着odex名字的oat文件”这一下你就把之前的知识点串起来了。7.3 大量数据与实际场景这些文件如何影响App体验这些文件不只是给逆向工程师看的它们直接影响普通用户的真实体验。举个例子在低配置机器上如果App安装后立即被全量编译成OAT安装耗时可能从几秒变成几十秒用户很容易以为是卡死。反过来如果系统只在后台Profile引导下编译安装秒完但前几次启动会因为JIT尚未收集热点而显得有些慢。很多主打性能的旗舰机厂商会在出厂时预编译常用App本质就是把AOT的时间成本转移到出厂前。我在做启动优化时做过一个测试同一台设备上verify-only状态和speed-profile状态下的冷启动耗时平均差了约20%。但前者额外占用存储几乎为零后者多了几十MB。所以你看根本没有完美的方案全是取舍。理解了OAT、VDEX的工作机制你就不会再天真地以为“把App优化到最快”只靠Java代码层就能解决底层的编译策略同样重要。8. 常见问题与排查技巧实录8.1 问题速查表下面这几类问题我在这几年的实际工作中基本都碰过整理成速查表方便你对照定位症状高概率原因排查动作启动报Couldnt find DexFileDEX路径或vdEx损坏清除App数据并重装修改dex重打包后安装成功但闪退校验和/签名未更新更新dex的checksum和签名替换boot.art后开机异常ART镜像与OAT不匹配双清并重刷对应的boot.art和boot.oat应用安装极慢编译过滤器被设为speed改用speed-profile或balanced逆向时vdex提取的dex方法体显示异常Qucikening字节码未处理使用支持dequickening的脚本插件加载时报oat file has invalid vdex插件运行时与宿主OAT版本不匹配保证Plugin加载路径不指向宿主的odex升级系统后应用目录出现大量残留旧版本编译产物未清理清除应用数据或重装应用8.2 排查思路先看版本再查路径最后抓现场这里有一个通用的方法论我每次遇到与这些文件相关的诡异问题都会用它。第一步是确定Android大版本。不同版本对应不同的运行时策略odex在不同版本完全指代不同的东西。你拿Android 9的路径去套Android 13失败概率极高。第二步是快速定位文件并判断文件头。用ls、find找到文件后不要只靠后缀名猜测用xxd -l 16 file看一眼前16个字节魔数直接告诉你它到底是什么。第三步才是抓当时崩溃现场的日志。很多OTA、dex2oat的报错信息都写在logcat的dex2oat标签或installd标签下按标签过滤比大海捞针强得多。8.3 修改这些文件前必须养成的三个习惯每次搞这些底层文件的时候我都要求自己先做三件事其实这是踩了多次坑换来的全量备份。无论是改系统boot.art还是改某个App的odex先把原文件备份到外部存储别只放手机本地万一你手动清数据把备份也清了就真没救了。尽量用系统命令代替手动改文件。比如要调整编译级别优先用cmd package compile而不是手动替换odex文件。手动替换会把SELinux上下文和文件所有权搞乱一家厂商的定制ROM还可能有额外的校验。小步验证。改一个包验证一个包别一次改一堆。底层文件的问题经常会导致设备完全无法启动到时候你连logcat都抓不了只能进recovery慢慢排查。这些习惯看起来简单但我见过太多人因为跳过了其中一步把设备搞成砖或者浪费掉整个下午的。安全永远是第一位的。最后再分享一个小技巧。如果你在做逆向分析时想要快速查看某个App的DEX与其费劲去脱壳不如直接看系统给的现成产物。很多情况下/data/app里的base.vdex就包含了完整的未加密DEX用VdexExtractor提取出来后Jadx直接打开阅读体验和看普通源码没太大区别。我自己就靠这招解决过好几个“找不到核心代码”的困惑。Android这些文件格式一路演进下来本质都是在“启动速度、安装时间、占用空间”三者之间做平衡。理解了这个大局观再回头看单个文件你就不会觉得它神秘了。
返回列表