Dalvik: Dex

Table of Contents

1. Dalvik: Dex

1.1. dex 文件结构

http://www.retrodev.com/android/dexformat.html http://stackoverflow.com/questions/7750448/dex-file-in-android http://source.android.com/devices/tech/dalvik/dex-format.html http://blog.csdn.net/hlchou/article/details/6303566 http://www.2cto.com/Article/201308/236054.html

dex 文件主要由一个 dex header, 多个表(ids, defs)和数据 (data) 部分组成. 构成一个复杂的索引关系, 这些表和数据的位置大约是:

  1. dex header
  2. dex string ids
  3. dex type ids
  4. dex field ids
  5. dex method ids o
  6. dex class defs
  7. data
  8. dex map list

1.1.1. dex header

dex header 中主要下面提到的各个 ids, defs 的 size 和 offset, 以便能找到这些 table, 例如:

struct header_item {
    struct dex_magic {
        /* dex.. */
    };
    uint checksum;
    char signature[20];
    uint file_size;
    uint header_size;
    uint endian_tag;
    uint link_size;
    uint link_off;
    uint map_off;               /* map list offset */
    uint string_ids_size;
    uint string_ids_off;
    uint type_ids_size;
    uint type_ids_off;
    uint proto_ids_size;
    uint proto_ids_off;
    uint field_ids_size;
    uint field_ids_off;
    uint method_ids_size;
    uint mehtod_ids_off;
    uint class_defs_size;
    uint class_defs_off;
    uint data_size;
    uint data_off;              /* data_off 可能主要用来检查 */
};

1.1.2. dex string ids

struct string_id_list {
    struct string_id_item {
        uint string_data_off;
    } [string_ids_size];
};

通过这个 string_data_off, 可以在 data 部分找到这个 string 对应的 string_item 结构.

1.1.3. dex type ids

struct type_id_list {
    struct type_id_item {
        uint descriptor_idx;    /* => string_id_item */
    } [type_ids_size];
};

1.1.4. dex proto ids

struct proto_id_list {
    struct proto_id_item {
        uint shorty_idx;            /* => string_id_item */
        uint reture_type_idx;       /* => type_id_item */
        unit parameters_off;
    } [proto_ids_size];
};
1.1.4.1. prarameters_off

parameters_off 表示该函数的参数情况, 指向 data 部分的一个 type_item_list 结构

struct type_item_list {
    uint size;                  /* 参数的个数 */
    struct type_item [size];
};
struct type_item {
    ushort type_idx;            /* 指向 string ids */
};

1.1.5. dex field ids

struct field_id_list {
    struct field_id_item {
        uint class_idx;             /* => type_id_item */
        uint type_idx;              /* => type_id_item */
        uint name_idx;              /* => string_id_item */
    } [field_ids_size];
};

1.1.6. dex method ids

struct method_id_list {
    struct method_id_item {
        ushort class_idx;           /* => type_id_item */
        ushort proto_idx;           /* => proto_id_item */
        uint name_idx;              /* => string_id_item */
    } [method_ids_size];
};

1.1.7. dex class defs

class defs 是这些表中是复杂的一个表.

struct class_def_item_list {
    struct class_def_item {
        uint class_idx;             /* => type_id_item */
        enum ACCESS_FLAGS;
        uint superclass_idx;        /* => type_id_item */
        uint interface_off;
        uint source_file_idx;       /* => string_id_item */
        uint annotation_off;
        uint class_data_off;
        uint static_value_off;
    } [class_defs_size];
};

与 string_id_item 中的 string_data_off 及 proto_id_item 中的 parameters_off 类似, class_def_item 中包含四项 off 也存在到 data 部分,

1.1.7.1. interface_off
struct type_item_list {
    uint size;
    struct type_item[size];
};

struct type_item {
    ushort type_index;          /* => type_id_item */
};
1.1.7.2. static_value_off
1.1.7.3. class_data_off

class_data_off 是 class_def_item 中最复杂的.

struct class_data_item {
    uleb128 static_fields_size;
    uleb128 instance_fields_size;
    uleb128 direct_methods_size;
    uleb128 virtual_methods_size;

    struct encoded_field_list {
        struct encoded_field {
            /* field_idx_diff 与前面提到的 xxx_idx 不同:
               actual_idx[i]= sum_{0}^{i}(field_idx_diff[x]d(x))
             ,*/
            uleb128 field_idx_diff; /* => field_id_item */
        } [static_fields_size];
    } static_fields;

    struct encoded_field_list {
        struct encoded_field {
            uleb128 field_idx_diff; /* => field_id_item */
            uleb128 access_flags;
        } [instance_fields_size];
    } instance_fields;

    struct encoded_methods_list {
        struct encoded_method {
            uleb128 method_idx_diff; /* => method_id_item */
            uleb128 access_flags;
            uleb128 code_off;
        } [direct_methods_size];
    } direct_methods;

    struct encoded_methods_list {
        struct encoded_method {
            uleb128 method_idx_diff; /* => method_id_item */
            uleb128 access_flags;
            uleb128 code_off;
        } [virtual_methods_size];
    } virtual_methods;
};
1.1.7.3.1. code off

encoded_method 中包含一个 code_off 的指针, 指向真正的 code_item

struct code_item {
    ushort register_size;       /* 使用多少个 register, 包括参数, 局域
                                 * 变量等 */
    ushort ins_size;            /* 参数用到的 register 个数 */
    ushort outs_size;           /* 调用其他函数需要用到的 register 个
                                 * 数 */
    ushort tries_size;
    uint debug_info_off;
    uint insns_size;            /* instruction size, 以 16 bits 为单位 */
    ushort insns[insns_size];   /*  真正的 byte code*/
};

如果 tries_size 不为 0, 则 insns 后还会有另两部分和 exception 相关的内容:

  1. try_item_list
  2. encoded_catch_handler_list

这两个 list 对应着 exception table.

其中 tries_item 为:

struct try_item {
    // catcher 对应的 start_addr
    uint start_addr;
    // catcher 对应的 end_addr 为 start_addr + insn_count
    ushort insn_count;
    // catcher 的位置. 以上三个值都是当前 method 中的相对地址
    ushort handler_off;
}

根据 handler_off 可以找到 encoded_catch_handler, encoded_catch_handler 结构比较啰嗦, 但基本信息为:

struct encoded_type_addr_pair {
    // catcher 要 catch 的异常的类型
    uleb128 type_idx;
    // catcher 的代码, 也是相对地址
    uleb128 addr;
};

1.1.8. dex map list

struct map_list_type {
    uint size;
    struct map_item_list {
        struct map_item {
            enum type;
            ushort unused;
            uint size;
            uint offset;
        } [size];
    };
};

`This is a list of the entire contents of a file, in order. It contains some redundancy with respect to the header_item but is intended to be an easy form to use to iterate over an entire file.`

map_list 中保存着许多不同类型的 entry 对应的 offset, 与 header_item 有些重复, 例如以下类型的 entry 在 header_item 中也是存在的

  • TYPE_TYPE_ID_ITEM
  • TYPE_PROTO_ID_ITEM
  • TYPE_FIELD_ID_ITEM

但它也可以包含以下类型:

  • TYPE_CLASS_DATA_ITEM
  • TYPE_CODE_ITEM
  • TYPE_STRING_DATA_ITEM

这些数据在 header_item 是不存在的, 以 CLASS_DATA_ITEM 为例, 如果不使用中 map_list, 必须通过 header_item -> class_def_item -> class_data_item 这条路径才能找到. 有了 map_list, 代码可以用一种更简单一致的方法来遍历整个 dex 文件.

1.2. odex 文件结构

odex 文件对 dex 做了如下的修改:

  1. 文件开头加入一个 odex header
  2. dex 文件本身会被修改, 例如 xxx => xxx_quick
  3. 在文件末尾加入两个数据结构: classLookup 与 registerMapPool

1.2.1. classLookup

1.2.2. registerMap

register map 主要用来帮助 dvm 知道在每个函数的每个 GC Point 处当前各个寄存器中哪些保存着 java 对象.以便进行 exact GC. register map 导致 odex 比 dex 大了 9% 左右.

在 Hotspot 中, 也有一个类似的结构称为 OopMap

若没有这个 map, 则 GC 只能进行 conservative GC, 即 GC 时 dvm 只能假设当前所有寄存器中都保存着对象引用, 从而导致一些本来已经是 garbage 的对象没有被回收. (例如 r1 本来保存着一个整数 0xxxxx, 但这个整数刚好对应着某个已经不可达的对象的地址, 那么这个对象就会被重新标记为可达).

conservative GC 可能通过堆内存上下界检查, 对齐检查 (java 对象都是 8 字节对齐) 等方法过滤掉一些明显不可能是引用的值. 但它相对于 exact GC 还是有两方面明显的问题:

  1. 某些对象不会被回收, 导致内存压力
  2. 无法支持 copying GC.

1.3. class 文件结构

1.4. DexFile 与 DvmDex

dalvik 在扫描 class path 时, 会负责将所有的 odex 文件 parse 一遍. parse 的过程基本是:

  1. 通过 mmap 将 odex 映射到内存 A
  2. 生成 DexFile 结构, 将其成员设置为到 A 各个区域的指针
pDexFile->pStringIds = (const DexStringId*) (A + pHeader->stringIdsOff);
pDexFile->pTypeIds = (const DexTypeId*) (A + pHeader->typeIdsOff);
pDexFile->pFieldIds = (const DexFieldId*) (A + pHeader->fieldIdsOff);
pDexFile->pMethodIds = (const DexMethodId*) (A + pHeader->methodIdsOff);
pDexFile->pProtoIds = (const DexProtoId*) (A + pHeader->protoIdsOff);
pDexFile->pClassDefs = (const DexClassDef*) (A + pHeader->classDefsOff);
pDexFile->pLinkData = (const DexLink*) (A + pHeader->linkOff);
  1. 生成 DvmDex 结构

    这个结构实际就是 procmem 时显示的 "dalvik-aux-structure".这是一个辅助的数据结构: 因为 DexFile 基本是直接映射了 odex 文件, 里面的内容都是一些 "ID", 但实际执行时程序需要的通常不是 "ID", 而是真正的内容, 这时 DvmDex 相当于一个将 "ID" 映射为真正内容的一个 cache: cache 中包括 StringObject, ClassObject, Method, Field 等内容.

    以 dvmResolveClass 为例:

    dvmResolveClass
      resClass = dvmDexGetResolvedClass(pDvmDex, classIdx);
        return pDvmDex->pResClasses[classIdx];
      if (resClass != NULL):
        return resClass;
    

1.5. dexopt

dexopt is located in dalvik/dexopt/OptMain.cpp

1.5.1. dexopt 的作用

  • verification
  • optimization /dalvik/vm/analysis/Optimize.cpp::optimizeMethod
    1. quick 替换 与 符号解析

      将本来 java 在类加载时完成的符号解析的工作拿出来,提前将符号解析出来, 并且使用相应的 `quick' 指令代替原来的基于常量池符号引用的指令, 例如: 将之前的

      invoke-virtual {v0,v1},Ljava/io/PrintStream;->println(Ljava/lang/String;)V
      

      替换为

      invoke-virtual-quick {v0,v1},vtable #0x3b
      
    2. inline method
  • register map

1.5.2. There are three ways to launch dexopt

  1. From the VM. This takes a dozen args, one of which is a file descriptor that acts as both input and output. This allows us to remain ignorant of where the DEX data originally came from.
  2. From installd or another native application. Pass in a file descriptor for a zip file, a file descriptor for the output, and a filename for debug messages. Many assumptions are made about what's going on (verification + optimization are enabled, boot class path is in BOOTCLASSPATH, etc).
  3. On the host during a build for preoptimization. This behaves almost the same as (2), except it takes file names instead of file descriptors.

1.5.3. dex 何时被 dexopt

  1. zygote 启动时 zygote 启动时会负责 boot class 的 dexopt (通过 dvmClassStartup -> prepareCpe -> dvmRawDexFileOpen)
  2. PMS.performBootDexOpt PMS 启动时
  3. AMS.ensurePackageDexOpt AMS 启动应用之前
  4. dvmRawDexFileOpen ClassLoader 初始化时
  5. app 被安装时

1.6. load

1.6.1. ClassLoader

ClassLoader 并不是一定要在 native 实现的, 有些 ClassLoader 是完全用 Java 写的. 但 dalvik 的 ClassLoader 因为底层都依赖于 dex, 所以都是通过 native 层的 DexFile (代表 dex), RawDexFile (代表 odex), DvmDex (代表 auxiliary cache) 实现的.

Java 层和 ClassLoader 相关的类主要有:

  1. VMClassLoader
  2. ClassLoader
  3. BootClassLoader
  4. BaseDexClassLoader
  5. PathClassLoader

                 -+-------------+
                  | ClassLoader |
                 -+-----+-------+
                        |
                        |
          -+------------+-----------------+
           |                              |
           |                              |
    -+-----+-----------+          -+------+-------------+
     | BootClassLoader |           | BaseDexClassLoader |
    -+-----+-----------+          -+------+-------------+
           |                              |
 comp      |                       comp   |
-+---------+-----+                -+------+---+
 | VMClassLoader |                 | DexFile  |
-+---------+-----+                -+------+---+
           |                              |
           |                              |
       native                         native


1.6.1.1. PathClassLoader

当 PathClassLoader 初始化时, 会初始化 DexFile, 相当的会设置对应的 odex 的路径.

  1. ContextImpl.getClassLoader 最终返回的是一个 PathClassLoader
  2. PathClassLoader 的构造函数

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    

    其中, 基类的第一个参数表示 apk/jar, 第二个参数表示 optimizedDirectory, 表示 odex 放置在哪里, 以后查找和生成 odex 时会使用该目录, 若该参数为 null, 则表示使用默认的 `/data/dalvik-cache/`

  3. PathClassLoader 的基类: BaseDexClassLoader 的构造函数

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    
    

    其中 optimizedDirectory 为 null, 表示使用默认的 `/data/dalvik-cache/`

  4. DexPathList 构造函数

    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
      dex = loadDexFile(file, optimizedDirectory);
        if (optimizedDirectory == null):
          return new DexFile(file);
            mCookie = openDexFile(sourceName, outputName, flags);
              Dalvik_dalvik_system_DexFile_openDexFile(const u4* args,)
                dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile,)
                  if (odexOutputName == NULL):
                    // 生成 odex 对应的文件名.
                    cachedName = dexOptGenerateCacheFileName(fileName,  NULL);
                  optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime,);
                  ...
        else:
          String optimizedPath = optimizedPathFor(file, optimizedDirectory);
          return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    
    
1.6.1.2. BootClassLoader
app_process.main()
  AndroidRuntime::start
    AndroidRuntime::startVM
      JNI_CreateJavaVM
        dvmStartup
          dvmClassStartup()
            // bootClassPathStr 包括 core.jar, bouncycastle.jar ...
            processClassPath(gDvm.bootClassPathStr, true);
              foreach entry in bootClassPathStr:
                prepareCpe(&entry, isBootstrap);
                  dvmRawDexFileOpen(cpe->fileName, NULL, &pRawDexFile, isBootstrap);

1.6.2. 双亲委派

ClassLoader 这个基类主要作用是实现双亲委派

protected Class<?> loadClass(String className) {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            // Don't want to see this.
        }

        if (clazz == null) {
            clazz = findClass(className);
        }
    }
    return clazz;
}

ClassLoader 通过 parent 指针可以形成一棵树, BootClassLoader 是树根.

在 dalvik 底层代码中并没有 BootClassLoader 这个东西: dalvik 通过把 clazz->classLoader 置为 NULL 表示该 class 是由 BootClassLoader 加载的.

/* defining class loader, or NULL for the "bootstrap" system loader */
Object*         classLoader;

简单起见, 我们假设 BaseDexClassLoader 的 parent 都是 BootClassLoader, 所以 PathClassLoader 的 loadClass 会先调用 BootClassLoader 的 loadClass:

BootClassLoader.loadClass
  BootClassLoader.findClass
    VMClassLoader.loadClass(name, false);
      // native, 其中 null 表示 ClassLoader 为 null, 以
      // 便从 bootstrap class 中查找
      clazz = dvmFindClassByName(nameObj, NULL, resolve);
        dvmFindClassNoInit
          dvmFindSystemClassNoInit
            findClassNoInit(descriptor, null);

若 BootClassLoader 没有找到 class, 则使用 BaseDexClassLoader 有加载

BaseDexClassLoader.findClass(String name)
  Class c = pathList.findClass(name, suppressedExceptions);
    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
      DexFile.defineClassNative(name, loader, cookie);
        // native
        clazz = dvmDefineClass(pDvmDex, descriptor, loader);
          findClassNoInit(descriptor, loader);

无论是 VMClassLoader 或是 DexFile, 最终都通过 native 的 findClassNoInit 进行 dex 的加载

1.6.3. 类的加载

和类加载相关的入口主要有:

  1. dvmFindClassNoInit
    1. findClassNoInit
  2. dvmResolveClass
  3. java 层的 classForName
  4. java 层的 ClassLoader.loadClass
1.6.3.1. dvmFindClassNoInit

dvmFindClassNoInit 较底层的加载类的方法, 它需要指定一个字符串来表示要加载的类, 以及一个 ClassLoader, 它作的基本上就是这么一件事:

  1. 若 ClassLoader 为空, 则调用更底层的 findClassNoInit 从 boot class path 中加载类
  2. 若 ClassLoader 不为空, 则通过反射去调用 java 层的 ClassLoader.loadClass. 而后者会先通过双亲委派以 NULL 做为 ClassLoader 再次调用 dvmFindClassNoInit (对应 BootClassLoader.loadClass), 然后会通过 DexFile 调用 findClassNoInit

后面提到的 dvmResolveClass 实际上就是把 ref 转换为字符串, 并且根据 referrer 找到调用者的 ClassLoader 后直接调用 dvmFindClassNoInit. 而 classForName 因为本身就使用字符串表示要加载的类, 所以它只需要找到调用者的 ClassLoader 后就可以直接调用 dvmFindClassNoInit

dvmFindClassNoInit(className, referrer->classLoader);
  // 此时 loader 不为空, 它对应的是 app 的 BaseDexClassLoader,
  // 所以会调用 findClassFromLoaderNoInit
  if (loader != NULL):
    return findClassFromLoaderNoInit(descriptor, loader);
      // 在调用 class loader 之前, 通过 dvmLookupClass 从 gDvm.loadedClasses 中
      // 查找要加载的 class 是否已经加载, found 的条件有两个:
      // 1. loadedClasses 中存在一个 class, class->descriptor 和 class->loader 与
      // 参数中的值都是一致的.
      // 2. loadedClasses 中存在一个 class, class->descriptor 是一致的, class->initiatingLoaderList
      // 中包含参数中的 loader
      ClassObject* clazz = dvmLookupClass(descriptor, loader, false);
      // ClassLoader 开始工作
      // 调用 ClassLoader.java 的 loadClass 函数
      const Method* loadClass =
        loader->clazz->vtable[gDvm.voffJavaLangClassLoader_loadClass];
      dvmCallMethod(self, loadClass, loader, &result, nameObj);
        // 双亲委派导致 ClassLoader.loadClass 被调用
        // BootClassLoader 导致 dvmFindClassNoInit 再次被调用, 但是
        // loader 参数为 null
        ClassLoader.loadClass
          BootClassLoader.loadClass
            dvmFindClassNoInit(className, null);
              return dvmFindSystemClassNoInit(descriptor);
                // 这个和双亲委派无关, 直接使用 dexFile 来真正加载类
                findClassNoInit(descriptor, NULL, NULL);
                  clazz = dvmLookupClass(descriptor, loader, true);
                  if (clazz == NULL):
                    // !!! THE HARD WORK !!!
                    clazz = loadClassFromDex(pDvmDex, pClassDef, loader);
                    dvmAddClassToHash(clazz);
                    // 后面的部分参考 <oo.org: findClassNoInit>
          // 若 BootClassLoader.loadClass 失败, 说明这个类是 BaseDexClassLoader
          // 定义的, 双亲委派会调用 BaseDexClassLoader.findClass
          BaseDexClassLoader.findClass
            pathList.findClass(name);
              findClassNoInit(className, loader);
      // findClassFromLoaderNoInit 最后会将 loader 加入到 clazz 的 initiatingLoaderList 中
      // 所谓的 initiatingLoaderList, 是指当前 class 的加载是由这个 loader 发起的, 因为最终
      // 负责加载的 loader (clazz->loader) 可能并不是这个 loader. 这个 initiatingLoaderList 在 dvmLookupClass
      // 会用到.
      dvmAddInitiatingLoader(clazz, loader);
      // end of findClassFromLoaderNoInit
  else:
    dvmFindSystemClassNoInit(descriptor);

1.6.3.2. dvmResolveClass 是 byte code 中加载 class 的入口 (因为它使用 ref 指示 class

而且不是 class 名)

HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
  clazz = dvmResolveClass(curMethod->clazz, ref, false);
    dvmResolveClass(referrer, ref, init)
      // referrer->classLoader 调用该代码的对象使用的 BaseDexClassLoader
      // jvm spec 规定了若 A 由 cl 加载, 则 A->B 时也要由 cl 负责加载 B
      dvmFindClassNoInit(className, referrer->classLoader);
1.6.3.3. classForName

classForName 即 Class.forName(str, classLoader), 若不指定 classLoader, 根据 jvm 规范, 当前方法所属的 class 的 classLoader 负责加载这个类. 即像 dvmResolveClass 中展示的那样: curMethod->clazz->classLoader.

这里有个问题: 若 Class.forName 在 java 层不指定 classLoader, 而要求 jni 层像 dvmResolveClass 那样自己获得 curMethod->clazz->classLoader 是否可行? 不可行, 因为:

若 classForName 对应的 native 函数 Dalvik_java_lang_Class_classForName 方法调用 curMethod, curMethod 代表是当前 jni 方法, 而它的 clazz 是 Class 类, 最终会使用 BootClassLoader 来加载, 这不是我们想要的.

因此, java 层调用 classForName 时, 需要提交获得 curMethod->clazz->classLoader, 并做为参数传给 findClassNoInit.

而 java 层获得当前的 classLoader 是使用 native VMStack.getCallingClassLoader

VMStack.getCallingClassLoader
  static void Dalvik_dalvik_system_VMStack_getCallingClassLoader
    ClassObject* clazz = dvmGetCaller2Class(dvmThreadSelf()->interpSave.curFrame);
      // dvmGetCaller2Class:  Get the caller's caller's class.  Pass in the current fp.
      // 所谓的 caller's caller's class, 是这么回事:
      // 第一个 caller 是指 调用 VMStack 的 getCallingClassLoader 这个方法的方法, 即
      // Class.forName 方法, 显然不是我们想要的.
      // caller's caller 是调用 Class.forName 的那个方法, 是我们想要的.

      // 当前的 frame 对应的是 getCallingClassLoader 这个 jni 方法,
      // prevFrame 对应 Class.forName 方法
      void* caller = SAVEAREA_FROM_FP(curFrame)->prevFrame;
      callerCaller = dvmGetCallerFP(caller);
        void* caller = SAVEAREA_FROM_FP(curFrame)->prevFrame;
        if (dvmIsBreakFrame((u4*)caller)):
          caller = SAVEAREA_FROM_FP(caller)->prevFrame;
        return caller;
      return SAVEAREA_FROM_FP(callerCaller)->method->clazz;

1.6.3.4. ClassLoader.loadClass

ClassLoader.loadClass 可能比 classForName 快一些. 因为 classForName 的调用路径是:

  1. 先通过 jni 调用到 Dalvik_java_lang_Class_classForName
  2. 然后调用到 dvmFindClassNoInit
  3. 然后 dvmFindClassNoInit 通过 dvmCallMethod 反过来再调用 ClassLoader.loadClass 进行双亲委派

显然, 若直接调用 ClassLoader.loadClass 的话可以跳过前两步.

1.6.4. 总结

  1. 对于 dvmResolveClass 和 classForName 都需要提交根据 curMethod->clazz->classLoader 获得`定义当前 method 的 class 使用的 classLoader`, 并使用这个 classLoader 调用 dvmFindClassNoInit, 这是 jvm 规范要求的.
  2. dvmFindClassNoInit 加载类时, 要 classLoader 不为空, 则使用 classLoader 来加载, 否则使用 dvmFindSystemClassNoInit 从 bootstrap classpath 中加载 (即从 `BootClassLoader` 中加载)

    通过 ClassLoader 加载时, java 层的双亲委派会优先使用 BootClassLoader 来加载, BootClassLoader 最终会通过 dvmFindSystemClassNoInit 来加载.

    若 BootClassLoader 没有加载成功, 则 BaseDexClassLoader 的 findClass 会负责通过 dvmDefineClass(pDvmDex, descriptor, loader) 在 app 自己的 dex 中加载类.

    即 dvmFindClassNoInit 与 java 层的各种 ClassLoader 类实现了 dalvik 的双亲委派模型.

  3. 最终 dex 都是通过 findClassNoInit 加载进来的. findClassNoInit 是负责从 ClassLoader 对应的 dexFile (若 ClassLoader 为空则从 boot class path) 中加载类, 它无须再考虑双亲委派.
  4. dvmFindClassNoInit 或 findClassNoInit 过程中, 都会用到 dvmLookupClass 来避免重复的加载.

    dvmLookupClass 进行查找时考虑到 clazz->loader 因为双亲委派的原因可能不是真正发起类加载的那个 ClassLoader, 所以它使用了 clazz->initiatingLoaderList 来记录这些信息.

1.7. verify

1.7.1. pre-verify

pre-verify 实际上指的就是 dexopt 时进行的 verify. 在默认配制下, dexopt 会先对 dex 进行 verify, 然后再进行 optimization. 实际上, 默认配制下 dexopt 要求必须先 verify 以后才能进行 optimization. 所以 pre-verify 发生的时机就是 dexopt 发生的时机: 例如通过 zygote 的 dvmClassStartup.

通过 dexopt 的 pre-verify, odex 中的 class 对象的 CLASS_ISPREVERIFIED 标志会被置位, 这个标记会阻止后续 dvminitclass 时的 dvmVerifyClass 调用.

实际上, pre-verify, dexopt 合起来做为 dex 的 prepare

1.7.2. dvmVerifyClass

通过 setprop dalvik.vm.verify-bytecode false, 可以禁用整个 dex prepare 过程 (包括 pre-verify 和 dexopt). 若开机启动时因为这个 prop 没有进行 verify, 但运行时通过 setprop dalvik.vm.verify-bytecode true 或直接通过 dalvik 的启动参数 (-Xverify:all 或 -Xverify:remote) 重新启用了 verify, 则在 class 初始化时, 会调用 dvmVerifyClass 进行 verify. 这种运行时的 verify 会导致应用加载明显变慢 (可能 40% 以上)

Xverify 参数接受三种值:

  1. all 所有 class 被需要被 verify
  2. remote boot class 以外的 class 需要被 verify
  3. none 任何 class 都不会被 verify

1.7.3. 关于 verify 和 dexopt 的实验

这里有一个 java 文件:

class Test2 {
    private static int a = 10;
}

public class Test {
    public static void main(String[] args) {
        System.out.println("hello: "+Test2.a);
    }
}

但这个 java 文件是编译不过, 为了让 javac 能生成这个语义的 class 文件(让 class 中的 main 函数直接访问 Test2 的私有成员), 我们可以这样操作:

  1. 把 private 暂时改为 public, javac Test.java
  2. 把生成的 Test.class 保存起来
  3. 把 public 变回 private, 但把 System.out.println 一行去掉, 然后 javac Test.java, 把 Test2.class 保存起来
  4. 把两次保存的 Test.class 和 Test2.class 打包为 Test.dex. 这样 Test.dex 中就会包含 Test 访问 Test2 的私有变量这样的非法代码.

在手机上用 dalvikvm 命令执行, 分别指定不同的 Xverify 和 Sexpot 参数:

  1. dalvikvm -Xverify:none -Xdexopt:all -cp Test.dex Test
    • dalvik-cache 下有 odex 生成, 且 Test 确实被优化了 (invoke-virtual 被替换为 invoke-virtual-quick)
    • 程序正常输出 10
  2. dalvikvm -Xverify:all -Xdexopt:all -cp Test.dex Test
    • dalvik-cache 下有 odex 生成, 且 Test 被优化
    • 程序报错:

      root@scx15_sp7715ga:/data # dalvikvm -Xverify:all -Xdexopt:all -cp Test.dex Te>
      Unable to find static main(String[]) in 'Test'
      java.lang.VerifyError: Test
              at dalvik.system.NativeStart.main(Native Method)
      java.lang.VerifyError: Test
              at dalvik.system.NativeStart.main(Native Method)
      

      这里报错是因为: Xverify:all 会导致 dexopt 时进行 pre-verify, 但这里 verify 会失败, 所以 class 不会被标记为 PREVERIFIED, 但因为 Xdexopt:all, 所以虽然 pre-verify 失败了, dexopt 还是会进行优化, 最终 odex 中包括的 Test 类是优化后的结果.

      dvminitclass @ Class.cpp 中的这段代码:

      if (clazz->status < CLASS_VERIFIED) {
          // ...
          if (IS_CLASS_FLAG_SET(clazz, CLASS_ISOPTIMIZED)) {
              ALOGW("Class '%s' was optimized without verification; "
                    "not verifying now",
                    clazz->descriptor);
              ALOGW("  ('rm /data/dalvik-cache/*' and restart to fix this)");
              // verify_failed 处会抛出异常
              goto verify_failed;
          }
      }
      

      会抛出上面的异常.

      即: 若 class 没有通过 verify 就被 dexopt, 则在 dvmInitClass 的阶段, dalvik 就会主动抛出异常. 这样做的原因可能是: 因为 code 已经被 dexopt, 这里可能无法再 replaceFailingInstruction

  3. dalvikvm -Xverify:all -Xdexopt:none -cp Test.dex Test
    • dalvik-cache 下有 odex 生成, 但 Test 没有被优化
    • 程序报错:

      root@scx15_sp7715ga:/data # dalvikvm -Xverify:all -Xdexopt:none -cp Test.dex T>
      java.lang.IllegalAccessError: tried to access field Test2.a from class Test
              at Test.main(Test.java:6)
              at dalvik.system.NativeStart.main(Native Method)
      

      这里报错是因为: Xverify:all 会导致 pre-verify, 但 pre-verify 会失败, 导致后续 dvmInitClass 还会再次调用 dvmVerifyClass 进行 late verify. (因为 Xdexopt 为 none, 所以不会发生 case 2 的问题). late verify 时还是会失败, 这里会通过 replaceFailingInstruction 将原来的指令 (sget) 替换为一个抛出异常的指令, 但这里做的仅仅是指令替换, 并不会直接抛出异常. 当被替换的指令直接执行时, 才会报错.

综上:

  1. 无法 Xdexopt 如何指定, odex 总是会生成, 但 Xdexopt 会影响 odex 中的代码是优化过的还是 dex 中的原始代码.
  2. Xverify 会影响 dexopt 时的 pre-verify 和 class 加载时的 late verify
  3. 在 late verify 阶段, 若当前代码并没有被 dexopt 优化, 则会通过 replaceFailingInstruction 进行指令替换, 否则直接报错
  4. 默认情况下 Xverify = all, Xdexopt = verified, 所以, 若一个类有问题, 则会在运行时报错 (通过 replaceFailingInstruction), 而不是在 verify 阶段 (pre-verify 或 late verify) 报错. 抛开 pre-verify, dexopt 这些不谈, java 的 verify 的功能本质上就是 replaceFailingInstruction
1.7.3.1. 关于 replaceFailingInstruction

在 pre-verify 阶段, 即使 verify 失败, 也不会进行 replaceFailingInstruction 的, 因为:

verifyInstruction @ CodeVerify.cpp
// ...
if (!VERIFY_OK(failure)) {
    // 在 pre-verify 阶段, gDvm.optimizing 为 true
    if (failure == VERIFY_ERROR_GENERIC || gDvm.optimizing) {
        /* immediate failure, reject class */
        LOG_VFY_METH(meth, "VFY:  rejecting opcode 0x%02x at 0x%04x",
                     decInsn.opcode, insnIdx);
        goto bail;
    } else {
        /* replace opcode and continue on */
        ALOGD("VFY: replacing opcode 0x%02x at 0x%04x",
              decInsn.opcode, insnIdx);
        if (!replaceFailingInstruction(meth, insnFlags, insnIdx, failure)) {
            LOG_VFY_METH(meth, "VFY:  rejecting opcode 0x%02x at 0x%04x",
                         decInsn.opcode, insnIdx);
            goto bail;
        }
        /* IMPORTANT: meth->insns may have been changed */
        insns = meth->insns + insnIdx;

        /* continue on as if we just handled a throw-verification-error */
        failure = VERIFY_ERROR_NONE;
        nextFlags = kInstrCanThrow;
    }
}

replaceFailingInstruction 需要修改 odex 映射的内存, 而不是 odex 本身. 因为 odex 都是以只读方式进行 mmap 的, 所以 replaceFailingInstruction 需要先修改 mmap 区域的权限, 再进行写操作, 这会导致额外的内存开销.

Author: [email protected]
Date:
Last updated: 2024-05-10 Fri 17:03

知识共享许可协议