AndroidDalvik虚拟机结构及机制剖析——第2卷Dalvik虚拟机各模块机制分析(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-26 18:58:37

点击下载

作者:吴艳霞,张国印

出版社:清华大学出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

AndroidDalvik虚拟机结构及机制剖析——第2卷Dalvik虚拟机各模块机制分析

AndroidDalvik虚拟机结构及机制剖析——第2卷Dalvik虚拟机各模块机制分析试读:

前言

读者在看过第1卷之后,应该已经大致了解了Dalvik虚拟机,包括其模块组成、Dex文件格式以及使用到的工具。本书作为第2卷,在第1卷的基础上,将要细致地分析Dalvik内部的组成原理,采用情景分析的方式,有针对性地分析Android Dalvik虚拟机的源代码,围绕类加载、解释器、即时编译、本地方法调用、内存管理及反射机制等功能模块逐个击破,帮助各位读者从微观上更深入地理解Dalvik虚拟机中各功能模块的实现原理及运行机制。在其中展示的所有源码,都来自于Android 4.0.4的源码,并在此基础上添加了些许注释,以便理解源码。源码篇幅可能有点长,但大多数都是其中最主要的内容,如果读者手边有源码,可以对照着看,效果更好。

此时,在翻开这第2卷时,您手边应该已经准备好了一个能正常调试Dalvik的环境。现在,就进入本卷,边学习边实践,揭开Dalvik内部的神秘面纱。

全书共分为6章:

第1章介绍类加载机制,包括其整体的工作流程和机制,详细讲解其中的三个阶段,并以一个实例验证了源码分析的结果;

第2章介绍Dalvik虚拟机中至关重要的内存管理机制,详细讲解其实现的两种算法;

第3章分析JNI模块的实现原理,在分析源码的基础上,细致入微地介绍为何用JNI编程会提升程序的执行效率;

第4章以反射机制的一个代码示例开始,介绍其涉及的API,并从宏观(实现的三个模块)到微观(具体实现细节)详细介绍了反射机制;

第5章介绍实现解释器的两种不同的技术,比较Fast解释器和Portable解释器的不同及各自的优势和劣势;

第6章从介绍最近在解释器中非常火的JIT(即时编译)开始,到JIT的所谓的前端分析,再到JIT的后端代码生成,为本书画上一个圆满的句号。

本书主要由哈尔滨工程大学吴艳霞、张国印负责编写,参与本书编写和校核工作的还有汪永峰、王彦璋、谢东良、于成、张婷婷、苗施亮、许圣明、檀凯,这里对他们的辛勤工作表示衷心的感谢。

本书主要是针对高级Android应用开发工程师、Android系统开发工程师、Android移植工程师及对Android Dalvik虚拟机源码实现感兴趣的读者参考使用。编 者2014年5月

第1章 类加载模块的原理及实现

本章内容提要➢ 什么是类加载机制?➢ 类加载机制具有怎样的功能?➢ 类加载的输入输出是什么?➢ 类加载机制的工作流程是什么?➢ 类加载机制的函数具体实现是怎样的?➢ 类加载机制是如何与解释器进行交互的?

本章主要围绕以下6点问题展开讨论,1.1节主要回答了什么是类加载、类加载的具体功能以及机制的输入与输出;1.2节从一个整体宏观的角度介绍了类加载机制的整体工作流程,给出了类加载机制的工作阶段划分;在接下来的三节中,分别对类加载机制的三个关键阶段进行详细的分析阐述,详细讲述了三个工作阶段的功能原理以及相关的函数源码分析;最后一节将会通过一个虚拟机运行实例,介绍类加载机制的产物是如何被解释器引用并执行,并从这一角度展示类加载机制与其他功能模块交互的方式。

1.1 类加载机制概述

谈起类加载,想必大多数读者比较陌生,类加载机制究竟具有怎样的功能以及它在虚拟机中是如何工作的是本章具体讨论的内容。

我们都知道程序是由机器指令和数据组成的,对于一个真实的机器,通常由人工将指令和数据交付给机器,再由机器按照指令去对数据进行计算。但是对于虚拟机来说,在其模拟真实机器执行程序之前,需要一种加载机制将程序的指令和数据装载进入虚拟机内部的运行时环境,使虚拟机中的执行模块可以根据程序执行的需要随时取得目标指令和相关数据,以完成程序的执行任务。对于Java虚拟机而言,这一种将类数据装载进入虚拟机运行时环境的过程就称为类加载。具体到Dalvik虚拟机,类加载机制的主要功能就是将应用程序中Dalvik操作码以及程序数据提取并加载到虚拟机内部,以保证程序的正确运行。

类加载机制在应用程序和执行模块之间建立起了一个桥梁,对于整个Dalvik虚拟机架构来说,类加载机制处在一个承上启下的位置。图1.1反映了类加载机制在Dalvik虚拟机执行过程中所承担的重要作用。图1.1 Dalvik虚拟机程序执行流程图

类加载机制作为Dalvik虚拟机一个重要的功能模块,其输入输出是什么呢?在第1卷第3章中,已经对Dex文件进行了介绍,在此就不再赘述。简单来说,Dex文件就是Android应用程序的可执行文件,里面封装了Dalvik字节码以及程序数据,而类加载机制就是负责提取装载Dex文件中的指令与数据。因此,Dex文件是Dalvik虚拟机的输入文件,更是类加载机制的主要处理对象。既然Dex文件是类加载机制的输入文件,那么类加载机制的输出又是什么呢?事实上,类加载机制的输出是一个名为ClassObject的数据结构实例对象,类加载将目标类的各项资源数据与ClassObject数据结构下的各个成员变量以指针的形式进行关联,执行模块只需通过该数据结构即可获得目标类所有的运行时数据,使程序的顺利执行成为可能。点拨 程序是由指令序列组成的,告诉计算机如何完成一个具体的任务。程序是软件开发人员根据用户需求开发的、用程序设计语言描述的适合计算机执行的指令(语句)序列。而对于Dalvik虚拟机来说,它所能“读懂”的指令是Dalvik操作码,其介绍可以参见第1卷第3章。

1.2 类加载机制整体工作流程介绍

在了解了类加载机制的主要功能以及输入输出产物后,我们不禁要问,类加载机制在工作过程中主要有哪几项关键工作以及其工作流程又是什么?经过对类加载机制源码的分析研究,类加载机制工作的主要内容以及其整体的工作流程主要分为以下三点。(1)对Dex文件进行验证并优化,验证的目的是对Dex文件中的类数据进行安全性、合法性检验,为虚拟机的安全稳定运行提供保证;而优化的目的则是根据当前设备平台特性对程序中的字节码进行优化替换并为Dex文件增加辅助信息,最后输出经过优化的Odex文件,使之可以代替原有的Dex文件更加高效地被虚拟机执行。(2)对优化后的Odex文件进行解析,其目标就是通过在内存中创建专用的数据结构描述表示该Odex文件,使虚拟机对目标Odex文件中各个部分的类数据都是可达的,为随后实际加载某一指定类做好数据准备。(3)对指定类进行实际加载,其功能是实时根据Dalvik虚拟机执行需要从已被解析的Dex文件中提取二进制Dalvik字节码并将其封装进运行时数据结构,以供解释器解释执行。而该运行时数据结构实际上是一个ClassObject结构体对象,也称为类对象,该数据结构用于封装程序类的所有运行时数据信息。当虚拟机执行一个类方法时,解释器将引用并执行类对象中封装的方法操作码,进而达到完成程序要求的执行目标。由此可见,类对象实例在程序运行的过程中承担着不可替代的重要作用。

图1.2简要描述了类加载机制的整体工作流程。图1.2 类加载机制整体工作概况图

在随后的内容中将分别从Dex文件的优化与验证、Dex文件解析以及类的实际加载等三方面(关键内容)展开介绍,以帮助读者对类加载机制的整体功能、设计原理以及函数工作流程有一个较为深入的理解。

1.3 Dex文件的优化与验证

事实上,Dex文件中类数据的优化与验证是Dex文件解析工作的一部分,但由于该优化与验证工作在Dalvik虚拟机中被前置,使之成为Dalvik区别于其他Java虚拟机的重要特征,因此这部分工作具有较高的重要性,其工作效果的好坏在一定程度上决定了Dalvik虚拟机是否可以高效安全地运行,因此本书特将这部分内容抽取出来单独介绍。同时,Dalvik虚拟机的优化技术一直是业界关注的焦点,如何进一步提高Dalvik虚拟机在嵌入式设备上的性能表现应该是未来工程开发人员工作的重点。

在这一节中,主要讲解了Dex文件的优化与验证技术的设计原理、关键数据结构以及其函数执行流程。希望通过对这三方面内容的介绍,可以让读者由表及里地了解该技术的功能原理以及具体实现。

1.3.1 Dex文件优化验证的原理与实现

依据Google提供的开发文档的描述,在通常情况下,优化和验证Dex文件最安全且最便捷的方式就是在虚拟机中直接加载目标Dex文件并运行其中包含的所有类,因为一旦加载失败或是运行失败,就意味着Dex文件优化验证的失败。因此,虚拟机究竟是使用何种方法,以较为高效的手段实现了对Dex文件的验证与优化,这将是一个非常值得深入研究的问题。为了得到最为直观、真实的结论,需要从Android源码入手,以彻底弄清问题的本质。

根据对优化机制的源码研究发现:Dalvik虚拟机的优化验证工作独立于程序的执行,同时优化验证这两部分功能也被整合成为一个功能模块并且类加载机制在加载工作的初期通过类似接口调用的方式调用这个优化模块对目标Dex文件进行优化。为了解决资源占用问题,Android系统将会新建一个虚拟机用于实现相应的功能,在Dex文件的优化验证工作结束并正常输出优化文件后,系统将释放该虚拟机占用的所有资源。同时,为了更大程度地保证原Dex文件的数据的安全以及优化机制的独立性,优化机制并不直接改写原Dex文件,而是重新创建一个后缀为.Odex的空文件并以严格的格式要求将所有的优化信息写入该文件,主要包括依赖库关系、寄存器映射关系以及类的索引关系,这些关系的建立会大大提高类加载机制的执图1.3 优化机制工作流程图行效率,同时,在优化过程中还会根据平台特性对原Dex文件中部分字节码进行替换(例如,对字段的访问方式由查找改为直接引用)以提高程序执行速率,最后再将重写的Dex文件也写入Odex文件(1.3.2节将详细介绍Odex文件结构以及各部分功能)。Odex文件作为优化机制的输出将会取代原Dex文件并作为直接的可执行文件被其他功能模块(例如,内存管理模块、解释器模块等)调用。Dex文件的优化机制大致的工作流程如图1.3所示。

Dex文件的优化与验证机制在Dalvik虚拟机中被设计成一个独立的系统工具,这样做的好处是使该机制具有比较高的独立性,使整个机制模块性更好,在一定程度上降低了整个Android系统的冗余。同时,也提供了一个技术参考,在适应低性能的移动平台时,应该尽量采用这种优化手段以提高效率。点拨 Android设备在第一次启动程序时往往耗费较长的时间,实际上,在这期间虚拟机对目标Dex文件进行了验证与优化,并为之生成了相应的Odex文件。当用户再次启动应用程序时,新生成的Odex文件将会代替原有的Dex的文件被虚拟机引用执行,由于不需要再次对程序数据进行验证优化,极大地缩短了启动时间。

1.3.2 Odex文件结构分析

直观上Odex文件在Dex文件的原有结构上进行了扩充,即在Dex文件前拼接了Odex文件头部信息,还在Dex文件尾部拼接了依赖库、寄存器映射关系以及类的哈希索引等辅助信息。其结构对比如图1.4所示。图1.4 Dex文件与Odex文件结构对比图

通过Odex文件的头部信息可以更好地了解一下Odex的文件结构以及各部分数据含义,表1.1为Odex文件头DexOptHeader在Dexfile.h文件中的定义。

在表1.1中,DexOptHeader结构中的magic字段与DexHeader结构中的magic字段类似,都是用于标识文件;dexOffset字段表示原Dex文件起始位置的偏移量,实际上它就等于DexOptHeader结构体的大小0x28;dexLength字段表示Dex文件的总长度,通过这两个字段可以非常快速定位并读取Dex文件;depsOffset字段表示依赖库起始的偏移量;depsLength表示依赖库的总长度;optOffset字段表示优化数据的起始偏移量;optLength字段表示优化信息的总长度,而对于类加载机制非常关键的类索引信息就封装在这部分优化信息中;flags字段为一个标识,其用于标示Dalvik虚拟机加载Odex文件时优化与验证选项;checksum字段为Odex文件的校验和。表1.1 DexOptHeader数据结构定义变量类型变量名称描 述magic[8]u1Odex文件版本标识u4dexOffsetDex文件头偏移量dexLengthu4Dex文件总长度depsOffsetu4Odex文件依赖库列表偏移量depsLengthu4依赖库信息总长度optOffsetu4优化数据信息偏移量optLengthu4优化数据总长度flagsu4标识位u4checksum文件校验和

通过这个头部信息,虚拟机可以非常高效地查找Dex文件中的各类信息,极大提高了执行效率,另外,Dalvik虚拟机对Dex文件所进行的优化工作主要体现在依赖库和辅助信息两部分上,因此,下文将会对这两部分内容的功能进行介绍。1.依赖库信息

依赖库顾名思义,就是指该Dex文件所需要链接的本地函数库,Dalvik虚拟机在程序执行前期通过优化机制将这部分整合到Odex文件中,可以在一定程度上提高程序的执行效率,表1.2为依赖库Dependence结构体定义。表1.2 Dependence数据结构定义变量类型变量名称描 述u4modWhen时间戳crcu4校验信息DALVIK_VM_BUIu4虚拟机版本号LDnumDepsu4依赖库个数u4lenName长度name[len]u4依赖库名称KSHA1DigestLensignatureSHA-1值

在表1.2中,modWhen用来记录Dex文件优化前的时间戳,crc为Dex文件优化前的crc校验值,DALVIK_VM_BUILD值表示的是虚拟机版本号;不同版本的Android系统定义不同,例如,Android 2.2.3为19、Android 2.3为23以及Android 4.0.4则为27。numDeps字段所代表的含义为该Dex文件的依赖库个数,其中table结构体的个数正是由numDeps决定的,也可以理解为每个依赖库都对应一个table结构体对象,在该结构体中,len表示依赖库名称的长度、name为依赖库名以及signature表示SHA-1签名。2.类索引信息

类索引信息的建立是优化机制的重要工作之一,在该索引表中,优化机制为Dex文件中的每一个类配置了一个table结构体对象,在这个对象中记录了类描述符哈希值、类描述符在Dex文件中偏移地址以及类定义区的偏移地址,类加载机制通过这些信息可以非常快速地定位类资源地址并加载类。同时,通过哈希查找的方式极大地提高了类加载机制的查找效率,表1.3为DexClassLookup结构体定义。表1.3 DexClassLookup数据结构定义变量类型变量名称描 述intsize表大小iIntnumEntries表项入口数量classDescriptorHau4类描述符的哈希值shclassDescriptorOffDex文件中该类描述符的偏移位u4set置u4classDefOffsetDex文件中该类定义偏移位置

在表1.3中,numEntries是一个比较特别的数目,它虽然表示的是表的项数,但实际上这个数值是通过dexRoundUpPower2( )函数生成。点拨 dexRoundUpPower2( )函数是源自斯坦福大学的一个算法——用于求比一个数大的最小的2的整数次幂,例如:当数为6时,该算法计算得到8。这样做的结果会比Dex文件中类的数量大,但好处是降低了哈希冲突率。

1.3.3 函数执行流程

Dex文件的优化始于Android源码中frameworks层的PackageManagerService类,该类实际上是通过Installer类实现对apk文件的安装、优化以及卸载等工作。而Installer类通过与c层的installd建立socket连接,使得在上层的install、remove、 dexopt等功能最终由installd在底层实现。在installd中,do_dexopt函数负责完成对Dex文件的优化,而do_dexopt函数将会调用dexopt函数去完成实际的优化工作。在dexopt函数中,首先完成一些必要的准备工作,比如声明关键变量,分析文件路径,而最关键的是创建了一个空文件并以Odex后缀结尾,由此,可以认定dexopt函数用于产生Odex文件,在完成准备工作后,将会委托run_dexopt函数完成实际的优化工作。

在run_dexopt函数中,可以看到这样一行代码: execl(DEX_OPT_BIN,DEX_OPT_BIN,"-zip",zip_num,Odex_num,input_file_name,dexopt_flags,(char*) NULL);

run_dexopt函数通过使用Execl函数将优化工作委托给了由第一参数DEX_OPT_BIN宏定义所指出的可运行程序,DEX_OPT_BIN宏定义的内容是: static const char* DEX_OPT_BIN="/system/bin/dexopt";

而/system/bin/dexopt中的dexopt程序的源码位于Android系统源码的dalvik\dexopt目录下,从目录结构上即可反映出dexopt优化程序是Dalvik虚拟机下第一级子程序,也正说明优化机制在Dalvik虚拟机中确实为一个相对独立的功能模块。

在清楚了优化机制的源头后,dexopt优化程序的工作流程成为我们比较关注的一个问题,因此,本文将在此着重分析优化机制的具体实现过程并介绍优化机制中关键的技术点以及有代表性的实现细节。

dexopt的主程序代码位于dalvik\dexopt\OptMain.cpp文件中,其中extractAndProcessZip( )函数用于处理并优化apk/jar/zip文件中的classes.dex,因此该函数将作为优化机制的主控函数,extractAndProcessZip( )函数的实现代码如下。

代码清单1.1 dalvik\dexopt\OptMain.cpp:extractAndProcessZip( )函数源代码 static int extractAndProcessZip (int zipFd,int cacheFd,const char* debugFileName, bool isBootstrap,const char* bootClassPath,const char* dexoptFlagStr) { /*函数在执行初期声明相关的中间变量*/ ZipArchive zippy; //用于描述ZIP压缩文件的数据结构 ZipEntry zipEntry; //用于表示一个ZIP入口 ... off_t dexOffset; //用于表示在Odex文件中,原Dex文件的起始地址 int err; //标示符 int result=-1; //函数返回值 int dexoptFlags=0; //优化标示符 /*设置默认的优化模式*/ DexClassVerifyMode verifyMode=VERIFY_MODE_ALL; DexOptimizerMode dexOptMode=OPTIMIZE_MODE_VERIFIED; memset(&zippy,0,sizeof(zippy)); //对zippy对象进行置0操作 /*对入口参数cacheFd文件描述符所代表的输入文件进行为空判断,该文件必须保证为空,因 为在后期要将优化后的数据写入该文件中*/ if (lseek(cacheFd,0,SEEK_END) !=0) { LOGE("DexOptZ:new cache file '%s' is not empty",debugFileName); goto bail; } /*当cacheFd所指文件为空,那么为其创建一个Odex文件的头部*/ err=dexOptCreateEmptyHeader(cacheFd); if (err !=0) //对函数执行结果进行判断,如果失败则将返回 goto bail; /*取得Odex文件中原Dex文件的起始位置,实际就是一个Odex文件头部的长度,并将结果赋值 给变量dexOffset*/ dexOffset=lseek(cacheFd,0,SEEK_CUR); if (dexOffset<0) goto bail; /*打开ZIP对象,在其中查找目标Dex文件*/ if (dexZipPrepArchive(zipFd,debugFileName,&zippy) !=0) { LOGW("DexOptZ:unable to open zip archive '%s'",debugFileName); goto bail; } /*获取目标Dex文件的解压入口*/ zipEntry=dexZipFindEntry(&zippy,kClassesDex); if (zipEntry==NULL) { LOGW("DexOptZ:zip archive '%s' does not include %s", debugFileName,kClassesDex); goto bail; } /*获取相关ZIP入口信息*/ if (dexZipGetEntryInfo(&zippy,zipEntry,NULL,&uncompLen,NULL, NULL,&modWhen,&crc32) !=0) { LOGW("DexOptZ:zip archive GetEntryInfo failed on %s", debugFileName); goto bail; } ┇ /*从ZIP文件将目标Dex文件解压出来,并写入cacheFd所指文件,此时cacheFd所指文件 非空,包括一个Odex文件头部加上一个原始的Dex文件*/ if (dexZipExtractEntryToFile(&zippy,zipEntry,cacheFd) !=0) { LOGW("DexOptZ:extraction of %s from %s failed", kClassesDex,debugFileName); goto bail; } /*根据入口参数dexoptFlagStr,对验证优化需求进行分析,dexoptFlagStr 实际上是一个字符串,记录了验证优化的要求*/ if (dexoptFlagStr\[0\]!='\0') { const char* opc; const char* val; /*设置验证模式*/ opc=strstr(dexoptFlagStr,"v="); /* verification */ if (opc !=NULL) { switch (*(opc+2)) { case 'n': verifyMode=VERIFY_MODE_NONE; break; case 'r': verifyMode=VERIFY_MODE_REMOTE; break; case 'a': verifyMode=VERIFY_MODE_ALL; break; default: break; } } /*设置优化模式*/ opc=strstr(dexoptFlagStr,"o="); /*optimization*/ if (opc !=NULL) { switch (*(opc+2)) { case 'n': dexOptMode=OPTIMIZE_MODE_NONE; break; case 'v': dexOptMode=OPTIMIZE_MODE_VERIFIED; break; case 'a': dexOptMode=OPTIMIZE_MODE_ALL; break; case 'f': dexOptMode=OPTIMIZE_MODE_FULL; break; default: break; } } ┇ } /*当完成了原Dex文件的提取以及验证优化选项的设置,即可以开始真正的优化工作,需要初始 化一个虚拟机专门用于验证优化工作*/ if (dvmPrepForDexOpt(bootClassPath,dexOptMode,verifyMode, dexoptFlags) !=0) { LOGE("DexOptZ:VM init failed"); goto bail; } /*调用dvmContinueOptimization函数完成对Dex文件的验证与优化工作*/ if (!dvmContinueOptimization(cacheFd,dexOffset,uncompLen, debugFileName,modWhen,crc32,isBootstrap)) { LOGE("Optimization failed"); goto bail; } result=0; //设置返回值,0表示成功 ┇ return result; //函数返回}

从上面的源码中可以看到,extractAndProcessZip( )函数首先会调用dexOptCreate-EmptyHeader( )函数为Odex文件创建一个文件头用于描述Odex文件内容,随后主函数将调用dexZipFindEntry( )函数检查目标文件中是否拥有classes.dex文件,如果目标Dex文件存在,则通过dexZipGetEntryInfo( )函数读取Dex文件相关的验证信息,接着调用dexZipExtractEntryToFile( )函数提取classes.dex文件并写入Odex文件,在写入完毕后,主程序将会根据入口参数dexoptFlagStr解析检验与优化模式并将优化选项dexOptMode与验证verifyMode写入到全局变量中。至此,优化机制的准备工作基本结束。

在准备工作完成后,主函数调用dvmPrepForDexOpt( )启动并初始化一个虚拟机进程,而以后的所有优化操作都会在这个进程中完成,当Odex文件正常输出后,这个进程的所有资源都会被释放。当优化的进程准备完毕后,主函数将调用dvmContinueOptimization( )函数开始真正的验证与优化工作。点拨 启用一个专门的进程用于负责Dex文件的优化与验证,这样做的好处是在一定程度上保证了虚拟机的安全运行,因为一个未经安全验证的程序,不能保证对其他虚拟机进程绝对安全。例如,恶意篡改共享数据、造成内存溢出等。

代码清单1.2 dalvik\\vm\\analysis\\DexPrepare.cpp :dvmContinueOptimization( )函数源代码 bool dvmContinueOptimization(int fd,off_t dexOffset,long dexLength, const char* fileName,u4 modWhen,u4 crc,bool isBootstrap) { /*声明相关中间变量*/ DexClassLookup* pClassLookup=NULL; RegisterMapBuilder* pRegMapBuilder=NULL; assert(gDvm.optimizing); LOGV("Continuing optimization (%s,isb=%d)",fileName,isBootstrap); assert(dexOffset >=0); //判断输入文件长度非0 /*对目标文件进行合法性检验*/ if (dexLength<(int) sizeof(DexHeader)) { /*一个Dex文件的长度不能小于其文件头的长度*/ LOGE("too small to be DEX"); return false; } /*Odex文件中的Dex文件的起始偏移量不能小于Odex文件头的长度*/ if (dexOffset<(int) sizeof(DexOptHeader)) { LOGE("not enough room for opt header"); return false; } ┇ /*将fd所指文件映射到某一位置,该位置的起始地址为mapAddr,其大小就为fd所指文 件大小,即一个Odex文件头部加上一个Dex文件长度*/ mapAddr=mmap(NULL,dexOffset+dexLength, PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if (mapAddr==MAP_FAILED) { LOGE("unable to mmap DEX cache:%s",strerror(errno)); goto bail; } /*设置相关的优化验证选项*/ bool doVerify,doOpt; if (gDvm.classVerifyMode==VERIFY_MODE_NONE) { doVerify=false; } else if (gDvm.classVerifyMode==VERIFY_MODE_REMOTE) { doVerify=!gDvm.optimizingBootstrapClass; } else /* if (gDvm.classVerifyMode==VERIFY_MODE_ALL)*/ { doVerify=true; } if (gDvm.dexOptMode==OPTIMIZE_MODE_NONE) { doOpt=false; } else if (gDvm.dexOptMode==OPTIMIZE_MODE_VERIFIED || gDvm.dexOptMode==OPTIMIZE_MODE_FULL) { doOpt=doVerify; } else /* if (gDvm.dexOptMode==OPTIMIZE_MODE_ALL) */ { doOpt=true; } /*调用rewriteDex函数对目标文件进行优化验证,其主要内容包括:字符顺序调整、字节码 替换、字节码验证以及文件结构重新对齐*/ success=rewriteDex(((u1*) mapAddr)+dexOffset,dexLength, doVerify,doOpt,&pClassLookup,NULL); ... /*对当前文件进行8字节对齐,并准备写入*/ off_t depsOffset,optOffset,endOffset,adjOffset; int depsLength,optLength; u4 optChecksum; depsOffset=lseek(fd,0,SEEK_END); //取得fd所指文件的总长 if (depsOffset<0) { LOGE("lseek to EOF failed:%s",strerror(errno)); goto bail; } /*根据fd所指文件总长使depsOffset(dependency list的起始地址)为8的倍数 (adjOffset应该大于等于depsOffset)*/ adjOffset=(depsOffset+7) & ~(0x07); if (adjOffset !=depsOffset) { LOGV("Adjusting deps start from %d to %d", (int) depsOffset,(int) adjOffset); depsOffset=adjOffset; lseek(fd,depsOffset,SEEK_SET); } /*写入依赖库信息*/ if (writeDependencies(fd,modWhen,crc) !=0) { LOGW("Failed writing dependencies"); goto bail; } ┇ /*写入其他优化信息,包括类索引信息以及寄存器映射关系*/ if (!writeOptData(fd,pClassLookup,pRegMapBuilder)) { LOGW("Failed writing opt data"); goto bail; } ┇ /*对Odex文件的头部内容进行修正*/ DexOptHeader optHdr; memset(&optHdr,0xff,sizeof(optHdr)); memcpy(optHdr.magic,DEX_OPT_MAGIC,4); memcpy(optHdr.magic+4,DEX_OPT_MAGIC_VERS,4); optHdr.dexOffset=(u4) dexOffset; optHdr.dexLength=(u4) dexLength; optHdr.depsOffset=(u4) depsOffset; optHdr.depsLength=(u4) depsLength; optHdr.optOffset=(u4) optOffset; optHdr.optLength=(u4) optLength; ┇ return result; }

dvmContinueOptimization( )函数首先对目标文件进行格式检验,并调用mmap( )函数将原Dex文件整体映射至内存,再调用rewriteDex( )函数并根据全局变量中的dexOptMode与classVerifyMode字段所规定的优化要求来重写映射文件,这里的重写内容包括字符顺序调整、字节码替换、字节码验证以及文件结构重新对齐等工作。当rewriteDex( )函数正常返回后,dvmContinueOptimization( )函数将会依次调用writeDependencies( )函数建立依赖库和writeOptData( )函数写入辅助数据。当以上几个步骤结束后,Odex文件的各个关键内容都已经具备,程序的最后将根据现实情况写入Odex文件头部信息并保存。

上面两个函数是位于机制函数调用链的上层,通过对源码的展示分析,相信读者已经对机制的关键的执行流程有了一定的认识。由于篇幅所限,再加上优化机制源码量较为巨大,本书在此就不再对源码进行更加深入的介绍展示了,希望读者能够结合下载的源码对文中出现的各个关键的功能点函数进行学习,将会非常有助于理解机制中一些关键技术点的具体实现细节。点拨 对于一个Dex文件来说,如果在cache中存在一个与之对应的Odex文件,虚拟机在接收到运行指令时,会直接引用执行相对应的Odex文件,这样就避免了二次验证与优化,减少了程序启动时间。

1.4 Dex文件的解析

Dalvik虚拟机与标准Java虚拟机最为直观的不同在于其输入文件格式,Dalvik虚拟机采用由多个Class文件整合而成的Dex文件,其结构设计与意义已在第1卷第3章中进行了介绍,此处不再赘述。由于多个类被整合到一个Dex文件中,且在Dex文件中类与类之间不但没有明确的界限,甚至还有共享数据的情况,因此,这就要求类加载机制在实际加载一个目标类之前要对Dex文件进行一系列预处理,使Dex文件对于虚拟机来说是可读的。简单来说,Dex文件解析的主要目的是对Dex文件进行读取分析并通过建立一个DexFile结构体的实例对象专门用于描述该Dex文件,使实际的类加载函数可以通过该数据结构对目标类全部的数据进行索引并提取以完成类的实际加载工作。

1.4.1 DexFile数据结构简析

为目标Dex文件生成一个DexFile数据结构实例对象是Dex文件解析工作的重要目标,在第1卷第3章中,已经对Dex文件的结构进行了详细的介绍,想必读者读到此处时仍还能回忆起些许内容。下面在介绍解析工作的原理和实现之前,先将对DexFile数据结构进行一个简要的介绍,如果读者对该结构体中各个成员变量以及其数据结构感到陌生,建议重新回顾一下第1卷第3章的内容。

DexFile结构体的定义如表1.4所示,从中可以发现该数据结构的成员变量所代表的内容以及其排列顺序与经过优化的Dex文件的结构非常相似,Dex文件中的各个数据部分似乎都可以和DexFile结构体中的各个成员变量形成相对应关系。表1.4 struct DexFile结构体成员变量成员变量类型变量名意义const pOptHeader优化数据头部*DexOptHeader*pHeaderDex文件头部const DexHeader*pStringIds指向字符串索引区const DexStringId*pTypeIds指向类型索引区const DexTypeId*pFieldIds指向字段索引区const DexFieldIdconst pMethodIds指向方法索引区DexMethodId*pProtoIds指向原型索引区const DexProtoIdconst pClassDefs指向类定义区*DexClassDef*pLinkData指向连接数据区const DexLinkconst pClassLookup指向类索引*DexClassLookup*baseAddr基地址const u1

写到这里读者就应该知道,虚拟机正是通过将目标Dex文件与一个DexFile数据结构进行关联,使得在虚拟机内部,负责类的实际加载的功能函数可以通过该数据结构实现对Dex文件中各类数据的查找获取工作。也正是利用这种方式,虚拟机实现了对一个存储在内存中不可读的数据块——Dex文件进行解析的目的。

1.4.2 Dex文件解析流程概述

当虚拟机获得了程序中的Classes.dex文件后,将首先对这个Dex文件进行初步的验证工作,主要包括:验证Magic数、校验SHA-1签名、计算Dex校验和等几个方面,其目的主要是检验该Dex文件是否符合Android系统规范。当校验的工作完毕后,将对Dex文件进行优化和验证并输出优化的产物——Odex文件,Dex优化与验证在1.3节已经进行了详细的分析和介绍。在接下来的工作中,Odex文件将取代原Dex文件被加载机制进行解析。

当优化的工作结束后,虚拟机将会开始对优化过的Dex文件进行解析,首先需要对优化后的Odex文件进行完整性检验,以确保该Odex文件是完整并且合乎Android系统规范。随后,虚拟机将对目标Odex文件中的优化数据进行解析,其主要目的是将在对Dex文件优化期间生成的优化数据,例如:类索引信息、寄存器映射关系等,提前与DexFile数据结构中的个别成员变量进行关联。完成对优化数据的处理之后,虚拟机将对原Dex数据进行解析,其做法仍然是将DexFile数据结构中其他各个成员变量与Dex文件的各个数据部分相关联,使虚拟机能够更加高效地对Dex文件中的类数据进行查找并获取,其关联效果如图1.5所示。图1.5 Dex文件与DexFile数据结构映射关系图

当解析函数正确输出DexFile数据结构的一个实例对象之后,虚拟机将再次对Dex文件进行校验并计算SHA-1值,最后保存相关设定并将该实例对象返回。至此,Dex文件的解析工作结束,其大致的工作流程如图1.6所示。图1.6 Dex文件解析工作流程示意图

在1.4.3节中,将从源码实现的角度对Dex文件解析这部分工作进行一个详细的分析,同时也希望读者能够结合这部分源码进行学习,以加深对这部分内容的理解。点拨 DexFile结构体是用于描述内存中的Dex文件,虚拟机可以通过该数据结构快速访问Dex文件中各部分的数据。然而,这一切的功能实现是需要建立在一个严格的格式体系之中。由此可见,系统架构的设计工作是非常重要的。

1.4.3 函数执行流程

Dex文件解析的工作主要由位于虚拟机源码目录dalvik/vm/RawDexFile.cpp中的dvmRawDexFileOpen函数完成,在这部分工作中称dvmRawDexFileOpen函数为主控函数,因为在它的调控下,各个功能点函数共同配合完成了对Dex文件进行解析的工作。dvmRawDexFileOpen函数源码如下。

代码清单1.3 dalvik\vm\RawDexFile.cpp:dvmRawDexFileOpen( )函数源代码 int dvmRawDexFileOpen(const char* fileName,const char* OdexOutputName, RawDexFile** ppRawDexFile,bool isBootstrap) { /*声明函数中间的执行变量*/ DvmDex* pDvmDex=NULL; //用于在虚拟机中描述解析的Dex文件 char* cachedName=NULL; //用于保存执行期间产生的优化Dex文件名 int result=-1; //设置函数返回值,0表示成功 int dexFd=-1; //初始化目标Dex文件的文件描述符 int optFd=-1; //初始化优化Dex文件的文件描述符 u4 modTime=0; //初始化文件修改时间参数 u4 adler32=0; //初始化校验和变量 size_t fileSize=0; //标示文件大小 bool newFile=false; //标示虚拟机是否需要对Dex文件进行优化 bool locked=false; //用于标示优化进程占用 /* fileName是dvmRawDexFileOpen函数最关键的入口参数,它是一个字符串,记录了目标 Dex文件在文件系统中的绝对路径,主函数通过open函数根据fileName参数将目标文件进行 读入至虚拟机中*/ dexFd=open(fileName,O_RDONLY); if (dexFd<0) goto bail; ┇ dvmSetCloseOnExec(dexFd); /*对Dex文件的合法性与正确性进行检验*/ if (verifyMagicAndGetAdler32(dexFd,&adler32)<0) { LOGE("Error with header for %s",fileName); goto bail; } /* 记录文件修改时间并赋值给modTime参数*/ if (getModTimeAndSize(dexFd,&modTime,&fileSize)<0) { LOGE("Error with stat for %s",fileName); goto bail; } /*根据目标Dex文件名为其产生相应的优化文件名并赋值给cachedName*/ if (OdexOutputName==NULL) { cachedName=dexOptGenerateCacheFileName(fileName,NULL); if (cachedName==NULL) goto bail; } else { cachedName=strdup(OdexOutputName); } LOGV("dvmRawDexFileOpen:Checking cache for %s (%s)", fileName,cachedName); /*尝试根据cachedName所指的优化文件名在cache中查找并读取优化文件,如果读取失败或 是当前的优化文件有误,则将要重新对Dex文件进行优化*/ optFd=dvmOpenCachedDexFile(fileName,cachedName,modTime, adler32,isBootstrap,&newFile,/*createIfMissing=*/true); if (optFd<0) { LOGI("Unable to open or create cache for %s (%s)", fileName,cachedName); goto bail; } locked=true;/*在前面提到,如果Odex文件打开失败,则虚拟机将把newFile参数置为真。 在此处虚拟机将根据newFile的值以决定是否需要对Dex文件进行优化*/ if (newFile) { u8 startWhen,copyWhen,endWhen; bool result; off_t dexOffset; dexOffset=lseek(optFd,0,SEEK_CUR); result=(dexOffset >0); if (result) { startWhen=dvmGetRelativeTimeUsec(); /*将dexFd所指文件复制至optFd所指文件中*/ result=copyFileToFile(optFd,dexFd,fileSize)==0; copyWhen=dvmGetRelativeTimeUsec(); } /*调用dvmOptimizeDexFile函数对optFd所指文件进行优化*/ if (result) { result=dvmOptimizeDexFile(optFd,dexOffset,fileSize, fileName,modTime,adler32,isBootstrap); } ┇ } /*当Dex文件的优化结束后,将会调用dvmDexFileOpenFromFd函数对该Dex文件进行解析*/ if (dvmDexFileOpenFromFd(optFd,&pDvmDex) !=0) { LOGI("Unable to map cached %s",fileName); goto bail; } ┇ LOGV("Successfully opened '%s'",fileName); ┇ /*对入口参数ppRawDexFile进行设置,ppRawDexFile变量是一个RawDexFile*类型的指 针,其作用是用于保存当前处理的Dex文件的相关信息*/ *ppRawDexFile=(RawDexFile*) calloc(1,sizeof(RawDexFile)); /*保存优化文件名*/ (*ppRawDexFile)->cacheFileName=cachedName; /*保存DvmDex数据结构*/ (*ppRawDexFile)->pDvmDex=pDvmDex; cachedName=NULL; //释放cachedName变量 result=0; //设置返回值,0表示成功 return result; }

dvmRawDexFileOpen函数首先通过调用verifyMagicAndGetAdler32函数完成对Dex文件的magic字段以及校验信息的验证工作,如果该函数正确返回,证明该Dex文件为一个合法文件。点拨 魔数(magic)是为了方便虚拟机识别目标文件是否为合格的Dex文件,在Dex文件中,magic为dex/n035/0;而在标准的Java Class文件中,其magic为CAFEBABE。

随后主函数通过调用dexOptGenerateCacheFileName函数生成该Dex文件的优化文件名,紧接着调用dvmOpenCachedDexFile函数,根据这个Dex文件的优化文件名在dalvik_cache中查找是否存在该Dex文件的优化文件(虚拟机在第一次执行一个Dex文件时会对这个Dex文件进行优化并生成Odex文件,置放于cache中,当再次运行该Dex文件时,将跳过优化的步骤,直接找到Odex文件并运行之),如果不存在,主函数在设置相关的优化参数后,将调用dvmOptimizeDexFile函数完成对Dex文件的优化工作并在cache中生成Odex文件。至此,主函数完成了对Dex文件的检验与优化工作。这部分的工作重点是对优化的Dex文件进行解析,那么该解析工作是由哪个函数具体承担呢?

dvmRawDexFileOpen函数在完成对目标Dex文件的优化后,将会调用dvmDexFileOpenFromFd函数完成Dex文件的后续解析工作,其源码如下。

代码清单1.4 dalvik\\vm\\DvmDex.cpp:dvmDexFileOpenFromFd( )函数源代码** int dvmDexFileOpenFromFd(int fd,DvmDex ppDvmDex) { /*声明函数执行过程中所用到的中间变量*/ DvmDex* pDvmDex; DexFile* pDexFile; MemMapping memMap; int parseFlags=kDexParseDefault; int result=-1; /*验证Dex文件校验和*/ if (gDvm.verifyDexChecksum) parseFlags |=kDexParseVerifyChecksum; if (lseek(fd,0,SEEK_SET)<0) { LOGE("lseek rewind failed"); goto bail; } /*对目标Dex文件进行映射,并将其设置为只读文件*/ if (sysMapFileInShmemWritableReadOnly(fd,&memMap) !=0) { LOGE("Unable to map file"); goto bail; } /*这一步是Dex文件解析工作的关键,dvmDexFileOpenFromFd函数通过调用dexFileParse 函数对Dex文件进行解析,并返回一个DexFile数据结构的实例对象*/ pDexFile=dexFileParse((u1*)memMap.addr,memMap.length, parseFlags); if (pDexFile==NULL) { LOGE("DEX parse failed"); sysReleaseShmem(&memMap); goto bail; } /*主函数将通过allocateAuxStructures函数并根据pDexFile变量作为参数,对DvmDex数 据结构的一些成员变量进行了设置*/ pDvmDex=allocateAuxStructures(pDexFile); if (pDvmDex==NULL) { dexFileFree(pDexFile); sysReleaseShmem(&memMap); goto bail; } sysCopyMap(&pDvmDex->memMap,&memMap); pDvmDex->isMappedReadOnly=true; *ppDvmDex=pDvmDex; result=0; //设置返回值,0表示成功 return result; }

从上面的源码中可以知道,dvmDexFileOpenFromFd函数首先对已经经过优化的Dex文件进行相关的正确性检验后,随即将调用dexFileParse函数将目标Dex文件进行解析,其目标就是将该Dex文件与一个DexFile结构体对象建立关联,那么dexFileParse函数究竟是如何建立该联系的呢?首先观察一下dexFileParse函数的源码。

代码清单1.5 dalvik\libdex\DexFile.cpp:dexFileParse( )函数源代码 DexFile* dexFileParse(const u1* data,size_t length,int flags) { /*声明一个DexFile数据结构的指针变量pDexFile 用于保存解析结果*/ DexFile* pDexFile=NULL; const DexHeader* pHeader; //用于保存Dex文件的头部信息 const u1* magic; //用于保存Dex文件的魔数信息 int result=-1; //初始化返回值 /*对Dex文件大小进行判断,如果其文件长度小于其文件头的长度,则该Dex文件必然是错误 的*/ if (length<sizeof(DexHeader)) { LOGE("too short to be a valid .dex");

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载