首页
社区
课程
招聘
[原创]初探Android Linker 动态库SO的加载流程(2)
发表于: 2025-12-2 22:32 6973

[原创]初探Android Linker 动态库SO的加载流程(2)

2025-12-2 22:32
6973

上一篇文章初探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编辑 ,原因:
收藏
免费 32
支持
分享
最新回复 (8)
雪    币: 368
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
学习
2025-12-3 09:49
0
雪    币: 112
活跃值: (1557)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
666
2025-12-3 13:54
0
雪    币: 104
活跃值: (7159)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
TQL
2025-12-3 14:22
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
11
2025-12-3 15:08
0
雪    币: 530
活跃值: (2000)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
666
2025-12-4 10:43
0
雪    币: 1495
活跃值: (3698)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
7
1
2025-12-4 10:44
0
雪    币: 720
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
111
2025-12-8 15:40
0
雪    币: 209
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
tql
2025-12-9 11:28
0
游客
登录 | 注册 方可回帖
返回