读书-程序员的自我修养-链接、封装与库(9: 第四章:静态链接(2)符号解析,重定位,COMMON块)
1. 符号解析与重定位
在完成空间和地址的分配步骤之后,链接器就进行符号解析与重定位。
1.1 重定位
先看a.o里面是怎么使用这两个外部符号的(iShared swap)。
也就是说a.c编译成指令时,它如何访问 iShared 变量的?
如何调用 swap 函数的?
objdump -d a.o //-d 参数看代码段反汇编结果
root@ubuntu-admin-a1:/home/4Chapter# objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
f: 00 00
11: 48 89 45 f8 mov %rax,-0x8(%rbp)
15: 31 c0 xor %eax,%eax
17: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
1e: 48 8d 45 f4 lea -0xc(%rbp),%rax
22: be 00 00 00 00 mov $0x0,%esi //main
27: 48 89 c7 mov %rax,%rdi
2a: b8 00 00 00 00 mov $0x0,%eax //iShared
2f: e8 00 00 00 00 callq 34 <main+0x34> //swap
34: b8 00 00 00 00 mov $0x0,%eax
39: 48 8b 55 f8 mov -0x8(%rbp),%rdx
3d: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
44: 00 00
46: 74 05 je 4d <main+0x4d>
48: e8 00 00 00 00 callq 4d <main+0x4d>
4d: c9 leaveq
4e: c3 retq
root@ubuntu-admin-a1:/home/4Chapter#
可以看到 main 的起始地址是 0x0000。
22: be 00 00 00 00 mov $0x0,%esi //main
因为未进行空间分配,都是0。
2a: b8 00 00 00 00 mov $0x0,%eax //iShared
2f: e8 00 00 00 00 callq 34 <main+0x34> //swap
objdump -d ab
我们再看目标文件ab的反汇编结果,其中main函数的两个重定位入口都已经被修正。
root@ubuntu-admin-a1:/home/4Chapter# objdump -d ab
ab: file format elf64-x86-64
Disassembly of section .init:
……
0000000000400546 <main>:
400546: 55 push %rbp
400547: 48 89 e5 mov %rsp,%rbp
40054a: 48 83 ec 10 sub $0x10,%rsp
40054e: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400555: 00 00
400557: 48 89 45 f8 mov %rax,-0x8(%rbp)
40055b: 31 c0 xor %eax,%eax
40055d: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp)
400564: 48 8d 45 f4 lea -0xc(%rbp),%rax
400568: be 38 10 60 00 mov $0x601038,%esi //iShared
40056d: 48 89 c7 mov %rax,%rdi
400570: b8 00 00 00 00 mov $0x0,%eax
400575: e8 1b 00 00 00 callq 400595 <swap> //swap
40057a: b8 00 00 00 00 mov $0x0,%eax
40057f: 48 8b 55 f8 mov -0x8(%rbp),%rdx
400583: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
40058a: 00 00
40058c: 74 05 je 400593 <main+0x4d>
40058e: e8 8d fe ff ff callq 400420 <__stack_chk_fail@plt>
400593: c9 leaveq
400594: c3 retq
……
root@ubuntu-admin-a1:/home/4Chapter#
修正以后 iShared 和 swap 的地址分别为: 0x38106000 0x1b000000(大端)
这里swap 的地址是个偏移量。
swap 的真实地址是: 0x0840057a + 1b = 0x08400595 正好等于swap的地址
1.2 重定位表
1.2.1 什么是重定位表?
链接器怎么知道哪些指令要被调整/重定位呢?怎么调整?
这就是重定位表的功能了。
重定位表专门保存这些与重定位相关的信息。
重定位表是ELF文件中的一个段。
1.2.2 查看重定位表 objdump -r a.o
root@ubuntu-admin-a1:/home/4Chapter# objdump -r a.o
a.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000023 R_X86_64_32 iShared
0000000000000030 R_X86_64_PC32 swap-0x0000000000000004
0000000000000049 R_X86_64_PC32 __stack_chk_fail-0x0000000000000004
……
root@ubuntu-admin-a1:/home/4Chapter#
可以看到 a.o 里面有两个重定位入口。
1.3 符号解析
1.3.1 常见链接失败例子
root@ubuntu-admin-a1:/home/4Chapter# ld a.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
a.o: In function `main':
a.c:(.text+0x23): undefined reference to `iShared'
a.c:(.text+0x30): undefined reference to `swap'
a.c:(.text+0x49): undefined reference to `__stack_chk_fail'
root@ubuntu-admin-a1:/home/4Chapter#
由于缺少符号的定义所以导致链接失败。一般是缺少某个库导致的。
1.3.2 readelf -s a.o
root@ubuntu-admin-a1:/home/4Chapter# readelf -s a.o
Symbol table '.symtab' contains 12 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS a.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 79 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND iShared
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail
root@ubuntu-admin-a1:/home/4Chapter#
UND 表示 undefined 未定义类型。
即 iShared swap 是外部符号,它们存在于全局符号表中。
1.4 指令修正方式
近址寻址和远址寻址
绝对寻址和相对寻址
寻址长度: 8 16 32 64位
绝对近址32位寻址
相对近址32位寻址
2 COMMON块
2.1 问题的出现
目前链接器不支持符号的类型,即链接器只知道符号的名字,不知道符号类型。
导致一个问题:
当我们定义多个符号定义类型不一致时,链接器怎么处理?
如: 多个弱符号怎么处理?
未初始化的全局变量是典型的弱符号
2.2 什么是 COMMON 块?
当不同的目标文件需要的COMMON 块空间大小不一致时,以最大的那块为准。
2.2.1 例子说明
当我们不同文件中定义两个同名的不同类型的未初始化的全局变量
a.c
int g_uinit_var;
b.c
double g_uinit_var;
即两个文件链接后输出文件中的 g_uinit_var 所占空间大小为8字节。
2.3 代码例子说明
simple.c
#include<stdio.h>
int g_init_var = 100;
int g_uinit_var;
void func1(int i)
{
printf("%d\n",i);
}
int main()
{
static int iStaticVar = 101;
static int iStaticVar2;
int a = 1;
int b;
func1(iStaticVar + iStaticVar2 + a + b);
return 0;
}
查看 g_uinit_var
root@ubuntu-admin-a1:/home# readelf -s simple.o
Symbol table ‘.symtab’ contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
……
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 g_init_var
12: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM g_uinit_var
13: 0000000000000000 34 FUNC GLOBAL DEFAULT 1 func1
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
15: 0000000000000022 53 FUNC GLOBAL DEFAULT 1 main
root@ubuntu-admin-a1:/home#
可以看到 未初始化的全局变量 g_uinit_var 的标志位 COM 即 COMMON块
来源于: 早期的Fortran。
2.4 为什么会有 COMMON 块?
编译器和链接器允许不同类型的弱符号存在。
本质原因是链接器不支持符号类型,即链接器无法判断各个符号的类型是否一致。