ART是怎么实现参数传递的?

先做假设

Java世界,有两类函数:普通的函数和native函数。Native函数是由C/C++或者汇编语言实现的函数。那么,函数之间的调用就有:普通->普通,native->普通,普通->native

这三种类型的调用,可以发生在任意时间、任意地点,以任意方式来调用。那么,这三种调用方式必然存在很大的共同点。如果我们能够破解一种的话,那么其余两种也就能够很轻易的破解了。

那么,选哪一种为突破口呢?首先考虑一下普通->普通的方式。普通的java函数会被ART编译为机器码,我们只能通过oatdump将函数还原成汇编码。但是函数的汇编码不仅晦涩难懂,而且没有对应源代码,读起来非常费劲。更重要的是,这类代码既没有合适的调试工具来单步执行,也无法通过插入LOG来查看它的执行步骤,所以,显然这种方法不适合。

再看一下普通->native的调用方式。我们知道,native函数必须通过JNI注册(TD附录:native函数注册方法)才能使用。在注册的时候,我们传递的是函数的指针。这意味着,在ART中,普通函数必须通过函数指针才能调用到native函数。这样一来,要追踪这个函数指针,也就变成了一个非常棘手的问题。

那么,就只剩下native->普通的方式了。回想一下JNI调用java函数的方法,都是通过CallMethod这样的函数进行的。这个函数只要查找ART的源代码,就可以很容易的找到。看来,这个方法是个很好的突破口。

 

CallVoidMethod是如何调用一个普通java函数的?

就以CallVoidMethod为例。

考虑一下,平常我们是如何在JNI中使用CallVoidMethod的?

env->CallVoidMethod(method_id, obj_this, param1, param2, ...);

 

我们已经知道,jmethodID,对应的是Art内部的ArtMethod

那么,CallVoidMethod该如何实现呢?首先,CallVoidMethod要调用到任意一个函数,必须有函数指针,那么函数指针肯定在ArtMethod内部;其次,考虑到被调用的函数,它的接受的参数也应该是obj_this, param1, param2...这样的序列。

但是,考虑到被调用函数的参数,可能有任意多个,其参数类型也各不相同,那么在C/C++语言层面,我们很难给出一个统一的函数类型来调用,这样一来,CallVoidMethod只能通过一个汇编编写的函数来做中转了。

那么,在这种情况下,Art要用什么规则传递参数呢?这让我想到了C语言中函数参数的传递规则。虽然,Art不一定会采用和C语言一样的方法来传递规则,但是,它们一定会采用类似的规则。因此,参考一下C语言的参数传递规则就很有必要。

 

由于Android设备几乎全是使用ARM芯片,所以,我们重点考虑ARM32汇编。

 

按照ARM32位的参数传递规则,如果参数个数少于等于4个,则使用r0, r1, r2, r3传递参数;如果参与大于4个,那么超出的参数将放依次放在堆栈上。

 

(这一部分详细些)。

 

再找代码

下面,我们找找代码。

 

CallVoidMethod的实现比较简单,它对应的是ArtMethod::Invoke函数。有兴趣的读者可以自行查找。

 

这个函数很短,我把它摘抄出来:

 

void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,                                                                                                                     
                       char result_type) {
  if (kIsDebugBuild) {
    self->AssertThreadSuspensionIsAllowable();
    CHECK_EQ(kRunnable, self->GetState());
  }

  // Push a transition back into managed code onto the linked list in thread.
  ManagedStack fragment;
  self->PushManagedStackFragment(&fragment);

  Runtime* runtime = Runtime::Current();
  // Call the invoke stub, passing everything as arguments.
  if (UNLIKELY(!runtime->IsStarted())) {
    LOG(INFO) << "Not invoking " << PrettyMethod(this) << " for a runtime that isn't started";
    if (result != NULL) {
      result->SetJ(0);
    }
  } else {
    const bool kLogInvocationStartAndReturn = false;
    if (GetEntryPointFromCompiledCode() != NULL) {
      if (kLogInvocationStartAndReturn) {
        LOG(INFO) << StringPrintf("Invoking '%s' code=%p", PrettyMethod(this).c_str(), GetEntryPointFromCompiledCode());
      }
#ifdef ART_USE_PORTABLE_COMPILER
      (*art_portable_invoke_stub)(this, args, args_size, self, result, result_type);
#else
      (*art_quick_invoke_stub)(this, args, args_size, self, result, result_type);
#endif
      if (UNLIKELY(reinterpret_cast(self->GetException(NULL)) == -1)) {
        // Unusual case where we were running LLVM generated code and an
        // exception was thrown to force the activations to be removed from the
        // stack. Continue execution in the interpreter.
        self->ClearException();
        ShadowFrame* shadow_frame = self->GetAndClearDeoptimizationShadowFrame(result);
        self->SetTopOfStack(NULL, 0);
        self->SetTopOfShadowStack(shadow_frame);
        interpreter::EnterInterpreterFromDeoptimize(self, shadow_frame, result);
      }
      if (kLogInvocationStartAndReturn) {
        LOG(INFO) << StringPrintf("Returned '%s' code=%p", PrettyMethod(this).c_str(), GetEntryPointFromCompiledCode());
      }
    } else {
      LOG(INFO) << "Not invoking '" << PrettyMethod(this)
          << "' code=" << reinterpret_cast(GetEntryPointFromCompiledCode());
      if (result != NULL) {
        result->SetJ(0);
      }
    }
  }

  // Pop transition.
  self->PopManagedStackFragment(fragment);
}

 先不管宏 ART_USE_PORTABLE_COMPILER。这个在ARM上肯定是关闭的。请读者注意代码中间的部分有个函数art_quick_invoke_stub

从字面意思看,很可能就是负责参数转换的。仔细再看看Invoke的其余代码,可以确定这个函数就是负责参数转换,并调用java函数的。

再看下这个函数的原型:

extern "C" void art_quick_invoke_stub(ArtMethod*, uint32_t*, uint32_t, Thread*, JValue*, char);

从该函数的调用看,可以猜测出,他的参数依次是:

  • ArtMethod* method : 表示一个method的类指针,其中包含了java函数的入口地址;
  • uint32_t* args  这是一个参数列表数组;我们传递给CallVoidMethod的参数,依次存储在这个数组内;
  • uint32_t  arg_size 这是参数的大小。这个大小是按字节算的(这是我后来才知道的,给大家先通报);
  • Thread* self, 这是表示线程的对象;
  • JValue* result, 这是返回值的指针;
  • char result_type 返回类型,应该是JNI的返回值签名(TD JNI签名)。

现在,请读者再想一想,如果你是ART的开发者,你该如何实现这个函数呢?

以ARM为例,首先,应该把args参数从数组里面提取处理,放到寄存器r0, r1, r2, r3里面,如果参数过多,需要在放在堆栈上;然后,从method中取出函数指针,并调用;最后,把返回结果存入到result内存中。


显然,完成这些任务,必须是汇编函数,那么,我们就在ART的汇编文件中搜索一下该函数的实现吧。

果然,在 art/runtime/arch/arm/quick_entrypoints_arm.S 文件中有art_quick_invoke_stub函数的实现。先把这个函数贴出来,请您先浏览一下:

    /*
     * Quick invocation stub.
     * On entry:
     *   r0 = method pointer
     *   r1 = argument array or NULL for no argument methods
     *   r2 = size of argument array in bytes
     *   r3 = (managed) thread pointer
     *   [sp] = JValue* result
     *   [sp + 4] = result type char
     */
ENTRY art_quick_invoke_stub
    push   {r0, r4, r5, r9, r11, lr}       @ spill regs
    .save  {r0, r4, r5, r9, r11, lr}
    .pad #24
    .cfi_adjust_cfa_offset 24
    .cfi_rel_offset r0, 0
    .cfi_rel_offset r4, 4
    .cfi_rel_offset r5, 8
    .cfi_rel_offset r9, 12
    .cfi_rel_offset r11, 16
    .cfi_rel_offset lr, 20
    mov    r11, sp                         @ save the stack pointer
    .cfi_def_cfa_register r11
    mov    r9, r3                          @ move managed thread pointer into r9
    mov    r4, #SUSPEND_CHECK_INTERVAL     @ reset r4 to suspend check interval
    add    r5, r2, #16                     @ create space for method pointer in frame
    and    r5, #0xFFFFFFF0                 @ align frame size to 16 bytes
    sub    sp, r5                          @ reserve stack space for argument array
    add    r0, sp, #4                      @ pass stack pointer + method ptr as dest for memcpy
    bl     memcpy                          @ memcpy (dest, src, bytes)
    ldr    r0, [r11]                       @ restore method*
    ldr    r1, [sp, #4]                    @ copy arg value for r1
    ldr    r2, [sp, #8]                    @ copy arg value for r2
    ldr    r3, [sp, #12]                   @ copy arg value for r3
    mov    ip, #0                          @ set ip to 0
    str    ip, [sp]                        @ store NULL for method* at bottom of frame
    ldr    ip, [r0, #METHOD_CODE_OFFSET]   @ get pointer to the code
    blx    ip                              @ call the method
    mov    sp, r11                         @ restore the stack pointer
    ldr    ip, [sp, #24]                   @ load the result pointer
    strd   r0, [ip]                        @ store r0/r1 into result pointer
    pop    {r0, r4, r5, r9, r11, lr}       @ restore spill regs
    .cfi_adjust_cfa_offset -24
    bx     lr
END art_quick_invoke_stub
函数上面的注释已经说明白参数的传递方式了。这个函数中,"ENTRY"和"END"是两个宏,用来定义一个函数的,读者可以先不要关注;".save", ".pad"和".cfi_...."这些是伪指令,是针对汇编器的,不影响实际运行结果,也不需要关注。

下面,我们来重点阅读。

请首先看

    add    r5, r2, #16                     @ create space for method pointer in frame
    and    r5, #0xFFFFFFF0                 @ align frame size to 16 bytes
    sub    sp, r5                          @ reserve stack space for argument array
    add    r0, sp, #4                      @ pass stack pointer + method ptr as dest for memcpy
    bl     memcpy                          @ memcpy (dest, src, bytes)

 他等价于 
  

r5 = (r2 + 16) & 0xFFFFFF0;
sp = r5;
r0 = sp + 4;
memcpy(r0, r1, r2);
显然,它在堆栈分配了参数所需要的大小并16字节对齐,然后把args拷贝到堆栈上。注意一点:栈顶元素空出了,应该有别的用途。


再继续阅读:

    ldr    r0, [r11]                       @ restore method*
    ldr    r1, [sp, #4]                    @ copy arg value for r1
    ldr    r2, [sp, #8]                    @ copy arg value for r2
    ldr    r3, [sp, #12]                   @ copy arg value for r3
    mov    ip, #0                          @ set ip to 0
    str    ip, [sp]                        @ store NULL for method* at bottom of frame
让r0 = ArtMethod*,r1, r2, r3分别是arg0, arg1, arg2。并把栈顶元素设置为 0。

各位读者请注意:显然,r0, r1, r2, r3,应该对应sp, sp+4, sp+8, sp+12才对。但是r0的值为ArtMethod*,但是sp位置的值却为0。因为我们现在研究的是 native->普通函数的调用,那么普通->普通的调用时,是否就让r0和sp位置的值保持一致吗?这是很有可能的。


现在看起来,ART的做法与C的标准不同:

  • ART仍然采用r0, r1, r2, r3传递参数,但是更多的参数不再使用堆栈传递;
  • ART使用堆栈存储参数序列;
  • 函数的第一个参数是ArtMethod*,第二个参数是this对象,然后依次是函数的参数。如果是static函数,那么就从第二个参数开始排列函数的参数。

下面的代码:

    ldr    ip, [r0, #METHOD_CODE_OFFSET]   @ get pointer to the code
    blx    ip                              @ call the method
这两句话,是从r0(即ArtMethod*)取出函数地址,然后调用。函数地址的偏移是METHOD_CODE_OFFSET,这个宏定义在 art/runtime/asm_support.h,值为40。


最后,将返回值(存储在r0和r1中)写入到参数result_type中(sp + 24的位置)。


如何验证?

(TODO)

你可能感兴趣的:(ART揭秘)