在Java世界,有两类函数:普通的函数和native函数。Native函数是由C/C++或者汇编语言实现的函数。那么,函数之间的调用就有:普通->普通,native->普通,普通->native。
这三种类型的调用,可以发生在任意时间、任意地点,以任意方式来调用。那么,这三种调用方式必然存在很大的共同点。如果我们能够破解一种的话,那么其余两种也就能够很轻易的破解了。
那么,选哪一种为突破口呢?首先考虑一下普通->普通的方式。普通的java函数会被ART编译为机器码,我们只能通过oatdump将函数还原成汇编码。但是函数的汇编码不仅晦涩难懂,而且没有对应源代码,读起来非常费劲。更重要的是,这类代码既没有合适的调试工具来单步执行,也无法通过插入LOG来查看它的执行步骤,所以,显然这种方法不适合。
再看一下普通->native的调用方式。我们知道,native函数必须通过JNI注册(TD附录:native函数注册方法)才能使用。在注册的时候,我们传递的是函数的指针。这意味着,在ART中,普通函数必须通过函数指针才能调用到native函数。这样一来,要追踪这个函数指针,也就变成了一个非常棘手的问题。
那么,就只剩下native->普通的方式了。回想一下JNI调用java函数的方法,都是通过Call
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);
从该函数的调用看,可以猜测出,他的参数依次是:
以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的标准不同:
下面的代码:
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的位置)。