pre-verified问题摘抄自 热修复之冷启动类加载原理与实现现象DexClassLoader加载patch.dex.我们试试跑在Android4.4及以下,结果报错了。java.lang.Ill
pre-verified问题
摘抄自 热修复之冷启动类加载原理与实现
现象
DexClassLoader加载patch.dex.我们试试跑在Android4.4及以下,结果报错了。
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementationat com.a.android_sample.MainActivity.onCreate(MainActivity.java:16)at android.app.Activity.perfromCreate(Activity.java:5266)at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1313)at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3733)at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3939)
出错代码
String str = M.a();
原因分析
简单来说
1.假如类A及其引用类都在同一个dex中,则类A会被提前验证和优化,并被标记CLASS_ISPREVERIFIED这里,MainActivity就会被标记上。2.当我们调用M.a()时,需要加载类M,此时虚拟机会去校验M和MainActivity是否属于同一个dex。很明显不在,这就报错了。
.不了解,Dalvik类加载机制,这个原因是分析不出来的。我们算是站在巨人的肩膀上,有迹可循,而不是小马过河。
.具体代码抛错处
Android4.4 dalvik/vm/oo/Resolve.cpp
//省略了部分代码ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant){ DvmDex* pDvmDex = referrer->pDvmDex; ClassObject* resClass; const char* className; //不用重复解析 resClass = dvmDexGetResolvedClass(pDvmDex, classIdx); if (resClass != NULL) return resClass; .... //这里的resClass是 com.a.fix.M, //referrer是com.a. resClass = dvmFindClassNoInit(className, referrer->classLoader); //.... if (resClass != NULL) { /* * If the referrer was pre-verified, the resolved class must come * from the same DEX or from a bootstrap class. */ if (!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)) { ClassObject* resClassCheck = resClass; if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != NULL){ dvmThrowIllegalAccessError( "Class ref in pre-verified class resolved to unexpected " "implementation"); return NULL; } } //存一下, dvmDexSetResolvedClass(pDvmDex, classIdx, resClass); } ..... return resClass;}
调用链路
这部分可以折叠不看。
M.a()
AndroidStudio安装插件java2smali,看看MainActivity编译后的产物。MainActivity.smali 部分代码
.class public Lcom/a/android_sample/MainActivity;.source "MainActivity.java".method protected onCreate(Landroid/os/Bundle;)V .registers 4 #执行到这一行出错了。 .line 16 invoke-static {}, Lcom/a/fix/M;->a()Ljava/lang/String; .line 17 ... invoke-virtual {v1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V ....end method
invoke-static
代码在Android4.4源码 dalvik/vm/mterp/out/InterpC-portable.cpp
GOTO_TARGET(invokeStatic, bool methodCallRange) methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref); if (methodToCall == NULL) { //还没解析过,就去解析它 methodToCall = dvmResolveMethod(curMethod->clazz, ref, METHOD_STATIC); } GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);GOTO_TARGET_END
dvmResolveMethod
Android4.4源码 dalvik/vm/oo/Resolve.cpp解析Method前,先解析其所在的class
/* * Find the method corresponding to "methodRef". * If this is a static method, we ensure that the method's class is * initialized. *///省略了部分代码Method* dvmResolveMethod(const ClassObject* referrer, u4 methodIdx, MethodType methodType){ ClassObject* resClass; const DexMethodId* pMethodId; pMethodId = dexGetMethodId(pDvmDex->pDexFile, methodIdx); //这里就开始调用到我们上一节提到的具体代码抛错处了。 resClass = dvmResolveClass(referrer, pMethodId->classIdx, false); if (resClass == NULL) { /* can't find the class that the method is a part of */ assert(dvmCheckException(dvmThreadSelf())); return NULL; } ....}
dex文件验证优化
回头在来看dex文件优化,我们就放上调用
//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.javaBaseDexClassLoader(dexPath,optimizedDirectory,libraryPath,parent)//libcore/dalvik/src/main/java/dalvik/system/DexPathList.javaDexPathList.loadDexFile(file, optimizedDirectory);//libcore/dalvik/src/main/java/dalvik/system/DexFile.javaDexFile.loadDex(file.getPath(), optimizedPath, 0);//dalvik/vm/native/dalvik_system_DexFile.cppDalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult)//dalvik/vm/RawDexFile.cppdvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) //dalvik/vm/analysis/DexPrepare.cpp dvmOptimizeDexFile(optFd, dexOffset, fileSize,fileName,....) //创建进程 /system/bing/dexopt//dalvik/dexopt/OptMain.cppint main(int argc, char* const argv[]) fromDex(int argc, char* const argv[])dvmContinueOptimization(fd, offset, length...) //dalvik/vm/analysis/DexPrepare.cpp rewriteDex(addr, int len,doVerify,doOpt,..)verifyAndOptimizeClasses(pDvmDex->pDexFile, doVerify, doOpt)verifyAndOptimizeClass(pDexFile, clazz, pClassDef, doVerify, doOpt)dvmVerifyClass(clazz)//Set the "is preverified" flag in the DexClassDef
dvmVerifyClass
//dalvik/vm/analysis/DexPrepare.cpp if (dvmVerifyClass(clazz)) {/* Set the "is preverified" flag in the DexClassDef. */ ((DexClassDef*)pClassDef)->accessFlags |= CLASS_ISPREVERIFIED; verified = true;}//dalvik/vm/analysis/DexVerify.cpp bool dvmVerifyClass(ClassObject* clazz)bool verifyMethod(method)bool dvmVerifyCodeFlow(VerifierData* vdata)//dalvik/vm/analysis/CodeVerify.cpp bool doCodeVerification()...
参考
深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明.pdf深入理解Dalvik虚拟机系统源码(AOSP) github地址链接,下载你想要的。或者这个官网链接安卓App热补丁动态修复技术介绍android热修复的pre-verify问题详解及实践05-DALVIK加载和解析DEX过程
pre-verified解决
方案分析
我们在把代码抄过来,发现有三个条件同时满足才会报错
//省略了部分代码ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx, bool fromUnverifiedConstant){ resClass = dvmDexGetResolvedClass(pDvmDex, classIdx); if (resClass != NULL) return resClass; if (!fromUnverifiedConstant && IS_CLASS_FLAG_SET(referrer, CLASS_ISPREVERIFIED)) { ClassObject* resClassCheck = resClass; if (referrer->pDvmDex != resClassCheck->pDvmDex && resClassCheck->classLoader != NULL){ dvmThrowIllegalAccessError( "Class ref in pre-verified class resolved to unexpected " "implementation"); return NULL; } } } return resClass;}
根据上述代码,解决方案大致上有以下四种。
- 禁止dexopt过程打上CLASS_ISPREVERIFIED标记
Q-zone插桩方案突破了此限制,但是导致preverify失效,损失了性能。
- 修改fromUnverfiedConstant=true
需要通过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改为 true, 风险大,几乎无人采用。Cydia native hook
- 使dvmDexGetResolvedClass返回不为null,直接返回
QFix采用此方案,
- 补丁类与引用类放在同一个dex中
Tinker等全量合成方案突破了此限制。
Q-zone插桩方案
方案分析
通过字节码技术,在每个类的构造方法中插入一段引用 HackCode.class的代码,使得MainActivity引用到hack.dex中的Hack.class,导致verify不通过。此时方案分成两部分
- 单独打包HackCode.class
- MainActivity引用HackCode.class。
package com.a.hack;public class HackCode {}
实际代码执行处。
//dalvik/vm/analysis/CodeVerify.cppcase OP_CONST_CLASS: //给它整失败了,会把错误值给failure,后面判断下失败,就返回失败了,就不标记了。 resClass = dvmOptResolveClass(meth->clazz, decInsn.vB, &failure);////dalvik/vm/analysis/Optimize.cpp/* * Performs access checks on every resolve, * and refuses to acknowledge the existence of classes * defined in more than one DEX file. * 不承认定义在多个dex中的类 */ClassObject* dvmOptResolveClass(ClassObject* referrer, u4 classIdx, VerifyError* pFailure){ ... const char* className = dexStringByTypeIdx(pDvmDex->pDexFile, classIdx); //referrer是所有引用类包括MainAcitivityClass,resClass的Hack.class //referrer的dex中当然没有Hack.class resClass = dvmFindClassNoInit(className, referrer->classLoader); if (resClass == NULL) { *pFailure = VERIFY_ERROR_NO_CLASS; ... } ...}
引用hackCode.class
apk源码不能包含HackCode.class,我们通过字节码插入引用。编写自定义Gradle插件,使用javassist字节码技术自定义Gradle插件参考 Gradle系列一 -- Groovy、Gradle和自定义Gradle插件javassist参考 javassist使用全解析关键代码,有点长
class HackTransform extends Transform { def pool = ClassPool.default def project .... @Override void transform(TransformInvocation transformInvocation) throws javax.xml.crypto.dsig.TransformException, InterruptedException, IOException { super.transform(transformInvocation) project.android.bootClasspath.each { pool.appendClassPath(it.absolutePath) } //这一行要注意,否则编译不通过哦 pool.makeClass("com.a.hack.HackCode") transformInvocation.inputs.each { it.jarInputs.each { pool.insertClassPath(it.file.absolutePath) // 重命名输出文件(同目录copyFile会冲突) def jarName = it.name def md5Name = DigestUtils.md5Hex(it.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } def dest = transformInvocation.outputProvider.getContentLocation( jarName + md5Name, it.contentTypes, it.scopes, Format.JAR) org.apache.commons.io.FileUtils.copyFile(it.file, dest) } it.directoryInputs.each { def inputDir = it.file.absolutePath pool.insertClassPath(inputDir) findTarget(it.file, inputDir) def dest = transformInvocation.outputProvider.getContentLocation( it.name, it.contentTypes, it.scopes, Format.DIRECTORY) org.apache.commons.io.FileUtils.copyDirectory(it.file, dest) } } } private void findTarget(File fileOrDir, String inputDir) { if (fileOrDir.isDirectory()) { fileOrDir.listFiles().each { findTarget(it, inputDir) } } else { modify(fileOrDir, inputDir) } } private void modify(File file, String fileName) { def filePath = file.absolutePath if (!filePath.endsWith(SdkConstants.DOT_CLASS) ||filePath.contains('R$') || filePath.contains('R.class') || filePath.contains("BuildConfig.class")) { return } def className = filePath.replace(fileName, "") .replace("//", ".").replace("/", ".") def name = className.replace(SdkConstants.DOT_CLASS, "").substring(1) CtClass ctClass = pool.get(name) //我们的自定义的Application是初始类,加载完dex以后的类,才能插入Hakcode引用。 if (ctClass.getSuperclass() != null && ctClass.getSuperclass().name == "android.app.Application") { return } //真正执行插入字节码的地方 ctClass.defrost() CtConstructor[] constructors = ctClass.getDeclaredConstructors() if (constructors != null && constructors.length > 0) { CtConstructor constructor = constructors[0] def body = "android.util.Log.e(/"alvin/",/"${constructor.name} constructor/" + com.a.hack.HackCode.class);" constructor.insertBefore(body) } ctClass.writeFile(fileName) ctClass.detach() }}
生成hack.dex
参考patch.dex的生成方式。编写app/main/java/com/a/hack/HackCode.java,单独编译成dex,生成后,可以删掉此java文件。
package com.a.hack;public class HackCode {}
//来到java源码目录下,cd app/main/java//.class文件javac com/a/hack/HackCode.java //生成hack.dexdx --dex --output com/a/hack/hack.dex com/a/hack/HackCode.class
加载hack.dex
参考patch.dex的方式。
验证
android4.4上验证成功
Cydia NativeHook
.需要通过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改为 true,
.这里我们采用Cydia Substrate,hook dvmResolveClass方法,步骤如下Demo代码:hook具体实现与动态库下载,注意方案只在Android4.4上验证可行。
实现步骤
cydia so库和头文件
这里可以下载。so库放到一个自己的目录底下 比如
<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate.so<moduleName>/src/main/jniLibs/armeabi-v7a/libsubstrate-dvm.so
导入头文件
<moduleName>/src/main/cpp/include/substrate.h
hook代码实现
//<moduleName>/src/main/cpp/cydia-hook.cpp#include "include/substrate.h"#include <android/log.h>#define TAG "alvin"#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)//旧函数指针,指向旧函数void *(*oldDvmResolveClass)(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant);//新函数实现void *newDvmResolveClass(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant) { //这里,fromUnverifiedConstant 强制为true,就不会去check dex是否相等了。 return oldDvmResolveClass(referrer, classIdx, true);}//指明要hook的lib,涉及到dvmResolveClass的soMSConfig(MSFilterLibrary, "/system/lib/libdvm.so")//指明要hook的应用MSConfig(MSFilterExecutable, "com.a.dexload.cydia")MSInitialize { MSImageRef image = MSGetImageByName("/system/lib/libdvm.so"); if (image == NULL) { return; } void *resloveMethd = MSFindSymbol(image, "dvmResolveClass"); if (resloveMethd == NULL) { return; } //具体的Hook实现 MSHookFunction(resloveMethd, (void *) newDvmResolveClass, (void **) &oldDvmResolveClass);}
CMakeLists.txt
生成libcydiahook.so
cmake_minimum_required(VERSION 3.10.2)add_library(cydiahook SHARED src/main/cpp/cydia-hook.cpp)target_include_directories(cydiahook PRIVATE ${CMAKE_SOURCE_DIR}/src/main/cpp/include)find_library(log-lib log)file(GLOB libs ${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libsubstrate.so ${CMAKE_SOURCE_DIR}/src/main/jniLibs/armeabi-v7a/libsubstrate-dvm.so)target_link_libraries( cydiahook ${libs} ${log-lib})
libcydiahook.so加载
public class ApplicationApp extends Application { static { System.loadLibrary("cydiahook"); }}
其他
ClassObject属性
如同Andfix,我们可以引入DexFile.h头文件,可以把参数和结果转成实际的class对象,查看class的一些属性
//新函数实现void *newDvmResolveClass(void *referrer, unsigned int classIdx, bool fromUnverifiedConstant) { void *res = oldDvmResolveClass(referrer, classIdx, true); ClassObject *referrerClass = reinterpret_cast<ClassObject *>(referrer); ClassObject *resClass = reinterpret_cast<ClassObject *>(res); if (resClass == NULL) { LOGE("newDvmResolveClass %s, %s", referrerClass->descriptor, "resClass is NULL"); } else { LOGE("newDvmResolveClass %s, %s", referrerClass->descriptor, resClass->descriptor); } return res;}
风险
和 Andfix 类似,native hook 方式存在各种兼容性和稳定性问题,甚至安全性问题。同时,拦截的是一个涉及 dalvik 基础功能同时调用很频繁的方法,无疑风险会大很多。
QFix方案实现
可参考这篇文章QFix探索之路—手Q热补丁轻量级方案
.
- 0