0x00
在上一篇文章apk安裝和優化原理,在最后我們分析了DexClassLoader和PathClassLoader的構造函數的不同。
PathClassLoader最后調用的是new DexFile(pathFile),而DexClassLoader調用的是DexFile.loadDex(dexPathList[i], outputName, 0)。
0x01
new DexFile(pathFile)相應的代碼位於libcore\dalvik\src\main\java\dalvik\system\DexFile.java。
public DexFile(String fileName) throws IOException { String wantDex = System.getProperty("android.vm.dexfile", "false"); if (!wantDex.equals("true")) throw new UnsupportedOperationException("No dex in this VM"); mCookie = openDexFile(fileName, null, 0); mFileName = fileName; //System.out.println("DEX FILE cookie is " + mCookie); }DexFile.loadDex(dexPathList[i], outputName, 0)相應的代碼也位於libcore\dalvik\src\main\java\dalvik\system\DexFile.java。
static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException { /* * 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); }
private DexFile(String sourceName, String outputName, int flags) throws IOException { String wantDex = System.getProperty("android.vm.dexfile", "false"); if (!wantDex.equals("true")) throw new UnsupportedOperationException("No dex in this VM"); mCookie = openDexFile(sourceName, outputName, flags); mFileName = sourceName; //System.out.println("DEX FILE cookie is " + mCookie); }我們能夠看到事實上兩者終於調用的都是openDexFile,僅僅只是DexClassLoader指定了生成優化后的apk路徑,而PathClassLoader則不須要,由於在安裝階段已經生成了/data/dalvik-cache/xxx@classes.dex。
0x02
我們繼續分析openDexFile,這種方法一個JNI方法。
native private static int openDexFile(String sourceName, String outputName, int flags) throws IOException;代碼位於libcore\dalvik\src\main\java\dalvik\system\DexFile.java。
相應的native方法位於dalvik\vm\native\dalvik_system_DexFile.c。
const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { { "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", Dalvik_dalvik_system_DexFile_openDexFile }, { "closeDexFile", "(I)V", Dalvik_dalvik_system_DexFile_closeDexFile }, { "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;ILjava/security/ProtectionDomain;)Ljava/lang/Class;", Dalvik_dalvik_system_DexFile_defineClass }, { "getClassNameList", "(I)[Ljava/lang/String;", Dalvik_dalvik_system_DexFile_getClassNameList }, { "isDexOptNeeded", "(Ljava/lang/String;)Z", Dalvik_dalvik_system_DexFile_isDexOptNeeded }, { NULL, NULL, NULL }, };我們看到openDexFile相應的是Dalvik_dalvik_system_DexFile_openDexFile方法。
static void Dalvik_dalvik_system_DexFile_openDexFile(const u4* args, JValue* pResult) { ...... if (dvmRawDexFileOpen(sourceName, outputName, &pRawDexFile, false) == 0) { LOGV("Opening DEX file '%s' (DEX)\n", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = true; pDexOrJar->pRawDexFile = pRawDexFile; } else if (dvmJarFileOpen(sourceName, outputName, &pJarFile, false) == 0) { LOGV("Opening DEX file '%s' (Jar)\n", sourceName); pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile; } else { LOGV("Unable to open DEX file '%s'\n", sourceName); dvmThrowException("Ljava/io/IOException;", "unable to open DEX file"); } ...... RETURN_PTR(pDexOrJar); }代碼位於dalvik\vm\native\dalvik_system_DexFile.c。
這里調用dvmJarFileOpen,代碼也位於dalvik\vm\JarFile.c中。
int dvmJarFileOpen(const char* fileName, const char* odexOutputName, JarFile** ppJarFile, bool isBootstrap) { ZipArchive archive; DvmDex* pDvmDex = NULL; char* cachedName = NULL; bool archiveOpen = false; bool locked = false; int fd = -1; int result = -1; /* Even if we're not going to look at the archive, we need to * open it so we can stuff it into ppJarFile. */ if (dexZipOpenArchive(fileName, &archive) != 0) goto bail; archiveOpen = true; /* If we fork/exec into dexopt, don't let it inherit the archive's fd. */ dvmSetCloseOnExec(dexZipGetArchiveFd(&archive)); /* First, look for a ".odex" alongside the jar file. It will * have the same name/path except for the extension. */ fd = openAlternateSuffix(fileName, "odex", O_RDONLY, &cachedName); if (fd >= 0) { ....... } else { ZipEntry entry; tryArchive: /* * Pre-created .odex absent or stale. Look inside the jar for a * "classes.dex". */ entry = dexZipFindEntry(&archive, kDexInJarName); if (entry != NULL) { bool newFile = false; /* * We've found the one we want. See if there's an up-to-date copy * in the cache. * * On return, "fd" will be seeked just past the "opt" header. * * If a stale .odex file is present and classes.dex exists in * the archive, this will *not* return an fd pointing to the * .odex file; the fd will point into dalvik-cache like any * other jar. */ if (odexOutputName == NULL) { cachedName = dexOptGenerateCacheFileName(fileName, kDexInJarName); if (cachedName == NULL) goto bail; } else { cachedName = strdup(odexOutputName); } LOGV("dvmDexCacheStatus: Checking cache for %s (%s)\n", fileName, cachedName); fd = dvmOpenCachedDexFile(fileName, cachedName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), isBootstrap, &newFile, /*createIfMissing=*/true); if (fd < 0) { LOGI("Unable to open or create cache for %s (%s)\n", fileName, cachedName); goto bail; } locked = true; /* * If fd points to a new file (because there was no cached version, * or the cached version was stale), generate the optimized DEX. * The file descriptor returned is still locked, and is positioned * just past the optimization header. */ if (newFile) { u8 startWhen, extractWhen, endWhen; bool result; off_t dexOffset; dexOffset = lseek(fd, 0, SEEK_CUR); result = (dexOffset > 0); if (result) { startWhen = dvmGetRelativeTimeUsec(); result = dexZipExtractEntryToFile(&archive, entry, fd) == 0; extractWhen = dvmGetRelativeTimeUsec(); } if (result) { result = dvmOptimizeDexFile(fd, dexOffset, dexGetZipEntryUncompLen(&archive, entry), fileName, dexGetZipEntryModTime(&archive, entry), dexGetZipEntryCrc32(&archive, entry), isBootstrap); } if (!result) { LOGE("Unable to extract+optimize DEX from '%s'\n", fileName); goto bail; } endWhen = dvmGetRelativeTimeUsec(); LOGD("DEX prep '%s': unzip in %dms, rewrite %dms\n", fileName, (int) (extractWhen - startWhen) / 1000, (int) (endWhen - extractWhen) / 1000); } } else { ...... } } /* * Map the cached version. This immediately rewinds the fd, so it * doesn't have to be seeked anywhere in particular. */ if (dvmDexFileOpenFromFd(fd, &pDvmDex) != 0) { LOGI("Unable to map %s in %s\n", kDexInJarName, fileName); goto bail; } ...... LOGV("Successfully opened '%s' in '%s'\n", kDexInJarName, fileName); *ppJarFile = (JarFile*) calloc(1, sizeof(JarFile)); (*ppJarFile)->archive = archive; (*ppJarFile)->cacheFileName = cachedName; (*ppJarFile)->pDvmDex = pDvmDex; cachedName = NULL; // don't free it below result = 0; bail: ...... return result; }我們主要關注dvmOpenCachedDexFile方法。首先調用這種方法,嘗試依據cachedName所指的優化文件名稱在cache中查找並讀取優化文件,假設沒有。則要就對Dex進行優化。我們前面說過對於PathClassLoader,在安裝階段已經生成了/data/dalvik-cache/xxx@classes.dex,所以dvmOpenCachedDexFile方法返回的newFile為false。而對於DexClassLoader已經指明了要生成優化后的dex的路徑。這里dvmOpenCachedDexFile方法返回的newFile為true。
對於PathClassLoader。newFile為false。所以不須要再一次優化dex。而對於DexClassLoader,newFile為true。所以調用dvmOptimizeDexFile來優化dex。dvmOptimizeDexFile代碼位於 dalvik\vm\analysis\DexPrepare.c。也就是說對於DexClassLoader也僅僅優化一次。第二次就不會再優化了。優化還是比較耗時的。
bool dvmOptimizeDexFile(int fd, off_t dexOffset, long dexLength, const char* fileName, u4 modWhen, u4 crc, bool isBootstrap) { ...... pid = fork(); if (pid == 0) { static const char* kDexOptBin = "/bin/dexopt"; ...... androidRoot = getenv("ANDROID_ROOT"); if (androidRoot == NULL) { LOGW("ANDROID_ROOT not set, defaulting to /system\n"); androidRoot = "/system"; } execFile = malloc(strlen(androidRoot) + strlen(kDexOptBin) + 1); strcpy(execFile, androidRoot); strcat(execFile, kDexOptBin); ..... if (kUseValgrind) execv(kValgrinder, argv); else execv(execFile, argv); LOGE("execv '%s'%s failed: %s\n", execFile, kUseValgrind ? " [valgrind]" : "", strerror(errno)); exit(1); } else { ...... } }也是fork出一個子線程去運行/system/bin/dexopt。也就是OptMain.c中的main方法,往后的運行流程請參考apk安裝和優化原理一文。
簡單的脫殼程序就在dvmDexFileOpenPartial下斷點,參考使用IDA Pro進行簡單的脫殼。DexClassLoader載入dex首先要進行優化。優化的過程中會調用dvmDexFileOpenPartial。而dvmDexFileOpenPartial的dexAddr是dex在內存的基地址。dexLength是dex在內存中長度,具體請參考apk安裝和優化原理一文。這樣就能夠在內存中把dex dump出來。
我們回到dvmJarFileOpen,繼續運行dvmDexFileOpenFromFd,代碼位於dalvik\vm\DvmDex.c。
int dvmDexFileOpenFromFd(int fd, DvmDex** ppDvmDex) { DvmDex* pDvmDex; DexFile* pDexFile; MemMapping memMap; int parseFlags = kDexParseDefault; int result = -1; if (gDvm.verifyDexChecksum) parseFlags |= kDexParseVerifyChecksum; if (lseek(fd, 0, SEEK_SET) < 0) { LOGE("lseek rewind failed\n"); goto bail; } if (sysMapFileInShmemWritableReadOnly(fd, &memMap) != 0) { LOGE("Unable to map file\n"); goto bail; } pDexFile = dexFileParse(memMap.addr, memMap.length, parseFlags); if (pDexFile == NULL) { LOGE("DEX parse failed\n"); sysReleaseShmem(&memMap); goto bail; } pDvmDex = allocateAuxStructures(pDexFile); if (pDvmDex == NULL) { dexFileFree(pDexFile); sysReleaseShmem(&memMap); goto bail; } /* tuck this into the DexFile so it gets released later */ sysCopyMap(&pDvmDex->memMap, &memMap); *ppDvmDex = pDvmDex; result = 0; bail: return result; }fd為優化后的dex。對於PathClassLoader,位於/data/dalvik-cache/xxx@classes.dex。
對於DexClassLoader。就是在new DexClassLoader(String dexPath, String dexOutputDir, String libPath, ClassLoader parent)指定的dexOutputDir路徑中。
在dvmDexFileOpenFromFd方法中首先調用sysMapFileInShmemWritableReadOnly方法把優化后的dex載入進內存。
int sysMapFileInShmemWritableReadOnly(int fd, MemMapping* pMap) { #ifdef HAVE_POSIX_FILEMAP off_t start; size_t length; void* memPtr; assert(pMap != NULL); if (getFileStartAndLength(fd, &start, &length) < 0) return -1; memPtr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_FILE | MAP_PRIVATE, fd, start); if (memPtr == MAP_FAILED) { LOGW("mmap(%d, R/W, FILE|PRIVATE, %d, %d) failed: %s\n", (int) length, fd, (int) start, strerror(errno)); return -1; } if (mprotect(memPtr, length, PROT_READ) < 0) { /* this fails with EACCESS on FAT filesystems, e.g. /sdcard */ int err = errno; LOGV("mprotect(%p, %d, PROT_READ) failed: %s\n", memPtr, length, strerror(err)); LOGD("mprotect(RO) failed (%d), file will remain read-write\n", err); } pMap->baseAddr = pMap->addr = memPtr; pMap->baseLength = pMap->length = length; return 0; #else return sysFakeMapFile(fd, pMap); #endif }代碼位於dalvik/libdex/SysUtil.c。
MemMapping結構體pMap中baseAddr和addr都指向dex映射到內存的首地址。
返回到dvmDexFileOpenFromFd,繼續運行dexFileParse。代碼位於dalvik\libdex\DexFile.c。
DexFile* dexFileParse(const u1* data, size_t length, int flags) { DexFile* pDexFile = NULL; const DexHeader* pHeader; const u1* magic; int result = -1; if (length < sizeof(DexHeader)) { LOGE("too short to be a valid .dex\n"); goto bail; /* bad file format */ } pDexFile = (DexFile*) malloc(sizeof(DexFile)); if (pDexFile == NULL) goto bail; /* alloc failure */ memset(pDexFile, 0, sizeof(DexFile)); /* * Peel off the optimized header. */ if (memcmp(data, DEX_OPT_MAGIC, 4) == 0) { magic = data; if (memcmp(magic+4, DEX_OPT_MAGIC_VERS, 4) != 0) { LOGE("bad opt version (0x%02x %02x %02x %02x)\n", magic[4], magic[5], magic[6], magic[7]); goto bail; } pDexFile->pOptHeader = (const DexOptHeader*) data; LOGV("Good opt header, DEX offset is %d, flags=0x%02x\n", pDexFile->pOptHeader->dexOffset, pDexFile->pOptHeader->flags); /* parse the optimized dex file tables */ if (!dexParseOptData(data, length, pDexFile)) goto bail; data += pDexFile->pOptHeader->dexOffset; length -= pDexFile->pOptHeader->dexOffset; ...... } dexFileSetupBasicPointers(pDexFile, data); pHeader = pDexFile->pHeader; ...... result = 0; bail: if (result != 0 && pDexFile != NULL) { dexFileFree(pDexFile); pDexFile = NULL; } return pDexFile; }
這種方法主要是生成DexFile結構,我們首先來看DexFile結構是什么樣子的?
typedef struct DexFile { /* directly-mapped "opt" header */ const DexOptHeader* pOptHeader; /* pointers to directly-mapped structs and arrays in base DEX */ const DexHeader* pHeader; const DexStringId* pStringIds; const DexTypeId* pTypeIds; const DexFieldId* pFieldIds; const DexMethodId* pMethodIds; const DexProtoId* pProtoIds; const DexClassDef* pClassDefs; const DexLink* pLinkData; /* * These are mapped out of the "auxillary" section, and may not be * included in the file. */ const DexClassLookup* pClassLookup; const void* pRegisterMapPool; // RegisterMapClassPool /* points to start of DEX file data */ const u1* baseAddr; /* track memory overhead for auxillary structures */ int overhead; /* additional app-specific data structures associated with the DEX */ //void* auxData; } DexFile;代碼位於dalvik\libdex\DexFile.h中。
dexFileParse這種方法首先為pDexFile->pOptHeader賦值,將優化文件頭部與DexFile數據結構下的pOptHeader變量進行關聯。
然后調用dexParseOptData函數對優化數據進行處理,其作用也是將各個優化數據與DexFile數據結構中的相應成員變量進行關聯。這里關聯的是pClassLookup和pRegisterMapPool。
最后調用dexFileSetupBasicPointers將Dex文件里其它各部分數據與DexFile數據結構建立完整的映射關系。代碼位於dalvik\libdex\DexFile.c。
void dexFileSetupBasicPointers(DexFile* pDexFile, const u1* data) { DexHeader *pHeader = (DexHeader*) data; pDexFile->baseAddr = data; pDexFile->pHeader = pHeader; pDexFile->pStringIds = (const DexStringId*) (data + pHeader->stringIdsOff); pDexFile->pTypeIds = (const DexTypeId*) (data + pHeader->typeIdsOff); pDexFile->pFieldIds = (const DexFieldId*) (data + pHeader->fieldIdsOff); pDexFile->pMethodIds = (const DexMethodId*) (data + pHeader->methodIdsOff); pDexFile->pProtoIds = (const DexProtoId*) (data + pHeader->protoIdsOff); pDexFile->pClassDefs = (const DexClassDef*) (data + pHeader->classDefsOff); pDexFile->pLinkData = (const DexLink*) (data + pHeader->linkOff); }重點說一下baseAddr指向dex文件的頭部。
運行完dexFileParse生成了DexFile結構。返回到dvmDexFileOpenFromFd,繼續運行allocateAuxStructures方法來生成DvmDex結構pDvmDex。代碼位於dalvik\vm\DvmDex.c。
static DvmDex* allocateAuxStructures(DexFile* pDexFile) { DvmDex* pDvmDex; const DexHeader* pHeader; u4 stringCount, classCount, methodCount, fieldCount; pDvmDex = (DvmDex*) calloc(1, sizeof(DvmDex)); if (pDvmDex == NULL) return NULL; pDvmDex->pDexFile = pDexFile; pDvmDex->pHeader = pDexFile->pHeader; pHeader = pDvmDex->pHeader; stringCount = pHeader->stringIdsSize; classCount = pHeader->typeIdsSize; methodCount = pHeader->methodIdsSize; fieldCount = pHeader->fieldIdsSize; pDvmDex->pResStrings = (struct StringObject**) calloc(stringCount, sizeof(struct StringObject*)); pDvmDex->pResClasses = (struct ClassObject**) calloc(classCount, sizeof(struct ClassObject*)); pDvmDex->pResMethods = (struct Method**) calloc(methodCount, sizeof(struct Method*)); pDvmDex->pResFields = (struct Field**) calloc(fieldCount, sizeof(struct Field*)); LOGV("+++ DEX %p: allocateAux %d+%d+%d+%d * 4 = %d bytes\n", pDvmDex, stringCount, classCount, methodCount, fieldCount, (stringCount + classCount + methodCount + fieldCount) * 4); pDvmDex->pInterfaceCache = dvmAllocAtomicCache(DEX_INTERFACE_CACHE_SIZE); if (pDvmDex->pResStrings == NULL || pDvmDex->pResClasses == NULL || pDvmDex->pResMethods == NULL || pDvmDex->pResFields == NULL || pDvmDex->pInterfaceCache == NULL) { LOGE("Alloc failure in allocateAuxStructures\n"); free(pDvmDex->pResStrings); free(pDvmDex->pResClasses); free(pDvmDex->pResMethods); free(pDvmDex->pResFields); free(pDvmDex); return NULL; } return pDvmDex; }然后返回到dvmDexFileOpenFromFd,運行sysCopyMap把DvmDex結構pDvmDex的memMap附上值。最后返回DvmDex結構pDvmDex。
運行完dvmDexFileOpenFromFd,返回到dvmJarFileOpen。生成了一個JarFile結構體ppJarFile,並賦值例如以下:
*ppJarFile = (JarFile*) calloc(1, sizeof(JarFile)); (*ppJarFile)->archive = archive; (*ppJarFile)->cacheFileName = cachedName; (*ppJarFile)->pDvmDex = pDvmDex;在返回到Dalvik_dalvik_system_DexFile_openDexFile中。生成一個DexOrJar結構體pDexOrJar。並賦值例如以下:
pDexOrJar = (DexOrJar*) malloc(sizeof(DexOrJar)); pDexOrJar->isDex = false; pDexOrJar->pJarFile = pJarFile;然后RETURN_PTR(pDexOrJar)返回給Java層的openDexFile,並賦值給mCookie。例如以下:
mCookie = openDexFile(fileName, null, 0);下次通過mCookie。我們就能一步一步找到DexFile結構體(DexOrJar->JarFile->DvmDex->DexFile)。
0x03
最后附上一張DexFile的結構圖:
圖中有一處錯誤,baseAddr和pHeader事實上都指向DexHeader。也就是dex文件頭在內存中的虛擬地址。
0x03
那么PatchClassLoader對象是什么時候生成的呢?
在Android加殼原理分析一文中,在handleBindApplication方法里須要獲取ClassLoader,這個ClassLoader對象就是PathClassLoader對象。以下來看看是怎么形成的?
private final void handleBindApplication(AppBindData data) { ...... try { java.lang.ClassLoader cl = instrContext.getClassLoader(); mInstrumentation = (Instrumentation) cl.loadClass(data.instrumentationName.getClassName()).newInstance(); } catch (Exception e) { throw new RuntimeException( "Unable to instantiate instrumentation " + data.instrumentationName + ": " + e.toString(), e); } ...... }代碼位於frameworks\base\core\java\android\app\ActivityThread.java。
instrContext.getClassLoader(),實現例如以下,代碼位於frameworks\base\core\java\android\app\ContextImpl.java。
@Override public ClassLoader getClassLoader() { return mPackageInfo != null ?mPackageInfo.getClassLoader()。實現例如以下。代碼位於frameworks\base\core\java\android\app\LoadedApk.java。mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader(); }
public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader != null) { return mClassLoader; } if (mIncludeCode && !mPackageName.equals("android")) { String zip = mAppDir; ...... mClassLoader = ApplicationLoaders.getDefault().getClassLoader( zip, mLibDir, mBaseClassLoader); initializeJavaContextClassLoader(); } else { if (mBaseClassLoader == null) { mClassLoader = ClassLoader.getSystemClassLoader(); } else { mClassLoader = mBaseClassLoader; } } return mClassLoader; } }ApplicationLoaders.getDefault().getClassLoader( zip, mLibDir, mBaseClassLoader),實現例如以下,代碼位於frameworks\base\core\java\android\app\ApplicationLoaders.java中。
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent) { /* * This is the parent we use if they pass "null" in. In theory * this should be the "system" class loader; in practice we * don't use that and can happily (and more efficiently) use the * bootstrap class loader. */ ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent(); synchronized (mLoaders) { if (parent == null) { parent = baseParent; } /* * If we're one step up from the base class loader, find * something in our cache. Otherwise, we create a whole * new ClassLoader for the zip archive. */ if (parent == baseParent) { ClassLoader loader = mLoaders.get(zip); if (loader != null) { return loader; } PathClassLoader pathClassloader = new PathClassLoader(zip, libPath, parent); mLoaders.put(zip, pathClassloader); return pathClassloader; } return new PathClassLoader(zip, parent); } }最后的時候,我們看到了PathClassLoader對象的生成。