熱修復類加載之pre-verified問題

pre-verified問題

摘抄自 熱修復之冷啓動類加載原理與實現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

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.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
複製代碼

dvmResolveMethod

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文件驗證優化

回頭在來看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熱補丁輕量級方案

相關文章
相關標籤/搜索