首页
社区
课程
招聘
[原创动画] [源码分析] --脱壳中, DEX 内存加载 和 磁盘路径方式加载
发表于: 2025-6-10 13:38 241

[原创动画] [源码分析] --脱壳中, DEX 内存加载 和 磁盘路径方式加载

2025-6-10 13:38
241

在脱壳中, 两种 DEX 内存加载 和 SD卡方式加载, 源代码分析.

InMemoryDexClassLoader 和 DexClassLoader 的流程的分析

动画采用4K录制,建议4k显示器观看.

分为两个部分, 为了视频的清晰, 暂时提供 ftp下载.

文件1, 2025-06-10_在脱壳中, 两种 DEX 内存加载 和 SD卡方式加载, 源代码分析(1).mkv

文件2, 2025-06-10_在脱壳中, 两种 DEX 内存加载 和 SD卡方式加载, 源代码分析(2).mkv

举例子, 有个同学问了你一个问题 , 就是老师说, 你搞懂了吗?
--- 回答, 搞懂了!
然后,老师说, 你把这个题,给那个同学讲一讲.
--- 于是, 思路不是太清晰, 好像懂了, 好像又没有懂.
---- 所以, 当你给别人真正 讲明白了的时候, 才开始更换角色思考. ==> 日语, 版本, 英语版本.

本篇目涉及两个,问题, 一个是课件, 一个是 通过 Android Studio 和 CLion 分别阅读 AOSP8.1 的 Java层源码和 CPP 层的源码.
过程中,带着繁琐的跳转, 没有人能够一次通过. 都是多遍通过.

在 2025 , 几乎不会没有人不用 AI , AI 什么都懂 ,但是不代表,你懂得, 所以, 只有你懂了, 才是真格. 没有严谨的 分析考究AI提供的依据 , AI 就是胡说!
使用它, 抱着最 朴素的观念, 就是证伪! (AI就是错的.)

InMemoryDexClassLoader 源码分析


???? 调用流程总览

InMemoryDexClassLoader (Java)
    └── BaseDexClassLoader (Java)
        └── DexPathList (Java)
            └── makeInMemoryDexElements (Java)
                └── DexFile (Java)
                    └── DexFile.openInMemoryDexFile (Java)
                        └── `DexFile.DexFile_createCookieWithDirectBuffer 或 `DexFile_createCookieWithArray (Native)
                            └── CreateSingleDexFileCookie (Native)
                                └── DexFile::CreateDexFile (Native)
                                    └──DexFile::Open (Native)
                                        └── DexFile::OpenCommon (Native)
                                            └── DexFile::DexFile 构造函数 (Native)

详细调用链与源码位置

1. InMemoryDexClassLoader(Java 层)

  • 位置libcore/dalvik/src/main/java/dalvik/system/InMemoryDexClassLoader.java

  • 作用:Java 层入口,允许从 ByteBuffer 加载 DEX 数据。

  • 调用:构造函数中调用父类 BaseDexClassLoader 的构造函数。

1
public InMemoryDexClassLoader(ByteBuffer[] dexBuffers, ClassLoader parent)

2. BaseDexClassLoader 构造函数 (Java)

  • 位置libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
    这里调用了父类构造函数,跟进到父类BaseDexClassLoader中

  • 作用:初始化 DexPathList,并将其赋值给成员变量 pathList

    1
    2
    3
    4
    5
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
          // TODO We should support giving this a library search path maybe.
          super(parent);
          this.pathList = new DexPathList(this, dexFiles);
      }

3. DexPathList 构造函数 (Java)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

  • 作用:解析 dexFiles,并调用 makeInMemoryDexElements 方法生成 dexElements

    1
    2
    3
    4
    5
    6
    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
        ...
            this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
        ...
      
        }

4. makeInMemoryDexElements 方法 (Java)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

  • 作用:遍历 dexPath 中的文件,调用 loadDexFile 方法加载每个 .dex 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
               List<IOException> suppressedExceptions) {
           Element[] elements = new Element[dexFiles.length];
           int elementPos = 0;
           for (ByteBuffer buf : dexFiles) {
               try {
                   DexFile dex = new DexFile(buf);
                   elements[elementPos++] = new Element(dex);
               } catch (IOException suppressed) {
                   System.logE("Unable to load dex file: " + buf, suppressed);
                   suppressedExceptions.add(suppressed);
               }
           }
           if (elementPos != elements.length) {
               elements = Arrays.copyOf(elements, elementPos);
           }
           return elements;
       }

5. DexFile 构造方法 (Java)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  • 作用:调用 openInMemoryDexFile 方法加载 Dex 文件到内存。

    1
    2
    3
    4
    5
    DexFile(ByteBuffer buf) throws IOException { // 构造函数,接受 ByteBuffer 作为参数,可能会抛出 IOException 异常
        mCookie = openInMemoryDexFile(buf); // 调用 openInMemoryDexFile 方法加载 Dex 文件到内存并返回 cookie // 这样做可以避免磁盘 I/O,提升加载速度
        mInternalCookie = mCookie; // 将 mCookie 赋值给 mInternalCookie // 这样做是为了保持内部状态的一致性,方便后续操作
        mFileName = null; // 初始化文件名为 null // 初始化为空,表示当前没有文件路径信息,可能是内存中的 Dex 文件
    }

6. DexFile.openInMemoryDexFile 方法 (Java)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  • 作用: 根据 ByteBuffer 类型决定使用 是direct buffer 还是 array buffer, 通过 createCookieWithDirectBuffer 方法,或者 createCookieWithArray 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
      // 定义静态方法,用于打开内存中的 dex 文件,并根据 ByteBuffer 类型决定使用何种方式处理 // 通过提供不同的实现方式,支持 direct buffer 和 array buffer
      private static Object openInMemoryDexFile(ByteBuffer buf) throws IOException {
          // 判断 ByteBuffer 是否是 direct 类型的缓冲区
          if (buf.isDirect()) {
              // 如果是 direct 缓冲区,使用直接内存创建 Cookie 对象
              return createCookieWithDirectBuffer(buf, buf.position(), buf.limit());
          // 如果不是 direct 类型的缓冲区
          } else {
              // 使用数组创建 Cookie对象,通过 buf.array() 直接访问底层数组,用于非 direct 缓冲区
              return createCookieWithArray(buf.array(), buf.position(), buf.limit());
          }
      }
     
      // 声明一个Native方法,使用直接缓冲区(ByteBuffer)创建 Cookie 对象
      // 使用直接缓冲区可以提高 I/O 操作的效率,避免了缓冲区的复制
    private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
     
      // 声明一个Native方法,使用字节数组(byte[])创建 Cookie 对象
      // 使用字节数组更为常见,适用于大多数情况下的内存操作
     private static native Object createCookieWithArray(byte[] buf, int start, int end);

7. DexFile. DexFile_createCookieWithDirectBuffer 或 DexFile_createCookieWithArray 方法 (Native)

  • 位置art/runtime/native/dalvik_system_DexFile.cc

  • Native查询: control + N 检索 clion 中的符号, symbol , 或者 使用 control + shift + F , 全文检索.

  • Clion中,查询Native 函数, Java 文件名 + "_" + JNI函数名 "DexFile_createCookieWithDirectBuffer "

  • 作用: 两种处理的分支是一样的, 只不过形态不一样. 都是走到最后一步,CreateSingleDexFileCookie( )

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    // 定义了一个 JNI 方法,用于通过直接缓冲区创建 dex 文件 cookie ==================>
    static jobject DexFile_createCookieWithDirectBuffer(JNIEnv* env,
                                                        jclass, // jclass 参数是类的引用,通常用于静态方法
                                                        jobject buffer, // 直接缓冲区对象 ,--> 直接对应 Java 第一参数
                                                        jint start, // 起始偏移量
                                                        jint end) { // 结束偏移量
         
      // 获取直接缓冲区的地址并转换为 uint8_t 类型的指针 // 这是为了直接操作内存,避免复制数据
      uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
      if (base_address == nullptr) { // 如果直接缓冲区的地址为空
        ScopedObjectAccess soa(env); // 创建 ScopedObjectAccess 对象,确保在 JNI 环境中操作时持有对象引用
        ThrowWrappedIOException("dexFileBuffer not direct"); // 抛出异常,提示缓冲区不是直接缓冲区
        return 0; // 返回 0 表示创建失败
      }
     
      // 分配内存映射,创建 dex 文件的内存映射对象 // 通过内存映射提高对大文件的处理效率
      std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
      if (dex_mem_map == nullptr) { // 如果内存映射失败
        DCHECK(Thread::Current()->IsExceptionPending()); // 确保异常已经被抛出
        return 0; // 返回 0 表示失败
      }
         
      // 计算数据的长度
      size_t length = static_cast<size_t>(end - start);
      // 将缓冲区中的数据复制到内存映射的起始位置 // 使用 memcpy 提高效率,避免逐字节处理 ==================>
      // ★ 此处的 dex_mem_map->Begin() 就是 dex 文件的起始地址 , length 是 dex 文件的长度
      memcpy(dex_mem_map->Begin(), base_address, length);
     
     
      // 创建 dex 文件 cookie,并返回  =================>   // ★ 核心跳转,  到最后就是一个 Memory Map.
      return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
     
     
    }
     
     
     
     
     
     // 定义了另一个 JNI 方法,通过字节数组创建 dex 文件 cookie  // ===========>=// DexFile 中 createCookieWithArray 函数对应的 C++ 函数
    static jobject DexFile_createCookieWithArray(JNIEnv* env,
                                                 jclass, // jclass 参数是类的引用,通常用于静态方法
                                                 jbyteArray buffer, // 字节数组缓冲区 --> 直接对应java 第一参数.
                                                 jint start, // 起始偏移量
                                                 jint end) { // 结束偏移量
         
      // 分配内存映射,创建 dex 文件的内存映射对象 // 内存映射提供了对内存区域的有效访问
      std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
      if (dex_mem_map == nullptr) { // 如果内存映射失败
        DCHECK(Thread::Current()->IsExceptionPending()); // 确保异常已经被抛出
        return 0; // 返回 0 表示失败
      }
         
      // 获取内存映射的起始位置,并转换为 jbyte* 类型
      auto destination = reinterpret_cast<jbyte*>(dex_mem_map.get()->Begin());
      // 从字节数组中获取指定范围的数据并写入到内存映射中 // GetByteArrayRegion 提供了对字节数组的高效访问
      env->GetByteArrayRegion(buffer, start, end - start, destination);
     
     
      // 创建 dex 文件 cookie,并返回 =========================> // ★ 核心跳转 ,到最后就是一个 Memory Map.
      return CreateSingleDexFileCookie(env, std::move(dex_mem_map)); 
     
     
    }

8. CreateSingleDexFileCookie 方法 (Native)

  • 位置art/runtime/native/dalvik_system_DexFile.cc

  • 作用:调用 CreateDexFile 方法接收 data 参数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 定义静态函数,用于创建一个单一的 Dex 文件 Cookie
    static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
         
     
     
     
      // 创建一个 DexFile 对象 dex_file,传入环境变量和数据,使用 std::move 传递数据 // 使用智能指针管理内存,避免手动释放
      std::unique_ptr<const DexFile> dex_file(CreateDexFile(env, std::move(data))); // ★ 核心语句,接收处理的参数.
         
     
         
         
      if (dex_file.get() == nullptr) { // 如果创建的 DexFile 为空
        DCHECK(env->ExceptionCheck()); // 检查是否抛出了异常,若有则进行调试输出 // 确保没有遗漏异常检查
        return nullptr; // 如果 DexFile 创建失败,返回空指针 // 失败时返回空值,防止后续操作使用无效对象
      }
      std::vector<std::unique_ptr<const DexFile>> dex_files; // 创建一个 DexFile 智能指针的 vector 用于保存多个 Dex 文件
      dex_files.push_back(std::move(dex_file)); // 将创建的 dex_file 添加到 vector 中,使用 std::move 转移所有权 // 使用 std::move 来避免不必要的复制
      return ConvertDexFilesToJavaArray(env, nullptr, dex_files); // 调用 ConvertDexFilesToJavaArray 函数将 Dex 文件转换为 Java 数组并返回 // 将 Dex 文件传递给 Java 环境以便进一步使用
    }

9. DexFile::CreateDexFile 方法 (Native)

  • 位置art/runtime/native/dalvik_system_DexFile.cc

  • 作用:调用 DexFile::Open 方法打开 .dex 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map) { // 创建一个函数,用于生成一个不可修改的 DexFile 对象,接受 JNIEnv 和唯一的 MemMap
      std::string location = StringPrintf("Anonymous-DexFile@%p-%p", // 使用 StringPrintf 格式化字符串,创建一个表示 DexFile 的位置
                                          dex_mem_map->Begin(), // 获取内存映射的起始地址
                                          dex_mem_map->End()); // 获取内存映射的结束地址 // 目的是创建一个唯一的标识符,方便调试
      std::string error_message; // 创建一个空的字符串用于存储错误信息
     
     
     
     
     
      std::unique_ptr<const DexFile> dex_file(DexFile::Open(location, // 调用 DexFile 的 Open 方法打开 Dex 文件
                                                            0, // 没有额外的标志位
                                                            std::move(dex_mem_map), // 将内存映射传递给 DexFile
                                                            /* verify */ true, // 启用验证
                                                            /* verify_location */ true, // 验证文件位置
                                                            &error_message)); // 传递错误信息的引用
     
     
     
     
      if (dex_file == nullptr) { // 如果 DexFile 打开失败
        ScopedObjectAccess soa(env); // 获取 JNIEnv 的访问权限
        ThrowWrappedIOException("%s", error_message.c_str()); // 抛出带有错误信息的 IO 异常
        return nullptr; // 返回空指针表示失败
      }
     
      if (!dex_file->DisableWrite()) { // 如果 DexFile 无法设置为只读
        ScopedObjectAccess soa(env); // 获取 JNIEnv 的访问权限
        ThrowWrappedIOException("Failed to make dex file read-only"); // 抛出设置只读失败的异常
        return nullptr; // 返回空指针表示失败
      }
     
      return dex_file.release(); // 返回释放后的 DexFile 指针,表示成功创建了 DexFile 对象
    }

10. DexFile::Open 方法 (Native)

  • 位置art/dex2oat/dex/dex_file.cc

  • 作用:调用 OpenCommon 方法进行通用的 .dex 文件打开逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    std::unique_ptr<const DexFile> DexFile::Open(const std::string& location, // 声明静态方法 Open,用于打开 Dex 文件 // 提供打开 Dex 文件的功能
                                                 uint32_t location_checksum, // 校验和,用于验证文件完整性 // 校验和可以确保文件未被篡改
                                                 std::unique_ptr<MemMap> map, // 内存映射对象,提供对文件的内存访问 // 使用内存映射提高文件访问性能
                                                 bool verify, // 是否验证文件内容 // 如果为真,增加校验来提高安全性
                                                 bool verify_checksum, // 是否验证校验和 // 如果为真,进一步验证文件的完整性
                                                 std::string* error_msg) { // 错误信息,存储打开文件时的错误信息 // 用于传递打开文件时的错误原因
      ScopedTrace trace(std::string("Open dex file from mapped-memory ") + location); // 创建一个跟踪对象,用于记录调用堆栈的相关信息 // 用于调试时追踪方法调用过程
      CHECK(map.get() != nullptr); // 检查 map 是否为 nullptr,如果为空抛出错误 // 保证 map 在使用前有效,防止空指针异常
     
      if (map->Size() < sizeof(DexFile::Header)) { // 如果内存映射的文件大小小于 DexFile 头部的大小
        *error_msg = StringPrintf( // 格式化并设置错误信息
            "DexFile: failed to open dex file '%s' that is too short to have a header", // 错误信息内容,提示文件过小无法解析
            location.c_str());
        return nullptr; // 返回 nullptr,表示文件打开失败 // 文件过小无法解析,返回空指针表示失败
      }
     
     
     
                        // 一般都是喜欢hook这里, 是一个热点地区, 使用Frida hook 这里, 拿到数据... dump出来...
      std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(), // 调用 OpenCommon 函数,进行文件解析 // 负责真正的文件解析
                                                     map->Size(), // 传入文件大小
                                                     location, // 传入文件路径
                                                     location_checksum, // 传入校验和
                                                     kNoOatDexFile, // 指定 OAT 文件标识
                                                     verify, // 是否验证文件
                                                     verify_checksum, // 是否验证校验和
                                                     error_msg); // 传入错误信息参数
     
     
      if (dex_file != nullptr) { // 如果文件成功打开
        dex_file->mem_map_ = std::move(map); // 将内存映射对象移交给 dex_file,确保文件数据的生命周期和 DexFile 一致 // 确保内存映射在 DexFile 关闭时释放
      }
      return dex_file; // 返回打开的 DexFile 对象,如果失败则返回 nullptr // 返回打开的 DexFile 对象或空指针
    }

11. DexFile::OpenCommon 方法 (Native)

  • 位置art/dex2oat/dex/dex_file.cc

  • 作用:验证 .dex 文件头部信息,并初始化 DexFile 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base, // 定义 OpenCommon 函数,用于打开 Dex 文件 // 该函数用于加载和验证 dex 文件
                                                 size_t size, // dex 文件的大小 // 需要指定文件的大小
                                                 const std::string& location, // dex 文件的路径 // 用于指定文件的位置
                                                 uint32_t location_checksum, // dex 文件路径的校验和 // 可能用于验证路径的完整性
                                                 const OatDexFile* oat_dex_file, // 可选的 oat 文件指针 // 用于包含 oat 文件的信息
                                                 bool verify, // 是否需要验证文件的有效性 // 控制是否执行验证
                                                 bool verify_checksum, // 是否验证校验和 // 控制是否进行校验和验证
                                                 std::string* error_msg, // 错误消息输出 // 用于返回错误信息
                                                 VerifyResult* verify_result) { // 验证结果输出 // 用于返回验证的结果
      if (verify_result != nullptr) { // 如果 verify_result 不是空指针
        *verify_result = VerifyResult::kVerifyNotAttempted; // 设置验证结果为未尝试验证 // 如果没有进行验证,返回初始状态
      }
     
      int pid = getpid();  // get the pid number to ceate_DEX Name
      char dexfilepath[100] = {0};  // get the Dex Path to  know where it is
      sprintf(dexfilepath,"/sdcard/size_%d_pid_%d_DexFile.dex",(int)size,pid); // xunlie ,  To wirte the Name of DEx in SDCARD/
     // int fd = open(dexfilepath,0 CREAT|0 RDWR,666);     // byte Stream input
      int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
      if(fd>0){
            int number = write(fd,base,size);  // write byte stream to Disk.
            if(number>0){
     
            }
            close(fd);
      }
     
     
     
     
     
        // ==================> 这里是 创建 一个 DexFile, 调用 New DexFile <========================
        // 创建 DexFile 对象 // 尝试用给定参数创建 DexFile 实例 
        // =====================> => 第二案例, 内部分析了又调用了, new DexFile()
      std::unique_ptr<DexFile> dex_file(new DexFile(base,
                                                    size, // dex 文件大小
                                                    location, // dex 文件路径
                                                    location_checksum, // dex 文件路径校验和
                                                    oat_dex_file)); // oat 文件指针
         // ==================> 这里是 创建 一个 DexFile, 调用 New DexFile <========================
         
     
     
     
         
      if (dex_file == nullptr) { // 如果 dex_file 创建失败
        *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(), // 设置错误消息 // 提供详细的错误信息,帮助定位问题
                                  error_msg->c_str()); // 包括错误内容
        return nullptr; // 返回空指针,表示打开失败
      }
      if (!dex_file->Init(error_msg)) { // 如果初始化 DexFile 失败
        dex_file.reset(); // 重置 dex_file 指针,释放资源
        return nullptr; // 返回空指针,表示初始化失败
      }
      if (verify && !DexFileVerifier::Verify(dex_file.get(), // 如果需要验证且验证失败
                                             dex_file->Begin(), // 获取文件开始位置
                                             dex_file->Size(), // 获取文件大小
                                             location.c_str(), // dex 文件路径
                                             verify_checksum, // 是否验证校验和
                                             error_msg)) { // 错误消息输出
        if (verify_result != nullptr) { // 如果 verify_result 不是空指针
          *verify_result = VerifyResult::kVerifyFailed; // 设置验证结果为验证失败
        }
        return nullptr; // 返回空指针,表示验证失败
      }
      if (verify_result != nullptr) { // 如果 verify_result 不是空指针
        *verify_result = VerifyResult::kVerifySucceeded; // 设置验证结果为验证成功
      }
      return dex_file; // 返回创建并验证成功的 dex_file
    }

12. DexFile::DexFile 构造函数 (Native)

  • 位置art/dex2oat/dex/dex_file.cc

  • 作用:初始化 DexFile 对象的各个成员,准备后续的类加载等操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    DexFile::DexFile(const uint8_t* base, // 构造函数的参数 base,指向 dex 文件的内存开始位置
                     size_t size, // 构造函数的参数 size,表示 dex 文件的大小
                     const std::string& location, // 构造函数的参数 location,表示文件位置
                     uint32_t location_checksum, // 构造函数的参数 location_checksum,表示文件位置的校验和
                     const OatDexFile* oat_dex_file) // 构造函数的参数 oat_dex_file,表示与此 dex 文件相关的 Oat 文件
        : begin_(base), // 初始化 begin_,将 base 赋值给 begin_,表示 dex 文件的起始位置
          size_(size), // 初始化 size_,将 size 赋值给 size_,表示 dex 文件的大小
          location_(location), // 初始化 location_,将 location 赋值给 location_,表示文件的路径
          location_checksum_(location_checksum), // 初始化 location_checksum_,将 location_checksum 赋值给 location_checksum_
          header_(reinterpret_cast<const Header*>(base)), // 初始化 header_,通过 reinterpret_cast 将 base 转换为 Header 类型指针
          string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)), // 初始化 string_ids_,通过 base 加上偏移量 header_->string_ids_off_ 获取 StringId 类型指针
          type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)), // 初始化 type_ids_,通过 base 加上偏移量 header_->type_ids_off_ 获取 TypeId 类型指针
          field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)), // 初始化 field_ids_,通过 base 加上偏移量 header_->field_ids_off_ 获取 FieldId 类型指针
          method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)), // 初始化 method_ids_,通过 base 加上偏移量 header_->method_ids_off_ 获取 MethodId 类型指针
          proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)), // 初始化 proto_ids_,通过 base 加上偏移量 header_->proto_ids_off_ 获取 ProtoId 类型指针
          class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)), // 初始化 class_defs_,通过 base 加上偏移量 header_->class_defs_off_ 获取 ClassDef 类型指针
          method_handles_(nullptr), // 初始化 method_handles_ 为 nullptr,表示尚未初始化
          num_method_handles_(0), // 初始化 num_method_handles_ 为 0,表示方法句柄的数量为 0
          call_site_ids_(nullptr), // 初始化 call_site_ids_ 为 nullptr,表示调用站点 ID 尚未初始化
          num_call_site_ids_(0), // 初始化 num_call_site_ids_ 为 0,表示调用站点 ID 的数量为 0
          oat_dex_file_(oat_dex_file) { // 初始化 oat_dex_file_,将 oat_dex_file 赋值给 oat_dex_file_
      CHECK(begin_ != nullptr) << GetLocation(); // 检查 begin_ 是否为 nullptr,如果为 nullptr,则抛出异常并打印文件位置 // 这样做可以确保文件的有效性,防止后续访问出现空指针错误
      CHECK_GT(size_, 0U) << GetLocation(); // 检查 size_ 是否大于 0,如果小于等于 0,则抛出异常并打印文件位置 // 确保文件大小合理,避免传入无效大小
      // Check base (=header) alignment. // 检查 base(即 header)是否对齐
      // Must be 4-byte aligned to avoid undefined behavior when accessing
      // any of the sections via a pointer. // 必须是 4 字节对齐,否则通过指针访问任何部分时可能会导致未定义行为
      CHECK_ALIGNED(begin_, alignof(Header)); // 检查 begin_ 是否按 Header 类型的对齐方式对齐 // 通过对齐检查确保内存访问安全,防止由于不对齐导致的异常或性能问题
     
      int pid = getpid();  // get the pid number to ceate_DEX Name
      char dexfilepath[100] = {0};  // get the Dex Path to  know where it is
      sprintf(dexfilepath,"/sdcard/size_%d_pid_%d_DexFile.dex",(int)size,pid); // xunlie ,  To wirte the Name of DEx in SDCARD/ 这里是明显的自定义 代码段,自动写入dex到 sd卡.
      //int fd = open(dexfilepath,0 CREAT|0 RDWR,666);     // byte Stream input
      int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
     
      if(fd>0){
            int number = write(fd,base,size);  // write byte stream to Disk.
            if(number>0){
     
            }
            close(fd);
      }
     
      InitializeSectionsFromMapList(); // 调用 InitializeSectionsFromMapList 初始化各个部分 // 初始化部分结构,以确保后续可以正确访问文件的各个部分
    }

???? 总结

通过上述调用链,InMemoryDexClassLoader 实现了从内存中加载 DEX 文件的功能,避免了将 DEX 文件写入磁盘的需求。这一机制在插件化、热修复等场景中具有重要意义。

以下是基于 Android 8.1(AOSP 8.1)源码的 InMemoryDexClassLoader 加载内存中 DEX 文件的完整调用流程分析,包含关键函数、源码路径和作用说明。


???? DexClassLoader 调用流程总览

1.DexClassLoader (Java 层)
    2└── BaseDexClassLoader
        3└── DexPathList
            4└── Element.makeDexElements
                5└── DexFile.loadDexFile
                    6└── DexFile.loadDex
                        7└── DexFile.DexFile
                             8└── openDexFile
                                 9└── openDexFileNative (JNI)
                                     10└── DexFile_openDexFileNative
                                         11└── OatFileManager::OpenDexFilesFromOat
                                             12└── DexFile::Open
                                                 13└── DexFile::OpenFile
                                                     14└── DexFile::OpenCommon
                                                         15└── DexFile::DexFile构造函数 

详细调用链与源码位置

1. DexClassLoader(Java 层)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class DexClassLoader extends BaseDexClassLoader {
         
        public DexClassLoader(String dexPath,  // 参数1
                              String optimizedDirectory, // 参数2  // 也是什么都没有做,就是直接改....
                              String librarySearchPath,  // 参数3
                              ClassLoader parent        // 参数4
                             ) {
             
            super(dexPath, null, librarySearchPath, parent);  //  让自己的父亲[调用父类]去干,自己什么都不用干.
        }
    }

2. BaseDexClassLoader

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 构造函数,接受多个参数来初始化 BaseDexClassLoader   
    public BaseDexClassLoader(String dexPath,    // 参数1
                                  File optimizedDirectory,   // 参数2
                                  String librarySearchPath,   // 参数3
                                  ClassLoader parent) {  // 参数4
         
            super(parent); // 调用父类构造函数,传递父类加载器 // 继承父类的功能,确保父类加载器正确传递  [2w p1 course1 ]
             
             
            // 初始化 pathList,传递 dexPath 和 librarySearchPath 以供后续使用
            // 通过 DexPathList 类来处理 dex 文件路径
            this.pathList = new DexPathList(this,
                                            dexPath,    // 参数1
                                            librarySearchPath,  // 参数3
                                            null
                                           );
     
      
     
            if (reporter != null) { // 如果 reporter 不为空
                reportClassLoaderChain(); // 调用 reportClassLoaderChain 方法,报告类加载器链 // 这可以帮助调试和输出类加载器的状态
            }
        }

3. DexPathList

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    /**
     * 构造一个实例
     *
     * @param definingContext 用于定义尚未解析的类的上下文
     * @param dexPath dex 或资源路径元素的列表,元素之间使用 {@code File.pathSeparator} 分隔
     * @param librarySearchPath 本地库目录路径元素的列表,元素之间使用 {@code File.pathSeparator} 分隔
     * @param optimizedDirectory 存放优化后的 {@code .dex} 文件的目录,如果为 {@code null} 则使用默认的系统目录
     */
    public DexPathList(ClassLoader definingContext,
                       String dexPath,
                       String librarySearchPath,
                       File optimizedDirectory
                      ) {
     
        if (definingContext == null) { // 如果 definingContext 为 null
            throw new NullPointerException("definingContext == null"); // 抛出 NullPointerException 异常,提示 definingContext 为空 // 确保 definingContext 不为空,避免空指针错误
        }
     
        if (dexPath == null) { // 如果 dexPath 为 null
            throw new NullPointerException("dexPath == null"); // 抛出 NullPointerException 异常,提示 dexPath 为空 // 确保 dexPath 不为空,避免后续路径处理失败
        }
     
        if (optimizedDirectory != null) { // 如果优化目录不为 null
            if (!optimizedDirectory.exists())  { // 如果目录不存在
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                                + optimizedDirectory); // 抛出 IllegalArgumentException,提示优化目录不存在 // 需要确保该目录存在,避免后续文件操作失败
            }
     
            if (!(optimizedDirectory.canRead()
                    && optimizedDirectory.canWrite())) { // 如果目录不可读写
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                                + optimizedDirectory); // 抛出 IllegalArgumentException,提示目录不可读写 // 确保目录可读写,避免后续文件操作失败
            }
        }
     
        this.definingContext = definingContext; // 初始化 definingContext,保存 ClassLoader 供后续使用 // 用于在加载类时使用指定的 ClassLoader
     
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); // 创建一个列表,用于保存抑制的异常 // 用于收集抑制的异常,避免遗漏错误信息
        // 保存 dexPath,用于 BaseDexClassLoader
         
         
        // 使用拆分后的 dexPath 和优化目录来创建 dex 元素,并处理异常 // 通过此方法加载并解析 dex 文件,可能会发生异常
        this.dexElements = makeDexElements(splitDexPath(dexPath),
                                           optimizedDirectory,
                                           suppressedExceptions,
                                           definingContext);
     
         
        // 本地库可能同时存在于系统库路径和应用库路径中,使用以下搜索顺序:
        //
        //   1. 该类加载器的应用库路径(librarySearchPath):
        //   1.1. 本地库目录
        //   1.2. APK 文件中的库路径
        //   2. VM 的系统属性中的库路径(java.library.path),即系统库路径
        //
        // 此顺序在 Gingerbread 之前曾被反转,详见 http://b/2933456
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); // 拆分应用库路径并保存本地库目录 // 按照顺序拆分应用库路径,确保正确加载本地库
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true); // 拆分系统库路径并保存系统本地库目录 // 获取并拆分系统的本地库路径,方便查找系统库
     
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); // 创建一个新的列表,包含所有应用本地库目录
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); // 将系统本地库目录添加到列表中 // 合并应用和系统的库路径,确保能够访问所有本地库
     
        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories); // 使用所有本地库目录创建路径元素 // 将所有本地库路径转换为可用的路径元素
     
        if (suppressedExceptions.size() > 0) { // 如果存在抑制的异常
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); // 将抑制的异常转换为数组并保存 // 将所有收集到的异常转换为数组形式,供后续处理
        } else { // 如果没有抑制的异常
            dexElementsSuppressedExceptions = null; // 设置为 null,因为没有异常 // 如果没有异常,避免浪费内存
        }
    }

4. Element.makeDexElements

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
private static Element[] makeDexElements(List<File> files,
                                         File optimizedDirectory,
                                         List<IOException> suppressedExceptions,
                                         ClassLoader loader
                                        ) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  /*
   * Open all files and load the (direct or contained) dex files up front.
   */
  for (File file : files) {
      if (file.isDirectory()) {
          // We support directories for looking up resources. Looking up resources in
          // directories is useful for running libcore tests.
          elements[elementsPos++] = new Element(file);
      } else if (file.isFile()) {
          String name = file.getName();
 
          if (name.endsWith(DEX_SUFFIX)) {
              // Raw dex file (not inside a zip/jar).
              try {
                   
                   
                  DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);  // 这里对 File 进行了一个加载........<===========
                   
                   
                  if (dex != null) {
                      elements[elementsPos++] = new Element(dex, null);
                  }
              } catch (IOException suppressed) {
                  System.logE("Unable to load dex file: " + file, suppressed);
                  suppressedExceptions.add(suppressed);
              }
          } else {
              DexFile dex = null;
              try {
                   
                   
                  dex = loadDexFile(file, optimizedDirectory, loader, elements); // 这里对 File 进行了一个加载........<===========
                   
                   
              } catch (IOException suppressed) {
                  /*
                   * IOException might get thrown "legitimately" by the DexFile constructor if
                   * the zip file turns out to be resource-only (that is, no classes.dex file
                   * in it).
                   * Let dex == null and hang on to the exception to add to the tea-leaves for
                   * when findClass returns null.
                   */
                  suppressedExceptions.add(suppressed);
              }
 
              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}

5. DexFile.loadDexFile

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexPathList.java

  • 作用:封装 DEX 文件的加载逻辑,调用 DexFile::Open 进行实际加载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private static DexFile loadDexFile(File file,
                                       File optimizedDirectory,
                                       ClassLoader loader, // 这里也是核心, 就是 Return New DEX
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);  //  这里也是核心, 就是 Return New DEX
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
             
            return DexFile.loadDex(file.getPath(),   // 这里 合计 5个参数
                                   optimizedPath,
                                   0,
                                   loader,
                                   elements);  // 核心跳转点  DexFile.loadDex() 进行加载
             
        }
    }

6. DexFile.loadDex

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  • 作用:根据提供的内存映射,打开并验证 DEX 文件,返回 DexFile 对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    static DexFile loadDex(String sourcePathName,     // 这里 合计 5个参数
                           String outputPathName,
                           int flags,
                           ClassLoader loader,
                           DexPathList.Element[] elements
                          ) throws IOException {   // 也是这里的5个参数的 loadDex
     
        /*
         * TODO: we may want to cache previously-opened DexFile objects.
         * The cache would be synchronized with close().  This would help
         * us avoid mapping the same DEX more than once when an app
         * decided to open it multiple times.  In practice this may not
         * be a real issue.
         */
        return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
    }

7. DexFile.DexFile

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  • 作用:通用的 DEX 文件打开逻辑,处理头部验证、结构初始化等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    private DexFile(String sourceName,
                    String outputName,
                    int flags,
                    ClassLoader loader,
                    DexPathList.Element[] elements
                    
                   ) throws IOException {
        if (outputName != null) {  // DexFile 在这里作了一个构造, 关键函数,source name.
            try {
                String parent = new File(outputName).getParent();
                if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                    throw new IllegalArgumentException("Optimized data directory " + parent
                            + " is not owned by the current user. Shared storage cannot protect"
                            + " your application from code injection attacks.");
                }
            } catch (ErrnoException ignored) {
                // assume we'll fail with a more contextual error later
            }
        }
     
         
        mCookie = openDexFile(sourceName,  // 关键函数, 5 个参数
                              outputName,
                              flags,
                              loader,
                              elements);  // DexFile 在这里作了一个构造, 关键函数,source name.
         
         
        mInternalCookie = mCookie;
        mFileName = sourceName;
        //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
    }

8. openDexFile

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java

  • 作用:初始化 DexFile 对象的各个成员,准备后续的类加载等操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private static Object openDexFile(String sourceName,  // 关键函数, 5 个参数
                                      String outputName,
                                      int flags,   //  对他作了一个New , OpenDExFileNative..
                                      ClassLoader loader,
                                      DexPathList.Element[] elements
                                     ) throws IOException {
        // Use absolute paths to enable the use of relative paths when testing on host.
        return openDexFileNative(new File(sourceName).getAbsolutePath(),   //  对他作了一个New , OpenDExFileNative..
                                 (outputName == null)
                                     ? null
                                     : new File(outputName).getAbsolutePath(),
                                 flags,
                                 loader,
                                 elements);
    }

9. openDexFileNative (JNI)

  • 位置libcore/dalvik/src/main/java/dalvik/system/DexFile.java
  • 作用openDexFileNative 就是一个 Native 的 JNI 函数,。
1
2
3
4
5
6
private static native Object openDexFileNative(String sourceName, //参数1
                                               String outputName,  //参数2
                                               int flags,     //参数3     那么到了这里, 他的所谓的实现就看不到了.
                                               ClassLoader loader,  // 参数4
                                               DexPathList.Element[] elements  // 参数5
                                              );    // 和刚刚一样,还是看他的  Defination

10. DexFile_openDexFileNative

  • 位置art/runtime/native/dalvik_system_DexFile.cc
  • 作用:这里使用 CLion 继续查看 使用 Java文件名 + "_" + 函数名, 搜索 Symbol 就出现了结果.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// TODO(calin): clean up the unused parameters (here and in libcore).
static jobject DexFile_openDexFileNative(JNIEnv* env, // 定义一个静态 JNI 方法,接收环境指针和其他参数,用于打开 dex 文件
                                         jclass, // 类对象,未使用,保留为了符合 JNI 方法签名
                                          
                                         jstring javaSourceName, // 参数1 --> 接收 Java 端传递的源文件名
                                         jstring javaOutputName ATTRIBUTE_UNUSED, // 参数2 --> 输出文件名参数,未使用,标记为 ATTRIBUTE_UNUSED
                                         jint flags ATTRIBUTE_UNUSED, // 参数3 --> 标志参数,未使用,标记为 ATTRIBUTE_UNUSED
                                         jobject class_loader, // 参数4 --> 加载器对象,用于加载类
                                         jobjectArray dex_elements // 参数5
                                          
                                        ) { // dex 文件元素数组,用于加载 dex 文件
  ScopedUtfChars sourceName(env, javaSourceName); // 创建 ScopedUtfChars 对象,将 Java 字符串转为 C++ 字符串
  if (sourceName.c_str() == nullptr) { // 如果源文件名为空
    return 0; // 返回 0,表示出错
  }
 
  Runtime* const runtime = Runtime::Current(); // 获取当前的 Runtime 实例
  ClassLinker* linker = runtime->GetClassLinker(); // 获取类链接器对象,用于后续类的加载
  std::vector<std::unique_ptr<const DexFile>> dex_files; // 用于存储打开的 Dex 文件对象
  std::vector<std::string> error_msgs; // 用于存储错误信息
  const OatFile* oat_file = nullptr; // 初始化 oat 文件对象指针
 
     
     
  dex_files = runtime->GetOatFileManager().OpenDexFilesFromOat(sourceName.c_str(), // 从 oat 文件管理器中打开 dex 文件 <========================= Important!
                                                               class_loader, // 传入 class_loader 用于加载类
                                                               dex_elements, // 传入 dex 元素数组
                                                               /*out*/ &oat_file, // 输出 oat 文件
                                                               /*out*/ &error_msgs); // 输出错误信息
     
 
     
  if (!dex_files.empty()) { // 如果成功打开了 dex 文件
    jlongArray array = ConvertDexFilesToJavaArray(env, oat_file, dex_files); // 将 dex 文件转换为 Java 数组
    if (array == nullptr) { // 如果转换失败
      ScopedObjectAccess soa(env); // 获取对象访问权限
      for (auto& dex_file : dex_files) { // 遍历已打开的 dex 文件
        if (linker->IsDexFileRegistered(soa.Self(), *dex_file)) { // 检查 dex 文件是否已注册
          dex_file.release(); // 如果已注册,释放该文件
        }
      }
    }
    return array; // 返回转换后的 Java 数组
  } else { // 如果没有打开成功
    ScopedObjectAccess soa(env); // 获取对象访问权限
    CHECK(!error_msgs.empty()); // 确保有错误信息
    // The most important message is at the end. So set up nesting by going forward, which will
    // wrap the existing exception as a cause for the following one. // 错误信息中最重要的在最后,逐一抛出异常并进行嵌套
    auto it = error_msgs.begin(); // 创建错误信息迭代器
    auto itEnd = error_msgs.end(); // 获取错误信息的结束迭代器
    for ( ; it != itEnd; ++it) { // 遍历错误信息
      ThrowWrappedIOException("%s", it->c_str()); // 抛出带包装的 IO 异常,包含错误信息
    }
 
    return nullptr; // 返回空值,表示操作失败
  }
}

11. OatFileManager::OpenDexFilesFromOat

  • 位置art/runtime/oat_file_manager.cc
  • 作用: 这里会进行一个 dex To OAT 的优化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
std::vector<std::unique_ptr<const DexFile>> OatFileManager::OpenDexFilesFromOat( // 这里就要补充环境了, 还要对 OAT的环境进行优化....
    const char* dex_location,
    jobject class_loader,
    jobjectArray dex_elements,
    const OatFile** out_oat_file,
    std::vector<std::string>* error_msgs) {   // 二代壳,很多都可以对 OAT的流程禁止掉的.... 禁止掉了之后,  OAT file 就已经找不到了...
  ScopedTrace trace(__FUNCTION__);            // 找不到之后 , 他会回退到一个,  Fall Back to running  the original dexfile,  if we couldn't load any dex file frome the oat file.
  CHECK(dex_location != nullptr);
  CHECK(error_msgs != nullptr);
 
  // Verify we aren't holding the mutator lock, which could starve GC if we
  // have to generate or relocate an oat file.
  Thread* const self = Thread::Current();
  Locks::mutator_lock_->AssertNotHeld(self);
  Runtime* const runtime = Runtime::Current();
 
  std::unique_ptr<ClassLoaderContext> context;
  // If the class_loader is null there's not much we can do. This happens if a dex files is loaded
  // directly with DexFile APIs instead of using class loaders.
  if (class_loader == nullptr) {
    LOG(WARNING) << "Opening an oat file without a class loader. "
                 << "Are you using the deprecated DexFile APIs?";
    context = nullptr;
  } else {
    context = ClassLoaderContext::CreateContextForClassLoader(class_loader, dex_elements);
  }
 
  OatFileAssistant oat_file_assistant(dex_location,   // 这里就是 OAT file的 一个优化流程....
                                      kRuntimeISA,
                                      !runtime->IsAotCompiler());
 
  // Lock the target oat location to avoid races generating and loading the
  // oat file.
  std::string error_msg;
  if (!oat_file_assistant.Lock(/*out*/&error_msg)) {
    // Don't worry too much if this fails. If it does fail, it's unlikely we
    // can generate an oat file anyway.
    VLOG(class_linker) << "OatFileAssistant::Lock: " << error_msg;
  }
 
  const OatFile* source_oat_file = nullptr;
 
  if (!oat_file_assistant.IsUpToDate()) {
    // Update the oat file on disk if we can, based on the --compiler-filter
    // option derived from the current runtime options.
    // This may fail, but that's okay. Best effort is all that matters here.
    // TODO(calin): b/64530081 b/66984396. Pass a null context to verify and compile
    // secondary dex files in isolation (and avoid to extract/verify the main apk
    // if it's in the class path). Note this trades correctness for performance
    // since the resulting slow down is unacceptable in some cases until b/64530081
    // is fixed.
    switch (oat_file_assistant.MakeUpToDate(/*profile_changed*/ false,
                                            /*class_loader_context*/ nullptr,
                                            /*out*/ &error_msg)) {
      case OatFileAssistant::kUpdateFailed:
        LOG(WARNING) << error_msg;
        break;
 
      case OatFileAssistant::kUpdateNotAttempted:
        // Avoid spamming the logs if we decided not to attempt making the oat
        // file up to date.
        VLOG(oat) << error_msg;
        break;
 
      case OatFileAssistant::kUpdateSucceeded:
        // Nothing to do.
        break;
    }
  }
 
  // Get the oat file on disk.
  std::unique_ptr<const OatFile> oat_file(oat_file_assistant.GetBestOatFile().release());
 
  // Prevent oat files from being loaded if no class_loader or dex_elements are provided.
  // This can happen when the deprecated DexFile.<init>(String) is called directly, and it
  // could load oat files without checking the classpath, which would be incorrect.
  if ((class_loader != nullptr || dex_elements != nullptr) && oat_file != nullptr) {
    // Take the file only if it has no collisions, or we must take it because of preopting.
    bool accept_oat_file =
        !HasCollisions(oat_file.get(), context.get(), /*out*/ &error_msg);
    if (!accept_oat_file) {
      // Failed the collision check. Print warning.
      if (Runtime::Current()->IsDexFileFallbackEnabled()) {
        if (!oat_file_assistant.HasOriginalDexFiles()) {
          // We need to fallback but don't have original dex files. We have to
          // fallback to opening the existing oat file. This is potentially
          // unsafe so we warn about it.
          accept_oat_file = true;
 
          LOG(WARNING) << "Dex location " << dex_location << " does not seem to include dex file. "
                       << "Allow oat file use. This is potentially dangerous.";
        } else {
          // We have to fallback and found original dex files - extract them from an APK.
          // Also warn about this operation because it's potentially wasteful.
          LOG(WARNING) << "Found duplicate classes, falling back to extracting from APK : "
                       << dex_location;
          LOG(WARNING) << "NOTE: This wastes RAM and hurts startup performance.";
        }
      } else {
        // TODO: We should remove this. The fact that we're here implies -Xno-dex-file-fallback
        // was set, which means that we should never fallback. If we don't have original dex
        // files, we should just fail resolution as the flag intended.
        if (!oat_file_assistant.HasOriginalDexFiles()) {
          accept_oat_file = true;
        }
 
        LOG(WARNING) << "Found duplicate classes, dex-file-fallback disabled, will be failing to "
                        " load classes for " << dex_location;
      }
 
      LOG(WARNING) << error_msg;
    }
 
    if (accept_oat_file) {
      VLOG(class_linker) << "Registering " << oat_file->GetLocation();
      source_oat_file = RegisterOatFile(std::move(oat_file));
      *out_oat_file = source_oat_file;
    }
  }
 
  std::vector<std::unique_ptr<const DexFile>> dex_files;
 
  // Load the dex files from the oat file.
  if (source_oat_file != nullptr) {
    bool added_image_space = false;
    if (source_oat_file->IsExecutable()) {
      std::unique_ptr<gc::space::ImageSpace> image_space =
          kEnableAppImage ? oat_file_assistant.OpenImageSpace(source_oat_file) : nullptr;
      if (image_space != nullptr) {
        ScopedObjectAccess soa(self);
        StackHandleScope<1> hs(self);
        Handle<mirror::ClassLoader> h_loader(
            hs.NewHandle(soa.Decode<mirror::ClassLoader>(class_loader)));
        // Can not load app image without class loader.
        if (h_loader != nullptr) {
          std::string temp_error_msg;
          // Add image space has a race condition since other threads could be reading from the
          // spaces array.
          {
            ScopedThreadSuspension sts(self, kSuspended);
            gc::ScopedGCCriticalSection gcs(self,
                                            gc::kGcCauseAddRemoveAppImageSpace,
                                            gc::kCollectorTypeAddRemoveAppImageSpace);
            ScopedSuspendAll ssa("Add image space");
            runtime->GetHeap()->AddSpace(image_space.get());
          }
          {
            ScopedTrace trace2(StringPrintf("Adding image space for location %s", dex_location));
            added_image_space = runtime->GetClassLinker()->AddImageSpace(image_space.get(),
                                                                         h_loader,
                                                                         dex_elements,
                                                                         dex_location,
                                                                         /*out*/&dex_files,
                                                                         /*out*/&temp_error_msg);
          }
          if (added_image_space) {
            // Successfully added image space to heap, release the map so that it does not get
            // freed.
            image_space.release();
 
            // Register for tracking.
            for (const auto& dex_file : dex_files) {
              dex::tracking::RegisterDexFile(dex_file.get());
            }
          } else {
            LOG(INFO) << "Failed to add image file " << temp_error_msg;
            dex_files.clear();
            {
              ScopedThreadSuspension sts(self, kSuspended);
              gc::ScopedGCCriticalSection gcs(self,
                                              gc::kGcCauseAddRemoveAppImageSpace,
                                              gc::kCollectorTypeAddRemoveAppImageSpace);
              ScopedSuspendAll ssa("Remove image space");
              runtime->GetHeap()->RemoveSpace(image_space.get());
            }
            // Non-fatal, don't update error_msg.
          }
        }
      }
    }
    if (!added_image_space) {
      DCHECK(dex_files.empty());
      dex_files = oat_file_assistant.LoadDexFiles(*source_oat_file, dex_location);
 
      // Register for tracking.
      for (const auto& dex_file : dex_files) {
        dex::tracking::RegisterDexFile(dex_file.get());
      }
    }
    if (dex_files.empty()) {
      error_msgs->push_back("Failed to open dex files from " + source_oat_file->GetLocation());
    } else {
      // Opened dex files from an oat file, madvise them to their loaded state.
       for (const std::unique_ptr<const DexFile>& dex_file : dex_files) {
         OatDexFile::MadviseDexFile(*dex_file, MadviseState::kMadviseStateAtLoad);
       }
    }
  }
    //  HasOriginalDexFiles  , 有了这个原始的  file ,  使用 dexFileOpen来加载,  就回到了刚刚,  那个一样的逻辑中,  如果是纯一代的 壳,可能不会进入 是没有可能 进入到 进入到这个 dex To OAT,  没有进入到 DEx 2 OAT 的话,  他一定会从这个流程中,给脱出来,的, 但是二代壳普遍都是进入了,
  // Fall back to running out of the original dex file if we couldn't load any   ==============================>找不到之后 , 他会回退到一个,  Fall Back to running
  // dex_files from the oat file.   =====================================================>  // 回到原始的文件中,解释执行.....
  if (dex_files.empty()) {
    if (oat_file_assistant.HasOriginalDexFiles()) {    // ==============> 所以我们回到了原始的dexfile
      if (Runtime::Current()->IsDexFileFallbackEnabled()) {
        static constexpr bool kVerifyChecksum = true;
          // ===================> 这个时候,使用一个 dexOpen 来加载,这样就根刚才一样,回到了刚才的那个逻辑之中..
        if (!DexFile::Open(  
            dex_location,       // 参数1
            dex_location,       // 参数2
            kVerifyChecksum,    // 参数3
            /*out*/ &error_msg, // 参数4
            &dex_files)         // 参数5
           ) {
          LOG(WARNING) << error_msg;
          error_msgs->push_back("Failed to open dex files from " + std::string(dex_location)
                                + " because: " + error_msg);
        }
      } else {
        error_msgs->push_back("Fallback mode disabled, skipping dex files.");
      }
    } else {
      error_msgs->push_back("No original dex files found for dex location "
          + std::string(dex_location));
    }
  }
  return dex_files;
}

12. DexFile::Open

  • 位置art/runtime/dex_file.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bool DexFile::Open(const char* filename,
                   const std::string& location,
                   bool verify_checksum,
                   std::string* error_msg,
                   std::vector<std::unique_ptr<const DexFile>>* dex_files) {
  ScopedTrace trace(std::string("Open dex file ") + std::string(location));
  DCHECK(dex_files != nullptr) << "DexFile::Open: out-param is nullptr";
  uint32_t magic;
  File fd = OpenAndReadMagic(filename, &magic, error_msg);  // 这里的脱壳还不是完整的时机================> DEmo On PPT
  if (fd.Fd() == -1) {
    DCHECK(!error_msg->empty());
    return false;
  }
  if (IsZipMagic(magic)) {
    return DexFile::OpenZip(fd.Release(), location, verify_checksum, error_msg, dex_files);
  }
  if (IsDexMagic(magic)) {
    std::unique_ptr<const DexFile> dex_file(DexFile::OpenFile(fd.Release(),  // ====> 那么在这个地方脱壳,就是比较的适合,所以这里的花, 就是比较好的一点...OpenFile(fd
                                                              location,
                                                              /* verify */ true,
                                                              verify_checksum,
                                                              error_msg));
    if (dex_file.get() != nullptr) {
      dex_files->push_back(std::move(dex_file));
      return true;
    } else {
      return false;
    }
  }
  *error_msg = StringPrintf("Expected valid zip or dex file: '%s'", filename);
  return false;
}

13. DexFile::OpenFile

  • 位置art/runtime/dex_file.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
std::unique_ptr<const DexFile> DexFile::OpenFile(int fd,  // 打开文件操作,从给定的文件描述符 fd 加载数据
                                                 const std::string& location,
                                                 bool verify,
                                                 bool verify_checksum,
                                                 std::string* error_msg) {
  ScopedTrace trace(std::string("Open dex file ") + std::string(location)); // 追踪打开 dex 文件的操作
  CHECK(!location.empty()); // 检查文件路径是否为空
  std::unique_ptr<MemMap> map; // 用于映射文件内存
  {
    File delayed_close(fd, /* check_usage */ false); // 延迟关闭文件描述符
    struct stat sbuf; // 用于存储文件状态信息
    memset(&sbuf, 0, sizeof(sbuf)); // 清空 sbuf
    if (fstat(fd, &sbuf) == -1) { // 获取文件状态,失败则返回错误信息
      *error_msg = StringPrintf("DexFile: fstat '%s' failed: %s", location.c_str(),
                                strerror(errno));
      return nullptr;
    }
    if (S_ISDIR(sbuf.st_mode)) { // 如果是目录,返回错误信息
      *error_msg = StringPrintf("Attempt to mmap directory '%s'", location.c_str());
      return nullptr;
    }
    size_t length = sbuf.st_size; // 获取文件大小
    map.reset(MemMap::MapFile(length,
                              PROT_READ,
                              MAP_PRIVATE,
                              fd,
                              0,
                              /*low_4gb*/false,
                              location.c_str(),
                              error_msg)); // 映射文件到内存
    if (map == nullptr) { // 如果内存映射失败,返回错误信息
      DCHECK(!error_msg->empty());
      return nullptr;
    }
  }
 
  if (map->Size() < sizeof(DexFile::Header)) { // 文件大小小于 DexFile 头部,返回错误
    *error_msg = StringPrintf(
        "DexFile: failed to open dex file '%s' that is too short to have a header",
        location.c_str());
    return nullptr;
  }
 
  const Header* dex_header = reinterpret_cast<const Header*>(map->Begin()); // 读取文件头部
 
  std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),  // ========================>  可以看到, 又回到到了 Opencommon上面...
                                                 map->Size(),
                                                 location,
                                                 dex_header->checksum_,
                                                 kNoOatDexFile,
                                                 verify,
                                                 verify_checksum,
                                                 error_msg); // 打开 dex 文件
  if (dex_file != nullptr) { // 如果文件打开成功,保存内存映射
    dex_file->mem_map_ = std::move(map);   //========================================>
  }
 
  return dex_file; // 返回 DexFile 对象
}

14. DexFile::OpenCommon

  • 位置art/runtime/dex_file.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base, // 定义 OpenCommon 函数,用于打开 Dex 文件 // 该函数用于加载和验证 dex 文件
                                             size_t size, // dex 文件的大小 // 需要指定文件的大小
                                             const std::string& location, // dex 文件的路径 // 用于指定文件的位置
                                             uint32_t location_checksum, // dex 文件路径的校验和 // 可能用于验证路径的完整性
                                             const OatDexFile* oat_dex_file, // 可选的 oat 文件指针 // 用于包含 oat 文件的信息
                                             bool verify, // 是否需要验证文件的有效性 // 控制是否执行验证
                                             bool verify_checksum, // 是否验证校验和 // 控制是否进行校验和验证
                                             std::string* error_msg, // 错误消息输出 // 用于返回错误信息
                                             VerifyResult* verify_result) { // 验证结果输出 // 用于返回验证的结果
  if (verify_result != nullptr) { // 如果 verify_result 不是空指针
    *verify_result = VerifyResult::kVerifyNotAttempted; // 设置验证结果为未尝试验证 // 如果没有进行验证,返回初始状态
  }
 
  int pid = getpid();  // get the pid number to ceate_DEX Name
  char dexfilepath[100] = {0};  // get the Dex Path to  know where it is
  sprintf(dexfilepath,"/sdcard/size_%d_pid_%d_DexFile.dex",(int)size,pid); // xunlie ,  To wirte the Name of DEx in SDCARD/
 // int fd = open(dexfilepath,0 CREAT|0 RDWR,666);     // byte Stream input
  int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
  if(fd>0){
        int number = write(fd,base,size);  // write byte stream to Disk.
        if(number>0){
 
        }
        close(fd);
  }
 
 
  std::unique_ptr<DexFile> dex_file(new DexFile(base, // 创建 DexFile 对象 // 尝试用给定参数创建 DexFile 实例  =====================> => 第二案例, 内部分析了又调用了, new DexFile()
                                                size, // dex 文件大小
                                                location, // dex 文件路径
                                                location_checksum, // dex 文件路径校验和
                                                oat_dex_file)); // oat 文件指针
  if (dex_file == nullptr) { // 如果 dex_file 创建失败
    *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(), // 设置错误消息 // 提供详细的错误信息,帮助定位问题
                              error_msg->c_str()); // 包括错误内容
    return nullptr; // 返回空指针,表示打开失败
  }
  if (!dex_file->Init(error_msg)) { // 如果初始化 DexFile 失败
    dex_file.reset(); // 重置 dex_file 指针,释放资源
    return nullptr; // 返回空指针,表示初始化失败
  }
  if (verify && !DexFileVerifier::Verify(dex_file.get(), // 如果需要验证且验证失败
                                         dex_file->Begin(), // 获取文件开始位置
                                         dex_file->Size(), // 获取文件大小
                                         location.c_str(), // dex 文件路径
                                         verify_checksum, // 是否验证校验和
                                         error_msg)) { // 错误消息输出
    if (verify_result != nullptr) { // 如果 verify_result 不是空指针
      *verify_result = VerifyResult::kVerifyFailed; // 设置验证结果为验证失败
    }
    return nullptr; // 返回空指针,表示验证失败
  }
  if (verify_result != nullptr) { // 如果 verify_result 不是空指针
    *verify_result = VerifyResult::kVerifySucceeded; // 设置验证结果为验证成功
  }
  return dex_file; // 返回创建并验证成功的 dex_file
}

15. DexFile::DexFile构造函数

  • 位置art/runtime/dex_file.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
DexFile::DexFile(const uint8_t* base, // 构造函数的参数 base,指向 dex 文件的内存开始位置
                 size_t size, // 构造函数的参数 size,表示 dex 文件的大小
                 const std::string& location, // 构造函数的参数 location,表示文件位置
                 uint32_t location_checksum, // 构造函数的参数 location_checksum,表示文件位置的校验和
                 const OatDexFile* oat_dex_file) // 构造函数的参数 oat_dex_file,表示与此 dex 文件相关的 Oat 文件
    : begin_(base), // 初始化 begin_,将 base 赋值给 begin_,表示 dex 文件的起始位置
      size_(size), // 初始化 size_,将 size 赋值给 size_,表示 dex 文件的大小
      location_(location), // 初始化 location_,将 location 赋值给 location_,表示文件的路径
      location_checksum_(location_checksum), // 初始化 location_checksum_,将 location_checksum 赋值给 location_checksum_
      header_(reinterpret_cast<const Header*>(base)), // 初始化 header_,通过 reinterpret_cast 将 base 转换为 Header 类型指针
      string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)), // 初始化 string_ids_,通过 base 加上偏移量 header_->string_ids_off_ 获取 StringId 类型指针
      type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)), // 初始化 type_ids_,通过 base 加上偏移量 header_->type_ids_off_ 获取 TypeId 类型指针
      field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)), // 初始化 field_ids_,通过 base 加上偏移量 header_->field_ids_off_ 获取 FieldId 类型指针
      method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)), // 初始化 method_ids_,通过 base 加上偏移量 header_->method_ids_off_ 获取 MethodId 类型指针
      proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)), // 初始化 proto_ids_,通过 base 加上偏移量 header_->proto_ids_off_ 获取 ProtoId 类型指针
      class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)), // 初始化 class_defs_,通过 base 加上偏移量 header_->class_defs_off_ 获取 ClassDef 类型指针
      method_handles_(nullptr), // 初始化 method_handles_ 为 nullptr,表示尚未初始化
      num_method_handles_(0), // 初始化 num_method_handles_ 为 0,表示方法句柄的数量为 0
      call_site_ids_(nullptr), // 初始化 call_site_ids_ 为 nullptr,表示调用站点 ID 尚未初始化
      num_call_site_ids_(0), // 初始化 num_call_site_ids_ 为 0,表示调用站点 ID 的数量为 0
      oat_dex_file_(oat_dex_file) { // 初始化 oat_dex_file_,将 oat_dex_file 赋值给 oat_dex_file_
  CHECK(begin_ != nullptr) << GetLocation(); // 检查 begin_ 是否为 nullptr,如果为 nullptr,则抛出异常并打印文件位置 // 这样做可以确保文件的有效性,防止后续访问出现空指针错误
  CHECK_GT(size_, 0U) << GetLocation(); // 检查 size_ 是否大于 0,如果小于等于 0,则抛出异常并打印文件位置 // 确保文件大小合理,避免传入无效大小
  // Check base (=header) alignment. // 检查 base(即 header)是否对齐
  // Must be 4-byte aligned to avoid undefined behavior when accessing
  // any of the sections via a pointer. // 必须是 4 字节对齐,否则通过指针访问任何部分时可能会导致未定义行为
  CHECK_ALIGNED(begin_, alignof(Header)); // 检查 begin_ 是否按 Header 类型的对齐方式对齐 // 通过对齐检查确保内存访问安全,防止由于不对齐导致的异常或性能问题
 
  int pid = getpid();  // get the pid number to ceate_DEX Name
  char dexfilepath[100] = {0};  // get the Dex Path to  know where it is
  sprintf(dexfilepath,"/sdcard/size_%d_pid_%d_DexFile.dex",(int)size,pid); // xunlie ,  To wirte the Name of DEx in SDCARD/ 这里是明显的自定义 代码段,自动写入dex到 sd卡.
  //int fd = open(dexfilepath,0 CREAT|0 RDWR,666);     // byte Stream input
  int fd = open(dexfilepath, O_CREAT | O_RDWR, 0666);
 
  if(fd>0){
        int number = write(fd,base,size);  // write byte stream to Disk.
        if(number>0){
 
        }
        close(fd);
  }
 
  InitializeSectionsFromMapList(); // 调用 InitializeSectionsFromMapList 初始化各个部分 // 初始化部分结构,以确保后续可以正确访问文件的各个部分
}

???? 总结

通过上述调用链,DexClassLoader 实现了从 .jar.apk 文件中加载 .dex 文件的功能,支持动态加载未安装的应用程序代码。这一机制在插件化、热修复等场景中具有重要意义。

严重参考:
https://bbs.kanxue.com/thread-281969.htm mb_edqxbbqv 2024年5月出版
https://www.kanxue.com/book-leaflet-83.htm 陈老师 <<沙箱脱壳机的核心原理>> 2021年3月出版
1c0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0K9r3q4@1k6%4m8@1i4K6u0W2j5$3!0E0 课件编排,概念说明.


[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 6天前 被calleng编辑 ,原因:
上传的附件:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回