Android執行時ART載入OAT檔案的過程分析

羅昇陽的部落格發表於2014-09-29

在前面一文中,我們介紹了Android執行時ART,它的核心是OAT檔案。OAT檔案是一種Android私有ELF檔案格式,它不僅包含有從DEX檔案翻譯而來的本地機器指令,還包含有原來的DEX檔案內容。這使得我們無需重新編譯原有的APK就可以讓它正常地在ART裡面執行,也就是我們不需要改變原來的APK程式設計介面。本文我們通過OAT檔案的載入過程分析OAT檔案的結構,為後面分析ART的工作原理打基礎。

OAT檔案的結構如圖1所示:

20140914023348415

 

由於OAT檔案本質上是一個ELF檔案,因此在最外層它具有一般ELF檔案的結構,例如它有標準的ELF檔案頭以及通過段(Section)來描述檔案內容。關於ELF檔案的更多知識,可以參考維基百科:http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

作為Android私有的一種ELF檔案,OAT檔案包含有兩個特殊的段oatdata和oatexec,前者包含有用來生成本地機器指令的dex檔案內容,後者包含有生成的本地機器指令,它們之間的關係通過儲存在oatdata段前面的oat頭部描述。此外,在OAT檔案的dynamic段,匯出了三個符號oatdata、oatexec和oatlastword,它們的值就是用來界定oatdata段和oatexec段的起止位置的。其中,[oatdata, oatexec – 4]描述的是oatdata段的起止位置,而[oatexec, oatlastword]描述的是oatlastword的起止位置。要完全理解OAT的檔案格式,除了要理解本文即將要分析的OAT載入過程之外,還需要掌握接下來文章分析的類和方法查詢過程。

在分析OAT檔案的載入過程之前,我們需要簡單介紹一下OAT是如何產生的。如前面Android ART執行時無縫替換Dalvik虛擬機器的過程分析一文所示,APK在安裝的過程中,會通過dex2oat工具生成一個OAT檔案:

這個函式定義在檔案frameworks/native/cmds/installd/commands.c中。

 

其中,引數zip_fd和oat_fd都是開啟檔案描述符,指向的分別是正在安裝的APK檔案和要生成的OAT檔案。OAT檔案的生成過程主要就是涉及到將包含在APK裡面的classes.dex檔案的DEX位元組碼翻譯成本地機器指令。這相當於是編寫一個輸入檔案為DEX、輸出檔案為OAT的編譯器。這個編譯器是基於LLVM開發的。編譯器的工作原理比較高大上,所幸的是它不會影響到我們接下來的分析,因此我們就略過DEX位元組碼翻譯成本地機器指令的過程,假設它很愉快地完成了。

APK安裝過程中生成的OAT檔案的輸入只有一個DEX檔案,也就是來自於打包在要安裝的APK檔案裡面的classex.dex檔案。實際上,一個OAT檔案是可以由若干個DEX生成的。這意味著在生成的OAT檔案的oatdata段中,包含有多個DEX檔案。那麼,在什麼情況下,會生成包含多個DEX檔案的OAT檔案呢?

從前面Android ART執行時無縫替換Dalvik虛擬機器的過程分析一文可以知道,當我們選擇了ART執行時時,Zygote程式在啟動的過程中,會呼叫libart.so裡面的函式JNI_CreateJavaVM來建立一個ART虛擬機器。函式JNI_CreateJavaVM的實現如下所示:

這個函式定義在檔案art/runtime/jni_internal.cc中。

 

引數vm_args用作ART虛擬機器的啟動引數,它被轉換為一個JavaVMInitArgs物件後,再按照Key-Value的組織形式儲存一個Options向量中,並且作該向量作為引數傳遞給Runtime類的靜態成員函式Create。

Runtime類的靜態成員函式Create負責在程式中建立一個ART虛擬機器。建立成功後,就呼叫Runtime類的另外一個靜態成員函式Start啟動該ART虛擬機器。注意,這個建立ART虛擬的動作只會在Zygote程式中執行,SystemServer系統程式以及Android應用程式程式的ART虛擬機器都是直接從Zygote程式fork出來共享的。這與Dalvik虛擬機器的建立方式是完全一樣的。

接下來我們就重點分析Runtime類的靜態成員函式Create,它的實現如下所示:

這個函式定義在檔案art/runtime/runtime.cc中。

 

instance_是Runtime類的靜態成員變數,它指向程式中的一個Runtime單例。這個Runtime單例描述的就是當前程式的ART虛擬機器例項。

函式首先判斷當前程式是否已經建立有一個ART虛擬機器例項了。如果有的話,函式就立即返回。否則的話,就建立一個ART虛擬機器例項,並且儲存在Runtime類的靜態成員變數instance_中,最後呼叫Runtime類的成員函式Init對該新建立的ART虛擬機器進行初始化。

Runtime類的成員函式Init的實現如下所示:

這個函式定義在檔案art/runtime/runtime.cc中。

 

Runtime類的成員函式Init首先呼叫ParsedOptions類的靜態成員函式Create對ART虛擬機器的啟動引數raw_options進行解析。解析後得到的引數儲存在一個ParsedOptions物件中,接下來就根據這些引數一個ART虛擬機器堆。ART虛擬機器堆使用一個Heap物件來描述。

建立好ART虛擬機器堆後,Runtime類的成員函式Init接著又建立了一個JavaVMExt例項。這個JavaVMExt例項最終是要返回給呼叫者的,使得呼叫者可以通過該JavaVMExt例項來和ART虛擬機器互動。再接下來,Runtime類的成員函式Init通過Thread類的成員函式Attach將當前執行緒作為ART虛擬機器的主執行緒,使得當前執行緒可以呼叫ART虛擬機器提供的JNI介面。

Runtime類的成員函式GetHeap返回的便是當前ART虛擬機器的堆,也就是前面建立的ART虛擬機器堆。通過呼叫Heap類的成員函式GetContinuousSpaces可以獲得堆裡面的連續空間列表。如果這個列表的第一個連續空間是一個Image空間,那麼就呼叫ClassLinker類的靜態成員函式CreateFromImage來建立一個ClassLinker物件。否則的話,上述ClassLinker物件就要通過ClassLinker類的另外一個靜態成員函式CreateFromCompiler來建立。建立出來的ClassLinker物件是後面ART虛擬機器載入載入Java類時要用到的。

後面我們分析ART虛擬機器的垃圾收集機制時會看到,ART虛擬機器的堆包含有三個連續空間和一個不連續空間。三個連續空間分別用來分配不同的物件。當第一個連續空間不是Image空間時,就表明當前程式不是Zygote程式,而是安裝應用程式時啟動的一個dex2oat程式。安裝應用程式時啟動的dex2oat程式也會在內部建立一個ART虛擬機器,不過這個ART虛擬機器是用來將DEX位元組碼編譯成本地機器指令的,而Zygote程式建立的ART虛擬機器是用來執行應用程式的。

接下來我們主要分析ParsedOptions類的靜態成員函式Create和ART虛擬機器堆Heap的建構函式,以便可以瞭解ART虛擬機器的啟動引數解析過程和ART虛擬機器的堆建立過程。

ParsedOptions類的靜態成員函式Create的實現如下所示:

這個函式定義在檔案art/runtime/runtime.cc中。

 

ART虛擬機器的啟動引數比較多,這裡我們只關注兩個:-Xbootclasspath、-Ximage和compiler。

引數-Xbootclasspath用來指定啟動類路徑。如果沒有指定啟動類路徑,那麼預設的啟動類路徑就通過環境變數BOOTCLASSPATH來獲得。

引數-Ximage用來指定ART虛擬機器所使用的Image檔案。這個Image是用來啟動ART虛擬機器的。

引數compiler用來指定當前要建立的ART虛擬機器是用來將DEX位元組碼編譯成本地機器指令的。

如果沒有指定Image檔案,並且當前建立的ART虛擬機器又不是用來編譯DEX位元組碼的,那麼就將該Image檔案指定為裝置上的/system/framework/boot.art檔案。我們知道,system分割槽的檔案都是在製作ROM時打包進去的。這樣上述程式碼的邏輯就是說,如果沒有指定Image檔案,那麼將system分割槽預先準備好的framework/boot.art檔案作為Image檔案來啟動ART虛擬機器。不過,/system/framework/boot.art檔案可能是不存在的。在這種情況下,就需要生成一個新的Image檔案。這個Image檔案就是一個包含了多個DEX檔案的OAT檔案。接下來通過分析ART虛擬機器堆的建立過程就會清楚地看到這一點。

Heap類的建構函式的實現如下所示:

這個函式定義在檔案art/runtime/gc/heap.cc中。

 

ART虛擬機器堆的詳細建立過程我們在後面分析ART虛擬機器的垃圾收集機制時再分析,這裡只關注與Image檔案相關的邏輯。

引數original_image_file_name描述的就是前面提到的Image檔案的路徑。如果它的值不等於空的話,那麼就以它為引數,呼叫ImageSpace類的靜態成員函式Create建立一個Image空間,並且呼叫Heap類的成員函式AddContinuousSpace將該Image空間作為本程式的ART虛擬機器堆的第一個連續空間。

接下來我們繼續分析ImageSpace類的靜態成員函式Create,它的實現如下所示:

這個函式定義在檔案art/runtime/gc/space/image_space.cc中。

ImageSpace類的靜態成員函式Create首先是檢查引數original_image_file_name指定的Image檔案是否存在。如果存在的話,就以它為引數,呼叫ImageSpace類的另外一個靜態成員函式Init來建立一個Image空間。否則的話,再呼叫函式GetDalvikCacheFilenameOrDie根據引數original_image_file_name構造另外一個在/data/dalvik-cache目錄下的檔案路徑,然後再檢查這個檔案是否存在。如果存在的話,就同樣是以它為引數,呼叫ImageSpace類的靜態成員函式Init來建立一個Image空間。否則的話,就要呼叫ImageSpace類的另外一個靜態成員函式GenerateImage來生成一個新的Image檔案,接著再呼叫ImageSpace類的靜態成員函式Init來建立一個Image空間了。

我們假設引數original_image_file_name的值等於“/system/framework/boot.art”,那麼ImageSpace類的靜態成員函式Create的執行邏輯實際上就是:

1. 檢查檔案/system/framework/boot.art是否存在。如果存在,那麼就以它為參婁,建立一個Image空間。否則的話,執行下一步。

2. 檢查檔案/data/dalvik-cache/system@framework@boot.art@classes.dex是否存在。如果存在,那麼就以它為引數,建立一個Image空間。否則的話,執行下一步。

3. 呼叫ImageSpace類的靜態成員函式GenerateImage在/data/dalvik-cache目錄下生成一個system@framework@boot.art@classes.dex,然後再以該檔案為引數,建立一個Image空間。

接下來我們再來看看ImageSpace類的靜態成員函式GenerateImage的實現,如下所示:

這個函式定義在檔案art/runtime/gc/space/image_space.cc中。

 

ImageSpace類的靜態成員函式GenerateImage實際上就呼叫dex2oat工具在/data/dalvik-cache目錄下生成兩個檔案:system@framework@boot.art@classes.dex和system@framework@boot.art@classes.oat。

system@framework@boot.art@classes.dex是一個Image檔案,通過–image選項傳遞給dex2oat工具,裡面包含了一些需要在Zygote程式啟動時預載入的類。這些需要預載入的類由/system/framework/framework.jar檔案裡面的preloaded-classes檔案指定。

system@framework@boot.art@classes.oat是一個OAT檔案,通過–oat-file選項傳遞給dex2oat工具,它是由系統啟動路徑中指定的jar檔案生成的。每一個jar檔案都通過一個–dex-file選項傳遞給dex2oat工具。這樣dex2oat工具就可以將它們所包含的classes.dex檔案裡面的DEX位元組碼翻譯成本地機器指令。

這樣,我們就得到了一個包含有多個DEX檔案的OAT檔案system@framework@boot.art@classes.oat。

通過上面的分析,我們就清楚地看到了ART執行時所需要的OAT檔案是如何產生的了。其中,由系統啟動類路徑指定的DEX檔案生成的OAT檔案稱為型別為BOOT的OAT檔案,即boot.art檔案。有了這個背景知識之後,接下來我們就繼續分析ART執行時是如何載入OAT檔案的。

ART執行時提供了一個OatFile類,通過呼叫它的靜態成員函式Open可以在本程式中載入OAT檔案,它的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

引數filename和location實際上是一樣的,指向要載入的OAT檔案。引數requested_base是一個可選引數,用來描述要載入的OAT檔案裡面的oatdata段要載入在的位置。引數executable表示要載入的OAT是不是應用程式的主執行檔案。一般來說,一個應用程式只有一個classes.dex檔案, 這個classes.dex檔案經過編譯後,就得到一個OAT主執行檔案。不過,應用程式也可以在執行時動態載入DEX檔案。這些動態載入的DEX檔案在載入的時候同樣會被翻譯成OAT再執行,它們相應打包在應用程式的classes.dex檔案來說,就不屬於主執行檔案了。

OatFile類的靜態成員函式Open的實現雖然只有寥寥幾行程式碼,但是要理解它還得先理解巨集ART_USE_PORTABLE_COMPILER的的作用。在前面Android執行時ART簡要介紹和學習計劃一文中提到,ART執行時利用LLVM編譯框架來將DEX位元組碼翻譯成本地機器指令,其中要通過一個稱為Backend的模組來生成本地機器指令。這些生成的機器指令就儲存在ELF檔案格式的OAT檔案的oatexec段中。

ART執行時會為每一個類方法都生成一系列的本地機器指令。這些本地機器指令不是孤立存在的,因為它們可能需要其它的函式來完成自己的功能。例如,它們可能需要呼叫ART執行的堆管理系統提供的介面來為物件分配記憶體空間。這樣就會涉及到一個模組依賴性問題,就好像我們在編寫程式時,需要依賴C庫提供的介面一樣。這要求Backend為類方法生成本地機器指令時,要處理呼叫其它模組提供的函式的問題。

ART執行時支援兩種型別的Backend:Portable和Quick。Portable型別的Backend通過整合在LLVM編譯框架裡面的一個稱為MCLinker的連結器來生成本地機器指令。關於MCLinker的更多知識,可以參考https://code.google.com/p/mclinker。簡單來說,假設我們有一個模組A,它依賴於模組B、C和D,那麼在為模組A生成本地機器指令時,指出它依賴於模組B、C和D就行了。在生成的OAT檔案中會記錄好這些依賴關係,這是ELF檔案格式本來就支援的特性。這些OAT檔案要通過系統的動態連結器提供的dlopen函式來載入。函式dlopen在載入OAT檔案的時候,會通過重定位技術來處理好它與其它模組的依賴關係,使得它能夠呼叫其它模組提供的介面。這個實際上就通用的編譯器、靜態聯結器以及動態連結器合作在一起幹的事情,MCLinker扮演的就是靜態連結器的角色。既然是通用的技術,因為就稱能產生這種OAT檔案的Backend為Portable型別的。

另一方面,Quick型別的Backend生成的本地機器指令用另外一種方式來處理依賴模組之間的依賴關係。簡單來說,就是ART執行時會在每一個執行緒的TLS(執行緒本地區域)提供一個函式表。可以這個函式表,Quick型別的Backend生成的本地機器指令通過引用這個函式表來呼叫其它模組的函式。也就是說,Quick型別的Backend生成的本地機器指令要依賴於ART運運時提供的函式表。這使得Quick型別的Backend生成的OAT檔案在載入時不需要再處理模式之間的依賴關係。再通俗一點說的就是Quick型別的Backend生成的OAT檔案在載入時不需要重定位,因此就不需要通過系統的動態連結器提供的dlopen函式來載入。由於省去重定位這個操作,Quick型別的Backend生成的OAT檔案在載入時就會更快,這也是稱為Quick的緣由。

關於ART執行時型別為Portable和Quick兩種型別的Backend,我們就暫時講解到這裡,後面分析ART執行時執行類方法的時候,我們再詳細分析。現在我們需要知道的就是,如果在編譯ART執行時時,定義了巨集ART_USE_PORTABLE_COMPILER,那麼就表示要使用Portable型別的Backend來生成OAT檔案,否則就使用Quick型別的Backend來生成OAT檔案。預設情況下,使用的是Quick型別的Backend。

接下就可以很好地理解OatFile類的靜態成員函式Open的實現了:

1. 如果編譯時指定了ART_USE_PORTABLE_COMPILER巨集,並且引數executable為true,那麼就通過OatFile類的靜態成員函式OpenDlopen來載入指定的OAT檔案。OatFile類的靜態成員函式OpenDlopen直接通過動態連結器提供的dlopen函式來載入OAT檔案。

2. 其餘情況下,通過OatFile類的靜態成員函式OpenElfFile來手動載入指定的OAT檔案。這種方式是按照ELF檔案格式來解析要載入的OAT檔案的,並且根據解析獲得的資訊將OAT裡面相應的段載入到記憶體中來。

接下來我們就分別看看OatFile類的靜態成員函式OpenDlopen和OpenElfFile的實現,以便可以對OAT檔案有更清楚的認識。

OatFile類的靜態成員函式OpenDlopen的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatFile類的靜態成員函式OpenDlopen首先是建立一個OatFile物件,接著再呼叫該OatFile物件的成員函式Dlopen載入引數elf_filename指定的OAT檔案。

OatFile類的成員函式Dlopen的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatFile類的成員函式Dlopen首先是通過動態連結器提供的dlopen函式將引數elf_filename指定的OAT檔案載入到記憶體中來,接著同樣是通過動態連結器提供的dlsym函式從載入進來的OAT檔案獲得兩個匯出符號oatdata和oatlastword的地址,分別儲存在當前正在處理的OatFile物件的成員變數begin_和end_中。根據圖1所示,符號oatdata的地址即為OAT檔案裡面的oatdata段載入到記憶體中的開始地址,而符號oatlastword的地址即為OAT檔案裡面的oatexec載入到記憶體中的結束地址。符號oatlastword本身也是屬於oatexec段的,它自己佔用了一個地址,也就是sizeof(uint32_t)個位元組,於是將前面得到的end_值加上sizeof(uint32_t),得到的才是oatexec段的結束地址。

實際上,上面得到的begin_值指向的是載入記憶體中的oatdata段的頭部,即OAT頭。這個OAT頭描述了OAT檔案所包含的DEX檔案的資訊,以及定義在這些DEX檔案裡面的類方法所對應的本地機器指令在記憶體的位置。另外,上面得到的end_是用來在解析OAT頭時驗證資料的正確性的。此外,如果引數requested_base的值不等於0,那麼就要求oatdata段必須要載入到requested_base指定的位置去,也就是上面得到的begin_值與requested_base值相等,否則的話就會出錯返回。

最後,OatFile類的成員函式Dlopen通過呼叫另外一個成員函式Setup來解析已經載入記憶體中的oatdata段,以獲得ART執行所需要的更多資訊。我們分析完成OatFile類的靜態成員函式OpenElfFile之後,再來看OatFile類的成員函式Setup的實現。

OatFile類的靜態成員函式OpenElfFile的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatFile類的靜態成員函式OpenElfFile的實現與前面分析的成員函式Dlopen是很類似的,唯一不同的是前者通過ElfFile類來手動載入引數file指定的OAT檔案,實際上就是按照ELF檔案格式來解析引數file指定的OAT檔案,並且將檔案裡面的oatdata段和oatexec段載入到記憶體中來。我們可以將ElfFile類看作是ART執行時自己實現的OAT檔案動態連結器。一旦引數file指定的OAT檔案指定的檔案載入完成之後,我們同樣是通過兩個匯出符號oatdata和oatlastword來獲得oatdata段和oatexec段的起止位置。同樣,如果引數requested_base的值不等於0,那麼就要求oatdata段必須要載入到requested_base指定的位置去。

將引數file指定的OAT檔案載入到記憶體之後,OatFile類的靜態成員函式OpenElfFile最後也是呼叫OatFile類的成員函式Setup來解析其中的oatdata段。OatFile類的成員函式Setup定義在檔案art/runtime/oat_file.cc中,我們分三部分來閱讀,以便可以更好地理解OAT檔案的格式。

OatFile類的成員函式Setup的第一部分實現如下所示:

我們先來看OatFile類的三個成員函式GetOatHeader、Begin和End的實現,如下所示:

這三個函式主要是涉及到了OatFile類的兩個成員變數begin_和end_,它們分別是OAT檔案裡面的oatdata段開始地址和oatexec段的結束地址。

 

通過OatFile類的成員函式GetOatHeader可以清楚地看到,OAT檔案裡面的oatdata段的開始儲存著一個OAT頭,這個OAT頭通過類OatHeader描述,定義在檔案art/runtime/oat.h中,如下所示:

類OatHeader的各個成員變數的含義如下所示:

 

magic: 標誌OAT檔案的一個魔數,等於‘oat\n’。

version: OAT檔案版本號,目前的值等於‘007、0’。

 adler32_checksum_: OAT頭部檢驗和。

instruction_set_: 本地機指令集,有四種取值,分別為  kArm(1)、kThumb2(2)、kX86(3)和kMips(4)。

dex_file_count_: OAT檔案包含的DEX檔案個數。

executable_offset_: oatexec段開始位置與oatdata段開始位置的偏移值。

interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_: ART執行時在啟動的時候,可以通過-Xint選項指定所有類的方法都是解釋執行的,這與傳統的虛擬機器使用直譯器來執行類方法差不多。同時,有些類方法可能沒有被翻譯成本地機器指令,這時候也要求對它們進行解釋執行。這意味著解釋執行的類方法在執行的過程中,可能會呼叫到另外一個也是解釋執行的類方法,也可能呼叫到另外一個按本地機器指令執行的類方法中。OAT檔案在內部提供有兩段trampoline程式碼,分別用來從直譯器呼叫另外一個也是通過直譯器來執行的類方法和從直譯器呼叫另外一個按照本地機器執行的類方法。這兩段trampoline程式碼的偏移位置就儲存在成員變數 interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_。

jni_dlsym_lookup_offset_: 類方法在執行的過程中,如果要呼叫另外一個方法是一個JNI函式,那麼就要通過存在放置jni_dlsym_lookup_offset_的一段trampoline程式碼來呼叫。

portable_resolution_trampoline_offset_和quick_resolution_trampoline_offset_: 用來在執行時解析還未連結的類方法的兩段trampoline程式碼。其中,portable_resolution_trampoline_offset_指向的trampoline程式碼用於Portable型別的Backend生成的本地機器指令,而quick_resolution_trampoline_offset_用於Quick型別的Backend生成的本地機器指令。

portable_to_interpreter_bridge_offset_和quick_to_interpreter_bridge_offset_: 與interpreter_to_interpreter_bridge_offset_和interpreter_to_compiled_code_bridge_offset_的作用剛好相反,用來在按照本地機器指令執行的類方法中呼叫解釋執行的類方法的兩段trampoline程式碼。其中,portable_to_interpreter_bridge_offset_用於Portable型別的Backend生成的本地機器指令,而quick_to_interpreter_bridge_offset_用於Quick型別的Backend生成的本地機器指令。

由於每一個應用程式都會依賴於boot.art檔案,因此為了節省由打包在應用程式裡面的classes.dex生成的OAT檔案的體積,上述interpreter_to_interpreter_bridge_offset_、interpreter_to_compiled_code_bridge_offset_、jni_dlsym_lookup_offset_、portable_resolution_trampoline_offset_、portable_to_interpreter_bridge_offset_、quick_resolution_trampoline_offset_和quick_to_interpreter_bridge_offset_七個成員變數指向的trampoline程式碼段只存在於boot.art檔案中。換句話說,在由打包在應用程式裡面的classes.dex生成的OAT檔案的oatdata段頭部中,上述七個成員變數的值均等於0。

image_file_location_data_: 用來建立Image空間的檔案的路徑的在記憶體中的地址。

 image_file_location_size_: 用來建立Image空間的檔案的路徑的大小。

image_file_location_oat_data_begin_: 用來建立Image空間的檔案的路徑在OAT檔案的偏移位置。

 image_file_location_oat_checksum_:  用來建立Image空間的檔案的路徑的檢驗和。

上述四個成員變數記錄了一個OAT檔案所依賴的用來建立Image空間的檔案的路徑。

通過OatFile類的成員函式Setup的第一部分程式碼的分析,我們就知道了,OAT檔案的oatdata段在最開始儲存著一個OAT頭,如圖2所示:

20140923010150806

 

我們接著再看OatFile類的成員函式Setup的第二部分程式碼:

呼叫OatFile類的成員函式GetOatHeader獲得的是正在開啟的OAT檔案的頭部OatHeader,通過呼叫它的成員函式GetImageFileLocationSize獲得的是正在開啟的OAT依賴的Image空間檔案的路徑大小。變數oat最開始的時候指向oatdata段的開始位置。讀出OAT頭之後,變數oat就跳過了OAT頭。由於正在開啟的OAT檔案引用的Image空間檔案路徑儲存在緊接著OAT頭的地方。因此,將Image空間檔案的路徑大小增加到變數oat去後,就相當於是跳過了儲存Image空間檔案路徑的位置。

通過OatFile類的成員函式Setup的第二部分程式碼的分析,我們就知道了,緊接著在OAT頭後面的是Image空間檔案路徑,如圖3所示:

20140923011120468

 

我們接著再看OatFile類的成員函式Setup的第三部分程式碼:

這部分程式碼用來獲得包含在oatdata段的DEX檔案描述資訊。每一個DEX檔案記錄在oatdata段的描述資訊包括:

1. DEX檔案路徑大小,儲存在變數dex_file_location_size中;

2. DEX檔案路徑,儲存在變數dex_file_location_data中;

3. DEX檔案檢驗和,儲存在變數dex_file_checksum中;

4. DEX檔案內容在oatdata段的偏移,儲存在變數dex_file_offset中;

5. DEX檔案包含的類的本地機器指令資訊偏移陣列,儲存在變數methods_offsets_pointer中;

在上述五個資訊中,最重要的就是第4個和第5個資訊了。

通過第4個資訊,我們可以在oatdata段中找到對應的DEX檔案的內容。DEX檔案最開始部分是一個DEX檔案頭,上述程式碼通過檢查DEX檔案頭的魔數和版本號來確保變數dex_file_offset指向的位置確實是一個DEX檔案。

通過第5個資訊我們可以找到DEX檔案裡面的每一個類方法對應的本地機器指令。這個陣列的大小等於header->class_defs_size_,即DEX檔案裡面的每一個類在陣列中都對應有一個偏移值。這裡的header指向的是DEX檔案頭,它的class_defs_size_描述了DEX檔案包含的類的個數。在DEX檔案中,每一個類都是有一個從0開始的編號,該編號就是用來索引到上述陣列的,從而獲得對應的類所有方法的本地機器指令資訊。

最後,上述得到的每一個DEX檔案的資訊都被封裝在一個OatDexFile物件中,以便以後可以直接訪問。如果我們使用OatDexFile來描述每一個DEX檔案的描述資訊,那麼就可以通過圖4看到這些描述資訊在oatdata段的位置:

20140924005112468

 

為了進一步理解包含在oatdata段的DEX檔案描述資訊,我們繼續看OatDexFile類的建構函式的實現,如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatDexFile類的建構函式的實現很簡單,它將我們在上面得到的DEX檔案描述息儲存在相應的成員變數中。通過這些資訊,我們就可以獲得包含在該DEX檔案裡面的類的所有方法的本地機器指令資訊。

例如,通過呼叫OatDexFile類的成員函式GetOatClass可以獲得指定類的所有方法的本地機器指令資訊:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

引數class_def_index表示要查詢的目標類的編號。這個編號用作陣列oat_class_offsets_pointer_(即前面描述的methods_offsets_pointer陣列)的索引,就可以得到一個偏移位置oat_class_offset。這個偏移位置是相對於OAT檔案的oatdata段的,因此將該偏移值加上OAT檔案的oatdata段的開始位置後,就可以得到目標類的所有方法的本地機器指令資訊。這些資訊的佈局如圖5所示:

20140924011349861

 

在OAT檔案中,每一個DEX檔案包含的每一個類的描述資訊都通過一個OatClass物件來描述。為了方便描述,我們稱之為OAT類。我們通過OatClass類的建構函式來理解它的作用,如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

引數oat_file描述的是宿主OAT檔案,引數status描述的是OAT類狀態,引數methods_pointer是一個陣列,描述的是OAT類的各個方法的資訊,它們被分別儲存在OatClass類的相應成員變數中。通過這些資訊,我們就可以獲得包含在該DEX檔案裡面的類的所有方法的本地機器指令資訊。

例如,通過呼叫OatClass類的成員函式GetOatMethod可以獲得指定類方法的本地機器指令資訊:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

引數method_index描述的目標方法在類中的編號,用這個編號作為索引,就可以在OatClass類的成員變數methods_pointer_指向的一個陣列中找到目標方法的本地機器指令資訊。這些本地機器指令資訊封裝在一個OatMethod物件,它們在OAT檔案的佈局如圖6下所示:

20140924013404573

 

為了進一步理解OatMethod的作用,我們繼續看它的建構函式的實現,如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatMethod類包含了很多對應類方法的本地機器指令執行時要用到的資訊,其中,最重要的就是引數base和code_offset描述的資訊。

引數base描述的是OAT檔案的OAT頭在記憶體的位置,而引數code_offset描述的是類方法的本地機器指令相對OAT頭的偏移位置。將這兩者相加,就可以得到一個類方法的本地機器指令在記憶體的位置。我們可以通過呼叫OatMethod類的成員函式GetCode來獲得這個結果。

OatMethod類的成員函式GetCode的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.cc中。

 

OatMethod類的成員函式呼叫另外一個成員函式GetOatPointer來獲得一個類方法的本地機器指令在記憶體的位置。

OatMethod類的成員函式GetOatPointer的實現如下所示:

這個函式定義在檔案art/runtime/oat_file.h中。

 

通過上面對OAT檔案載入過程的分析,我們就可以清楚地看到OAT檔案的格式,以及如何在OAT檔案中找到一個類方法的本地機器指令。我們通過圖7來總結在OAT檔案中找到一個類方法的本地機器指令的過程:

20140924020914024

 

我們從左往右來看圖7。首先是根據類簽名資訊從包含在OAT檔案裡面的DEX檔案中查詢目標Class的編號,然後再根據這個編號找到在OAT檔案中找到對應的OatClass。接下來再根據方法簽名從包含在OAT檔案裡面的DEX檔案中查詢目標方法的編號,然後再根據這個編號在前面找到的OatClass中找到對應的OatMethod。有了這個OatMethod之後,我們就根據它的成員變數begin_和code_offset_找到目標類方法的本地機器指令了。其中,從DEX檔案中根據簽名找到類和方法的編號要求對DEX檔案進行解析,這就需要利用Dalvik虛擬機器的知識了。

至此,我們就通過OAT檔案的載入過程分析完成OAT檔案的格式了。為了加深對OAT檔案格式的理解,有接下來的一篇文章中,我們再詳細分析上面描述的類方法的本地機器指令的查詢過程。

相關文章