摘抄自 热修复之冷启动类加载原理与实现html
DexClassLoader加载patch.dex.咱们试试跑在Android4.4及如下,结果报错了。java
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation at 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) 复制代码
出错代码android
String str = M.a();
复制代码
1.假如类A及其引用类都在同一个dex中,则类A会被提早验证和优化,并被标记CLASS_ISPREVERIFIED 这里,MainActivity就会被标记上。 2.当咱们调用M.a()时,须要加载类M,此时虚拟机会去校验M和MainActivity是否属于同一个dex。很明显不在,这就报错了。git
不了解,Dalvik类加载机制,这个缘由是分析不出来的。咱们算是站在巨人的肩膀上,有迹可循,而不是小马过河。github
Android4.4 dalvik/vm/oo/Resolve.cppapache
//省略了部分代码
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;
}
复制代码
这部分能够折叠不看。bootstrap
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
复制代码
代码在Android4.4源码 dalvik/vm/mterp/out/InterpC-portable.cppmarkdown
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
复制代码
Android4.4源码 dalvik/vm/oo/Resolve.cpp 解析Method前,先解析其所在的classapp
/* * 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文件优化,咱们就放上调用
//libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
BaseDexClassLoader(dexPath,optimizedDirectory,libraryPath,parent)
//libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList.loadDexFile(file, optimizedDirectory);
//libcore/dalvik/src/main/java/dalvik/system/DexFile.java
DexFile.loadDex(file.getPath(), optimizedPath, 0);
//dalvik/vm/native/dalvik_system_DexFile.cpp
Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult)
//dalvik/vm/RawDexFile.cpp
dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false)
//dalvik/vm/analysis/DexPrepare.cpp
dvmOptimizeDexFile(optFd, dexOffset, fileSize,fileName,....)
//建立进程 /system/bing/dexopt
//dalvik/dexopt/OptMain.cpp
int 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过程
咱们在把代码抄过来,发现有三个条件同时知足才会报错
//省略了部分代码
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;
}
复制代码
根据上述代码,解决方案大体上有如下四种。
Q-zone插桩方案突破了此限制,可是致使preverify失效,损失了性能。
须要经过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改成 true, 风险大,几乎无人采用。Cydia native hook
QFix采用此方案,
Tinker等全量合成方案突破了此限制。
经过字节码技术,在每一个类的构造方法中插入一段引用 HackCode.class的代码,使得MainActivity引用到hack.dex中的Hack.class,致使verify不经过。 此时方案分红两部分
package com.a.hack;
public class HackCode {}
复制代码
实际代码执行处。
//dalvik/vm/analysis/CodeVerify.cpp
case 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;
...
}
...
}
复制代码
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()
}
}
复制代码
参考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.dex
dx --dex --output com/a/hack/hack.dex com/a/hack/HackCode.class
复制代码
参考patch.dex的方式。
android4.4上验证成功
须要经过 native hook 拦截系统方法,更改方法的入口参数,将 fromUnverifiedConstant 统一改成 true,
这里咱们采用Cydia Substrate,hook dvmResolveClass方法,步骤以下 Demo代码:hook具体实现与动态库下载,注意方案只在Android4.4上验证可行。
这里能够下载。 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
复制代码
//<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的so
MSConfig(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);
}
复制代码
生成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})
复制代码
public class ApplicationApp extends Application {
static {
System.loadLibrary("cydiahook");
}
}
复制代码
如同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探索之路—手Q热补丁轻量级方案