Android ART Oat文件格式简析(上)

前面写了一篇博客大致描述了一下Image文件的结构,本文将接下来简单描述一下Oat文件的大致结构。

和前面一样,还是来看一下代码,代码非常复杂,为了保证大家不分心,我会尽量去除一些冗余的部分,只留下主体部分。

好了,Oat文件的读取是通过OatFile::ElfFileOpen(art\runtime\Oat_file.cc)进行的:

bool OatFile::ElfFileOpen(File* file, byte* requested_base, bool writable, bool executable) {
  elf_file_.reset(ElfFile::Open(file, writable, true));
  if (elf_file_.get() == NULL) {
    if (writable) {
      PLOG(WARNING) << "Failed to open ELF file for " << file->GetPath();
    }
    return false;
  }
  ......

咦,不是oat文件吗?怎么冒出来了elf文件?其实所谓的oat文件是影藏在elf文件中的,先卖个关子,后面再具体说明。

代码逻辑很简单,先通过ElfFile::Open函数打开文件,然后设置到成员变量elf_file_中,如果为空代表加载elf文件失败,直接退出。

下面看看ElfFile::Open函数(art\runtime\Elf_file.cc):

ElfFile* ElfFile::Open(File* file, bool writable, bool program_header_only) {
  UniquePtr elf_file(new ElfFile());
  if (!elf_file->Setup(file, writable, program_header_only)) {
    return NULL;
  }
  return elf_file.release();
}

也很简单,创建一个elf_file实例,然后调用其Setup函数,如果失败的话返回NULL,如果成功就返回创建的elf_file实例。

接下来,看看ElfFile::Setup函数的实现:

bool ElfFile::Setup(File* file, bool writable, bool program_header_only) {
  CHECK(file != NULL);
  file_ = file;
  writable_ = writable;
  program_header_only_ = program_header_only;

  int prot;
  int flags;
  if (writable_) {
    prot = PROT_READ | PROT_WRITE;
    flags = MAP_SHARED;
  } else {
    prot = PROT_READ;
    flags = MAP_PRIVATE;
  }
  int64_t file_length = file_->GetLength();
  if (file_length < 0) {
    errno = -file_length;
    PLOG(WARNING) << "Failed to get length of file: " << file_->GetPath() << " fd=" << file_->Fd();
    return false;
  }
  if (file_length < sizeof(llvm::ELF::Elf32_Ehdr)) {
    if (writable) {
      LOG(WARNING) << "File size of " << file_length
                   << " bytes not large enough to contain ELF header of "
                   << sizeof(llvm::ELF::Elf32_Ehdr) << " bytes: " << file_->GetPath();
    }
    return false;
  }

  if (program_header_only) {
    // first just map ELF header to get program header size information
    size_t elf_header_size = sizeof(llvm::ELF::Elf32_Ehdr);
    if (!SetMap(MemMap::MapFile(elf_header_size, prot, flags, file_->Fd(), 0))) {
      return false;
    }
    // then remap to cover program header
    size_t program_header_size = header_->e_phoff + (header_->e_phentsize * header_->e_phnum);
    if (file_length < program_header_size) {
      LOG(WARNING) << "File size of " << file_length
                   << " bytes not large enough to contain ELF program header of "
                   << program_header_size << " bytes: " << file_->GetPath();
      return false;
    }
    if (!SetMap(MemMap::MapFile(program_header_size, prot, flags, file_->Fd(), 0))) {
      LOG(WARNING) << "Failed to map ELF program headers: " << file_->GetPath();
      return false;
    }
  } else {
    ......
    }
  }

  // Either way, the program header is relative to the elf header
  program_headers_start_ = Begin() + GetHeader().e_phoff;

  if (!program_header_only) {
     ......
  }
  return true;
}
传进来的三个参数,第一个参数file表示要打开的elf文件,第二个参数writable表示映射到内存中后是否允许写入,第三个参数program_header_only表示是否只映射elf的Program Headers,当然elf头是无论如何要映射的。这个函数比较大,为了便于分析,由于传进来的参数program_header_only是true,我删除了代码没有走到的地方。

程序首先判断是否允许写入,并且设置相应的参数,这些都是mmap要用到的。然后是一些错误判断,如文件大小不能为负数,并且文件头的大小不能大于文件整体的长度。下面就是动真格的了,先计算一下elf头的大小,这个值是固定的,然后把这个elf头映射到内存中。

接下来计算elf头包含了Program Header后的长度,因为elf头中记录了Program Header相对于文件初始位置的偏移(e_phoff),每一项所占据空间的大小(e_phentsize)和有多少项组成(e_phnum),并且Program Header肯定是接在elf头之后的,所以非常好计算。

再接下来,又重新映射了一下MemMap,把Program Header包含了进来。为什么要映射两次呢?答案很简单,因为不读elf头的话,没法知道Program Header到底有多长。

最后初始化了成员变量programs_headers_start_,很简单,就是文件起始地址加上Program Header偏移。初始化后,ElfFile::GetProgramHeader函数才可以用,后面会用到。

那到底是怎么映射的呢?从代码来看,具体是通过MemMap::MapFile(art\runtime\Mem_map.cc)来映射的,然后调用SetMap来设置自己的成员变量。首先来看MemMap::MapFile:

static MemMap* MapFile(size_t byte_count, int prot, int flags, int fd, off_t start) {
    return MapFileAtAddress(NULL, byte_count, prot, flags, fd, start, false);
  }

直接调用自己的MemMap::MapFileAtAddress,但是第一个参数传NULL,也就是无所谓映射到哪,接下来看MemMap::MapFileAtAddress:

MemMap* MemMap::MapFileAtAddress(byte* addr, size_t byte_count,
                                 int prot, int flags, int fd, off_t start, bool reuse) {
  CHECK_NE(0, prot);
  CHECK_NE(0, flags & (MAP_SHARED | MAP_PRIVATE));
  if (byte_count == 0) {
    return new MemMap("file", NULL, 0, NULL, 0, prot);
  }
  // Adjust 'offset' to be page-aligned as required by mmap.
  int page_offset = start % kPageSize;
  off_t page_aligned_offset = start - page_offset;
  // Adjust 'byte_count' to be page-aligned as we will map this anyway.
  size_t page_aligned_byte_count = RoundUp(byte_count + page_offset, kPageSize);
  // The 'addr' is modified (if specified, ie non-null) to be page aligned to the file but not
  // necessarily to virtual memory. mmap will page align 'addr' for us.
  byte* page_aligned_addr = (addr == NULL) ? NULL : (addr - page_offset);
  if (!reuse) {
    // reuse means it is okay that it overlaps an existing page mapping.
    // Only use this if you actually made the page reservation yourself.
    CheckMapRequest(page_aligned_addr, page_aligned_byte_count);
  } else {
    CHECK(addr != NULL);
  }
  byte* actual = reinterpret_cast(mmap(page_aligned_addr,
                                              page_aligned_byte_count,
                                              prot,
                                              flags,
                                              fd,
                                              page_aligned_offset));
  if (actual == MAP_FAILED) {
    std::string maps;
    ReadFileToString("/proc/self/maps", &maps);
    PLOG(ERROR) << "mmap(" << reinterpret_cast(page_aligned_addr)
                << ", " << page_aligned_byte_count
                << ", " << prot << ", " << flags << ", " << fd << ", " << page_aligned_offset
                << ") failed\n" << maps;
    return NULL;
  }
  return new MemMap("file", actual + page_offset, byte_count, actual, page_aligned_byte_count,
                    prot);
}

首先还是做一些出错判断,接下来计算映射到内存中的真正大小。不是传入文件大小了嘛,为什么还要计算要映射的大小?因为内存映射是以页为单位的,如果要映射的地址不是4K对齐的,就需要重新结算大小。扯远了,接下来就真的调用mmap,把文件映射到内存,返回的actual表示实际上映射到的内存地址,然后创建一个MemMap实例(MemMap::MapFileAtAddress函数是静态的),并初始化各个变量,然后返回。这样看来,MemMap其实就是代表了从文件映射到内存中的一段空间以及其属性。

好,接下来回过来看ElfFile::SetMap:

bool ElfFile::SetMap(MemMap* map) {
  if (map == NULL) {
    // MemMap::Open should have already logged
    return false;
  }
  map_.reset(map);
  CHECK(map_.get() != NULL) << file_->GetPath();
  CHECK(map_->Begin() != NULL) << file_->GetPath();

  header_ = reinterpret_cast(map_->Begin());
  if ((llvm::ELF::ElfMagic[0] != header_->e_ident[llvm::ELF::EI_MAG0])
      || (llvm::ELF::ElfMagic[1] != header_->e_ident[llvm::ELF::EI_MAG1])
      || (llvm::ELF::ElfMagic[2] != header_->e_ident[llvm::ELF::EI_MAG2])
      || (llvm::ELF::ElfMagic[3] != header_->e_ident[llvm::ELF::EI_MAG3])) {
    LOG(WARNING) << "Failed to find ELF magic in " << file_->GetPath()
                 << ": " << std::hex
                 << static_cast(header_->e_ident[llvm::ELF::EI_MAG0])
                 << static_cast(header_->e_ident[llvm::ELF::EI_MAG1])
                 << static_cast(header_->e_ident[llvm::ELF::EI_MAG2])
                 << static_cast(header_->e_ident[llvm::ELF::EI_MAG3]);
    return false;
  }

  ......
  
  return true;
}
非常简单,无非就是设置自己的成员变量map_到上面新创建的MemMap,将成员变量header_指到加载进来的elf头的位置,并检查最初的四个字节是不是符合elf的magic code。所以,必须要SetMap之后,ElfFile的很多函数才可以用,否则header_指向的值是不对的。

 而这里的magic code就是标准的elf文件标识,即:

llvm::ELF::ElfMagic[0] = 0x7f;
llvm::ELF::ElfMagic[1] = 'E';
llvm::ELF::ElfMagic[2] = 'L';
llvm::ELF::ElfMagic[3] = 'F';

好了,绕了一大圈,其实就是把elf的头以及Program Header加载进了内存,我们回到OatFile::ElfFileOpen接着分析:

  bool loaded = elf_file_->Load(executable);
  if (!loaded) {
    LOG(WARNING) << "Failed to load ELF file " << file->GetPath();
    return false;
  }

接下来主要是调用了ElfFile::Load函数,如果失败就返回false。好接下来看ElfFile::Load:

bool ElfFile::Load(bool executable) {
  CHECK(program_header_only_) << file_->GetPath();
  for (llvm::ELF::Elf32_Word i = 0; i < GetProgramHeaderNum(); i++) {
    llvm::ELF::Elf32_Phdr& program_header = GetProgramHeader(i);

    // Record .dynamic header information for later use
    if (program_header.p_type == llvm::ELF::PT_DYNAMIC) {
      dynamic_program_header_ = &program_header;
      continue;
    }

    // Not something to load, move on.
    if (program_header.p_type != llvm::ELF::PT_LOAD) {
      continue;
    }

    int64_t file_length = file_->GetLength();
    if (program_header.p_vaddr == 0) {
      std::string reservation_name("ElfFile reservation for ");
      reservation_name += file_->GetPath();
      UniquePtr reserve(MemMap::MapAnonymous(reservation_name.c_str(),
                                                     NULL, GetLoadedSize(), PROT_NONE));
      CHECK(reserve.get() != NULL) << file_->GetPath();
      base_address_ = reserve->Begin();
      segments_.push_back(reserve.release());
    }
    // empty segment, nothing to map
    if (program_header.p_memsz == 0) {
      continue;
    }
    ......
代码很长,先只列出前面的一部分。可以看出程序先获得Program Header内到底有多少项,然后逐项遍历。为了便于理解,我挑了一个例子,就是系统的Boot Oat(system@[email protected])文件,用readelf可以得到其Program Header如下:


可以看到其一共包含5项,第一项表示的是Program Header本身在要映射到内存中的大小和位置。第二项到第四项是三个要加载进内存的段,为什么要分三个?因为他们的属性不同。最后一个指向的是动态节区,其有很多项组成。其它的项先不管,有一个特别重要的项是SYMTAB,即符号表节,这个节中记录了几个符号已经其对应的属性和值。对于内含oat的elf文件来说,一共有三个非常重要的符号分别是oatdata,oatexec和oatlastword,具体用处后面再解释。

Android ART Oat文件格式简析(上)_第1张图片


细心的读者有没有发现第四项和第五项的内容除了类型不同外,其它都完全一样。

好了,还是回来看代码。程序会遍历Program Header中的所有项。对各项,首先,看该该项是不是PT_DYNAMIC类型的(第五项),如果是就用来初始化dynamic_program_header_成员变量。奇怪的是直接就赋值了,没有把其指定的段加载进内存中?答案其实前面提到过了,在处理第四项的时候已经将其加载进内存中的,而第五项和第四项所有属性都一样。

接下来,判断该项的类型是不是PT_LOAD,如果不是,直接忽略,即非PT_LOAD类型的段不需要加载。

再下来,判断了一下该项指定的虚拟地址是否是0。如果为0的话,就调用MemMap::MapAnonymous来加载。看看前面的例子,咦,怎么没有虚拟地址为0的段?我们再随便写 一个HelloWorld程序,装上手机,拿出转换过后的oat文件看看,通过readelf,其Program Header如下:


看到第二项了吧,真的有一个PT_LOAD段的虚拟地址指定的是0。顺便说一句,虚拟地址为0的项一定是Program Header各个PT_LOAD类型项的第一项。那么有虚拟地址为0的段和没有虚拟地址为0的段,处理起来有什么区别呢?

首先,我们看看有的情况,接着看MemMap::MapAnonymous:

MemMap* MemMap::MapAnonymous(const char* name, byte* addr, size_t byte_count, int prot) {
  if (byte_count == 0) {
    return new MemMap(name, NULL, 0, NULL, 0, prot);
  }
  size_t page_aligned_byte_count = RoundUp(byte_count, kPageSize);
  CheckMapRequest(addr, page_aligned_byte_count);

#ifdef USE_ASHMEM
  // android_os_Debug.cpp read_mapinfo assumes all ashmem regions associated with the VM are
  // prefixed "dalvik-".
  std::string debug_friendly_name("dalvik-");
  debug_friendly_name += name;
  ScopedFd fd(ashmem_create_region(debug_friendly_name.c_str(), page_aligned_byte_count));
  int flags = MAP_PRIVATE;
  if (fd.get() == -1) {
    PLOG(ERROR) << "ashmem_create_region failed (" << name << ")";
    return NULL;
  }
#else
  ScopedFd fd(-1);
  int flags = MAP_PRIVATE | MAP_ANONYMOUS;
#endif

  byte* actual = reinterpret_cast(mmap(addr, page_aligned_byte_count, prot, flags, fd.get(), 0));
  if (actual == MAP_FAILED) {
    std::string maps;
    ReadFileToString("/proc/self/maps", &maps);
    PLOG(ERROR) << "mmap(" << reinterpret_cast(addr) << ", " << page_aligned_byte_count
                << ", " << prot << ", " << flags << ", " << fd.get() << ", 0) failed for " << name
                << "\n" << maps;
    return NULL;
  }
  return new MemMap(name, actual, byte_count, actual, page_aligned_byte_count, prot);
}

这个函数比较容易理解,不多做解释。传进来的参数addr为NULL,所以调用过后的作用其实就是在内存中预留了一段内存段,并且起始地址没指定,表明可以分配到任何地方。

那到底要分配多大呢,来看ElfFile::GetLoadedSize:

size_t ElfFile::GetLoadedSize() {
  llvm::ELF::Elf32_Addr min_vaddr = 0xFFFFFFFFu;
  llvm::ELF::Elf32_Addr max_vaddr = 0x00000000u;
  for (llvm::ELF::Elf32_Word i = 0; i < GetProgramHeaderNum(); i++) {
    llvm::ELF::Elf32_Phdr& program_header = GetProgramHeader(i);
    if (program_header.p_type != llvm::ELF::PT_LOAD) {
      continue;
    }
    llvm::ELF::Elf32_Addr begin_vaddr = program_header.p_vaddr;
    if (begin_vaddr < min_vaddr) {
       min_vaddr = begin_vaddr;
    }
    llvm::ELF::Elf32_Addr end_vaddr = program_header.p_vaddr + program_header.p_memsz;
    if (end_vaddr > max_vaddr) {
      max_vaddr = end_vaddr;
    }
  }
  min_vaddr = RoundDown(min_vaddr, kPageSize);
  max_vaddr = RoundUp(max_vaddr, kPageSize);
  CHECK_LT(min_vaddr, max_vaddr) << file_->GetPath();
  size_t loaded_size = max_vaddr - min_vaddr;
  return loaded_size;
}
该程序遍历所有Program Header项,略去所有PT_LOAD段,计算剩下的所有项中起始地址最小的,还有结束地址加上该段长度后最大的地址,然后按照页大小调整一下,最后用得到的最大值减去最小值。其实很简单了,就是该elf文件的全部大小。

所以如果有虚拟地址为0的段话,就是先按照文件的大小随便在内存中映射一段内存,不指定起始地址,然后设置变量base_address_为该段的起始地址。那么如果没有虚拟地址为0的段,会怎么样呢?这是base_address_就是0。好,接着看剩下的部分:

    byte* p_vaddr = base_address_ + program_header.p_vaddr;
    int prot = 0;
    if (executable && ((program_header.p_flags & llvm::ELF::PF_X) != 0)) {
      prot |= PROT_EXEC;
    }
    if ((program_header.p_flags & llvm::ELF::PF_W) != 0) {
      prot |= PROT_WRITE;
    }
    if ((program_header.p_flags & llvm::ELF::PF_R) != 0) {
      prot |= PROT_READ;
    }
    int flags = MAP_FIXED;
    if (writable_) {
      prot |= PROT_WRITE;
      flags |= MAP_SHARED;
    } else {
      flags |= MAP_PRIVATE;
    }
    if (file_length < (program_header.p_offset + program_header.p_memsz)) {
      LOG(WARNING) << "File size of " << file_length
                   << " bytes not large enough to contain ELF segment " << i
                   << " of " << (program_header.p_offset + program_header.p_memsz)
                   << " bytes: " << file_->GetPath();
      return false;
    }
    UniquePtr segment(MemMap::MapFileAtAddress(p_vaddr,
                                                       program_header.p_memsz,
                                                       prot, flags, file_->Fd(),
                                                       program_header.p_offset,
                                                       true));
    CHECK(segment.get() != NULL) << file_->GetPath();
    CHECK_EQ(segment->Begin(), p_vaddr) << file_->GetPath();
    segments_.push_back(segment.release());
  }
这下就非常简单了,很容易懂,MemMap::MapFileAtAddress前面分析过。

好,讲了那么多,大致总结一下。

1)如果elf文件中包含了虚拟地址为0的PT_LOAD段,则证明它不是Boot Oat,而是一个普通的应用程序的oat,这时候该elf文件无所谓被映射到内存中的任何地方,其虚拟地址(p_vaddr)中记录的是该段相对于文件头的偏移;

2)如果elf文件中没有包含任何虚拟地址为0的PT_LOAD段,则证明它是一个Boot Oat,必须被加载到一个指定位置(实际是紧接在Image之后),其虚拟地址(p_vaddr)中记录的就是实际要被加载的绝对地址。接下来,ElfFile::Load函数会初始化自己的内部变量,指向各个特殊节的位置:

  dynamic_section_start_
      = reinterpret_cast(base_address_ + GetDynamicProgramHeader().p_vaddr);
  for (llvm::ELF::Elf32_Word i = 0; i < GetDynamicNum(); i++) {
    llvm::ELF::Elf32_Dyn& elf_dyn = GetDynamic(i);
    byte* d_ptr = base_address_ + elf_dyn.d_un.d_ptr;
    switch (elf_dyn.d_tag) {
      case llvm::ELF::DT_HASH: {
        hash_section_start_ = reinterpret_cast(d_ptr);
        break;
      }
      case llvm::ELF::DT_STRTAB: {
        dynstr_section_start_ = reinterpret_cast(d_ptr);
        break;
      }
      case llvm::ELF::DT_SYMTAB: {
        dynsym_section_start_ = reinterpret_cast(d_ptr);
        break;
      }
      case llvm::ELF::DT_NULL: {
        CHECK_EQ(GetDynamicNum(), i+1);
        break;
      }
    }
  }

  return true;
}
好了,到现在位置,elf文件已经被完全加载到内存中了,并且所有环境变量都已经初始化完成。那下面要干什么呢?我们接着往下看OatFile::ElfFileOpen:

  begin_ = elf_file_->FindDynamicSymbolAddress("oatdata");
  if (begin_ == NULL) {
    LOG(WARNING) << "Failed to find oatdata symbol in " << file->GetPath();
    return false;
  }
  if (requested_base != NULL && begin_ != requested_base) {
    std::string maps;
    ReadFileToString("/proc/self/maps", &maps);
    LOG(WARNING) << "Failed to find oatdata symbol at expected address: oatdata="
                 << reinterpret_cast(begin_) << " != expected="
                 << reinterpret_cast(requested_base)
                 << " /proc/self/maps:\n" << maps;
    return false;
  }
  end_ = elf_file_->FindDynamicSymbolAddress("oatlastword");
  if (end_ == NULL) {
    LOG(WARNING) << "Failed to find oatlastword symbol in " << file->GetPath();
    return false;
  }
  // Readjust to be non-inclusive upper bound.
  end_ += sizeof(uint32_t);
  return Setup();
}
看到oatdata和oatlastword了吧,这段代码就是初始化了begin_和end_两个内部变量,分别是oatdata和oatlastword指向的地址。对于前面的Boot Oat文件来说,其值分别为0x60a9d000和0x646161a4。后面还把end_的位置加了4个字节,这是因为oatlastword长度被指定为4个字节,位置又在文件的末尾,必须得加上才真的是指向文件的末尾。那begin_和end_到底被指向了哪里呢?可以通过函数最后调用的ElfFile::Setup函数找出答案。

写了太多了,在下篇我们再接着分析。

你可能感兴趣的:(Android)