安全矩阵

 找回密码
 立即注册
搜索
查看: 2413|回复: 0

Android加壳脱壳学习—动态加载和类加载机制详解

[复制链接]

189

主题

191

帖子

903

积分

高级会员

Rank: 4

积分
903
发表于 2022-3-19 23:42:16 | 显示全部楼层 |阅读模式
本帖最后由 margin 于 2022-3-19 23:57 编辑

原文链接:Android加壳脱壳学习—动态加载和类加载机制详解 (qq.com)




前言

最近一直在学习Android 加壳和脱壳,在进行Android加壳和脱壳的学习中,第一步便是深入理解类加载器和动态加载二者之间的关系。
本文详细的介绍了类加载器和动态加载之间的关系和原理,之所以详细的讲解两者之间的关系,一是学习脱壳和加壳的需要,二是为后面Android插件化漏洞挖掘的讲解做铺垫。


类加载器

Android中的类加载器机制与JVM一样遵循双亲委派模式。
1.双亲委派模式
(1)双亲委派模式定义
  1. (1)加载.class文件时,以递归的形式逐级向上委托给父加载器ParentClassLoader加载,如果加载过了,就不用再加载一遍(2)如果父加载器没有加载过,继续委托给父加载器去加载,一直到这条链路的顶级,顶级ClassLoader如果没有加载过,则尝试加载,加载失败,则逐级向下交还调用者加载
复制代码


  1. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
  2.            //1.先检查是否已经加载过--findLoaded
  3.            Class<?> c = findLoadedClass(name);
  4.            if (c == null) {
  5.                try {
  6.                    //2.如果自己没加载过,存在父类,则委托父类
  7.                    if (parent != null) {
  8.                        c = parent.loadClass(name, false);
  9.                    } else {
  10.                        c = findBootstrapClassOrNull(name);
  11.                    }
  12.                } catch (ClassNotFoundException e) {
  13.                }

  14.                if (c == null) {
  15.                    //3.如果父类也没加载过,则尝试本级classLoader加载
  16.                    c = findClass(name);
  17.                }
  18.            }
  19.           return c;
  20.    }
复制代码


               if (c == null) {                   //3.如果父类也没加载过,则尝试本级classLoader加载                   c = findClass(name);               }           }          return c;   }
代码解释:
① 先检查自己是否已经加载过class文件,用findLoadedClass方法,如果已经加载了直接返。
② 如果自己没有加载过,存在父类,则委派父类去加载,用parent.loadClass(name,false)方法,此时会向上传递,然后去父加载器中循环第1步,一直到顶级ClassLoader。
③ 如果父类没有加载,则尝试本级classLoader加载,如果加载失败了就会向下传递,交给调用方式实现.class文件的加载。

(2)双亲委派模式加载流程

(3)双亲委派的作用
① 防止同一个.class文件重复加载。
② 对于任意一个类确保在虚拟机中的唯一性。由加载它的类加载器和这个类的全类名一同确立其在Java虚拟机中的唯一性。
③ 保证.class文件不被篡改,通过委派方式可以保证系统类的加载逻辑不被篡改。

2. Android中类加载机制
(1)Android基本类预加载
我们了解Android基本类预加载,首先我们回顾上文的Dalvik虚拟机启动相关:

我们执行app_process程序,进入main函数里面,然后进行AndroidRuntime::start:


Zygote native 进程主要工作:
① 创建虚拟机–startVM
② 注册JNI函数–startReg
③ 通过JNI知道Java层的com.android.internal.os.ZygoteInit 类,调用main 函数,进入java 世界

然后进入Java层:




Zygote总结:
① 解析init.zygote.rc中的参数,创建AppRuntime并调用AppRuntime.start()方法。
② 调用AndroidRuntime的startVM()方法创建虚拟机,再调用startReg()注册JNI函数。
③ 通过JNI方式调用ZygoteInit.main(),第一次进入Java世界。
④ registerZygoteSocket()建立socket通道,zygote作为通信的服务端,用于响应客户端请求。
⑤ preload()预加载通用类、drawable和color资源、openGL以及共享库以及WebView,用于提高app启动效率。
⑥ 通过startSystemServer(),fork得力帮手system_server进程,也是Java Framework的运行载体(下面讲到system server再详细讲解)。
⑦ 调用runSelectLoop(),随时待命,当接收到请求创建新进程请求时立即唤醒并执行相应工作。
Android的类加载机制和JVM一样遵循双亲委派模式,在dalvik/art启动时将所有Java基本类和Android系统框架的基本类加载进来,预加载的类记录在/frameworks/base/config/preloaded-classes中。

  1. android.R$styleable
  2. android.accessibilityservice.AccessibilityServiceInfo$1
  3. android.accessibilityservice.AccessibilityServiceInfo
  4. android.accessibilityservice.IAccessibilityServiceClient$Stub$Proxy
  5. android.accessibilityservice.IAccessibilityServiceClient$Stub
  6. android.accessibilityservice.IAccessibilityServiceClient
  7. android.accounts.AbstractAccountAuthenticator$Transport
  8. android.accounts.AbstractAccountAuthenticator
  9. android.accounts.Account$1
  10. android.accounts.Account
  11. ...

  12. java.lang.Short
  13. java.lang.StackOverflowError
  14. java.lang.StackTraceElement
  15. java.lang.StrictMath
  16. java.lang.String$1
  17. java.lang.String$CaseInsensitiveComparator
  18. java.lang.String
  19. java.lang.StringBuffer
  20. java.lang.StringBuilder
  21. java.lang.StringFactory
  22. java.lang.StringIndexOutOfBoundsException
  23. java.lang.System$PropertiesWithNonOverrideableDefaults
  24. java.lang.System
  25. java.lang.Thread$1
  26. ...
复制代码

这些类只需要在Zygote进程启动时加载一遍就可以了,后续没一个APP或Android运行时环境的进程,都是从Zygote中fork出来,天然保留了加载过的类缓存。 ZygoteInit.preload()


  1. static void preload(TimingsTraceLog bootTimingsTraceLog) {
  2.     // ...省略
  3.     preloadClasses();
  4.     // ...省略
  5. }

  6. private static void preloadClasses() {
  7.     final VMRuntime runtime = VMRuntime.getRuntime();

  8.     // 读取 preloaded_classes 文件
  9.     InputStream is;
  10.     try {
  11.         is = new FileInputStream(PRELOADED_CLASSES);
  12.     } catch (FileNotFoundException e) {
  13.         Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
  14.         return;
  15.     }

  16.     // ...省略

  17.     try {
  18.         BufferedReader br =
  19.                 new BufferedReader(new InputStreamReader(is), Zygote.SOCKET_BUFFER_SIZE);

  20.         int count = 0;
  21.         String line;
  22.         while ((line = br.readLine()) != null) {
  23.             // Skip comments and blank lines.
  24.             line = line.trim();
  25.             if (line.startsWith("#") || line.equals("")) {
  26.                 continue;
  27.             }

  28.             Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
  29.             try {
  30.                 // 逐行加载基本类
  31.                 Class.forName(line, true, null);
  32.                 count++;
  33.                 // ...省略
  34.             } catch (Throwable t) {
  35.                 // ...省略
  36.             }
  37.         }

  38.         // ...省略
  39.     } catch (IOException e) {
  40.         Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
  41.     } finally {
  42.         // ...省略
  43.     }
  44. }
复制代码

(2)Android类加载器层级关系及分析



Android中的ClassLoader类型分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种是BootClassLoader、DexClassLoader、PathClassLoader。
① BootClassLoader:Android平台上所有Android系统启动时会使用BootClassLoader来预加载常用的类。
② BaseDexClassLoader:实际应用层类文件的加载,而真正的加载委托给pathList来完成。
③ DexClassLoader:可以加载dex文件以及包含dex的压缩文件(apk,dex,jar,zip),可以安装一个未安装的apk文件,一般为自定义类加载器。
④ PathClassLoader:可以加载系统类和应用程序的类,通常用来加载已安装的apk的dex文件。 补充:Android 提供的原生加载器叫做基础类加载器,包括:BootClassLoader,PathClassLoader,DexClassLoader,InMemoryDexClassLoader(Android 8.0 引入),DelegateLastClassLoader(Android 8.1 引入)。
<1> BootClassLoader

启动类加载器,用于加载 Zygote 进程已经预加载的基本类,可以推测它只需从缓存中加载。这是基类 ClassLoader 的一个内部类,是包访问权限,所以应用程序无权直接访问。

  1. public abstract class ClassLoader {
  2.     // ...省略

  3.     class BootClassLoader extends ClassLoader {
  4.         private static BootClassLoader instance;

  5.         public static synchronized BootClassLoader getInstance() {
  6.             if (instance == null) {
  7.                 instance = new BootClassLoader();
  8.             }

  9.             return instance;
  10.         }

  11.         public BootClassLoader() {
  12.             super(null);
  13.         }

  14.         @Override
  15.         protected Class<?> findClass(String name) throws ClassNotFoundException {
  16.             return Class.classForName(name, false, null);
  17.         }

  18.         // ...省略

  19.         @Override
  20.         protected Class<?> loadClass(String className, boolean resolve)
  21.                throws ClassNotFoundException {
  22.             Class<?> clazz = findLoadedClass(className);

  23.             if (clazz == null) {
  24.                 clazz = findClass(className);
  25.             }

  26.             return clazz;
  27.         }

  28.         // ...省略
  29.     }
  30. }
复制代码

源码分析:    我们可以看见,BootClassLoader没有父加载器,在缓存取不到类是直接调用自己的findClass()方法。    findClass()方法调用Class.classForName()方法,而ZygoteInit.preloadClasses()中,加载基本类是Class.forName()。
  1. ublic final class Class<T> implements java.io.Serializable,
  2.                               GenericDeclaration,
  3.                               Type,
  4.                               AnnotatedElement {
  5.     // ...省略

  6.     public static Class<?> forName(String className)
  7.                 throws ClassNotFoundException {
  8.         Class<?> caller = Reflection.getCallerClass();
  9.         return forName(className, true, ClassLoader.getClassLoader(caller));
  10.     }

  11.     public static Class<?> forName(String name, boolean initialize,
  12.                                    ClassLoader loader)
  13.         throws ClassNotFoundException
  14.     {
  15.         if (loader == null) {
  16.             loader = BootClassLoader.getInstance();
  17.         }
  18.         Class<?> result;
  19.         try {
  20.             result = classForName(name, initialize, loader);
  21.         } catch (ClassNotFoundException e) {
  22.             Throwable cause = e.getCause();
  23.             if (cause instanceof LinkageError) {
  24.                 throw (LinkageError) cause;
  25.             }
  26.             throw e;
  27.         }
  28.         return result;
  29.     }

  30.     // 本地方法
  31.     static native Class<?> classForName(String className, boolean shouldInitialize,
  32.             ClassLoader classLoader) throws ClassNotFoundException;

  33.     // ...省略
  34. }
复制代码


我们可以发现,预加载时,ZygoteInit.preloadClasses()中调用Class.forName(),实际是指定BootClassLoader为类加载器,且只需要在预加载的时候进行类初始化,只需要一次。
总之,通过 Class.forName() 或者 Class.classForName() 可以且仅可以直接加载基本类,一旦基本类预加载后,对于应用程序而言,我们虽然不能直接访问BootClassLoader,但可以通过Class.forName/Class.classForName加载。

无论是系统类加载器(PathClassLoader)还是自定义的类加载器(DexClassLoader),最顶层的祖先加载器默认是 BootClassLoader,与 JVM 一样,保证了基本类的类型安全。 Class文件加载:
① 通过Class.forName()方法动态加载
② 通过ClassLoader.loadClass()方法动态加载
类的加载分为3个步骤:1.装载(Load),2.链接(Link),3.初始化(Intialize)

​​

类加载时机:
1.隐式加载:① 创建类的实例,也就是new一个对象② 访问某个类或接口的静态变量,或者对该静态变量赋值③ 调用类的静态方法④ 反射Class.forName("android.app.ActivityThread")⑤ 初始化一个类的子类(会首先初始化子类的父类)
2.显示加载:
① 使用LoadClass()加载
② 使用forName()加载

<2> PathClassLoader
主要用于系统和app的类加载器,其中optimizedDirectory为null, 采用默认目录/data/dalvik-cache/。
PathClassLoader 是作为应用程序的系统类加载器,也是在 Zygote 进程启动的时候初始化的(基本流程为:ZygoteInit.main() -> ZygoteInit.forkSystemServer() -> ZygoteInit.handleSystemServerProcess() -> ZygoteInit.createPathClassLoader()。在预加载基本类之后执行),所以每一个 APP 进程从 Zygote 中 fork 出来之后都自动携带了一个 PathClassLoader,它通常用于加载 apk 里面的 .dex 文件。
<3> DexClassLoader
可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载, 但必须是app私有可写目录来缓存odex文件. 能够加载系统没有安装的apk或者jar文件, 因此很多热修复和插件化方案都是采用DexClassLoader。

  1. public class
  2. DexClassLoader extends BaseDexClassLoader {

  3.    public DexClassLoader(String dexPath, String optimizedDirectory,
  4.             String librarySearchPath, ClassLoader parent) {
  5.         super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
  6.     }
  7. }
复制代码


总结:
    我们可以发现DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了自己的构造函数,没有额外的实现。

区别:
    DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载。

<4> BaseDexClassLoader

  1. public class BaseDexClassLoader extends ClassLoader {
  2.     private final DexPathList pathList;  //记录dex文件路径信息

  3.     public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
  4.         super(parent);
  5.         this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
  6.     }
  7. }
复制代码

dexPath: 包含目标类或资源的apk/jar列表;当有多个路径则采用:分割;optimizedDirectory: 优化后dex文件存在的目录, 可以为null;libraryPath: native库所在路径列表;当有多个路径则采用:分割;ClassLoader:父类的类加载器。
BaseDexClassLoader会初始化dexPathList,收集dex文件和Native文件动态库。 初始化: DexPathList: 该类主要用来查找Dex、SO库的路径,并这些路径整体呈一个数组。
  1. final class DexPathList {
  2.     private Element[] dexElements;
  3.     private final List<File> nativeLibraryDirectories;
  4.     private final List<File> systemNativeLibraryDirectories;

  5.     final class DexPathList {
  6.     public DexPathList(ClassLoader definingContext, String dexPath,
  7.             String libraryPath, File optimizedDirectory) {
  8.         ...
  9.         this.definingContext = definingContext;
  10.         ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();

  11.         //记录所有的dexFile文件
  12.         this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);

  13.         //app目录的native库
  14.         this.nativeLibraryDirectories = splitPaths(libraryPath, false);
  15.         //系统目录的native库
  16.         this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
  17.         List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
  18.         allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
  19.         //记录所有的Native动态库
  20.         this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions);
  21.         ...
  22.     }
  23. }
复制代码
DexPathList初始化过程,主要收集以下两个变量信息:(1)dexElements: 根据多路径的分隔符“;”将dexPath转换成File列表,记录所有的dexFile(2)nativeLibraryPathElements: 记录所有的Native动态库, 包括app目录的native库和系统目录的native库
makePathElements:
  1. private static Element[] makePathElements(List<File> files, File optimizedDirectory,
  2.         List<IOException> suppressedExceptions) {
  3.     return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
  4. }
复制代码
makeDexElements:
makeDexElements方法的作用是获取一个包含dex文件的元素集合。
  1. private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
  2.         List<IOException> suppressedExceptions, ClassLoader loader) {
  3.     return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false);
  4. }

  5. private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
  6.         List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  7.   Element[] elements = new Element[files.size()];  //获取文件个数
  8.   int elementsPos = 0;
  9.   for (File file : files) {
  10.       if (file.isDirectory()) {
  11.           elements[elementsPos++] = new Element(file);
  12.       } else if (file.isFile()) {
  13.           String name = file.getName();
  14.           DexFile dex = null;
  15.           //匹配以.dex为后缀的文件
  16.           if (name.endsWith(DEX_SUFFIX)) {
  17.               dex = loadDexFile(file, optimizedDirectory, loader, elements);
  18.               if (dex != null) {
  19.                   elements[elementsPos++] = new Element(dex, null);
  20.               }
  21.           } else {
  22.               dex = loadDexFile(file, optimizedDirectory, loader, elements);            
  23.               if (dex == null) {
  24.                   elements[elementsPos++] = new Element(file);
  25.               } else {
  26.                   elements[elementsPos++] = new Element(dex, file);
  27.               }
  28.           }
  29.           if (dex != null && isTrusted) {
  30.             dex.setTrusted();
  31.           }
  32.       } else {
  33.           System.logW("ClassLoader referenced unknown path: " + file);
  34.       }
  35.   }
  36.   if (elementsPos != elements.length) {
  37.       elements = Arrays.copyOf(elements, elementsPos);
  38.   }

  39.   return elements;
  40. }
复制代码

该方法的主要功能是创建Element数组。
loadDexFile: 加载DexFile文件,而且会把优化后的dex文件缓存到对应目录。
  1. private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
  2.                                    Element[] elements)
  3.         throws IOException {
  4.     if (optimizedDirectory == null) {
  5.         return new DexFile(file, loader, elements);  //创建DexFile对象
  6.     } else {
  7.         String optimizedPath = optimizedPathFor(file, optimizedDirectory);
  8.         return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
  9.     }
  10. }
复制代码

DexFile: 用来描述Dex文件,Dex的加载以及Class的查找都是由该类调用它的native方法完成的。

  1. DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
  2.         throws IOException {
  3.     this(file.getPath(), loader, elements);
  4. }

  5. DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
  6.     mCookie = openDexFile(fileName, null, 0, loader, elements);
  7.     mInternalCookie = mCookie;
  8.     mFileName = fileName;
  9. }
复制代码

openDexFile:
  1. private static Object openDexFile(String sourceName, String outputName, int flags,
  2.         ClassLoader loader, DexPathList.Element[] elements) throws IOException {
  3.     return openDexFileNative(new File(sourceName).getAbsolutePath(),
  4.                              (outputName == null) ? null : new File(outputName).getAbsolutePath(),
  5.                              flags,
  6.                              loader,
  7.                              elements);
  8. }
复制代码

  1. 此时参数取值说明:
  2. sourceName为PathClassLoader构造函数传递的dexPath中以分隔符划分之后的文件名;
  3. outputName为null;
  4. flags = 0
  5. loader为null;
  6. elements为makeDexElements()过程生成的Element数组;
复制代码


openDexFileNative:
  1. static jobject DexFile_openDexFileNative(JNIEnv* env,
  2.                                          jclass,
  3.                                          jstring javaSourceName,
  4.                                          jstring javaOutputName ATTRIBUTE_UNUSED,
  5.                                          jint flags ATTRIBUTE_UNUSED,
  6.                                          jobject class_loader,
  7.                                          jobjectArray dex_elements) {
  8.   ScopedUtfChars sourceName(env, javaSourceName);
  9.   if (sourceName.c_str() == nullptr) {
  10.     return 0;
  11.   }
  12.   Runtime* const runtime = Runtime::Current();
  13.   ClassLinker* linker = runtime->GetClassLinker();
  14.   std::vector<std::unique_ptr<const DexFile>> dex_files;
  15.   std::vector<std::string> error_msgs;
  16.   const OatFile* oat_file = nullptr;

  17.   dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(),
  18.                                                                class_loader,
  19.                                                                dex_elements,
  20.                                                                /*out*/ &oat_file,
  21.                                                                /*out*/ &error_msgs);

  22.   if (!dex_files.empty()) {
  23.     jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files);
  24.     ...
  25.     return array;
  26.   } else {
  27.     ...
  28.     return nullptr;
  29.   }
  30. }
复制代码
这样就完成了dex的加载过程,而BaseDexClassLoader派生出两个子类加载器:PathClassLoader和DexClassLoader。

Android中如果parent类加载器加载不到类,最终还是会调用ClassLoader对象自己的findClass()方法。 loadClass()加载:
  1. public abstract class ClassLoader {

  2.     public Class<?> loadClass(String className) throws ClassNotFoundException {
  3.         return loadClass(className, false);
  4.     }

  5.     protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
  6.         //判断当前类加载器是否已经加载过指定类,若已加载则直接返回
  7.         Class<?> clazz = findLoadedClass(className);

  8.         if (clazz == null) {
  9.             //如果没有加载过,则调用parent的类加载递归加载该类,若已加载则直接返回
  10.             clazz = parent.loadClass(className, false);

  11.             if (clazz == null) {
  12.                 //还没加载,则调用当前类加载器来加载
  13.                 clazz = findClass(className);
  14.             }
  15.         }
  16.         return clazz;
  17.     }
  18. }
复制代码
该方法的加载流程如下:
① 判断当前类加载器是否已经加载过指定类,若已加载则直接返回,否则继续执行;
② 调用parent的类加载递归加载该类,检测是否加载,若已加载则直接返回,否则继续执行;
③ 调用当前类加载器,通过findClass加载。

findLoadedClass: [-> ClassLoader.java]
  1. protected final Class<?> findLoadedClass(String name) {
  2.     ClassLoader loader;
  3.     if (this == BootClassLoader.getInstance())
  4.         loader = null;
  5.     else
  6.         loader = this;
  7.     return VMClassLoader.findLoadedClass(loader, name);
  8. }
复制代码
findClass: [-> BaseDexClassLoader.java]

  1. public class BaseDexClassLoader extends ClassLoader {
  2.     protected Class<?> findClass(String name) throws ClassNotFoundException {
  3.         Class c = pathList.findClass(name, suppressedExceptions);
  4.         ...
  5.         return c;
  6.     }
  7. }
复制代码
DexPathList.findClass:
  1. public Class findClass(String name, List<Throwable> suppressed) {
  2.     for (Element element : dexElements) {
  3.         DexFile dex = element.dexFile;
  4.         if (dex != null) {
  5.             //找到目标类,则直接返回
  6.             Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
  7.             if (clazz != null) {
  8.                 return clazz;
  9.             }
  10.         }
  11.     }
  12.     return null;
  13. }
复制代码

代码解释:    一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组 dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。
热修复原理:    现在很多热修复技术就是把修复的dex文件放在DexPathList中Element[]数组的前面,这样就实现了修复后的Class抢先加载了,达到了修改bug的目的。
DexFile.loadClassBinaryName:

  1. public final class DexFile {

  2.     public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
  3.         return defineClass(name, loader, mCookie, suppressed);
  4.     }

  5.     private static Class defineClass(String name, ClassLoader loader, Object cookie, List<Throwable> suppressed) {
  6.         Class result = null;
  7.         try {
  8.             result = defineClassNative(name, loader, cookie);
  9.         } catch (NoClassDefFoundError e) {
  10.             if (suppressed != null) {
  11.                 suppressed.add(e);
  12.             }
  13.         } catch (ClassNotFoundException e) {
  14.             if (suppressed != null) {
  15.                 suppressed.add(e);
  16.             }
  17.         }
  18.         return result;
  19.     }
  20. }
复制代码

defineClassNative()这是native方法。 defineClassNative: [-> dalvik_system_DexFile.cc]
  1. static jclass DexFile_defineClassNative(JNIEnv* env, jclass, jstring javaName, jobject javaLoader,
  2.                                         jobject cookie) {
  3.   std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
  4.   if (dex_files.get() == nullptr) {
  5.     return nullptr; //dex文件为空, 则直接返回
  6.   }

  7.   ScopedUtfChars class_name(env, javaName);
  8.   if (class_name.c_str() == nullptr) {
  9.     return nullptr; //类名为空, 则直接返回
  10.   }

  11.   const std::string descriptor(DotToDescriptor(class_name.c_str()));
  12.   const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str())); //将类名转换为hash码
  13.   for (auto& dex_file : *dex_files) {
  14.     const DexFile::ClassDef* dex_class_def = dex_file->FindClassDef(descriptor.c_str(), hash);
  15.     if (dex_class_def != nullptr) {
  16.       ScopedObjectAccess soa(env);
  17.       ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
  18.       class_linker->RegisterDexFile(*dex_file);
  19.       StackHandleScope<1> hs(soa.Self());
  20.       Handle<mirror::ClassLoader> class_loader(
  21.           hs.NewHandle(soa.Decode<mirror::ClassLoader*>(javaLoader)));
  22.       //获取目标类
  23.       mirror::Class* result = class_linker->DefineClass(soa.Self(), descriptor.c_str(), hash,
  24.                                                         class_loader, *dex_file, *dex_class_def);
  25.       if (result != nullptr) {
  26.         // 找到目标对象
  27.         return soa.AddLocalReference<jclass>(result);
  28.       }
  29.     }
  30.   }
  31.   return nullptr; //没有找到目标类
  32. }
复制代码

在native层创建目标类的对象并添加到虚拟机列表。

​​



我们继续分析Native层可以发现:
  1. DexFile.defineClassNative() 的实现在 /art/runtime/native/dalvik_system_DexFile.cc,最终由 ClassLinker.DefineClass() 实现
  2. Class.classForName() 的实现在 /art/runtime/native/java_lang_Class.cc,最终由 ClassLinker.FindClass() 实现
复制代码

ClassLinker核心原理:
先从已加载类的 class_table 中查询,若找到则直接返回;若找不到则说明该类是第一次加载,则执行加载流程,其中可能需要穿插加载依赖的类,加载完成后将其缓存到 class_table 中。
在 ClassLinker 中,会维护两类 class_table,一类针对基本类,一类针对其它的类。class_table 是作为缓存已经加载过的类的缓冲池。不管以什么样的方式去加载类,都需要先从 class_table 中先进行查询以提高加载性能。
ClassLinker 在加载类的时候遇到该类依赖的类,进行穿插加载依赖类:

我们总结BaseDexClassLoader初始化和加载原理:

Android类加载详细流程:


3.案例
(1)验证类加载器
我们验证App中的MainActivity类加载器和系统类String类的类加载器:
  1. ClassLoader thisclassloader = MainActivity.class.getClassLoader();
  2. ClassLoader StringClassloader = String.class.getClassLoader();
  3. Log.e("ClassLoader1","MainActivity is in" + thisclassloader.toString());
  4. Log.e("ClassLoader1","String is in" + StringClassloader.toString());
复制代码


我们可以明显发现PathClassLoader加载已安装的APK类加载器,而BootClassLoader加载系统预安装的类。
(2)遍历父类加载器
  1. public static  void printClassLoader(ClassLoader classLoader) {
  2.        Log.e("printClassLoader","this->"+ classLoader.toString());
  3.        ClassLoader parent = classLoader.getParent();
  4.        while (parent!=null){
  5.            Log.i("printClassLoader","parent->"+parent.toString());
  6.            parent = parent.getParent();
  7.        }
  8.    }
复制代码
(3)验证双亲委派机制

  1. try {
  2.             Class StringClass = thisclassloader.loadClass("java.lang.String");
  3.             Log.e("ClassLoader1","load StringClass!"+thisclassloader.toString());
  4.         } catch (ClassNotFoundException e) {
  5.             e.printStackTrace();
  6.             Log.e("ClassLoader1","load MainActivity fail!"+thisclassloader.toString());
  7.         }
复制代码
我们使用PathClassLoader去加载 String.class类,还是可以加载成功,因为双亲委派的机制。
(4)动态加载
这里我借用网上寒冰大佬动态加载的案例,来进一步讲述使用DexClassLoader类实现简单的动态加载插件dex,并验证ClassLoader的继承关系。 我们先编写一个测试类文件,然后生成dex文件。

我们先将dex文件放到模拟器的sdcard/下。

我们新建一个程序,然后编写主程序的代码,并授权sd读取权限。
  1. Context appContext = this.getApplication();
  2. testDexClassLoader(appContext,"/sdcard/classes.dex");
复制代码
  1. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
  2. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
复制代码

然后我们编写类加载器代码。
  1. private void testDexClassLoader(Context context, String dexfilepath) {
  2.         //构建文件路径:/data/data/com.emaxple.test02/app_opt_dex,存放优化后的dex,lib库
  3.         File optfile = context.getDir("opt_dex",0);
  4.         File libfile = context.getDir("lib_dex",0);

  5.         ClassLoader parentclassloader = MainActivity.class.getClassLoader();
  6.         ClassLoader tmpclassloader = context.getClassLoader();
  7.     //可以为DexClassLoader指定父类加载器
  8.         DexClassLoader dexClassLoader = new DexClassLoader(dexfilepath,optfile.getAbsolutePath(),libfile.getAbsolutePath(),parentclassloader);

  9.         Class clazz = null;
  10.         try {
  11.             clazz = dexClassLoader.loadClass("com.example.test.TestClass");
  12.         } catch (ClassNotFoundException e) {
  13.             e.printStackTrace();
  14.         }
  15.         if(clazz!=null){
  16.             try {
  17.                 Method testFuncMethod = clazz.getDeclaredMethod("test02");
  18.                 Object obj = clazz.newInstance();
  19.                 testFuncMethod.invoke(obj);
  20.             } catch (NoSuchMethodException e) {
  21.                 e.printStackTrace();
  22.             } catch (IllegalAccessException e) {
  23.                 e.printStackTrace();
  24.             } catch (InstantiationException e) {
  25.                 e.printStackTrace();
  26.             } catch (InvocationTargetException e) {
  27.                 e.printStackTrace();
  28.             }
  29.         }

  30.     }
复制代码
(5)获得类列表
我们通过getClassNameList来获取类列表
  1. private static native String[] getClassNameList(Object cookie);
复制代码

  1. public static void getClassListInClassLoader(ClassLoader classLoader){
  2.         //先拿到BaseDexClassLoader
  3.         try {
  4.             //拿到pathList
  5.             Class BaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
  6.             Field pathListField = BaseDexClassLoader.getDeclaredField("pathList");
  7.             pathListField.setAccessible(true);
  8.             Object pathListObj = pathListField.get(classLoader);

  9.             //拿到dexElements
  10.             Class DexElementClass = Class.forName("dalvik.system.DexPathList");
  11.             Field DexElementFiled = DexElementClass.getDeclaredField("dexElements");
  12.             DexElementFiled.setAccessible(true);
  13.             Object[]  dexElementObj = (Object[]) DexElementFiled.get(pathListObj);
  14.             //拿到dexFile
  15.             Class Element = Class.forName("dalvik.system.DexPathList$Element");
  16.             Field dexFileField = Element.getDeclaredField("dexFile");
  17.             dexFileField.setAccessible(true);
  18.             Class DexFile =Class.forName("dalvik.system.DexFile");
  19.             Field mCookieField = DexFile.getDeclaredField("mCookie");
  20.             mCookieField.setAccessible(true);
  21.             Field mFiledNameField = DexFile.getDeclaredField("mFileName");
  22.             mFiledNameField.setAccessible(true);
  23.             //拿到getClassNameList
  24.             Method getClassNameListMethod = DexFile.getDeclaredMethod("getClassNameList",Object.class);
  25.             getClassNameListMethod.setAccessible(true);

  26.             for(Object dexElement:dexElementObj){
  27.                 Object dexfileObj = dexFileField.get(dexElement);
  28.                 Object mCookiedobj = mCookieField.get(dexfileObj);
  29.                 String mFileNameobj = (String) mFiledNameField.get(dexfileObj);
  30.                 String[] classlist = (String[]) getClassNameListMethod.invoke(null,mCookiedobj);
  31.                 for(String classname:classlist){
  32.                     Log.e("classlist",classLoader.toString()+"-----"+mFileNameobj+"-----"+classname);
  33.                 }
  34.             }

  35.         } catch (ClassNotFoundException e) {
  36.             e.printStackTrace();
  37.         } catch (NoSuchFieldException e) {
  38.             e.printStackTrace();
  39.         } catch (IllegalAccessException e) {
  40.             e.printStackTrace();
  41.         } catch (NoSuchMethodException e) {
  42.             e.printStackTrace();
  43.         } catch (InvocationTargetException e) {
  44.             e.printStackTrace();
  45.         }
  46.     }
复制代码


实验总结

花了一段时间,断断续续总算把这篇类加载器和动态加载的帖子写完了,从中学习到了很多,这里如果有什么错误,就请各位大佬指正了。

参考文献:
http://gityuan.com/2017/03/19/android-classloader/
https://www.jianshu.com/p/7193600024e7
https://www.jianshu.com/p/ff489696ada2
https://www.jianshu.com/p/363a4ad0489d
https://github.com/huanzhiyazi/articles/issues/30
https://juejin.cn/post/6844903940094427150#heading-12




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-24 12:52 , Processed in 0.016959 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表