天天看点

Java方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式

前言

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方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式

每个 Java 类经过 javac 的编译都会生成对应的 class 文件,这些 class 文件便可以在 Jvm 虚拟机上运行;但是在 Android 中同一个 apk 的 class 文件会被 dx 工具打包为一个 dex 文件(某些情况下可能是多个),dex 文件经过 dex2oat 会生成对应的 oat 文件,art 虚拟机运行的就是这些 oat 文件

1.3 与 dalvik 虚拟机的区别

Java方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式

这张图应该是开发者文档中的一张图,很好的表现出了 art 虚拟机和 dalvik 虚拟机的区别,可以看到它们最主要的区别是对 Dex File 所作的处理不同:

  • dalvik 虚拟机会通过 dexopt 处理 Dex File 生成 Odex 文件
  • art 虚拟机会通过 dex2oat 处理 Dex File 生成 Oat 文件,图中的 ELF 是 Linux 上可执行文件的一种格式,Oat 文件也是一种 ELF 文件;dex2oat 会将 dex 字节码编译为机器可以直接运行的汇编指令,除此之外,Oat 文件当中还会包含原来的 Dex 文件

1.4 启动时机

Java方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式

(本篇文章暂时不对 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 时会对其进行赋值)

Java方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式

四、 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 模式的运行流程如下所示:

Java方法在art虚拟机中的执行前言一、art 虚拟机二、ArtMethod三、quick code 模式四、 Interpreter 模式