Kitkat系列文章—OAT文件分析—Part2
本文由 ImportNew - Peter Pan 翻译自 google。如需转载本文,请先参见文章末尾处的转载要求。
这是关于最新Android版本Kitkat系列文章中的第二部分,我把它们写在了+inovex GmbH上。在这一部分,我们将进一步查看这种新的文件格式:ART运行时的OAT,并且简要看一下它的垃圾回收机制(可以在这查看第一部分)。
3、进一步挖掘:OAT文件分析
目前为止我们发现系统在设备上执行了一些编译。不仅是应用,而且还有Android框架层的很大一部分被ART转化成了oat文件。在这篇文章中,我们会努力找出oat到底是什么东西以及oat文件是如何生成的。
正如前提到的,所有已安装应用通过dex2oat编译从而得已运行。现在让我们进一步查看一下这些生成的文件。那么,让我们通过adb分析dex2oat后的一个结果,例如这个由SystemUI.apk转化后的结果:
/data/dalvik-cache/system@priv-app@SystemUI.apk@classes.dex
便捷的“file”命令会返回:
system@priv-app@SystemUI.apk@classes.dex: ELF 32-bit LSB shared object, ARM, version 1 (GNU/Linux), dynamically linked, stripped Wow.. that escalated quickly! With ART we go from java -> class -> dex -> oat, which is a shared object! Further analysis with objdump shows the following: system@priv-app@SystemUI.apk@classes.dex: file format elf32-little DYNAMIC SYMBOL TABLE: 00001000 g DO .rodata 0007d000 oatdata 0007e000 g DO .text 000a9f8f oatexec 00127f8b g DO .text 00000004 oatlastword
这里只确定了三个标识:元数据、执行的起始与终止地址。很明显的,新的运行时把应用当作共享对象来进行处理(!)。共享对象被动态加载到虚拟机的上下文(很可能是先前解释过的启动镜像)中。通过查看源可以知道:实际上在运行时调动dlopen()来加载这些库。
现在让我们使用新的oatdump来获取更多关于oat文件格式的知识。我的首次尝试是在启动镜像文件“/data/dalvik-cache/system@framework@boot.art@classes.dex”中使用oatdump。但是结果显示这个文件的整个dump文件有1.6GB大小,这对于我将尝试的这种分析而言是十分不方便,
所以我写了一个小程序,虽然几乎谈不上有什么具体的功能,但是却简单到足以理解这个OAT是如何工作的。源代码如下:
package de.inovex.arttest; import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); int a = 100; a = foo(a); } private int foo(int a) { return a + 4711; } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return true; } }
安装之后,我们能够在主机上查看它的编译版本并在其上执行objdump:
data@app@de.inovex.arttest.apk@classes.dex: file format elf32-little DYNAMIC SYMBOL TABLE: 00001000 g DO .rodata 00001000 oatdata 00002000 g DO .text 00000238 oatexec 00002234 g DO .text 00000004 oatlastword
目前而言没什么惊喜…它显然是一个几乎没有任何功能、有0×238字节的应用程序;-)
所以,让我在文件”oatdump –oat-file= data@app@de.inovex.arttest.apk@classes.dex”上使用oatdump:
MAGIC: oat 007 CHECKSUM: 0x7fcf3941 INSTRUCTION SET: Thumb2 DEX FILE COUNT: 1 EXECUTABLE OFFSET: 0x00001000 IMAGE FILE LOCATION OAT CHECKSUM: 0xd950003d IMAGE FILE LOCATION OAT BEGIN: 0x60a95000 ...
这个header向我们展示了一些信息,包括文件内容、体系结构、一些完整性检测值和一些想必是用来正确地移动该库的地址。有意思的部分在于这个dump输出体中的方法名、dex码和这个方法的ARM拆解码。例如:foo方法的oat-dump输出如下:
1: int de.inovex.arttest.MainActivity.foo(int) (dex_method_idx=5) DEX CODE: 0x0000: add-int/lit16 v0, v2, #+4711 0x0002: return v0 OAT DATA: frame_size_in_bytes: 32 core_spill_mask: 0x00008060 (r5, r6, r15) fp_spill_mask: 0x00000000 vmap_table: 0xf73b00da (offset=0x000010da) v0/r5, v2/r6, v65535/r15 mapping_table: 0xf73b00d8 (offset=0x000010d8) gc_map: 0xf73b00e0 (offset=0x000010e0) CODE: 0xf73b00bd (offset=0x000010bd size=28)... 0xf73b00bc: e92d4060 push {r5, r6, lr} 0xf73b00c0: b085 sub sp, sp, #20 0xf73b00c2: 9000 str r0, [sp, #0] 0xf73b00c4: 9109 str r1, [sp, #36] 0xf73b00c6: 1c16 mov r6, r2 0xf73b00c8: f2412267 movw r2, #4711 0xf73b00cc: eb160502 adds.w r5, r6, r2 0xf73b00d0: 1c28 mov r0, r5 0xf73b00d2: b005 add sp, sp, #20 0xf73b00d4: e8bd8060 pop {r5, r6, pc}
DEXCODE部分体现的信息十分明显:Java源码中的整型a映射到虚拟寄存器v2中,加上常量4711,然后在v0上存储结果并返回。
OAT DATA还没完全被处理,但明显的是“core_spill_mask”描述了被用在那个ARM方法里面传递数据的寄存器,“vmap_table”显示出虚拟寄存器与真实寄存器的映射关系。
CODE区域显示处理器事实上将要执行的东西:起初,r2持有整型a;在新的栈桢创建之后,常量4711回到家整型a上;之后,结果被传回来。然见到这些虽然不是惊喜,但也令人印象深刻!
同时还提示:上述过程中几乎没有任何优化,更像是一个“gcc-00”。显然不需要新的栈桢,整个“计算”通过单独的一条指令完成:adds.w r0, r2, #4711。
最后,让我们来总结一下OAT文件是什么:OAT是类似APK的一种预编译文件,像共享库一样被正在运行的进程加载。OAT包含了APK中所有类的信息,比如方法、方法名、描述信息和偏移列表,可以在二进制中定位这些方法。
4、留意堆处理:ART中的GC
ATR中的垃圾回收机制跟Dalvik极其相似,两者都采用“标记—清除”的方式保持堆清洁。诈一看令人十分惊奇,但事实上却十分容易理解。从Java源码,到类、dex,再到机器码都可以追踪。尽管代码执行的方式已经改变,但数据结构和对象引用却依然保持不变。因此,垃圾回收进程可以用Dalvik相同的方式进行回收。
对源art/runtime/gc的简单了解可以发现ART使用了4种不同类型的GC。它们可以并行,也可以被列举出来释放堆空间:
// The type of collection to be performed. The ordering of the enum matters, it // is used to // determine which GCs are run first. enum GcType { // Placeholder for when no GC has been performed. kGcTypeNone, // Sticky mark bits GC that attempts to only free objects allocated since // the last GC. kGcTypeSticky, // Partial GC that marks the application heap but not the Zygote. kGcTypePartial, // Full GC that marks and frees in both the application and Zygote heap. kGcTypeFull, // Number of different GC types. kGcTypeMax, };
GC通过上面枚举的次序进行循环,直到有足够可用的空间来分配需要的内存:
art/runtime/gc/heap.cc // Loop through our different Gc types and try to Gc until we get enough free memory. for (size_t i = static_cast<size_t>(last_gc) + 1; i < static_cast<size_t>(collector::kGcTypeMax); ++i) {...
如果这个程序失败了,系统会通过增大堆空间等方式再次尝试分配。但这完全是一个标准程序,没有任何不同于Dalvik垃圾回收的地方,至少我没有发现。
原文链接: google 翻译: ImportNew.com - Peter Pan
译文链接: http://www.importnew.com/8825.html
[ 转载请保留原文出处、译者和译文链接。]
相关文章
- Android Kitkat系列文章—OAT文件分析—Part1
- 成为JavaGC专家Part I — 深入浅出Java垃圾回收机制
- Java 7 Fork/Join 框架
- RenderScript的性能演进
- Java开发2.0: 使用 Hibernate Shards 进行分片–Part.1
- JVM函数式编程资源
- 几分钟内学习 Clojure
- Log4J 2 API
- 深入解析Android关机
- 为什么存储密码字符数组比字符串更合适?