静态链接
静态链接涉及的内容包含如下
- 空间地址的分配
- 符号解析和重定位
- 静态库链接
本文的测试代码以及其他文件存在地址 CSFoundationLearning#les4
准备工作
首先需要准本两个源文件a.c和b.c,文件的内容如下:
[root@localhost linux]# cat a.c
extern int shared;
int main()
{
int a = 100;
swap(&a, &shared);
return 0;
}
[root@localhost linux]# cat b.c
int shared = 1;
void swap(int * a, int * b)
{
*a ^= *b ^= *a ^= *b;
}
使用 gcc -c
只编译不链接生成对应的目标文件
[root@localhost linux]# gcc -c a.c
[root@localhost linux]# gcc -c b.c
[root@localhost linux]# ls
a.c a.o b.c b.o
空间地址的分配
对于有多个目标文件的链接情况,存在两种地址空间分配的策略按序叠加和相似段合并,最后进行符号地址的确定,下面具体分析这两种情况
按序叠加
这是一种最简单的方案:直接把目标文件依次合并
这种分配策略有两个缺点:
- 段很多并且零散,每个文件有m个段,n个文件就会产生m*n个段
- 浪费空间,段要求地址和空间对其(x86硬件平台是一个页,也就是4096字节),零散的段就会造成空间的浪费
因此,这个方案实际并不可行,所有分析另一种方案
相似段合并
相似段合并,顾名思义就是把相同类型的段合并在一起,比如.text段分为一组合并,.data段分为一组合并,这样可以解决按序叠加这种分配策略带来的问题
何为地址和空间
地址和空间,会存在两种解释:
- 链接输出可执行文件中的空间
- 装载后的虚拟地址空间
对于这两种情况 - 有实际数据的段,文件和虚拟地址空间都存在
- 没有实际数据的段,只有在虚拟地址空间客观存在
在链接阶段,链接器为目标文件分配地址和空间,这里谈到的地址空间只关注与虚拟地址空间的分配,因为这个关系到链接器后面的关于地址计算的步骤,与文件中的空间关系不大。
真实的链接策略
使用ld
命令链接目标文件生成可执行文件,其中
- -e 表示可执行文件入口函数
- -o 表示可执行文件的名称
[root@localhost linux]# ld a.o b.o -e main -o ab
[root@localhost linux]# ls
ab a.c a.o b.c b.o
使用objdump
查看目标文件和链接生成的可执行文件的段属性
[root@localhost linux]# objdump -h a.o
a.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000002c 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 0000006c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 0000006c 2**2
ALLOC
3 .comment 0000002d 0000000000000000 0000000000000000 0000006c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000099 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000a0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[root@localhost linux]# objdump -h b.o
b.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000004c 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000004 0000000000000000 0000000000000000 0000008c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000090 2**2
ALLOC
3 .comment 0000002d 0000000000000000 0000000000000000 00000090 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 000000bd 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 000000c0 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
[root@localhost linux]# objdump -h ab
ab: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000078 00000000004000e8 00000000004000e8 000000e8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .eh_frame 00000058 0000000000400160 0000000000400160 00000160 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000004 00000000006001b8 00000000006001b8 000001b8 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .comment 0000002c 0000000000000000 0000000000000000 000001bc 2**0
CONTENTS, READONLY
根据以上的数据,发现合并的段数量没有变多,段的大小(Size值)变大了,针对.text段和.data段分析,合并之后段的大小如下图所示
链接之后可以看到之前为空的VMA(Virtual Memory Address 虚拟地址)都分配的了对应的虚拟地址空间,.text段的VMA为00000000004000e8,偏移File off为000000e8,因为64位的Linux系统进程的虚拟地址空间分配规则是从0000000000400000开始的。
符号解析和重定位
使用objdump -d
查看目标文件的反汇编结果
查看未链接的目标文件a.o的反汇编结果:
[root@localhost linux]# objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25
25: b8 00 00 00 00 mov $0x0,%eax
2a: c9 leaveq
2b: c3 retq
其中:
-
13: be 00 00 00 00 mov $0x0,%esi
这条指令表示的是对shared变量的引用 -
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
18: 48 89 c7 mov %rax,%rdi
这几条指令表示的是对变量a的赋值,最终保存在rax寄存器中 -
20: e8 00 00 00 00 callq 25
这条指令表示函数swap的调用
从上面的结果可知,编译阶段,shared变量的引用和函数swap的调用地址都是为0,到了链接节点,才会把地址指向虚拟地址空间的地址,下面通过查看链接之后的汇编代码,找到这两个符号发生了那些变化。
查看链接之后的ab的反汇编结果:
[root@localhost linux]# objdump -d ab
ab: file format elf64-x86-64
Disassembly of section .text:
00000000004000e8 :
4000e8: 55 push %rbp
4000e9: 48 89 e5 mov %rsp,%rbp
4000ec: 48 83 ec 10 sub $0x10,%rsp
4000f0: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
4000f7: 48 8d 45 fc lea -0x4(%rbp),%rax
4000fb: be b8 01 60 00 mov $0x6001b8,%esi
400100: 48 89 c7 mov %rax,%rdi
400103: b8 00 00 00 00 mov $0x0,%eax
400108: e8 07 00 00 00 callq 400114
40010d: b8 00 00 00 00 mov $0x0,%eax
400112: c9 leaveq
400113: c3 retq
0000000000400114 :
# 省略swap函数的实现代码
发生的变化如下:
-
4000fb: be b8 01 60 00 mov $0x6001b8,%esi
这条指令表示的是对shared变量的引用 -
400108: e8 07 00 00 00 callq 400114
这条指令表示函数swap的调用
可以看到对应的地址重定位到了了对应的变量和函数的虚拟地址空间的地址,为什么是这些地址,分析如下
- 0x6001b8 对应的是
shared
的地址,从前面的objdump -h ab
看到.data
段的地址为00000000006001b8
,因为.data
段中只保存一个值就是shared
变量,所以0x6001b8
就是变量shared
的地址 - 400114 对应的是函数
swap
的地址,从objdump -d ab
的结果000000000400114
就可以直接看到: swap
的地址了,call 是一条近地址相对位移调用指令,他的下一条指令mov地址是0x40010d,最终的地址为0x40010d+0x7=0x400114,对应的是swap的地址
以上介绍了链接的策略以及链接符号的地址重定位的变化过程,在链接的步骤中有哪些符号是需要重定位的呢?接下来就是要介绍的内容。
重定位表
ELF文件中定义了一个重定位表段,文件定义了需要在链接阶段进行重定位的符号,使用 objdump -r
命令查看a.o
目标文件的重定位表信息如下
[root@localhost linux]# objdump -r a.o
a.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000014 R_X86_64_32 shared
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
RELOCATION RECORDS FOR [.eh_frame]:
OFFSET TYPE VALUE
0000000000000020 R_X86_64_PC32 .text
对应的定义在 /usr/lib/elf.h 头文件中的重定位表信息的结构体如下
/* Relocation table entry without addend (in section of type SHT_REL). */
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
字段说明如下:
下面还是列出 objdump -d a.o
反汇编的结果,和 objdump -r a.o
重定位表信息进行对照分析
[root@localhost linux]# objdump -d a.o
a.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 :
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
f: 48 8d 45 fc lea -0x4(%rbp),%rax
13: be 00 00 00 00 mov $0x0,%esi
18: 48 89 c7 mov %rax,%rdi
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 00 00 00 00 callq 25
25: b8 00 00 00 00 mov $0x0,%eax
2a: c9 leaveq
2b: c3 retq
-
0000000000000014 R_X86_64_32 shared
该重定位信息的的值为14,即13: be 00 00 00 00 mov $0x0,%esi
指令的操作数部分的地址,也就是shared变量地址 -
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
该重定位信息的值为21,即20: e8 00 00 00 00 callq 25
指令的操作数部分的地址,也就是call函数地址变量
上面的分析我们看到了符号解析以及指令修正的结果,接下来回具体的分析符号的解析和指令的修正过程
符号解析和指令的修正
从上面 objdump -r a.o
的结果看到了重定位的两种类型 R_X86_64_32
和 R_X86_64_PC32
,解释如下,下表中的386表示的是32位的,X86_64表示的是64位的,一一对应就行了,重定位修正方法是一致的。
其中:
- A=保存在被修改位置的值,重定位表可以查看该值,
0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
表示swap的值为-0x4 - P=被修改的位置(相对于段开始的偏移量或者虚拟地址),该值通过r_offset计算得到
- S=符号的实际地址,f_info的高24位指定的符号实际地址
下面针对20: e8 00 00 00 00 callq 25
该指令进行分析指令的修正 ,假设main函数地址为0x1000,swap函数地址为0x2000,重定位表信息0000000000000021 R_X86_64_PC32 swap-0x0000000000000004
看到修正的swap位置的值为 0x0000000000000004,并且是类型为R_X86_64_PC32属于相对寻址修正,所有对应的S/A/P值如下:
- S=0x2000
- A=-0x04
- P=0x1000+0x21=0x1021
地址修正:S+A-P=0x2000+(-0x04)-(0x1021) = 0xFDB
...
20: e8 db 0f 00 00 callq 0xfdb
25: b8 00 00 00 00 mov $0x0,%eax
...
实际调用的地址是下一条指令的起始地址加上偏移量,即 0xFDB+0x1025=0x2000,也就是swap函数的虚拟地址
静态库链接
以C的静态库 libc.a 分析
使用命令objdump -t libc.a | grep printf
查找libc.a文件中的printf符号,可以看到printf符号位于printf.o目标文件中
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ objdump -t libc.a | grep printf
...
fprintf.o: file format elf64-x86-64
0000000000000000 g F .text 000000000000008f __fprintf
0000000000000000 *UND* 0000000000000000 vfprintf
0000000000000000 g F .text 000000000000008f fprintf
0000000000000000 w F .text 000000000000008f _IO_fprintf
printf.o: file format elf64-x86-64
0000000000000000 g F .text 000000000000009e __printf
0000000000000000 *UND* 0000000000000000 vfprintf
0000000000000000 g F .text 000000000000009e printf
0000000000000000 g F .text 000000000000009e _IO_printf
...
libc.a静态库文件其实是目标文件的一个组合,使用 ar -t
命令查看静态库中的所有目标文件,可以看到里面包含了许多的目标文件
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ar -t libc.a
init-first.o
libc-start.o
sysdep.o
version.o
check_fds.o
libc-tls.o
elf-init.o
dso_handle.o
errno.o
init-arch.o
errno-loc.o
hp-timing.o
iconv_open.o
iconv.o
iconv_close.o
gconv_open.o
...
使用 ar -x
命令把静态库中的所有目标文件解压到当前文件夹
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ar -t libc.a
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ ls
C-address.o getaliasname_r.o mkdtemp.o spawn.o
C-collate.o getauxval.o mkfifo.o spawn_faction_addclose.o
C-ctype.o getc.o mkfifoat.o spawn_faction_adddup2.o
C-identification.o getc_u.o mknod.o spawn_faction_addopen.o
C-measurement.o getchar.o mknodat.o spawn_faction_destroy.o
C-messages.o getchar_u.o mkostemp.o spawn_faction_init.o
C-monetary.o getclktck.o mkostemp64.o spawnattr_destroy.o
...
下面以简单的 hello.c
文件为例,做个简单的测试,因为 hello.c
文件中只包含引用符号 printf
,而 printf
符号位于 printf.o
文件中,所以链接的时候单独链接 printf.o
文件,看下结果如何
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/hello$ ld hello.o ../ubuntu_libc/printf.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
hello.o: In function `main':
hello.c:(.text+0xa): undefined reference to `puts'
../ubuntu_libc/printf.o: In function `__printf':
(.text+0x6e): undefined reference to `stdout'
../ubuntu_libc/printf.o: In function `__printf':
(.text+0x92): undefined reference to `vfprintf'
链接发生了错误,因为 printf.o
文件本身有对其他目标对象符号的引用,可以看到对 stdout
和 vfprintf
这两个符号有引用,类型是UND的,所以还需要链接对应的目标文件才行,这是一个递归的过程,使用 gcc
自动编译链接的时候会自动处理,所以不在深入研究了。
aron@ubuntu:~/gitrepo/CSFoundationLearning/les4/linux/ubuntu_libc$ objdump -t printf.o
printf.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 g F .text 000000000000009e __printf
0000000000000000 *UND* 0000000000000000 stdout
0000000000000000 *UND* 0000000000000000 vfprintf
0000000000000000 g F .text 000000000000009e printf
0000000000000000 g F .text 000000000000009e _IO_printf
总结
以上就是对静态链接过程的一个学习型的总结,如有不妥之处还请不吝赐教