追踪Android方法调用1
youncyb11 发表于 湖北 移动安全 902浏览 · 2025-05-28 12:10

1. 前言

本文是为了学习如何在 AOSP 源码中插桩,以便 Trace Java 函数和 Native 函数的调用关系。以 Android10.0 为源码,根据《深入理解 Android Java 虚拟机 ART》的指导,分析了 Native 函数注册流程和 Native 函数调用 Java 函数的流程。

2. Java 层调用 Native 函数流程

2.1 Native 函数注册流程

通过类的加载流程,我们可以清晰的在 ClassLinker::LoadClass 中知晓 ClassLinker::LinkCode 在解释模式下如何注册函数,

其中包括了静态方法的注册和 Native 方法的注册,如下所示,Native 方法通过 GetQuickGenericJniStub() 进行注册,真实调用的方法则是平台相关的汇编代码:art_quick_generic_jni_trampoline

进入 art_quick_generic_jni_trampoline,这里我们以 arm64 为例。如下所示,通过 artQuickGenericJniTrampoline 计算 Native 函数所需要的栈空间,准备 Native 函数的参数,然后将 native 函数的地址保存到 x0 寄存器,再通过 blr xIP0 执行。

继续跟进 artQuickGenericJniTrampoline 函数,如下所示,其会设置 cookie=JniMethodStart ,然后将其存储到栈空间上,所以每个 Native 函数执行前都会执行 JniMethodStart 函数。相应的结束后,也会执行一个名为 JniMethodEnd 函数。

然后通过 void const* nativeCode = called->GetEntryPointFromJni(); 获取 nativeCode 地址,如果其地址为 art_jni_dlsym_lookup_stub ,说明此时 Native 函数是第一次调用,还没有被注册过。然后会通过 artFindNativeMethod 进行注册。

继续跟进 artFindNativeMethod

其通过 FindCodeForNativeMethod 函数,使用 dlsym 对每个加载的 so 进行查找,

最终通过 method->RegisterNative(native_code) 进行注册。

所以,当一个 native 函数被调用过一次后,则不会再发生上述查找过程,即不会进入 art_jni_dlsym_lookup_stub ,而是直接进入 ArtMethod 对象的机器码然后再跳转到 JNI 机器码的入口点。

2.2 Native 函数注册总结

根据《深入理解 Android Java 虚拟机 ART》可知:


dex2oat 编译这个 Java native 方法后将会生成一段 机器码。ArtMethod 对象的机器码入口地址会指向这段 生成的机器码。这段机器码本身会跳转到这个 ArtMethod 对象的 JNI 机器码入口地址。如果这个 JNI 方 法没有注册过(即这个 native 方法还未和 Native 层对应 的函数相关联), 这个 JNI 机器码入口地址是 art_jni_dlsym_lookup_stub。否则, JNI 机器码入口地址 指向 Native 层对应的函数。

如果 dex2oat 没有编译过这个 Java native 方法, 则 ArtMethod 对象的机器码入口地址为跳转代码 art_quick_generic_jni_trampoline。同样, 如果这个 JNI 方 法没有注册过, 则 JNI 机器码入口地址为跳转代码 art_jni_dlsym_lookup_stub。否则, JNI 机器码入口地址 指向 Native 层对应的函数。


art_quick_generic_jni_trampoline 和 dex2oat 过程中为 native 函数准备参数类似,是一段 native 函数执行前必须经历的机器码。

image-20250514000853540-1747152541075-1.png


注:图来自《深入理解 Android Java 虚拟机 ART》

2.3 调用 Native 函数

调用 Native 函数,有两种情况:

native 调用 native

Java 调用 native

Java 调用 native,我们称发起调用的 Java 函数为 A,被调用的 native 函数为 B。首先 A 会进入 ArtMethod::Invoke,如以下代码所示。

(这里我们假设 A 没有被 oat 编译,走的仍然是解释模式)A 会进入 art::interpreter::EnterInterpreterFromInvoke,然后经历:

Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter)

ExecuteMterpImpl(self, accessor.Insns(), &shadow_frame, &result_register) 或者 ExecuteSwitchImpl<false, false>(self, accessor, shadow_frame, result_register, false)

MterpInvokexxxx 或者 ExecuteSwitchImplCpp 的switch模式:Invoke-xxxx

DoInvoke

Docall

DoCallCommon

继续跟进 DoInvoke,其代码如下,其中参数的 shadow_frame 保存了 A 的 ArtMethod 对象 和 B 的参数,instinst_data 代表 B 的 smali 指令。

use_fast_path = true,则继续通过 ExecuteXXXImpl 执行 B 方法,否则调用 DoCallDocall 调用 DoCallCommon。所以,当我们对 Docall 以后的函数进行 hook 时,需要确保 use_fast_path = false

DoCallCommon 如下所示,called_method 代表 B 方法的 ArtMethod 对象,shadow_frame 属于 A 方法,arg 代表 B 方法的参数。该函数通过拷贝的方式创建 B 方法的 shadow_frame,然后通过 PerformCall 进行调用。

PerformCall 如下所示,当方法 B 通过解释模式执行,则调用 ArtInterpreterToInterpreterBridge;当方法 B 是 Native 函数或者被编译过,则通过 ArtInterpreterToCompiledCodeBridge 执行。

ArtInterpreterToInterpreterBridge 比较简单,继续回到 Execute

ArtInterpreterToCompiledCodeBridge,则会回到 ArtMethod::Invoke,通过 art_quick_invoke_stubart_quick_invoke_static_stub 执行。

3. Native 层调用 Java 层函数流程

以下是两个 JNI 调用 Java 层方法的例子,其中一个是调用返回值为 static int,另一个是调用返回值为 int。这些 Call 开头的函数最终都会调用 InvokeWithArgArray 函数。

以 CallIntMethod 为例,其会调用 InvokeVirtualOrInterfaceWithVarArgs 函数。

如下所示,该函数首先通过 ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj) 获取 Java 对象。

然后通过 jni::DecodeArtMethod(mid) 获取 ArtMethod 指针,从以下代码可知,jmethodID 对象其实就是 ArtMethod 对象。

然后获取了函数的短签名 shorty 和参数 arag_array,最后调用 InvokeWithArgArray(soa, method, &arg_array, &result, shorty);

InvokeWithArgArray 会调用 ArtMethod::Invoke,如下所示,在解释模式下,通过 art::interpreter::EnterInterpreterFromInvoke 执行代码;在 quick 模式下,通过 art_quick_invoke_stubart_quick_invoke_static_stub 执行代码。

继续跟进 EnterInterpreterFromInvoke,该函数会调用 Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter);,需要注意的是,当处于解释模式时,stay_in_interpreter = true

继续进入 Execute 函数,重要代码如下所示,当需要访问权限检测 AccessChecks(),即使指定了解释器为 kMterpImplKind,也是通过 ExecuteSwitchImpl 执行 dex 指令。如果不需要权限检测且 transaction_active = false,则使用 ExecuteMterpImpl 执行 dex 指令。

根据《深入理解 Android Java 虚拟机 ART》,transaction_active 与 dex2oat 编译逻辑有关,在完整的虚拟机运行时返回 false。LIKELY(method->SkipAccessChecks()) 也表明大概率是跳过访问权限检测。结合以下 aosp 源码,默认情况下,Android10 的解释模式走 ExecuteMterpImpl

当然,ART 运行了多种代码执行的模式,例如:假设一个类还没有被编译为 oat 文件,其中一个 Java 函数 A 调用了 Native 函数,则会由解释模式切换到 quick 模式。假设该 Java 函数调用的另一个 Java 函数 B,该函数已经被 JIT 编译了,则也会从解释模式切换到 quick 模式。

4. 参考

1《深入理解 Android Java 虚拟机 ART》

0 条评论
某人
表情
可输入 255