静态链接
ld是静态链接器,可重定位目标文件由代码段和数据节组成。链接器ld主要完成:符号解析和重定位。符号解析就是将符号引用与符号定义联系起来,重定位是符号定义与存储器位置联系起来。
目标文件
分为三种:可重定位目标文件(.o),可执行文件,共享文件(.so/.dll)。
可重定位目标文件
其有很多类型,以ELF (Executable and Linkable Format)为例,其文件结构为
ELF header、节和描述节的节头部表,每个节都有固定大小的entry(条目),不同节的位置与大小由节头部表确定。
典型的ELF文件中,
ELF头
.text:编译的代码
.rodata:只读数据,比如printf中的string
.data:初始化的全局变量
.bss:未初始化的全局变量,只是占位符,所以better save space
.symtab:符号表,存放定义和引用的函数与全局变量信息
.rel.text
.rel.data 重定位条目
.strtab:字符串表,其内容包含.symtab中的符号表,以及节头部中的节名称。字符串表实际上是以null结尾的字符串
等等
注意:这里局部变量存在程序运行的栈中,不在重定位目标文件中。
符号和符号表
符号表(在.symtab中)中有三种类型符号
- 没有static的全局变量或函数
- 带有static的本地变量或函数
- 其他模块定义,本模块引用的全局变量或函数
注意:不包含任何局部变量,但是如果局部变量带有static修饰,则在.data中存储而不在栈中存储。
ELF符号表条目
对于main.c swap.c而言,使用
gcc -s main.c swap.c
得到main.o swap.o
readelf -a main.o
可以查看符号表条目,其中value表示该符号相对于所属节的偏移,size是大小,bind表示全局还是本地,这里没有局部变量,Ndx表示所属字段,包括ABS(不该被重定位的符号), UNDEF(未定义的符号,例如extern修饰的变量), COMMON(为初始化的变量,将来变为.bss中的内容), 1(.text), 3(.data)
符号解析
连接器解析 符号引用<=>符号定义 联系起来
最后编译器中每个模块中每个本地符号只有一个定义,拥有唯一的名字。
对于全局变量的引用解析相对复杂:如果一个全局变量在当前模块中没有,则假设符号是在其他模块中定义的,生成一个链接器符号表条目,链接器在其他模块中寻找。
全局符号的多重定义
对于链接器,需要处理全局符号的多重定义,在编译时,编译器(得到汇编语言文件.s)向汇编器(得到可定位重定向文件.o,之后对.o操作的是ld链接)输出的每个符号,有强/弱之分,而汇编器将这写到可重定位目标文件中:函数与初始化的全局变量是强,未初始化的全局变量是弱符号。
同时规定 1. 不能有多个强符号(所以定义多个同名函数时,编译出错); 2. 有强和弱多个符号,选强的; 3. 有弱符号多个,随机选。
下面开始说链接的事情
与静态库链接
Unix> gcc main.c /usr/lib/libc.o
如此,libc.o将和main.c链接成同一个可执行文件,将libc.o中的文件整个copy,大小很大。
静态库中相关函数编译为独立的目标模块,然后封装为一个单独的静态库文件。如下
Unix> gcc main.c /usr/lib/libm.a /usr/lib/libc.a
链接器只拷贝被程序引用的目标模块,这样减少了可执行文件的大小。
更具体一点:将两个可重定位目标文件链接为一个静态库(本身静态库就是一种称为存档【archive】的特殊文件格式,存档文件由一组链接起来的可重定位目标文件集合而成)
Unix> gcc -c addvec.c multvec.c
unix> ar rcs libvetcor.a addvec.o multvec.o
如果main函数只调用了addvec.c中定义的函数,而没有调用multvec.c中定义的函数,链接器运行时将只拷贝addvec.o到生成的可执行文件中,不引用multvec.o的内容。
重定位
在完成符号解析后,可以进行重定位。在这步骤中,将合并输入模块,并未每个符号分配运行时地址。重定位可以分为两步:
- 重定位节和符号定义
合并同一类型的节,例如合成.data节为一个节,这个节为输出的可执行文件的.data节。
- 重定位节的符号引用
链接器修改代码节和数据节中对每个符号的引用,使得它们指向正确的运行地址,这依赖于重定位条目
重定位条目
汇编器对最终位置未知的目标引用,会生成一个重定位条目,告诉链接器(下一步)在讲目标文件合并为可执行文件时,如何修改这个引用
代码的重定位条目放在.rel.text中,已初始化的重定位条目在.rel.data中。
加载可执行目标文件
Linux运行时存储器映像:在32位系统中,代码段(只读段 .text .rodata)总是从地址0x0804 8000处开始,数据段(读/写段 .data,.bss)从接下来的下一个4KB对齐的地址处。
运行时堆在读/写段之后接下来的第一个4KB对齐的地址处,并通过调用malloc库往上动态增长。
用户栈总是从最大的合法用户地址开始往下(低地址)增长,栈的上部开始的段是为操作系统留的存储器部分(内核的代码和数据)。
在用户栈和运行时堆之间的某个区域保留为动态库的存储器映射区域。
可执行目标文件与可重定位目标文件结构类似,如下图:
ELF头
*段头部表*
*.init*
.text:编译的代码
.rodata:只读数据,比如printf中的string
.data:初始化的全局变量
.bss:未初始化的全局变量,只是占位符,所以better save space
.symtab:符号表,存放定义和引用的函数与全局变量信息
.strtab:字符串表,其内容包含.symtab中的符号表,以及节头部中的节名称。字符串表实际上是以null结尾的字符串
*节头表*
等等
ELF头部描述文件的总体格式,包括程序的入口点,也就是程序运行时执行的第一条指令地址。
.text, .rodata, .data与可重定位目标文件相似,只是已经被重定位到最终运行时的存储器地址。
.init节定义了一个小函数,叫_init,程序的初始化代码会调用它。
因为可执行文件时完全链接的(已经被重定位) , 所以不需要重定位,故没有.rel节。
段头部表描述了可执行文件连续的片(chunk)银蛇到连续的存储器段的这种映射关系。
动态链接共享库
使用命令
ldd liborb.so
可以查看动态库链接了的其他库目录
静态库的缺点是如果有50~100个进程都使用到同一个静态库,则每个运行进程的文本段都存在一个静态库的备份,因为静态库在链接时,已经写到文本段里,嵌入可执行文件中(静态库是.o的集合)。
共享库(shared library)是一个目标模块,在运行时,可以加载到任意的存储器地址,并和一个存储器中的程序链接起来。这个过程称为动态链接(dynamic linking),由一个叫做动态链接器(dynamic linker)的程序执行。
unix> gcc -shared -fPIC -o libvector.so addvec.c multvec.c //将两个.c文件编译为.so文件
-fPID指示编译器生成与位置无关的代码,-shared指示链接器创建一个共享的目标文件.
将.so链接到main函数中,得到可执行目标文件p2:
unix> gcc -o p2 main2.c ./libvector.so
在链接结束,还没有运行代码前:链接器没有拷贝整个动态库函数文本和数据,而是拷贝了重定位和符号表信息,使得运行时可以解析对libvector.so的引用。在运行时,重定位.so的文本和数据到某个存储器段
与位置无关的代码(PIC)
本节是关于多个进程如何共享程序的一个copy的:
编译库代码:使得不需要链接器修改库代码就可以在任何地方加载和执行这些代码,这样的代码叫做与位置无关的代码(Position-Independent Code, PIC)
无论在存储器中何处加载一个目标模块(包括共享目标模块),数据段总是在代码段后,数据段中某变量和代码段某个指令的距离是常量。编译器在数据段开始的位置创建一个表,叫做全局偏移量(Global Offset Table, GOT)。
GOT中,每个被目标模块引用的全局数据对象都有一个条目,编译器为GOT的每个条目生成一个重定位记录。加载时,动态链接器会重定位GOT中每个条目,使得它包含正确的绝对地址。每个引用全局数据的目标模块都有自己的GOT。GOT是.data的一部分。
处理目标文件的工具
- AR:创建静态库(archive),插入、删除、列出和提取成员
- strings:列出一个目标文件中所有可以打印的字符串
- NM:列出一个目标文件的符号表中定义的符号
- strip:从目标文件中删除符号表信息
- size:列出目标文件中节的名字和大小
- readelf:显示一个目标文件的完整结构,包括ELF头中编码的所有信息。包含size和nm的功能
- objdump:所有二进制工具之母。和readelf功能相同。最大的作用是反汇编.text节中的二进制指令
- ldd:列出.so链接的库或者可执行文件等可重定位目标文件的链接关系