首页
社区
课程
招聘
[原创]Android从ELF-Loader到自定义Linker的实现及原理
发表于: 4小时前 140

[原创]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_NEEDEDd_val是字符串表中的偏移, 指向依赖库名 (如libc.solibm.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-Loader64-run-result

理解了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_phoffe_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位流程类似, 但调用链略有差异: loadLibraryloadLibrary0nativeLoad

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进行初始化


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 5
支持
分享
最新回复 (2)
雪    币: 60
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
严肃学习
4小时前
0
雪    币: 12560
活跃值: (8751)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
3
x1a0f3n9 严肃学习
风佬带带
4小时前
0
游客
登录 | 注册 方可回帖
返回