JVMTI(JVM Tool Interface)是 Java 虚拟机所提供的 Native 编程接口。通过这些接口,开发人员不仅可以调试在虚拟机上运行的 Java 程序,还能查看它们运行的状态、控制环境变量,甚至修改代码逻辑,从而帮助开发人员监控和优化程序性能。
Android JVMTI
Android 的 JVMTI 功能是从 Android 8.0(API 26)开始支持的,官方的叫法是 ART Tooling Interface (ART TI) 。提供的重要功能主要有:
- 运行状态监控
- 重定义 Class
- 跟踪对象分配和垃圾回收过程
- 遵循对象的引用树,遍历堆中的所有对象
- 检查 Java 调用堆栈
- 暂停和恢复所有线程
要使用 JVMTI 的能力,需要提供一个 Agent,利用 ART TI 和 Runtime 进行通信。JVMTI 支持在 JVM 启动时和运行时加载这个 Agent。
dalvikvm -Xplugin:libopenjdkjvmti.so -agentpath:/path/to/agent/libagent.so …
VM 启动时加载不适用于 Android 应用,因为 Android 应用的进程都是从已运行的 zygote 进程 fork 出来的。所以只能在运行时加载。系统提供了 am 命令和 Debug 接口两种加载方式。
adb shell 'am attach-agent com.example.android.displayingbitmaps
\'/data/data/com.example.android.displayingbitmaps/code_cache/libfieldnulls.so=Ljava/lang/Class;.name:Ljava/lang/String;\''
Debug 接口是通过attachJvmtiAgent
加载,这个是 Android9.0 才开始提供。
/**
* Attach a library as a jvmti agent to the current runtime, with the given classloader
* determining the library search path.
* Note: agents may only be attached to debuggable apps. Otherwise, this function will
* throw a SecurityException.
*
* @param library the library containing the agent.
* @param options the options passed to the agent.
* @param classLoader the classloader determining the library search path.
*
* @throws IOException if the agent could not be attached.
* @throws a SecurityException if the app is not debuggable.
*/
public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
@Nullable ClassLoader classLoader) throws IOException
Android8 上需要通过反射调用,但是反射的接口没有classLoader
参数,这样会因为 namespace 问题导致 agent.so 无法调用第三方 so。一个简单的办法就是用一个空的 agent.so,加载它的作用只是用来初始化应用中的 JVMTI 环境。然后在通过System.load
加载一个使用了 JVMTI 接口的 so 就可以了。
因为 JVMTI 提供了代码重定义的能力,所以 Android 上对 JVMTI 的功能进行了限制:
- 只能在可调式的 APP 包中使用 (android:debuggable = true)
- 未提供代码重定义相关的 Java 接口
- 对于 APP 进程来说,无法在进程启动时加载 Agent
- 尚未实现全部的 JVMTI 规范中的能力
突破限制
JVMTI 是一个强有力的监控工具,如果只能在 Debug 包上运行,那么实用性将大大降低。而且 Debug 包得到的数据本身就和实际线上环境相差很大,所以要想利用好 JVMTI,第一步就是要突破 Debug 包的限制。
▐ 限制原理
Agent 的加载主要包括两步:
- EnsureJvmtiPlugin: 加载 libopenjdkjvmti.so, 初始化进程的 JVMTI 运行环境
- AgentSpec.Attach: 加载 agent.so, 使用 JVMTI 的能力
static bool EnsureJvmtiPlugin(Runtime* runtime, std::string* error_msg) { // TODO Rename Dbg::IsJdwpAllowed is IsDebuggingAllowed. DCHECK(Dbg::IsJdwpAllowed() || !runtime->IsJavaDebuggable()) << "Being debuggable requires that jdwp (i.e. debugging) is allowed."; // Is the process debuggable? Otherwise, do not attempt to load the plugin unless we are // specifically allowed. if (!Dbg::IsJdwpAllowed()) { *error_msg = "Process is not allowed to load openjdkjvmti plugin. Process must be debuggable"; return false; } constexpr const char* plugin_name = kIsDebugBuild ? "libopenjdkjvmtid.so" : "libopenjdkjvmti.so"; return runtime->EnsurePluginLoaded(plugin_name, error_msg); }
从源码中我们发现了限制 Debug 包的地方主要有两个接口:
- Dbg::IsJdwpAllowed()
- Runtime::IsJavaDebuggable()
这两个接口中返回的变量是在进程从ZygoteHooks_nativePostForkChild
中设置的
static uint32_t EnableDebugFeatures(uint32_t runtime_flags) { Runtime* const runtime = Runtime::Current(); // 设置JDWP Dbg::SetJdwpAllowed((runtime_flags & DEBUG_ENABLE_JDWP) != 0); if ((runtime_flags & DEBUG_ENABLE_JDWP) != 0) { EnableDebugger(); } runtime_flags &= ~DEBUG_ENABLE_JDWP; // 设置Runtime的DEBUG_JAVA_DEBUGGABLE bool needs_non_debuggable_classes = false; if ((runtime_flags & DEBUG_JAVA_DEBUGGABLE) != 0) { runtime->AddCompilerOption("--debuggable"); runtime_flags |= DEBUG_GENERATE_MINI_DEBUG_INFO; runtime->SetJavaDebuggable(true); // Deoptimize the boot image as it may be non-debuggable. runtime->DeoptimizeBootImage(); runtime_flags &= ~DEBUG_JAVA_DEBUGGABLE; needs_non_debuggable_classes = true; } // 还有native debug相关,对JVMTI影响不大 ... return runtime_flags; }
这两个 flag 实际是由android:debuggable决定的,但是线上包设置为 true 开启 debug 是不可行的。
▐ 修改 JDWP
//Source: platform/art/runtime/debugger.cc // JDWP is allowed unless the Zygote forbids it. static bool gJdwpAllowed = true; bool Dbg::IsJdwpAllowed() { return gJdwpAllowed; } void Dbg::SetJdwpAllowed(bool allowed) { gJdwpAllowed = allowed; }
从源码看到,gJdwpAllowed
是一个静态变量。我们可以通过dlsym
的方式获取到SetJdwpAllowed方法符号来修改他的状态。需要注意的是,这个符号在libart.so
中,Android 7.0 对加载系统库进行了限制。不过目前绕过的方式都比较成熟,我们采用了读取/proc/self/maps
查找 libart.so 和解析 ELF 的方式获取到了此方法的符号。
▐ 修改 Runtime
//Source: platform/art/runtime/runtime.h // Whether Java code needs to be debuggable. bool is_java_debuggable_; bool IsJavaDebuggable() const { return is_java_debuggable_; } void Runtime::SetJavaDebuggable(bool value) { is_java_debuggable_ = value; // Do not call DeoptimizeBootImage just yet, the runtime may still be starting up. }
is_java_debuggable_
是Runtime
中的一个成员变量,没办法采用和 JDWP 一样的方式进行修改。但是在 C 层我们通JavaVM
对象可以拿到唯一Runtime
的对象。
class JavaVMExt : public JavaVM { private: Runtime* const runtime_; }
然后通过内存布局找到is_java_debuggable_
地址进行修改。虽然我们针对不同版本进行适配, 但是手机厂商可能会对 Runtime 结构进行修改,这就会导致一旦修改了错误的地址,可能会引发 Crash。
class Runtime { // Specifies target SDK version to allow workarounds for certain API levels. int32_t target_sdk_version_; CompatFramework compat_framework_; bool implicit_null_checks_; // NullPointer checks are implicit. bool implicit_so_checks_; // StackOverflow checks are implicit. bool implicit_suspend_checks_; // Thread suspension checks are implicit. bool no_sig_chain_; bool force_native_bridge_; bool is_native_bridge_loaded_; bool is_native_debuggable_; bool async_exceptions_thrown_; bool non_standard_exits_enabled_; // Whether Java code needs to be debuggable. bool is_java_debuggable_; }
从源码看到,在这个字段之前,有一些字段的值是相对固定的,我们可以通过这些字段作为参考点,来更准确的找到is_java_debuggable_
。这里我们主要选取了target_sdk_version_
作为结构起始的参考点,而后面几个 bool 值变量,则成为我们检验的字段,因为他们的值也是相对固定的。
Android12 之后多了一个compat_framework_
字段,使用偏移量进行计算容易出错,所以我们自己定义了MockRuntime
对象,以target_sdk_version_
地址为起点进行对象转换。这里又涉及到CompatFramework
内部的结构以及内存对齐的问题,这里就不展开了。
对 JDWP 和 Rumtime 修改之后,Agent 就可以成功的在 Release 环境加载了,我们在加载完成后会恢复原有的值,保证不影响 APP 的正常运行。针对线上加载失败的情况,我们把 Runtime 的内存 Dump 后进行了线下分析,发现基本都是在一些双开模拟器中获取不到target_sdk_version_
导致失败。所以我们也增加了多开设备的开关。
▐ 使用受限版本
通过对 ART 中 JVMTI 源码的深入分析,我们发现源码中提供给了一个kArtTiVersion
的版本号。
// A special version that we use to identify special tooling interface versions which mostly matches // the jvmti spec but everything is best effort. This is used to implement the userdebug // 'debug-anything' behavior. // // This is the value 0x70010200. static constexpr jint kArtTiVersion = JVMTI_VERSION_1_2 | 0x40000000;
当 JVMTI 以这个版本号运行时,他只提供了部分功能,而使用JVMTI_VERSION_1_2
版本时则提供了完整功能。从代码可以看到,如果运行在 Debug 环境或者完全解释执行的环境时,所有功能可用。也就是说这个受限版本可以在 Release 包使用。
// Returns whether we are able to use all jvmti features. static bool IsFullJvmtiAvailable() { art::Runtime* runtime = art::Runtime::Current(); return runtime->GetInstrumentation()->IsForcedInterpretOnly() || runtime->IsJavaDebuggable(); }
通过源码分析,我们发现使用受限版本只是无法使用调试和热修复的能力,但是监控和获取运行信息的能力基本不受影响。也从另一个方面说明 JVMTI 进行线上监控的可行性。
// These are capabilities that are disabled if we were loaded without being debuggable. // // This includes the following capabilities: // can_retransform_any_class: // can_retransform_classes: // can_redefine_any_class: // can_redefine_classes: // can_pop_frame: // can_force_early_return: // We need to ensure that inlined code is either not present or can always be deoptimized. This // is not guaranteed for non-debuggable processes since we might have inlined bootclasspath code // on a threads stack.
考虑到后续可能会使用到更多的能力,所以我们最终没有使用受限版本,还是通过 Mock 方式开启了全功能版本。但是要注意的是,即便使用了全功能版本,要使用全部的能力,还有许多事情需要解决。
更多精彩内容,欢迎观看:
JVMTI 在淘宝 Profiler 中的应用(下):https://developer.aliyun.com/article/1396407