1. 程式人生 > >Java方法在art虛擬機器中的執行

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 模式的執行流程如下所示:
在這裡插入圖片描述