摘要
接上一篇文章:【个人笔记一】ART系统类的编译解析加载探究
- 在ART上用YAHFA、Legend以及一个java层实现的Andix: http://weishu.me/2017/03/20/dive-into-art-hello-world/,发现除了framework层的类(如telephonymanager)和应用中的类有效外,对于java核心库的类(如IOBridge和Class等)的hook都无效,所以我就以telephonymanager和IOBridge这两个类为例,试图从编译解析加载等角度分析这两者的区别以及造成hook结果不同的原因,如果有大牛能指点一二的话,不胜感激。。。
上次讲了系统如何生成boot.art和boot.oat文件,并没有发现telephonymanager和IOBridge这两个类有什么区别,都是打包进了boot文件,所以接下来从boot.oat文件的加载解析入手,进一步分析原因。
首先看整体流程图:
当调用ImageSpace::Init,继续调用ImageSpace::OpenOatFile,从而调用了OatFile::Open方法进行下一步调用,所以我们的分析也从OatFile::Open开始:
OatFile* OatFile::Open(const std::string& filename,
const std::string& location,
uint8_t* requested_base,
uint8_t* oat_file_begin,
bool executable,
const char* abs_dex_location,
std::string* error_msg) {
CHECK(!filename.empty()) << location;
CheckLocation(location);
std::unique_ptr<OatFile> ret;
// Use dlopen only when flagged to do so, and when it's OK to load things executable.
// TODO: Also try when not executable? The issue here could be re-mapping as writable (as
// !executable is a sign that we may want to patch), which may not be allowed for
// various reasons.
if (kUseDlopen && (kIsTargetBuild || kUseDlopenOnHost) && executable) {
// Try to use dlopen. This may fail for various reasons, outlined below. We try dlopen, as
// this will register the oat file with the linker and allows libunwind to find our info.
ret.reset(OpenDlopen(filename, location, requested_base, abs_dex_location, error_msg));
if (ret.get() != nullptr) {
return ret.release();
}
if (kPrintDlOpenErrorMessage) {
LOG(ERROR) << "Failed to dlopen: " << *error_msg;
}
}
// 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.
//
// 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
//
// On host, dlopen is expected to fail when cross compiling, so fall back to OpenElfFile.
//
//
// Another independent reason is the absolute placement of boot.oat. dlopen on the host usually
// does honor the virtual address encoded in the ELF file only for ET_EXEC files, not ET_DYN.
std::unique_ptr<File> file(OS::OpenFileForReading(filename.c_str()));
if (file == nullptr) {
*error_msg = StringPrintf("Failed to open oat filename for reading: %s", strerror(errno));
return nullptr;
}
ret.reset(OpenElfFile(file.get(), location, requested_base, oat_file_begin, false, executable,
abs_dex_location, error_msg));
// It would be nice to unlink here. But we might have opened the file created by the
// ScopedLock, which we better not delete to avoid races. TODO: Investigate how to fix the API
// to allow removal when we know the ELF must be borked.
return ret.release();
}
这个函数定义在文件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的实现了:
- 如果编译时指定了ART_USE_PORTABLE_COMPILER宏,并且参数executable为true,那么就通过OatFile类的静态成员函数OpenDlopen来加载指定的OAT文件。OatFile类的静态成员函数OpenDlopen直接通过动态链接器提供的dlopen函数来加载OAT文件。
- 其余情况下,通过OatFile类的静态成员函数OpenElfFile来手动加载指定的OAT文件。这种方式是按照ELF文件格式来解析要加载的OAT文件的,并且根据解析获得的信息将OAT里面相应的段加载到内存中来。
接下来我们就分别看看OatFile类的静态成员函数OpenDlopen和OpenElfFile的实现,以便可以对OAT文件有更清楚的认识。
OatFile类的静态成员函数OpenDlopen的实现如下所示:
OatFile* OatFile::OpenDlopen(const std::string& elf_filename,
const std::string& location,
uint8_t* requested_base,
const char* abs_dex_location,
std::string* error_msg) {
std::unique_ptr<OatFile> oat_file(new OatFile(location, true));
bool success = oat_file->Dlopen(elf_filename, requested_base, abs_dex_location, error_msg);
if (!success) {
return nullptr;
}
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, uint8_t* requested_base,const char* abs_dex_location, std::string* error_msg) {
#ifdef __APPLE__
// The dl_iterate_phdr syscall is missing. There is similar API on OSX,
// but let's fallback to the custom loading code for the time being.
UNUSED(elf_filename);
UNUSED(requested_base);
UNUSED(abs_dex_location);
UNUSED(error_msg);
return