静态链接
一、空间与地址分配
这里的“空间和地址”有两个含义:第一,在输出的可执行文件ab.o中的空间;第二,是在装载后的虚拟地址中的虚拟地址空间。
现在的链接器的策略基本上都是:将a.o和b.o中相似段合并(如.text和.text段合并),然后再分配空间(ab文件中分配空间基本上是两个.text段加起来的大小;类似地,在虚拟地址空间中也去指定一段空间)。见P102图
实验:观察静态链接过程的“空间和地址分配”:
1. 将a.o和b.o链接成ab.o
两个源代码文件:
/*a.c*/ extern int shared; int main(){ int a=100; swap(&a, &shared); }
和
/*b.c*/ int shared =1 ; void swap(int *a, int* b){ *a^=*b^=*a^=*b; }
$gcc -c a.c b.c ==> 使用-c参数只完成预处理、编译、汇编,(不链接),产生a.o和b.o
$ld a.o b.o -e main -o ab ==> 链接a.o和b.o,指定main()作为程序入口(默认入口是_start),指定输出ab
2. 观察“空间和地址分配情况”
$ readelf -S a.o 或 objdump -h a.o
$ readelf -S b.o 或 objdump -h b.o
$ readelf -S ab 或 objdump -h ab
二、符号解析与重定位
上面虽然解决了地址和空间的分配,但是对于a.o目标文件引用到b.o中“变量shared”和"函数swap"的问题还为解决。每个目标文件都可能定义一些符号,也可能引用到其他目标文件的符号,输入链接器的目标文件的符号表会组成一个“全局符号表”;在“符号解析”的过程中找到了一些在a.o中未定义、引用其他目标文件中定义的符号;而将a.o中这些未定义的符号的占位符(目标文件中某些偏移处),修改为正确的符号变量地址/函数入口地址(注:也是“虚拟地址空间中的地址”),进而产生可执行文件ab的过程,称为“指令修正/重定位”。
实验:符号解析和指令修正
1. 通过查看a.o的重定位段(又称为“重定位表”),可以找到未定义的符号。这些符号必须通过ld链接从其他模块获取,否则报错:一般地,UND这种未定义的符号都是因为该目标文件中有关于它们的重定位项,所以在链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就会报“符号未定义”错。
[hadoop@sam1 mydir]$ readelf -s a.o
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS a.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 6
6: 00000000 0 SECTION LOCAL DEFAULT 5
7: 00000000 39 FUNC GLOBAL DEFAULT 1 main
8: 00000000 0 NOTYPE GLOBAL DEFAULT UND shared
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND swap
2. 反汇编a.o和ab,可以看到指令修正的痕迹。
[hadoop@sam1 mydir]$ objdump -r a.o ==> 查看“重定位表”(即“重定位段”),可知文件a.o中分别偏移0x15和0x21两个位置的4个字节需要“重定位”(或称“符号修正”)。
a.o: file format elf32-i386
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000015 R_386_32 shared ==> 每个需要重定位之处(目标文件中某个偏移)叫“重定位入口”
00000021 R_386_PC32 swap
Sam: 从上面可以看到重定位表的结构:一个需要重定位的section对应有一个重定位段;是一个数组,每一个元素记录的是某个偏移处需要被重定位的变量(函数)名称。
[hadoop@sam1 mydir]$ objdump -d a.o
a.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 20 sub $0x20,%esp
9: c7 44 24 1c 64 00 00 movl $0x64,0x1c(%esp)
10: 00
11: c7 44 24 04 00 00 00 movl $0x0,0x4(%esp)
18: 00
19: 8d 44 24 1c lea 0x1c(%esp),%eax
1d: 89 04 24 mov %eax,(%esp)
20: e8 fc ff ff ff call 21 <main+0x21>
25: c9 leave
26: c3 ret
[hadoop@sam1 mydir]$ objdump -d ab
ab: file format elf32-i386
Disassembly of section .text:
08048094 <main>:
8048094: 55 push %ebp
8048095: 89 e5 mov %esp,%ebp
8048097: 83 e4 f0 and $0xfffffff0,%esp
804809a: 83 ec 20 sub $0x20,%esp
804809d: c7 44 24 1c 64 00 00 movl $0x64,0x1c(%esp)
80480a4: 00 ==> 这里4个字节本类被修正为“shared的地址” (P109 “绝对近址32位寻址”方式,重定位入口修正)
80480a5: c7 44 24 04 f8 90 04 movl $0x80490f8,0x4(%esp)
80480ac: 08
80480ad: 8d 44 24 1c lea 0x1c(%esp),%eax
80480b1: 89 04 24 mov %eax,(%esp)
80480b4: e8 03 00 00 00 call 80480bc <swap> ==> 这里4个字节被修正为“swap的入口地址”(P109 “相对近址32位寻址”方式,重定位入口修正)
80480b9: c9 leave
80480ba: c3 ret
80480bb: 90 nop
080480bc <swap>:
80480bc: 55 push %ebp
80480bd: 89 e5 mov %esp,%ebp
80480bf: 53 push %ebx
80480c0: 8b 45 08 mov 0x8(%ebp),%eax
80480c3: 8b 10 mov (%eax),%edx
80480c5: 8b 45 0c mov 0xc(%ebp),%eax
80480c8: 8b 08 mov (%eax),%ecx
80480ca: 8b 45 08 mov 0x8(%ebp),%eax
80480cd: 8b 18 mov (%eax),%ebx
80480cf: 8b 45 0c mov 0xc(%ebp),%eax
80480d2: 8b 00 mov (%eax),%eax
80480d4: 31 c3 xor %eax,%ebx
80480d6: 8b 45 08 mov 0x8(%ebp),%eax
80480d9: 89 18 mov %ebx,(%eax)
80480db: 8b 45 08 mov 0x8(%ebp),%eax
80480de: 8b 00 mov (%eax),%eax
80480e0: 31 c1 xor %eax,%ecx
80480e2: 8b 45 0c mov 0xc(%ebp),%eax
80480e5: 89 08 mov %ecx,(%eax)
80480e7: 8b 45 0c mov 0xc(%ebp),%eax
80480ea: 8b 00 mov (%eax),%eax
80480ec: 31 c2 xor %eax,%edx
80480ee: 8b 45 08 mov 0x8(%ebp),%eax
80480f1: 89 10 mov %edx,(%eax)
80480f3: 5b pop %ebx
80480f4: 5d pop %ebp
80480f5: c3 ret
注:对于32位x86平台的ELF文件的重定位入口修正的指令寻址方式只有以上两种。这两种方式 每个被修正的位置的长度都是4个字节。
三、COMMON块 (P112)
(1)编译为目标文件时,未初始化的局部静态变量就在.bss段中分配空间了;
(2)编译为目标文件时,未初始化的全局变量是弱符号,占用空间大小未知(因为其他编译单元中所占用的空间可能比本编译单元所占空间要大,最终分配空间取最大的一个),因此暂时标记为common;当链接器读取所有输入目标文件之后,才能最终确定在.bss段中分配的空间——因此,最终还是被要在.bss段中分配空间的。
实验:链接之前,未初始化的全局变量尚未放在.bss段中(而是被标记为COMMON)
C代码test.c
int printf(const char* format, ...); int global_init_var=84; //.data int global_uninit_var; //COM, 可能在别的文件中被定义 static int static_global_init_var=84; //.data static int static_global_uninit_var; //.bss, 只在本文件中被用到 void func1(int i){ printf("%d\n",i); } int main(void){ static int static_var=85; //.data static int static_var2; //.bss, 只在本文件中被用到 int a=1; //stack int b; //stack func1(static_var + static_var2 + a + b); return a; }$ gcc -c test.c -o test.o
$ readelf -s test.o
Symbol table '.symtab' contains 18 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 static_global_init_var
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5
7: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 static_var.0
8: 0000000000000000 4 OBJECT LOCAL DEFAULT 4 static_var2.1
9: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 static_global_uninit_var
10: 0000000000000000 0 SECTION LOCAL DEFAULT 6
11: 0000000000000000 0 SECTION LOCAL DEFAULT 8
12: 0000000000000000 0 SECTION LOCAL DEFAULT 9
13: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 global_init_var
14: 0000000000000000 31 FUNC GLOBAL DEFAULT 1 func1
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
16: 000000000000001f 45 FUNC GLOBAL DEFAULT 1 main
17: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM global_uninit_var
四、C++相关问题 (略,P113~)
五、静态库链接
一种语言的开发环境往往附带自己的“语言库”(Language Library),这些库是对操作系统API的封装。如C语言标准库中的printf()函数,最终会调用Linux下的write系统调用/或Windows下的WriteConsole系统API.
一个静态(链接)库可以简单看作是一组目标文件经过压缩打包形成的一个文件。
Linux中最常用的C语言静态库libc是/usr/lib/libc.a (属于glibc项目一部分,貌似要安装才行), C语言集成开发环境VC2008附带的一些C运行库则放在$VC2008/lib/下。
六、链接过程控制
$ld -verbose ==> 查看ld默认链接脚本
实验:使用自己的ld链接脚本——将三个Section合并到一个名为"tinytext"的Section,并抛弃.comment段
C程序TinyHelloWorld.c
char* str = "Hello world\n"; void print() { //使用write的系统调用, write的调用号为4,原型为int write(int filedesc, char* buffer, int size) //这里的系统调用,先将参数写入寄存器,之后传入write调用 asm( "movl $13, %%edx \n\t" //str的长度 "movl %0, %%ecx \n\t" //缓冲区,这里的%0,指的是“r”后面的字符串地址,也就是传入到这段汇编的参数。 "movl $0, %%ebx \n\t" //打印到标准输出, 也就是0 "movl $4, %%eax \n\t" //将系统调用号传入eax。 "int $0x80 \n\t" //执行中断, 调用write函数 ::"r" (str):"edx","ecx", "ebx" //传入的参数列表, 被重命名的寄存器列表。 ); } void exit() { asm( "movl $42,%ebx \n\t " "movl $1, %eax \n\t" "int $0x80 \n\t" ); } //这里是程序的入口 void nomain() { print(); exit(); }
ld链接脚本TinyHelloWorld.lds
ENTRY(nomain) SECTIONS { . = 0x00008000 + SIZEOF_HEADERS; tinytext : { *(.text) *(.data) *(.rodata) } /DISCARD/ : { *(.comment) } }
[hadoop@sam1 test]$ gcc -c -fno-builtin TinyHelloWorld.c
-c: 预处理、编译、汇编到.o文件,不链接
-fno-builtin:关闭GCC内置函数优化
[hadoop@sam1 test]$ ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.o
-static:使用静态链接,而非动态链接
[hadoop@sam1 test]$ ls -l
-rwxrwxr-x 1 hadoop hadoop 604 Jan 21 23:38 TinyHelloWorld
-rw-rw-r-- 1 hadoop hadoop 870 Jan 21 23:37 TinyHelloWorld.c
-rw-rw-r-- 1 hadoop hadoop 132 Jan 21 23:37 TinyHelloWorld.lds
-rw-rw-r-- 1 hadoop hadoop 1008 Jan 21 23:38 TinyHelloWorld.o
七、BFD库
BFD库(Binary File Descriptor library)是一个GNU项目,他的目标是通过一种统一的接口处理不同格式的目标文件,需要安装binutils-dev。