前言
ART 虚拟机执行 Java 方法主要有两种模式:quick code 模式和 Interpreter 模式
- quick code 模式:执行 arm 汇编指令
- Interpreter 模式:由解释器解释执行 Dalvik 字节码
在之前的文章 ART 虚拟机 — Interpreter 模式 中详细介绍了 Interpreter 模式,因此本篇文章将代入一些例子,来帮助大家更好的理解 quick code 模式和 Interpreter 模式
一、art 虚拟机
在介绍这两种模式之前,我们先大体介绍一下 art 虚拟机
1.1 什么是虚拟机?
Virtual Machine:
- Run program like a physical machine
- Implemented by software
Functional classification:
- Run an Operate System (VirtualBox, VMWare)
- Only support for single process execution
根据上面的定义和分类,我们可以确定 art 虚拟机和 Jvm 虚拟机类似,都属于第二种,仅支持单一进程的运行;对于 single process 可以这样理解,在 Android 中,每个 Java 进程都有自己的虚拟机实例,换言之,每个虚拟机实例上面只运行着一个 Java 进程
1.2 与 Jvm 虚拟机的区别
每个 Java 类经过 javac 的编译都会生成对应的 class 文件,这些 class 文件便可以在 Jvm 虚拟机上运行;但是在 Android 中同一个 apk 的 class 文件会被 dx 工具打包为一个 dex 文件(某些情况下可能是多个),dex 文件经过 dex2oat 会生成对应的 oat 文件,art 虚拟机运行的就是这些 oat 文件
1.3 与 dalvik 虚拟机的区别
这张图应该是开发者文档中的一张图,很好的表现出了 art 虚拟机和 dalvik 虚拟机的区别,可以看到它们最主要的区别是对 Dex File 所作的处理不同:
- dalvik 虚拟机会通过 dexopt 处理 Dex File 生成 Odex 文件
- art 虚拟机会通过 dex2oat 处理 Dex File 生成 Oat 文件,图中的 ELF 是 Linux 上可执行文件的一种格式,Oat 文件也是一种 ELF 文件;dex2oat 会将 dex 字节码编译为机器可以直接运行的汇编指令,除此之外,Oat 文件当中还会包含原来的 Dex 文件
1.4 启动时机
(本篇文章暂时不对 Zygote 和 Zygote64 作区分)
我们知道 Zygote 进程是第一个 Java 进程,其是 init 通过加载 init.rc 来启动的,图片里第一个框中的内容是 init.zygote64_32.rc 中的, 表示定义一个名为 zygote 的 service,它的启动入口是 /system/bin/app_process64,后面是传入的一些参数。在 app_process/app_main.cpp 中,系统会去启动 runtime,runtime 启动时会启动虚拟机并且调用 ZygoteInit 类的 static void main(String[] args) 方法,这也是被调用的第一个 Java 方法
二、ArtMethod
(基于 Android 8.1)
想要理解 Java 方法在虚拟机中的执行,肯定绕不开 art::ArtMethod 这个类,在 Android 中,每个 Java 方法(包括 native 方法)都对应一个 art::ArtMethod 对象,一个 art::ArtMethod 对象描述一个 Java 方法,art::ArtMethod 的结构如下所示:
(gdb) ptype 'art::ArtMethod'
type = class art::ArtMethod {
public:
static const bool kCheckDeclaringClassState;
static const uint32_t kRuntimeMethodDexMethodIndex;
protected:
art::GcRoot<art::mirror::Class> declaring_class_;
std::__1::atomic<unsigned int> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
art::ArtMethod::PtrSizedFields ptr_sized_fields_;
}
(gdb) ptype 'art::ArtMethod::PtrSizedFields'
type = struct art::ArtMethod::PtrSizedFields {
art::mirror::MethodDexCacheType *dex_cache_resolved_methods_;
void *data_;
void *entry_point_from_quick_compiled_code_;
}
class art::ArtMethod 各个字段分别的含义:
- declaring_class_:The class we are a part of
- dex_code_item_offset_:Offset to the CodeItem
- dex_method_index_:Index into method_ids of the dex file associated with this method
- method_index_:Entry within a dispatch table for this method. For static/direct methods the index is into the declaringClass.directMethods,for virtual methods the vtable and for interface methods the ifTable
struct art::ArtMethod::PtrSizedFields 各个字段分别的含义:
- entry_point_from_quick_compiled_code_:Method dispatch from quick compiled code invokes this pointer which may cause bridging into the interpreter
也就是说以 quick code 模式执行的方法在调用另外一个方法时,会调用这个方法的 entry_point_from_quick_compiled_code_ 成员,但是这个指针不一定指向这个方法的汇编指令,还有可能是转到 Interpreter 模式的桥接;想具体知道其是怎么赋值的,可以关注我另一篇博客 FindClass 流程分析 中的 LinkCode(…) 方法
三、quick code 模式
在接下来的讲解过程中,将会以 frameworks 中的 addLinks 方法为例来进行讲解:
frameworks/base/core/java/android/text/util/Linkify.java
/**
* Scans the text of the provided Spannable and turns all occurrences
* of the link types indicated in the mask into clickable links.
* If the mask is nonzero, it also removes any existing URLSpans
* attached to the Spannable, to avoid problems if you call it
* repeatedly on the same text.
*
* @param text Spannable whose text is to be marked-up with links
* @param mask Mask to define which kinds of links will be searched.
*
* @return True if at least one link is found and applied.
*/
public static final boolean addLinks(@NonNull Spannable text, @LinkifyMask int mask) {
return addLinks(text, mask, null);
}
可以看到这个方法的实现非常简单,仅仅是调用了另外一个三参数的 addLinks(…) 方法,刚好作为我们的研究对象,这样我们可以把关注点放在最关键的地方,知晓在 quick code 模式和 Interpreter 模式下,方法分别是如何执行和相互调用的。
3.1 dump Oat 文件
我们要想研究
addLinks(@NonNull Spannable text, @LinkifyMask int mask)
方法在 quick code 模式下是如何执行的,首先需要将它从 oat 文件中 dump 出来,来看一下其经过 dex2oat 之后变成了什么样子;其经过 dex2oat 编译后应该位于 boot-framework.oat 中,我们可以通过如下命令来对其进行 dump:
adb shell oatdump --oat-file=/system/framework/arm64/boot-framework.oat
先看一下 dump 出来的
addLinks(@NonNull Spannable text, @LinkifyMask int mask)
方法:
6: boolean android.text.util.Linkify.addLinks(android.text.Spannable, int) (dex_method_idx=20460)
DEX CODE:
0x0000: 1200 | const/4 v0, #+0
0x0001: 7130 ed4f 2100 | invoke-static {v1, v2, v0}, boolean android.text.util.Linkify.addLinks(android.text.Spannable, int, android.content.Context) // [email protected]
0x0004: 0a00 | move-result v0
0x0005: 0f00 | return v0
OatMethodOffsets (offset=0x0007d518)
code_offset: 0x01c79d40
OatQuickMethodHeader (offset=0x01c79d28)
vmap_table: (offset=0x01bb9e42)
Optimized CodeInfo (number_of_dex_registers=3, number_of_stack_maps=3)
StackMapEncoding (native_pc_bit_offset=0, dex_pc_bit_offset=5, dex_register_map_bit_offset=7, inline_info_bit_offset=10, register_mask_bit_offset=10, stack_mask_index_bit_offset=12, total_bit_size=13)
DexRegisterLocationCatalog (number_of_entries=3, size_in_bytes=3)
entry 0: in register (21)
entry 1: in register (1)
entry 2: in register (2)
QuickMethodFrameInfo
frame_size_in_bytes: 48
core_spill_mask: 0x40200000 (r21, r30)
fp_spill_mask: 0x00000000
vr_stack_locations:
locals: v0[sp + #24]
ins: v1[sp + #56] v2[sp + #60]
method*: v3[sp + #0]
outs: v0[sp + #8] v1[sp + #12] v2[sp + #16]
CODE: (code_offset=0x01c79d40 size_offset=0x01c79d3c size=72)...
0x01c79d40: d1400bf0 sub x16, sp, #0x2000 (8192)
0x01c79d44: b940021f ldr wzr, [x16]
StackMap [native_pc=0x1c79d48] [entry_size=0xd bits] (dex_pc=0x0, native_pc_offset=0x8, dex_register_map_offset=0xffffffff, inline_info_offset=0xffffffff, register_mask=0x0, stack_mask=0b)
0x01c79d48: f81d0fe0 str x0, [sp, #-48]!
0x01c79d4c: a9027bf5 stp x21, lr, [sp, #32]
0x01c79d50: 79400270 ldrh w16, [tr] ; state_and_flags
0x01c79d54: 35000150 cbnz w16, #+0x28 (addr 0x1c79d7c)
0x01c79d58: aa0103f5 mov x21, x1
0x01c79d5c: 52800003 mov w3, #0x0
0x01c79d60: b00059a0 adrp x0, #+0xb35000 (addr 0x27ae000)
0x01c79d64: f941f800 ldr x0, [x0, #1008]
0x01c79d68: f940141e ldr lr, [x0, #40]
0x01c79d6c: d63f03c0 blr lr
StackMap [native_pc=0x1c79d70] [entry_size=0xd bits] (dex_pc=0x1, native_pc_offset=0x30, dex_register_map_offset=0x0, inline_info_offset=0xffffffff, register_mask=0x200000, stack_mask=0b)
v1: in register (21) [entry 0]
0x01c79d70: a9427bf5 ldp x21, lr, [sp, #32]
0x01c79d74: 9100c3ff add sp, sp, #0x30 (48)
0x01c79d78: d65f03c0 ret
0x01c79d7c: f9427a7e ldr lr, [tr, #1264] ; pTestSuspend
0x01c79d80: d63f03c0 blr lr
StackMap [native_pc=0x1c79d84] [entry_size=0xd bits] (dex_pc=0x0, native_pc_offset=0x44, dex_register_map_offset=0x2, inline_info_offset=0xffffffff, register_mask=0x2, stack_mask=0b)
v1: in register (1) [entry 1]
v2: in register (2) [entry 2]
0x01c79d84: 17fffff5 b #-0x2c (addr 0x1c79d58)
其中
CODE: (code_offset=0x01c79d40 size_offset=0x01c79d3c size=72)...
下面的便是编译出的汇编指令,当以 quick code 模式运行时,逻辑比较简单,就是依次执行每条汇编指令,用 blr 等指令进行跳转,以上面为例:
0x01c79d48: f81d0fe0 str x0, [sp, #-48]!
0x01c79d4c: a9027bf5 stp x21, lr, [sp, #32]
0x01c79d50: 79400270 ldrh w16, [tr] ; state_and_flags
0x01c79d54: 35000150 cbnz w16, #+0x28 (addr 0x1c79d7c)
0x01c79d58: aa0103f5 mov x21, x1
0x01c79d5c: 52800003 mov w3, #0x0
0x01c79d60: b00059a0 adrp x0, #+0xb35000 (addr 0x27ae000)
0x01c79d64: f941f800 ldr x0, [x0, #1008]
0x01c79d68: f940141e ldr lr, [x0, #40]
0x01c79d6c: d63f03c0 blr lr
StackMap [native_pc=0x1c79d70] [entry_size=0xd bits] (dex_pc=0x1, native_pc_offset=0x30, dex_register_map_offset=0x0, inline_info_offset=0xffffffff, register_mask=0x200000, stack_mask=0b)
这一段是调用
addLinks(android.text.Spannable, int, android.content.Context)
方法的核心部分,需要注意的是 StackMap 中的 dex_pc=0x1 表示这段汇编指令对应 DEX CODE 中的 0x0001
0x01c79d5c: 52800003 mov w3, #0x0
0x01c79d60: b00059a0 adrp x0, #+0xb35000 (addr 0x27ae000)
0x01c79d64: f941f800 ldr x0, [x0, #1008]
0x01c79d68: f940141e ldr lr, [x0, #40]
0x01c79d6c: d63f03c0 blr lr
- mov w3, #0x0:准备参数,w3 对应参数 Context context,因为代码中传递过去的参数为 null,所以这里对应的赋值为0,quick code 模式下调用一个 Java 方法规定 x0 寄存器中应为被调用方法的 ArtMethod 的指针;x1 应为被调用方法对应的实例的指针(非 static 方法)或者第一个参数,依次类推
- adrp x0, #+0xb35000 (addr 0x27ae000)
- ldr lr, [x0, #40]:将被调用方法的 ArtMethod 的 entry_point_from_quick_compiled_code_ 放到 lr 中
可以用 gdb 来证明一下:
(gdb) p &(('art::ArtMethod'*)0)->ptr_sized_fields_.entry_point_from_quick_compiled_code_
$1 = (void **) 0x28
可以看到 64bit 下,entry_point_from_quick_compiled_code_ 的偏移为 0x28,即 40
quick code 模式下,一个方法调用另外一个方法的图示如下所示,可以看到实际上就是通过 blr 指令跳转到另外一个 ArtMethod 的 entry_point_from_quick_compiled_code_ 指针(Method dispatch from quick compiled code invokes this pointer which may cause bridging into the interpreter,其可能指向方法的 quick compiled code,也可能指向 art_quick_to_interpreter_bridge 等,在 LinkCode 时会对其进行赋值)
四、 Interpreter 模式
4.1 DEX CODE
addLinks 方法对应的 dex code 如下所示:
DEX CODE:
0x0000: 1200 | const/4 v0, #+0
0x0001: 7130 ed4f 2100 | invoke-static {v1, v2, v0}, boolean android.text.util.Linkify.addLinks(android.text.Spannable, int, android.content.Context) // [email protected]
0x0004: 0a00 | move-result v0
0x0005: 0f00 | return v0
在之前的文章 ART 虚拟机 — Interpreter 模式 中详细介绍了 Interpreter 模式,想要了解代码细节的,可以参考一下;本文主要以上述 dex code 为例,讲一下具体是如何解释执行的
4.2 解释执行的过程
首先,可以看到第一条指令为
1200
, 其 opcode 为
12
,关于 opcode 需要知道:
- opcode 由两位 16 进制数组成,因此共有 256 种可能
- 在 Mterp 解释器当中维护了一种对应关系:opcode 与实现这个 opcode 的汇编指令的对应关系
- 我们在解释执行的时候,实际上是取出一条指令,通过 opcode 找到对应的汇编实现,然后运行
- 大部分 opcode 中都会包含取出下一条指令、然后跳转执行的操作,形成一个循环
opcode
12
对应的汇编指令为:
/* ------------------------------ */
.balign 128
.L_op_const_4: /* 0x12 */
/* File: arm64/op_const_4.S */
/* const/4 vA, #+B */
sbfx w1, wINST, #12, #4 // w1<- sssssssB
ubfx w0, wINST, #8, #4 // w0<- A
FETCH_ADVANCE_INST 1 // advance xPC, load wINST
GET_INST_OPCODE ip // ip<- opcode from xINST
SET_VREG w1, w0 // fp[A]<- w1
GOTO_OPCODE ip // execute next instruction
因此
1200
对应的实现等同于
const/4 v0, #+0
,那么是如何执行下一条指令的呢?看一下
FETCH_ADVANCE_INST
:
/*
* Fetch the next instruction from the specified offset. Advances rPC
* to point to the next instruction. "_count" is in 16-bit code units.
*
* This must come AFTER anything that can throw an exception, or the
* exception catch may miss. (This also implies that it must come after
* EXPORT_PC().)
*/
261#define FETCH_ADVANCE_INST(_count) \
262 lhu rINST, ((_count)*2)(rPC); \
263 addu rPC, rPC, ((_count) * 2)
也就是说这里会通过改变 rPC 来使其指向下一条指令的地址,每个地址中是一个8位的二进制数,也就是2位16进制数,因此需要将 _count 乘 2(也就是说取
1200
的下一条指令,rPC 需要略过4个16进制数,即需要+2)
lhu rINST, ((_count)*2)(rPC)
即从 rPC 的偏移为 (_count)*2 的内存地址中读取一个半字,然后无符号扩展至32位,保存到 rINST 寄存器中
GET_INST_OPCODE
:
/*
* Put the instruction's opcode field into the specified register.
*/
#define GET_INST_OPCODE(rd) and rd, rINST, 0xFF
将 instruction 中的 opcode 取出,放入寄存器 rd 中
GOTO_OPCODE
:
/*
* Begin executing the opcode in rd.
*/
#define GOTO_OPCODE(rd) \
GET_OPCODE_TARGET(rd); \
JR(rd)
/*
* Transform opcode into branch target address.
*/
#define GET_OPCODE_TARGET(rd) \
sll rd, rd, ${handler_size_bits}; \
addu rd, rIBASE, rd
第二条指令为
7130 ed4f 2100
,其 opcode 为
71
,opcode
71
对应的汇编指令为:
/* ------------------------------ */
.balign 128
.L_op_invoke_static: /* 0x71 */
/* File: arm64/op_invoke_static.S */
/* File: arm64/invoke.S */
/*
* Generic invoke handler wrapper.
*/
/* op vB, {vD, vE, vF, vG, vA}, [email protected] */
/* op {vCCCC..v(CCCC+AA-1)}, [email protected] */
.extern MterpInvokeStatic
EXPORT_PC
mov x0, xSELF
add x1, xFP, #OFF_FP_SHADOWFRAME
mov x2, xPC
mov x3, xINST
bl MterpInvokeStatic
cbz w0, MterpException
FETCH_ADVANCE_INST 3
bl MterpShouldSwitchInterpreters
cbnz w0, MterpFallback
GET_INST_OPCODE ip
GOTO_OPCODE ip
由
FETCH_ADVANCE_INST 3
可以看出,opcode 不同,那么指令(instruction)的长度也不尽相同,一旦 opcode 确定,那么本条指令的长度就是固定的,opcode 对应的汇编代码实现中会包含取下条指令的操作
4.3 Interpreter 模式总结
Interpreter 模式的运行流程如下所示: