Art 相关文件结构

一、dex 文件结构

dex 文件的结构图如下所示:

这里写图片描述

更多内容请参考博客 Android 虚拟机 — .dex 文件格式

二、odex 文件结构

对于一个 Android 应用程序,其主要的执行代码都在其 apk 的 class.dex 文件中。在程序第一次被加载的时候,为了提高以后的启动速度和执行效率,Android 系统会对这个 class.dex 文件做一定程度的优化,并生成一个 ODEX 文件,存放在 /data/dalvik-cache 目录下。ODEX 文件结构图如下所示:

这里写图片描述

点击查看大图

  • ODEX 文件中包含了一个完整的 DEX 文件,不过在后面的优化步骤中,其中的某些指令会被优化(通过 rewriteDex 函数)
  • 对于每一条依赖库来说,都要写入其优化文件名字符串长度(len)、优化文件名字符串(cacheFileName)、依赖库的 SHA1 值(signature)
  • OptData 中写入了两个数据块(Chunk)以及一个表示结尾的数据块
    • 每个数据块都有一个8字节的头,前4字节表示这个数据块的类型,后4个字节表示这个数据块占用多少字节的空间
    • 类型 kDexChunkClassLookup 用来存放针对该 DEX 文件的 DexClassLookup 结构,它主要是用来帮助快速查找 DEX 中的某个类的。
    • 类型 kDexChunkEnd 用来表示数据块结束了。
    • 类型 kDexChunkRegisterMaps 用来存放针对该 DEX 文件的寄存器图(Register Map)信息,它主要用来帮助 Dalvik 虚拟机做精确 GC 用。

参考博文: Android系统ODEX文件格式解析

三、elf 文件结构

3.1 总体结构

ELF 文件总体结构如下所示:

这里写图片描述

  • ELF: 描述整个文件的组织
  • Program Header Table: 描述文件中的各种 segments,用来告诉系统如何创建进程映像
  • sections 或者 segments: segments 是从运行的角度来描述 elf 文件,也就是说,在链接阶段,我们可以忽略 program header table 来处理此文件,在运行阶段可以忽略 section header table 来处理此程序。从图中我们也可以看出,segments 与 sections 是包含的关系,一个 segments 包含若干个 sections
  • Section Header Table: 包含了文件各个 section 的属性信息

查看 ELF 文件,可以使用 readelf 命令,常用的命令如下所示:

  • readelf -S android_server: 查看该文件中有哪些 section
  • readelf –segments android_server: 查看该文件的 segments 信息
  • readelf -h android_server: 查看 ELF Header 信息

3.2 ELF Header

ELF Header 的结构如下所示:

namesize含义
e_ident[EI_NIDENT]16 * unsigned char目标文件标识,包括魔数等
e_typeELF32_Half目标文件类型
e_machineELF32_Half文件的目标体系结构类型,如 arm
e_versionELF32_Word文件的版本
e_entryELF32__Addr程序入口的虚拟地址
e_phoffELF32_OffPHT 偏移量
e_shoffELF32_OffSHT 偏移量
e_flagsELF32_Word文件的 flags
e_ehsizeELF32_HalfELF Header 的 size
e_phentsizeELF32_HalfProgram Header Entry size
e_phnumELF32_HalfProgram Header Entry num
e_shentsizeELF32_HalfSection Header Entry size
e_shnumELF32_HalfSection Header Entry num
e_shstrndxELF32_Half字符串表在 Section Header Table 中的索引

3.3 Section Header Table

一个 ELF 文件中到底有哪些具体的 sections,由包含在这个 ELF 文件中的 section header table(SHT)决定。在 SHT 中,针对每一个 section,都设置有一个条目(entry),用来描述对应的这个 section,entry 的结构如下所示:

namesize含义
sh_nameElf32_WordSection 的名字
sh_typeElf32_WordSection 的类别
sh_flagsElf32_WordSection 在进程中执行的特性 (读、写等)
sh_addrElf32_AddrSection 在内存中开始的虚地址
sh_offsetElf32_OffSection 在文件中的偏移
sh_sizeElf32_WordSection 的 size
sh_linkElf32_Word
sh_infoElf32_Word
sh_addralignElf32_Word
sh_entsizeElf32_Word
  • sh_name 值实际上是 .shstrtab 中的索引,.shstrtab 中存储着所有 section 的名字

3.4 Program Header Table

namesize含义
p_typeElf32_Word段的类型
p_offsetElf32_Off从文件头到该段第一个字节的偏移
p_vaddrElf32_Addr段的第一个字节将被放到内存中的虚拟地址
p_paddrElf32_Addr仅用于与物理地址相关的系统中
p_fileszElf32_Word段在文件映像中所占的字节数,可以为0
p_memszElf32_Word段在内存映像中占用的字节数,可以为0
p_flagsElf32_Word与段相关的标志
p_alignElf32_Word段在文件中和内存中如何对齐


参考博客ELF文件格式解析
参考规范一种ELF参考规范的文档

四、oat 文件结构

OAT 文件是一种 Android 私有 ELF 文件格式,它不仅包含有从 DEX 文件翻译而来的本地机器指令,还包含有原来的 DEX 文件内容

4.1 OAT 头

OAT 文件里面的 oatdata 段的开始储存着一个 OAT 头,这个 OAT 头通过类 OatHeader 描述(定义在文件 art/runtime/oat.h 中),如下所示:

name类型含义
magic_[4]uint8_t魔数,等于 ‘oat\n’
version_[4]uint8_tOAT 文件版本号
adler32_checksum_uint32_tOAT 头部检验和
instruction_set_InstructionSet本地机指令集,有四种取值,分别为 kArm(1)、kThumb2(2)、kX86(3)和kMips(4)
dex_file_count_uint32_tOAT 文件包含的 DEX 文件个数
executable_offset_uint32_toatexec 段开始位置与 oatdata 段开始位置的偏移值
interpreter_to_interpreter_bridge_offset_uint32_t
interpreter_to_compiled_code_bridge_offset_uint32_t
jni_dlsym_lookup_offset_uint32_t类方法在执行的过程中,如果要调用另外一个方法是一个 JNI 函数,那么就要通过存在放置 jni_dlsym_lookup_offset_ 的一段 trampoline 代码来调用
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 空间的 OAT 文件的检验和
image_file_location_oat_data_begin_uint32_t用来创建 Image 空间的 OAT 文件的 oatdata 段在内存的位置
image_file_location_size_uint32_t用来创建 Image 空间的文件的路径的大小
image_file_location_data_uint32_t用来创建 Image 空间的文件的路径的在内存中的地址
  • interpreter_to_interpreter_bridge_offset_ 和 interpreter_to_compiled_code_bridge_offset_: OAT 文件在内部提供有两段 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 代码。

4.2 加载 OAT 文件过程

  如果编译时指定了ART_USE_PORTABLE_COMPILER宏,并且参数executable为true,那么就通过OatFile类的静态成员函数OpenDlopen来加载指定的OAT文件。OatFile类的静态成员函数OpenDlopen直接通过动态链接器提供的dlopen函数来加载OAT文件。
  其余情况下,通过OatFile类的静态成员函数OpenElfFile来手动加载指定的OAT文件。这种方式是按照ELF文件格式来解析要加载的OAT文件的,并且根据解析获得的信息将OAT里面相应的段加载到内存中来。
  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();  
}  

从上面可以看到此函数创建了一个 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();  
}  

通过两个导出符号 oatdata 和 oatlastword 来获得 oatdata 段和 oatexec 段的起止位置。如果参数requested_base的值不等于0,那么就要求oatdata段必须要加载到requested_base指定的位置去。
将参数 file 指定的 OAT 文件加载到内存之后,OatFile 类的静态成员函数 OpenElfFile 最后也是调用 OatFile 类的成员函数 Setup 来解析其中的 oatdata 段。我们分三部分来阅读,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 类的成员函数 Setup 的第一部分代码的分析,我们就知道了,OAT 文件的 oatdata 段在最开始保存着一个 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 空间文件路径。
  我们接着再看OatFile类的成员函数Setup的第三部分代码:

  for (size_t i = 0; i < GetOatHeader().GetDexFileCount(); i++) {
    // 第1个值,DEX文件路径大小
    size_t dex_file_location_size = *reinterpret_cast<const uint32_t*>(oat);  
    ......  

    oat += sizeof(dex_file_location_size);  
    ......  
    // 第2个值,DEX文件路径
    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);  
    // 第3个值,DEX文件检验和
    uint32_t dex_file_checksum = *reinterpret_cast<const uint32_t*>(oat);  
    oat += sizeof(dex_file_checksum);  
    ......  
    // 第4个值,DEX文件内容在oatdata段的偏移
    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;
    // 检测是否是一个 DEX 文件
    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 段的描述信息包括:

  • DEX 文件路径大小,保存在变量 dex_file_location_size 中;
  • DEX 文件路径,保存在变量 dex_file_location_data 中;
  • DEX 文件检验和,保存在变量 dex_file_checksum 中;
  • DEX 文件内容在 oatdata 段的偏移,保存在变量 dex_file_offset 中;
  • DEX 文件包含的类的本地机器指令信息偏移数组,保存在变量 methods_offsets_pointer 中;

通过第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 文件的描述信息,那么就可以通过下图看到这些描述信息在 oatdata 段的位置:

这里写图片描述

4.3 获得本地机器指令信息过程

为了进一步理解包含在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) {}  

  OatDexFile 类的构造函数的实现很简单,它将我们在上面得到的 DEX 文件描述息保存在相应的成员变量中。通过这些信息,我们就可以获得包含在该 DEX 文件里面的类的所有方法的本地机器指令信息。
  例如,通过调用 OatDexFile 类的成员函数 GetOatClass 可以获得指定类的所有方法的本地机器指令信息:

const OatFile::OatClass* OatFile::OatDexFile::GetOatClass(uint16_t class_def_index) const {
  // 获得 class_def_index 指示类的所有方法的本地机器指令信息的偏移
  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();
  // 从偏移处先提取出 status 信息
  mirror::Class::Status status = *reinterpret_cast<const mirror::Class::Status*>(oat_class_pointer);  
  // 得到 methods 信息指针
  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<const OatMethodOffsets*>(methods_pointer));  
} 

在 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) {}  

参数 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_);  
} 

  参数 method_index 描述的目标方法在类中的编号,用这个编号作为索引,就可以在 OatClass 类的成员变量 methods_pointer_ 指向的一个数组中找到目标方法的本地机器指令信息。这些本地机器指令信息封装在一个 OatMethod 对象。
  为了进一步理解 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) {  
    ......  
}  

  参数 base 描述的是 OAT 文件的 OAT 头在内存的位置,而参数 code_offset 描述的是类方法的本地机器指令相对 OAT 头的偏移位置。将这两者相加,就可以得到一个类方法的本地机器指令在内存的位置。我们可以通过调用 OatMethod 类的成员函数 GetCode 来获得这个结果。
  OatMethod 类的成员函数 GetCode 的实现如下所示:

const void* OatFile::OatMethod::GetCode() const {  
  return GetOatPointer<const void*>(code_offset_);  
}  

  通过上面对 OAT 文件加载过程的分析,我们就可以清楚地看到 OAT 文件的格式,以及如何在 OAT 文件中找到一个类方法的本地机器指令。我们通过下图来总结在 OAT 文件中找到一个类方法的本地机器指令的过程:

这里写图片描述

4.4 OAT 文件结构总结

OAT文件结构图如下所示:

这里写图片描述

参考博客 Android运行时ART加载OAT文件的过程分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值