一. 链接
1. 链接是将各种代码和数据部分收集起来组合成为一个单一文件,这个文件可被加载或拷贝到存储器并执行。静态链接是在生成可执行文件的时候,将所有需要的函数的二进制代码都包含到可执行文件中去。链接器需要知道参与链接的目标文件需要哪些函数,同时也要知道每个目标文件都能提供什么函数。这样链接器才能知道是不是每个目标文件所需要的函数都能正确地链接。
2 现在有两个程序main.c和 swap.c,通过预处理、编译和汇编后生成两个可重定位目标文件main.o和 swap.o。
// main.c
#include <stdio.h>
void swap();
int buf[2] = {1, 2};
int main(void) {
swap();
int i = 0;
for (; i < 2; ++i)
printf("%2d", buf[i]);
printf("\n");
return 0;
}
// swap.c
extern int buf[2];
int *bufp0 = &buf[1];
int *bufp1;
void swap() {
int temp;
bufp1 = &buf[0];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
生成可重定位目标文件:
kernel@Ubuntu:~/Desktop/LIB$ gcc -c swap.c -o swap.o
kernel@Ubuntu:~/Desktop/LIB$ gcc -c main.c -o main.o
使用连接器程序ld,将main.o 和 swap.o 以及一些必要的系统目标文件组合起来,生成可执行目标文件。由于main.c中含有printf函数,需要将定义printf函数的.o文件也进行链接,否则会出现未定义的引用的错误。
二. 静态链接
1. Linux ld程序这样的静态链接器以一组可重定位目标文件(将汇编代码生成为机器代码)和命令行参数作为输入,生成一个完全链接的可以加载和运行的可执行目标文件。输入的可重定向目标文件有各种不同的代码和数据节组成。链接器主要完成以下两个任务:
(1)符号解析。目标文件定义和引用符号。符号解析的目的是将每个符号引用刚好与一个符号的定义相联系。
(2)重定位。编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定位与一个存储器位置联系起来,然后修改这些符号的引用,使得它们指向这个存储器,从而重定位这些节。
在说明符号解析之前先了解一下什么是符号和符号表。
2. 符号和符号表
每个可重定位目标模块m都有一个符号表,它包含所定义和引用的符号信息。在链接器的上下文,有三种不同的符号。
(1)由m定义并能其他模块引用的全局符号。main.c中定义的全局变量buf,该符号可以被swap.c引用。全局链接器对应于非静态的C函数以及被定义不带staic的全局变量。
(2)由其他模块定义并被模块m引用的全局符号。这些符号称为外部符号。在swap.c中使用extern 关键字引用了定义在main.c的全局变量buf。在main.c中函数声明了swap函数,函数默认有extern修饰。当swap.c去掉extern对全局变量buf的修饰,编译器没有报错,buf被声明。但是在给定初值的时候,在链接的时候会报错,buf被多次定义。
(3)只被模块m定义和引用的本地符号。用static修饰的全局变量,只能被该模块引用,而不能被其他模块引用。对main.c中buf用static,swap.c对其extern引用,在编译时会出现未定义的引用错误。
(4)在ELF可重定位目标文件中,.symtab中的符号表不包含对应于本地非静态成员程序变量(局部变量)的任何符号。此时查看一下main.o中的符号表:
kernel@Ubuntu:~/Desktop/LIB$ readelf -s main.o
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
5: 0000000000000000 8 OBJECT LOCAL DEFAULT 3 buf
buf是在.symtab节的符号表中,但buf是属于本地的符号。不能被外部引用。由Ndx = 3表示,buf存放在.data节。
(5)符号表由汇编器构造,使用编译器输出到汇编语言的.s文件中的符号。.symtab包含ELF符号表。这张符号表包含一个条目的数组。每个条目的结构体如下:
typedef struct {
int name; // 字符串表的字节偏移,指向符号以NULL结尾的字符串名字
int value; // 距定义目标的节起始位置的偏移
int size; // 目标大小
char type:4, // 变量或函数
binding:4; // 符号是本地还是全局
char reserved;
char section;
}Elf_Symbol;
每个符号都与目标文件的一个节相关联,由section字段表示,该字段是一个到节头部表的索引。有三个特殊的伪节,它们在节头部表中没有条目
a. ABS不该被重定义的符号。
b. UNDEF 未定义的符号,本模块引用其他模块的符号。swap.c中的buf,main.c的swap函数。
c. COMMON 还未被分配位置的未初始化的数据目标。.bss节中存放未初始化的全局变量。在目标文件中,.bss节不占用世界空间,仅仅是一个占位符。
main.o的符号表
Symbol table '.symtab' contains 14 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 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 buf
10: 0000000000000000 83 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND swap
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND putchar
swap.o的符号表:
swap.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 swap.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000000 0 SECTION LOCAL DEFAULT 7
6: 0000000000000000 0 SECTION LOCAL DEFAULT 8
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 bufp0
9: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND buf
10: 0000000000000008 8 OBJECT GLOBAL DEFAULT COM bufp1
11: 0000000000000000 59 FUNC GLOBAL DEFAULT 1 swap
3. 符号解析。
链接器解析符号引用的方法是将每个引用与它输入的可重定位目标文件的符号表的定义进行关联。对与本地符号的定义引用的解析非常简单。编译器只允许一个符号在一个模块中只有一次定义。
对于全局符号的引用和解析比较麻烦,如果当前模块没有定义一个符号,例如swap函数在main.c的声明,编译器会假设该符号在其他某个模块定义,生成一个链接器符号表条目,上面所示main.o的符号表,交给链接器处理。如果链接器在其他模块没有找到swap函数的定义,那么链接器会输出一个错误。
还有一种情况,此时又有一个swap1.c定义了swap函数。此时swap函数被定义了两次,在链接时,会输出如下错误:
/tmp/ccDnVpZp.o:在函数‘swap’中:
/home/kernel/Desktop/LIB/swap1.c:4: `swap'被多次定义
/tmp/ccbSAbzW.o:/home/kernel/Desktop/LIB/swap.c:6:第一次在此定义
collect2: error: ld returned 1 exit status
如果swap1.c的swap函数参数与之前定义的swap函数不一样,C++因为可以重载,会造成链接器符号的破坏。
3.1 强符号指函数和已经初始化的全局变量。弱符号指未初始化的全局变量。
根据强弱符号定义,Unix链接器使用下面的规则来处理多重定义的符号
a. 规则1:不允许有多个强符号
b. 规则2: 如果有一个强符号和多个弱符号,那么选择强符号。
c. 规则3:如果有多个弱符号,那么从模块中任选一个。
对于规则1,就是刚才所说的swap1和swap两个都定义了swap函数。
对于规则2,如下示例:
//m.c
#include <stdio.h>
int x = 99;
void fun();
int main(void) {
fun();
printf("%d\n", x);
}
// fun.c
int x;
void fun(){
x += 1;
}
对于规则3,如下示例:
//m.c
#include <stdio.h>
int x;
void fun();
int main(void) {
x = 99;
fun();
printf("%d\n", x);
}
x // fun.c
int x;
void fun(){
x += 1;
}
kernel@Ubuntu:~/Desktop/LIB$ gcc -g m.c fun.c
kernel@Ubuntu:~/Desktop/LIB$ ./a.out
100
下面的程序会出现问题:
//m.c
#include <stdio.h>
int x = 100;
int y = 101;
void fun();
int main(void) {
fun();
printf("%d, %d\n", x, y);
}
// fun.c
double x;
void fun(){
x = -0.0;
}
kernel@Ubuntu:~/Desktop/LIB$ gcc -g m.c fun.c
/usr/bin/ld: Warning: alignment 4 of symbol `x' in /tmp/ccjICf8o.o is smaller than 8 in /tmp/ccwD55iJ.o
kernel@Ubuntu:~/Desktop/LIB$ ./a.out
0, -2147483648
因为在m模块和fun模块中都定义了x,但是m模块x是强符号,链接器会选择该符号,在fun函数中,给double型x赋值-0.0双精度浮点,由于double为8个字节,int为4个字节,且查询符号表,y相对于x偏离了4个字节,x,y两个符号相连。所以y的值是0x80000000,也就是32位最小补码—2147483648,x则为0。因此在怀疑此类的错误时,使用下列的参数进行编译:
gcc –nfo-common