Java方法在art虛擬機器中的執行
前言
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 中都會包含取出下一條指令、然後跳轉執行的操作,形成一個迴圈
opcode12
對應的彙編指令為:
/* ------------------------------ */
.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
,opcode71
對應的彙編指令為:
/* ------------------------------ */
.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 模式的執行流程如下所示: