7章: 动态链接
1 引入
(1) 静态链接 2个问题
1) 2个进程 (prog1 / prog2) 共享 .o 时, 磁盘和内存中 要存 2 份 .o
=> 空间资源浪费
2) 1个 (不依赖其他 .o 的) .o 变, 所有 .o 要重新链接
=> 程序 更新、部署、发布 难
|
| 引入
|/
(2) 动态链接
1) 思想: 把 链接/重定位
过程 (从 静态链接的 链接时
) 延迟到 装载/运行时
[1] 多进程 `共享的 .o` 在磁盘和内存中 `只存 1 份`
[2] 1个 .o(不依赖其他 .o) 变 -> 只要 覆盖该 .o
[3] 可扩展性&兼容性 好, 插件(Plug-in)式:
运行时 可 动态(选择)装载
各模块
2) 实现: 把程序 按模块拆分
成各 (相对) 独立 的 part
, 程序 装载/运行时 再链接
动态链接文件
Linux
`动态共享对象` DSO (Dynamic Shared Objects); 即 `elf 动态链接文件`
.so
C 运行库 glibc: /lib/libc.so
Windows
动态链接库 DLL (Dynamical Linking Library)
.dll
Note: 理论上可直接用 .o 进行动态链接
3) 动态链接符号
(运行时)地址空间: 装载时
, 由 装载器 动态分配
2 例子
proj1.c -> 编译 —— —— —— -> proj1.o
\ 共 |
\ Linker
\ |
Lib.c -> 编译 -> Lib.so: 动态符号 foobar
/ |
/ 享 gcc -fPIC -shared -o Lib.so Lib.c
proj1.c
linker 发现 proj1.o 和 proj2.o 中引用的 符号 foobar 是 Lib.so 中的 动态符号 => `重定位 延迟到 装载时`
3 地址无关代码
(1) 共享对象 固定装载地址: 地址冲突
(2) 装载时 重定位
问题: 指令无法
在 多进程间 共享
解决:
(3) 地址无关 代码 PIC (Position-Independent Code)
1) 思想: 将 指令中 需要重定位
的部分 分离
, 与 数据 放一起
2) 共享对象
中 4 种寻址模式
共享模块 中 地址引用
按 是否跨模块
分为2类: 模块 内/外 引用
——————————————————————————————————————————————————————————
4种 地址引用
——————————————————————————————————————————————————————————
| 调用、指令跳转 | 数据访问
——————————————————————————————————————————————————————————
模块内 | (1) 相对 跳转、调用 | (2) `相对` 地址访问
模块外 | (3) 间接 跳转、调用 (GOT) | (4) `间接` 访问 (GOT)
——————————————————————————————————————————————————————————
[1] 模块内 地址引用: 相对 位置固定
1] `callee 与 caller` 间: `相对 地址调用 ( call )`
2] `指令` 与它要访问的 `数据` 间: 只需 相对于 PC 所指当前指令 加上 一 固定偏移
转化为
- - - > 相对于 `PC 所指 call <__i686.get_pc_thunk.cx> 指令
的 next 指令( 函数 ...get_pc_thunk... 返回地址: 放 %ecx) 加上一固定偏移
[2] 模块外 地址引用: GOT (Global Offset Table, 全局偏移表)
pointer array
各 表项 `指向` 放在 `进程虚拟内存 数据段(.data VMA) 中 地址相关的 func/var`
3) -fPIC / -fPIE: 产生 地址无关代码
4 延迟绑定(Lazy Binding)
动态链接下, 一开始就 link 所有 func => 程序 启动速度 慢
func 第1次 被用到时, 才绑定 (符号查找、重定位)
=> 加快 程序启动速度
实现: PLT (Procedure Linkage Table): GOT
+ 增加1个 中间层 间接跳转
1) PLT
_dl_runtime_resolve(moduleReferingFunc, func)
将 func/bar 最终映射在 `.data VMA 中的 地址` 填到 GOT 的 `bar@GOT 表项`
|
2) PLT 结构 bar@GOT GOT 结构 |/
表项 —— —— —— —— —— —— —— ——> 表项 —— —— ——> `bar()` 在 进程虚拟空间 `.data VMA` 中 的 `地址`
GOT 中 指向 bar的 表项
5 动态链接 相关 结构 : 存在 ELF 可执行文件
中
动态链接
OS 装载 可执行文件
|
| 控制权 转交
|/
动态链接器 的 入口地址
| \
| \ Linux 下, 是
| \
| 共享对象 ld.so -> 被 OS 装载进 进程地址空间
|/
自身初始化 + 动态链接
|
| 控制权 转交
|/
可执行文件 的 入口地址
(1) .interp 段 // (interpreter 解释器)
1) 作用
`ELF 可执行文件` 中 `.interp 段 决定 `动态链接器 的 位置`
2) 内容 Linux 下
字符串 = 可执行文件 所需 动态链接器的 路径 —— —— ——> /lib/ld-linux.so.2
(2) .dynamic 段
存 动态链接器 所需基本信息
动态链接 符号表位置/重定位表位置
依赖于哪些共享对象
共享对象 初始化代码 的 地址
(3) 动态符号表 .dynsym
依赖于
proj1 — —— —> Lib.so
\ |
\ | 定义 -> foobar 是 Lib.so 的 导出函数 (Export Function)
引用 \ |
| \/ |/
| foobar() 函数
|
|/
foobar 是 proj1 的 `导入函数 (Import Function)`
动态符号表 .dynsym
(4) 动态链接 重定位表
可执行文件 / 共享对象, 一旦 `依赖于 other 共享对象` ( 即, `有 导入导出符号`)
其 code 或 data 中就会 `引用 导入符号`
导入符号地址 在 运行时 才确定
=> 运行时 重定位
用 PIC 的 可执行文件/共享对象 也需要 重定位
|
|
|/
代码段 中 绝对地址的引用 被 分离 -> 变成 GOT -> 放 数据段
数据段 中 除了 GOT 之外, 还有 数据本身 绝对地址的引用
————————————————————————————————————————————————————————————
重定位表 | 修正 data 引用 | 修正 函数引用
/修正的位置 | |
————————————————————————————————————————————————————————————
静态链接 | .rel.data | .rel.text
————————————————————————————————————————————————————————————
动态链接 | .rel.dynamic + 数据段 | .rel.plt
| / .got(也在数据段) | / .got.plt
————————————————————————————————————————————————————————————
6 动态链接 步骤 与 实现
(1) 3 step
1) 动态链接器 `自举(BootStrap)`
——————————————————————————————————————
| 由谁完成 重定位 ?
——————————————————————————————————————
普通共享对象 | 动态链接器
——————————————————————————————————————
动态链接器 | 自身
(特殊共享对象)|
————————|——————————————————————————————
|/
`鸡生蛋, 蛋生鸡` 的 无限循环问题
|
| 解决
|/
2个问题
动态链接器
1> 本身 不能依赖 other 共享对象
-> 编程时 人为控制
2> 本身 所需 globalVar 和 staticVar 的 重定位 由自身完成
称
-> 启动时 用 精巧的代码 - - -> 自举
2) 装载 共享对象
[1] 从 可执行文件 开始 构建 `依赖(共享对象)关系图`
`.dynamic 段` 中, 入口为 `DT_NEEDED` 的 类型 -> 指出的是 `所依赖的共享对象`
[2] 图遍历: 广度优先 / 深度优先
打开 共享对象
读 ELFHeader 和 .dynamic 段
相应 代码/数据段 映射到 进程空间
符号表 合并到 `全局符号表`
3) 重定位 与 初始化
(2) Linux 动态链接器 实现
OS 内核 装载完 `ELF 可执行文件`
| 1) `ELFHeader 中 e_entry`
|/ /
返回 用户空间 / 静态链接 (没 .interp 段)
+ 控制权 转交给 程序入口
| \ 动态链接 (有 .interp 段)
| \
据 ELF 文件是否有 .interp 段 \
\
2) 内核 分析 `动态链接器 地址(在 .interp 段)`
|
|
|/
将 动态链接器 映射到 进程空间
|
|
|/
控制权 转交给 `动态链接器 的 e_entry`
共享库 与 可执行文件 没本质区别
ELFHeader 的 标志位 和 扩展名
(3) 3个问题 `动态链接器` (ld.so)
1) 本身 `是 动态/静态链接` ?
静态
它 不能依赖 other 共享对象
2) 本身 `必须是 PIC 吗` ?
可以是(更简单), 也可以不是(代码段 无法共享 / 自举时 还要对 代码段 重定位)
3) 可被当作 可执行文件 运行, 那它的 `装载地址` 是多少 ?
0x00000000 -> 无效地址 -> 作为 `共享库`, OS 内核 装载 ld.so 时, 会为其 选择合适的装载地址
7 显式运行时 链接
动态链接器 提供 3 个 API
(1) dlopen() 打开动态库
(2) dlsym() 查找符号
(3) dlerror() 错误处理
(4) dlclose() 关闭动态库
静态链接: 磁盘/内存 中 2 份共享库 Lib.o.jpg
静态链接: 磁盘/内存 中 1 份共享库 Lib.so/o.jpg
动态链接过程.jpg
4种 寻址模式.jpg
相对偏移 调用指令.jpg
模块 内 数据访问.jpg
模块 间 数据访问.jpg
模块间 调用、跳转.jpg
GOT 中 PLT.jpg
Lib.so 中 .got.plt 结构.jpg