乱贴源码系列:结合Android6.0源码linker.cpp来学习一下so库的加载流程
so的加载和链接
整体流程
System.load
通常我们要在代码中使用so库的话,都要在静态块手动的去载入so库,形如:
1 | static { |
跟进代码,会一直走到java.lang.Runtime
包下的native方法nativeLoad
,再到源码中的java_lang_Runtime.cc中的Runtime_nativeLoad
,然后跳转到java_vm_ext.cc的LoadNativeLibrary
,在line645调用了dlopen:
1 | 643 Locks::mutator_lock_->AssertNotHeld(self); |
看到dlopen基本就是跳转到linker下执行了,dlopen
是linker目录下的dlfcn.cpp的函数:
1 | 70 static void* dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo) { |
自下往上,最后调用到linker.cpp的do_dlopen
。
do_dlopen
1 | ... |
前面做了一堆非空值的判断,然后调用了一个ProtectedDataGuard
类的protect_data
方法
1 | 791class ProtectedDataGuard { |
最后通过LinkerTypeAllocator
调用到LinkerBlockAllocator
的protect_all
,实际上调的是系统函数mprotect
来修改页的读写属性。
1 | 89void LinkerBlockAllocator::protect_all(int prot) { |
ok,回归正文,刚才do_dlopen
最主要一行代码,通过find_library就完成了so的加载,返回一个结构体soinfo,然后调用这个soinfo的call_constructors()方法,即so库的构造函数,来完成so的加载。
1 | 1695 soinfo* si = find_library(name, flags, extinfo); |
soinfo
在进入find_library之前先介绍一下表示一个so库的信息的结构体soinfo
我很想把代码都贴上来,但是那样凑字数意图太明显,我还是贴个简单版的
1 | struct soinfo{ |
phdr
在之前的ELF文件格式执行视图中已经介绍过了,就是程序头表,dynamic就是动态节区,特别注意有2个hash表相关的,这个放到后续我们hook的时候会再细说,主要是有个印象,知道符号查找不一定是根据hash表来,还有个gnuhash。
find_library
内部调用find_libraries
->find_library_internal
->load_library
->load_library
,做一系加载,预链接,已加载判断等,在6个参数的load_library中开始通过ElfReader这个类来读取so文件信息并赋值到soinfo中来实现so文件加载到内存中。
1 | 1303 // Read the ELF header and load the segments. |
加载
ElfReader
ElfReader的定义在linker_phdr中,就是通过解析so库的执行视图,来读取so文件的各种信息,因为后续介绍的hook的时候也是根据这里的思路来进行的。
Load
根据上面的load_library
代码,可以看到先执行了ElfReader的Load方法,如下:
1 | 149bool ElfReader::Load(const android_dlextinfo* extinfo) { |
很完美,读取头文件-验证-读取程序头-分配内存空间-加载段区-查找程序头表项。
每个步骤在参考中的Linker与So加壳技术都有说明,规则都很死,读取文件信息,找偏移,读取正确位置即可,我这里只强调一下分配空间这个步骤。
1 | 307bool ElfReader::ReserveAddressSpace(const android_dlextinfo* extinfo) { |
这段代码先是计算了so库在内存中需要的空间,然后调用系统函数mmap
来映射。
在line352有个变量load_bias_
,他的值是start-addr,这个addr的值是min_vaddr
,这是so库在加载的时候指定的加载基址,通常来说这个值为0,对,通常来说。
在Android4.4及其之前的版本,loadbias=load_start,但是在Anroid6.0(5.0没有机子测试)之后,min_vaddr不为0,所以loadbias要做一个修正:
1 | load_bias_ = reinterpret_cast<uint8_t*>(start) - addr; |
在linker的line3339也可以看到,可以计算load_bias
1 | 3329/* Compute the load-bias of an existing executable. This shall only |
这个在后续的hook也是参考上述方法来得到实际的地址。
链接
在加载完毕之后,在line1319来执行预链接perlink_image方法来链接。
1 | 2499bool soinfo::prelink_image() { |
定位动态节区
通过phdr_table_get_dynamic_section来找到type为PT_DYNAMIC
的动态节区。
然后再遍历解析,获取重定位相关的各种信息(DT_SYMTAB DT_HASH等)。
遍历动态节区
找到动态节区之后,遍历来根据节区的结构体成员d_tag,确定需要用到的各种参数,各种节区,别如DT_PLTRELSZ就是与重定位有关的,DT_HASH DT_GNU_HASH就是和符号表索引相关的,DT_INIT就是初始化相关的节。
重定位
在prelink_image之后,回到find_libraries
函数中,line1527调用了linke_image方法,内部调用relocate函数
解析完信息之后,就要遍历重定位表,来重定位每个符号的实际地址。
占坑,放在(三)中和hook原理一起讲解。