4章 静态链接
2022-07-10 本文已影响0人
my_passion
2 个 目标文件 (.o) 如何链接成 可执行文件 (.out)
// a.c
extern int sharedd;
int main()
{
int a = 100;
swap(&a, &shared);
}
// b.c
int shared = 1;
void swap(int* a, int* b)
{ /* ...*/ }
// 得 a.o b.o
$ gcc -c a.c b.c
4.1 空间与地址分配
段 / 符号 映射: 目标文件 -> 可执行文件 -> 进程虚拟空间
1 相似段合并 => 各 段虚拟地址 确定
.bss 段
在 .o elf 内 不占空间
装载时, 在 `进程虚拟空间` 才占空间
各 .o 的 .code 段 -> 合并: 放 elf 的 .code 段
.data 段 .data 段
————————————————————————————————————————————————
| VMA 虚拟内存地址
————————————————————————————————————————————————
链接前 | 所有段 的 VMA = 0: 虚拟空间 还没分配
————————————————————————————————————————————————
链接后 | VMA = `进程虚拟内存地址`
| 文件偏移 忽略
————————————————————————————————————————————————
2 各段内 符号
: 段 虚拟地址 + 段内相对偏移
.o
符号地址确定, 各 符号表 中 符号 定义/引用
elf
放 全局符号表, 函数符号 / 变量符号 放 .code 段 / .data 段
各符号 `段内相对偏移 固定`
.out
符号 虚拟地址 = 段虚拟地址 + 段内相对偏移`
例: 进程虚拟地址分配
elf: 默认从 0x0804 8000 开始分配
.code 段虚拟地址 = 0x0804 8094
main 符号 位于 a.o 的 .code 段 最开始
=> 段内 相对偏移 = 0
=> 进程虚拟空间 地址 = 0x0804 8094 + 0 = 0x0804 8094
全局符号表
——————————————————————————————————
符号 类型 (进程)虚拟地址
——————————————————————————————————
main 函数 0x0804 8094
swap 函数 0x0804 80c8
shared 变量 0x0804 9108
——————————————————————————————————
4.2 符号解析 与 重定位
1 符号解析
= 匹配 符号声明 与 符号定义 = 据 符号引用
去 全局符号表
查匹配的 符号(进程虚拟) 地址
无匹配 => `符号未定义` symbol undefined:
1) 符号 声明 与 定义 不一致
2) 目标文件 路径不对
3) 缺少 某个 库
2 重定位
linker
查 重定位表(.rel.data 段)
据 指令修正方式(绝对/相对 寻址修正) 调整
(1) 查 重定位表
得 .o 文件中 要重定位的 符号位置
: 0x1c / 0x27
$ objdump -r a.o
...
OFFSET TYPE VALUE
0x1c R_386_32 shared
0x27 R_386_PC32 swap
重定位表 是 array, elem 是 `重定位入口` struct
r_offset
重定位入口 的 偏移
r_info
重定位入口 的
类型 ( FUNC / VAR) : 低 8 位
-> 绝对/相对 寻址修正
符号 在 全局符号表 中 下标 : 高 24 位
+ 查 全局符号表 -> 符号虚拟地址
(2) compiler 不知道 所引用的 sym 地址, fill 临时假地址
查 .o 反汇编
-d(disassembly)
$ objdump -d a.o
...
0000 0000 <main>:
. 0x1c: 指令 (相对地址) 偏移
. | 0x0000 0000 = 0
. |/ /
18: c7 44 24 04 00 00 00 movl $0x0, 0x4(%esp)
1f: 00 \_ _ _
... \
26: e8 fc ff ff ff call 27 <main + 0x27>
/| \
| \
0x27 小端存储: 低地址存低字节 0xffff fffc = -4
(3) 指令修正
1) globalVar - 绝对寻址修正: .o / .elf -> 0 / 符号虚拟地址 (查 全局符号表 )
—————————————————————————————————————————————————————
| `重定位入口处` 的 值
—————————————————————————————————————————————————————
.o 中 | 固定填 0
—————————————————————————————————————————————————————
.elf 中 | 填 符号虚拟地址 (查 全局符号表 )
————————————————————————————————————————————————————
2) func -> 相对寻址修正: .o / .elf -> 临时假地址 / x = S + A - P
S: callee 符号虚拟地址
A: 临时假地址 = 要修正的长度 的负值, 通常 = -4
P: caller 中 要修正的指令位置 = 引用 funcSym 的 .code 段(本例是 main)起始地址 + 重定位表 中 OFFSET
P - A = call S 指令的 next 指令的地址
P - A + x = S
S = 0x0804 80c8
A = -4
P = 0x0804 8094 + 0x27 = 0x0804 80bb
S + A - P = 0x09
——————————————————————————————————————————————————————————————————————————
| 重定位入口 处 的值
——————————————————————————————————————————————————————————————————————————
.o 中 | 临时假地址 = `要修正的长度 (-A)` : Compiler 可计算出
——————————————————————————————————————————————————————————————————————————
.elf 中 | x = S + A - P = 0x09
—————————————————————————————————————————————————————————————————————————
=> 查 elf 可执行文件 的 反汇编
$ objdump -d ab
...
0x0804 8094 <main>: shared
... /
0x0804 80ac: c7 44 24 04 08 91 04 movl $0x8049708, 0x4(%esp)
0x0804 80b3: 08
...
0x0804 80ba: e8 09 00 00 00 call 80480c8<swap>
0x0804 80bf: ... \ \
| \ \
|_ _ _ _ _ _ _ _\ + = 0x09 + 0x0804 80bf = 0x0804 80c8
|
call 指令 的 next 指令 的 地址
相似段合并.jpg
段映射 / 段地址分配.jpg
绝对/相对 地址指令.jpg
4.3 COMMON 块
弱符号 机制
(1) 允许 `同名` 但 `不同类型` 的 `符号定义` 存在于 多个文件, 只要它们 `未初始化`
(2) 典型 弱符号
uninit_global_var
(3)
——————————————————————————————————————————————————————————————
| 处理 弱符号, 为啥 (在 .o 中) 不放 .bss 段 ?
——————————————————————————————————————————————————————————————
编译器 | 将 `1个编译单元` 编译成 `1个 目标文件 .o` 时,
| `不知道` 弱符号 `最终应占的空间大小`
| => 无法 在 .o 中 为其 分配/预留 空间 (大小)
| => 无法 在 .o 中 将其放 .bss 段
| => 标记为 `符号表` 中的 `COMMOM 型`
——————————————————————————————————————————————————————————————
链接器 | 据 所有 .o 可确定 弱符号 大小
| => .elf 文件中 可将其放 `(虚拟) .bss 段`
| => 最终放 进程虚拟内存 中 .bss 段
——————————————————————————————————————————————————————————————
(4) note
链接器
1) 不知道 变量类型 (int / float 等)
2) 知道 变量大小(size)
3) 能识别 变量 在/是 `COMMON 型`/弱符号
uninit_local_static_var 强符号 -> 编译器 会放 .o 的 .bss 段
(5)
————————————————————————————————————————————————————————————————————
多个 `同名 不同类型` | `链接器` 如何处理?
的 强/弱符号 | 最终在 .elf/进程虚拟空间 中 `所占空间大小`
————————————————————————————————————————————————————————————————————
1) 2 强 | 报错 `符号重定义` (symbol multidefined)
————————————————————————————————————————————————————————————————————
2) 1 强 other 弱 | = 强符号大小
————————————————————————————————————————————————————————————————————
3) 全 弱 | 按 COMMON 型 链接规则: = size 最大者 的 size
————————————————————————————————————————————————————————————————————
4.4 C++ 去重 -> 函数级链接 / 全局构造与析构函数 ( 链接后 放 .init / .fini 段) / ABI
(1) C++ 中 `必须由 编译器 和 链接器 共同协作` 才能完成的 `2 个特性`
1) 去重: 消除 重复代码
产生 `重复代码` 的
1> 表现:
在 `不同 编译单元 (.o)` 生成 `相同 code`
2> 3个 弊端:
1] 空间浪费
2] 指向 同一函数 `2个 函数指针 可能不相等`
3] 指令 cache 的 命中率 降低
CPU 缓存 指令和数据 + 同一份指令 有多份 copy
3> 原因:
1] 模板
本质上 像 宏
模板 在 `某个编译单元 被 实例化` 时, `它(模板) 并不知道` 自己是否在 `other 编译单元` 也被实例化
=> 模板 在 多个编译单元 同时实例化为 `相同类型` 时, 产生 重复 code
4> 解决
模板 的 `每份 实例化代码 单独放 1 个段`
add<T>()
编译器
1.o 2 个段
.temp.add<int>
.temp.add<float>
2.o 2 个段
.temp.add<int>
.temp.add<float>
|
| gcc 标识 `Link Once` 段
|/
链接器
.elf
2 个 .o 中 的 `同名(同实例化代码) 代码段`
`只取 1个` 放到 .elf 中 .code 段
|
|
gcc / visual C++ 都是这种做法
gcc
编译器
将 `模板 实例化代码 所放段` 标识为 `Link Once 段`
段名
.gnu.linlonce.decoratedNameOfTemplateInstantiation
2] vtbl
含 vf 的 class
编译器 在 `每个 引用 该 class` 的 `编译单元(.o)` 中都会生成 `class 相应的 vtbl` -> 代码重复
3-6] extern inline func
默认 ctor / copy ctor / operator=
4> 解决
都类似 模板 的去重思路
|
| 问题: 不同 编译单元(.o) 的 编译器版本/编译优化选项不同
| => 同名段 content 不同
|/
编译器 任选 1 个 作 链接输入 + 给出 warning
|
| 由模板 去重思路 延伸到 `优化/减小 链接后 的 .elf 文件 size`:
| 对 任一 `函数 或 变量` -> 放 .o 中 `独立段`
|/
`函数(/变量) 级别链接`
只 引用 .o 中 某个 func/var 时,
`不用将 .o 整体 链接`, `只对 所用到的 func/var 进行 链接`
|
| 好处: 链接输出的 `.elf 文件 变小`
|
| 代价: 编译 / 链接 变慢
| 所有函数/var 放 独立段 => 段数量大大增加
| =>
| 1> .o 文件变大
| 2> 链接器 重定位过程 复杂
|/
gcc 编译链接选项: -ffunction-sections / fdata-sections
2) 全局构造 与 析构: 在 main 之前/后 执行
Linux 系统 程序入口: _start -> Lunux 库 Glibc 的 part
.elf 文件 2 个特殊段
`.init / .fini 段`
存 进程 初始化/终止 的 可执行指令
main 之前/后, Glibc 安排执行 .init / .fini 段 可执行指令(代码)
=> C++ 全局 构造/析构 函数 -> 链接后 放 .init / .fini 段
(2) C++ 可执行代码 二进制兼容性 —— ABI (Application Binary Interface) 二进制层面的接口
要考虑 ABI 的 原因:
重载 / 继承 / vf / 异常 机制, 使得 C++ 背后的 数据结构 非常复杂, 在 不同 编译器和链接器 之间 不可移植
两个编译器 编译出的 目标文件 能链接 的 条件
同
目标文件格式
符号修饰规则
内存分布
函数调用方式
许多团体和社区都在致力于 C++ ABI 标准的统一
4.5 静态库 链接
(1) 程序 I/O
|
| 用
|/
scanf / printf
| 是对 OS 的 API 的 wrap (包装)
|
| 调
|/
Linux 下 API: write 的 system call
(2) 静态库
是一组 `目标文件(.o) 的 集合`
Linux 下 C 语言 `静态库 libc 文件`: /user/lib/`libc.a` 是 `glibc 项目` 的一部分
glibc 是用 C 语言开发的, 由 数千个 C 源文件组成
编译输出 数量相同的 目标文件
scanf.o / printf.o
fread.o / fwrite.o
malloc.o
静态库链接.jpg