热修复之冷启动类加载原理与实现

概述

利用DexClassLoader类加载原理,apk包含多个dex文件,会从dex中依次查找类,若是找到了就不继续日后面找了。咱们把补丁包.dex放到最前面,就优先从补丁包中查找类。
html

原理分析

Dex分包

dex是java文件编译的二进制产物,能够理解成Android优化后的.class合并文件。原先全部java文件都会被打包单个dex,但因为dex的65536问题,会分包成多个dex。
截屏2020-11-18 下午5.03.44.png java

DexClassLoader机制

Android提供了从Dex中加载类的DexClassLoader。
咱们把修复后的com.a.fix.M生成patch.dex,
想办法把path.dex插入到dexElements最前面,
当loader要查找com.a.fix.M时,会从前日后遍历dexElements数组,查到了就终止遍历。
截屏2020-11-19 下午3.34.47.png android

DexClassLoader源码

截屏2020-11-19 下午3.55.32.png

查看DexClassLoader源码,已7.0的源码为例,选取部分代码git

//DexClassLoader的基类,代码有省略
public class BaseDexClassLoader extends ClassLoader {
    
    //具体加载的事宜都交给了DexPathList,私有属性,能够反射调用
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, ...) {
        this.pathList = new DexPathList(this, dexPath, ...);
    }
    @Override
    protected Class<?> findClass(String name){
        return pathList.findClass(name, suppressedExceptions);
    }
    /** * @hide,隐藏方法,能够反射调用。能够用来插入patch.dex */
    public void addDexPath(String dexPath) {
        pathList.addDexPath(dexPath, null /*optimizedDirectory*/);
    }
复制代码
//只能反射使用这个类
/*package*/ final class DexPathList {

   	//这个dexElements,数组很关键实际上Element 多是dex文件,包含dex的apk文件,包含dex的jar文件
    private Element[] dexElements;

    public DexPathList(ClassLoader definingContext, String dexPath,...) {
        this.dexElements = makeDexElements(splitDexPath(dexPath), ...);   
    }

    //只能反射调用,能够用来插入patch.dex。
    public void addDexPath(String dexPath, File optimizedDirectory) {
       final Element[] newElements = makeDexElements(splitDexPath(dexPath),...);
       final Element[] oldElements = dexElements;
       dexElements = new Element[oldElements.length + newElements.length];
       System.arraycopy(oldElements, 0, dexElements, 0, oldElements.length);
        //新增的Elements只能加入到数组后面。咱们须要把patch加到最前面
       System.arraycopy(newElements, 0, dexElements, oldElements.length, newElements.length);
    }
    
	//上图的总结来自这里,遍历dexElements数组。 
   	public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            //最终调用在platform/art/rumtime/native/dalvik_system_DexFile.cc中
			Class clazz = element.dexFile.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null)  return clazz;   
        }  
        return null;
    }
    /** * Element of the dex/resource/native library path */
    /*package*/ static class Element {
        private final File dir;
        private final boolean isDirectory;
        private final File zip;
        private final DexFile dexFile; 
    }
}
复制代码


前置插入Patch.dex


咱们须要把patch.dex插入到dexElements最前面。
因为没有对外暴露方法,须要反射执行。这就很简单了,方案就有不少,好比github

  • a.DexPathList.makeDexElements, 生成path element数组,合并新老数组
  • b.DexPathList.addDexPath,而后把新插入patch的element调整到数组最前面

a方案,也是网上比较多的方案
截屏2020-11-19 下午4.30.48.png
b方案,理解这个东西,就能够整出来了。
截屏2020-11-19 下午4.51.39.png
apache

过程实现

先来整一段用来显示的代码。完整版代码在 热修复之冷启动 module:hotfix_dexload bootstrap

待修复功能

//待修复类
package com.a.fix;
public class M {
    public static String a(){return  "M aaa";}
}

//用来展现数据的类
package com.a.android_sample;
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        String str = M.a();
        ((TextView) findViewById(R.id.tv)).setText(str));
    }
}
复制代码

生成patch.dex

类修复代码

package com.a.fix;
public class M {
    public static String a(){return  "M aaa fix";}
}
复制代码

java -> dex

这样,简单的dex文件就生成了。生成patch.dex后,咱们把代码恢复成修复前的。数组

//来到java源码目录下
cd app/main/java
//.class文件
javac com/a/fix/M.java   
//生成patch.dex
dx --dex --output com/a/fix/patch.dex com/a/fix/M.class 
复制代码

好奇点,咱们能够名看看dex里面的是啥,可使用smali相关技术 AndroidStudio安装插件java2smali或者smali.jar包执行安全

存放patch.dex

patch.dex复制到assets文件夹中markdown

patch.dex 该放哪应用启动后能读取就行。放到sd卡固然能够。 咱们选择assets文件夹中,程序启动时copy到咱们想要的地方,就当模拟下载了。

插入patch.dex

从这里开始,都在自定义ApplicationApp的attachBaseContext()重载中完成。

为何这个方法中合适, ApplicationApp是应用建立时,apk中最早实例化的类,实际上attachBaseContext在onCreate()以前调用。 这又是一个话题了,能够看看 CSDN上老罗的应用启动过程,有提到Applicaiton的建立。 参考源码LoadedApk.makeApplication()

public class ApplicationApp extends Application {
   
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        	//1.从assets中复制出来
            String dexFilePath = copyAssetsDex("patch.dex");
        	//2.装载补丁包,即插入到dexElements
            installDex(this, dexFilePath);
        }
    }

	//复制assets中的patch.dex到手机的存储系统,
    private String copyAssetsDex(String dexFileName) {
        //这个ExternalCacheDir应用沙盒存储,读写自由遍历
        String hackPath = getExternalCacheDir().getAbsolutePath() + "/" + dexFileName;
        File destFile = new File(hackPath);
        if (destFile.exists())  destFile.delete();
        InputStream is = getAssets().open(dexFileName);
        FileOutputStream fos = new FileOutputStream(destFile);
        byte[] buffer = new byte[1024];
        int byteCount;
        while ((byteCount = is.read(buffer)) != -1) {
            fos.write(buffer, 0, byteCount);
        }
       ...
        return destFile.getAbsolutePath();
    }

	//插入patch.dex,由于是反射调用,不一样系统版本的源码多是不一致的,要作区分。省略部分代码
	//这里,我找了一些不一样版本,能够丰富起来。
	private void installDex(Context context, String filePath) {
            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                installDexh4_3_And_Below(context, filePath);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
                installDexh4_4_TO_5_1(context, filePath);
            } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
                installDexAbove_6_0_And_Above(context, filePath);
            }
        }
    }

	/** * 执行插入,羞愧,都是别人的代码。也就是咱们的a方案,下面还有简单的b方案。 */
    public static void installDexAbove_6_0_And_Above(Context context, String patch) {
        //优化目录必须是私有目录,可自由访问。
        File cacheDir = context.getCacheDir();
        //PathClassLoader
        ClassLoader classLoader = context.getClassLoader();
        try {
            //先获取pathList属性
            Field pathList = getField(classLoader, "pathList");
            //经过属性反射获取属性的对象 DexPathList
            Object pathListObject = pathList.get(classLoader);
            //经过 pathListObject 对象获取 pathList类中的dexElements 属性
            //本来的dex element数组
            Field dexElementsField = getField(pathListObject, "dexElements");

            //经过dexElementsField 属性获取它存在的对象
            Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject);

            List<File> files = new ArrayList<>();

            File file = new File(patch);//补丁包
            if (file.exists()) {
                files.add(file);
            }
            //插桩所用到的类
// files.add(antiazyFile);
            Method method = getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class, ClassLoader.class);
            final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
            //补丁的element数组
            Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader);
            //用于替换系统本来的element数组
            Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(),
                    dexElementsObject.length + patchElement.length);

            //合并复制element
            System.arraycopy(patchElement, 0, newElement, 0, patchElement.length);
            System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length);

            // 替换
            dexElementsField.set(pathListObject, newElement);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
	
	//b方案就简单点,也好理解点。
	 public static void installDexAbove_6_0_And_Above(Context context, String patch) {
        try {
            ClassLoader classLoader = context.getClassLoader();
            Object pathListObject  = getField(classLoader, "pathList").get(classLoader);

            //1.先记录插入patch前 dexElements的长度
            Field dexElementsField = getField(pathListObject, "dexElements");
            int oldLength = ((Object[]) dexElementsField.get(pathListObject)).length;

            //2.插入patch.dex
            Method method = getMethod(classLoader, "addDexPath", String.class);
            method.invoke(classLoader, patch);

            //3.读取插入patch后 dexElements的长度
            Object[] newDexElements = (Object[]) dexElementsField.get(pathListObject);
            int newLength = newDexElements.length;
            
            //4.先后交换生成新的dexElements,
             Object[] resultElements = (Object[]) Array.newInstance(newDexElements.getClass().getComponentType(),
                    newLength);
            System.arraycopy(newDexElements, 0, resultElements, newLength - oldLength, oldLength);
            System.arraycopy(newDexElements, oldLength, resultElements, 0, newLength - oldLength);
            
            //5.从新反射替换dexElements
            dexElementsField.set(pathListObject, resultElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

验证

到此为止,咱们的修复功能就实现了。我试了Android7都ok。

pre-verified问题

现象

上面的代码,咱们试试跑在Android4.4及如下,结果报错了。

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)  复制代码

出错代码

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.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过程

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。

截屏2020-11-25 下午8.41.16.png

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;
         ...			
     }
    ...
}
复制代码

引用hackCode.class

apk源码不能包含HackCode.class,咱们经过字节码插入引用。 编写自定义Gradle插件,使用javassist字节码技术 自定义Gradle插件参考 Gradle系列一 -- Groovy、Gradle和自定义Gradle插件 javassist参考 javassist使用全解析 截屏2020-11-25 下午9.20.30.png 关键代码,有点长

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.dex
dx --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的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);
}
复制代码

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热补丁轻量级方案

方案

回到这张图,从dvmResolveClass方法入手,提早解析patch类。 image.png 一开始想到的方案是提早使用"const-class" 或者 "instance-of"指令建立类,fromUnverifiedConstant = true,绕过dex检测。实际也成功了。但有两个问题:

  • 怎么提早知道哪些补丁类?
  • 或者干脆引用全部类?性能问题?如何实现?
public class ApplicationApp extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        DexInstaller.installDex(base, this.getExternalCacheDir().getAbsolutePath() + "/patch.dex");
		//会执行 const-class 指令
        Log.d("alvin", "bug class:" + com.a.fix.M.class);
}    
复制代码

QFix放弃了此直接load patch class的方案。通过分析,

  • 补丁包中的class数量是有限的。
  • apk中dex文件的数量也是有限的。

获得以下方案:

  • 构建apk时,dex预先埋入空白类,同时获得每一个dex与空白类的关联文件。
  • 构建补丁包,映射关联即bug dex的空白类与补丁类在原dex中的 classIdx。
  • -----------运行app,加载补丁包-------------
  • 使用java方法,调用classLoader.loadClass(空白类name)
  • 使用jni方法,调用 dvmFindLoadedClass(空白类descriptor)
  • 使用jni方法,调用dvmResolveClass(referrer:空白类,classIdx,fromUnverifiedConstant:true)

至于怎么找到这个方法的,固然就是源码里面游荡了。

截屏2020-12-02 下午9.46.32.png

实操

所有实现代码都在github中

空白类注入到Dex

自定义gradle插件,使用smali操做dexfile,注入class。

1.buildSrc/build.gradle加入依赖

//buildSrc/build.gradle
dependencies {
 	...
    compile group: 'org.smali', name: 'dexlib2', version: '2.2.4'
 	...
}
复制代码

2.plugin代码

class QFixPlugin implements Plugin<Project> {

    void apply(Project project1) {
        project1.afterEvaluate { project ->
            project.tasks.mergeDexDebug {
                doLast {
                    println 'QFixPlugin inject Class after mergeDexDebug'
                    project.tasks.mergeDexDebug.getOutputs().getFiles().each { dir ->
                        println "outputs: " + dir
                        if (dir != null && dir.exists()) {
                            def files = dir.listFiles()
                            files.each { file ->
                                String dexfilepath = file.getAbsolutePath()
                                println "Outputs Dex file's path: " + dexfilepath
                                  InjectClassHelper.injectHackClass(dexfilepath)
                            }
                        }
                    }
                }
            }
        }
    }
}

复制代码

InjectClassHelper.java

public class InjectClassHelper {

    public static void injectHackClass(String dexPath) {
        try {
            File file = new File(dexPath);
            String fileName = file.getName();
            String indexStr = fileName.split("\\.")[0].replace("classes", "");
            System.out.println(" =============indexStr:"+indexStr);
            String className = "com.a.Hack"+ indexStr;
            String classType = "Lcom/a/Hack" + indexStr + ";";
            
            DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexPath, Opcodes.getDefault());
			ImmutableDexFile immutableDexFile = ImmutableDexFile.of(dexFile);

            Set<ClassDef> classDefs = new HashSet<>();
            for (ImmutableClassDef classDef : immutableDexFile.getClasses()) {
                classDefs.add(classDef);
            }
            ImmutableClassDef immutableClassDef = new ImmutableClassDef(
                    classType,
                    AccessFlags.PUBLIC.getValue(),
                    "Ljava/lang/Object;",
                    null, null, null, null, null);
            classDefs.add(immutableClassDef);

            String resultPath = dexPath;
            File resultFile = new File(resultPath);
            if (resultFile != null && resultFile.exists()) resultFile.delete();
            DexFileFactory.writeDexFile(resultPath, new DexFile() {
                
                @Override
                public Set<ClassDef> getClasses() {
                    return new HashSet<>(classDefs);
                }

                @Override
                public Opcodes getOpcodes() {
                    return dexFile.getOpcodes();
                }
            });
            System.out.println("Outputs injectHackClass: " + file.getName() + ":" + className);
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
复制代码

Mapping

Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes2.dex Outputs injectHackClass: classes2.dex:com.a.Hack2 Outputs Dex file's path: /Users/mawenqiang/Documents/demo_project/hotfix_dexload/dexload_QFix/build/intermediates/dex/debug/out/classes.dex
Outputs injectHackClass: classes.dex:com.a.Hack
复制代码

执行指令 dexdump

#dexdump -h classes2.dex > classes2.dump

Class #1697 header:
class_idx           : 2277 #class_idx
......
Class descriptor  : 'Lcom/a/fix/M;'
......
复制代码

咱们能够获得mapping.txt

classes2.dex:com.a.Hack2:com.a.fix.M:2277
复制代码

截屏2020-12-04 上午10.46.12.png

导入patch.dex和mapping.text

load patch.dex

patch.dex的生成和加载不变,参看本文上方。

resolve 补丁M.class

一样在ApplicationApp.attachBaseContext()中执行,在load patch以后执行。 代码文件 ApplicationApp.java

  • 解析Mapping.txt,获得hackClassName,patchClassIdx
  • classLoader.loadClass(com.a.Hack2)
  • nativeResolveClass(hackClassDescriptor, patchClassIdx)
public static void resolvePatchClasses(Context context) {
        try {
            BufferedReader br = new BufferedReader(new FileReader(context.getExternalCacheDir().getAbsolutePath() + "/classIdx.txt"));
            String line = "";
            while (!TextUtils.isEmpty(line = br.readLine())) {
                String[] ss = line.split(":");
                //classes2.dex:com.a.Hack2:com.a.fix.M:2277
                if (ss != null && ss.length == 4) {
                    String hackClassName = ss[1];
                    long patchClassIdx = Long.parseLong(ss[3]);
                    Log.d("alvin", "readLine:" + line);
                    String hackClassDescriptor = "L" + hackClassName.replace('.', '/') + ";";
                    Log.d("alvin", "classNameToDescriptor: " + hackClassName + " --> " + hackClassDescriptor);
                    ResolveTool.loadClass(context, hackClassName);
                    ResolveTool.nativeResolveClass(hackClassDescriptor, patchClassIdx);
                }
            }
            br.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /** * * "descriptor" should have the form "Ljava/lang/Class;" or * * "[Ljava/lang/Class;", i.e. a descriptor and not an internal-form * * class name. * * @param referrerDescriptor * @param classIdx * @return */
    public static native boolean nativeResolveClass(String referrerDescriptor, long classIdx);

    public static void loadClass(Context context, String className) {
        try {
            Log.d("alvin", context.getClassLoader().loadClass(className).getSimpleName());
        } catch (Exception e) {
            e.printStackTrace();
            Log.d("alvin", e.getMessage());
        }
    }
复制代码

nativeResolveClass 就是正常的jni方法,代码实际也是简单的。

#include <jni.h>
#include <android/log.h>
#include <dlfcn.h>

#define LOG_TAG "alvin"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

//方法指针
void *(*dvmFindLoadedClass)(const char *);

//方法指针
void *(*dvmResolveClass)(const void *, unsigned int, bool);


extern "C" jboolean Java_com_a_dexload_qfix_ResolveTool_nativeResolveClass(JNIEnv *env, jclass thiz, jstring referrerDescriptor, jlong classIdx) {
    LOGE("enter nativeResolveClass");
    void *handle = 0;
    handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);
    if (!handle)  LOGE("dlopen libdvm.so fail");
    if (!handle) return false;

    const char *loadClassSymbols[3] = {
            "_Z18dvmFindLoadedClassPKc", "_Z18kvmFindLoadedClassPKc", "dvmFindLoadedClass"};
    for (int i = 0; i < 3; i++) {
        dvmFindLoadedClass = reinterpret_cast<void *(*)(const char *)>(
                dlsym(handle, loadClassSymbols[i]));
        if (dvmFindLoadedClass) {
            LOGE("dlsym dvmFindLoadedClass success %s", loadClassSymbols[i]);
            break;
        }
    }

    const char *resolveClassSymbols[2] = {"dvmResolveClass", "vResolveClass"};
    for (int i = 0; i < 2; i++) {
        dvmResolveClass = reinterpret_cast<void *(*)(const void *, unsigned int, bool)>(
                dlsym(handle, resolveClassSymbols[i]));
        if (dvmResolveClass) {
            LOGE("dlsym dvmResolveClass success %s", resolveClassSymbols[i]);
            break;
        }
    }
    if (!dvmFindLoadedClass)  LOGE("dlsym dvmFindLoadedClass fail");
    if (!dvmResolveClass)  LOGE("dlsym dvmResolveClass fail");
    if (!dvmFindLoadedClass || !dvmResolveClass) return false;

    const char *descriptorChars = (*env).GetStringUTFChars(referrerDescriptor, 0);
    //referrerClassObj 即为 com.a.Hack2
    void *referrerClassObj = dvmFindLoadedClass(descriptorChars);
    dvmResolveClass(referrerClassObj, classIdx, true);
    return true;
}
复制代码

到此,代码就所有实现了。

相关文章
相关标签/搜索