在我的第一个 Xposed 模块中曾出现Class ref in pre-verified class resolved to unexpected implementation错误,Xposed log如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
01-03 19:42:03.974 134-134/? I/Xposed: Loading modules from /data/app/space.kiya.xposedtest-1.apk
01-03 19:42:04.285 134-134/? I/Xposed: Loading class space.kiya.xposedtest.ChangeStatusBar
01-03 19:42:04.298 134-134/? I/Xposed: java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
01-03 19:42:04.298 134-134/? I/Xposed: at dalvik.system.DexFile.defineClass(Native Method)
01-03 19:42:04.298 134-134/? I/Xposed: at dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
01-03 19:42:04.298 134-134/? I/Xposed: at dalvik.system.DexPathList.findClass(DexPathList.java:313)
01-03 19:42:04.298 134-134/? I/Xposed: at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:51)
01-03 19:42:04.298 134-134/? I/Xposed: at java.lang.ClassLoader.loadClass(ClassLoader.java:501)
01-03 19:42:04.298 134-134/? I/Xposed: at java.lang.ClassLoader.loadClass(ClassLoader.java:461)
01-03 19:42:04.298 134-134/? I/Xposed: at de.robv.android.xposed.XposedBridge.loadModule(XposedBridge.java:421)
01-03 19:42:04.298 134-134/? I/Xposed: at de.robv.android.xposed.XposedBridge.loadModules(XposedBridge.java:386)
01-03 19:42:04.298 134-134/? I/Xposed: at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:120)
01-03 19:42:04.298 134-134/? I/Xposed: at dalvik.system.NativeStart.main(Native Method)

从中可以看到,因为XposedBridge的main函数调用了loadModules,而loadModules又调用了loadModule,loadModule调用loadClass,这样一层一层下去最终导致了现在的错误.
我们就从案发现场loadclass和错误信息Class ref in pre-verified class resolved to unexpected implementation入手吧!

案发现场 loadclass

XposedBridge的代码clone下来,找到de/robv/android/xposed/XposedBridge文件.

我们知道Xposed Installer的界面有一个模块列表,在写Xposed模块时也需要新建文件xposed_init写入实现了Xposed接口的类名.

main函数的作用就是初始化Xposed框架和模块,模块是怎么初始化的呢?来看loadModules函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* Try to load all modules defined in <code>BASE_DIR/conf/modules.list</code>
*/
private static void loadModules() throws IOException {
final String filename = BASE_DIR + "conf/modules.list";
BaseService service = SELinuxHelper.getAppDataFileService();
if (!service.checkFileExists(filename)) {
Log.e(TAG, "Cannot load any modules because " + filename + " was not found");
return;
}
InputStream stream = service.getFileInputStream(filename);
BufferedReader apks = new BufferedReader(new InputStreamReader(stream));
String apk;
while ((apk = apks.readLine()) != null) {
loadModule(apk);
}
apks.close();
}

Xposed Installer的安装目录/data/data/de.robv.android.xposed.installer下的conf文件夹下有一个modules.list文件,记录着每个xposed模块的apk的路径.
loadModules对每行(一行对应一个apk路径)调用loadModule函数:

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
/**
* Load a module from an APK by calling the init(String) method for all classes defined
* in <code>assets/xposed_init</code>.
*/
@SuppressWarnings("deprecation")
private static void loadModule(String apk) {
log("Loading modules from " + apk);
if (!new File(apk).exists()) {
log(" File does not exist");
return;
}
ClassLoader mcl = new PathClassLoader(apk, BOOTCLASSLOADER);
InputStream is = mcl.getResourceAsStream("assets/xposed_init");
if (is == null) {
log("assets/xposed_init not found in the APK");
return;
}
BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
try {
String moduleClassName;
while ((moduleClassName = moduleClassesReader.readLine()) != null) {
moduleClassName = moduleClassName.trim();
if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
continue;
try {
log (" Loading class " + moduleClassName);
Class<?> moduleClass = mcl.loadClass(moduleClassName);
if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
log (" This class doesn't implement any sub-interface of IXposedMod, skipping it");
continue;
} else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
log (" This class requires resource-related hooks (which are disabled), skipping it.");
continue;
}
final Object moduleInstance = moduleClass.newInstance();
if (isZygote) {
if (moduleInstance instanceof IXposedHookZygoteInit) {
IXposedHookZygoteInit.StartupParam param = new IXposedHookZygoteInit.StartupParam();
param.modulePath = apk;
param.startsSystemServer = startsSystemServer;
((IXposedHookZygoteInit) moduleInstance).initZygote(param);
}
if (moduleInstance instanceof IXposedHookLoadPackage)
hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));
if (moduleInstance instanceof IXposedHookInitPackageResources)
hookInitPackageResources(new IXposedHookInitPackageResources.Wrapper((IXposedHookInitPackageResources) moduleInstance));
} else {
if (moduleInstance instanceof IXposedHookCmdInit) {
IXposedHookCmdInit.StartupParam param = new IXposedHookCmdInit.StartupParam();
param.modulePath = apk;
param.startClassName = startClassName;
((IXposedHookCmdInit) moduleInstance).initCmdApp(param);
}
}
} catch (Throwable t) {
log(t);
}
}
} catch (IOException e) {
log(e);
} finally {
try {
is.close();
} catch (IOException ignored) {}
}
}

loadModule读取每个apk文件中的assets/xposed_init,使用PathClassLoaderloadclass方法加载每个注册的类,然后将类的实例作为相应xposed模块的回调.

那么Class<?> moduleClass = mcl.loadClass(moduleClassName);这句代码是怎么会出错的呢?

何方神圣 CLASS_ISPREVERIFIED

CLASS_ISPREVERIFIED是虚拟机在对dex文件优化时的一个ClassFlags,表示某个类是否已经被预验证过.
也就是说当某个类中引用的方法全部来自所在的dex文件中时,这个类就会被打上这个标记.
若app有多个dex文件时,当引用了某个带有这个标记的类时,虚拟机不需要在全部的dex文件中查找,从而帮助提高dalvik虚拟机的性能.

关于如何验证的细节稍后分析.

我们写的ChangeStatusBar类实现了IXposedHookLoadPackage,同时jar包的引用方式为compile,所以也把XposedBridge.jar编译进dex了,因此在验证ChangeStatusBar类时在本dex中可以找到IXposedHookLoadPackage从而被标记了CLASS_ISPREVERIFIED.

那么是在哪里检查是否有CLASS_ISPREVERIFIED标记的呢?

源代码文件/dalvik/vm/oo/Resolve.cppdvmResolveClass函数中:有这么一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED))
{
ClassObject* resClassCheck = resClass;
if (dvmIsArrayClass(resClassCheck))
resClassCheck = resClassCheck->elementClass;
if (referrer->pDvmDex != resClassCheck->pDvmDex &&
resClassCheck->classLoader != NULL)
{
ALOGW("Class resolved by unexpected DEX:"
" %s(%p):%p ref [%s] %s(%p):%p",
referrer->descriptor, referrer->classLoader,
referrer->pDvmDex,
resClass->descriptor, resClassCheck->descriptor,
resClassCheck->classLoader, resClassCheck->pDvmDex);
ALOGW("(%s had used a different %s during pre-verification)",
referrer->descriptor, resClass->descriptor);
dvmThrowIllegalAccessError(
"Class ref in pre-verified class resolved to unexpected "
"implementation");
return NULL;
}
}

当虚拟机找到需要引用的类时,如果引用者已经被预验证过,且引用者所在的dex和被引用者所在的dex不同时,抛出Class ref in pre-verified class ...异常!

而在我的第一个 Xposed 模块出现错误时dalvik的log如下(经过剪切):

1
2
3
4
5
01-04 20:11:03.463 134-134/? W/dalvikvm: Class resolved by unexpected DEX: Lspace/kiya/xposedtest/ChangeStatusBar;(0x40e59fc8):0x5028f000 ref [Lde/robv/android/xposed/IXposedHookLoadPackage;] Lde/robv/android/xposed/IXposedHookLoadPackage;(0x40da2fd0):0x4f2f8000
01-04 20:14:06.184 134-134/? W/dalvikvm: (Lspace/kiya/xposedtest/ChangeStatusBar; had used a different Lde/robv/android/xposed/IXposedHookLoadPackage; during pre-verification)
01-04 20:11:03.467 134-134/? W/dalvikvm: Class resolved by unexpected DEX: Lspace/kiya/xposedtest/GetQQPassword;(0x40e59fc8):0x5028f000 ref [Lde/robv/android/xposed/IXposedHookLoadPackage;] Lde/robv/android/xposed/IXposedHookLoadPackage;(0x40da2fd0):0x4f2f8000
01-04 20:14:06.187 134-134/? W/dalvikvm: (Lspace/kiya/xposedtest/GetQQPassword; had used a different Lde/robv/android/xposed/IXposedHookLoadPackage; during pre-verification)

这里可以看出在loadclass时的引用者是Lspace/kiya/xposedtest/ChangeStatusBar,被引用者是Lde/robv/android/xposed/IXposedHookLoadPackage.
(解析类的过程包括类的所有引用的检查,这里问题是出在模块类引用其他类时而非xposed加载模块类时,故引用者是被加载的模块类)

dalvik解析的时候发现IXposedHookLoadPackageChangeStatusBar不在一个dex文件内,但是ChangeStatusBar类已经被校验过了呀,里面所有的方法都在本dex内,现在又告诉我不在一起了,那肯定要报错了!

和上一小节的问题结合起来,必然是loadclass加载类的同时做了验证!是否是这样我们随后再验证.

现在的问题是明明ChangeStatusBar已经通过了校验,IXposedHookLoadPackageChangeStatusBar在同一个dex中,怎么加载时IXposedHookLoadPackage又不在这里了?

罪魁祸首 PathClassLoader

这就要说说PathClassLoader了.

XposedBridge源码中使用的类加载器就是PathClassLoader.

1
2
public static final ClassLoader BOOTCLASSLOADER = ClassLoader.getSystemClassLoader();
ClassLoader mcl = new PathClassLoader(apk, BOOTCLASSLOADER);

其中apk是模块apk的路径;BOOTCLASSLOADER是系统类加载器的实例,从classpath中加载类.

loadClass方法在加载一个类的实例的时候,
会先查询当前ClassLoader实例是否加载过此类,有就返回;
如果没有。查询Parent是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;
如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;

也就是说我们的apk是最后一个被查找的路径.

而在Xposed源码中的app_main.cpp(被修改过的app_process)调用了xposed::initialize(zygote, startSystemServer, className, argc, argv),initialize中又调用了addJarToClasspath(),功能就是将XposedBridge.jar添加到Java classpath!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define XPOSED_JAR "/system/framework/XposedBridge.jar"
/** Add XposedBridge.jar to the Java classpath. */
bool addJarToClasspath() {
ALOGI("-----------------");
if (access(XPOSED_JAR, R_OK) == 0) {
if (!addPathToEnv("CLASSPATH", XPOSED_JAR))
return false;
ALOGI("Added Xposed (%s) to CLASSPATH", XPOSED_JAR);
return true;
} else {
ALOGE("ERROR: Could not access Xposed jar '%s'", XPOSED_JAR);
return false;
}
}

所以loadclass在检查apk路径之前系统类加载器就在系统类路径中的XposedBridge.jar中找到了IXposedHookLoadPackage,虽然我们的apk中也有这个函数,但是还没有等到加载就已经结束了.

到这里就可以理清错误为什么发生了:

  1. 编写模块时我们把XposedBridge.jar编译进了dex文件中;
  2. dalvik在优化dex时为模块类打上了CLASS_ISPREVERIFIED标记,表示没有引用的类全部来自本dex;
  3. xposed框架修改后的app_process会在开机时将/system/framework/XposedBridge.jar加入类路径;
  4. 随后xposed要加载我们注册的模块类,在事先为其准备好的jar包中找到了引用的类;
  5. 但是模块类被打上标记了,xposed发现模块就没打算用自己准备好的类,反而要自给自足,这哪是框架和模块的关系!简直是造反!于是愤而报错.

前面也提到解决方法就是不将XposedBridge.jar编译进dex即可,这样不会被打上标记,万事ok.
现在app的热修复原理也是这样.

凌晨4点 startvm

到了现在,如果不在意一些细节,本案已经被破!

之前说到,当类加载器调用loadclass加载类时发现ChangeStatusBar类已经被校验过…
等等!我们的XposedBridge加载模块类的代码是在app_process中呀,而校验是属于dexopt(dex优化)的步骤哇,难道dex优化比app_process这个启动代码还要早?

那么问题就清晰多了: app_process 和 dexopt 的关系.

一个app实质上就是一个app_process进程,app_process的作用如下:

  1. 创建jvm
  2. 执行startClass的main方法

app_main.cpp中的部分添加代码:

1
2
3
4
5
6
#define XPOSED_CLASS_DOTS_ZYGOTE "de.robv.android.xposed.XposedBridge"
isXposedLoaded = xposed::initialize(zygote, startSystemServer, className, argc, argv);
if (zygote) {
runtime.start(isXposedLoaded ? XPOSED_CLASS_DOTS_ZYGOTE : "com.android.internal.os.ZygoteInit",
startSystemServer ? "start-system-server" : "");
}

initialize在上面提到过,而runtime.start就是android源码/frameworks/base/core/jni/AndroidRuntime.cpp中的AndroidRuntime::start函数:

1
2
3
4
5
6
7
8
9
/*
* Start the Android runtime. This involves starting the virtual machine
* and calling the "static void main(String[] args)" method in the class
* named by "className".
*
* Passes the main function two arguments, the class name and the specified
* options string.
*/
void AndroidRuntime::start(const char* className, const char* options)

此函数主要功能是: 启动虚拟机,执行传入类的main函数
流程: AndroidRuntime::start -> startvm -> 执行传入的类的main函数
本案中传入的类是de.robv.android.xposed.XposedBridge.

而startvm中又执行了什么呢?
startvm -> JNI_CreateJavaVM -> dvmStartup -> dvmClassStartup -> … -> 直到优化dex时验证class

====> 因此当XposedBridge的main函数执行时,dex中的各个类已经被verify过了.我们的猜想是对哩.

Reference

【新技能get】让App像Web一样发布新版本

Android 热补丁动态修复框架小结

Android动态加载基础 ClassLoader工作机制