在日常的 Android 应用安全分析中,经常会遇到一些对抗,比如目标应用加壳、混淆、加固,需要进行脱壳还原;又或者会有针对常用注入工具的检测,比如 frida、Xposed 等,这时候也会想知道这些工具的核心原理以及是否自己可以实现。
其实这些问题的答案就在 Android 的 Java 虚拟机实现中。可以是早期的 Dalvik 虚拟机,也可以是最新的 ART 虚拟机。从时代潮流来看,本文主要专注于 ART。不过,为了铭记历史,也会对 Dalvik 虚拟机做一个简单的介绍。最后会从 ART 的实现出发j对一些实际的应用场景进行讨论。
注: 本文分析基于 AOSP android-12.0.0_r11
Java VM
我们知道,Java 是一门跨平台的语言,系统实际运行的是 Java 字节码,由 Java 虚拟机去解释执行。如果读者之前看过 如何破解一个Python虚拟机壳并拿走12300元ETH 一文或者对 Python 虚拟机有所了解的话就会知道,解释执行的过程可以看做是一个循环,对每条指令进行解析,并针对指令的名称通过巨大的 switch-case 分发到不同的分支中处理。其实 Java 虚拟机也是类似的,但 JVM 对于性能做了很多优化,比如 JIT 运行时将字节码优化成对应平台的二进制代码,提高后续运行速度等。
Android 代码既然是用 Java 代码编写的,那么运行时应该也会有一个解析字节码的虚拟机。和标准的 JVM 不同,Android 中实际会将 Java 代码编译为 Dalvik 字节码,运行时解析的也是用自研的虚拟机实现。之所以使用自研实现,也许一方面有商业版权的考虑,另一方面也确实是适应了移动端的的运行场景。Dalvik 指令基于寄存器,占 1-2 字节,Java 虚拟机指令基于栈,每条指令只占 1 字节;因此 Dalvik 虚拟机用空间换时间从而获得比 Oracle JVM 更快的执行速度。
启动
其实 Java 代码执行并不慢,但其启动时间却是一大瓶颈。如果每个 APP 运行都要启动并初始化 Java 虚拟机,那延时将是无法接受的。在 Android 12 应用启动流程分析 一文中我们说到,APP 应用进程实际上是通过 zygote 进程 fork 出来的。这样的好处是子进程继承了父进程的进程空间,对于只读部分可以直接使用,而数据段也可以通过 COW(Copy On Write) 进行延时映射。查看 zygote 与其子进程的 /proc/self/maps
可以发现大部分系统库的映射都是相同的,这就是 fork 所带来的好处。
在 Android 用户态启动流程分析 中我们分析了 init、zygote 和 system_server 的启动流程,其中在介绍 zygote 的启动流程时说到这是个 native 程序,在其中 main 函数的结尾有这么一段代码:
代码语言:javascript复制int main(int argc, char* const argv[]) {
// ...
if (zygote) {
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
} else if (className) {
runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
} else {
fprintf(stderr, "Error: no class name or --zygote supplied.n");
// ....
}
}
上述代码在 frameworks/base/cmds/app_process/app_main.cpp 中,runtime.start 的作用就是启动 Java 虚拟机并将执行流转交给对应的 Java 函数。
代码语言:javascript复制void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android nativesn");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jclass startClass = env->FindClass(slashClassName);
jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}
详细介绍可以会看 Android 用户态启动流程分析 一文,这里我们只需要知道 Java 虚拟机是在 Zygote 进程创建的,并由子进程继承,因此 APP 从 zygote 进程中 fork 启动后就无需再次启动 Java 虚拟机,而是复用原有的虚拟机执行轻量的初始化即可。
接口
Android Java 虚拟机包括早期的 Dalvik 虚拟机和当前的 ART 虚拟机,我们将其统称为 Java 虚拟机,因为对于应用程序而言应该是透明的,也就是说二者应该提供了统一的对外接口。
这个接口可以分为两部分,一部分是提供给 Java 应用的接口,即我们常见的 JavaVM、JNIEnv 结构体提供的诸如 FindClass、GetMethodID、CallVoidMethod 等接口;另一部分则是提供给系统开发者的接口,系统通过这些接口去初始化并创建虚拟机,从而使自身具备执行 Java 代码的功能。
JniInvocation.Init 方法中即进行了第二部分接口的初始化操作,其中主要逻辑是根据系统属性 (persist.sys.dalvik.vm.lib.2) 判断待加载的虚拟机动态库,Dalvik 虚拟机对应的是 libdvm.so,ART 虚拟机对应的是 libart.so;然后通过 dlopen 进行加载,并通过 dlsym 获取其中三个函数符号,作为抽象 Java 虚拟机的接口:
- JNI_GetDefaultJavaVMInitArgs: 获取默认的 JVM 初始化参数;
- JNI_CreateJavaVM: 创建 Java 虚拟机;
- JNI_GetCreatedJavaVMs: 获取已经创建的 Java 虚拟机实例;
例如,在上述 zygote 的 AndroidRuntime::startVm
方法实现中,就是通过指定参数最终调用 JNI_CreateJavaVM 来完成 Java 虚拟机的创建工作。
通过这三个接口实现了对于不同 Java 虚拟机细节的隐藏,既可以用 ART 无缝替换 Dalvik 虚拟机,也可以在未来用某个新的虚拟机无缝替换掉 ART 虚拟机。
总的来说,Java 虚拟机只在 Zygote 进程中创建一次,子进程通过 fork 获得虚拟机的一个副本,因此 zygote 才被称为所有 Java 进程的父进程;同时,也因为每个子进程拥有独立的虚拟机副本,所以某个进程的虚拟机崩溃后不影响其他进程,从而实现安全的运行时隔离。
Dalvik
Dalvik 是早期 Android 的 Java 虚拟机,伴随着 Android 5.0 的更新,正式宣告其历史使命的结束:
代码语言:javascript复制commit 870b4f2d70d67d6dbb7d0881d101c61bed8caad2
Author: Brian Carlstrom <bdc@google.com>
Date: Tue Aug 5 12:46:17 2014 -0700
Dalvik is dead, long live Dalvik!
虽然现在 Dalvik 已经被 ART 虚拟机所取代,但其简洁的实现有助于我们理解 Java 代码的运行流程,因此还是先对其进行简单的介绍。
上节中我们知道 zygote 进程创建并初始化 Java 虚拟机后执行的第一个 Java 函数是 com.android.internal.os.ZygoteInit 的 main 方法,这是个静态方法,因此在 Native 层调用的是 JNI 接口函数 CallStaticVoidMethod。其调用流程可以简化如下所示:
method | file |
---|---|
JNIEnv.CallStaticVoidMethod | dalvik/libnativehelper/include/nativehelper/jni.h |
JNINativeInterface.CallStaticVoidMethodV | dalvik/vm/Jni.c |
Jni.dvmCallMethodV | dalvik/vm/interp/Stack.c |
Stack.dvmInterpret | dalvik/vm/interp/Interp.c |
dvmInterpretStd | dalvik/vm/mterp/out/InterpC-portstd.c (动态生成) |
Dalvik 虚拟机支持三种执行模式,分别是:
- kExecutionModeInterpPortable: 可移植模式,能运行在不同的平台中,对应的运行方法是 dvmInterpretStd;
- kExecutionModeInterpFast: 快速模式,针对特定平台优化,对应的运行方法是 dvmMterpStd;
- kExecutionModeJit: JIT 模式,运行时编译为特定平台的 native 代码,对应运行方法也是 dvmMterpStd;
以上述调用流程中的 portable 模式为例,对应 dvmInterpretStd 实现的核心代码如下所示:
代码语言:javascript复制#define INTERP_FUNC_NAME dvmInterpretStd
bool INTERP_FUNC_NAME(Thread* self, InterpState* interpState) {
// ...
/* core state */
const Method* curMethod; // method we're interpreting
const u2* pc; // program counter
u4* fp; // frame pointer
u2 inst; // current instruction
/* copy state in */
curMethod = interpState->method;
pc = interpState->pc;
fp = interpState->fp;
retval = interpState->retval; /* only need for kInterpEntryReturn? */
methodClassDex = curMethod->clazz->pDvmDex;
while (1) {
/* fetch the next 16 bits from the instruction stream */
inst = FETCH(0);
switch (INST_INST(inst)) {
HANDLE_OPCODE(OP_INVOKE_DIRECT /*vB, {vD, vE, vF, vG, vA}, meth@CCCC*/)
GOTO_invoke(invokeDirect, false);
OP_END
HANDLE_OPCODE(OP_RETURN /*vAA*/)
HANDLE_OPCODE(...)
}
}
/* export state changes */
interpState->method = curMethod;
interpState->pc = pc;
interpState->fp = fp;
/* debugTrackedRefStart doesn't change */
interpState->retval = retval; /* need for _entryPoint=ret */
interpState->nextMode =
(INTERP_TYPE == INTERP_STD) ? INTERP_DBG : INTERP_STD;
return true;
}
可以看到其核心在于一个巨大的 switch/case,以 PC 为起点不断读取字节码(4字节对齐),并根据 op_code 去分发解释执行不同的指令直到 Java 方法运行结束返回或者抛出异常。之所以称为可移植模式(portable)正是因为该代码纯粹是解释执行,既没有提前优化也没有运行时的 JIT 优化,也因此具有平台无关性,只要 C 编译器支持对应平台即可运行。
虽然 Dalvik 已经被 ART 取代,但其中的 Dalvik 字节码格式还是被保留了下来。即便在最新版本的 Android 中,编译 Java 生成的依旧是 DEX 文件,其格式可以参考 Dalvik Executable format,Dalvik 字节码的介绍可以参考官方文档 Dalvik bytecode。
ART
ART 全称为 Android Runtime,是继 Dalvik 之后推出的高性能 Android Java 虚拟机。在本文中我们重点关注 ART 虚拟机执行 Java 代码的流程。在介绍 ART 的代码执行流程之前,我们需要先了解在 ART 中针对 DEX 的一系列提前优化方案,以及由此产生的各类中间文件。
提前优化
在我们使用 Android-Studio 编译应用时,实际上是通过 Java 编译器先将 .java
代码编译为对应的 Java 字节码,即 .class
类文件;然后用 dx
(在新版本中是d8
) 将 Java 字节码转换为 Dalvik 字节码,并将所有生成的类打包到统一的 DEX 文件中,最终和资源文件一起 zip 压缩为 .apk
文件。
在安装用户的 APK 时,Android 系统主要通过 PacketManager 对应用进行解包和安装。其中在处理 DEX 文件时候,会通过 installd 进程调用对应的二进制程序对字节码进行优化,这对于 Dalvik 虚拟机而言使用的是 dexopt 程序,而 ART 中使用的是 dex2oat 程序。
dexopt 将 dex 文件优化为 odex 文件,即 optimized-dex 的缩写,其中包含的是优化后的 Dalvik 字节码,称为 quickend dex;dex2oat 基于 LLVM,优化后生成的是对应平台的二进制代码,以 oat 格式保存,oat 的全称为 Ahead-Of-Time。oat 文件实际上是以 ELF 格式进行存储的,并在其中 oatdata 段(section) 包含了原始的 DEX 内容。
在 Android 8 之后,将 OAT 文件一分为二,原 oat 仍然是 ELF 格式,但原始 DEX 文件内容被保存到了 VDEX 中,VDEX 有其独立的文件格式。整体流程如下图所示:
LIEF Documentation - Android formats
值得一提的是,在 Andorid 系统中 dex2oat 会将优化后的代码保存在 /data/app
对应的应用路径下,系统应用会保存在 /data/dalvik-cache/
下,对于后者,产生的实际有三个文件,比如:
$ ls -l | grep Settings.apk
-rw-r----- 1 system system 77824 2021-12-10 10:33 system_ext@priv-app@Settings@Settings.apk@classes.art
-rw-r----- 1 system system 192280 2021-11-19 12:50 system_ext@priv-app@Settings@Settings.apk@classes.dex
-rw-r----- 1 system system 59646 2021-12-10 10:33 system_ext@priv-app@Settings@Settings.apk@classes.vdex
system_ext@priv-app@Settings@Settings.apk@classes.dex
实际上是 ELF 格式的 OAT 文件,所以我们不能以貌(后缀)取人;.art
也是一个特殊的文件格式,如前文所言,Android 实现了自己的 Java 虚拟机,这个虚拟机本身是用 C/C 实现的,其中的一些 Java 原语有对应的 C 类,比如:
- java.lang.Class 对应 art::mirror::Class
- java.lang.String 对应 art::mirror::String
- java.lang.reflect.Method 对应 art::mirror::Method
- ……
当创建一个 Java 对象时,内存中会创建对应的 C 对象并调用其构造函数,JVM 管理者这些 C 对象的引用。为了加速启动过程,避免对这些常见类的初始化,Android 使用了 .art
格式来保存这些 C 对象的实例,简单来说,art 文件可以看做是一系列常用 C 对象的内存 dump。
不论是 oat、vdex 还是 art,都是 Android 定义的内部文件格式,官方并不保证其兼容性,事实上在 Android 各个版本中这些文件格式都有不同程度的变化,这些变化是不反映在文档中的,只能通过代码去一窥究竟。因此对于这些文件格式我们现在只需要知道其大致作用,无需关心其实现细节。
文件加载
在前一篇文章 (Android 12 应用启动流程分析) 中我们知道 APP 最终在 ActivityThread 中完成 Application 的创建和初始化,最终调用 Activity.onCreate 进入视图组件的生命周期。但这里其实忽略了一个问题: APP 的代码(DEX/OAT 文件) 是如何加载到进程中的?
在 Java 中负责加载指定类的对象是 ClassLoader,Android 中也是类似,BaseDexClassLoader 继承自 ClassLoader 类,实现了许多 DEX 相关的加载操作,其子类包括:
- DexClassLoader: 负责从
.jar
或者.apk
中加载类; - PathClassLoader: 负责从本地文件中初始化类加载器;
- InMemoryDexClassLoader: 从内存中初始化类加载器;
ClassLoader
以常见的 PathClassLoader
为例,其构造函数会调用父类的构造函数,整体调用链路简化如下表:
method | file |
---|---|
new PathClassLoader | … |
new BaseDexClassLoader | libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java |
new DexPathList | libcore/dalvik/src/main/java/dalvik/system/DexPathList.java |
DexPathList.makeDexElements | … |
DexPathList.loadDexFile | … |
new DexFile | libcore/dalvik/src/main/java/dalvik/system/DexFile.java |
DexFile.openDexFile | … |
DexFile.openDexFileNative | … |
DexFile_openDexFileNative | art/runtime/native/dalvik_system_DexFile.cc |
OatFileManager::OpenDexFilesFromOat | art/runtime/oat_file_manager.cc |
在 OpenDexFilesFromOat 中执行了真正的代码加载工作,伪代码如下:
代码语言:javascript复制std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat() {
std::vector<std::unique_ptr<const DexFile>> dex_files = OpenDexFilesFromOat_Impl(...);
for (std::unique_ptr<const DexFile>& dex_file : dex_files) {
if (!dex_file->DisableWrite()) {
error_msgs->push_back("Failed to make dex file " dex_file->GetLocation() " read-only");
}
}
return dex_files;
}
通过 OpenDexFilesFromOat_Impl 加载获取 DexFile 结构体数组,值得注意的是加载完 DEX 之后会将内存中的 dex_file 设置为不可写,当然目前还没有强制,但可见这是未来的趋势。
继续看实现部分是如何加载 Dex 文件的:
代码语言:javascript复制std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat_Impl() {
// Extract dex file headers from `dex_mem_maps`.
const std::vector<const DexFile::Header*> dex_headers = GetDexFileHeaders(dex_mem_maps);
// Determine dex/vdex locations and the combined location checksum.
std::string dex_location;
std::string vdex_path;
bool has_vdex = OatFileAssistant::AnonymousDexVdexLocation(dex_headers,
kRuntimeISA,
&dex_location,
&vdex_path);
if (has_vdex && OS::FileExists(vdex_path.c_str())) {
vdex_file = VdexFile::Open(vdex_path,
/* writable= */ false,
/* low_4gb= */ false,
/* unquicken= */ false,
&error_msg);
}
// Load dex files. Skip structural dex file verification if vdex was found
// and dex checksums matched.
std::vector<std::unique_ptr<const DexFile>> dex_files;
for (size_t i = 0; i < dex_mem_maps.size(); i) {
static constexpr bool kVerifyChecksum = true;
const ArtDexFileLoader dex_file_loader;
std::unique_ptr<const DexFile> dex_file(dex_file_loader.Open(
DexFileLoader::GetMultiDexLocation(i, dex_location.c_str()),
dex_headers[i]->checksum_,
std::move(dex_mem_maps[i]),
/* verify= */ (vdex_file == nullptr) && Runtime::Current()->IsVerificationEnabled(),
kVerifyChecksum,
&error_msg));
if (dex_file != nullptr) {
dex::tracking::RegisterDexFile(dex_file.get()); // Register for tracking.
dex_files.push_back(std::move(dex_file));
}
}
// Initialize an OatFile instance backed by the loaded vdex.
std::unique_ptr<OatFile> oat_file(OatFile::OpenFromVdex(
MakeNonOwningPointerVector(dex_files),
std::move(vdex_file),
dex_location));
if (oat_file != nullptr) {
VLOG(class_linker) << "Registering " << oat_file->GetLocation();
*out_oat_file = RegisterOatFile(std::move(oat_file));
}
return dex_files;
}
加载过程首先将 vdex 映射到内存中,然后将已经映射到内存中的 dex 或者在磁盘中的 dex 转换为 DexFile 结构体,最后再将 vdex 和 oat 文件关联起来。
VdexFile
vdex 是 Android 8.0 加入的新文件格式,主要用于保存优化代码的原始 DEX 信息,而 OAT 中则主要保存 dex2oat 编译后的 Native 代码。
VdexFile 的结构大致如下所示,其中 D
代表 VDEX 中包含的 DEX 文件个数:
VdexFileHeader fixed-length header
VdexSectionHeader[kNumberOfSections]
Checksum section
VdexChecksum[D]
Optionally:
DexSection
DEX[0] array of the input DEX files
DEX[1]
...
DEX[D-1]
VerifierDeps
4-byte alignment
uint32[D] DexFileDeps offsets for each dex file
DexFileDeps[D][] verification dependencies
4-byte alignment
uint32[class_def_size] TypeAssignability offsets (kNotVerifiedMarker for a class that isn't verified)
uint32 Offset of end of AssignabilityType sets
uint8[] AssignabilityType sets
4-byte alignment
uint32 Number of strings
uint32[] String data offsets for each string
uint8[] String data
VdexFile 结构详见: art/runtime/vdex_file.h
DexFile
dex_file_loader.Open 的调用路径如下:
- ArtDexFileLoader::Open
- ArtDexFileLoader::OpenCommon
- DexFileLoader::OpenCommon
- magic == “dexn” -> new StandardDexFile()
- magic == “cdex” -> new CompactDexFile()
实际根据起始 4 字节判断是标准 DEX 还是紧凑型 DEX (cdex, compat dex),并使用对应的结构体进行初始化。cdex 是当前 ART 内部使用的 DEX 文件格式,主要是为了减少磁盘和内存的使用。但不论是 StandardDexFile 还是 CompactDexFile 都继承于 DexFile,二者的构造函数最终还是会调用 DexFile 的构造函数。
DexFile 结构详见 art/libdexfile/dex/dex_file.h
OatFile
在完成所有 DexFile 的初始化之后,会继续使用 OatFile::OpenFromVdex
创建 oat_file 并进行注册。该函数的调用链路如下:
- OatFile::OpenFromVdex
- OatFileBackedByVdex::Open
- new OatFileBackedByVdex
- OatFileBase::OatFileBase
- OatFile::OatFile
与早期使用 odex 的区别是现在在创建完 OatFile 之后,会调用 oat_file->SetVdex
获取 vdex 对象的所有权,用以实现 OAT 的部分接口,比如获取内存中对应 DEX 文件的起始地址:
const uint8_t* OatFile::DexBegin() const {
return vdex_->Begin();
}
详见: art/runtime/oat_file.h
方法调用
本来按照时间线来看的话,这里应该先介绍 ART 运行时类和方法的加载过程,但我从实践出发,先看 Java 方法的调用过程,并针对其中涉及到的概念在下一节继续介绍。
在 Web 安全中,Java 服务端通常带有一个称为 RASP (Runtime Application Self-Protection) 的动态防护方案,比如监控某些执行命令的敏感函数调用并进行告警,其实际 hook 点是在 JVM 中,不论是方法直接调用还是反射调用都可以检测到。因此我们有理由猜测在 Android 中也有类似的调用链路,为了方便观察,这里先看反射调用的场景,一般反射调用的示例如下:
代码语言:javascript复制import java.lang.reflect.*;
public class Test {
public static void main(String args[]) throws Exception {
Class c = Class.forName("com.evilpan.DemoClass");
Method m = c.getMethod("foo", null);
m.invoke();
}
}
因此一个方法的调用会进入到 Method.invoke
方法,这是一个 native 方法,实际实现在 art/runtime/native/java_lang_reflect_Method.cc:
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
jobjectArray javaArgs) {
ScopedFastNativeObjectAccess soa(env);
return InvokeMethod<kRuntimePointerSize>(soa, javaMethod, javaReceiver, javaArgs);
}
InvokeMethod 定义在 art/runtime/reflection.cc,其实现的核心代码如下:
代码语言:javascript复制template <PointerSize kPointerSize>
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
jobject javaReceiver, jobject javaArgs, size_t num_frames) {
ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod();
if (UNLIKELY(!declaring_class->IsVisiblyInitialized())) {
Thread* self = soa.Self();
Runtime::Current()->GetClassLinker()->EnsureInitialized(
self, h_class,
/*can_init_fields=*/ true,
/*can_init_parents=*/ true)
}
if (!m->IsStatic()) {
if (declaring_class->IsStringClass() && m->IsConstructor()) {
m = WellKnownClasses::StringInitToStringFactory(m);
} else {
m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m, kPointerSize);
}
}
if (!accessible && !VerifyAccess(/*...*/)) {
ThrowIllegalAccessException(
StringPrintf("Class %s cannot access %s method %s of class %s", ...));
}
InvokeMethodImpl(soa, m, np_method, receiver, objects, &shorty, &result);
}
上面省略了许多细节,主要是做了一些调用前的检查和预处理工作,流程可以概况为:
- 判断方法所属的类是否已经初始化过,如果没有则进行初始化;
- 将
String.<init>
构造函数调用替换为对应的工厂StringFactory
方法调用; - 如果是虚函数调用,替换为运行时实际的函数;
- 判断方法是否可以访问,如果不能访问则抛出异常;
- 调用函数;
值得注意的是,jobject 类型的 javaMethod 可以转换为 ArtMethod
指针,该结构体是 ART 虚拟机中对于具体方法的描述。之后经过一系列调用:
- InvokeMethodImpl
- InvokeWithArgArray
method->Invoke()
最终进入 ArtMethod::Invoke
函数,还是只看核心代码:
void ArtMethod::Invoke(Thread* self, uint32_t* args, uint32_t args_size, JValue* result,
const char* shorty) {
Runtime* runtime = Runtime::Current();
if (UNLIKELY(!runtime->IsStarted() ||
(self->IsForceInterpreter() && !IsNative() && !IsProxyMethod() && IsInvokable()))) {
art::interpreter::EnterInterpreterFromInvoke(...);
} else {
bool have_quick_code = GetEntryPointFromQuickCompiledCode() != nullptr;
if (LIKELY(have_quick_code)) {
if (!IsStatic()) {
(*art_quick_invoke_stub)(this, args, args_size, self, result, shorty);
} else {
(*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty);
}
} else {
LOG(INFO) << "Not invoking '" << PrettyMethod() << "' code=null";
}
}
self->PopManagedStackFragment(fragment);
}
ART 对于 Java 方法实现了两种执行模式,一种是像 Dalvik 虚拟机一样解释执行字节码,姑且称为解释模式;另一种是快速模式,即直接调用通过 OAT 编译后的本地代码。
在 ART 早期指定本地代码还细分为 Portable 和 Quick 两种模式,但由于对极致速度的追求以及随着 Quick 模式的不断优化,Portable 也逐渐退出了历史舞台。
阅读上述代码可以得知,当 ART 运行时尚未启动或者指定强制使用解释执行时,虚拟机执行函数使用的是解释模式,ART 可以在启动时指定 -Xint
参数强制使用解释执行,但即便指定了使用解释执行模式,还是有一些情况无法使用解释执行,比如:
- 当所执行的方法是 Native 方法时,这时只有二进制代码,不存在字节码,自然无法解释执行;
- 当所执行的方法无法调用,比如 access_flag 判定无法访问或者当前方法是抽象方法时;
- 当所执行的方式是代理方法时,ART 对于代理方法有单独的本地调用方式;
解释执行
解释执行的入口是 art::interpreter::EnterInterpreterFromInvoke
,该函数定义在 art/runtime/interpreter/interpreter.cc,关键代码如下:
void EnterInterpreterFromInvoke(Thread* self,
ArtMethod* method,
ObjPtr<mirror::Object> receiver,
uint32_t* args,
JValue* result,
bool stay_in_interpreter) {
CodeItemDataAccessor accessor(method->DexInstructionData());
if (accessor.HasCodeItem()) {
num_regs = accessor.RegistersSize();
num_ins = accessor.InsSize();
}
// 初始化栈帧 ......
if (LIKELY(!method->IsNative())) {
JValue r = Execute(self, accessor, *shadow_frame, JValue(), stay_in_interpreter);
if (result != nullptr) {
*result = r;
}
}
}
其中的 CodeItem
就是 DEX 文件中对应方法的字节码,还是老样子,直接看简化的调用链路:
method | file |
---|---|
Execute | art/runtime/interpreter/interpreter.cc |
ExecuteSwitch | … |
ExecuteSwitchImpl | art/runtime/interpreter/interpreter_switch_impl.h |
ExecuteSwitchImplAsm | … |
ExecuteSwitchImplAsm | art/runtime/arch/arm64/quick_entrypoints_arm64.S |
ExecuteSwitchImplCpp | art/runtime/interpreter/interpreter_switch_impl-inl.h |
ExecuteSwitchImplAsm
为了速度直接使用汇编实现,在 ARM64 平台中的定义如下:
// Wrap ExecuteSwitchImpl in assembly method which specifies DEX PC for unwinding.
// Argument 0: x0: The context pointer for ExecuteSwitchImpl.
// Argument 1: x1: Pointer to the templated ExecuteSwitchImpl to call.
// Argument 2: x2: The value of DEX PC (memory address of the methods bytecode).
ENTRY ExecuteSwitchImplAsm
SAVE_TWO_REGS_INCREASE_FRAME x19, xLR, 16
mov x19, x2 // x19 = DEX PC
CFI_DEFINE_DEX_PC_WITH_OFFSET(0 /* x0 */, 19 /* x19 */, 0)
blr x1 // Call the wrapped method.
RESTORE_TWO_REGS_DECREASE_FRAME x19, xLR, 16
ret
END ExecuteSwitchImplAsm
本质上是调用保存在 x1 寄存器的第二个参数,调用处的代码片段如下:
代码语言:javascript复制template<bool do_access_check, bool transaction_active>
ALWAYS_INLINE JValue ExecuteSwitchImpl() {
//...
void* impl = reinterpret_cast<void*>(&ExecuteSwitchImplCpp<do_access_check, transaction_active>);
const uint16_t* dex_pc = ctx.accessor.Insns();
ExecuteSwitchImplAsm(&ctx, impl, dex_pc);
}
即调用了 ExecuteSwitchImplCpp
,在该函数中,可以看见典型的解释执行代码:
template<bool do_access_check, bool transaction_active>
void ExecuteSwitchImplCpp(SwitchImplContext* ctx) {
Thread* self = ctx->self;
const CodeItemDataAccessor& accessor = ctx->accessor;
ShadowFrame& shadow_frame = ctx->shadow_frame;
self->VerifyStack();
uint32_t dex_pc = shadow_frame.GetDexPC();
const auto* const instrumentation = Runtime::Current()->GetInstrumentation();
const uint16_t* const insns = accessor.Insns();
const Instruction* next = Instruction::At(insns dex_pc);
while (true) {
const Instruction* const inst = next;
dex_pc = inst->GetDexPc(insns);
shadow_frame.SetDexPC(dex_pc);
TraceExecution(shadow_frame, inst, dex_pc);
uint16_t inst_data = inst->Fetch16(0); // 一条指令 4 字节
if (InstructionHandler(...).Preamble()) {
switch (inst->Opcode(inst_data)) {
case xxx: ...;
case yyy: ...;
...
}
}
}
}
在当前版本中 (Android 12),实际上是通过宏展开去定义了所有 op_code 的处理分支,不同版本实现都略有不同,但解释执行的核心思路从 Android 2.x 版本到现在都是一致的,因为字节码的定义并没有太多改变。
快速执行
再回到 ArtMethod 真正调用之前,如果不使用解释模式执行,则通过 art_quick_invoke_stub
去调用。stub 是一小段中间代码,用于跳转到实际的 native 执行,该符号使用汇编实现,在 ARM64 中的定义在 art/runtime/arch/arm64/quick_entrypoints_arm64.S,核心代码如下:
.macro INVOKE_STUB_CALL_AND_RETURN
REFRESH_MARKING_REGISTER
REFRESH_SUSPEND_CHECK_REGISTER
// load method-> METHOD_QUICK_CODE_OFFSET
ldr x9, [x0, #ART_METHOD_QUICK_CODE_OFFSET_64]
// Branch to method.
blr x9
.endm
/*
* extern"C" void art_quick_invoke_stub(ArtMethod *method, x0
* uint32_t *args, x1
* uint32_t argsize, w2
* Thread *self, x3
* JValue *result, x4
* char *shorty); x5
*/
ENTRY art_quick_invoke_stub
// ...
INVOKE_STUB_CALL_AND_RETURN
END art_quick_invoke_static_stub
中间省略了一些保存上下文以及调用后恢复寄存器的代码,其核心是调用了 ArtMethod
结构体偏移 ART_METHOD_QUICK_CODE_OFFSET_64
处的指针,该值对应的代码为:
ASM_DEFINE(ART_METHOD_QUICK_CODE_OFFSET_64,
art::ArtMethod::EntryPointFromQuickCompiledCodeOffset(art::PointerSize::k64).Int32Value())
即 entry_point_from_quick_compiled_code_
属性所指向的地址。
// art/runtime/art_method.h
static constexpr MemberOffset EntryPointFromQuickCompiledCodeOffset(PointerSize pointer_size) {
return MemberOffset(PtrSizedFieldsOffset(pointer_size) OFFSETOF_MEMBER(
PtrSizedFields, entry_point_from_quick_compiled_code_) / sizeof(void*)
* static_cast<size_t>(pointer_size));
}
可以认为这就是所有快速模式执行代码的入口,至于该指针指向什么地方,又是什么时候初始化的,可以参考下一节代码加载部分。实际在方法调用时,快速模式执行的方法可能在其中执行到了需要以解释模式执行的方法,同样以解释模式执行的方法也可能在其中调用到 JNI 方法或者其他以快速模式执行的方法,所以在单个函数执行的过程中运行状态并不是一成不变的,但由于每次切换调用前后都保存和恢复了当前上下文,使得不同调用之间可以保持透明,这也是模块化设计的一大优势所在。
代码加载
在上节我们知道在 ART 虚拟机中,Java 方法的调用主要通过 ArtMethod::Invoke
去实现,那么 ArtMethod 结构是什么时候创建的呢?为什么 jmethod/jobject 可以转换为 ArtMethod
指针呢?
在 Java 这门语言中,方法是需要依赖类而存在的,因此要分析方法的初始化需要先分析类的初始化。虽然我们前面知道如何从 OAT/VDEX/DEX 文件中构造对应的 ClassLoader 来进行类查找,但那个时候类并没有初始化,可以编写一个简单的类进行验证:
代码语言:javascript复制public class Demo {
static {
Log.i("Demo", "static block called");
}
{
Log.i("Demo", "IIB called");
}
}
如果 Demo 类在代码中没有使用,那么上述两个打印都不会触发;如果使用 Class.forName("Demo")
进行反射引用,则 static block 中的代码会被调用。跟踪 Class.forName 调用:
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
// 笔者注: initialize = true
return forName(className, true, ClassLoader.getClassLoader(caller));
}
最终调用到名为 classForName
的 native 方法,其定义在 art/runtime/native/java_lang_Class.cc:
// "name" is in "binary name" format, e.g. "dalvik.system.Debug$1".
static jclass Class_classForName(JNIEnv* env, jclass, jstring javaName, jboolean initialize,
jobject javaLoader) {
ScopedFastNativeObjectAccess soa(env);
ScopedUtfChars name(env, javaName);
std::string descriptor(DotToDescriptor(name.c_str()));
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
Handle<mirror::Class> c(
hs.NewHandle(class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader)));
if (initialize) {
class_linker->EnsureInitialized(soa.Self(), c, true, true);
}
return soa.AddLocalReference<jclass>(c.Get());
}
首先将 Java 格式的类表示转换为 smali 格式,然后通过指定的 class_loader 去查找类,查找过程主要通过 class_linker
实现。由于 forName 函数中指定了 initialize
为 true
,因此在找到对应类后还会额外执行一步 EnsureInitialized
,在后文会进行详细介绍。
FindClass
FindClass 实现了根据类名查找类的过程,定义在 art/runtime/class_linker.cc 中,关键流程如下:
代码语言:javascript复制ObjPtr<mirror::Class> ClassLinker::FindClass(Thread* self,
const char* descriptor,
Handle<mirror::ClassLoader> class_loader)
if (descriptor[1] == '