上一篇文章初探Android Linker 动态库SO的加载流程(1),其中两步的内容,总结了核心重点是如何进行加载so,映射了程序头表、节头表、dynamic section 到内存地址中,这些地址都保存在了ElfReader当中。
这一篇我们来继续分析
这里进行load_list遍历,然后判断si如果不是已经链接的,然后调用std::find_if进行去重,传入函数闭包,shuffle则进行打乱load_list,随机化加载。
接着到重点了,遍历load_list然后调用task的load方法
代码如下
开始分析task的load方法
这里get_elf_reader获取了,前面步骤使用的ElfReader,然后开始调用ElfReader的Load方法,该方法的实现位于linker_phdr.cpp,上一节文章中,如何进行加载so,映射了程序头表、节头表、dynamic section 到内存地址中,从这里开始,加载程序头表的load类型。这里有三个核心方法ReserveAddressSpace和LoadSegments以及FindPhdr
代码如下
我们先来看第一个方法ReserveAddressSpace,我们开一下这个方法的注释,这里的意思是创建一段带有PROT_NONE的私有匿名mmap(),权限为PROT_NONE。
PROT_NONE 是 Unix/Linux 系统中用于内存区域保护权限的常量(定义在 <sys/mman.h> 头文件中),字面意思是 “无任何访问权限”—— 被标记为 PROT_NONE 的内存页,程序无法执行读(read)、写(write)、执行(execute)任何操作。
通过Linux系统调用,申请一段内存,然后用于后续程序头表的段加载(LOAD)。 预留一整块连续的虚拟地址空间、 保证所有 PT_LOAD 段可以放进去 。
代码如下
phdr_table_get_load_size方法,遍历程序头表,寻找p_type为PT_LOAD类型。
作用
给定一个 ELF 文件的 Program Header Table (PHDR),这个方法要计算:
原因
这里拓展一下,PT_LOAD,需要加载到内存的段,例如
代码如下
我们回到ReserveAddressSpace方法,phdr_table_get_load_size计算好需要申请的内存大小,接着看核心的流程,这里会进行调用ReserveAligned方法
int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;这里设置成了,私有的、以及匿名的flags,根据size,调用mmap预留出地址空间,到此ElfReader::Load的ReserveAddressSpace结束了,所以ReserveAddressSpace计算出PT_LOAD需要的空间,然后mmap预留出地址空间。
代码如下
接下来进行分析ElfReader::Load的中的LoadSegments,该方法是处理程序头表。
t找出Segmen的p_type为PT_LOAD 。
segment 在内存空间中的起始地址 segstart 和结束地址 seg_end,seg_start 等于虚拟偏移加上基址load_bias,同时由于 mmap 的要求,都要对齐到页边界得到 seg_page_start 和 seg_page_end。
计算 segment 在文件中的页对齐后的起始地址 file_page_start 和长度 file_length。
使用 mmap 将 segment 映射到内存,指定映射地址为 seg_page_start,长度为 file_length,文件偏移为 file_page_start。,然后进行加载到上一步预留的地址空间当中
将seg_start和seg_end对齐后得到seg_page_start、seg_page_end,然后调用mmap64,本质就是mmap的别名,进行Segmen映射到seg_page_start的地址中。
代码如下
到此,LoadSegments到此结束,我们继续研究第三个方法FindPhdr,先说结论,比较简单,是找到装载后的 phdr 地址。
我们先看一下注释,大概说将loaded_phdr_设置为程序头表显示的地址
const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_;这里是遍历程序头表,然后寻找p_type为PT_PHDR,找到装载后的 phdr 地址,该方法中,寻找Phdr有两种情况,第一中比较容易,就是在phdr_table_表中,查找p_type为PT_PHDR的;第二种情况,就是在PT_LOAD中且p_offset为0(理解这段代码,必须先记住一个 ELF 规范中的关键特性:如果 ELF 文件的第一个可加载段(PT_LOAD 类型)的「文件偏移(p_offset)= 0」)
代码如下
到此结束,elf_reader.Load方法结束,我们继续回到linker.cpp的load()方法中,最后初始化si_
代码如下
到此,Step 3,完结。
最后总结一下Step 3。
* 打开 so 文件(或使用 extinfo 中的 fd)
* 读取 ELF header
* 读取 Program Header Table
* 找到所有 PT_LOAD 段
* 使用 mmap() 将 PT_LOAD 段映射到内存
* 修正对齐、权限(PROT_READ/WRITE/EXEC)
* 构建 load_bias
* 找到加载后的 phdr 在内存中的实际地址
接下来的Step 3、Step 4比较容易
直接看核心方法,si->is_linked()如果没有被链接,然后调用prelink_image。
我们直接查看prelink_image方法,这个方法比较长,以下截取部分代码。
先看phdr_table_get_dynamic_section方法,上面写有注释,意思是返回ELF文件的dynamic section,但是这dynamic section在上一篇文章(1)中已经进行了,加载了映射了。但是这里为什么又再去加载呢,我们接着往下看。
这要要回顾上一篇文章中,介绍了load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;这行代码的意思就是,当前elf加载到内存的地址,而addr是so在编译时期进行确定的,而start是每次mmap的地址,load_bias_则是偏移量,后续涉及so之类的地址访问,加载,都会加上这个偏移,才能正确访问到。下面这张图可以看到,确实是加上了,load_bias_偏移量。所以这个phdr_table_get_dynamic_section方法,获取了程序头表的dynamic_section的虚拟地址,这个地址是编译时确定,dynamic是soinfo的成员变量。所以这里就是设置了,内存的真实地址。以及设置了dynamic_flags的flags。flags具体的可以上一篇文章中推荐的书,当中有介,绍这里不赘述。
在上一篇文章的结论中,映射地址,映射了程序头表、节头表、dynamic section 到内存地址中,这些地址都保存在了ElfReader当中。

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-12-3 09:30
被Ayuer编辑
,原因: