接着上篇(2),目前讲解了step 2,step 3,回到find_libraries方法中,继续分析。
下面是find_libraries的代码片段,我们先看注释。这部分挺容易的。
下面这段结合AI分析,得出的结论
先明确核心背景:Android Linker 支持 “命名空间(Namespace)” 机制,默认情况下,一个命名空间内的库只能访问本命名空间内的符号(隔离性);而 Global Group 是 “打破隔离” 的特殊机制,加入该组的库,其符号会被所有命名空间共享。
目标就是全局符号组(Global Group)构建 让标记为 DF_1_GLOBAL 的库,其全局符号对全进程所有 “命名空间(Namespace)” 可见。
到这里,就是为了构建全局符号表,为后续的重定位和链接做准备。注释都写得很清楚了。
代码如下
至此,分析完成,AI总结得出。核心是 “构建全局符号共享机制”
本质是平衡 “命名空间的隔离性” 和 “核心库的共享性”—— 大部分库默认隔离,只有标记为 DF_1_GLOBAL 的库(系统核心库、预加载库)才全局共享,既保证了进程稳定性,又满足了通用符号的共享需求。
接下来分析,Step 5的代码片段。在讲解这段代码之前,我们需要知道,每个so都有自己的Primary Namespace 主命名空,存在soinfo 结构体当中。然后可以有都多个次命名空间。Linker 的命名空间(Namespace)核心是 “隔离性”—— 每个命名空间有自己的 库搜索路径(比如 app 命名空间搜索 /data/app/xxx/lib/,platform 命名空间搜索 /system/lib/)和 可见库集合。
if (si->get_primary_namespace() != ns)满足这个条件才会进去分支
只处理“主命名空间 != 当前目标命名空间”的库(跨命名空间引用),就会递归调用find_libraries,去到so的主命名空间进行加载。举个例子:如果一个 so 是在其他 namespace 找到的,但它是我需要的依赖,那我必须在那个 namespace 再执行一遍依赖解析(find_libraries)。
对于soinfo* needed_by = task->get_needed_by();获取的是“谁需要我”,例如有一个依赖关系为libA -> libB,此时加载的B,get_needed_by获取到就是A,所以,在needed_by->get_primary_namespace()条件下,如果等于ns,这个ns指的是B的name space,所以如果相等的话,那就是A依赖B,所以对于此时的si(也就是libB)它的引用数要加1,跨域依赖的引用计数递增(防止误卸载)
至此,大概的流程就是如此,讲解完毕。
代码如下
结合AI分析,得出结论
核心结论:
这里开始讲解最后一步,前面经历了这么长的流程,终于到了,重定位和链接的阶段,实际上这部分是核心重点,也是难点。我们结合《程序员的自我修养——链接、装载与库》这本书来,理解一些关键流程。
先看walk_dependencies_tree方法,我们看一下注解的内容,在一开始第一次调用的时候,传递进来的root_soinfos,只有我们要加载的目标so一个,root_soinfos_size = 1
所以visit_list当中只有目标so,这时候,出栈visit_list,然后获取目标so,调用action,为前面传递的闭包函数,调用ns(当前命名空间)的is_accessible,来判断是否有权限访问这个 so 。is_accessible方法后面介绍。
然后到了下面的代码
获取到need库,也就是依赖的so,方法结束后visited类型是SoinfoLinkedList就是深度优先遍历得到的结果。传入的闭包函数。local_group最后的结果就是深度优先遍历得到的结果。
举个例子:A.so 被 dlopen()
A.so → B.so → D.so
A.so → C.so → D.so
local_group = [A.so, B.so, D.so, C.so] // 顺序视 DFS 而定,但保证拓扑合法
find_libraries方法,继续往下看,主库所在 namespace 的 preloaded 全局符号库,global_group 就是前面添加到,在上一篇文章(2)中的step 4,linked_ns->add_soinfo(si);local_group.visit:逐个 link 每个 so,判断条件是如果so还没有link就会调用link_image,开始进行链接。
接下来我们重点查看link_image方法,动态链接,是如何做的。
条件编译分支中!defined(__LP64__)意思是,32 位架构才会成立,这里贴出了代码,作为了解一下,我们主要看AArch64,分析64位叫架构。
if (has_text_relocations) 否判断是否有可重定位rel.text段,这里so中肯定有的可重定位的text section,进来分支查看,调用了phdr_table_unprotect_segments
继续分析phdr_table_unprotect_segments做了,什么,代码的实现在linker_phdr.cpp中,我们看看注释的内容说了些什么。
/*将内存中所有加载段的保护更改为可写。
*这在执行重新定位之前很有用。一旦完成,你
*必须调用phdr_table_protect_segments来恢复原始状态
*所有分段上的保护标志。
*
*请注意,一些可写段也可以转换其内容
*通过调用phdr_table_protect_gnu_relro将其设置为只读。这不是
*在这里表演。
*
*输入:
*phdr_table->程序头表
*phdr_count->表中的条目数
*load_bias->负载偏差
*返回:
*0表示错误,-1表示失败(错误代码为errno)。
*/
继续进入_phdr_table_set_load_prot
遍历程序头表,找到p_type为PT_LOAD,然后设置添加权限
extra_prot_flags传进来的是PROT_WRITE,先从 ELF 程序头(phdr)提取编译时定义的基础权限,再通过 extra_prot_flags 施加安全限制(禁止 “同时可写可执行”),最终确定内存映射时的权限(prot 参数将传给 mmap 系统调用),这个操作就是为了,解除内存保护,然后进行text重定位。到这里,
回到link_image方法,后面的__LP32__都将跳过,不讲解,往下分析,结合AI分析android_relocs_ != nullptr的时候,该分支的作用。
普通 ELF 重定位表 .rel.dyn 或 .rela.dyn 较大,例如每个 entry 在 arm64 可能 24 字节。
而 Android 中大量 APK embedded so 非常多 → 增加 APK 体积 + 加载更慢。
为解决:
Google 定义了一个 Android 专用的压缩重定位格式 APS2,可把重定位表压缩 50%~80%。
源码逻辑逐行解释
if (android_relocs_ != nullptr) {
so 里如果存在 DT_ANDROID_RELSZ 和 DT_ANDROID_REL(或 RELA 版本),则 android_relocs_ 非空。
这里我们不关心,so 里如果存在 DT_ANDROID_RELSZ 和 DT_ANDROID_REL(或 RELA 版本),则 android_relocs_ 非空。我们加载的普通so一般都会存在rela的。这里了解一下即可。
接下来分析继续往下分析,重定位表可以是两种格式之一:REL或RELA。我们关心USE_RELA和rela和rel类型的的重定位。这里我们只分析RELA类型的重定位。代码中是对rela_以及plt_rela_进行重定位,调用relocate方法,这个方法就是重定位的核心了。
直接来到#if defined(USE_RELA),rela_以及plt_rela_这些都是在prelink_image之前初始化了的,可以看前面的文章中有举例。
我们看一下,调用relocate之前,这个plain_reloc_iterator是一个类。传入了,rela的地址(本质是一个数组rel_array)和size。
接下来我们分析relocate方法,看看做了什么,可以看到,是一个模板方法。方法中用了很多的条件编译#if#elif#endif,来区分不同的cpu架构,这不同的cpu都有不同的特性,重定位的的细节也会有所不同,我们只看__aarch64__的。对其他架构感兴趣的可以自行研究。
可以看到,方法的开始处,有一个大的for循环。for (size_t idx = 0; rel_iterator.has_next(); ++idx) ,对rel进行遍历。具体做了什么分点来解释。
代码如下
在讲解之前,我们看一下elf.h头文件是如何定义,目标文件 / 可执行文件 / 共享库。这里的重定位,我们只靠考虑共享库的动态重定位。ELF文件分为三种。这里的符号索引,不同的文件类型也是不同的,在共享库是指向dynamic表中的strtab的索引。下面的代码是relocate代码的片段。
这里我们先来《ARM汇编与逆向工程 蓝狐卷 基础知识》这本书中的第二章,讲解了程序重定位一些知识。推荐阅读了,再来看。
看到第一个方法lookup_version_info,该方法是跟符号版本管理相关的,想了解这部分的,可以看我推荐看的书《程序员的自我修养——链接、装载与库》,这里面有关于符号版本管理。
/**
* 在 Linux 等系统中,共享库(.so)会存在 “版本迭代”(比如 libc.so.6 是 libc 的第 6 个大版本)。
* 为了兼容旧程序(比如旧程序依赖 libc.so.6 的旧版本符号,而系统安装了更新版 libc),ELF 引入了 符号版本控制 机制
* 同一个符号名可能对应多个版本(比如 printf@GLIBC_2.2.5 和 printf@GLIBC_2.34)
* 程序链接时会记录 “依赖的符号版本”,运行时动态链接器必须找到完全匹配版本的符号,否则可能因接口不兼容导致程序崩溃。
*/
开始讲解第一个核心方法soinfo_do_lookup,该方法根据符号名字,来查找符号所在so的符号,值得注意的是,这就是为什么so不能抹除so的动态符号表,动态字符串表。忽略has_DT_SYMBOLIC的判断,感兴趣的可以去了解一下,AI分析一下,就是执行符号查找的优先级。
然后就到了方法中的注释// 1. Look for it in global_group,很容易知道,这个是在全局库当中查找符号,如libc.so等就是全局库global_group。
调用了global_group的visit方法,传入的闭包函数。我们接着分析一下这个闭包函数做了什么。调用了global_si->find_symbol_by_name(symbol_name, vi, &s),查找当前的soinfo,根据名字查找符号。
代码如下
我们继续分析find_symbol_by_name方法,看看是如何实现的。这里有一个is_gnu_hash,这个就是判断是否存在gnu_hash这个section,我们知道,这个section是用来加速符号查找的,底层类似红黑数这种结构。这里我们看一下elf_lookup是如何实现的。
代码如下
我们继续回到soinfo_do_lookup方法中,来往下看,
// 2. Look for it in the local group代码上注释写得很清除,在global_group还是找不到符号,就去 local group, local group当中保存的是依赖的so。dfs依赖顺序的序列。和上面的方法同理,就不赘述了。
到此符号的查找流程结束,我们继续回到relocate方法当中,接着判断,s 符号是否查找成功。
符号没找到,s = nullptr,进入该分支,判断是不是弱符号。是的话,就开始进入switch中,匹配type,这个tpye是前面的代码中,是表示重定位类型。
这里只匹配绝对地址重定位。sym_addr = reloc;然后代码直接break。就允许该符号的地址保持0。
代码如下

符号找到,s != nullptr,我们接着看,进去该分支,说明找到了符号,
sym_addr = lsi->resolve_symbol_address(s);然后调用该方法,解析出符号的地址。获取了计算处符号的地址,s->st_value + load_bias
代码如下
终于要到尾声了,接着到了relocate最后的阶段,这是一个很大的switch (type) {},匹配重定位的类型。这里我们挑选两个重定位类型来讲解一下。
这里将符号地址sym_addr + addend,设置到reloc。
代码如下
最后感兴趣其他架构的可以去结合AI学习。下面交给AI讲解的重定位类型,都对应上了。
笔者能力有限
下面我给你 多个真实且典型的 ELF 重定位示例,让你彻底理解各种重定位(JUMP_SLOT、GLOB_DAT、RELATIVE、ABS、COPY)是如何作用到内存的。
每个例子都包含:
这些例子适用于 ARM64 / x86 / ARM,只是具体类型名字略有不同。
PLT/GOT 表项的函数重定位。
GOT 中某个 entry 需要解析 printf:
字段
内容
r_offset
0x1000
r_info
(symbol: printf, type: JUMP_SLOT)
r_addend
0
调用函数时:
这是全局变量的地址绑定。
例如:
字段
内容
r_offset
0x2000
r_info
(symbol: errno, type: GLOB_DAT)
r_addend
0
这个重定位不涉及符号,只是:
把 r_offset 地址写成 (base + addend)
例如 shared library load bias = 0x7f10000000
字段
内容
r_offset
0x3000
r_info
(type: RELATIVE)
r_addend
0x5000
即:
用途:
比符号查找快得多。
用于代码或数据段中包含符号的 绝对地址。
例子:
假设:
字段
内容
r_offset
0x4000
r_info
(symbol: global_value, type: ABS)
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-12-3 09:30
被Ayuer编辑
,原因: