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) 部分组成. 构成一个复杂的索引关系, 这些表和数据的位置大约是:
- dex header
- dex string ids
- dex type ids
- dex field ids
- dex method ids o
- dex class defs
- data
- 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 相关的内容:
- try_item_list
- 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 做了如下的修改:
- 文件开头加入一个 odex header
- dex 文件本身会被修改, 例如 xxx => xxx_quick
- 在文件末尾加入两个数据结构: 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 还是有两方面明显的问题:
- 某些对象不会被回收, 导致内存压力
- 无法支持 copying GC.
1.3. class 文件结构
1.4. DexFile 与 DvmDex
dalvik 在扫描 class path 时, 会负责将所有的 odex 文件 parse 一遍. parse 的过程基本是:
- 通过 mmap 将 odex 映射到内存 A
- 生成 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);
生成 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
quick 替换 与 符号解析
将本来 java 在类加载时完成的符号解析的工作拿出来,提前将符号解析出来, 并且使用相应的 `quick' 指令代替原来的基于常量池符号引用的指令, 例如: 将之前的
invoke-virtual {v0,v1},Ljava/io/PrintStream;->println(Ljava/lang/String;)V
替换为
invoke-virtual-quick {v0,v1},vtable #0x3b
- inline method
- …
- register map
1.5.2. There are three ways to launch dexopt
- 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.
- 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).
- 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
- zygote 启动时 zygote 启动时会负责 boot class 的 dexopt (通过 dvmClassStartup -> prepareCpe -> dvmRawDexFileOpen)
- PMS.performBootDexOpt PMS 启动时
- AMS.ensurePackageDexOpt AMS 启动应用之前
- dvmRawDexFileOpen ClassLoader 初始化时
- 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 相关的类主要有:
- VMClassLoader
- ClassLoader
- BootClassLoader
- BaseDexClassLoader
- PathClassLoader
-+-------------+ | ClassLoader | -+-----+-------+ | | -+------------+-----------------+ | | | | -+-----+-----------+ -+------+-------------+ | BootClassLoader | | BaseDexClassLoader | -+-----+-----------+ -+------+-------------+ | | comp | comp | -+---------+-----+ -+------+---+ | VMClassLoader | | DexFile | -+---------+-----+ -+------+---+ | | | | native native
1.6.1.1. PathClassLoader
当 PathClassLoader 初始化时, 会初始化 DexFile, 相当的会设置对应的 odex 的路径.
- ContextImpl.getClassLoader 最终返回的是一个 PathClassLoader
PathClassLoader 的构造函数
public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); }
其中, 基类的第一个参数表示 apk/jar, 第二个参数表示 optimizedDirectory, 表示 odex 放置在哪里, 以后查找和生成 odex 时会使用该目录, 若该参数为 null, 则表示使用默认的 `/data/dalvik-cache/`
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/`
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. 类的加载
和类加载相关的入口主要有:
- dvmFindClassNoInit
- findClassNoInit
- dvmResolveClass
- java 层的 classForName
- java 层的 ClassLoader.loadClass
1.6.3.1. dvmFindClassNoInit
dvmFindClassNoInit 较底层的加载类的方法, 它需要指定一个字符串来表示要加载的类, 以及一个 ClassLoader, 它作的基本上就是这么一件事:
- 若 ClassLoader 为空, 则调用更底层的 findClassNoInit 从 boot class path 中加载类
- 若 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 的调用路径是:
- 先通过 jni 调用到 Dalvik_java_lang_Class_classForName
- 然后调用到 dvmFindClassNoInit
- 然后 dvmFindClassNoInit 通过 dvmCallMethod 反过来再调用 ClassLoader.loadClass 进行双亲委派
显然, 若直接调用 ClassLoader.loadClass 的话可以跳过前两步.
1.6.4. 总结
- 对于 dvmResolveClass 和 classForName 都需要提交根据 curMethod->clazz->classLoader 获得`定义当前 method 的 class 使用的 classLoader`, 并使用这个 classLoader 调用 dvmFindClassNoInit, 这是 jvm 规范要求的.
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 的双亲委派模型.
- 最终 dex 都是通过 findClassNoInit 加载进来的. findClassNoInit 是负责从 ClassLoader 对应的 dexFile (若 ClassLoader 为空则从 boot class path) 中加载类, 它无须再考虑双亲委派.
dvmFindClassNoInit 或 findClassNoInit 过程中, 都会用到 dvmLookupClass 来避免重复的加载.
dvmLookupClass 进行查找时考虑到 clazz->loader 因为双亲委派的原因可能不是真正发起类加载的那个 ClassLoader, 所以它使用了 clazz->initiatingLoaderList 来记录这些信息.
1.7. verify
http://www.netmite.com/android/mydroid/dalvik/docs/verifier.html http://www.netmite.com/android/mydroid/dalvik/docs/dexopt.html http://www.netmite.com/android/mydroid/dalvik/docs/embedded-vm-control.html
verify 的发生有两种场合:
- pre-verify
- dvmVerifyClass
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 参数接受三种值:
- all 所有 class 被需要被 verify
- remote boot class 以外的 class 需要被 verify
- 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 的私有成员), 我们可以这样操作:
- 把 private 暂时改为 public, javac Test.java
- 把生成的 Test.class 保存起来
- 把 public 变回 private, 但把 System.out.println 一行去掉, 然后 javac Test.java, 把 Test2.class 保存起来
- 把两次保存的 Test.class 和 Test2.class 打包为 Test.dex. 这样 Test.dex 中就会包含 Test 访问 Test2 的私有变量这样的非法代码.
在手机上用 dalvikvm 命令执行, 分别指定不同的 Xverify 和 Sexpot 参数:
- dalvikvm -Xverify:none -Xdexopt:all -cp Test.dex Test
- dalvik-cache 下有 odex 生成, 且 Test 确实被优化了 (invoke-virtual 被替换为 invoke-virtual-quick)
- 程序正常输出 10
- 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
- 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) 替换为一个抛出异常的指令, 但这里做的仅仅是指令替换, 并不会直接抛出异常. 当被替换的指令直接执行时, 才会报错.
综上:
- 无法 Xdexopt 如何指定, odex 总是会生成, 但 Xdexopt 会影响 odex 中的代码是优化过的还是 dex 中的原始代码.
- Xverify 会影响 dexopt 时的 pre-verify 和 class 加载时的 late verify
- 在 late verify 阶段, 若当前代码并没有被 dexopt 优化, 则会通过 replaceFailingInstruction 进行指令替换, 否则直接报错
- 默认情况下 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 区域的权限, 再进行写操作, 这会导致额外的内存开销.