Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android運行時ART加載OAT文件的過程分析

Android運行時ART加載OAT文件的過程分析

編輯:關於Android編程

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

老羅的新浪微博:http://weibo.com/shengyangluo,歡迎關注!

OAT文件的結構如圖1所示:

\

圖1 OAT文件結構

由於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文件:

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文件裡面的classex.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(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 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 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*>(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/system@[email protected]@classes.dex是否存在。如果存在,那麼就以它為參數,創建一個Image空間。否則的話,執行下一步。

3. 調用ImageSpace類的靜態成員函數GenerateImage在/data/dalvik-cache目錄下生成一個system@[email protected]@classes.dex,然後再以該文件為參數,創建一個Image空間。

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

static bool GenerateImage(const std::string& image_file_name) {
  const std::string boot_class_path_string(Runtime::Current()->GetBootClassPathString());
  std::vector boot_class_path;
  Split(boot_class_path_string, ':', boot_class_path);
  ......

  std::vector 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_args;
  for (std::vector::iterator it = arg_vector.begin(); it != arg_vector.end();
      ++it) {
    char_args.push_back(const_cast(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目錄下生成兩個文件:system@[email protected]@classes.dex和system@[email protected]@classes.oat。

system@[email protected]@classes.dex是一個Image文件,通過--image選項傳遞給dex2oat工具,裡面包含了一些需要在Zygote進程啟動時預加載的類。這些需要預加載的類由/system/framework/framework.jar文件裡面的preloaded-classes文件指定。

system@[email protected]@classes.oat是一個OAT文件,通過--oat-file選項傳遞給dex2oat工具,它是由系統啟動路徑中指定的jar文件生成的。每一個jar文件都通過一個--dex-file選項傳遞給dex2oat工具。這樣dex2oat工具就可以將它們所包含的classes.dex文件裡面的DEX字節碼翻譯成本地機器指令。

這樣,我們就得到了一個包含有多個DEX文件的OAT文件system@[email protected]@classes.oat。

通過上面的分析,我們就清楚地看到了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(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 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(dlsym(dlopen_handle_, "oatdata"));
  ......

  if (requested_base != NULL && begin_ != requested_base) {
    ......
    return false;
  }

  end_ = reinterpret_cast(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的實現如下所示:

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(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文件的偏移位置。

image_file_location_oat_checksum_: 用來創建Image空間的文件的路徑的檢驗和。

上述四個成員變量記錄了一個OAT文件所依賴的用來創建Image空間的文件的路徑。

通過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(Begin())
               << "+" << sizeof(OatHeader)
               << "+" << GetOatHeader().GetImageFileLocationSize()
               << "<=" << reinterpret_cast(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(oat);
    ......

    oat += sizeof(dex_file_location_size);
    ......

    const char* dex_file_location_data = reinterpret_cast(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(oat);
    oat += sizeof(dex_file_checksum);
    ......

    uint32_t dex_file_offset = *reinterpret_cast(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(dex_file_pointer);
    const uint32_t* methods_offsets_pointer = reinterpret_cast(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文件檢驗和,保存在變量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段的位置:

\

圖4 OAT頭、Image空間文件路徑、DEX文件描述信息

為了進一步理解包含在oatdata段的DEX文件描述信息,我們繼續看OatDexFile類的構造函數的實現,如下所示:

OatFile::OatDexFile::OatDexFile(const OatFile* oat_file,
                                const std::string& dex_file_location,
                                uint32_t dex_file_location_checksum,
                                const byte* dex_file_pointer,
                                const uint32_t* oat_class_offsets_pointer)
    : oat_file_(oat_file),
      dex_file_location_(dex_file_location),
      dex_file_location_checksum_(dex_file_location_checksum),
      dex_file_pointer_(dex_file_pointer),
      oat_class_offsets_pointer_(oat_class_offsets_pointer) {}
這個函數定義在文件art/runtime/oat_file.cc中。

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

例如,通過調用OatDexFile類的成員函數GetOatClass可以獲得指定類的所有方法的本地機器指令信息:

const OatFile::OatClass* OatFile::OatDexFile::GetOatClass(uint16_t class_def_index) const {
  uint32_t oat_class_offset = oat_class_offsets_pointer_[class_def_index];

  const byte* oat_class_pointer = oat_file_->Begin() + oat_class_offset;
  CHECK_LT(oat_class_pointer, oat_file_->End()) << oat_file_->GetLocation();
  mirror::Class::Status status = *reinterpret_cast(oat_class_pointer);

  const byte* methods_pointer = oat_class_pointer + sizeof(status);
  CHECK_LT(methods_pointer, oat_file_->End()) << oat_file_->GetLocation();

  return new OatClass(oat_file_,
                      status,
                      reinterpret_cast(methods_pointer));
}
這個函數定義在文件art/runtime/oat_file.cc中。

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

\

圖5 DEX文件裡面的類描述信息

在OAT文件中,每一個DEX文件包含的每一個類的描述信息都通過一個OatClass對象來描述。為了方便描述,我們稱之為OAT類。我們通過OatClass類的構造函數來理解它的作用,如下所示:

OatFile::OatClass::OatClass(const OatFile* oat_file,
                            mirror::Class::Status status,
                            const OatMethodOffsets* methods_pointer)
    : oat_file_(oat_file), status_(status), methods_pointer_(methods_pointer) {}
這個函數定義在文件art/runtime/oat_file.cc中。

參數oat_file描述的是宿主OAT文件,參數status描述的是OAT類狀態,參數methods_pointer是一個數組,描述的是OAT類的各個方法的信息,它們被分別保存在OatClass類的相應成員變量中。通過這些信息,我們就可以獲得包含在該DEX文件裡面的類的所有方法的本地機器指令信息。

例如,通過調用OatClass類的成員函數GetOatMethod可以獲得指定類方法的本地機器指令信息:

const OatFile::OatMethod OatFile::OatClass::GetOatMethod(uint32_t method_index) const {
  const OatMethodOffsets& oat_method_offsets = methods_pointer_[method_index];
  return OatMethod(
      oat_file_->Begin(),
      oat_method_offsets.code_offset_,
      oat_method_offsets.frame_size_in_bytes_,
      oat_method_offsets.core_spill_mask_,
      oat_method_offsets.fp_spill_mask_,
      oat_method_offsets.mapping_table_offset_,
      oat_method_offsets.vmap_table_offset_,
      oat_method_offsets.gc_map_offset_);
}
這個函數定義在文件art/runtime/oat_file.cc中。

參數method_index描述的目標方法在類中的編號,用這個編號作為索引,就可以在OatClass類的成員變量methods_pointer_指向的一個數組中找到目標方法的本地機器指令信息。這些本地機器指令信息封裝在一個OatMethod對象,它們在OAT文件的布局如圖6下所示:

\

圖6 DEX文件裡面的類(OatClass)描述信息

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

OatFile::OatMethod::OatMethod(const byte* base,
                              const uint32_t code_offset,
                              const size_t frame_size_in_bytes,
                              const uint32_t core_spill_mask,
                              const uint32_t fp_spill_mask,
                              const uint32_t mapping_table_offset,
                              const uint32_t vmap_table_offset,
                              const uint32_t gc_map_offset)
  : begin_(base),
    code_offset_(code_offset),
    frame_size_in_bytes_(frame_size_in_bytes),
    core_spill_mask_(core_spill_mask),
    fp_spill_mask_(fp_spill_mask),
    mapping_table_offset_(mapping_table_offset),
    vmap_table_offset_(vmap_table_offset),
    native_gc_map_offset_(gc_map_offset) {
    ......
}
這個函數定義在文件art/runtime/oat_file.cc中。

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

參數base描述的是OAT文件的OAT頭在內存的位置,而參數code_offset描述的是類方法的本地機器指令相對OAT頭的偏移位置。將這兩者相加,就可以得到一個類方法的本地機器指令在內存的位置。我們可以通過調用OatMethod類的成員函數GetCode來獲得這個結果。

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

const void* OatFile::OatMethod::GetCode() const {
  return GetOatPointer(code_offset_);
}
這個函數定義在文件art/runtime/oat_file.cc中。

OatMethod類的成員函數調用另外一個成員函數GetOatPointer來獲得一個類方法的本地機器指令在內存的位置。

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

class OatFile {
  ......

  class OatMethod {
   ......

   private:
    template
    T GetOatPointer(uint32_t offset) const {
      if (offset == 0) {
        return NULL;
      }
      return reinterpret_cast(begin_ + offset);
    }
  
   ......
  };
 
  ......
};
這個函數定義在文件art/runtime/oat_file.h中。

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

\

圖7 在OAT文件中查找類方法的本地機器指令的過程

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

至此,我們就通過OAT文件的加載過程分析完成OAT文件的格式了。為了加深對OAT文件格式的理解,有接下來的一篇文章中,我們再詳細分析上面描述的類方法的本地機器指令的查找過程。敬請關注!更多信息也可以關注老羅的新浪微博:http://weibo.com/shengyangluo。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved