前面写了一篇博客大致描述了一下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;
}
......
代码逻辑很简单,先通过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();
}
接下来,看看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* 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);
}
好,接下来回过来看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';
bool loaded = elf_file_->Load(executable);
if (!loaded) {
LOG(WARNING) << "Failed to load ELF file " << file->GetPath();
return false;
}
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,具体用处后面再解释。
细心的读者有没有发现第四项和第五项的内容除了类型不同外,其它都完全一样。
好了,还是回来看代码。程序会遍历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);
}
那到底要分配多大呢,来看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函数找出答案。
写了太多了,在下篇我们再接着分析。