-
-
[原创]Android从ELF-Loader到自定义Linker的实现及原理
-
发表于: 4小时前 140
-
本文为完善旧坑所作, 学习ELF文件结构后, 实现了解析器和加载器, 当初便想实现自定义Linker, 无奈碰到抽象bug暂时弃坑, 断断续续尝试几次终于解决bug成功跑通 之后让AI重构屎山代码, 更加清晰易懂, 遂填坑
Push AI一把嗦固然非常爽, 但我个人的体验往往是脑中空空, 学得快忘得快, 印象不深, 长此以往容易让人浮躁, 知识消化吸收不良
最后, 希望给学习相关知识点的师傅一些帮助, 同师傅们静下心学习底层原理
阅读本文的正确姿势:
本文分为以下部分:
ELF文件结构
ELF文件相关理论基础快速入门
Linker概述
系统Linker的工作流程简介 + 自定义Linker的概述
ELF Loader
一个简单的ELF可执行程序加载器, 用于辅助理解自定义Linker核心流程
Custom Linker
将ELF Loader迁移到Android平台, 手搓自定义Linker
Custom Linker Test
自定义Linker效果测试, 匿名内存模块扫描脚本, 抹去ELF关键信息增加逆向难度
Linker源码阅读分析
从Android 4.4和Android 10源码深入Android系统linker内部流程
参考及推荐资料
学习自定义Linker期间参考的资料, 以及部分推荐资料
附件:
ELF-Loader
ELF Loader源码
SelfDefineLoader
自定义Linker源码项目
scan_hidden_modules.js
匿名内存模块frida扫描脚本
DumpedSO
测试自定义Linker时dump出的SO
SoFixer
小风编译的修复版SoFixer
环境:
声明:
在学习自定义linker前, 先过一遍ELF文件结构相关知识点
Linux 的一个 .so 或可执行文件本质上是 ELF(Executable and Linkable Format) 文件
ELF文件结构解析和加载器实现可以参考本人之前的一篇文章: ELF文件结构浅析-解析器和加载器实现
可以将ELF文件分为5大核心块:
ELF Header
描述ELF文件核心结构的基本信息, 例如文件类型, 架构, 入口点地址等
并且指定了Program Headers和Section Headers的起始地址以及对应表项数目
Program Headers
每个表项描述段(Segment)的基本信息, 包括段的起始地址, 大小, 权限等
供Linker判断segment是否需要加载, 如何加载到内存中
Sections / Segments
节 Section 是文件视图, 每个节都有其功能, 划分各个节有助于静态分析工具和用户了解ELF文件的细节
段 Segment 是内存视图, Linker加载ELF文件到内存中只需要关注段如何加载, 不需要了解各个节的细节
一个段可以包含多个节, 如果节的权限相同且地址连续便可以视为同一个段方便
Dynamic段
Section Headers 中, 为 .dynamic 节, Program Headers中, 为 PT_DYNAMIC 段
称之为 dynamic段 更加合适, 因为Linker在加载和链接时非常依赖它的信息
其指向了符号表(导入/导出符号), 重定位表, Hash表(导出表), 依赖库, PLT, GOT, .init, .init_array等结构
Section Headers
每个表项描述节(Section)的基本信息, 包括节名, 起始地址, 大小等
通常位于ELF文件末尾, Linker加载ELF文件时通常不会加载它, 因为运行时并不需要该结构, 但静态分析工具非常需要
ELF文件按照顺序的布局大致如下 (以'.'开头的均为节区, 它们的顺序并非固定, 和编译器生成规则有关):

Section和Segment两者的关系:
一个Segment可以包含多个Section, 一个Section只属于一个Segment
加载器只关心Segment(Program Header), 不需要Section Header
strip掉Section Header的SO仍然可以正常加载运行, 但会让IDA等逆向工具难以分析
Section和Segment在不同视角下的映射图:

IDA显示的segments, 可以发现相同权限的section通常是连续的
即多个相同权限的section可以视为同一个segment

ELF Header位于文件最开头, 64位下固定64字节, 是整个文件的"身份证", 包含:
ELF文件类型, 目标CPU架构, 入口点地址
ELF Header大小, Program / Section Headers的起始地址, 表项大小, 表项数等关键信息
Linker会首先读取该结构, 校验Magic Number (\x7fELF) 和 CPU架构, 然后从 e_phoff 定位段表
每个Section Header描述一个节的名称、类型、在文件中的位置和大小:
一些常见的节区列表汇总如下, 文章后续会讲解这些节区:
以上并非所有节区, 一般还会有支持异常处理, 调试器辅助信息等功能的节区
010editor查看Section Headers效果如下

Program Header描述了文件中哪些部分需要映射到内存, 以及映射的属性:
常见的段类型:
010editor查看Program Headers效果如下, 带有很多辅助信息:

Linker加载ELF文件时, 会遍历所有PT_LOAD段, 计算映像大小并分配内存, 将段填充至指定的虚拟地址, 之后设置段权限完成加载
ELF文件中有很多字符串,例如段名,变量名等, 由于字符串长度往往不固定,所以使用固定结构描述比较困难
常见做法是将字符串集中起来存放到一张字符串表,然后通过索引查表来引用字符串
字符串表的内部结构极其简单:一块连续的字节数组
设计规则:
每个字符串都以空字符 \0 (NULL) 结尾
表的第 0 个字节永远是 \0
一个字符串可以包含另一个字符串
例如,如果有 "printf\0",恰好有个符号叫 rintf,那么偏移量向后移动 1 位,就可以复用这段内存
ELF文件中有3种字符串表, 其中 .dynstr 最重要:
.shstrtab 节头字符串表
存储“节区”自身的名字, 例如 ".text", ".data", ".bss" 等字符串
非运行时必须, 主要供链接器和静态分析工具(如 readelf)解析文件结构时使用
.strtab 静态字符串表
存储“静态符号”的名字, 包含了代码中所有的函数名, 全局变量名,用于调试的局部变量名和源文件名称
非运行时必须, 用于静态链接和调试。为了减小文件体积,发布前常使用 strip 命令将其剥离
.dynstr 动态字符串表
存储“动态链接”所需的名字: 1. 动态符号名(导入/导出函数和变量) 2. 依赖的外部共享库名称如 "libc.so.6"
运行时必须, 它是linker在运行时寻找外部函数、加载依赖库的符号名称来源,不可剥离。
010editor查看.dynstr效果如下:
符号表记录了ELF导出和导入的所有符号(函数/全局变量等):
通过符号表和对应的字符串表可以得到符号名,符号大小,符号地址等信息
.symtab 静态符号表
存储文件中的所有符号,包括局部函数、静态变量、调试信息等
非运行时必须,主要用于静态链接和调试(如 gdb 解析函数名),与 .strtab 一样,发布前常被 strip 剥离
.dynsym 动态符号表
仅存储动态链接所需的符号:导入/导出的外部函数/变量
运行时必须,它是Linker解析外部依赖的符号来源,不可剥离
值得一提的是符号表中st_name存的不是字符串本身, 而是一个偏移量, 实际的函数名/变量名存在**字符串表(.dynstr)**中
查找符号名时: symbolName = dynstr[sym.st_name]
例如该样本中, strTableAddr = 0xA28, sym.st_name = 0x2F
所以 sym_name_off = 0xA28+0x2F = 0xA57 , 即 strTable[0xA57] = "memcpy\0"

Dynamic Table 是动态链接的核心"索引目录", 它是一个Elf_Dyn数组, 每个元素是一个(tag, value)键值对:
Linker遍历Program Headers, 通过PT_DYNAMIC段属性找到dynamic table, 然后遍历提取所有需要的信息
常见d_tag标志含义及对应d_un作用如下:
后续ELF Loader和自定义Linker会使用到其中大部分tag, 有一部分并不需要使用
010editor查看Dynamic Segment效果如下

重定位表告诉Linker 哪些位置需要重定位, 如何重定位修复, 有2种常见格式:
Rel (32位常用, 无显式addend):
Rela (64位常用, 带显式addend):
有2张重定位表需要处理:
常见的重定位类型 (不同CPU架构对应枚举不同, 以AArch64为例):
其中R_RELATIVE不涉及外部符号, 不需要查符号表, 其余3种需要先通过符号表解析出符号地址
Hash表用于快速按名查找符号, 避免遍历整个符号表, 实际上承担了 导出符号表 的功能
有两种格式:
SysV Hash(传统格式, 结构简单):
查找: hash(name) % nbucket -> 得到起始索引 -> 沿chain逐个strcmp
GNU Hash (现代格式, NDK r23+默认):
查找: Bloom Filter预筛 -> Bucket定位 -> Chain比较
NDK r23+(2021年起) 默认--hash-style=gnu, 生成的SO只有.gnu.hash没有.hash
所以Linker优先使用GNU Hash, 而SysV Hash作为兼容备选
关于Hash Table详细机制比较复杂此处不展开, 先前的文章有详细介绍 Hash Table (Export Table), 兴趣的师傅可以自行学习
汇总相关机制如下
PLT(Procedure Linkage Table) 和 GOT(Global Offset Table) 是实现 外部函数调用 的核心机制:
GOT 位于数据段 (RW-)
一个地址数组, 每个外部符号一个槽位, 加载时由linker进行重定位, 填入真实地址
每个元素可以是变量地址, 也可以是函数地址, 即 addr = *GOT[offset]
**PLT **位于代码段 (R-X)
每个外部函数对应一个PLT跳转存根, 间接通过GOT存储的地址跳转到目标
例如一个ELF程序调用 printf() 函数, 实际流程如下:
核心:.plt (跳板代码) + .got (可读可写地址表) + Linker 重定位填充地址
很显然, PLT是固定指向GOT的, 但GOT表项可以修改, 方便程序运行时动态加载依赖库的外部函数/变量
前文提到了: .got, .plt, .got.plt, .plt.got 这四张表, 前两张好理解, 后两张是干嘛的呢?
实际上它们都是为了外部函数调用服务的, 只不过使用场景不同:
.got.plt 是专为 延迟绑定 服务的全局地址表
为什么要延迟绑定? 当 linker 进行链接时, 会进行重定位并默认填充所有GOT表项
但当程序依赖的外部库函数过多 (如上千个外部函数), 运行却只调用某几个时
显然提前链接这些函数会严重拖慢程序启动运行速度, 所以需要延迟绑定
延迟绑定机制, 外部函数调用流程如下:
第一次调用:触发 Linker 解析函数地址
第二次及之后的调用:和前文 PLT -> GOT 原理一致
核心:.plt (跳板代码) + .got.plt (可写地址表) + Linker 解析函数真实地址
值得一提的是, Linux ELF程序默认使用延迟绑定机制以加快程序运行速度
但不难看出, 为了实现延迟绑定机制, .got.plt 的权限为RW-, 可写则意味着存在被劫持的可能
为了封堵 .got.plt 可写的安全漏洞,现代程序支持开启 完全重定位只读 (Full RELRO) , 代价是牺牲启动速度,放弃延迟绑定
程序启动时, Linker 把所有外部函数的真实地址全部找出来,填入 .got 表中
之后将整个 .got 表所在的内存页设置为只读, 保证再也无法篡改函数指针
核心:.plt.got (跳板代码) + .got (只读地址表) + Linker 重定位填充地址
系统的dlopen是一个黑盒: 调用它之后, 帮你完成加载、链接、重定位、初始化, 然后返回一个handle。整个过程无法干预, 也无法定制。
自定义Linker的核心价值在于 对SO加载过程的完全控制:
被加载的SO对系统不可见
自定义Linker加载的SO可以不在系统的soinfo链表中, 从而实现在/proc/pid/maps中没有文件路径的效果
可插入自定义逻辑
加载前可以解密SO, 加载后可以抹除头部, 中间可以插入反调试检查
自定义Linker的实现方式目前了解到两种:
网上了解到的大部分文章基于Android官方或魔改版的soinfo实现方式1的自定义linker
本文基于之前实现的ELF Loader, 使用方式2实现
值得一提的是, 随着功能增加, 用于描述so的class向soinfo靠拢, 所以本质上方式1和方式2的核心原理是一致的
自定义Linker实际上是把系统linker做的事情做一遍。
linker加载ELF的核心工作并不复杂:
系统linker通过 ElfReader 读取文件, soinfo_link_image 链接重定位, CallConstructors 初始化
自定义linker做同样的事, 只是不走系统的代码路径:
在实现自定义Linker之前, 先用一个 ELF 可执行程序加载器来理解核心流程
这个 ELF Loader 只有200多行代码, 但覆盖了加载器的全部核心步骤, 后面的自定义 Linker 只是在同样的骨架上做适配和扩展
ELF-Loader.h 根据32/64位定义不同结构体, 并定义 ElfLoader 类如下, 封装了部分关键结构方便函数调用
外部程序可通过ElfLoader::load()函数加载so
映射ELF文件为file buffer, 方便读取信息, 使用mmap比直接读取到内存中更方便快速
open -> fstat 获取大小 -> mmap 只读映射文件到内存, 不需要额外的read/malloc
校验 Magic Number ("\x7fELF") + 文件类型 (ET_EXEC/ET_DYN)
然后从 e_phoff 定位段表方便后续操作
遍历段表找到最后一个PT_LOAD段的结束地址, 页对齐后就是映像总大小
(一般情况下, 各个段连续, 所以最后一个段的结束地址对应了映像大小, 但并不严谨)
用MAP_ANONYMOUS分配一块RW匿名内存, 初始可写是因为后续Step 4要memcpy、Step 7要修改重定位目标
遍历所有PT_LOAD段, 从文件映射(fileMap + p_offset)复制到映像内存(image + p_vaddr)
p_filesz是文件中的实际数据大小, p_memsz是内存中应占的大小, 差值部分是BSS段 (未初始化全局变量), ELF规范要求填零
Dynamic段本质是一个(tag, value)键值对数组, 是linker的"索引目录", 这一步从中提取链接和重定位所需信息:
注意:
DT_NEEDED的d_val是字符串表中的偏移, 指向依赖库名 (如libc.so、libm.so)
此处为了便于理解, 没有实现依赖库的加载和符号解析功能, 直接使用 dlopen拿到handle, 后续用dlsym从中解析符号
ELF编译时不知道自己会被加载到哪个地址, 所有绝对地址引用都需要在加载时修正
有两张重定位表需要处理:
重定位类型分两大类:
相对偏移地址重定位 (R_RELATIVE)
最常见, r_offset 指示修正位置, 直接加上基址即可修复
符号重定位 (GLOB_DAT/JMP_SLOT)
需要获取外部符号地址, 从r_info提取符号索引, 查符号表获取名称, resolveSymbol遍历依赖库逐个dlsym查找符号地址
其中resolveSymbol实现如下:
注意: 重定位表 32位架构通常使用Elf32_Rel , 64位使用 Elf64_Rela, 此处没有考虑RELA的append所以并不严谨, 只是恰好样本append=0, 后续实现自定义Linker注意
之前分配映像时统一设为RW (方便写入和重定位), 现在一切就位, 按Program Header中p_flags指定的权限恢复
前8步完成后, ELF的代码和数据已填充, 完成链接和重定位, 并设置段权限, 此时程序映像可以执行
最后从 ELF Header 的 e_entry 获取入口先地址并跳转即可
hello.cpp
main.cpp
分别编译
效果如下:
32位ELF程序

64位ELF程序

理解了ELF Loader的9条步骤后, 接下来将其迁移到Android平台——实现自定义Linker用于加载SO
ELF Loader 加载的是可执行程序 (有e_entry入口点), 而 Android 的 SO 文件没有入口点
需要依次调用 .init -> .init_array -> JNI_OnLoad 进行初始化
本节将 ELF Loader 迁移到 Android AArch64 平台, 实现自定义Linker加载SO
tip: 为了章节的完整性, 部分重复内容并没有去除, 但对标题进行了标注, 师傅们可以按需跳过
二者共享相同的9条核心步骤, 但部分步骤有差异:
同样的, 外部程序可通过 Loader::load() 函数加载SO
注意:
open -> fstat 获取大小 -> mmap 只读映射文件到内存, 与ELF Loader的mapFile完全相同
校验Magic Number(\x7fELF), 文件类型, 目标CPU架构(EM_AARCH64=183), 任一不匹配则拒绝加载
遍历所有PT_LOAD段, 算出虚拟地址范围, 页对齐后用MAP_ANONYMOUS分配一块连续的匿名内存
初始设为RW-, 因为后面要往里写数据(加载段)和改数据(重定位)
使用 PAGE_START 和 PAGE_END 宏计算对齐值
由于申请的是匿名内存, 也没有使用soinfo并注册到soinfo list, 所以加载的目标so信息对maps和soinfo list是不可见的
遍历所有PT_LOAD段, 从文件映射(fileMap + p_offset)复制到映像内存(image + p_vaddr)
p_filesz是文件中的实际数据大小, p_memsz是内存中应占的大小, 差值部分是BSS段 (未初始化全局变量), ELF规范要求填零
与ELF Loader相比, 多提取了GNU Hash和SysV Hash表
并将GNU Hash的内部结构(布隆过滤器、桶数组、链数组)全部解析到成员变量中
AArch64使用RELA格式(带addend), 依然有2张重定位表 .rela.dyn 和 .rela.plt 需要处理
4种重定位类型分2大类:
基址重定位(R_AARCH64_RELATIVE)
最常见, 处理方式: *target = pImageBase + r_addend
符号重定位(ABS64/GLOB_DAT/JUMP_SLOT)
需要在依赖库中查找外部符号地址, 通过resolveSymbol遍历所有依赖库的dlsym实现
与ELF Loader的区别:
resolveSymbol()
把Step 4中统一设成RW的映像, 按每个段的实际属性重新设置权限
注意: __builtin___clear_cache是AArch64上必须的: D-Cache和I-Cache分离, 前面通过memcpy写入的代码在D-Cache中, CPU执行走的是I-Cache, 不刷新会SIGILL崩溃。这是ELF Loader中不需要的步骤
这是ELF Loader与自定义Linker最大的区别: 可执行程序跳转e_entry, 而SO没有入口点, 需要主动调用初始化函数链
SO的初始化分三层, 调用顺序严格遵循系统linker:
到这步执行后, 被加载的SO便可以被正常调用——代码就位, 全局变量初始化, 外部符号链接完毕, 构造函数执行完毕
getSymbol 和 findSymbol 实现如下, 用于查找SO自身符号, 底层通过GNU/Sysv Hash Table查找实现
自定义Linker相关文件分别为Loader.h和Loader.cpp, 大部分代码前文已经提到此处不做展开
值得一提的是Loader.cpp的JNI_OnLoad用于主动加载目标so
加载libselfdefineloader.so, 之后测试init_array和JNI函数是否能正确执行
创建全局变量, 实现init_array函数, JNI_OnLoad动态注册JNI函数
生成libtestdemo.so到build/outputs/lib/arm64-v8a/目录下, 且不打包进apk, 方便测试
编译后推送libtestdemo.so至"/data/local/tmp"目录下
注意: 由于selinux权限限制, 如果不能读取该目录, 则需要root权限的shell使用setenforce 0关闭selinux
启动app, 运行效果如下:

自定义Linker可以保证在"/proc/tid/maps"中无法发现目标so, 因为so所在内存属于匿名内存

不难发现so加载到内存后, 保留了ELF Header, 这是一个关键特征, 另外一个特征是部分段具有R-X权限
一个具有可执行权限的匿名内存段会干什么呢? 好难猜啊
基于这些特征, 可以编写frida脚本, 扫描匿名内存模块:
获取所有已知模块的基址, 设置白名单
枚举进程内存范围
对于单个SO, 通常情况下内存范围是连续的
匹配到R-X的内存段时, 判断其基地址首部4字节是否为ELF Magic Number
匹配到可疑内存段后, 判断是否属于已知模块, 若不属于则为匿名模块, 进行dump
attach模式注入脚本, 扫描并dump

拉取到PC并使用SoFixer修复

最后, IDA分析对比修复前后的SO, 左边未修复无法看到符号, 右边可以看到JNI_OnLoad

值得一提的是, 该脚本是笔者逆向分析 zgcbank-4.5.1(某bang加固) 时 拷打AI得到的, 所以针对市面上部分自定义Linker加固壳有效
加载完成后, ELF Header, Program Header, Dynamic Segment, 这三块数据已完成使命 (所有需要的信息已提取到成员变量中), 可以直接抹去, 增大逆向分析的难度:
注意: 抹除前需要mprotect修改内存页权限, 抹除后需要恢复原权限
以下内容偏理论, 主要从 Android 4.4.4_r1 和 Android 10.0.0_r47 源码入手, 分别分析32位和64位的linker加载和链接so的工作流程
其中32位的linker较为简单, 64位的linker添加了更多操作, 但只要逐步跟进可以发现并不复杂
32位linker: 简单直接, 加载和链接在find_library_internal中串行完成, 不涉及命名空间隔离
Android 4.4.4_r1 Linker加载和链接so的核心调用链:
流程图:

64位linker: 引入了命名空间隔离(android_dlopen_ext), find_libraries加载和链接SO, 支持随机序加载防攻击
Android 10.0.0_r47 Linker 核心调用链:
加载和链接so的流程图:

前文内容是我们自己从零实现Linker, 本节换个角度——阅读 Android 官方 linker 的源码, 看看系统是如何实现的
这一节不是实现参考, 而是帮助读者理解系统 linker 的完整工作流程
本节基于 Android 4.4.4_r1 源码, 32位 linker 结构简单直接, 适合入门理解整体流程
java层通常使用以下代码加载一个so
Android 4.4定义如下
056K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4Q4x3V1k6D9K9h3u0U0L8%4u0W2i4K6u0r3L8s2g2F1K9g2)9J5c8Y4y4J5j5#2)9J5c8X3#2S2K9h3&6Q4x3V1k6B7j5i4k6S2i4K6u0r3K9X3q4$3j5g2)9J5c8X3I4S2L8X3N6Q4x3V1k6e0P5i4y4@1k6h3#2Q4x3X3g2B7j5i4k6S2i4K6t1K6y4e0t1#2
Android 4.4
eedK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4D9K9h3u0U0L8%4u0W2i4K6u0r3L8s2g2F1K9g2)9J5c8Y4y4J5j5#2)9J5c8X3#2S2K9h3&6Q4x3V1k6B7j5i4k6S2i4K6u0r3K9X3q4$3j5g2)9J5c8X3I4S2L8X3N6Q4x3V1k6d9N6h3&6@1K9h3#2W2i4K6u0W2K9X3q4$3j5g2)9K6b7X3u0H3N6W2)9K6c8o6l9`.
获取ClassLoader的native library搜索路径, 传递给nativeLoad。加锁保证同一时间只有一个LD_LIBRARY_PATH在使用
Java层的nativeLoad进入Native层, 最终调用ART虚拟机的LoadNativeLibrary完成SO加载
native函数声明, 对应的JNI实现为Runtime_nativeLoad
nativeLoad的JNI实现。更新LD_LIBRARY_PATH后, 调用JavaVMExt::LoadNativeLibrary执行真正的加载
445K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4S2M7Y4c8Q4x3V1k6J5N6h3&6@1K9h3#2W2i4K6u0r3L8X3q4@1K9i4k6W2i4K6u0r3K9X3q4$3j5g2)9#2k6X3I4S2L8X3N6Q4y4h3k6d9N6h3&6@1K9h3#2W2i4K6u0W2j5$3x3`.
785K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4S2M7Y4c8Q4x3V1k6J5N6h3&6@1K9h3#2W2i4K6u0r3K9X3&6A6i4K6g2X3K9h3&6@1k6i4u0F1j5h3I4Q4x3X3g2U0j5H3`.`.
Linker为了加载so的部分前置操作
fd8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6V1L8r3k6U0L8W2)9J5k6h3y4H3M7l9`.`.
调用了do_dlopen,并返回soinfo指针
662K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3X3g2U0M7s2l9`.
1cdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3X3g2U0M7s2m8Q4x3U0x3%4y4e0p5`.
遍历solist,从已加载so中获取soinfo
6deK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3X3g2U0M7s2m8Q4x3U0x3%4x3K6t1`.
加载SO文件到内存的核心流程, 通过ElfReader完成ELF解析、地址空间分配和段加载
加载SO的入口: 打开文件 → 创建ElfReader执行加载 → 分配soinfo并填充加载结果(基址、大小、load_bias等)
ElfReader的主流程, 串联6个步骤: 读取ELF头 → 验证ELF头 → 读取程序头表 → 分配地址空间 → 加载段 → 定位Phdr。任一步骤失败则整体失败
36aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4y4h3k6H3K9r3c8J5i4K6u0W2j5%4m8H3
从文件描述符读取sizeof(Elf32_Ehdr)字节到header_结构体, 后续所有解析基于这个结构
校验ELF头的合法性: Magic Number(\x7fELF) → 32位(ELFCLASS32) → 小端序(ELFDATA2LSB) → 共享库(ET_DYN) → 版本号 → 目标架构(ARM/x86/MIPS)
根据e_phoff和e_phnum定位程序头表(段表), 用mmap将其映射到内存。段表描述了SO中每个Segment的类型、偏移、地址和权限
计算SO映像需要的内存大小(通过phdr_table_get_load_size遍历所有PT_LOAD段), 然后用mmap匿名映射一块足够大的连续内存。load_bias_记录实际加载地址与默认虚拟地址的偏差, 后续重定位依赖这个值
遍历所有PT_LOAD段, 找出最小和最大虚拟地址, 页对齐后差值即为SO映射到内存需要的总大小
ReserveAddressSpace只是申请内存,这里才是实际加载段的地方
上述流程将一个SO的PT_LOAD段加载到内存中,而这之后还需要进行链接和重定位操作才能使用so
在Linker Load SO过程中,起点是load_library函数,调用了elf_reader.Load()
load_library的上层是find_library_internal
该函数在调用load_library加载so后调用了soinfo_link_image进行链接操作,接下来学习链接相关代码
f6dK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3X3g2U0M7s2m8Q4x3U0x3I4x3K6l9K6
链接完成后进入重定位阶段, 修正SO代码和数据中的所有地址引用
核心重定位函数, 在soinfo_link_image中被调用两次: 一次处理.plt.rel(函数调用), 一次处理.rel(数据引用)。对每个重定位条目: 提取类型和符号索引 → 如果需要符号则通过soinfo_do_lookup从依赖库查找 → 根据类型(JUMP_SLOT/GLOB_DAT/ABS32/RELATIVE等)计算目标地址并回填
符号查找的总调度函数。查找顺序: 主可执行文件 → 自身(如果有DT_SYMBOLIC) → 预加载库(LD_PRELOAD) → 依赖库(DT_NEEDED列表)。内部通过soinfo_elf_lookup使用SysV Hash表在各个soinfo中查找符号
其中soinfo_elf_lookup通过elfhash计算符号名的hash值, bucket[hash % nbucket]定位桶, 沿chain链逐个strcmp比较
回到do_dlopen,在调用find_library以及后续的加载,链接,重定位操作后
调用了soinfo的CallConstructors()函数
该函数的作用如下
879K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1y4q4)9J5k6e0c8Q4x3X3f1@1i4K6g2X3M7U0q4Q4x3@1q4T1K9h3!0F1K9h3y4Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3V1k6D9K9h3&6C8k6i4u0Q4x3X3g2U0M7s2m8Q4x3U0x3I4x3e0V1J5
调用单个初始化/析构函数。跳过NULL和-1(无效地址), 调用完成后重新设置soinfo_pool为可写(因为被调用的函数可能调用了dlopen/dlclose修改了数据结构)
遍历函数指针数组(如init_array), 逐个调用CallFunction。支持正序/逆序遍历(init正序, fini逆序)
64位 linker (Android 10.0.0_r47) 在 32 位的基础上新增了命名空间隔离、批量加载、随机序等特性
与32位流程类似, 但调用链略有差异: loadLibrary → loadLibrary0 → nativeLoad
Android 10的入口与4.4基本一致, 调用loadLibrary0替代了旧版的loadLibrary
ff1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3I4A6j5X3y4G2M7X3g2Q4x3V1k6G2K9X3I4#2L8X3W2Q4x3V1k6K6M7X3y4Q4x3V1k6E0j5h3W2F1i4K6u0r3K9X3q4$3j5g2)9J5c8X3A6S2N6X3q4Q4x3V1k6D9j5h3&6Y4i4K6u0r3f1%4W2K6N6r3g2E0i4K6u0W2K9X3q4$3j5b7`.`.
与32位的loadLibrary功能相同: 通过ClassLoader查找SO真实路径, 然后调用nativeLoad进入Native层。新增了BootClassLoader的特殊处理
8c2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3I4A6j5X3y4G2M7X3g2Q4x3V1k6G2K9X3I4#2L8X3W2Q4x3V1k6K6M7X3y4Q4x3V1k6E0j5h3W2F1i4K6u0r3K9X3q4$3j5g2)9J5c8X3A6S2N6X3q4Q4x3V1k6D9j5h3&6Y4i4K6u0r3f1Y4g2F1N6r3W2E0k6g2)9J5k6h3A6S2N6X3p5`.
Java层进入Native层的边界。与32位不同, Android 10多了一层JVM_NativeLoad跳转
native函数声明, 内部转发到三参数版本
247K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3I4A6j5X3y4G2M7X3g2Q4x3V1k6G2K9X3I4#2L8X3W2Q4x3V1k6K6M7X3y4Q4x3V1k6E0j5h3W2F1i4K6u0r3K9X3q4$3j5g2)9J5c8X3A6S2N6X3q4Q4x3V1k6D9j5h3&6Y4i4K6u0r3f1Y4g2F1N6r3W2E0k6g2)9J5k6h3A6S2N6X3q4Q4x3U0x3I4x3e0p5@1
nativeLoad的JNI实现。直接转发给JVM_NativeLoad, 这是Android 10新增的一层跳转
e15K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3I4A6j5X3y4G2M7X3g2Q4x3V1k6G2K9X3I4#2L8X3W2Q4x3V1k6K6M7X3y4Q4x3V1k6E0j5h3W2F1i4K6u0r3L8X3q4@1K9i4k6W2i4K6u0r3f1Y4g2F1N6r3W2E0k6g2)9J5k6h3x3`.
Android 10新增的跳转层。获取JavaVMExt实例后调用LoadNativeLibrary, 功能与32位的Runtime_nativeLoad对等
4b6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3q4J5N6q4)9J5c8X3!0H3k6h3&6B7k6r3E0B7N6X3#2Q4x3V1k6a6M7r3g2F1K9X3c8C8d9Y4k6E0i4K6u0W2j5$3x3`.
cbdK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3q4J5N6q4)9J5c8Y4u0#2L8Y4c8A6L8h3g2Q4x3V1k6B7L8X3W2Q4x3V1k6B7j5i4k6S2i4K6g2X3N6X3#2Q4y4h3k6W2P5s2c8Q4x3X3g2U0j5#2)9K6c8X3k6A6i4K6y4p5e0r3!0S2k6p5&6S2N6r3W2$3k6f1I4A6j5Y4u0S2M7Y4W2Q4x3U0y4x3L8$3q4V1e0X3q4@1K9i4k6W2e0r3W2T1M7X3q4J5P5b7`.`.
e04K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7i4y4&6M7%4c8W2L8g2)9J5c8X3y4G2M7X3g2Q4x3V1k6D9K9h3u0F1j5i4c8A6N6X3g2D9L8$3q4V1k6i4u0Q4x3V1k6F1j5i4c8A6N6X3g2Q4y4h3k6D9L8$3q4V1k6i4u0Q4x3X3g2U0M7s2l9`.
该函数内部有android_dlopen_ext和dlopen两种打开so的方式
android_dlopen_ext 是 Android 特有的扩展版本的动态库加载函数,它提供了比标准 dlopen更丰富的功能,主要用于:
当 caller_location 不为空且能找到对应的 boot_namespace 时,函数会优先使用 android_dlopen_ext 并指定 boot_namespace,这样可以确保 SO 文件在特定的命名空间上下文中加载,避免符号冲突。
当无法找到 boot_namespace 时(例如 caller_location 为空),函数会回退到使用标准的dlopen。这种情况下,SO 文件会在默认的全局命名空间中加载,这可能会导致符号冲突,但在某些兼容性场景下是必要的。
总而言之,大部分情况下走android_dlopen_ext分支,其他情况为了兼容性走dlopen分支
进入linker核心代码, 从android_dlopen_ext出发经过多层调用最终到达do_dlopen, 这是整个SO加载的真正入口
调用了__loader_android_dlopen_ext
415K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3u0A6L8$3&6A6j5#2)9J5c8X3I4A6j5X3c8D9i4K6u0r3L8r3W2T1k6r3I4Q4x3X3g2U0M7s2l9`.
android_dlopen_ext 函数的常见 flag 含义:
内建函数 __builtin_return_address(LEVEL) 用于返回当前函数或调用者的返回地址
函数的参数LEVEL表示函数调用链中的不同层次的函数,各个值代表的意义如下:
0 返回当前函数的返回地址
1 返回当前函数调用者的返回地址
2 返回当前函数调用者的调用者的返回地址
linker内部的转发函数, 直接调用dlopen_ext
900K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3u0A6L8$3&6A6j5#2)9J5c8X3I4A6L8X3E0W2M7W2)9J5c8X3c8D9k6X3y4F1i4K6u0W2j5%4m8H3i4K6t1K6x3e0b7$3
加锁后调用do_dlopen, 失败时格式化错误信息到dlerror缓冲区
b89K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3u0A6L8$3&6A6j5#2)9J5c8X3I4A6L8X3E0W2M7W2)9J5c8X3c8D9k6X3y4F1i4K6u0W2j5%4m8H3i4K6t1K6x3e0x3J5
a65K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6U0M7#2)9J5k6h3q4F1k6s2u0G2K9h3c8Q4x3X3g2U0L8$3#2Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0r3M7r3I4S2N6r3k6G2M7X3#2Q4x3V1k6K6N6i4m8W2M7Y4m8J5L8$3A6W2j5%4c8Q4x3V1k6Q4x3V1u0Q4x3V1k6S2L8X3c8J5L8$3W2V1i4K6u0V1x3e0m8Q4x3X3f1H3i4K6u0W2x3q4)9#2k6Y4t1@1y4#2)9K6b7h3u0A6L8$3&6A6j5#2)9J5c8X3I4A6L8X3E0W2M7W2)9J5c8X3I4A6L8X3E0W2M7W2)9J5k6h3y4H3M7q4)9J5x3K6t1%4x3e0R3`.
进入该函数后便正式进入linker的核心流程, 而该函数执行完毕后so便成功加载并且可以执行
该函数主要有2个核心功能:
调用find_library加载so并返回soinfo
调用soinfo.call_constructors()
其内部递归调用依赖库的constructors, 再调用自身init和init_array进行初始化