首页
社区
课程
招聘
[原创]Frida 脚本运行时机与Java.perform的原理
发表于: 7小时前 349

[原创]Frida 脚本运行时机与Java.perform的原理

7小时前
349

F02 · Frida 第一个脚本运行时机:从顶层 JS 到 Java.perform()

《凡人修仙传之 - Android 逆向开发》· 03 动态插桩功法 · 技能 F02

前置:F01 Frida 整体启动、进程注入与 Zygote 门控

后续:F03 server IPC;F06 Stalker;01 炼气境 · 第 10 课《Android Java Hook 原理》

源码锚点:frida-core / frida-gum 17.9.1frida-tools 14.8.0frida-java-bridge 固定提交 f72e61ed18fa6b72f0559df223b22a899685b22c;AOSP frameworks/base 固定提交 6e47c7075b91983ae501114425ea25e6df7690c8;AOSP ART / libnativehelper 固定 tag android-14.0.0_r1

进度:已打通脚本 top-level、Android LoadedApk、JavaVM/JNIEnv、Java.perform()、ART ClassLoader 枚举与指定 ClassFactory Hook 的完整源码链;待补插件双 ClassLoader 真机记录


开课词库

词库按“脚本执行、JNI 环境、类加载命名空间”三段排列。带 ★ 的对象决定本课主时间线。

A. 脚本从创建到执行

术语 通俗解释 正文位置
Session 控制端与目标进程内 agent 建立的会话 §1
create_script() 创建脚本实例和 JS 运行环境,不执行用户顶层代码 §1
Script.load() 加载脚本并执行用户顶层代码 §1
★ top-level 不在回调函数里的脚本正文,例如 Hook 注册和第一行 send() §0、§1
JS scheduler 在目标进程内调度 QuickJS/V8 任务的线程与主循环 §1
resume() 放开 F01 的 spawn 门控,让 App 主线程继续启动 §2

B. Frida 进入 Java 世界

术语 通俗解释 正文位置
JavaVM 进程级虚拟机入口;Frida 通过它查询或附加当前线程 §4.1–§4.3
★ JNIEnv 当前线程调用 JNI 函数的接口;不能跨线程共用 §4.1–§4.3
Java.perform() 取得当前线程 JNIEnv,并在 Android App 中等待默认 loader §4.4–§4.8
Java.performNow() 取得当前线程 JNIEnv 后立即执行,不等待默认 loader §4.9
Java.scheduleOnMainThread() 把任务显式安排到 Android 主线程 §4.9

C. Android 类加载命名空间

术语 通俗解释 正文位置
LoadedApk Android 在 App 进程中保存包路径、资源、ClassLoader 和 Application 状态的对象 §3
ClassLoader 决定某个类名从哪些 dex 路径解析,以及类身份属于哪个命名空间 §3、§4.10–§4.11
ClassFactory.loader Java.use() 默认采用的 ClassLoader §4.11
Java.enumerateClassLoaders() 通过 ART ClassLinker 枚举当前已注册且仍存活的 ClassLoader §4.10
Java.ClassFactory.get(loader) 为指定 loader 创建或复用独立 ClassFactory §4.11

0. 先确定本课结论

“Frida 第一个脚本运行时机”指的是用户脚本的第一行 JavaScript,不是最早进入进程的 Frida 原生代码。后者是 F01 已讲过的 zymbiote、loader 和 agent 初始化;用户 JS 要等 Script.load() 才执行。

对 Android 普通、无 instrumentation 的早期 spawn,主线顺序是:

Zygote fork / specialize
  → zymbiote 让 App 主线程停在 recv()
  → frida-server 向新进程注入完整 agent
  → client 建立 Session
  → create_script() 创建脚本实例
  → Script.load() 在 agent 的 JS 线程执行 top-level
  → Java.perform(fn) 发现默认 loader 为空,将 fn 入队
  → VM.perform 为 JS 线程取得 JNIEnv,安装 framework Hook
  → client 调用 resume()
  → zymbiote 收到 ACK,App 主线程继续
  → ActivityThread.main()
  → bindApplication
  → LoadedApk / App ClassLoader
  → Hook 设置 ClassFactory.loader
  → VM.perform 为触发 Hook 的线程取得 JNIEnv
  → 执行 Java.perform(fn) 回调
  → Application / Provider / Activity

这条时间线只有三个需要分别判断的边界:

边界 已经发生 尚不能证明
create_script() 返回 脚本对象和 JS context 已创建 顶层 JS 已执行
Script.load() 返回 顶层 JS 已执行,顶层 Hook 注册代码已运行 Hook 已命中、App ClassLoader 已就绪
Java.perform() 回调开始 当前线程可调用 JNI,默认主包 loader 已就绪 当前线程一定是 Android 主线程、插件类一定可见

后文只沿这三个边界展开。


1. 一次讲完 create_script()Script.load()

源码直达:client Session.create_script() · client Script.load() · agent create/load 入口 · ScriptEngine.create_script() · ScriptInstance.load() · QuickJS backend create · QuickJS load / JS_EvalFunction() · JS scheduler

脚本生命周期由 client 发起,由目标进程内的 agent 执行。frida-server 负责会话路由,不负责运行 JavaScript [1][2]。

client
  → frida-server
  → AgentSession
  → BaseAgentSession
  → ScriptEngine
  → QuickJS / V8

client 侧先创建,再加载 [1]:

public async Script create_script (string source,
        ScriptOptions? options = null,
        Cancellable? cancellable = null)
        throws Error, IOError {
    check_open ();

    var raw_options = (options != null)
            ? options._serialize ()
            : make_parameters_dict ();

    AgentScriptId script_id;
    try {
        script_id = yield active_session.create_script (
                source, raw_options, cancellable);
    } catch (GLib.Error e) {
        throw_dbus_error (e);
    }

    check_open ();

    var script = new Script (this, script_id);
    scripts[script_id] = script;
    return script;
}

public async void load (Cancellable? cancellable = null)
        throws Error, IOError {
    check_open ();

    try {
        yield session.active_session.load_script (id, cancellable);
    } catch (GLib.Error e) {
        throw_dbus_error (e);
    }
}

active_session 是远端 AgentSession 的代理。第一段调用返回 script_id,client 据此建立一个可控制的 Script 对象;第二段才要求 agent 加载这个实例。

agent 侧的 create 路径把源码交给 ScriptEngine,选择 backend,创建 Gum.Script 并保存实例 [2][3]:

Gum.ScriptBackend backend = pick_backend (options.runtime);

Gum.Script script;
try {
    if (source != null)
        script = yield backend.create (
                name, source, options.snapshot);
    else
        script = yield backend.create_from_bytes (
                bytes, options.snapshot);
} catch (Gum.Error e) {
    throw new Error.INVALID_ARGUMENT ("%s", e.message);
}

var instance = new ScriptInstance (script_id, script);
instances[script_id] = instance;

QuickJS 的 backend create 会建立 runtime/context,并解析或编译源码 [4]:

script = g_object_new (GUM_QUICK_TYPE_SCRIPT,
    "name", d->name,
    "source", d->source,
    "main-context", gum_script_task_get_context (task),
    "backend", self,
    NULL);

gum_quick_script_create_context (script, &error);

所以语法错误可能在 create 阶段出现,但这不等于用户顶层代码已经执行。create 完成时,脚本状态仍是 CREATED

load 路径从实例表取回脚本并推进状态 [3]:

if (state != CREATED)
    throw new Error.INVALID_OPERATION (
            "Script cannot be loaded in its current state");

load_request = new Promise<bool> ();
state = LOADING;

yield script.load ();

state = LOADED;
load_request.resolve (true);

QuickJS 不在控制请求线程上直接执行用户代码,而是把 load 任务交给 JS scheduler [5]:

gum_script_task_run_in_js_thread (
    task,
    gum_quick_script_backend_get_scheduler (self->backend));

JS 线程中的 load 最终求值 entrypoint:

result = JS_EvalFunction (
    ctx, g_array_index (entrypoints, JSValue, i));

V8 对应路径使用 module Evaluate() 或 script Run();实现函数不同,但 create 与 load 的边界相同 [6]。上游 17.9.1 的 scheduler 创建 gum-js-loop 后台线程并运行 GLib main loop [7]:

self->js_thread = g_thread_new (
    "gum-js-loop",
    (GThreadFunc) gum_script_scheduler_run_js_loop,
    self);

g_main_loop_run (self->js_loop);

因此这部分只需记住一条源码链:

create_script()
  → backend.create()
  → context + Gum.Script + script_id
  → state = CREATED

Script.load()
  → state = LOADING
  → JS scheduler
  → QuickJS JS_EvalFunction / V8 Evaluate 或 Run
  → 用户 top-level
  → state = LOADED

LOADED 只说明顶层初始化完成。Interceptor.attach() 可以已经安装,但它的回调仍要等目标控制流经过 Hook 点;Java.perform() 也可能仍在等待 App ClassLoader。


2. 为什么 top-level 能早于 App 业务代码

源码直达:frida-tools application.py · repl.py · F01 · resume 放行

frida -U -f TARGET_PACKAGE -l probe.js 在工具内部不是一个不可分割的动作,而是下面的固定次序 [8]:

Device.spawn(TARGET_PACKAGE)
  → attach(pid)
  → Session.create_script(source)
  → 注册 message handler
  → Script.load()
  → Device.resume(pid)

F01 的 zymbiote 此时只阻塞 App 主线程。agent 已经拥有自己的控制线程和 JS scheduler,所以它能在 App 主线程尚未进入 ActivityThread.main() 时执行 top-level。

这也是 spawn 模式能提前安装 Hook 的原因:

App 主线程:specialize → zymbiote.recv() ─────────────→ ActivityThread.main()
                                      ↑ resume / ACK

agent 线程:attach → create → load → top-level ──────→ 等待 Hook 命中

--pause 只让 frida-tools 跳过最后的自动 resume()。它不暂停 agent,也不阻止 Script.load()

命令 top-level App 主线程
frida -U -f TARGET -l probe.js 在 resume 前执行 load 返回后自动放行
frida -U -f TARGET -l probe.js --pause 同样执行 保持在门控点,直到 %resume

已有进程的 attach 不经过这段 spawn 门控。目标进程已经运行到哪里,取决于 attach 时的真实状态;不能把“top-level 早于 Application”从 spawn 直接推广到 attach。

send()console.log() 还要经过 agent 消息队列、控制通道与主机回调。终端显示时间晚于代码执行时间,适合证明阶段是否到达,不适合推断进程内的精细时间差 [9]。


3. Android App 怎样建立 LoadedApk 与最终 ClassLoader

理解 Java.perform() 之前,必须先建立 Android 自身的装载基线:

ActivityThread.main()
  → attachApplication()
  → ApplicationThread.bindApplication()
  → H.BIND_APPLICATION
  → handleBindApplication()
  → getPackageInfo()
  → LoadedApk
  → LoadedApk.getClassLoader()
  → Application / Provider / Activity

下一节再解释 Frida 在哪个返回点接入这条链。

源码直达:AOSP ActivityThread.java · LoadedApk.java · ApplicationLoaders.java · ContextImpl.java · AppComponentFactory.java

3.1 从 ActivityThread.main()handleBindApplication()

源码直达:AOSP ActivityThread.main() · ActivityThread.attach() · ApplicationThread.bindApplication() · H.handleMessage() · ActivityManagerService.attachApplicationLocked()

App 主线程进入 ActivityThread.main() 后准备主 Looper,并把本进程的 ApplicationThread Binder 对象交给 system_server [13][18]:

Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
}

Looper.loop();

attach(false, startSeq) 的非 system 分支调用:

final IActivityManager mgr = ActivityManager.getService();
mgr.attachApplication(mAppThread, startSeq);

system_server 的 attachApplicationLocked() 随后跨 Binder 调用 thread.bindApplication(...)。App 进程的 ApplicationThread.bindApplication() 保存关键参数并向主线程发消息 [13][18]:

AppBindData data = new AppBindData();
data.processName = processName;
data.appInfo = appInfo;
data.providers = providerList.getList();
data.instrumentationName = instrumentationName;
data.instrumentationArgs = instrumentationArgs;
data.restrictedBackupMode = isRestrictedBackupMode;
data.config = config;
data.compatInfo = compatInfo;
data.initProfilerInfo = profilerInfo;

sendMessage(H.BIND_APPLICATION, data);

主线程的 H.handleMessage() 收到消息后调用:

case BIND_APPLICATION:
    Trace.traceBegin(
            Trace.TRACE_TAG_ACTIVITY_MANAGER,
            "bindApplication");
    AppBindData data = (AppBindData) msg.obj;
    handleBindApplication(data);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    break;

handleBindApplication() 才是建立目标包运行状态、创建 Application 和安装 Provider 的主线程入口。

3.2 getPackageInfo() 先建立包状态,不加载全部类

源码直达:AOSP ActivityThread.handleBindApplication() · ActivityThread.getPackageInfo() · LoadedApk 构造函数 · LoadedApk.setApplicationInfo()

handleBindApplication() 先把绑定数据保存到 mBoundApplication,再请求一份包含代码的包状态 [13]:

mBoundApplication = data;

final boolean isSdkSandbox =
        data.sdkSandboxClientAppPackage != null;
data.info = getPackageInfo(
        data.appInfo,
        mCompatibilityInfo,
        null /* baseLoader */,
        false /* securityViolation */,
        true /* includeCode */,
        false /* registerPackage */,
        isSdkSandbox
);

AppBindData.appInfo 是 PackageManager 解析出的 ApplicationInfogetPackageInfo() 先按包名查询 mPackages 缓存;没有可复用对象时创建 [13]:

packageInfo = new LoadedApk(
    this,
    aInfo,
    compatInfo,
    baseLoader,
    securityViolation,
    includeCode && (aInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0,
    registerPackage
);

LoadedApk.setApplicationInfo() 把包路径分开保存 [14]:

mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
mDataDir = aInfo.dataDir;
mLibDir = aInfo.nativeLibraryDir;
mSplitAppDirs = aInfo.splitSourceDirs;
mSplitResDirs = aInfo.uid == myUid
        ? aInfo.splitSourceDirs
        : aInfo.splitPublicSourceDirs;
mSplitClassLoaderNames = aInfo.splitClassLoaderNames;

此时得到的是“当前进程中目标包的运行时档案”。它已经知道 base APK、split APK、资源、数据和 native 库路径,但不表示所有 dex 已打开,也不表示业务类已经逐个加载。

3.3 getClassLoader() 按需创建两层 loader

源码直达:AOSP LoadedApk.createOrUpdateClassLoaderLocked() · LoadedApk.getClassLoader() · ApplicationLoaders · AppComponentFactory.instantiateClassLoader()

LoadedApk.getClassLoader() 检查最终字段 mClassLoader [14]:

public ClassLoader getClassLoader() {
    synchronized (mLock) {
        if (mClassLoader == null) {
            createOrUpdateClassLoaderLocked(null /*addedPaths*/);
        }
        return mClassLoader;
    }
}

createOrUpdateClassLoaderLocked() 先由 makePaths() 汇总:

zipPaths
  → sourceDir
  → 非隔离 splitSourceDirs
  → Java shared libraries

libPaths
  → nativeLibraryDir
  → APK 内当前 ABI 的 lib 目录
  → 允许访问的 system/vendor/product native 路径

这些路径交给 ApplicationLoaders,产生默认应用 loader [14][15]:

mDefaultClassLoader =
    ApplicationLoaders.getDefault().getClassLoaderWithSharedLibraries(
        zip,
        mApplicationInfo.targetSdkVersion,
        isBundledApp,
        librarySearchPath,
        libraryPermittedPath,
        mBaseClassLoader,
        mApplicationInfo.classLoaderName,
        sharedLibraries.first,
        nativeSharedLibraries,
        sharedLibraries.second
    );

ApplicationLoaders 在未传入父 loader 时,以 system class loader 的 parent 作为基础父节点,再通过 ClassLoaderFactory.createClassLoader() 创建包含 APK 路径的 loader [15]:

ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
if (parent == null) {
    parent = baseParent;
}

这解释了为什么应用 loader 的父链中能看到 bootstrap loader:它是父节点,不是 Frida 最终保存的对象。

最后,AppComponentFactory 有机会保留或替换默认 loader [14][16]:

if (mClassLoader == null) {
    mClassLoader = mAppComponentFactory.instantiateClassLoader(
        mDefaultClassLoader,
        new ApplicationInfo(mApplicationInfo)
    );
}

默认 AppComponentFactory.instantiateClassLoader() 返回传入的 mDefaultClassLoader;应用也可通过 manifest 配置自定义 factory。真正返回给 bridge 的是最后的 mClassLoader

ApplicationInfo.sourceDir / splitSourceDirs
  → LoadedApk.makePaths()
  → ApplicationLoaders
  → mDefaultClassLoader
  → AppComponentFactory.instantiateClassLoader()
  → final mClassLoader

3.4 ClassLoader 为什么早于 Application 对象

源码直达:AOSP ActivityThread.currentApplication() · ActivityThread.handleBindApplication() · LoadedApk.makeApplicationInner() · Instrumentation.newApplication()

ActivityThread.getPackageInfo() 返回 LoadedApk 后,Android 才继续创建 App Context 与 Application。这个返回点因此位于最终 loader 可创建、Application 尚未创建的中间位置;下一节的 Frida early hook 正是接在这里。

ActivityThread.currentApplication() 读取的正是 mInitialApplication [13]:

public static Application currentApplication() {
    ActivityThread am = currentActivityThread();
    return am != null ? am.mInitialApplication : null;
}

Android 从 getPackageInfo() 返回后,尚未走到 mInitialApplication = app。接下来的源码顺序如下。

先创建 App Context [13]:

final ContextImpl appContext =
        ContextImpl.createAppContext(this, data.info);

再创建 Application [13]:

app = data.info.makeApplicationInner(
        data.restrictedBackupMode,
        null
);

随后保存 mInitialApplication,安装 Provider,最后调用 Application.onCreate() [13]:

mInitialApplication = app;

if (!data.restrictedBackupMode) {
    if (!ArrayUtils.isEmpty(data.providers)) {
        installContentProviders(app, data.providers);
    }
}

try {
    mInstrumentation.onCreate(data.instrumentationArgs);
} catch (Exception e) {
    throw new RuntimeException(
            "Exception thrown in onCreate() of "
            + data.instrumentationName + ": " + e.toString(), e);
}

try {
    mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
    if (!mInstrumentation.onException(app, e)) {
        throw new RuntimeException(
                "Unable to create application " + app.getClass().getName()
                + ": " + e.toString(), e);
    }
}

makeApplicationInner() 自己也使用同一个最终 loader [14]:

final java.lang.ClassLoader cl = getClassLoader();
ContextImpl appContext =
        ContextImpl.createAppContext(mActivityThread, this);

app = mActivityThread.mInstrumentation.newApplication(
        cl,
        appClass,
        appContext
);
appContext.setOuterContext(app);

因此时序不是“先创建 Application,再产生 loader”,而是:

new LoadedApk
  → [getPackageInfo 返回:此处已经可以调用 apk.getClassLoader()]
  → ContextImpl.createAppContext()
  → makeApplicationInner()
       → getClassLoader()
       → final mClassLoader
  → mInitialApplication = app
  → ContentProvider
  → Application.onCreate()

Android 的自然主线最迟会在 makeApplicationInner() 中取得最终 loader。getPackageInfo() 返回点更早,并且返回的 LoadedApk 已经具备按需创建 loader 所需的全部路径;下一节会看到 Frida 正是在这个间隙主动调用 apk.getClassLoader()

3.5 Activity 为什么继续使用同一个 loader

源码直达:AOSP ActivityThread.performLaunchActivity() · ContextImpl.createActivityContext() · AppComponentFactory.instantiateActivity()

普通同包 Activity 启动时,performLaunchActivity() 取得或复用同一包的 LoadedApk,再从 Activity Context 取 loader [13]:

if (r.packageInfo == null) {
    r.packageInfo = getPackageInfo(
        aInfo.applicationInfo,
        mCompatibilityInfo,
        Context.CONTEXT_INCLUDE_CODE
    );
}

ContextImpl appContext = createBaseContextForActivity(r);
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
        cl,
        component.getClassName(),
        r.intent
);

所以主包默认路径是:

同一个 LoadedApk.mClassLoader
  ├─ 实例化 Application
  └─ 实例化普通同包 Activity

隔离 split 会通过 getSplitClassLoader() 使用专用 loader;插件、加固和热更新也能在默认链之外创建 DexClassLoaderPathClassLoader 或自定义 loader。主包 loader 成功不代表这些独立命名空间已经可见。


4. 从 JavaVM、JNIEnv 到 Java.perform()

top-level 开始执行,只证明 GumJS 已经运行。bridge 初始化与 Java.perform() 调用按下面两层推进:

bridge 初始化
  → 发现进程中的 JavaVM
  → 从 JavaVM invocation table 绑定 GetEnv / AttachCurrentThread

Java.perform(fn)
  → 先判断 App 默认 ClassLoader 是否就绪
  ├─ 已就绪:VM.perform 取得当前线程 JNIEnv → 执行 fn
  └─ 未就绪:fn 入队
       → VM.perform 取得当前线程 JNIEnv → 安装 framework Hook
       → Hook 取得 LoadedApk / ClassLoader
       → VM.perform 再为触发 Hook 的线程取得 JNIEnv → 执行 fn
  → 按需枚举其他 ClassLoader

4.1 JavaVM、JNIEnv、ClassLoader 是三种不同对象

源码直达:AOSP libnativehelper jni.hJNIEnv 定义 · JNIInvokeInterface / JavaVM · Android Developers · JNI Tips

三个对象解决的问题不同:

对象 生命周期与归属 负责什么
JavaVM 进程级;同一进程中的线程共享 VM 入口 附加/分离线程、为当前线程查询 JNIEnv
JNIEnv 线程级;只能在取得它的线程使用 调用 FindClassNewGlobalRef、方法调用等 JNI 函数
ClassLoader Java 对象;一个进程可同时存在很多个 决定类名从哪些 dex 查找,并参与 Java 类身份

AOSP jni.h 中,JNIEnv 是 JNI 函数表包装 [22]:

struct _JNIEnv {
    const struct JNINativeInterface* functions;
};

JavaVM 则持有 invocation table,其中包含线程相关入口 [22]:

struct JNIInvokeInterface {
    void* reserved0;
    void* reserved1;
    void* reserved2;

    jint (*DestroyJavaVM)(JavaVM*);
    jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint (*DetachCurrentThread)(JavaVM*);
    jint (*GetEnv)(JavaVM*, void**, jint);
    jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};

struct _JavaVM {
    const struct JNIInvokeInterface* functions;
};

所以“拿到 JavaVM”“当前线程拿到 JNIEnv”“选对业务 ClassLoader”是三道独立条件。JavaVM 已存在时,Frida 的 JS 线程仍可能尚未附加;JNIEnv 已取得时,默认 App loader 仍可能为空。

必须讲清 JNIEnv,是因为 frida-agent 的 GumJS scheduler 本质上从 native 线程执行脚本。它不能只拿一个进程级 JavaVM 指针就直接调用 Java API:每次进入 JNI 都要先确认当前 tid 对应的 JNIEnv。必须讲清 ClassLoader,则是因为 JNIEnv 只提供调用接口,不会替 Frida 决定目标类位于主 APK、隔离 split 还是插件 dex。

4.2 Frida 怎样找到进程中的 JavaVM

源码直达:frida-java-bridge lib/api.js · lib/android.jsgetApi() 与 libart/libdvm 识别 · JNI_GetCreatedJavaVMs 取 VM · index.js::_tryInitialize()

lib/api.js 先按运行环境选择 Android 或标准 JVM backend [20]:

import {
  getApi as androidGetApi,
  getAndroidVersion
} from './android.js';
import { getApi as jvmGetApi } from './jvm.js';

let getApi = androidGetApi;
try {
  getAndroidVersion();
} catch (e) {
  getApi = jvmGetApi;
}

Android backend 枚举模块,只接受真实的 libart.solibdvm.so [20]:

const vmModules = Process.enumerateModules()
  .filter(m => /^lib(art|dvm).so$/.test(m.name))
  .filter(m => !/\/system\/fake-libs/.test(m.path));

if (vmModules.length === 0) {
  return null;
}

const vmModule = vmModules[0];
const flavor =
    (vmModule.name.indexOf('art') !== -1)
      ? 'art'
      : 'dalvik';

解析 JNI_GetCreatedJavaVMs 后,bridge 请求当前进程已经创建的 VM,并保存第一个 JavaVM* [20]:

const vms = Memory.alloc(pointerSize);
const vmCount = Memory.alloc(jsizeSize);

checkJniResult(
  'JNI_GetCreatedJavaVMs',
  temporaryApi.JNI_GetCreatedJavaVMs(vms, 1, vmCount)
);

if (vmCount.readInt() === 0) {
  return null;
}

temporaryApi.vm = vms.readPointer();

Runtime._tryInitialize() 再用这个指针创建 VM 包装,并初始化 Android bridge 与 ClassFactory [10]:

const api = getApi();
if (api === null) {
  return false;
}

const vm = new VM(api);
this.vm = vm;

initialize(vm);
ClassFactory._initialize(vm, api);
this.classFactory = new ClassFactory();

到这里仅完成了“发现进程 JavaVM”。classFactory.loader 仍是 null,当前 Frida 线程也要通过下一步确认自己是否已有 JNIEnv。

4.3 VM.perform() 怎样为当前线程取得 JNIEnv

源码直达:frida-java-bridge lib/vm.js:JavaVM vtable · VM.perform() / attach / GetEnv · env 嵌套缓存与清理 · AOSP jni.h invocation table

VM 构造函数从 JavaVM* 的 invocation table 取出三个函数。索引 4、5、6 正好对应 AOSP jni.h 中的 AttachCurrentThreadDetachCurrentThreadGetEnv [11][22]:

const vtable = handle.readPointer();
const options = {
  exceptions: 'propagate'
};

attachCurrentThread = new NativeFunction(
  vtable.add(4 * pointerSize).readPointer(),
  'int32',
  ['pointer', 'pointer', 'pointer'],
  options
);
detachCurrentThread = new NativeFunction(
  vtable.add(5 * pointerSize).readPointer(),
  'int32',
  ['pointer'],
  options
);
getEnv = new NativeFunction(
  vtable.add(6 * pointerSize).readPointer(),
  'int32',
  ['pointer', 'pointer', 'int32'],
  options
);

VM.perform() 的完整执行主线如下 [11]:

this.perform = function (fn) {
  const threadId = Process.getCurrentThreadId();

  const cachedEnv = tryGetCachedEnv(threadId);
  if (cachedEnv !== null) {
    return fn(cachedEnv);
  }

  let env = this._tryGetEnv();
  const alreadyAttached = env !== null;
  if (!alreadyAttached) {
    env = this.attachCurrentThread();
    attachedThreads.set(threadId, true);
  }

  this.link(threadId, env);

  try {
    return fn(env);
  } finally {
    const isJsThread = threadId === jsThreadID;

    if (!isJsThread) {
      this.unlink(threadId);
    }

    if (!alreadyAttached && !isJsThread) {
      const allowedToDetach =
          attachedThreads.get(threadId);
      attachedThreads.delete(threadId);

      if (allowedToDetach) {
        this.detachCurrentThread();
      }
    }
  }
};

它调用的查询与附加函数也是 bridge 自己从 vtable 绑定的 [11]:

this.attachCurrentThread = function () {
  const envBuf = Memory.alloc(pointerSize);
  checkJniResult(
    'VM::AttachCurrentThread',
    attachCurrentThread(handle, envBuf, NULL)
  );
  return new Env(envBuf.readPointer(), this);
};

this.detachCurrentThread = function () {
  checkJniResult(
    'VM::DetachCurrentThread',
    detachCurrentThread(handle)
  );
};

this.getEnv = function () {
  const cachedEnv =
      tryGetCachedEnv(Process.getCurrentThreadId());
  if (cachedEnv !== null) {
    return cachedEnv;
  }

  const envBuf = Memory.alloc(pointerSize);
  const result =
      getEnv(handle, envBuf, JNI_VERSION_1_6);
  if (result === -2) {
    throw new Error(
      'Current thread is not attached to the Java VM; ' +
      'please move this code inside a Java.perform() callback'
    );
  }
  checkJniResult('VM::GetEnv', result);
  return new Env(envBuf.readPointer(), this);
};

this._tryGetEnv = function () {
  const h = this.tryGetEnvHandle(JNI_VERSION_1_6);
  if (h === null) {
    return null;
  }
  return new Env(h, this);
};

this.tryGetEnvHandle = function (version) {
  const envBuf = Memory.alloc(pointerSize);
  const result = getEnv(handle, envBuf, version);
  if (result !== JNI_OK) {
    return null;
  }
  return envBuf.readPointer();
};

嵌套调用通过 activeEnvs 按 tid 复用同一份 wrapper [11]:

this.link = function (tid, env) {
  const entry = activeEnvs.get(tid);
  if (entry === undefined) {
    activeEnvs.set(tid, [env, 1]);
  } else {
    entry[1]++;
  }
};

this.unlink = function (tid) {
  const entry = activeEnvs.get(tid);
  if (entry[1] === 1) {
    activeEnvs.delete(tid);
  } else {
    entry[1]--;
  }
};

function tryGetCachedEnv (threadId) {
  const entry = activeEnvs.get(threadId);
  if (entry === undefined) {
    return null;
  }
  return entry[0];
}

VM.dispose = function (vm) {
  if (attachedThreads.get(jsThreadID) === true) {
    attachedThreads.delete(jsThreadID);
    vm.detachCurrentThread();
  }
};

准确顺序是:

当前 tid
  → activeEnvs 中有缓存:直接复用
  → 无缓存:JavaVM.GetEnv(JNI_VERSION_1_6)
       → 成功:当前线程原本已附加
       → 非 JNI_OK:JavaVM.AttachCurrentThread
  → link(tid, env)
  → 执行 fn(env)
  → 非 JS 线程按条件 unlink / DetachCurrentThread
  → JS scheduler 线程保留 attach,bridge dispose 时再 detach

这条路径没有调用 ThreadList::SuspendAll。固定 index.jswithAllArtThreadsSuspended() 的调用点位于 _enumerateClassLoadersArt(),不在 perform()VM.perform() 中 [21]。取得 JNIEnv 是当前线程与 JavaVM 的关系;全线程暂停发生在 §4.10 的 ART ClassLoader 枚举中。

4.4 Java.perform():立即执行,还是等待 App loader

源码直达:frida-java-bridge index.js::perform() · Runtime._isAppProcess()

Java.perform(fn) 在 JNIEnv 之外还处理 Android App 默认 ClassLoader 的时机 [10]:

perform (fn) {
  this._checkAvailable();

  if (!this._isAppProcess() ||
      this.classFactory.loader !== null) {
    try {
      this.vm.perform(fn);
    } catch (e) {
      Script.nextTick(() => { throw e; });
    }
  } else {
    this._pendingVmOps.push(fn);
    if (this._pendingVmOps.length === 1) {
      this._performPendingVmOpsWhenReady();
    }
  }
}
条件 动作
不是 Android App 进程 直接 vm.perform(fn)
默认 ClassFactory.loader 已存在 直接 vm.perform(fn)
是 Android App,且默认 loader 为空 fn 进入 _pendingVmOps

只有队列从 0 变成 1 时才安装等待 Hook;后续 Java.perform() 只继续入队。_isAppProcess() 则通过 /proc/self/exe 是否匹配 /system/bin/app_process 判断并缓存结果 [10]。

因此 Java.perform() 是两层组合:

VM.perform
  → 当前线程取得 JNIEnv

Android App loader gate
  → 默认 loader 为空时等待 LoadedApk

4.5 _performPendingVmOpsWhenReady():完整 Hook 代码

源码直达:frida-java-bridge index.js::_performPendingVmOpsWhenReady() · _performPendingVmOps() · AOSP ActivityThread.handleBindApplication()

下面是固定 bridge 提交 index.js:400-454 的完整实现 [10]:

_performPendingVmOpsWhenReady () {
  this.vm.perform(() => {
    const { classFactory: factory } = this;

    const ActivityThread = factory.use('android.app.ActivityThread');
    const app = ActivityThread.currentApplication();
    if (app !== null) {
      initFactoryFromApplication(factory, app);
      this._performPendingVmOps();
      return;
    }

    const runtime = this;
    let initialized = false;
    let hookpoint = 'early';

    const handleBindApplication = ActivityThread.handleBindApplication;
    handleBindApplication.implementation = function (data) {
      if (data.instrumentationName.value !== null) {
        hookpoint = 'late';

        const LoadedApk = factory.use('android.app.LoadedApk');
        const makeApplication = LoadedApk.makeApplication;
        makeApplication.implementation = function (forceDefaultAppClass, instrumentation) {
          if (!initialized) {
            initialized = true;
            initFactoryFromLoadedApk(factory, this);
            runtime._performPendingVmOps();
          }

          return makeApplication.apply(this, arguments);
        };
      }

      handleBindApplication.apply(this, arguments);
    };

    const getPackageInfoCandidates = ActivityThread.getPackageInfo.overloads
      .map(m => [m.argumentTypes.length, m])
      .sort(([arityA,], [arityB,]) => arityB - arityA)
      .map(([_, method]) => method);
    const getPackageInfo = getPackageInfoCandidates[0];
    getPackageInfo.implementation = function (...args) {
      const apk = getPackageInfo.call(this, ...args);

      if (!initialized && hookpoint === 'early') {
        initialized = true;
        initFactoryFromLoadedApk(factory, apk);
        runtime._performPendingVmOps();
      }

      return apk;
    };
  });
}

这段等待逻辑没有定时器,也没有 sleep。它先检查当前 Application,若仍为空,就通过 Frida Java 方法替换安装三个可能的 Hook 点:

Hook 点 安装时机 作用
ActivityThread.handleBindApplication(AppBindData) Application 为空时立即安装 判断本次绑定是否带 instrumentation
参数数量最多的 ActivityThread.getPackageInfo(...) overload Application 为空时立即安装 普通启动路径拿到返回的 LoadedApk
LoadedApk.makeApplication(boolean, Instrumentation) 仅发现 instrumentationName != null 时安装 instrumentation 路径延后取得 loader

此时 ClassFactory.loader 虽然为空,factory.use('android.app.ActivityThread') 仍可工作,因为它是 framework 类;空 loader 分支会走 JNI FindClass。业务 APK 类此时仍不具备同样的可见性,具体分支见 §4.11。

安装完 Hook 后,最初那次 _performPendingVmOpsWhenReady() 就返回。用户的 fn 仍在 _pendingVmOps 中,top-level 可以继续执行;只有 Android 后续真正调用这些方法,队列才会被清空。

固定源码没有在 initialized = true 后恢复这几个 .implementation。这些 wrapper 仍保留,但 initialized 阻止重复初始化和重复清空队列,wrapper 随后继续调用原方法。

4.6 晚注入:Application 已存在时立即拿 loader

源码直达:frida-java-bridge 晚注入分支与 initFactoryFromApplication() · initFactoryFromApplication() · AOSP Application.attach() · ContextWrapper.getClassLoader() · ContextImpl.getClassLoader()

如果进入 _performPendingVmOpsWhenReady() 时:

const app = ActivityThread.currentApplication();

已经返回非空对象,bridge 不安装上述 Hook,而是立即执行 [10]:

initFactoryFromApplication(factory, app);
this._performPendingVmOps();
return;

对应初始化函数是:

function initFactoryFromApplication (factory, app) {
  const Process = factory.use('android.os.Process');

  factory.loader = app.getClassLoader();

  if (Process.myUid() === Process.SYSTEM_UID.value) {
    factory.cacheDir = '/data/system';
    factory.codeCacheDir = '/data/dalvik-cache';
  } else {
    if ('getCodeCacheDir' in app) {
      factory.cacheDir = app.getCacheDir().getCanonicalPath();
      factory.codeCacheDir = app.getCodeCacheDir().getCanonicalPath();
    } else {
      factory.cacheDir = app.getFilesDir().getCanonicalPath();
      factory.codeCacheDir = app.getCacheDir().getCanonicalPath();
    }
  }
}

Application.getClassLoader()ContextWrapper → ContextImpl → LoadedApk.getClassLoader() 返回目标包的最终 loader [14][17]。赋值完成后,pending 队列立即执行。

这条委托链在 AOSP 中是直接代码,不是概念推导 [17]:

// Application.attach()
final void attach(Context context) {
    attachBaseContext(context);
    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

// ContextWrapper.getClassLoader()
public ClassLoader getClassLoader() {
    return mBase.getClassLoader();
}

// ContextImpl.getClassLoader()
public ClassLoader getClassLoader() {
    return mClassLoader != null
            ? mClassLoader
            : (mPackageInfo != null
                    ? mPackageInfo.getClassLoader()
                    : ClassLoader.getSystemClassLoader());
}

4.7 早期普通启动:在 getPackageInfo() 返回处拿到 LoadedApk

源码直达:frida-java-bridge early getPackageInfo hook · initFactoryFromLoadedApk() · AOSP ActivityThread.getPackageInfo()

spawn 早期 currentApplication() 为空,bridge 已经安装前面的 Hook。客户端 resume() 后,Android 按 §3.1 的顺序在主线程进入 handleBindApplication(data),并按 §3.2 调用参数最多的 getPackageInfo(...)。Frida 此时已经在等待这个调用。

bridge 把所有 getPackageInfo overload 按参数数量降序排列:

const getPackageInfoCandidates = ActivityThread.getPackageInfo.overloads
  .map(m => [m.argumentTypes.length, m])
  .sort(([arityA,], [arityB,]) => arityB - arityA)
  .map(([_, method]) => method);
const getPackageInfo = getPackageInfoCandidates[0];

固定 Android 14 中,排在第一位的正是 handleBindApplication() 使用的最长 overload。它的 replacement 先调用原方法:

const apk = getPackageInfo.call(this, ...args);

原方法查询 mPackages 缓存;未命中时创建目标包的 LoadedApk 并返回。返回值首先回到 Frida replacement,所以此刻 Android 已经有 LoadedApk,但 handleBindApplication() 还没有继续创建 Application。

随后 replacement 执行:

if (!initialized && hookpoint === 'early') {
  initialized = true;
  initFactoryFromLoadedApk(factory, apk);
  runtime._performPendingVmOps();
}

initFactoryFromLoadedApk() 的完整实现是 [10]:

function initFactoryFromLoadedApk (factory, apk) {
  const JFile = factory.use('java.io.File');

  factory.loader = apk.getClassLoader();

  const dataDir = JFile.$new(apk.getDataDir()).getCanonicalPath();
  factory.cacheDir = dataDir;
  factory.codeCacheDir = dataDir + '/cache';
}

关键语句不是枚举 ClassLoader,也不是读取线程 context loader,而是直接调用:

factory.loader = apk.getClassLoader();

如果 LoadedApk.mClassLoader 仍为空,Android 会在这次调用内部执行 createOrUpdateClassLoaderLocked();如果此前已创建,则直接返回同一个最终 loader。§3 已经展开这个 Android 内部过程。

loader 和缓存目录都设置后,bridge 立即调用 _performPendingVmOps() [10]:

_performPendingVmOps () {
  const { vm, _pendingVmOps: pending } = this;

  let fn;
  while ((fn = pending.shift()) !== undefined) {
    try {
      vm.perform(fn);
    } catch (e) {
      Script.nextTick(() => { throw e; });
    }
  }
}

shift() 表明回调按入队顺序逐个取出。每个回调再次经过 vm.perform(fn),确保当前触发 Hook 的线程具有合法 JNI 环境;异常被转交到下一轮 JS 调度。

普通早期路径的精确执行点因此是:

App 主线程进入 handleBindApplication()
  → 调用 getPackageInfo(...)
  → Frida replacement 调用原 getPackageInfo(...)
  → 原方法返回 LoadedApk
  → replacement 调用 apk.getClassLoader()
  → factory.loader = 最终 App ClassLoader
  → replacement 同步清空 _pendingVmOps
  → Java.perform(fn) 的 fn 在这里执行
  → replacement 返回 LoadedApk
  → handleBindApplication() 继续创建 Context 和 Application

在这条固定实现路径上,pending 回调由 App 主线程调用 getPackageInfo() 时同步触发,所以本次回调会落在 App 主线程上;这是当前 Hook 位置产生的结果,不是 Java.perform() 的 API 线程承诺。

4.8 instrumentation:为什么改走 makeApplication

源码直达:frida-java-bridge instrumentation late hook · AOSP Android 14 handleBindApplication() 调用 makeApplicationInner()

handleBindApplication replacement 最先检查:

if (data.instrumentationName.value !== null) {
  hookpoint = 'late';
  // 安装 LoadedApk.makeApplication replacement
}

一旦 hookpoint 变成 late,前面的 getPackageInfo replacement 仍会执行原方法并返回 LoadedApk,但不会初始化 factory,也不会清空队列:

if (!initialized && hookpoint === 'early') {
  // instrumentation 下条件不成立
}

固定 bridge 选择等到 LoadedApk.makeApplication() 被调用,再以该 LoadedApkthis 执行:

initFactoryFromLoadedApk(factory, this);
runtime._performPendingVmOps();

原因是 instrumentation 的 APK、split 与 native 路径会参与 loader 组合,普通路径的早期时点可能尚未形成最终环境。

这里存在明确版本边界:固定 bridge 提交 Hook 的是 LoadedApk.makeApplication(),本课固定 Android 14 的 handleBindApplication() 调用的是 makeApplicationInner() [10][13]。因此 instrumentation 分支要按目标 ROM 验证真实调用点;不能用普通启动路径成功替代。

4.9 performNow():只尝试一次,不安装等待 Hook

源码直达:frida-java-bridge index.js::performNow() · scheduleOnMainThread()

performNow() 的完整实现是 [10]:

performNow (fn) {
  this._checkAvailable();

  return this.vm.perform(() => {
    const { classFactory: factory } = this;

    if (this._isAppProcess() && factory.loader === null) {
      const ActivityThread = factory.use('android.app.ActivityThread');
      const app = ActivityThread.currentApplication();
      if (app !== null) {
        initFactoryFromApplication(factory, app);
      }
    }

    return fn();
  });
}

它只检查当前 Application:有就取 loader,没有也立即执行 fn。它不写 _pendingVmOps,也不 Hook handleBindApplication()getPackageInfo()makeApplication()

所以早期 spawn 中:

console.log("A");
Java.perform(() => console.log("B"));
console.log("C");

会先执行 A、将 B 入队、继续执行 C;resume 后 Android 进入前述 Hook 点,才执行 B

四种 API 的职责至此完全分开:

API 等待什么 线程含义
Script.load() JS backend 执行 top-level agent 的 JS scheduler
Java.performNow() 不等待 App loader 当前回调线程取得 JNIEnv
Java.perform() Android App 中等待默认主包 loader 由取得 loader 的路径触发,不承诺固定线程
Java.scheduleOnMainThread() 主 Looper 可调度 显式转到 Android 主线程

4.10 Java.enumerateClassLoaders() 怎样取得当前全部 loader

源码直达:frida-java-bridge enumerateClassLoaders() · ART 枚举实现 · withRunnableArtThread() / withAllArtThreadsSuspended() · makeArtClassLoaderVisitor() · AOSP ART ClassLinker::VisitClassLoaders() · ThreadList::SuspendAll() / ResumeAll()

Java.perform() 只为默认 Java.classFactory 选择主包 loader。插件、壳、热更新框架或隔离 split 创建的 loader,需要在 Java 环境可用后另行枚举。

公共 API 先按 VM 类型分派 [21]:

enumerateClassLoaders (callbacks) {
  this._checkAvailable();

  const { flavor } = this.api;
  if (flavor === 'jvm') {
    this._enumerateClassLoadersJvm(callbacks);
  } else if (flavor === 'art') {
    this._enumerateClassLoadersArt(callbacks);
  } else {
    throw new Error(
      'Enumerating class loaders is not supported on Dalvik'
    );
  }
}

标准 JVM 路径通过 choose('java.lang.ClassLoader') 枚举堆实例;Android ART 使用 ClassLinker::VisitClassLoaders。固定 bridge 中的 ART 实现是 [21]:

_enumerateClassLoadersArt (callbacks) {
  const { classFactory: factory, vm, api } = this;
  const env = vm.getEnv();

  const visitClassLoaders =
      api['art::ClassLinker::VisitClassLoaders'];
  if (visitClassLoaders === undefined) {
    throw new Error(
      'This API is only available on Android >= 7.0'
    );
  }

  const ClassLoader = factory.use('java.lang.ClassLoader');

  const loaderHandles = [];
  const addGlobalReference =
      api['art::JavaVMExt::AddGlobalRef'];
  const { vm: vmHandle } = api;

  withRunnableArtThread(vm, env, thread => {
    const collectLoaderHandles =
        makeArtClassLoaderVisitor(loader => {
          loaderHandles.push(
            addGlobalReference(vmHandle, thread, loader)
          );
          return true;
        });

    withAllArtThreadsSuspended(() => {
      visitClassLoaders(
        api.artClassLinker.address,
        collectLoaderHandles
      );
    });
  });

  try {
    loaderHandles.forEach(handle => {
      const loader =
          factory.cast(handle, ClassLoader);
      callbacks.onMatch(loader);
    });
  } finally {
    loaderHandles.forEach(handle => {
      env.deleteGlobalRef(handle);
    });
  }

  callbacks.onComplete();
}

这段代码分成四个阶段。

第一,vm.getEnv() 要求调用线程已经拥有 JNIEnv。因此实际脚本通常把枚举放在 Java.perform() 回调中;这里没有再次自动 attach [11][21]。

第二,withRunnableArtThread() 把当前 JNI 线程带入 bridge 需要的 ART 线程状态,并把底层 art::Thread* 交给回调 [20]:

export function withRunnableArtThread (vm, env, fn) {
  const perform =
      getArtThreadStateTransitionImpl(vm, env);

  const id = getArtThreadFromEnv(env).toString();
  artThreadStateTransitions[id] = fn;

  perform(env.handle);

  if (artThreadStateTransitions[id] !== undefined) {
    delete artThreadStateTransitions[id];
    throw new Error(
      'Unable to perform state transition; please file a bug'
    );
  }
}

makeArtClassLoaderVisitor() 则按 ART 的 C++ visitor ABI 在内存中构造对象和虚表,把 Visit() 转回 JavaScript 回调 [20]:

class ArtClassLoaderVisitor {
  constructor (visit) {
    const visitor = Memory.alloc(4 * pointerSize);

    const vtable = visitor.add(pointerSize);
    visitor.writePointer(vtable);

    const onVisit =
        new NativeCallback((self, klass) => {
          visit(klass);
        }, 'void', ['pointer', 'pointer']);
    vtable.add(2 * pointerSize).writePointer(onVisit);

    this.handle = visitor;
    this._onVisit = onVisit;
  }
}

export function makeArtClassLoaderVisitor (visit) {
  return new ArtClassLoaderVisitor(visit);
}

第三,真正访问 ART 的 ClassLinker 列表前,bridge 才暂停所有 ART mutator 线程 [20]:

export function withAllArtThreadsSuspended (fn) {
  const api = getApi();

  const threadList = api.artThreadList;
  const longSuspend = false;
  api['art::ThreadList::SuspendAll'](
    threadList,
    Memory.allocUtf8String('frida'),
    longSuspend ? 1 : 0
  );
  try {
    fn();
  } finally {
    api['art::ThreadList::ResumeAll'](threadList);
  }
}

finally 保证 visitor 抛出异常时仍执行 ResumeAll()。AOSP Android 14 的关键代码是 [23]:

void ThreadList::SuspendAll(
    const char* cause,
    bool long_suspend) {
  Thread* self = Thread::Current();

  SuspendAllInternal(self, self);

  Locks::mutator_lock_->ExclusiveLock(self);
  long_suspend_ = long_suspend;
}

void ThreadList::ResumeAll() {
  Thread* self = Thread::Current();

  long_suspend_ = false;
  Locks::mutator_lock_->ExclusiveUnlock(self);

  {
    MutexLock mu(self, *Locks::thread_list_lock_);
    MutexLock mu2(
        self,
        *Locks::thread_suspend_count_lock_);

    --suspend_all_count_;
    for (const auto& thread : list_) {
      if (thread == self) {
        continue;
      }
      bool updated = thread->ModifySuspendCount(
          self,
          -1,
          nullptr,
          SuspendReason::kInternal);
      DCHECK(updated);
    }

    Thread::resume_cond_->Broadcast(self);
  }
}

真实函数还包含超时、追踪和调试检查;这里摘出改变线程状态与 mutator lock 的主干。SuspendAllInternal() 的源码注释明确说明:先请求所有正在运行 Java 的线程暂停,并等待它们完成暂停;新线程也不能绕过 suspend-request 直接开始执行 Java [23]。

第四,暂停窗口内只做 ClassLoader 指针遍历和 global reference 收集。AOSP Android 14 的 VisitClassLoaders() 直接遍历 ClassLinker::class_loaders_,解码仍存活的 weak global root,再调用 visitor [23]:

void ClassLinker::VisitClassLoaders(
    ClassLoaderVisitor* visitor) const {
  Thread* const self = Thread::Current();
  for (const ClassLoaderData& data : class_loaders_) {
    ObjPtr<mirror::ClassLoader> class_loader =
        ObjPtr<mirror::ClassLoader>::DownCast(
            self->DecodeJObject(data.weak_root));
    if (class_loader != nullptr) {
      visitor->Visit(class_loader);
    }
  }
}

Frida visitor 为每个 native loader 指针调用 JavaVMExt::AddGlobalRef。这样退出暂停窗口、恢复其他线程后,这些对象仍不会被 GC 回收;随后 bridge 才把 handle 转成 java.lang.ClassLoader wrapper,调用用户的 onMatch(),最后删除 global ref [21]。

因此这里的“全部 ClassLoader”有明确边界:

边界 含义
时间 只包含本次 VisitClassLoaders 时已经注册的 loader,不包含稍后才创建的插件 loader
存活性 weak root 已清除的 loader 会被跳过
bootstrap bootstrap loader 在 Java 层以 null 表示,不是普通 ClassLoader 对象
Android 版本 固定实现要求 ART 提供 VisitClassLoaders,bridge 对 Android 7.0 以下报错
暂停范围 只在 native visitor 收集 global refs 时暂停;onMatch() 在恢复线程后执行

所以应把两个动作严格分开:

Java.perform()
  → 获取/附加当前线程的 JNIEnv
  → 不暂停全部 Java 线程

Java.enumerateClassLoaders()
  → withRunnableArtThread
  → SuspendAll
  → ClassLinker::VisitClassLoaders
  → AddGlobalRef 保存结果
  → ResumeAll
  → onMatch(loader)

4.11 为什么换成目标 loader 后 Java.use() 才能 Hook

源码直达:ClassFactory get(loader) · loader / use() · FindClass / loader.loadClass() · AOSP ART LookupClassesVisitor · Android JNI Tips · FindClass

factory.loader = apk.getClassLoader() 会进入 ClassFactory.loader setter [12]:

set loader (value) {
  const isInitial = this._loader === null && value !== null;

  this._loader = value;

  if (isInitial &&
      factoryCache.state === 'ready' &&
      this === factoryCache.factories[0]) {
    addFactoryToCache(this, value);
  }
}

真正决定后续类查找路径的是 _loader 字段。

ClassFactory.use() 根据 _loader 选择两条类解析路径 [12]:

const { _loader: loader } = this;
const getClassHandle = (loader !== null)
  ? makeLoaderClassHandleGetter(className, loader, env)
  : makeBasicClassHandleGetter(className);

loader == null 时,基础分支最终调用 JNI FindClass

function makeBasicClassHandleGetter (className) {
  const canonicalClassName = className.replace(/\./g, '/');
  return function (env) {
    return env.findClass(canonicalClassName);
  };
}

附加的原生线程没有 App Java 调用栈,Android 的 FindClass 会从 system class loader 语境查找;framework 类可见,不能证明 APK loader 已建立 [19]。

loader != null 后,bridge 改为显式调用该对象的 loadClass() [12]:

cachedLoaderMethod =
    usedLoader.loadClass
        .overload('java.lang.String').handle;

const result = cachedLoaderInvoke(
    env.handle,
    usedLoader.$h,
    cachedLoaderMethod,
    classNameValue
);
ClassFactory 状态 实际查找动作 可见范围
loader == null JNI FindClass system/bootstrap 可见类
默认 loader 已设置 最终 App loader 的 loadClass() base APK、普通 split 及父委派链
Java.ClassFactory.get(pluginLoader) 指定 loader 的 loadClass() 对应插件或动态 dex

类名相同,不代表是同一个类

Java 类身份不只由二进制类名决定,还取决于 defining ClassLoader。AOSP ART 查找指定 loader 定义的类时,也明确检查 [23]:

ObjPtr<mirror::Class> klass =
    class_table->Lookup(descriptor_, hash_);

if (klass != nullptr &&
    klass->GetClassLoader() == class_loader) {
  result_->push_back(klass);
}

因此以下对象可以同时存在:

com.example.Target + PathClassLoader(main)
com.example.Target + DexClassLoader(plugin)

它们有相同类名,却是两个不同的 java.lang.Class,各自拥有独立的静态字段、方法入口和实例类型关系。

主包默认 loader 通常只能看到:

bootstrap / framework
  → base APK
  → 普通 split

插件 loader 可能把另一个 dex 加在自己的搜索路径中。父加载器通常不能反向看到子加载器新增的类,两个并列插件 loader 也不共享命名空间。因此:

Java.perform() 已执行
  ≠ 所有插件 loader 已创建
  ≠ 默认 factory 能解析所有业务类

如果默认 factory 看不到目标类,Java.use(TARGET_CLASS) 在得到方法 wrapper 之前就会抛出 ClassNotFoundException,自然没有可替换的目标方法。

如果多个 loader 都能解析同名类,默认 factory 还可能拿到另一份 Class。此时 Hook 安装成功但业务调用走的是另一 loader 定义的类,回调仍不会命中。

ClassFactory.get(loader) 怎样绑定独立命名空间

固定 bridge 会按 loader 缓存独立 factory [12]:

static get (classLoader) {
  const cache = getFactoryCache();
  const defaultFactory = cache.factories[0];

  if (classLoader === null) {
    return defaultFactory;
  }

  const indexObj = cache.loaders.get(classLoader);
  if (indexObj !== null) {
    const index =
        defaultFactory.cast(indexObj, cache.Integer);
    return cache.factories[index.intValue()];
  }

  const factory = new ClassFactory();
  factory.loader = classLoader;
  factory.cacheDir = defaultFactory.cacheDir;
  addFactoryToCache(factory, classLoader);

  return factory;
}

指定 factory 的 use() 会通过该 loader 的 loadClass() 取得目标 Class,然后围绕这份 class handle 构造方法 wrapper。Hook 因而落在该命名空间中的真实方法上。

推荐保持默认 factory 不变,为候选 loader 建立独立 factory:

const targetFactory =
    Java.ClassFactory.get(targetLoader);
const Target =
    targetFactory.use('TARGET_CLASS');

const targetMethod = Target.targetMethod.overload();
targetMethod.implementation = function () {
  return targetMethod.call(this);
};

直接替换全局默认值也会改变后续 Java.use() 的查找入口:

Java.classFactory.loader = targetLoader;
const Target = Java.use('TARGET_CLASS');

ClassFactory 会缓存已成功创建的 class wrapper,而 loader setter 本身只更新 _loader,不会清空 _classes。在同一默认 factory 上反复切换 loader,容易把先前缓存的同名 wrapper 与新 loader 混在一起。独立 Java.ClassFactory.get(loader) 的边界更清楚。

从枚举到 Hook 的完整脚本

下面的脚本先要求候选 loader 成功执行 loadClass(),再记录返回 Class 的 defining loader;只有验证通过后才创建 factory 并安装 Hook:

Java.perform(() => {
  const TARGET_CLASS = 'TARGET_CLASS';
  const TARGET_METHOD = 'targetMethod';
  const TARGET_OVERLOAD = ['java.lang.String'];
  const matches = [];

  Java.enumerateClassLoaders({
    onMatch(loader) {
      try {
        const klass = loader.loadClass(TARGET_CLASS);
        const definingLoader = klass.getClassLoader();

        matches.push({
          candidate: String(loader),
          defining: definingLoader === null
            ? '<bootstrap>'
            : String(definingLoader),
          definesClass: definingLoader !== null &&
              definingLoader.equals(loader)
        });

        if (definingLoader === null ||
            !definingLoader.equals(loader)) {
          return;
        }

        const factory =
            Java.ClassFactory.get(loader);
        const Target =
            factory.use(TARGET_CLASS);
        const method =
            Target[TARGET_METHOD]
                .overload(...TARGET_OVERLOAD);

        method.implementation = function (...args) {
          send({
            event: 'target-hit',
            loader: String(loader)
          });
          return method.call(this, ...args);
        };

        send({
          event: 'hook-installed',
          loader: String(loader),
          definingLoader: String(
            Target.class.getClassLoader()
          )
        });
      } catch (e) {
        // 当前 loader 看不到目标类时继续检查下一个。
      }
    },
    onComplete() {
      send({
        event: 'loader-scan-complete',
        matches
      });
    }
  });
});

TARGET_OVERLOAD 必须替换为目标方法的真实参数类型。脚本只在 candidate.equals(definingLoader) 时安装 Hook,因此通过父委派拿到目标 Class 的其他候选 loader只负责留下记录,不会重复改写同一方法。

先判断是不是 loader 问题

现象 结论
默认 Java.use()ClassNotFoundException,某候选 loader.loadClass() 成功 默认 loader 选错或插件 loader 尚未接入
factory 创建成功,Target.class.getClassLoader() 与预期 loader 不同 父委派返回了另一命名空间中的同名类
defining loader 正确,Hook 安装成功但不命中 继续检查 overload、真实调用路径、类是否被替换、JIT/内联;不能继续归因于 loader
枚举中没有目标 loader loader 尚未创建、已被回收,或目标运行在另一个进程

sleep 只改变采样时刻。可靠做法是观察 loader 创建事件,或在确定的业务阶段重新枚举并用 loadClass() 验证。


5. 一次实验验证整条时间线

实验分两部分:主机端主动停在 create、load、resume 三个点;目标脚本记录 top-level、performNow()perform() 和主线程回调。

测试 App 的 Application.onCreate() 需输出一条可识别日志:

Log.i("F02Target", "Application.onCreate");

另开终端执行 adb logcat -s F02Target。这样 resume 前后是否进入 Application 不依赖界面现象判断。

目标脚本

将下面内容保存为 f02_probe.js,把 TARGET_CLASS 替换为测试 App base APK 中已知的类:

const TARGET_CLASS = "TARGET_CLASS";
let seq = 0;

function text(value) {
  return value === null ? null : String(value);
}

function tryClass(name) {
  try {
    Java.use(name);
    return "found";
  } catch (e) {
    return String(e);
  }
}

function findTargetLoaders(name) {
  const matches = [];

  Java.enumerateClassLoaders({
    onMatch(loader) {
      try {
        const klass = loader.loadClass(name);
        const definingLoader = klass.getClassLoader();
        const factory = Java.ClassFactory.get(loader);
        const Target = factory.use(name);

        matches.push({
          candidate: String(loader),
          defining: definingLoader === null
            ? "<bootstrap>"
            : String(definingLoader),
          definesClass: definingLoader !== null &&
              definingLoader.equals(loader),
          wrapperLoader:
              String(Target.class.getClassLoader())
        });
      } catch (e) {
      }
    },
    onComplete() {
    }
  });

  return matches;
}

function mark(stage, extra = {}) {
  send({
    seq: ++seq,
    stage,
    tid: Process.getCurrentThreadId(),
    ...extra
  });
}

mark("top-level");

Java.performNow(() => {
  const ActivityThread = Java.use("android.app.ActivityThread");
  mark("performNow", {
    appReady: ActivityThread.currentApplication() !== null,
    factoryLoader: text(Java.classFactory.loader),
    frameworkClass: tryClass("java.lang.String"),
    targetClass: tryClass(TARGET_CLASS)
  });
});

Java.perform(() => {
  const ActivityThread = Java.use("android.app.ActivityThread");
  const app = ActivityThread.currentApplication();
  const loader = Java.classFactory.loader;

  mark("perform", {
    appReady: app !== null,
    factoryLoader: text(loader),
    targetClass: tryClass(TARGET_CLASS),
    targetLoaders: findTargetLoaders(TARGET_CLASS)
  });

  Java.scheduleOnMainThread(() => {
    mark("main-thread");
  });
});

主机端控制

将下面内容保存为 f02_host.py

import json
import sys
import time

import frida


def on_message(message, data):
    print("SCRIPT", json.dumps(message, ensure_ascii=False))


package = sys.argv[1]
device = frida.get_usb_device(timeout=5)
pid = device.spawn([package])
print(f"HOST spawned pid={pid}")
session = device.attach(pid)

with open("f02_probe.js", "r", encoding="utf-8") as source_file:
    source = source_file.read()

script = session.create_script(source)
script.on("message", on_message)
print("HOST create returned")
time.sleep(1)

script.load()
print("HOST load returned")
time.sleep(1)

device.resume(pid)
print("HOST resume returned")
sys.stdin.read()

执行:

python3 f02_host.py TARGET_PACKAGE

验收看阶段边界,不要求不同机器输出完全相同:

  1. HOST create returned 前后没有脚本消息,证明 create 没有执行 top-level。
  2. load 后出现 stage=top-level,证明用户 JS 由 Script.load() 触发。
  3. resume 前 App 的 Application.onCreate() 尚未执行,证明 JS 线程与被门控的 App 主线程是两条执行线。
  4. performNow 中 framework 类应可用;appReady、默认 loader 和业务类可能尚未就绪。
  5. resume 后 perform 应拿到非空默认 loader,并能解析正确的主包目标类。
  6. targetLoaders 至少记录候选 loader、defining loader 和 factory wrapper 实际使用的 loader。
  7. main-thread 证明显式主线程调度完成;Android App 主线程的 tid 应与主机端打印的 pid 一致。

消息跨线程、队列和控制通道返回,HOST load returned 与相邻 SCRIPT 打印的视觉顺序可能受主机事件循环影响。硬证据是 create 阶段没有任何 top-level 消息,以及 resume 前后 App 主线程行为发生变化。

源码复核

仓库快照保存了 frida-corefrida-gum 以及 Java bridge 的 index.jsvm.jsclass-factory.js,这些文件可直接离线定位。lib/api.jslib/android.js 未收进快照,下面用固定 GitHub 提交在线复核,不把外部源码写成本地已有文件:

cd 检测大全/source/frida

rg -n "create_script|load_script|yield script.load" \
  subprojects/frida-core/src/frida.vala \
  subprojects/frida-core/lib/payload

rg -n "run_in_js_thread|JS_EvalFunction|Evaluate\\(|Run\\(" \
  subprojects/frida-gum/bindings/gumjs

rg -n "_tryInitialize|performNow|_pendingVmOps|enumerateClassLoaders|scheduleOnMainThread" \
  subprojects/frida-java-bridge/index.js

rg -n "this.perform|AttachCurrentThread|tryGetEnvHandle|activeEnvs" \
  subprojects/frida-java-bridge/lib/vm.js

rg -n "static get|set loader|findClass|loadClass" \
  subprojects/frida-java-bridge/lib/class-factory.js

BRIDGE_COMMIT=f72e61ed18fa6b72f0559df223b22a899685b22c
curl -fsSL \
  "9e3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6J5j5i4N6Q4x3X3g2Y4K9i4c8Z5N6h3u0#2M7$3g2J5j5$3!0F1N6r3g2F1N6q4)9J5k6h3y4G2L8g2)9J5c8X3k6J5K9h3c8S2i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3c8B7j5i4k6S2i4K6u0V1j5Y4u0A6k6r3N6W2i4K6u0r3$BRIDGE_COMMIT/lib/api.js" \
  | rg -n "androidGetApi|jvmGetApi|getAndroidVersion"

curl -fsSL \
  "18bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6J5j5i4N6Q4x3X3g2Y4K9i4c8Z5N6h3u0#2M7$3g2J5j5$3!0F1N6r3g2F1N6q4)9J5k6h3y4G2L8g2)9J5c8X3k6J5K9h3c8S2i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3c8B7j5i4k6S2i4K6u0V1j5Y4u0A6k6r3N6W2i4K6u0r3$BRIDGE_COMMIT/lib/android.js" \
  | rg -n "JNI_GetCreatedJavaVMs|withRunnableArtThread|withAllArtThreadsSuspended|makeArtClassLoaderVisitor"

ART_TAG=android-14.0.0_r1
curl -fsSL \
  "e80K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6S2M7Y4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6J5k6h3k6K6i4K6u0r3N6r3q4Y4M7#2)9J5c8R3`.`.$ART_TAG/runtime/class_linker.cc?format=TEXT" \
  | base64 --decode \
  | rg -n "VisitClassLoaders|class_loaders_"

curl -fsSL \
  "998K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6S2M7Y4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6J5k6h3k6K6i4K6u0r3N6r3q4Y4M7#2)9J5c8R3`.`.$ART_TAG/runtime/thread_list.cc?format=TEXT" \
  | base64 --decode \
  | rg -n "ThreadList::SuspendAll|ThreadList::ResumeAll"

curl -fsSL \
  "b68K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6D9K9h3u0F1j5i4c8A6N6X3g2Z5k6h3I4H3k6i4u0Q4x3V1k6Q4x3V1u0Q4x3V1k6J5k6h3k6K6i4K6u0r3N6r3q4Y4M7#2)9J5c8R3`.`.$ART_TAG/include_jni/jni.h?format=TEXT" \
  | base64 --decode \
  | rg -n "struct _JNIEnv|struct _JavaVM|AttachCurrentThread|GetEnv"

6. 故障定位与兼容边界

不要把所有“没输出”都归到注入失败。按最后一个已确认阶段定位:

现象 已确认到哪里 下一处检查
create 抛语法错误 请求已到 backend JavaScript 语法、runtime 选择、完整异常
create 返回但无脚本日志 脚本对象已建立,这是正常状态 是否调用 Script.load()
load 长时间不返回 load 已到 agent 顶层死循环、同步阻塞、runtime 异常、session 状态
top-level 已输出,Hook 不命中 顶层 Hook 注册代码已运行 地址、签名、目标控制流是否经过 Hook 点
top-level 已输出,perform() 不执行 GumJS 正常,App loader 等待未完成 bridge 私有 Hook 与目标 Android 版本是否匹配
vm.getEnv() 报当前线程未附加 JavaVM 已发现,但当前线程没有 JNIEnv 把 JNI 操作放进 Java.perform() / performNow()
framework 类成功,业务类失败 VM/JNIEnv 可用 ClassFactory.loader、类名、base/split APK 归属
主包类成功,插件类失败 默认 LoadedApk loader 正常 插件 loader 是否已创建,使用独立 ClassFactory
loader 枚举期间短暂停顿 已进入 ART SuspendAll → VisitClassLoaders 缩短枚举频率,不在高频回调中反复全量扫描
多个 loader 都能 loadClass() 父委派或同名类并存 比较 Class.getClassLoader(),确认 defining loader
Java 回调执行,UI API 报线程错误 JNI 环境正常 将 UI 操作放入 scheduleOnMainThread()
%resume 后 App 仍停住 脚本阶段可能已完成 回到 F01 检查 zymbiote ACK 与清理

还要保留四条边界:

  • 多进程 App 的每个 PID 都有独立 ActivityThreadLoadedApk 缓存和 ClassLoader 时间线。
  • Java.perform() 只选择主包默认 loader,不保证插件、热更新或加固后的业务 loader 已经出现。
  • native Hook 不依赖 Java VM 或 LoadedApk;它只受目标模块映射与符号时机约束。
  • Java.perform() 解决执行时机、JNIEnv 和默认 loader,不等于 Java 方法替换已经成功;ArtMethod、去优化与 JIT/解释器分派属于 Java Hook 原理课。

课后答疑

一、为什么 --pause 时能看到 top-level,却看不到 Application 日志?

--pause 跳过的是 load 之后的自动 resume()。agent 的 JS scheduler 已经执行脚本,App 主线程仍停在 zymbiote 的门控点。

二、为什么 Java.perform() 已执行,currentApplication() 仍可能为 null?

早期路径在 getPackageInfo() 返回 LoadedApk 后就取得最终 loader,并清空 pending 回调。此时 ClassLoader 可以使用,而 makeApplicationInner() 还没有创建 Application。

三、为什么 java.lang.String 能找到,业务类却找不到?

先检查 Java.classFactory.loader。它为空时,Java.use() 走 JNI FindClass,framework 类可见不代表 APK loader 已建立;它非空时,再检查业务类实际属于 base APK、隔离 split 还是插件 loader。

四、Java.perform() 获取 JNIEnv 时会先暂停全部 Java 线程吗?

不会。固定 bridge 的 VM.perform() 先查当前 tid 的 env 缓存,再调用 JavaVM GetEnv;当前线程未附加时才调用 AttachCurrentThreadThreadList::SuspendAll 出现在 ART 的 enumerateClassLoaders() 路径,用于稳定遍历 ClassLinker::class_loaders_,不属于 JNIEnv 获取流程。

五、为什么切换 ClassFactory.loader 后同一段 Hook 才开始命中?

原 factory 可能通过主包 loader 找不到插件类,或者解析到另一 loader 定义的同名类。切换后 Java.use() 改用目标 loader 的 loadClass(),得到业务实际调用的那份 Class 和方法。工程代码优先使用 Java.ClassFactory.get(loader) 隔离 wrapper 缓存,而不是反复改全局默认 factory。

参考文献

  1. Frida Core 17.9.1 — client 侧 Session.create_script()Script.load()。<b06K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3M7%4u0U0i4K6u0r3k6Y4u0A6k6r3q4Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6t1K6y4K6u0Q4x3X3c8x3x3U0x3&6x3q4)9J5y4X3N6@1i4K6y4n7
  2. Frida Core 17.9.1 — agent 侧 create/load 入口。<bb8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3M7r3q4&6L8r3!0S2k6q4)9J5c8X3u0S2M7$3g2Q4x3X3c8S2k6$3g2F1N6q4)9J5k6s2y4W2M7%4y4A6L8$3&6Q4x3X3g2$3j5h3I4S2i4K6t1K6e0o6p5K6z5q4)9J5k6p5H3I4z5o6m8Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  3. Frida Core 17.9.1 — backend 创建、实例缓存与 ScriptInstance.load() 状态机。<10cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1j5$3!0J5k6g2)9J5c8X3u0D9L8$3u0Q4x3V1j5I4y4#2)9J5k6e0W2Q4x3X3f1I4i4K6u0r3L8r3W2T1i4K6u0r3M7r3q4&6L8r3!0S2k6q4)9J5c8Y4y4U0M7X3W2H3N6q4)9J5k6r3g2F1k6$3W2F1k6g2)9J5k6i4k6S2L8r3q4Q4x3U0y4x3z5e0m8Q4x3X3c8x3x3e0t1J5i4K6t1$3k6%4c8Q4x3@1t1`.
  4. Frida Gum 17.9.1 — QuickJS backend 创建任务与 context。<b90K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6u0r3j5X3I4G2j5W2)9J5c8U0p5%4i4K6u0W2z5g2)9J5k6e0q4Q4x3V1k6T1K9h3&6V1K9h3&6Y4M7#2)9J5c8X3N6#2L8h3A6K6i4K6u0r3k6%4g2E0M7i4g2A6j5$3E0K6j5%4u0A6M7s2c8T1j5h3y4C8k6h3&6V1i4K6u0W2j5#2)9J5x3@1H3$3z5e0N6Q4x3X3c8x3z5o6l9%4i4K6t1$3k6%4c8Q4x3@1t1`.
  5. Frida Gum 17.9.1 — QuickJS load 任务、JS 线程与 JS_EvalFunction()。<27dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6u0r3j5X3I4G2j5W2)9J5c8U0p5%4i4K6u0W2z5g2)9J5k6e0q4Q4x3V1k6T1K9h3&6V1K9h3&6Y4M7#2)9J5c8X3N6#2L8h3A6K6i4K6u0r3k6%4g2E0M7i4g2A6j5$3E0K6j5%4u0A6M7s2c8Q4x3X3g2U0i4K6t1K6e0o6j5J5z5q4)9J5k6p5H3^5x3o6g2Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  6. Frida Gum 17.9.1 — V8 module Evaluate() 与 script Run()。<00cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6u0r3j5X3I4G2j5W2)9J5c8U0p5%4i4K6u0W2z5g2)9J5k6e0q4Q4x3V1k6T1K9h3&6V1K9h3&6Y4M7#2)9J5c8X3N6#2L8h3A6K6i4K6u0r3k6%4g2E0N6U0S2K6j5%4u0A6M7s2c8Q4x3X3g2U0M7s2m8Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  7. Frida Gum 17.9.1gum-js-loop 与 JS main loop。<f5aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1k6%4g2E0i4K6u0r3j5X3I4G2j5W2)9J5c8U0p5%4i4K6u0W2z5g2)9J5k6e0q4Q4x3V1k6T1K9h3&6V1K9h3&6Y4M7#2)9J5c8X3N6#2L8h3A6K6i4K6u0r3k6%4g2E0M7$3y4J5K9i4m8@1M7$3y4Z5k6h3c8#2L8r3g2J5i4K6u0W2j5#2)9J5x3@1H3I4x3e0m8Q4x3X3c8x3x3e0t1^5i4K6t1$3k6%4c8Q4x3@1t1`.
  8. Frida Tools 14.8.0 — spawn、attach、load、resume 与 --pause。<596K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6S2M7s2m8D9K9h3y4S2N6r3W2G2L8W2)9J5k6i4m8&6i4K6t1K6e0o6j5I4y4W2)9J5k6p5H3$3x3U0k6Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1N6r3!0G2L8s2y4Q4x3V1k6T1L8r3!0T1i4K6u0r3x3e0c8Q4x3X3f1^5i4K6u0W2x3q4)9J5c8X3k6J5K9h3c8S2i4K6g2X3N6r3!0G2L8s2y4Q4x3V1k6J5k6i4m8D9i4K6u0W2M7s2W2Q4x3U0y4x3x3U0b7%4i4K6u0V1e0o6x3H3y4W2)9J5y4X3N6@1i4K6y4n7
  9. Frida JavaScript API — Script runtime、消息与下一轮调度。<507K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3M7X3W2V1j5g2)9J5k6i4u0W2i4K6u0r3k6r3!0U0M7#2)9J5c8X3A6S2N6X3q4K6j5%4u0A6M7s2c8Q4x3X3c8S2M7r3W2Q4x3V1k6Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  10. Frida Java Bridge f72e61ed_tryInitialize()perform()performNow() 与 pending VM ops。<a4bK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3K9h3&6V1k6i4S2Q4x3X3g2B7M7#2)9J5y4X3N6@1i4K6y4n7
  11. Frida Java Bridge f72e61ed — JNI thread attach 与 VM.perform()。<1faK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3L8r3W2T1i4K6u0r3N6X3#2Q4x3X3g2B7M7#2)9J5y4X3N6@1i4K6y4n7
  12. Frida Java Bridge f72e61edClassFactory.get(loader)ClassFactory.loader、JNI FindClassloader.loadClass()。<d54K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3L8r3W2T1i4K6u0r3j5$3I4S2M7%4y4Q4x3X3c8X3j5h3y4@1L8%4u0&6i4K6u0W2K9Y4y4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  13. AOSP Android 14 ActivityThread.javamain()bindApplication()getPackageInfo()handleBindApplication()performLaunchActivity()。<723K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6m8j5%4c8A6N6X3W2@1P5g2c8Z5M7X3g2S2k6q4)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  14. AOSP Android 14 LoadedApk.java — 包路径、getClassLoader()、最终 loader 与 makeApplicationInner()。<87cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6x3L8$3q4V1k6h3c8m8M7r3E0Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`.
  15. AOSP Android 14 ApplicationLoaders.java — APK 路径、父加载器与 ClassLoaderFactory。<ba8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6m8M7s2m8D9K9h3y4S2N6r3W2G2L8V1I4G2j5h3c8W2M7Y4y4Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`.
  16. AOSP Android 14 AppComponentFactory.javaInstrumentation.java — 最终 loader、Application 与 Activity 实例化。<07fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6m8M7s2m8o6L8$3#2H3L8$3&6W2L8Y4c8r3j5h3y4@1L8%4u0&6i4K6u0W2K9X3q4$3j5g2)9J5y4X3N6@1i4K6y4n7i4@1g2r3i4@1u0o6i4K6W2n7i4K6t1$3L8s2c8Q4x3@1u0Z5N6s2c8H3M7#2)9K6b7g2)9J5c8W2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3g2Y4L8$3!0Y4L8r3g2K6L8%4g2J5j5$3g2Q4x3X3g2U0L8$3#2Q4x3V1k6H3L8r3q4@1k6X3!0J5L8g2)9J5c8X3k6J5j5h3#2W2N6$3!0J5K9%4y4Q4x3V1k6T1j5i4y4W2i4K6u0r3i4K6u0n7i4K6u0r3y4X3f1@1y4$3x3%4x3o6M7#2j5U0V1I4z5e0R3K6j5h3f1#2x3o6p5I4x3e0b7@1x3U0g2W2j5e0t1#2k6e0k6V1k6U0M7$3z5e0m8U0z5q4)9J5c8X3y4G2M7X3g2Q4x3V1k6B7j5i4k6S2i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8X3q4H3M7q4)9J5c8V1W2F1M7%4c8J5N6h3#2W2L8Y4c8S2N6r3W2G2L8W2)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  17. AOSP Android 14 ContextImpl.javaApplication.javaContextWrapper.java — Context 到 LoadedApk 的 ClassLoader 委托链。<927K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6o6L8$3&6@1k6i4S2@1d9h3#2H3L8q4)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6S2M7s2m8Q4x3V1k6m8M7s2m8D9K9h3y4S2N6r3W2G2L8W2)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6U0L8%4u0W2i4K6u0r3K9X3q4$3j5g2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3V1k6U0L8$3&6@1k6h3&6@1i4K6u0r3b7$3!0F1N6r3g2^5N6q4N6J5j5i4m8H3k6i4u0Q4x3X3g2B7j5i4k6S2i4K6t1$3k6%4c8Q4x3@1t1`.
  18. AOSP Android 14 ActivityManagerService.javaattachApplicationLocked() 向 App 进程发送 bindApplication()。<6c2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6X3M7X3q4E0k6i4N6G2M7X3E0K6i4K6u0r3j5X3q4K6k6g2)9J5c8W2)9J5b7W2)9J5c8U0k6W2y4o6N6U0y4K6l9%4y4h3t1&6x3e0V1^5x3$3q4W2y4e0l9I4x3e0p5@1y4o6t1#2k6h3p5J5y4h3f1$3k6r3j5%4y4U0V1H3j5K6S2Q4x3V1k6K6k6i4u0$3K9h3y4W2M7#2)9J5c8X3y4G2M7X3g2Q4x3V1k6B7j5i4k6S2i4K6u0r3j5$3!0E0i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5c8Y4y4W2M7Y4k6W2M7W2)9J5c8X3q4E0i4K6u0r3b7h3y4@1K9i4k6A6N6s2W2y4j5h3&6S2k6$3g2J5f1$3g2J5N6X3W2U0k6g2)9J5k6h3A6S2N6X3q4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  19. Android Developers — JNI Tips:JNIEnv 的线程局部语义与附加原生线程中的 FindClass。<6c3K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3N6s2u0S2K9h3&6A6L8X3N6Q4x3V1k6S2M7Y4c8A6j5$3I4W2M7#2)9J5c8Y4m8W2M7X3k6Q4x3X3c8B7L8X3W2Q4x3U0y4X3j5i4q4Q4y4h3k6r3K9h3&6V1b7$3I4S2M7%4y4Q4x3U0k6Y4N6q4)9K6b7R3`.`.
  20. Frida Java Bridge f72e61edlib/api.jslib/android.js:运行时选择、JNI_GetCreatedJavaVMs、ART 线程状态切换、SuspendAll / ResumeAll 与 native visitor。<d19K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3L8r3W2T1i4K6u0r3j5i4m8A6i4K6u0W2K9Y4y4Q4x3U0k6Y4N6q4)9K6b7W2!0q4c8W2!0n7b7#2)9&6b7W2)9J5y4X3I4@1i4K6y4n7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3L8r3W2T1i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6h3A6K6i4K6t1$3k6%4c8Q4x3@1t1`.
  21. Frida Java Bridge f72e61edindex.jsenumerateClassLoaders() 的 VM 分派与 ART VisitClassLoaders 实现。<388K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3M7X3W2V1j5g2)9J5c8X3k6J5K9h3c8S2i4K6u0V1K9X3q4$3j5g2)9J5k6r3u0J5K9h3c8Y4k6g2)9J5c8X3u0D9L8$3u0Q4x3V1k6X3y4K6u0W2y4U0q4W2k6o6p5^5k6X3p5$3j5U0M7J5k6U0l9#2y4e0W2V1k6U0t1J5x3$3t1J5x3X3p5^5z5e0V1$3z5o6g2T1x3U0u0U0i4K6u0r3K9h3&6V1k6i4S2Q4x3X3g2B7M7#2)9J5x3@1H3I4y4e0y4Q4x3X3c8x3x3U0M7J5i4K6t1$3k6%4c8Q4x3@1t1`.
  22. AOSP libnativehelper android-14.0.0_r1jni.hJNIEnvJavaVMJNIInvokeInterface。<1c7K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6D9K9h3u0F1j5i4c8A6N6X3g2Z5k6h3I4H3k6i4u0Q4x3V1k6Q4x3V1u0Q4x3V1k6J5k6h3k6K6i4K6u0r3N6r3q4Y4M7#2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7I4y4q4)9J5k6e0m8Q4x3X3f1H3i4K6g2X3M7U0q4Q4x3V1k6A6L8X3y4D9N6h3c8W2i4K6g2X3K9X3&6A6i4K6u0r3K9X3&6A6i4K6u0W2K9q4)9J5y4X3N6@1i4K6y4n7
  23. AOSP ART android-14.0.0_r1ClassLinker::VisitClassLoaders()ThreadList::SuspendAll()ResumeAll()。<5bbK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0W2k6$3!0G2k6$3I4W2M7$3!0#2M7X3y4W2i4K6u0W2j5$3!0E0i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6S2M7Y4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6J5k6h3k6K6i4K6u0r3N6r3q4Y4M7#2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3b7I4y4q4)9J5k6e0m8Q4x3X3f1H3i4K6g2X3M7U0q4Q4x3V1k6J5N6h3&6@1K9h3#2W2i4K6u0r3j5$3I4S2M7%4y4Q4y4h3k6D9K9h3&6C8k6i4u0Q4x3X3g2U0j5#2)9J5x3K6p5H3y4K6t1%4i4K6t1$3k6%4c8Q4x3@1u0Q4c8f1k6Q4b7V1y4Q4z5f1u0Q4x3U0k6D9N6q4)9K6b7X3S2@1N6s2m8K6i4K6y4m8i4K6u0r3i4K6u0r3j5h3&6V1M7X3!0A6k6q4)9J5k6h3N6G2L8$3N6D9k6i4y4G2N6i4u0U0k6g2)9J5k6h3y4G2L8g2)9J5c8Y4m8D9j5i4c8X3L8%4u0E0i4K6u0r3j5i4u0@1i4K6u0r3i4K6u0n7i4K6u0r3M7X3g2X3M7#2)9J5c8Y4c8S2k6%4y4Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0c8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1I4i4K6u0r3M7Y4g2F1N6r3W2E0k6g2)9J5c8Y4c8Z5M7X3g2S2k6q4)9#2k6X3I4A6M7%4c8Q4x3X3g2U0j5#2)9J5x3K6j5J5y4#2)9J5y4X3N6@1i4K6y4n7

传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 11
打赏
分享
最新回复 (5)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
666
7小时前
0
雪    币: 51
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
tql
7小时前
0
雪    币: 195
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
太强了
7小时前
0
雪    币: 530
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
小佳我的神
6小时前
0
雪    币: 217
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
0d5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2T1K9h3I4A6j5X3W2D9K9g2)9J5k6h3y4G2L8g2)9J5c8Y4k6A6k6r3g2G2i4K6u0r3b7W2j5I4y4W2c8T1N6K6j5%4c8i4S2i4i4K6u0r3i4K6y4r3N6X3c8Q4y4h3k6K6L8%4g2J5j5$3g2Q4x3@1c8V1j5K6M7^5z5e0x3I4y4$3g2X3z5o6m8W2x3r3f1I4x3r3p5J5y4X3p5K6y4o6q4U0j5$3p5J5j5e0W2U0x3b7`.`.
视频教程
6小时前
0
游客
登录 | 注册 方可回帖
返回