Android執行時ART載入OAT檔案的過程分析
在前面一文中,我們介紹了Android執行時ART,它的核心是OAT檔案。OAT檔案是一種Android私有ELF檔案格式,它不僅包含有從DEX檔案翻譯而來的本地機器指令,還包含有原來的DEX檔案內容。這使得我們無需重新編譯原有的APK就可以讓它正常地在ART裡面執行,也就是我們不需要改變原來的APK程式設計介面。本文我們通過OAT檔案的載入過程分析OAT檔案的結構,為後面分析ART的工作原理打基礎。
《Android系統原始碼情景分析》一書正在進擊的程式設計師網(http://0xcc0xcd.com)中連載,點選進入!
OAT檔案的結構如圖1所示:
圖1 OAT檔案結構
由於OAT檔案本質上是一個ELF檔案,因此在最外層它具有一般ELF檔案的結構,例如它有標準的ELF檔案頭以及通過段(Section)來描述檔案內容。關於ELF檔案的更多知識,可以參考維基百科:
作為Android私有的一種ELF檔案,OAT檔案包含有兩個特殊的段oatdata和oatexec,前者包含有用來生成本地機器指令的dex檔案內容,後者包含有生成的本地機器指令,它們之間的關係通過儲存在oatdata段前面的oat頭部描述。此外,在OAT檔案的dynamic段,匯出了三個符號oatdata、oatexec和oatlastword,它們的值就是用來界定oatdata段和oatexec段的起止位置的。其中,[oatdata, oatexec - 1]描述的是oatdata段的起止位置,而[oatexec, oatlastword + 3]描述的是oatexec的起止位置。要完全理解OAT的檔案格式,除了要理解本文即將要分析的OAT載入過程之外,還需要掌握接下來文章分析的類和方法查詢過程。
在分析OAT檔案的載入過程之前,我們需要簡單介紹一下OAT是如何產生的。如前面Android ART執行時無縫替換Dalvik虛擬機器的過程分析一文所示,APK在安裝的過程中,會通過dex2oat工具生成一個OAT檔案:
static void run_dex2oat(int zip_fd, int oat_fd, const char* input_file_name, const char* output_file_name, const char* dexopt_flags) { static const char* DEX2OAT_BIN = "/system/bin/dex2oat" ; static const int MAX_INT_LEN = 12; // '-'+10dig+'\0' -OR- 0x+8dig char zip_fd_arg[strlen("--zip-fd=") + MAX_INT_LEN]; char zip_location_arg[strlen("--zip-location=") + PKG_PATH_MAX]; char oat_fd_arg[strlen("--oat-fd=") + MAX_INT_LEN]; char oat_location_arg[strlen("--oat-name=") + PKG_PATH_MAX]; sprintf(zip_fd_arg, "--zip-fd=%d", zip_fd); sprintf(zip_location_arg, "--zip-location=%s", input_file_name); sprintf(oat_fd_arg, "--oat-fd=%d", oat_fd); sprintf(oat_location_arg, "--oat-location=%s", output_file_name); ALOGV("Running %s in=%s out=%s\n", DEX2OAT_BIN, input_file_name, output_file_name); execl(DEX2OAT_BIN, DEX2OAT_BIN, zip_fd_arg, zip_location_arg, oat_fd_arg, oat_location_arg, (char*) NULL); ALOGE("execl(%s) failed: %s\n", DEX2OAT_BIN, strerror(errno)); }
這個函式定義在檔案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檔案裡面的classes.dex檔案。實際上,一個OAT檔案是可以由若干個DEX生成的。這意味著在生成的OAT檔案的oatdata段中,包含有多個DEX檔案。那麼,在什麼情況下,會生成包含多個DEX檔案的OAT檔案呢?
從前面Android ART執行時無縫替換Dalvik虛擬機器的過程分析一文可以知道,當我們選擇了ART執行時時,Zygote程序在啟動的過程中,會呼叫libart.so裡面的函式JNI_CreateJavaVM來建立一個ART虛擬機器。函式JNI_CreateJavaVM的實現如下所示:
extern "C" jint JNI_CreateJavaVM(JavaVM** p_vm, JNIEnv** p_env, void* vm_args) { const JavaVMInitArgs* args = static_cast<JavaVMInitArgs*>(vm_args); if (IsBadJniVersion(args->version)) { LOG(ERROR) << "Bad JNI version passed to CreateJavaVM: " << args->version; return JNI_EVERSION; } Runtime::Options options; for (int i = 0; i < args->nOptions; ++i) { JavaVMOption* option = &args->options[i]; options.push_back(std::make_pair(std::string(option->optionString), option->extraInfo)); } bool ignore_unrecognized = args->ignoreUnrecognized; if (!Runtime::Create(options, ignore_unrecognized)) { return JNI_ERR; } Runtime* runtime = Runtime::Current(); bool started = runtime->Start(); if (!started) { delete Thread::Current()->GetJniEnv(); delete runtime->GetJavaVM(); LOG(WARNING) << "CreateJavaVM failed"; return JNI_ERR; } *p_env = Thread::Current()->GetJniEnv(); *p_vm = runtime->GetJavaVM(); return JNI_OK;}
這個函式定義在檔案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,它的實現如下所示:
bool Runtime::Create(const Options& options, bool ignore_unrecognized) { // TODO: acquire a static mutex on Runtime to avoid racing. if (Runtime::instance_ != NULL) { return false; } InitLogging(NULL); // Calls Locks::Init() as a side effect. instance_ = new Runtime; if (!instance_->Init(options, ignore_unrecognized)) { delete instance_; instance_ = NULL; return false; } return true;}
這個函式定義在檔案art/runtime/runtime.cc中。instance_是Runtime類的靜態成員變數,它指向程序中的一個Runtime單例。這個Runtime單例描述的就是當前程序的ART虛擬機器例項。
函式首先判斷當前程序是否已經建立有一個ART虛擬機器例項了。如果有的話,函式就立即返回。否則的話,就建立一個ART虛擬機器例項,並且儲存在Runtime類的靜態成員變數instance_中,最後呼叫Runtime類的成員函式Init對該新建立的ART虛擬機器進行初始化。
Runtime類的成員函式Init的實現如下所示:
bool Runtime::Init(const Options& raw_options, bool ignore_unrecognized) { ...... UniquePtr<ParsedOptions> options(ParsedOptions::Create(raw_options, ignore_unrecognized)); ...... heap_ = new gc::Heap(options->heap_initial_size_, options->heap_growth_limit_, options->heap_min_free_, options->heap_max_free_, options->heap_target_utilization_, options->heap_maximum_size_, options->image_, options->is_concurrent_gc_enabled_, options->parallel_gc_threads_, options->conc_gc_threads_, options->low_memory_mode_, options->long_pause_log_threshold_, options->long_gc_log_threshold_, options->ignore_max_footprint_); ...... java_vm_ = new JavaVMExt(this, options.get()); ...... Thread* self = Thread::Attach("main", false, NULL, false); ...... if (GetHeap()->GetContinuousSpaces()[0]->IsImageSpace()) { class_linker_ = ClassLinker::CreateFromImage(intern_table_); } else { ...... class_linker_ = ClassLinker::CreateFromCompiler(*options->boot_class_path_, intern_table_); } ...... return true;}
這個函式定義在檔案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的實現如下所示:
Runtime::ParsedOptions* Runtime::ParsedOptions::Create(const Options& options, bool ignore_unrecognized) { UniquePtr<ParsedOptions> parsed(new ParsedOptions()); const char* boot_class_path_string = getenv("BOOTCLASSPATH"); if (boot_class_path_string != NULL) { parsed->boot_class_path_string_ = boot_class_path_string; } ...... parsed->is_compiler_ = false; ...... for (size_t i = 0; i < options.size(); ++i) { const std::string option(options[i].first); ...... if (StartsWith(option, "-Xbootclasspath:")) { parsed->boot_class_path_string_ = option.substr(strlen("-Xbootclasspath:")).data(); } else if (option == "bootclasspath") { parsed->boot_class_path_ = reinterpret_cast<const std::vector<const DexFile*>*>(options[i].second); } else if (StartsWith(option, "-Ximage:")) { parsed->image_ = option.substr(strlen("-Ximage:")).data(); } else if (......) { ...... } else if (option == "compiler") { parsed->is_compiler_ = true; } else { ...... } } ...... if (!parsed->is_compiler_ && parsed->image_.empty()) { parsed->image_ += GetAndroidRoot(); parsed->image_ += "/framework/boot.art"; } ...... return parsed.release();}
這個函式定義在檔案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類的建構函式的實現如下所示:
Heap::Heap(size_t initial_size, size_t growth_limit, size_t min_free, size_t max_free, double target_utilization, size_t capacity, const std::string& original_image_file_name, bool concurrent_gc, size_t parallel_gc_threads, size_t conc_gc_threads, bool low_memory_mode, size_t long_pause_log_threshold, size_t long_gc_log_threshold, bool ignore_max_footprint) : ...... { ...... std::string image_file_name(original_image_file_name); if (!image_file_name.empty()) { space::ImageSpace* image_space = space::ImageSpace::Create(image_file_name); ...... AddContinuousSpace(image_space); ...... } ......}
這個函式定義在檔案art/runtime/gc/heap.cc中。ART虛擬機器堆的詳細建立過程我們在後面分析ART虛擬機器的垃圾收集機制時再分析,這裡只關注與Image檔案相關的邏輯。
引數original_image_file_name描述的就是前面提到的Image檔案的路徑。如果它的值不等於空的話,那麼就以它為引數,呼叫ImageSpace類的靜態成員函式Create建立一個Image空間,並且呼叫Heap類的成員函式AddContinuousSpace將該Image空間作為本程序的ART虛擬機器堆的第一個連續空間。
接下來我們繼續分析ImageSpace類的靜態成員函式Create,它的實現如下所示:
ImageSpace* ImageSpace::Create(const std::string& original_image_file_name) { if (OS::FileExists(original_image_file_name.c_str())) { // If the /system file exists, it should be up-to-date, don't try to generate return space::ImageSpace::Init(original_image_file_name, false); } // If the /system file didn't exist, we need to use one from the dalvik-cache. // If the cache file exists, try to open, but if it fails, regenerate. // If it does not exist, generate. std::string image_file_name(GetDalvikCacheFilenameOrDie(original_image_file_name)); if (OS::FileExists(image_file_name.c_str())) { space::ImageSpace* image_space = space::ImageSpace::Init(image_file_name, true); if (image_space != NULL) { return image_space; } } CHECK(GenerateImage(image_file_name)) << "Failed to generate image: " << image_file_name; return space::ImageSpace::Init(image_file_name, true);}
這個函式定義在檔案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/[email protected]@[email protected]是否存在。如果存在,那麼就以它為引數,建立一個Image空間。否則的話,執行下一步。
3. 呼叫ImageSpace類的靜態成員函式GenerateImage在/data/dalvik-cache目錄下生成一個[email protected]@[email protected],然後再以該檔案為引數,建立一個Image空間。
接下來我們再來看看ImageSpace類的靜態成員函式GenerateImage的實現,如下所示:
static bool GenerateImage(const std::string& image_file_name) { const std::string boot_class_path_string(Runtime::Current()->GetBootClassPathString()); std::vector<std::string> boot_class_path; Split(boot_class_path_string, ':', boot_class_path); ...... std::vector<std::string> arg_vector; std::string dex2oat(GetAndroidRoot()); dex2oat += (kIsDebugBuild ? "/bin/dex2oatd" : "/bin/dex2oat"); arg_vector.push_back(dex2oat); std::string image_option_string("--image="); image_option_string += image_file_name; arg_vector.push_back(image_option_string); ...... for (size_t i = 0; i < boot_class_path.size(); i++) { arg_vector.push_back(std::string("--dex-file=") + boot_class_path[i]); } std::string oat_file_option_string("--oat-file="); oat_file_option_string += image_file_name; oat_file_option_string.erase(oat_file_option_string.size() - 3); oat_file_option_string += "oat"; arg_vector.push_back(oat_file_option_string); ...... if (kIsTargetBuild) { arg_vector.push_back("--image-classes-zip=/system/framework/framework.jar"); arg_vector.push_back("--image-classes=preloaded-classes"); } ...... // Convert the args to char pointers. std::vector<char*> char_args; for (std::vector<std::string>::iterator it = arg_vector.begin(); it != arg_vector.end(); ++it) { char_args.push_back(const_cast<char*>(it->c_str())); } char_args.push_back(NULL); // fork and exec dex2oat pid_t pid = fork(); if (pid == 0) { ...... execv(dex2oat.c_str(), &char_args[0]); ...... return false; } else { ...... // wait for dex2oat to finish int status; pid_t got_pid = TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)); ....... } return true;}
這個函式定義在檔案art/runtime/gc/space/image_space.cc中。ImageSpace類的靜態成員函式GenerateImage實際上就呼叫dex2oat工具在/data/dalvik-cache目錄下生成兩個檔案:[email protected]@[email protected]和[email protected]@[email protected]。
[email protected]@[email protected]是一個Image檔案,通過--image選項傳遞給dex2oat工具,裡面包含了一些需要在Zygote程序啟動時預載入的類。這些需要預載入的類由/system/framework/framework.jar檔案裡面的preloaded-classes檔案指定。
[email protected]@[email protected]是一個OAT檔案,通過--oat-file選項傳遞給dex2oat工具,它是由系統啟動路徑中指定的jar檔案生成的。每一個jar檔案都通過一個--dex-file選項傳遞給dex2oat工具。這樣dex2oat工具就可以將它們所包含的classes.dex檔案裡面的DEX位元組碼翻譯成本地機器指令。
這樣,我們就得到了一個包含有多個DEX檔案的OAT檔案[email protected]@[email protected]。
通過上面的分析,我們就清楚地看到了ART執行時所需要的OAT檔案是如何產生的了。其中,由系統啟動類路徑指定的DEX檔案生成的OAT檔案稱為型別為BOOT的OAT檔案,即boot.art檔案。有了這個背景知識之後,接下來我們就繼續分析ART執行時是如何載入OAT檔案的。
ART執行時提供了一個OatFile類,通過呼叫它的靜態成員函式Open可以在本程序中載入OAT檔案,它的實現如下所示:
OatFile* OatFile::Open(const std::string& filename, const std::string& location, byte* requested_base, bool executable) { CHECK(!filename.empty()) << location; CheckLocation(filename);#ifdef ART_USE_PORTABLE_COMPILER // If we are using PORTABLE, use dlopen to deal with relocations. // // We use our own ELF loader for Quick to deal with legacy apps that // open a generated dex file by name, remove the file, then open // another generated dex file with the same name. http://b/10614658 if (executable) { return OpenDlopen(filename, location, requested_base); }#endif // If we aren't trying to execute, we just use our own ElfFile loader for a couple reasons: // // On target, dlopen may fail when compiling due to selinux restrictions on installd. // // On host, dlopen is expected to fail when cross compiling, so fall back to OpenElfFile. // This won't work for portable runtime execution because it doesn't process relocations. UniquePtr<File> file(OS::OpenFileForReading(filename.c_str())); if (file.get() == NULL) { return NULL; } return OpenElfFile(file.get(), location, requested_base, false, executable);}
這個函式定義在檔案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的實現如下所示:
OatFile* OatFile::OpenDlopen(const std::string& elf_filename, const std::string& location, byte* requested_base) { UniquePtr<OatFile> oat_file(new OatFile(location)); bool success = oat_file->Dlopen(elf_filename, requested_base); if (!success) { return NULL; } return oat_file.release();}
這個函式定義在檔案art/runtime/oat_file.cc中。OatFile類的靜態成員函式OpenDlopen首先是建立一個OatFile物件,接著再呼叫該OatFile物件的成員函式Dlopen載入引數elf_filename指定的OAT檔案。
OatFile類的成員函式Dlopen的實現如下所示:
bool OatFile::Dlopen(const std::string& elf_filename, byte* requested_base) { char* absolute_path = realpath(elf_filename.c_str(), NULL); ...... dlopen_handle_ = dlopen(absolute_path, RTLD_NOW); ...... begin_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_, "oatdata")); ...... if (requested_base != NULL && begin_ != requested_base) { ...... return false; } end_ = reinterpret_cast<byte*>(dlsym(dlopen_handle_, "oatlastword")); ...... // Readjust to be non-inclusive upper bound. end_ += sizeof(uint32_t); return Setup();}
這個函式定義在檔案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的實現如下所示:
OatFile* OatFile::OpenElfFile(File* file, const std::string& location, byte* requested_base, bool writable, bool executable) { UniquePtr<OatFile> oat_file(new OatFile(location)); bool success = oat_file->ElfFileOpen(file, requested_base, writable, executable); if (!success) { return NULL; } return oat_file.release();}
這個函式定義在檔案art/runtime/oat_file.cc中。OatFile類的靜態成員函式OpenElfFile建立了一個OatFile物件後,就呼叫它的成員函式ElfFileOpen來執行載入OAT檔案的工作,它的實現如下所示:
bool OatFile::ElfFileOpen(File* file, byte* requested_base, bool writable, bool executable) { elf_file_.reset(ElfFile::Open(file, writable, true)); ...... bool loaded = elf_file_->Load(executable); ...... begin_ = elf_file_->FindDynamicSymbolAddress("oatdata"); ...... if (requested_base != NULL && begin_ != requested_base) { ...... return false; } end_ = elf_file_->FindDynamicSymbolAddress("oatlastword"); ...... // Readjust to be non-inclusive upper bound. end_ += sizeof(uint32_t); return Setup();}
這個函式定義在檔案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的第一部分實現如下所示:
bool OatFile::Setup() { if (!GetOatHeader().IsValid()) { LOG(WARNING) << "Invalid oat magic for " << GetLocation(); return false; } const byte* oat = Begin(); oat += sizeof(OatHeader); if (oat > End()) { LOG(ERROR) << "In oat file " << GetLocation() << " found truncated OatHeader"; return false; }
我們先來看OatFile類的三個成員函式GetOatHeader、Begin和End的實現,如下所示:
const OatHeader& OatFile::GetOatHeader() const { return *reinterpret_cast<const OatHeader*>(Begin());}const byte* OatFile::Begin() const { CHECK(begin_ != NULL); return begin_;}const byte* OatFile::End() const { CHECK(end_ != NULL); return end_;}
這三個函式主要是涉及到了OatFile類的兩個成員變數begin_和end_,它們分別是OAT檔案裡面的oatdata段開始地址和oatexec段的結束地址。通過OatFile類的成員函式GetOatHeader可以清楚地看到,OAT檔案裡面的oatdata段的開始儲存著一個OAT頭,這個OAT頭通過類OatHeader描述,定義在檔案art/runtime/oat.h中,如下所示:
class PACKED(4) OatHeader { public: ...... private: uint8_t magic_[4]; uint8_t version_[4]; uint32_t adler32_checksum_; InstructionSet instruction_set_; uint32_t dex_file_count_; uint32_t executable_offset_; uint32_t interpreter_to_interpreter_bridge_offset_; uint32_t interpreter_to_compiled_code_bridge_offset_; uint32_t jni_dlsym_lookup_offset_; uint32_t portable_resolution_trampoline_offset_; uint32_t portable_to_interpreter_bridge_offset_; uint32_t quick_resolution_trampoline_offset_; uint32_t quick_to_interpreter_bridge_offset_; uint32_t image_file_location_oat_checksum_; uint32_t image_file_location_oat_data_begin_; uint32_t image_file_location_size_; uint8_t image_file_location_data_[0]; // note variable width data at end ......};
類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檔案的oatdata段在記憶體的位置。
image_file_location_oat_checksum_: 用來建立Image空間的OAT檔案的檢驗和。
上述四個成員變數記錄了一個OAT檔案所依賴的用來建立Image空間檔案以及建立這個Image空間檔案所使用的OAT檔案的相關資訊。
通過OatFile類的成員函式Setup的第一部分程式碼的分析,我們就知道了,OAT檔案的oatdata段在最開始儲存著一個OAT頭,如圖2所示:
圖2 OAT頭部
我們接著再看OatFile類的成員函式Setup的第二部分程式碼:
oat += GetOatHeader().GetImageFileLocationSize(); if (oat > End()) { LOG(ERROR) << "In oat file " << GetLocation() << " found truncated image file location: " << reinterpret_cast<const void*>(Begin()) << "+" << sizeof(OatHeader) << "+" << GetOatHeader().GetImageFileLocationSize() << "<=" << reinterpret_cast<const void*>(End()); return false; }
呼叫OatFile類的成員函式GetOatHeader獲得的是正在開啟的OAT檔案的頭部OatHeader,通過呼叫它的成員函式GetImageFileLocationSize獲得的是正在開啟的OAT依賴的Image空間檔案的路徑大小。變數oat最開始的時候指向oatdata段的開始位置。讀出OAT頭之後,變數oat就跳過了OAT頭。由於正在開啟的OAT檔案引用的Image空間檔案路徑儲存在緊接著OAT頭的地方。因此,將Image空間檔案的路徑大小增加到變數oat去後,就相當於是跳過了儲存Image空間檔案路徑的位置。通過OatFile類的成員函式Setup的第二部分程式碼的分析,我們就知道了,緊接著在OAT頭後面的是Image空間檔案路徑,如圖3所示:
圖3 OAT頭和Image空間檔案路徑
我們接著再看OatFile類的成員函式Setup的第三部分程式碼:
for (size_t i = 0; i < GetOatHeader().GetDexFileCount(); i++) { size_t dex_file_location_size = *reinterpret_cast<const uint32_t*>(oat); ...... oat += sizeof(dex_file_location_size); ...... const char* dex_file_location_data = reinterpret_cast<const char*>(oat); oat += dex_file_location_size; ...... std::string dex_file_location(dex_file_location_data, dex_file_location_size); uint32_t dex_file_checksum = *reinterpret_cast<const uint32_t*>(oat); oat += sizeof(dex_file_checksum); ...... uint32_t dex_file_offset = *reinterpret_cast<const uint32_t*>(oat); ...... oat += sizeof(dex_file_offset); ...... const uint8_t* dex_file_pointer = Begin() + dex_file_offset; if (!DexFile::IsMagicValid(dex_file_pointer)) { ...... return false; } if (!DexFile::IsVersionValid(dex_file_pointer)) { ...... return false; } const DexFile::Header* header = reinterpret_cast<const DexFile::Header*>(dex_file_pointer); const uint32_t* methods_offsets_pointer = reinterpret_cast<const uint32_t*>(oat); oat += (sizeof(*methods_offsets_pointer) * header->class_defs_size_); ...... oat_dex_files_.Put(dex_file_location, new OatDexFile(this, dex_file_location, dex_file_checksum, dex_file_pointer, methods_offsets_pointer)); } return true;}
這部分程式碼用來獲得包含在oatdata段的DEX檔案描述資訊。每一個DEX檔案記錄在oatdata段的描述資訊包括:
1. DEX檔案路徑大小,儲存在變數dex_file_location_size中;
2. DEX檔案路徑,儲存在變數dex_file_location_data中;
3. DEX檔案檢驗和