- APP安装
对于一个Android的apk应用程序,其主要的执行代码都在其中的class.dex文件中。在程序第一次被加载的时候,为了提高以后的启动速度和执行效率,Android系统会对这个class.dex文件做一定程度的优化,
并生成一个ODEX文件,存放在/data/dalvik-cache目录下。以后再运行这个程序的时候,就只要直接加载这个优化过的ODEX文件就行了,省去了每次都要优化的时间。
说明优化代码的第一部分只执行一次
PackagemanagerService中
mInstaller.install(pkgName, useEncryptedFSDir, pkg.applicationInfo.uid,
pkg.applicationInfo.uid);
ret = mInstaller.dexopt(path, pkg.applicationInfo.uid,
!isForwardLocked(pkg));
mInstaller.dexopt 通过socket通信 让installd 进程(由init进程起来了)执行do_dexopt-->dexopt-->fork出子进程去执行run_dexopt,安装和优化的
dexopt(const char *apk_path, uid_t uid, int is_public) (在/frameworks/native/cmds/installd/commands.c中 )
if (create_cache_path(out_path, apk_path)) //这句的apk_path为apk的路径 即data/app/xxx.apk 表明此时已经在
sprintf(path,"%s%s%s",DALVIK_CACHE_PREFIX,src + 1, DALVIK_CACHE_POSTFIX); //其中installd.h #define DALVIK_CACHE_PREFIX "/data/dalvik-cache/"
// installd.h #define DALVIK_CACHE_POSTFIX "/classes.dex"
out_fd = open(out_path, O_RDWR | O_CREAT | O_EXCL, 0644); 自此说明/data/dalvik-cache/目录下已经生成了data@app@*.apk@classes.dex文件,之后的文件优化验证都是对这个文件进行读写操作
tatic void run_dexopt(int zip_fd, int odex_fd, const char* input_file_name, const char* dexopt_flags)
static const char* Dex_OPT_BIN = "/system/bin/dexopt"
DEX文件优化与验证:(完成了Odex文件头的构造)
run_dexopt:
static const char* Dex_OPT_BIN = “/system/bin/dexopt” 由run_dexopt执行/system/bin/dexopt进入dalvik的Optmain.cpp
\dexopt\Optmain.cpp:extractAndProcessZip() //从apk包zip文件中读取和抽出dex,加上odex文件头,设置优化选项,可以看作DEX文件优化的主控函数
err = dexOptCreateEmptyHeader(cacheFd);//创建 odex文件头,然后写入cacheFd指向的data@app@*.apk@classes.dex文件,并且将文件位置定位到真正的class.dex要填充的位置
write(fd, &optHdr, sizeof(optHdr))
if (dexZipExtractEntryToFile(&zippy, zipEntry, cacheFd) != 0) { //将zip中的class.dex内容抽取到cacheFd指向文件的当前当前偏移处 dexOffset开始的位置
if (sysCopyFileToFile(fd, pArchive->mFd, uncompLen) != 0)
if (sysWriteFully(outFd, buf, getSize, "sysCopyFileToFile") != 0)
ssize_t actual = TEMP_FAILURE_RETRY(write(fd, buf, count));
\vm\analysis\DexPrepare.cpp:dvmContinueOptimization()//生成odex文件,可以说优化与验证工作的完成就是生成odex文件
mapAddr = mmap(NULL, dexOffset + dexLength, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); 将之前已经写入odex文件头(未设置)和zip中的class.dex内容的整个文件映射到内存继续优化验证
rewriteDex(((u1*) mapAddr) + dexOffset, dexLength, doVerify, doOpt, &pClassLookup, NULL); 只调整从odex头部偏移dexOffset处的原始dex字节序等
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) { //创建一个Dexfile结构,此处是真正的dex文件
dexFileParse 来具体解析Dex文件
allocateAuxStructures(pDexFile); //设置Dexfile的辅助数据字段
if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) { //再次调用该函数来验证odex文件 , Returns nonzero on error. 返回值非0即失败
dvmGenerateRegisterMaps(pDvmDex); //填充辅助数据区结构
updateChecksum(dexAddr, dexLength, pHeader); //重写优化后dex的checksum
writeDependencies(fd, modWhen, crc) != 0) //写入依赖库信息
writeOptData(fd, pClassLookup, pRegMapBuilder) //写入其他优化信息,包括类索引信息以及寄存器映射关系。
DexOptHeader optHdr; //填充odex头部
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;
至此生成odex文件,其实就填充了/data/dalvik-cache/xxx@classes.dex文件的内容
点击APP icon图标,从Launcher Activity所在进程切入AMS
startProcessLocked 函数,该函数最终又是通过调用 Process.start 方法请求 Zygote 进程 fork 目标进程的: app的新进程fork完毕
(I) app的新进程–》 handlerChildProc 加载目标apk所有类
(1.1) 加载好dex文件到内存
指定并初始化apk的加载器是 PathClassLoader ,加载好dex文件到内存 ClassLoader cloader; if (parsedArgs.classpath != null) { cloader = new PathClassLoader(parsedArgs.classpath, ClassLoader.getSystemClassLoader()); //ClassLoader.getSystemClassLoader()系统类加载器 PathClassLoader[DexPathList [[directory "."],nativeLibraryDirectories=[/vendor/lib,/system/lib]]] PathClassLoader基类是 BaseDexClassLoader BaseDexClassLoader 的构造函数如下: public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) { ……….. this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); ……….. } 前面是一些对于传入参数的验证,然后调用了makeDexElements。 private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) { ArrayList<Element> elements = new ArrayList<Element>(); for (File file : files) { File zip = null; DexFile dex = null; String name = file.getName(); if (name.endsWith(DEX_SUFFIX)) { //dex文件处理 // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException ex) { System.logE(“Unable to load dex file: ” + file, ex); } } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX) || name.endsWith(ZIP_SUFFIX)) { //apk,jar,zip文件处理 zip = file; try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } } else if (file.isDirectory()) { elements.add(new Element(file, true, null, null)); } else { System.logW(“Unknown file type for: ” + file); } if ((zip != null) || (dex != null)) { elements.add(new Element(file, false, zip, dex)); } } return elements.toArray(new Element[elements.size()]); } } 不管是dex文件,还是apk文件最终加载的都是loadDexFile,跟进这个函数: private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { if (optimizedDirectory == null) { return new DexFile(file); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0); } } 如果optimizedDirectory为null就会调用openDexFile(fileName, null, 0);加载文件。 否则调用DexFile.loadDex(file.getPath(), optimizedPath, 0); 而这个函数也只是直接调用new DexFile(sourcePathName, outputPathName, flags); 里面调用的也是openDexFile(sourceName, outputName, flags); 所以最后都是调用openDexFile,跟进这个函数-------------------------------------------(openDexFileNative native层hook就hook这里): private static int openDexFile(String sourceName, String outputName, int flags) throws IOException { return openDexFileNative(new File(sourceName).getCanonicalPath(), (outputName == null) ? null : new File(outputName).getCanonicalPath(), flags); //而这个函数调用的是so的Dalvik_dalvik_system_DexFile_openDexFileNative个函数。 *******打开成功则返回一个cookie(会传递给下面1.2步) *******。这个cookie基本上是已经脱过壳的(Zj) static void Dalvik_dalvik_system_DexFile_openDexFileNative(const u4* args, JValue* pResult) { …………… if (hasDexExtension(sourceName) && dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { ******* dvmRawDexFileOpen ******* 打开 dex 文件并进行优化与加载,如果已经有odex则直接加载,否则用/system/bin/dexopt先优化后加载 函数的最后会(*ppRawDexFile)->pDvmDex = pDvmDex; ,也就是下面 (1.2)的 pDvmDex来源, *******至此dex加载完成了 ******* dvmRawDexFileOpen 的作用就是给 DexOrJar 的成员 RawDexFile* pRawDexFile 赋值,赋值后返回这个 DexOrJar,在 java 层对应的就是一个 int 值 DexFile.mCookie RawDexFile.cpp:dvmRawDexFileOpen()//DEX文件解析的主控函数 \libdex\OptInvocation.cpp:dexOptGenerateCacheFileName()//构造一个dex cache name optFd = dvmOpenCachedDexFile(fileName, cachedName, modTime,adler32, isBootstrap, &newFile, /*createIfMissing=*/true); //用刚刚创建的cache文件名到对应的/data/dalvik-cache目录下找odex文件,不存在的话会newFile置位 if (newFile) { result = dvmOptimizeDexFile(optFd, dexOffset, fileSize, fileName, modTime, adler32, isBootstrap); //,调用/system/bin/dexopt 重新生成/system/bin/dexopt该dex对应的odex文件 \vm\DvmDex.cpp:dvmDexFileOpenFromFd()//如果newFile==false(/data/dalvik-cache目录下本来就有odex),或者已经在newFile代码块中在/data/dalvik-cache目录下创建好了odex文件,则直接调用mmap对DEX文件映射,设置为只读文件,并进一步优化 \libdex\DexFile.cpp:dexFileParse()//真正的解析ODEX, 最终初始化好了文件的结构体引用 在初始化文件结构体的引用时,虚拟机根据全局变量gDvm中的启动类路径来为每个类生成一个ClassPathEntry的结构体引用,处理的过程中为压缩文件/dex调用了不同的函数,处理dex文件时顺便对齐进行了优化,生成了odex文件。 ALOGV(“Opening DEX file ‘%s’ (DEX)”, sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; pDexOrJar->pDexMemory = NULL; } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { ALOGV(“Opening DEX file ‘%s’ (Jar)”, sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile; pDexOrJar->pDexMemory = NULL; } else { ALOGV(“Unable to open DEX file ‘%s’”, sourceName); dvmThrowIOException(“unable to open DEX file”); } …………… } }
Note:第0步就是为了生成一个好用的odex文件,第1.1步将odex结构放到内存中以DvmDex存在
在实践中,我们发现并不是所有的dvmDexFileOpenPartial都能断下来,该函数之前用在dex的优化过程中(创建Dexfile结构时),
但 _Z27dexOptGenerateCacheFileNamePKcS0
dvmdexfileopenfromfd3
dvmdexfileparse
这些函数一定能断下来,因为每次应用启动后都会执行dvmRawDexFileOpen。而dvmDexFileOpenPartial常常在有壳时(之所以能断到,是因为这些壳自己调用了dvmdvmDexFileOpenPartial进行优化,。)
可知,其实dalvik很喜欢走的路就是用odex,而不是dex。dex文件只有在odex文件不好用时才去用,这与oat文件一致。
(1.2) 加载APK 包中所有类(不包括代码中动态加载的dex包)
在 APK 的 ClassLoader 被指定后,APK 包中所有类(不包括代码中动态加载的dex包)都由该 ClassLoader 来加载,我们从 PathClassLoader 的
注意:DexClassLoader和PathClassLoader其实都是通过DexFile这个类来实现类加载的。DexFile在加载类时,具体是调用成员方法loadClass(DexClassLoader)或者loadClassBinaryName(PathClassLoader)。
//PathClassLoader在加载类时调用的是DexFile的loadClassBinaryName************************
而DexClassLoader调用的是loadClass。************************
DexFile.java
public Class loadClass(String name, ClassLoader loader)
{
String slashName = name.replace('.', '/'); //因此,在使用PathClassLoader时类全名需要用”/”替换”.”
return loadClassBinaryName(slashName, loader);
}
PathClassLoader loadClass 实际进入了 ClassLoader loadClass
ClassLoader loadClass // loadClass 方法看起,由于 PathClassLoader 并没有复写 loadClass,所以调用的仍是 ClassLoader 类的 loadClass 方法:
findClass //此时在PathClassLoader调用该方法实际是继承自父类BaseDexClassLoader findClass
DexPathList findClass
DexFile dex.loadClassBinaryName
defineClass
defineClassNative
dalvik_system_DexFile.cpp Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args, JValue* pResult)
int cookie = args[2];
struct DexOrJar {
******* //此处用到的cookie就是第(1.1)步中ClassLoader构造时创建的 ******* char* fileName;
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile;
u1* pDexMemory; // malloc()ed memory, if any
};
DexOrJar* pDexOrJar = (DexOrJar*) cookie;
if (pDexOrJar->isDex)
DvmDex* pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile);//RawDexFile* pRawDexFile struct RawDexFile {
return pRawDexFile->pDvmDex; //从cookie中拿到内存中已经加载好的dex char* cacheFileName;
DvmDex* pDvmDex;
};
Class.cpp:dvmDefineClass(pDvmDex, descriptor, loader)
Class.cpp:findClassNoInit(onst char* descriptor, Object* loader, DvmDex* pDvmDex)//在findClassNoInit 中执行寻找类、加载类的逻辑,但不会执行初始化类的逻辑
clazz = dvmLookupClass(descriptor, loader, true); //首先查找指定类加载器加载过的类,如果已经加载,则不会执行加载的逻辑
found = dvmHashTableLookup(gDvm.loadedClasses, hash, &crit,hashcmpClassByCrit, false);//从已经加载过的类(gDvm.loadedClasses)哈希表中查找该类是否存在
if (found && !unprepOkay && !dvmIsClassLinked((ClassObject*)found)) {//如果找到匹配了,判断该类是否已经链接,如果已经链接,就是已经被加载了,如果还没有链接,那就仍被认为没找到
dexFindClass(pDvmDex->pDexFile, descriptor) //如果通过 dvmLookupClass 发现该类没有加载,就会首先通过dexFindClass从加载进来的 Dex 文件中查找该类的定义
const DexClassLookup* pLookup = pDexFile->pClassLookup;
if (pLookup->table[idx].classDescriptorHash == hash) {//DexFile::pClassLookup 实际上是在加载 Dex 文件时解析的每个类存储的一个映射表,key 是通过类的说明descriptor计算的哈希,value 是存放解析的类的偏移吗,这一步是计算hash和表的下标;
if (strcmp(str, descriptor) == 0) {
return (const DexClassDef*) (pDexFile->baseAddr + pLookup->table[idx].classDefOffset);//找到了就返回该类定义 DexClassDef:
clazz = loadClassFromDex(pDvmDex, pClassDef, loader);
pEncodedData = dexGetClassData(pDexFile, pClassDef); //拿到 ClassData 的指针;
dexReadClassDataHeader(&pEncodedData, &header); //读取 ClassData 的头信息
//根据前两步拿到的信息loadClass
loadClassFromDex0(pDvmDex, pClassDef, &header, pEncodedData, classLoader);
newClass = (ClassObject*) dvmMalloc(size, ALLOC_NON_MOVING);//分配 ClassObject 对象(newClass)的内存;
//DVM_OBJECT_INIT(newClass, gDvm.classJavaLangClass); // 初始化 java.lang.Class 成员 初始化该类的 java.lang.Class 成员,每一个 java 对象在 native 层都会对应的 ClassObject 结构体其实都是继承于 Object:
dvmSetFieldObject((Object *)newClass,
OFFSETOF_MEMBER(ClassObject, classLoader),
(Object *)classLoader); // 初始化 ClassLoader
newClass->pDvmDex = pDvmDex;
newClass->primitiveType = PRIM_NOT; //初始化 ClassLoader、Dex对象、加载状态等,此时的状态为 CLASS_IDX,区别于 CLASS_LOADED,CLASS_IDX 状态时 ClassObject 中的成员都不是直接的指针/引用而是数字下标index;
newClass->status = CLASS_IDX; // 初始化类加载状态为 CLASS_IDX
newClass->super = (ClassObject*) pClassDef->superclassIdx; //父类(newClass->super)初始化;
pInterfacesList = dexGetInterfacesList(pDexFile, pClassDef);//接口(newClass->interfaces)初始化;
第六步:静态成员初始化;
第七步:实例成员初始化;
第八步:普通函数初始化;
第九步:虚函数初始化;
第3步才是odex文件自身结构的解析
往后便是FindClass、GetStaticMthodID、CallStaticMthod、dvmInterpretStd解释Dalvik字节码的宏指令
*************************************************************************************************************************************************************************************************************************************8
dalvikvm做的事情:
将dex从zip文件中拽出来;
dex验证优化 : 读取抽出dex加上odex头 并对dex字节码进行替换修改 ,写入odex辅助信息和依赖库;
dex文件解析: 利用之前的odex头生成odex文件于 /data/dalvik-cache/包名下;
3.解析释放odex到内存以Dvmdex结构存在
利用之前的cookie中拿到DvmDex 然后进行类加载—-》 找到类,找到方法,指令,最后执行
第二代脱壳、加固需要搞 pRawDexFile文件在 dalvik_system_DexFile.cpp中 老版360脱壳扣pDvmDex(内存中dex文件)
脱壳机就是对比较好的系统源码进行hook修改和dump dvmAddClassToHash(clazz)//将加载了的类添加进哈希表 gDvm.loadedClasses 中:以后可以不用加载了
(II) app的新进程–》运行目标APP的ActivityThread线程main方法,然后加载创建Application类