前言

本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。

原理

app_process部分启动流程

启动流程

这里我们重点关注ParsedOptions这个处理启动参数的函数,基本上能调整的参数都在这里。

我们往下找,发现有两个与NativeBridge相关的参数,一个是-XX:NativeBridge,一个是-Xforce-nb-testing

.Define("-XX:NativeBridge=_")
    .WithType<std::string>()
    .IntoKey(M::NativeBridge)
.Define("-Xzygote-max-boot-retry=_")
    .WithType<unsigned int>()
    .IntoKey(M::ZygoteMaxFailedBoots)
.Define("-Xno-sig-chain")
    .IntoKey(M::NoSigChain)
.Define("--cpu-abilist=_")
    .WithType<std::string>()
    .IntoKey(M::CpuAbiList)
.Define("-Xfingerprint:_")
    .WithType<std::string>()
    .IntoKey(M::Fingerprint)
.Define("-Xexperimental:_")
    .WithType<ExperimentalFlags>()
    .AppendValues()
    .IntoKey(M::Experimental)
.Define("-Xforce-nb-testing")
    .IntoKey(M::ForceNativeBridge)

-XX:NativeBridge是指定NativeBridge Provider的路径,-Xforce-nb-testing是强行开启NativeBridge支持的关键参数,这个参数本来是Google用来测试的,它可以让-XX:NativeBridge参数在非Zygote进程中启用。

如果没有-Xforce-nb-testing这个参数,-XX:NativeBridge参数在非Zygote进程中会被加载一次,然后再卸载掉,然后就没有后续了。

所以如果哪天Google要是移除了-Xforce-nb-testing这个参数那么这个方法就失效了。

至于为何设置了-XX:NativeBridge参数就能支持NativeBridge,这个涉及到JavaVM加载动态库的逻辑,我贴几个相关的地方和函数的链接,感兴趣可以自己去分析一下,就不详细说了。

  1. JavaVMExt::LoadNativeLibrary
  2. JavaVMExt::LoadNativeLibrary::needs_native_bridge
  3. OpenNativeLibrary
  4. OpenNativeLibrary::is_bridged
  5. NativeBridgeIsPathSupported
  6. SharedLibrary::FindSymbol
  7. SharedLibrary::NeedsNativeBridge

代码分析

我们先来看-XX:NativeBridge参数的默认来源AndroidRuntime::startVm

// Native bridge library. "0" means that native bridge is disabled.
//
// Note: bridging is only enabled for the zygote. Other runs of
//       app_process may not have the permissions to mount etc.
property_get("ro.dalvik.vm.native.bridge", propBuf, "");
if (propBuf[0] == '\0') {
    ALOGW("ro.dalvik.vm.native.bridge is not expected to be empty");
} else if (zygote && strcmp(propBuf, "0") != 0) {
    snprintf(nativeBridgeLibrary, sizeof("-XX:NativeBridge=") + PROPERTY_VALUE_MAX,
                "-XX:NativeBridge=%s", propBuf);
    addOption(nativeBridgeLibrary);
}

可以看到ART默认就会读取系统属性ro.dalvik.vm.native.bridge来开启对NativeBridge的支持,但是仅限Zygote进程,非Zygote进程是没有这个参数的。

而我们普通的APP都是从Zygote进程fork出来的,所以只要你的机器有设置ro.dalvik.vm.native.bridge属性,那么你的APP进程就可以支持NativeBridge。

所以我们单独启动的进程就需要手动的加上相应的启动参数。

我们在Runtime::Init函数中可以看到有这么一段代码

{
    std::string native_bridge_file_name = runtime_options.ReleaseOrDefault(Opt::NativeBridge);
    is_native_bridge_loaded_ = LoadNativeBridge(native_bridge_file_name);
}

这里会尝试加载-XX:NativeBridge参数指定的NativeBridge库,如果加载成功则is_native_bridge_loaded_true,没指定参数或加载失败则is_native_bridge_loaded_false

再来看Runtime::Start函数中有这么一段代码

if (!is_zygote_) {
    if (is_native_bridge_loaded_) {
        PreInitializeNativeBridge(".");
    }
    NativeBridgeAction action = force_native_bridge_
        ? NativeBridgeAction::kInitialize
        : NativeBridgeAction::kUnload;
    InitNonZygoteOrPostFork(self->GetJniEnv(),
                            /* is_system_server= */ false,
                            /* is_child_zygote= */ false,
                            action,
                            GetInstructionSetString(kRuntimeISA));
}

这里的is_native_bridge_loaded_就是前面尝试加载NativeBridge库的结果。

force_native_bridge_true对应设置了-Xforce-nb-testing参数,没设置则为fasle

这里还有个很重要的参数kRuntimeISA,它表示了当前的运行时架构,比如arm64x86_64等。

非常蛋疼的是这个参数是一个编译时常量,没法修改,但是它又涉及到关键的加载逻辑,所以后面我们需要特殊方法来绕过它,相关代码在instruction_set.h中,这里我把代码贴出来

#if defined(__arm__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kArm;
#elif defined(__aarch64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kArm64;
#elif defined (__riscv)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kRiscv64;
#elif defined(__i386__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kX86;
#elif defined(__x86_64__)
static constexpr InstructionSet kRuntimeISA = InstructionSet::kX86_64;
#else
static constexpr InstructionSet kRuntimeISA = InstructionSet::kNone;
#endif

PreInitializeNativeBridge为初始化一些信息的函数,作用是格式化code_cache的路径到app_code_cache_dir变量中和挂载一些信息到cpuinfo,参数是code_cache文件夹的路径,可能会用于保存转译后的东西(保不保存看系统或者参数)。

InitNonZygoteOrPostFork为初始化NativeBridge的函数,我们重点关注这段代码

if (is_native_bridge_loaded_) {
    switch (action) {
        case NativeBridgeAction::kUnload:
            UnloadNativeBridge();
            is_native_bridge_loaded_ = false;
            break;
        case NativeBridgeAction::kInitialize:
            InitializeNativeBridge(env, isa);
            break;
    }
}

其中action对应前面的force_native_bridge_判断的结果,如果我们没有设置-Xforce-nb-testing参数的话这里会是kUnload,直接将加载的NativeBridge卸载掉,反之就开始初始化。

绕过编译时常量ISA

前面说过kRuntimeISA是一个编译时常量,它涉及到初始化NativeBridge的目标转译架构,虽然我们没法修改它(不考虑做动态Patch的情况下),但是NativeBridge Provider是按我们指定的so路径来加载的,所以我们可以用劫持so的思路来改掉isa参数。

对应的是NativeBridgeCallbacks::initialize函数,我们在劫持的so中将传入的instruction_set参数改成我们需要转译的目标平台再调用原函数即可,例如在模拟器(x86)中转译arm64的so就是

bool android::native_bridge::v1::Initialize(const android::NativeBridgeRuntimeCallbacks *androidRuntimeCallbacks, const char *appCodeCacheDir, const char *isa)
{
    auto callbacks = detail::GetCallbacksInternal();

    isa = "arm64";

    return callbacks->initialize(androidRuntimeCallbacks, appCodeCacheDir, isa);
}

然后-XX:NativeBridge参数改为劫持的so路径就可以实现了。

结语

在线搜索找代码还是有点蛋疼的,好在引用功能做的好。

那就这样了,有缘再见~