前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
原理
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加载动态库的逻辑,我贴几个相关的地方和函数的链接,感兴趣可以自己去分析一下,就不详细说了。
- JavaVMExt::LoadNativeLibrary
- JavaVMExt::LoadNativeLibrary::needs_native_bridge
- OpenNativeLibrary
- OpenNativeLibrary::is_bridged
- NativeBridgeIsPathSupported
- SharedLibrary::FindSymbol
- 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
,它表示了当前的运行时架构,比如arm64
,x86_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路径就可以实现了。
结语
在线搜索找代码还是有点蛋疼的,好在引用功能做的好。
那就这样了,有缘再见~