追踪Android方法调用1
字数 3634 2025-08-29 22:41:24

Android方法调用追踪技术详解

1. 前言

本文详细讲解如何在AOSP源码中进行插桩,以追踪Java函数和Native函数的调用关系。基于Android 10.0源码,结合《深入理解Android Java虚拟机ART》的分析,重点剖析Native函数注册流程和Native函数调用Java函数的流程。

2. Java层调用Native函数流程

2.1 Native函数注册流程

类加载与函数注册

  • 通过ClassLinker::LoadClassClassLinker::LinkCode在解释模式下注册函数
  • 包含静态方法注册和Native方法注册
  • Native方法通过GetQuickGenericJniStub()注册
  • 实际调用的是平台相关的汇编代码:art_quick_generic_jni_trampoline

ARM64架构下的执行流程

  1. artQuickGenericJniTrampoline计算Native函数所需栈空间
  2. 准备Native函数参数
  3. 将native函数地址保存到x0寄存器
  4. 通过blr xIP0执行

JNI方法执行前后处理

  • 设置cookie=JniMethodStart并存储到栈空间
  • 每个Native函数执行前都会执行JniMethodStart函数
  • 执行结束后会执行JniMethodEnd函数

Native函数查找与注册

  1. 通过void const* nativeCode = called->GetEntryPointFromJni()获取nativeCode地址
  2. 如果地址为art_jni_dlsym_lookup_stub,说明是第一次调用,尚未注册
  3. 通过artFindNativeMethod进行注册
  4. FindCodeForNativeMethod函数使用dlsym对每个加载的so进行查找
  5. 最终通过method->RegisterNative(native_code)完成注册

2.2 Native函数注册总结

已编译Native方法的情况

  • dex2oat编译Java native方法生成机器码
  • ArtMethod对象的机器码入口地址指向生成的机器码
  • 生成的机器码会跳转到ArtMethod对象的JNI机器码入口地址
  • 如果JNI方法未注册,JNI机器码入口地址是art_jni_dlsym_lookup_stub
  • 已注册则JNI机器码入口地址指向Native层对应函数

未编译Native方法的情况

  • ArtMethod对象的机器码入口地址为跳转代码art_quick_generic_jni_trampoline
  • 未注册时JNI机器码入口地址为art_jni_dlsym_lookup_stub
  • 已注册则指向Native层对应函数

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

2.3 调用Native函数

调用场景

  1. native调用native
  2. Java调用native

Java调用native流程

假设:

  • 发起调用的Java函数为A
  • 被调用的native函数为B
  • A未被oat编译,走解释模式

调用链:

  1. ArtMethod::Invoke
  2. art::interpreter::EnterInterpreterFromInvoke
  3. Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter)
  4. ExecuteMterpImplExecuteSwitchImpl<false, false>
  5. MterpInvokexxxxExecuteSwitchImplCpp的switch模式
  6. Invoke-xxxx
  7. DoInvoke
  8. Docall
  9. DoCallCommon

关键函数分析

DoInvoke函数

  • shadow_frame保存A的ArtMethod对象和B的参数
  • instinst_data代表B的smali指令
  • use_fast_path = true时通过ExecuteXXXImpl执行B方法
  • use_fast_path = false时调用DoCallDoCallCommon

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层函数流程

3.1 JNI调用Java方法示例

// 调用static int方法
jint result = env->CallStaticIntMethod(clazz, mid, args...);

// 调用int方法
jint result = env->CallIntMethod(obj, mid, args...);

3.2 调用流程分析

  1. Call开头的函数最终调用InvokeWithArgArray
  2. CallIntMethod为例:
    • 调用InvokeVirtualOrInterfaceWithVarArgs
    • 获取Java对象:ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj)
    • 获取ArtMethod指针:jni::DecodeArtMethod(mid)
    • 获取函数短签名shorty和参数arag_array
    • 调用InvokeWithArgArray(soa, method, &arg_array, &result, shorty)

3.3 InvokeWithArgArray执行路径

  • 解释模式:通过art::interpreter::EnterInterpreterFromInvoke执行
  • quick模式:通过art_quick_invoke_stubart_quick_invoke_static_stub执行

3.4 EnterInterpreterFromInvoke细节

  • 调用Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter)
  • 解释模式下stay_in_interpreter = true

3.5 Execute函数关键逻辑

  • 需要访问权限检测AccessChecks()时:
    • 即使指定解释器为kMterpImplKind
    • 也通过ExecuteSwitchImpl执行dex指令
  • 不需要权限检测且transaction_active = false时:
    • 使用ExecuteMterpImpl执行dex指令

3.6 执行模式说明

  • transaction_active与dex2oat编译逻辑相关,完整虚拟机运行时返回false
  • LIKELY(method->SkipAccessChecks())表明大概率跳过访问权限检测
  • Android10默认解释模式走ExecuteMterpImpl

3.7 执行模式切换场景

  1. 未编译类中的Java函数A调用Native函数:
    • 从解释模式切换到quick模式
  2. Java函数A调用已被JIT编译的Java函数B:
    • 从解释模式切换到quick模式

4. 关键知识点总结

  1. Native函数注册

    • 首次调用时通过dlsym查找并注册
    • 后续调用直接跳转到已注册地址
    • 注册过程涉及art_jni_dlsym_lookup_stubartQuickGenericJniTrampoline
  2. Java调用Native

    • 通过解释器或编译代码桥接
    • 涉及DoInvokePerformCall等关键函数
    • 执行路径取决于方法是否被编译
  3. Native调用Java

    • 通过JNI接口函数发起
    • 最终进入解释器或编译代码执行
    • 执行模式可动态切换
  4. 执行模式

    • 解释模式:ExecuteMterpImplExecuteSwitchImpl
    • Quick模式:直接执行编译后的机器码
    • 模式间可动态切换

5. 参考

  1. 《深入理解Android Java虚拟机ART》
Android方法调用追踪技术详解 1. 前言 本文详细讲解如何在AOSP源码中进行插桩,以追踪Java函数和Native函数的调用关系。基于Android 10.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 ARM64架构下的执行流程 artQuickGenericJniTrampoline 计算Native函数所需栈空间 准备Native函数参数 将native函数地址保存到x0寄存器 通过 blr xIP0 执行 JNI方法执行前后处理 设置 cookie=JniMethodStart 并存储到栈空间 每个Native函数执行前都会执行 JniMethodStart 函数 执行结束后会执行 JniMethodEnd 函数 Native函数查找与注册 通过 void const* nativeCode = called->GetEntryPointFromJni() 获取nativeCode地址 如果地址为 art_jni_dlsym_lookup_stub ,说明是第一次调用,尚未注册 通过 artFindNativeMethod 进行注册 FindCodeForNativeMethod 函数使用 dlsym 对每个加载的so进行查找 最终通过 method->RegisterNative(native_code) 完成注册 2.2 Native函数注册总结 已编译Native方法的情况 dex2oat编译Java native方法生成机器码 ArtMethod对象的机器码入口地址指向生成的机器码 生成的机器码会跳转到ArtMethod对象的JNI机器码入口地址 如果JNI方法未注册,JNI机器码入口地址是 art_jni_dlsym_lookup_stub 已注册则JNI机器码入口地址指向Native层对应函数 未编译Native方法的情况 ArtMethod对象的机器码入口地址为跳转代码 art_quick_generic_jni_trampoline 未注册时JNI机器码入口地址为 art_jni_dlsym_lookup_stub 已注册则指向Native层对应函数 art_quick_generic_jni_trampoline 是native函数执行前必须经历的机器码,功能类似于dex2oat过程中为native函数准备参数。 2.3 调用Native函数 调用场景 native调用native Java调用native Java调用native流程 假设: 发起调用的Java函数为A 被调用的native函数为B A未被oat编译,走解释模式 调用链: ArtMethod::Invoke art::interpreter::EnterInterpreterFromInvoke Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter) ExecuteMterpImpl 或 ExecuteSwitchImpl<false, false> MterpInvokexxxx 或 ExecuteSwitchImplCpp 的switch模式 Invoke-xxxx DoInvoke Docall DoCallCommon 关键函数分析 DoInvoke函数 shadow_frame 保存A的ArtMethod对象和B的参数 inst 和 inst_data 代表B的smali指令 use_fast_path = true 时通过 ExecuteXXXImpl 执行B方法 use_fast_path = false 时调用 DoCall 和 DoCallCommon 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_stub 和 art_quick_invoke_static_stub 执行 3. Native层调用Java层函数流程 3.1 JNI调用Java方法示例 3.2 调用流程分析 Call 开头的函数最终调用 InvokeWithArgArray 以 CallIntMethod 为例: 调用 InvokeVirtualOrInterfaceWithVarArgs 获取Java对象: ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj) 获取ArtMethod指针: jni::DecodeArtMethod(mid) 获取函数短签名 shorty 和参数 arag_array 调用 InvokeWithArgArray(soa, method, &arg_array, &result, shorty) 3.3 InvokeWithArgArray执行路径 解释模式:通过 art::interpreter::EnterInterpreterFromInvoke 执行 quick模式:通过 art_quick_invoke_stub 和 art_quick_invoke_static_stub 执行 3.4 EnterInterpreterFromInvoke细节 调用 Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter) 解释模式下 stay_in_interpreter = true 3.5 Execute函数关键逻辑 需要访问权限检测 AccessChecks() 时: 即使指定解释器为 kMterpImplKind 也通过 ExecuteSwitchImpl 执行dex指令 不需要权限检测且 transaction_active = false 时: 使用 ExecuteMterpImpl 执行dex指令 3.6 执行模式说明 transaction_active 与dex2oat编译逻辑相关,完整虚拟机运行时返回false LIKELY(method->SkipAccessChecks()) 表明大概率跳过访问权限检测 Android10默认解释模式走 ExecuteMterpImpl 3.7 执行模式切换场景 未编译类中的Java函数A调用Native函数: 从解释模式切换到quick模式 Java函数A调用已被JIT编译的Java函数B: 从解释模式切换到quick模式 4. 关键知识点总结 Native函数注册 : 首次调用时通过 dlsym 查找并注册 后续调用直接跳转到已注册地址 注册过程涉及 art_jni_dlsym_lookup_stub 和 artQuickGenericJniTrampoline Java调用Native : 通过解释器或编译代码桥接 涉及 DoInvoke 和 PerformCall 等关键函数 执行路径取决于方法是否被编译 Native调用Java : 通过JNI接口函数发起 最终进入解释器或编译代码执行 执行模式可动态切换 执行模式 : 解释模式: ExecuteMterpImpl 或 ExecuteSwitchImpl Quick模式:直接执行编译后的机器码 模式间可动态切换 5. 参考 《深入理解Android Java虚拟机ART》