类加载器与动态加载

类加载器与动态加载是学习Android加壳与脱壳的需要。

Java程序的编译执行过程:

1. 类加载器

类加载器遵循双亲委派模式。

JVM的类加载器包括3种:

  • Bootstrap ClassLoader(引导类加载器):C/C++代码实现的加载器,用于加载指定的JDK核心类库,比如java.lang、java.uti等这些系统类。Java虚拟机的启动就是通过Bootstrap,该ClassLoader在Java里无法获取,负责加载/lib下的类。
  • Extensions ClassLoader(拓展类加载器):Java中的实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在Java里获取,负责加载/lib/ext下的类。
  • Application ClassLoader(应用程序类加载器):Java中的实现类为AppClassLoader,是与我们接触最多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

也可以自定义类加载器,只需要通过继承java.lang.ClassLoader类的方式来实现自己的类加载器即可。

在Android中与ClassLoader相关的类加载器共有8个:

  • ClassLoader:抽象类。
  • BootClassLoader:预加载常用类,单例模式。与Java中的Bootstrap ClassLoader不同,它并不是由C/C++实现,而是由Java实现的。
  • BaseDexClassLoader:是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
  • SecurityClassLoader:继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性。
  • URLClassLoader:SecurityClassLoader子类,用URL路径从JAR文件中加载类和资源。
  • PathClassLoader:Android默认使用的类加载器,一个APK中的Activity等类便是在其中加载。
  • DexClassLoader:可以加载任意目录下的DEX/JAR/APK/ZIP文件,比PathClassLoader更灵活,是实现插件化、热修复以及DEX加壳的重点。
  • InMemoryDexClassLoader:Android 8.0新引入,用于直接从内存中加载DEX。

1.1 双亲委派模式

1.1.1 双亲委派模式定义

加载.class文件时,以递归的形式逐级向上委托给父加载器ParentClassLoader加载,如果加载过了,就不用再加载一遍;如果父加载器没有加载过,继续委托给父加载器去加载,一直到这条链路的顶级,顶级ClassLoader如果没有加载过,则尝试加载,加载失败,则逐级向下交还调用者加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//1.先检查是否已经加载过--findLoaded
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//2.如果自己没加载过,存在父类,则委托父类
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}

if (c == null) {
//3.如果父类也没加载过,则尝试本级classLoader加载
c = findClass(name);
}
}
return c;
}

(1)先检查自己是否已经加载过.class文件,用findLoadedClass()方法,如果已经加载了直接返回
(2)如果自己没有加载过,存在父类,则委派父类去加载,用parent.loadClass(name,false)方法,此时会向上传递,然后去父加载器中循环第1步,一直到顶级ClassLoader
(3)如果父类没有加载,则尝试本级ClassLoader加载,如果加载失败了就会向下传递,交给调用方式实现.class文件的加载

1.1.2 双亲委派加载流程

我们要加载一个.class文件,定义一个CustomerClassLoader类加载器:

(1)首先会判断自己的CustomerClassLoader是否加载过,如果加载过直接返回;
(2)如果没有加载过则会调用父类PathClassLoader去加载,该父类同样会判断自己是否加载过,如果没有加载过则委托给父类BootClassLoader去加载;
(3)这个BootClassLoader是顶级ClassLoader,同样会去判断自己有没有加载过,如果也没有加载过则会调用自己的findClass(name)去加载;
(4)如果顶级BootClassLoader加载失败了,则会把加载这个动作向下交还给PathClassLoader;
(5)这个PathClassLoader也会尝试去调用findClass(name)去加载,如果加载失败了,则会继续向下交还给CustomerClassLoader来完成加载。这整个过程感觉是一个递归的过程,逐渐往上然后又逐渐往下,直到加载成功。

1.1.3 双亲委派的作用

(1) 防止同一个.class文件重复加载;
(2) 对于任意一个类确保在虚拟机中的唯一性,由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性;
(3) 保证.class文件不被篡改,通过委派方式可以保证系统类的加载逻辑不被篡改。

1.2 Android中的类加载机制

1.2.1 Dalvik虚拟机启动过程

Dalvik虚拟机的启动过程可以分为8个步骤:

1.2.1.1 AndroidRuntime.start()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void AndroidRuntime::start(const char* className, const bool startSystemServer){
......
/* start the virtual machine */
if (startVm(&mJavaVM, &env) != 0)
goto bail;

/* * Register android functions. */
if (startReg(env) < 0) {
LOGE("Unable to register all android natives\n");
goto bail;
}
......
/* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */
jclass startClass;
jmethodID startMeth;
slashClassName = strdup(className);
for (cp = slashClassName; *cp != '\0'; cp++)
if (*cp == '.')
*cp = '/';
startClass = env->FindClass(slashClassName);
if (startClass == NULL) {
LOGE("JavaVM unable to locate class '%s'\n", slashClassName);
/* keep going */
} else {
startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V");
if (startMeth == NULL) {
LOGE("JavaVM unable to find main() in '%s'\n", className);
/* keep going */
} else {
env->CallStaticVoidMethod(startClass, startMeth, strArray);
......
}
}
LOGD("Shutting down VM\n");
if (mJavaVM->DetachCurrentThread() != JNI_OK)
LOGW("Warning: unable to detach main thread\n");
if (mJavaVM->DestroyJavaVM() != 0)
LOGW("Warning: VM did not shut down cleanly\n");
......
}

这个函数主要做了4件事情:

  • 调用成员函数startVm()来创建一个Dalvik虚拟机实例,并且保存在成员变量mJavaVm中。
  • 调用成员函数startReg()来注册一些Android核心类的JNI方法。
  • 调用参数className所描述的一个Java类的静态成员函数main(),来作为Zygote进程的Java层入口。这个入口类就为com.android.internal.os.ZygoteInit。执行这一步的时候,Zygote进程中的Dalvik虚拟机实例就开始正式运作了。注意,在这一步中,也就是在com.android.internal.os.ZygoteInit类的静态成员函数main(),会进行大量的Android核心类和系统资源文件预加载。
  • 从com.android.internal.os.ZygoteInit类的静态成员函数main()返回来的时候,就说明Zygote进程准备要退出来了。在退出之前,会调用前面创建的Dalvik虚拟机实例的成员函数DetachCurrentThread()DestroyJavaVM()。其中,前者用来将Zygote进程的主线程脱离前面创建的Dalvik虚拟机实例,后者是用来销毁前面创建的Dalvik虚拟机实例。

1.2.1.2 AndroidRuntime.startVm()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv){  
int result = -1;
JavaVMInitArgs initArgs;
JavaVMOption opt;
......
property_get("dalvik.vm.checkjni", propBuf, "");
if (strcmp(propBuf, "true") == 0) {
checkJni = true;
} else if (strcmp(propBuf, "false") != 0) {
/* property is neither true nor false; fall back on kernel parameter */
property_get("ro.kernel.android.checkjni", propBuf, "");
if (propBuf[0] == '1') {
checkJni = true;
}
}
......
property_get("dalvik.vm.execution-mode", propBuf, "");
if (strcmp(propBuf, "int:portable") == 0) {
executionMode = kEMIntPortable;
} else if (strcmp(propBuf, "int:fast") == 0) {
executionMode = kEMIntFast;
#if defined(WITH_JIT)
} else if (strcmp(propBuf, "int:jit") == 0) {
executionMode = kEMJitCompiler;
#endif
}
property_get("dalvik.vm.stack-trace-file", stackTraceFileBuf, "");
......
strcpy(heapsizeOptsBuf, "-Xmx");
property_get("dalvik.vm.heapsize", heapsizeOptsBuf+4, "16m");
//LOGI("Heap size: %s", heapsizeOptsBuf);
opt.optionString = heapsizeOptsBuf;
mOptions.add(opt);
......
if (checkJni) {
/* extended JNI checking */
opt.optionString = "-Xcheck:jni";
mOptions.add(opt);
......
}
......
if (executionMode == kEMIntPortable) {
opt.optionString = "-Xint:portable";
mOptions.add(opt);
} else if (executionMode == kEMIntFast) {
opt.optionString = "-Xint:fast";
mOptions.add(opt);
#if defined(WITH_JIT)
} else if (executionMode == kEMJitCompiler) {
opt.optionString = "-Xint:jit";
mOptions.add(opt);
#endif
}
......
if (stackTraceFileBuf[0] != '\0') {
static const char* stfOptName = "-Xstacktracefile:";
stackTraceFile = (char*) malloc(strlen(stfOptName) + strlen(stackTraceFileBuf) + 1);
strcpy(stackTraceFile, stfOptName);
strcat(stackTraceFile, stackTraceFileBuf);
opt.optionString = stackTraceFile;
mOptions.add(opt);
}
......
initArgs.options = mOptions.editArray();
initArgs.nOptions = mOptions.size();
......
/* * Initialize the VM. *
* The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.
* If this call succeeds, the VM is ready, and we can start issuing
* JNI calls. */
if (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < 0) {
LOGE("JNI_CreateJavaVM failed\n");
goto bail;
}
result = 0;
bail:
free(stackTraceFile);
return result;
}

在启动Dalvik虚拟机的时候,可以指定一系列的选项,这些选项可以通过特定的系统属性来指定。下面我们就简单了解几个可能有用的选项。

  • -Xcheck:jni:用来启动JNI方法检查。我们在C/C++代码中,可以修改Java对象的成员变量或者调用Java对象的成员函数。加了-Xcheck:jni选项之后,就可以对要访问的Java对象的成员变量或者成员函数进行合法性检查,例如,检查类型是否匹配。我们可以通过dalvik.vm.checkjni或者ro.kernel.android.checkjni这两个系统属性来指定是否要启用-Xcheck:jni选项。注意,加了-Xcheck:jni选项之后,会使用得JNI方法执行变慢。
  • -Xint:portable,-Xint:fast,-Xint:jit:用来指定Dalvik虚拟机的执行模式。Dalvik虚拟机支持三种运行模式,分别是Portable、Fast和Jit。Portable是指Dalvik虚拟机以可移植的方式来进行编译,也就是说,编译出来的虚拟机可以在任意平台上运行。Fast是针对当前平台对Dalvik虚拟机进行编译,这样编译出来的Dalvik虚拟机可以进行特殊的优化,从而使得它能更快地运行程序。Jit不是解释执行代码,而是将代码动态编译成本地语言后再执行。我们可以通过dalvik.vm.execution-mode系统属性来指定Dalvik虚拟机的解释模式。
  • -Xstacktracefile:用来指定调用堆栈输出文件。Dalvik虚拟机接收到SIGQUIT(Ctrl-\或者kill -3)信号之后,会将所有线程的调用堆栈输出来,默认是输出到日志里面。指定了-Xstacktracefile选项之后,就可以将线程的调用堆栈输出到指定的文件中去。我们可以通过dalvik.vm.stack-trace-file系统属性来指定调用堆栈输出文件。
  • -Xmx:用来指定Java对象堆的最大值。Dalvik虚拟机的Java对象堆的默认最大值是16M,不过我们可以通过dalvik.vm.heapsize系统属性来指定为其它值。

设置好Dalvik虚拟机的启动选项之后,AndroidRuntime的成员函数startVm()就会调用另外一个函数JNI_CreateJavaVM()来创建以及初始化一个Dalvik虚拟机实例。

1.2.1.3 JNI_CreateJavaVM()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/* * Create a new VM instance. *
* The current thread becomes the main VM thread. We return immediately,
* which effectively means the caller is executing in a native method. */
jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args){
const JavaVMInitArgs* args = (JavaVMInitArgs*) vm_args;
JNIEnvExt* pEnv = NULL;
JavaVMExt* pVM = NULL;
const char** argv;
int argc = 0;
......
/* zero globals; not strictly necessary the first time a VM is started */
memset(&gDvm, 0, sizeof(gDvm));
/* * Set up structures for JNIEnv and VM. */
//pEnv = (JNIEnvExt*) malloc(sizeof(JNIEnvExt));
pVM = (JavaVMExt*) malloc(sizeof(JavaVMExt));
memset(pVM, 0, sizeof(JavaVMExt));
pVM->funcTable = &gInvokeInterface;
pVM->envList = pEnv;
......
argv = (const char**) malloc(sizeof(char*) * (args->nOptions));
memset(argv, 0, sizeof(char*) * (args->nOptions));
......
/* * Convert JNI args to argv. *
* We have to pull out vfprintf/exit/abort, because they use the
* "extraInfo" field to pass function pointer "hooks" in. We also
* look for the -Xcheck:jni stuff here. */
for (i = 0; i < args->nOptions; i++) {
......
}
......
/* set this up before initializing VM, so it can create some JNIEnvs */
gDvm.vmList = (JavaVM*) pVM;
/* * Create an env for main thread. We need to have something set up
* here because some of the class initialization we do when starting
* up the VM will call into native code. */
pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);
/* initialize VM */
gDvm.initializing = true;
if (dvmStartup(argc, argv, args->ignoreUnrecognized, (JNIEnv*)pEnv) != 0) {
free(pEnv);
free(pVM);
goto bail;
}
/* * Success! Return stuff to caller. */
dvmChangeStatus(NULL, THREAD_NATIVE);
*p_env = (JNIEnv*) pEnv;
*p_vm = (JavaVM*) pVM;
result = JNI_OK;bail:
gDvm.initializing = false;
if (result == JNI_OK)
LOGV("JNI_CreateJavaVM succeeded\n");
else
LOGW("JNI_CreateJavaVM failed\n");
free(argv);
return result;
}

JNI_CreateJavaVM主要完成以下4件事情:

  • 为当前进程创建一个Dalvik虚拟机实例,即一个JavaVMExt对象。
  • 为当前线程创建和初始化一个JNI环境,即一个JNIEnvExt对象,这是通过调用函数dvmCreateJNIEnv()来完成的。
  • 将参数vm_args所描述的Dalvik虚拟机启动选项拷贝到变量argv所描述的一个字符串数组中去,并且调用函数dvmStartup()来初始化前面所创建的Dalvik虚拟机实例。
  • 调用函数dvmChangeStatus()将当前线程的状态设置为正在执行Native代码,并且将前面所创建和初始化好的JavaVMExt对象和JNIEnvExt对象通过输出参数p_vm和p_env返回给调用者。

gDvm是一个类型为DvmGlobals的全局变量,用来收集当前进程所有虚拟机相关的信息,其中,它的成员变量vmList指向的就是当前进程中的Dalvik虚拟机实例,即一个JavaVMExt对象。以后每当需要访问当前进程中的Dalvik虚拟机实例时,就可以通过全局变量gDvm的成员变量vmList来获得,避免了在函数之间传递该Dalvik虚拟机实例。

每一个Dalvik虚拟机实例都有一个函数表,保存在对应的JavaVMExt对象的成员变量funcTable中,而这个函数表又被指定为gInvokeInterface。gInvokeInterface是一个类型为JNIInvokeInterface的结构体,如下所示:

1
2
3
4
5
6
7
8
9
10
static const struct JNIInvokeInterface gInvokeInterface = { 
NULL,
NULL,
NULL,
DestroyJavaVM,
AttachCurrentThread,
DetachCurrentThread,
GetEnv,
AttachCurrentThreadAsDaemon,
};

有了这个Dalvik虚拟机函数表之后,我们就可以将当前线程Attach或者Detach到Dalvik虚拟机中去,或者销毁当前进程的Dalvik虚拟机等。

每一个Dalvik虚拟机实例还有一个JNI环境列表,保存在对应的JavaVMExt对象的成员变量envList中。注意,JavaVMExt对象的成员变量envList描述的是一个JNIEnvExt列表,其中,每一个Attach到Dalvik虚拟机中去的线程都有一个对应的JNIEnvExt,用来描述它的JNI环境。有了这个JNI环境之后,我们才可以在Java函数和C/C++函数之间互相调用。

每一个JNIEnvExt对象都有两个成员变量prev和next,它们均是一个JNIEnvExt指针,分别指向前一个JNIEnvExt对象和后一个JNIEnvExt对象,也就是说,每一个Dalvik虚拟机实例的成员变量envList描述的是一个双向JNIEnvExt列表,其中,列表中的第一个JNIEnvExt对象描述的是主线程的JNI环境。

1.2.1.4 dvmCreateJNIEnv()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* * Create a new JNIEnv struct and add it to the VM's list. * 
* "self" will be NULL for the main thread, since the VM hasn't started
* yet; the value will be filled in later. */
JNIEnv* dvmCreateJNIEnv(Thread* self){
JavaVMExt* vm = (JavaVMExt*) gDvm.vmList;
JNIEnvExt* newEnv;
......
newEnv = (JNIEnvExt*) calloc(1, sizeof(JNIEnvExt));
newEnv->funcTable = &gNativeInterface;
newEnv->vm = vm;
......
if (self != NULL) {
dvmSetJniEnvThreadId((JNIEnv*) newEnv, self);
assert(newEnv->envThreadId != 0);
} else {
/* make it obvious if we fail to initialize these later */
newEnv->envThreadId = 0x77777775;
newEnv->self = (Thread*) 0x77777779;
}
......
/* insert at head of list */
newEnv->next = vm->envList;
assert(newEnv->prev == NULL);
if (vm->envList == NULL) // rare, but possible
vm->envList = newEnv;
else
vm->envList->prev = newEnv;
vm->envList = newEnv;
......
return (JNIEnv*) newEnv;
}

函数dvmCreateJNIEnv()主要是执行了以下3个操作:

  • 创建一个JNIEnvExt对象,用来描述一个JNI环境,并且设置这个JNIEnvExt对象的宿主Dalvik虚拟机,以及所使用的本地接口表,即设置这个JNIEnvExt对象的成员变量funcTable和vm。这里的宿主Dalvik虚拟机即为当前进程的Dalvik虚拟机,它保存在全局变量gDvm的成员变量vmList中。本地接口表由全局变量gNativeInterface来描述。
  • 参数self描述的是前面创建的JNIEnvExt对象要关联的线程,可以通过调用函数dvmSetJniEnvThreadId()来将它们关联起来。注意,当参数self的值等于NULL的时候,就表示前面的JNIEnvExt对象是要与主线程关联的,但是要等到后面再关联,因为现在用来描述主线程的Thread对象还没有准备好。通过将一个JNIEnvExt对象的成员变量envThreadId和self的值分别设置为0x77777775和0x77777779来表示它还没有与线程关联。
  • 在一个Dalvik虚拟机里面,可以运行多个线程。所有关联有JNI环境的线程都有一个对应的JNIEnvExt对象,这些JNIEnvExt对象相互连接在一起保存在用来描述其宿主Dalvik虚拟机的一个JavaVMExt对象的成员变量envList中。因此,前面创建的JNIEnvExt对象需要连接到其宿主Dalvik虚拟机的JavaVMExt链表中去。

    gNativeInterface是一个类型为JNINativeInterface的结构体,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const struct JNINativeInterface gNativeInterface = { 
......
FindClass,
......
GetMethodID,
......
CallObjectMethod,
......
GetFieldID,
......
SetIntField,
......
RegisterNatives,
UnregisterNatives,
......
GetJavaVM,
......
};

这一步执行完成之后,返回到前面的第3步中,即函数JNI_CreateJavaVM()中,接下来就会继续调用函数dvmStartup()来初始化前面所创建的Dalvik虚拟机实例。

1.2.1.5 dvmStartup()

这个函数用来初始化Dalvik虚拟机,我们分段来阅读:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* * VM initialization.  Pass in any options provided on the command line. 
* Do not pass in the class name or the options for the class. *
* Returns 0 on success. */
int dvmStartup(int argc, const char* const argv[], bool ignoreUnrecognized, JNIEnv* pEnv){
int i, cc;
......
setCommandLineDefaults();
/* prep properties storage */
if (!dvmPropertiesStartup(argc))
goto fail;
/* * Process the option flags (if any). */
cc = dvmProcessOptions(argc, argv, ignoreUnrecognized);
if (cc != 0) {
......
goto fail;
}

这段代码用来处理Dalvik虚拟机的启动选项,这些启动选项保存在参数argv中,并且个数等于argc。在处理这些启动选项之前,还会执行以下两个操作:

  • 调用函数setCommandLineDefaults()来给Dalvik虚拟机设置默认参数,因为启动选项不一定会指定Dalvik虚拟机的所有属性。
  • 调用函数dvmPropertiesStartup()来分配足够的内存空间来容纳由参数argv和argc所描述的启动选项。

完成以上两个操作之后,就可以调用函数dvmProcessOptions()来处理参数argv和argc所描述的启动选项了,也就是根据这些选项值来设置Dalvik虚拟机的属性,例如,设置Dalvik虚拟机的Java对象堆的最大值。

1
2
3
 /* configure signal handling */   
if (!gDvm.reduceSignals)
blockSignals();

如果我们没有在Dalvik虚拟机的启动选项中指定-Xrs,那么gDvm.reduceSignals的值就会被设置为false,表示要在当前线程中屏蔽掉SIGQUIT信号。在这种情况下,会有一个线程专门用来处理SIGQUIT信号。这个线程在接收到SIGQUIT信号的时候,就会将各个线程的调用堆栈打印出来,因此,这个线程又称为dump-stack-trace线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 /*     * Initialize components.     */    
if (!dvmAllocTrackerStartup())
goto fail;
if (!dvmGcStartup())
goto fail;
if (!dvmThreadStartup())
goto fail;
if (!dvmInlineNativeStartup())
goto fail;
if (!dvmVerificationStartup())
goto fail;
if (!dvmRegisterMapStartup())
goto fail;
if (!dvmInstanceofStartup())
goto fail;
if (!dvmClassStartup())
goto fail;
if (!dvmThreadObjStartup())
goto fail;
if (!dvmExceptionStartup())
goto fail;
if (!dvmStringInternStartup())
goto fail;
if (!dvmNativeStartup())
goto fail;
if (!dvmInternalNativeStartup())
goto fail;
if (!dvmJniStartup())
goto fail;
if (!dvmReflectStartup())
goto fail;
if (!dvmProfilingStartup())
goto fail;

这段代码用来初始化Dalvik虚拟机的各个子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* make sure we got these [can this go away?] */   
assert(gDvm.classJavaLangClass != NULL);
assert(gDvm.classJavaLangObject != NULL);
//assert(gDvm.classJavaLangString != NULL);
assert(gDvm.classJavaLangThread != NULL);
assert(gDvm.classJavaLangVMThread != NULL);
assert(gDvm.classJavaLangThreadGroup != NULL);
/* * Make sure these exist. If they don't, we can return a failure out
* of main and nip the whole thing in the bud. */
static const char* earlyClasses[] = {
"Ljava/lang/InternalError;",
"Ljava/lang/StackOverflowError;",
"Ljava/lang/UnsatisfiedLinkError;",
"Ljava/lang/NoClassDefFoundError;",
NULL
};
const char** pClassName;
for (pClassName = earlyClasses; *pClassName != NULL; pClassName++) {
if (dvmFindSystemClassNoInit(*pClassName) == NULL)
goto fail;
}

这段代码检查java.lang.Class、java.lang.Object、java.lang.Thread、java.lang.VMThread和java.lang.ThreadGroup这五个核心类经过前面的初始化操作后已经得到加载,并且确保系统中存在java.lang.InternalError、java.lang.StackOverflowError、java.lang.UnsatisfiedLinkError和java.lang.NoClassDefFoundError这四个核心类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 /*     * Miscellaneous class library validation.     */ 
if (!dvmValidateBoxClasses())
goto fail;
/* * Do the last bits of Thread struct initialization we need to allow
* JNI calls to work. */
if (!dvmPrepMainForJni(pEnv))
goto fail;
/* * Register the system native methods, which are registered through JNI. */
if (!registerSystemNatives(pEnv))
goto fail;
/* * Do some "late" initialization for the memory allocator. This may * allocate storage and initialize classes. */
if (!dvmCreateStockExceptions())
goto fail;
/* * At this point, the VM is in a pretty good state. Finish prep on
* the main thread (specifically, create a java.lang.Thread object to go
* along with our Thread struct). Note we will probably be executing
* some interpreted class initializer code in here. */
if (!dvmPrepMainThread())
goto fail;
/* * Make sure we haven't accumulated any tracked references. The main
* thread should be starting with a clean slate. */
if (dvmReferenceTableEntries(&dvmThreadSelf()->internalLocalRefTable) != 0) {
LOGW("Warning: tracked references remain post-initialization\n");
dvmDumpReferenceTable(&dvmThreadSelf()->internalLocalRefTable, "MAIN");
}
/* general debugging setup */
if (!dvmDebuggerStartup())
goto fail;

这段代码继续执行其它函数来执行其它的初始化和检查工作。

上述初始化和检查操作执行完成之后,我们再来看最后一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*     * Init for either zygote mode or non-zygote mode.  The key difference   
* is that we don't start any additional threads in Zygote mode. */
if (gDvm.zygote) {
if (!dvmInitZygote())
goto fail;
} else {
if (!dvmInitAfterZygote())
goto fail;
}
......
return 0;
fail:
dvmShutdown();
return 1;
}

这段代码完成Dalvik虚拟机的最后一步初始化工作。它检查Dalvik虚拟机是否指定了-Xzygote启动选项。如果指定了的话,那么就说明当前是在Zygote进程中启动Dalvik虚拟机,因此,接下来就会调用函数dvmInitZygote()来执行最后一步初始化工作。否则的话,就会调用另外一个函数dvmInitAfterZygote()来执行最后一步初始化工作。

1.2.1.6 dvmInitZygote()

1
2
3
4
5
6
/* * Do zygote-mode-only initialization. */
static bool dvmInitZygote(void){
/* zygote goes into its own process group */
setpgid(0,0);
return true;
}

函数dvmInitZygote()的实现很简单,它只是调用了系统调用setpgid()来设置当前进程,即Zygote进程的进程组ID。注意,在调用setpgid()的时候,传递进去的两个参数均为0,这意味着Zygote进程的进程组ID与进程ID是相同的,也就是说,Zygote进程运行在一个单独的进程组里面。

这一步执行完成之后,Dalvik虚拟机的创建和初始化工作就完成了,回到前面的第1步中,即AndroidRuntime类的成员函数start()中,接下来就会调用AndroidRuntime类的另外一个成员函数startReg()来注册Android核心类的JNI方法。

1.2.1.7 AndroidRuntime.startReg()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* * Register android native functions with the VM. */
/*static*/
int AndroidRuntime::startReg(JNIEnv* env){
/* * This hook causes all future threads created in this process to be
* attached to the JavaVM. (This needs to go away in favor of JNI
* Attach calls.) */
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
LOGV("--- registering native functions ---\n");
/* * Every "register" function calls one or more things that return
* a local reference (e.g. FindClass). Because we haven't really
* started the VM yet, they're all getting stored in the base frame
* and never released. Use Push/Pop to manage the storage. */
env->PushLocalFrame(200);
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
//createJavaThread("fubar", quickTest, (void*) "hello");
return 0;
}

AndroidRuntime类的成员函数startReg()首先调用函数androidSetCreateThreadFunc()来设置一个线程创建钩子javaCreateThreadEtc。这个线程创建钩子是用来初始化一个Native线程的JNI环境的,也就是说,当我们在C++代码中创建一个Native线程的时候,函数javaCreateThreadEtc()会被调用来初始化该Native线程的JNI环境。后面在分析Dalvik虚拟机线程的创建过程时,我们再详细分析函数javaCreateThreadEtc()的实现。

AndroidRuntime类的成员函数startReg()接着调用函数register_jni_procs()来注册Android核心类的JNI方法。在注册JNI方法的过程中,需要在Native代码中引用到一些Java对象,这些Java对象引用需要记录在当前线程的一个Native堆栈中。但是此时Dalvik虚拟机还没有真正运行起来,也就是当前线程的Native堆栈还没有准备就绪。在这种情况下,就需要在注册JNI方法之前,手动地将在当前线程的Native堆栈中压入一个帧(Frame),并且在注册JNI方法之后,手动地将该帧弹出来。

当前线程的JNI环境是由参数env所指向的一个JNIEnv对象来描述的,通过调用它的成员函数PushLocalFrame()PopLocalFrame()就可以手动地往当前线程的Native堆栈压入和弹出一个帧。注意,这个帧是一个本地帧,只可以用来保存Java对象在Native代码中的本地引用。

函数register_jni_procs()的实现如下所示:

1
2
3
4
5
6
7
8
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env){
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}

从前面的调用过程可以知道,参数array指向的是全局变量gRegJNI所描述的一个JNI方法注册函数表,其中,每一个表项都用一个RegJNIRec对象来描述,而每一个RegJNIRec对象都有一个成员变量mProc,指向一个JNI方法注册函数。通过依次调用这些注册函数,就可以将Android核心类的JNI方法注册到前面的所创建的Dalvik虚拟机中去。

回到AndroidRuntime类的成员函数startReg()中,接下来我们就继续分析函数androidSetCreateThreadFunc()的实现,以便可以了解线程创建钩子javaCreateThreadEtc的注册过程。

1.2.1.8 androidSetCreateThreadFunc()

1
2
3
4
5
static android_create_thread_fn gCreateThreadFn = androidCreateRawThreadEtc;
......
void androidSetCreateThreadFunc(android_create_thread_fn func){
gCreateThreadFn = func;
}

从这里就可以看到,线程创建钩子javaCreateThreadEtc被保存在一个函数指针gCreateThreadFn中。注意,函数指针gCreateThreadFn默认是指向函数androidCreateRawThreadEtc()的,也就是说,如果我们不设置线程创建钩子的话,函数androidCreateRawThreadEtc()就是默认使用的线程创建函数。后面在分析Dalvik虚拟机线程的创建过程时,我们再详细分析函数指针gCreateThreadFn是如何使用的。

1.2.1.9 总结

至此,我们就分析完成Dalvik虚拟机在Zygote进程中的启动过程,这个启动过程主要就是完成了以下四件事情:

  1. 创建了一个Dalvik虚拟机实例;
  2. 加载了Java核心类及其JNI方法;
  3. 为主线程设置了一个JNI环境;
  4. 注册了Android核心类的JNI方法。

换句话说,就是Zygote进程为Android系统准备好了一个Dalvik虚拟机实例,以后Zygote进程在创建Android应用程序进程的时候,就可以将它自身的Dalvik虚拟机实例复制到新创建Android应用程序进程中去,从而加快了Android应用程序进程的启动过程。此外,Java核心类和Android核心类(位于DEX文件中),以及它们的JNI方法(位于so文件中),都是以内存映射的方式来读取的,因此,Zygote进程在创建Android应用程序进程的时候,除了可以将自身的Dalvik虚拟机实例复制到新创建的Android应用程序进程之外,还可以与新创建的Android应用程序进程共享Java核心类和Android核心类,以及它们的JNI方法,这样就可以节省内存消耗。

同时,Zygote进程为了加快Android应用程序进程的启动过程,牺牲了自己的启动速度,因为它需要加载大量的Java核心类,以及注册大量的Android核心类JNI方法。Dalvik虚拟机在加载Java核心类的时候,还需要对它们进行验证以及优化,这些通常都是比较耗时的。又由于Zygote进程是由init进程启动的,也就是说Zygote进程在是开机的时候进行启动的,因此,Zygote进程的牺牲是比较大的。不过毕竟我们在玩手机的时候,很少会关机,也就是很少开机,因此,牺牲Zygote进程的启动速度是值得的,换来的是Android应用程序的快速启动。而且,Android系统为了加快Java类的加载速度,还会想方设法地提前对DEX文件进行验证和优化。

1.2.2 Android基本类预加载

Android中的ClassLoader类型分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种,分别是BootClassLoader、DexClassLoader、PathClassLoader。

(1)BootClassLoader:Android平台上所有Android系统启动时会使用BootClassLoader来预加载常用的类。
(2)BaseDexClassLoader:实际应用层类文件的加载,而真正的加载委托给pathList来完成。
(3)DexClassLoader:可以加载DEX文件以及包含DEX的压缩文件(APK, DEX, JAR, ZIP),可以加载一个未安装的APK文件的DEX文件,一般为自定义类加载器。
(4)PathClassLoader:可以加载系统类和应用程序的类,通常用来加载已安装的APK的DEX文件。

2. 壳的动态加载及修复流程

2.1 App启动流程

  1. BootClassLoader加载系统核心库
  2. PathClassLoader加载App自身DEX
  3. 进入App自身组件开始执行
  4. 调用声明Application的attachBaseContext
  5. 调用声明Application的onCreate

2.2 加壳应用的启动流程

壳要做的工作有:

  1. 对原先App的DEX进行解密
  2. 初始化自定义类加载器
  3. 替换LoadApk中的加载器为自定义加载器

在这个启动流程有两个问题需要解决:

  1. 何时进行DEX的解密?在自定义Application的attachBaseContext方法中进行解密。
  2. 如何解决动态加载的DEX中的类的生命周期问题?在自定义Application的onCreate方法中实现。

2.3 生命周期类处理

DexClassLoader加载的类是没有组件生命周期的,也就是说即使DexClassLoader通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常。为什么呢?

因为Activity不像函数方法,Activity具有生命周期和相关组件信息,只有当ClassLoader被修正后,才能正确加载被解密后的DEX类和方法。

ClassLoader修正的两种解决方案:

  • 通过层层反射,拿到mPackage内容,然后根据包名通过LoadApk获取App内的类加载器,最终使用自定义类加载器进行替换。
  • 利用双亲委派机制,在BootClassloader和PathClassloader中插入我们自定义的类加载器,完成修复。

方法一是加壳厂商经常会用的方式,也就是通过自定义DexClassLoader对原先的类加载器进行替换修复。

这里还分为两种情况:

  • 一种是原始的App没有自定义Application子类,那么这种情况比较简单,直接替换壳的Application即可。
  • 另一种情况,是原始App定义了Application子类,那么壳Application的工作就不仅仅是进行解密、类加载器修复等工作了,还有对解密后的DEX原始的Application的方法和类重新进行处理,保证整个过程运行顺畅。

我们可以看AndroidManifest.xml内的主Application是否被替换为壳Application入口,同时观察壳Application的汇编代码,会发现进行了大量的修复。