深入理解计算机系统笔记3:链接
第7章 链接
将高级语言源程序文件转换为可执行目标文件通常分为预处理、编译、汇编和链接四个步骤。前三个步骤用来对每个模块生成可重定位目标文件,最后一步用来将若干可重定位目标文件组合起来,生成可执行目标文件。
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,链接可以执行于编译时(compile time);也可以执行于加载时(load time),也就是在程序被加载器(loader)加载到内存并执行时;甚至执行于运行时(run time)。
链接的分类:传统静态链接、加载时共享库的动态链接、运行时共享库的动态链接。
预处理命令
gcc –E hello.c –o hello.i
cpp hello.c > hello.i
编译
gcc –S hello.i –o hello.s
gcc –S hello.c –o hello.s
汇编
gcc –c hello.s –o hello.o
gcc –c hello.c -o hello.o
as hello.s –o hello.o
链接
gcc –static –o myproc main.o test.o
ld –static –omyproc main.o test.o
1 静态链接 (static linking)
为了创建可执行文件,链接器必须先后完成以下两个任务:符号解析与重定位
链接操作步骤
![]() | ![]() |
2 目标文件
目标文件可分为三种:
1). 可重定位目标文件:可以与其他可重定位目标文件合并起来,生成一个可执行目标文件。
2). 可执行目标文件 :可以被直接加载至存储器并执行
3). 共享目标文件:一种特殊的可重定位目标文件,可以在加载或运行时,被动态载入存储器并链接。
windows使用可一直可执行(Portable Executable, PE)格式,现代x86-64 Linux 和 unix使用ELF格式(EXecutable and Linkable Format 即可执行可链接格式)。
可重定位目标文件格式
| |
C语言规定:
– 未初始化的全局变量和局部静态变量的默认初始值为0
将未初始化变量(.bss节)与已初始化变量(.data节)分开的好处
– .data节中存放具体的初始值,需要占磁盘空间
– .bss节中无需存放初始值,只要说明.bss中的每个变量将来在执行时占用几个字节即可,因此,.bss节实际上不占用磁盘空间,提高了磁盘空间利用率
BSS(Block Started by Symbol)最初是UA-SAP汇编程序中所用的一个伪指令,用于为符号预留一块内存空间
所有未初始化的全局变量和局部静态变量都被汇总到.bss节中,通过专门的“节头表(Section header table)”来说明应该为.bss节预留多大的空间
readelf -h main.o 读取可重定位目标文件的ELF头
readelf -S main.o 读取节头表信息
3 符号和符号表
每个可重定位目标文件(模块m)中都有一个符号表,它包含在m中定义和引用的符号。有三种不同的符号
1). 由模块m定义并可以被其他模块引用的全局符号
2). 由其他模块定义并被模块m引用的全局符号
3). 在模块m定义但只能被模块m引用的局部符号
注意,本地符号和函数的局部变量是两码事。局部变量在程序运行时在桟中被管理,链接器不关心这类符号。
![]() | ![]() |
定义为带有C static属性的本地变量是不在栈中管理。相反,编译器在.data或.bss中为每个定义分配空间,并在符号表中创建一个有唯一名字的本地链接器符号。比如,在同一个模块中两个函数各自定义了一个静态局部变量x:
int f() { static int x = 0; return x; } int g() { static int x =1; return x; }
readelf -s sym.o
这种情况下,编译器向汇编器输出两个不同名字的局部链接符号: x.1723 x.1726. READELF 用一个整数索引来标识每个节, 图中 Ndx =1 表示.text节,而Ndx = 3 表示.data节,Ndx = 4 表示.bss节。
利用static属性隐藏变量和函数名字
使用static属性隐藏模块内部的变量和函数声明,在c中,源文件扮演模块的角色,任何带有static属性声明的全局变量或者函数都是模块私有的。尽可能用static属性保护变量和函数是比较好的编程习惯。
目标文件中的符号表是由汇编器利用编译器生成的.s汇编文件中的符号所生成的。每个符号都被分配到目标文件的某个节,由section字段表示,该字段是一个到节头部表的索引,有三个特殊的伪节,他们在节头部表中没有条目。
ABS: 不应该被重定位的符号
UNDEF: 未定义的外部符号,也就是在本目标模块中引用,但是在其他地方定义的符号
COMMON: 还未被分配位置的未被初始化的数据目标。
4 符号解析
链接器解析符号引用的方法,是将每个引用与输入的所有可重定位目标文件的符号表中的一个确定的符号定义关联起来。
c++和java都允许重载,这些方法在源代码中有相同的名字,却有不同的参数列表,编译器将每个唯一的方法和参数列表组合编码成一个对链接器来说唯一的名字。这种编码过程叫做重整 mangling,相反的过程叫恢复demangling.
![]() |
![]() |
4.1 解析多处定义的全局符号
全局符号分为强、弱两种类型。函数和初始化的全局变量是强符号,而未初始化的全局变量为弱符号。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 /* $begin foo5 */ 2 /* foo5.c */ 3 #include <stdio.h> 4 void f(void); 5 6 int y = 15212; 7 int x = 15213; 8 9 int main() 10 { 11 f(); 12 printf("x = 0x%x y = 0x%x \n", x, y); 13 return 0; 14 } 15 /* $end foo5 */ 16 17 /* bar5.c */ 18 double x; 19 20 void f() 21 { 22 x = -0.0; 23 }
double 类型是8个字节,而int 类型是4个字节,程序中x=-0.0 将用负0的双精度浮点表示覆盖内存中x 和y的位置。
gcc -Wall -0g -o foobar5 foo5.c bar5.c
/usr/bin/ld: Warning: alignment 4 of symbol `x' in /tmp/ccXeNLMl.o is smaller than 8 in /tmp/cc39SYZR.o
./foobar5
x = 0x0 y = 0x80000000
多重定义全局符号问题
• 尽量避免使用全局变量
• 一定需要用的话,就按以下规则使用
– 尽量使用本地变量(static)
– 全局变量要赋初值
– 外部全局变量要使用extern
多重定义全局变量会造成一些意想不到的错误,而且是默默发生的,编译系统不会警告,并会在程序执行很久后才能表现出来,且远离错误引发处。特别是在一个具有几百个模块的大型软件中,这类错误很难修正。大部分程序员并不了解链接器如何工作,因而养成良好的编程习惯是非常重要的。
4.2 与静态库链接
将相关的目标模块打包成一个单独的文件,称为静态库,它可以用作链接器的输入。当链接器构造一个输出的可执行文件时,它只复制静态库里被应用程序引用的目标模块。
静态链接对象:
多个可重定位目标模块 + 静态库(标准库、自定义库)
(.o文件) (.a文件,其中包含多个.o模块)
• 库函数模块:许多函数无需自己写,可使用共享的库函数
– 如数学库, 输入/输出库, 存储管理库,字符串处理等
• 对于自定义模块,避免以下两种极端做法
– 将所有函数都放在一个源文件中
• 修改一个函数需要对所有函数重新编译
• 时间和空间两方面的效率都不高
– 一个源文件中仅包含一个函数
• 需要程序员显式地进行链接
• 效率高,但模块太多,故太繁琐
- 使用静态库,可增强链接器功能,使其能通过查找一个或多个库文件中定义的符号来解析符号
- 在构建可执行文件时,只需指定库文件名,链接器会自动到库中寻找那些应用程序用到的目标模块,并且只把用到的模块从库中拷贝出来
- 在gcc命令行中无需明显指定C标准库libc.a(默认库)
在Linux 中,静态库以一种称为存档(archive)的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和位置。存档文件名由后缀.a标识。
使用AR工具,创建一些函数的静态库。
gcc –c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o
使用静态库
gcc –c main2.c
gcc –static –o prog2 main.o ./libvector.a 或者等价使用 gcc –static –o prog2 main.o -L. –lvector
-static 参数告诉编译器,链接器应该创建一个完全链接的可执行目标文件,它可以加载到内存并运行,在加载时无须更进一步的链接。 –lvector 参数是 libvector.a的缩写, –L.参数告诉编译器在当前目录下查找libvector.a。
使用静态库解析引用
解析结果:
E中有main.o、myproc1.o、printf.o及其调用的模块;D中有main、myproc1、printf及其引用的符号; 注意:E中无myproc2.o
若命令为:$ gcc –static –o myproc ./mylib.a main.o, 结果怎样?
首先,扫描mylib,因是静态库,应根据其中是否存在U中未解析符号对应的定义符号来确定哪个.o被加入E。因为开始U为空,故其中两个.o模块都不被加入E中而被丢弃。
然后,扫描main.o,将myfunc1加入U,直到最后它都不能被解析。
因此,出现链接错误!
Why?它只能用mylib.a中符号来解析,而mylib中两个.o模块都已被丢弃!
链接器对外部引用的解析算法要点如下:
- 按照命令行给出的顺序扫描.o 和.a 文件
- 扫描期间将当前未解析的引用记录到一个列表U中
- 每遇到一个新的.o 或 .a 中的模块,都试图用其来解析U中的符号
- 如果扫描到最后,U中还有未被解析的符号,则发生错误
问题和对策
- 能否正确解析与命令行给出的顺序有关
- –好的做法:将静态库放在命令行的最后
5 重定位
一旦链接器完成了符号解析,就把代码中的每个符号引用和正好一个符号定义关联起来。就可以开始重定位步骤了:合并输入模块,为每个符号分配运行时地址。
- 合并相同的节
将集合E的所有目标模块中相同的节合并成新节例如,所有.text节合并作为可执行文件中的.text节
- 对定义符号进行重定位(确定地址)
确定新节中所有定义符号在虚拟地址空间中的地址。例如,为函数确定首地址,进而确定每条指令的地址,为变量确定首地址;– 完成这一步后,每条指令和每个全局或局部变量都可确定地址
- 对引用符号进行重定位(确定地址)
修改.text节和.data节中对每个符号的引用(地址),需要用到在.rel_data和.rel_text节中保存的重定位信息
5.1 重定位条目
在汇编器生成一个目标模块时,它并不知道数据和代码最终运行时会载入到存储器的什么位置,也不知道这个模块引用的任何外部定义的函数或全局变量的位置。因此,在汇编器遇到对最终位置未知的对象的引用时,就为整个对象生成一个重定位表表项。
用命令 objdump –dx main.o产生main.o的反汇编代码
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: b8 00 00 00 00 mov $0x0,%eax
9: e8 00 00 00 00 callq e <main+0xe>
a: R_X86_64_PC32 swap-0x4
e: b8 00 00 00 00 mov $0x0,%eax
13: 5d pop %rbp
14: c3 retq
查看main.o的符号表 readelf -s main.o
Symbol table '.symtab' contains 11 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.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 8 OBJECT GLOBAL DEFAULT 3 buf
9: 0000000000000000 21 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
因为swap不是在main.c中定义的,所以这个符号的Ndx(符号所在section的index)为UND。为了能让程序顺利执行,我们希望在未来链接的过程中可以从其他文件中找到swap这个符号,并确定这个符号的地址,确定未定义符号的地址的过程即是“重定位”(relocation)。
查看节头部表信息 section header table
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
readelf -S main.o There are 12 section headers, starting at offset 0x128: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .text PROGBITS 0000000000000000 00000040 0000000000000015 0000000000000000 AX 0 0 4 [ 2] .rela.text RELA 0000000000000000 00000548 0000000000000018 0000000000000018 10 1 8 [ 3] .data PROGBITS 0000000000000000 00000058 0000000000000008 0000000000000000 WA 0 0 4 [ 4] .bss NOBITS 0000000000000000 00000060 0000000000000000 0000000000000000 WA 0 0 4 [ 5] .comment PROGBITS 0000000000000000 00000060 000000000000002d 0000000000000001 MS 0 0 1 [ 6] .note.GNU-stack PROGBITS 0000000000000000 0000008d 0000000000000000 0000000000000000 0 0 1 [ 7] .eh_frame PROGBITS 0000000000000000 00000090 0000000000000038 0000000000000000 A 0 0 8 [ 8] .rela.eh_frame RELA 0000000000000000 00000560 0000000000000018 0000000000000018 10 7 8 [ 9] .shstrtab STRTAB 0000000000000000 000000c8 0000000000000059 0000000000000000 0 0 1 [10] .symtab SYMTAB 0000000000000000 00000428 0000000000000108 0000000000000018 11 8 8 [11] .strtab STRTAB 0000000000000000 00000530 0000000000000016 0000000000000000 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), l (large) I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown) O (extra OS processing required) o (OS specific), p (processor specific)
注意rela.text entry区 :link=10,info=1,link表示被重定位的符号所在的符号表的section index,info表示需要被重定位的section的index,通俗点讲就是,将来有朝一日我知道了该符号的地址,我该把这个地址写到哪个section里面去,这里是.text。
用命令readelf -r main.o可详细显示main.o中的重定位条目(表项)
Relocation section '.rela.text' at offset 0x548 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000a 000a00000002 R_X86_64_PC32 0000000000000000 swap - 4
Relocation section '.rela.eh_frame' at offset 0x560 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
offset表示该符号在被重定位的section中的偏移,info的高4个字节表示该符号在.symtab中的index,低4字节表示重定位的类型,不同的类型计算目标地址的方法不一样。swap的重定位地址是在.text的偏移为a处,将来的链接过程中,连接器要将swap的地址写到这个位置上来,swap在.symtab中的index为0xa。
重定位表项
r.offset = 0xa,r.symble = swap,r.type = R_X86_64_PC32 ,r.addend = –4;
链接器修改偏移量为0xa处的32位pc相对引用,在运行时会指向swap函数地址
运行时,main函数的地址为4004f0,swap函数地址为ADDR(swap)=400508
引用的运行时地址为refaddr = 0X4004f0+0xa = 0x400fa,然后更新该引用,使得它在运行时指向swap程序
*refptr = (unsigned)(ADDR(r.symbol)+r.addend – refaddr)=(unsigned)(0x400508 +(-4) –0x400fa)= 0xa
在得到的可执行目标文件中,call指令有如下的重定位形式
在运行时call指令将存放在地址0x4004f9处,当cpu执行call指令时,PC的值为0x4004fe,即call指令下条指令的地址。为了执行call指令,cpu执行下面步骤:将PC值入栈,将PC值赋值为PC+0xa=0x4004f9+0xa=0x400508,即为swap函数的地址,执行swap函数。
重定位后的反汇编代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
00000000004004f0 <main>: 4004f0: 55 push %rbp 4004f1: 48 89 e5 mov %rsp,%rbp 4004f4: b8 00 00 00 00 mov $0x0,%eax 4004f9: e8 0a 00 00 00 callq 400508 <swap> 4004fe: b8 00 00 00 00 mov $0x0,%eax 400503: 5d pop %rbp 400504: c3 retq 400505: 0f 1f 00 nopl (%rax) 0000000000400508 <swap>: 400508: 55 push %rbp 400509: 48 89 e5 mov %rsp,%rbp 40050c: 48 c7 05 31 0b 20 00 movq $0x601030,0x200b31(%rip) # 601048 <bufp1> 400513: 30 10 60 00 400517: 48 8b 05 1a 0b 20 00 mov 0x200b1a(%rip),%rax # 601038 <bufp0> 40051e: 8b 00 mov (%rax),%eax 400520: 89 45 fc mov %eax,-0x4(%rbp) 400523: 48 8b 05 0e 0b 20 00 mov 0x200b0e(%rip),%rax # 601038 <bufp0> 40052a: 48 8b 15 17 0b 20 00 mov 0x200b17(%rip),%rdx # 601048 <bufp1> 400531: 8b 12 mov (%rdx),%edx 400533: 89 10 mov %edx,(%rax) 400535: 48 8b 05 0c 0b 20 00 mov 0x200b0c(%rip),%rax # 601048 <bufp1> 40053c: 8b 55 fc mov -0x4(%rbp),%edx 40053f: 89 10 mov %edx,(%rax) 400541: 5d pop %rbp 400542: c3 retq 400543: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 40054a: 00 00 00 40054d: 0f 1f 00 nopl (%rax
5 可执行文件的加载
• 通过调用execve系统调用函数来调用加载器
• 加载器(loader)根据可执行文件的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷贝”到存储器中(实际上不会真正
拷贝,仅建立一种映射)
• 加载后,将PC(EIP)设定指向Entry point (即符号_start处),最终执行main函数,以启动程序执行
_start: __libc_init_first -->_init –> atexit –> main –>_exit
程序的加载和运行
- UNIX/Linux系统中,可通过调用execve()函数来启动加载器。
- execve()函数的功能是在当前进程上下文中加载并运行一个新程序。
execve()函数的用法如下:
int execve(char *filename, char *argv[], *envp[]);
filename是加载并运行的可执行文件名(如./hello),可带参数列表
argv和环境变量列表envp。若错误(如找不到指定文件filename),则返回-1,并将控制权交给调用程序; 若函数执行成功,则不返回,最终将控制权传递到可执行目标中的主函数main。 - 主函数main()的原型形式如下:
int main(int argc, char **argv, char **envp); 或者:
int main(int argc, char *argv[], char *envp[]);
argc指定参数个数,参数列表中第一个总是命令名(可执行文件名)
例如:命令行为“ld -o test main.o test.o” 时,argc=6
hello程序的加载和运行过程
Step1:在shell命令行提示符后输入命令:$./hello[enter]
Step2:shell命令行解释器构造argv和envp
Step3:调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段、可读写数据段、堆以及用户栈等。
Step4:调用execve()函数,在当前进程(新创建的子进程)的上下文中加载并运行hello程序。将hello中的.text节、.data节、.bss节等内容加载到当前进程的虚拟地址空间(仅修改当前进程上下文中关于存储映像的一些数据结构,不从磁盘拷贝代码、数据等内容)
Step5:调用hello程序的main()函数,hello程序开始在一个进程的上下文中运行。 int main(int argc, char *argv[], char *envp[]);
6 共享库和动态链接
静态库有一些缺点:
– 库函数(如printf)被包含在每个运行进程的代码段中,对于并发运行上百个进程的系统,造成极大的主存资源浪费
– 库函数(如printf)被合并在可执行目标中,磁盘上存放着数千个可执行文件,造成磁盘空间的极大浪费
– 程序员需关注是否有函数库的新版本出现,并须定期下载、重新编译和链接,更新困难、使用不便
解决方案: Shared Libraries (共享库)
– 是一个目标文件,包含有代码和数据
– 从程序中分离出来,磁盘和内存中都只有一个备份
– 可以动态地在装入时或运行时被加载并链接
– Window称其为动态链接库(Dynamic Link Libraries,.dll文件)
– Linux称其为动态共享对象( Dynamic Shared Objects, .so文件)
动态链接可以按以下两种方式进行:
• 在第一次加载并运行时进行 (load-time linking).
– Linux通常由动态链接器(ld-linux.so)自动处理
– 标准C库 (libc.so) 通常按这种方式动态被链接
• 在已经开始运行后进行(run-time linking).
– 在Linux中,通过调用 dlopen()等接口来实现, 分发软件包、构建高性能Web服务器等
动态共享库优点
在内存中只有一个备份,被所有进程共享,节省内存空间
一个共享库目标文件被所有程序共享链接,节省磁盘空间
共享库升级时,被自动加载到内存和程序动态链接,使用方便
共享库可分模块、独立、用不同编程语言进行开发,效率高
第三方开发的共享库可作为程序插件,使程序功能易于扩展
自定义动态库文件
gcc –c myproc1.c myproc2.c
gcc –shared –fPIC –o mylib.so myproc1.o myproc2.o
–shared –fPIC 表示 位置无关的共享代码库文件
PIC:Position Independent Code位置无关代码
1)保证共享库代码的位置可以是不确定的
2)即使共享库代码的长度发生变化,也不会影响调用它的程序
位置无关代码
库打桩机制
编译时打桩,链接时打桩,运行时打桩
处理目标文件的工具
- AR :创建静态库,插入、删除、列出和提取成员;
- SRING: 列出目标文件中所有可打印的字符串;
- SIRIP: 从目标文件中删除符号表信息;
- NM :列出目标文件符号表中定义的符号;
- SIZE: 列出目标文件中节的名字和大小;
- READELF:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含SIZE和NM的功能。
- OBJDUMP :所有二进制工具之母,可显示一个目标文件中所有的信息,最大作用是反汇编.text节中的二进制指令。
Linux系统为操作共享库还提供了LDD程序
LDD: 列出一个可执行文件在运行时所需要的共享库。
参考:csapp第七章,袁春风计算机系统基础课程及教材
readelf 命令用法详解: http://man.linuxde.net/readelf?gwzalg=etooc3