编译与链接 - 链接
由 C/C++ 源码编译得到的目标文件是一个个孤立的使用 ELF 格式的存储的目标文件,源码层面,多个文件之间通过符号名彼此关联,但目标文件中彼此分散在不同文件中,尚未建立联系。链接,就是将多个目标文件链接起来,构造出一个整体的过程。链接器以目标文件为输入,最终得到的产物是可执行文件或者可动态加载的动态链接库。链接分为静态链接与动态链接,本文将描述这两种链接方式的基本概念,对其中涉及到的部分细节,会在后续文章中详细描述。
静态链接
静态链接以一组可重定位目标文件做为输入,构造出一个可被加载到内存执行的可执行文件或动态库。静态链接是相对于动态链接而言的,静态链接就是将所有目标文件打包在一起,构造出一个没有依赖的可执行程序或动态库文件。
以如下程序为例:
// swap.c
void swap(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
// main.c
#include <stdio.h>
void swap(int *a, int *b);
int main() {
int a = 10, b = 20;
swap(&a, &b);
printf("a: %d b: %d\n", a, b);
return 0;
}
swap.c 与 main.c 编译后,得到 swap.o 和 main.o 两个目标文件。使用如下命令做静态链接:
$ gcc -c swap.c
$ gcc -c main.c
$ gcc swap.o main.o -o main
$ ./main
a: 20 b: 10
使用静态链接后,main.o 与 swap.o 都合并到可执行文件 main 中。main.o 与 swap.o 两个目标文件中均存有指令、数据、符号等信息,静态链接时候会将这些信息抽出来放置在一起:

目标文件各 section 之间存在引用关系,比如符号表与字符串表之间的关联,链接后,原目标文件中各 section 的位置必然改变,链接时候需要修改引用的地址。
链接前,目标文件之间不存在关联,目标文件如果使用了其他目标文件中定义的符号,它自然是不知道最终这个符号的地址的,此时会在目标文件中生成一个重定位记录。链接时,链接器会基于重定位信息在其他目标文件中查找对应的符号,并将引用地址修改为该符号真实的地址。
在 main.c 中使用了 printf 和 swap 这两个定义与其他目标文件中的函数,在 main.o 中就可以看到针对这两个函数的重定位记录:
$ readelf -r main.o
Relocation section '.rela.text' at offset 0x220 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000025 000a00000004 R_X86_64_PLT32 0000000000000000 swap - 4
000000000032 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000003c 000b00000004 R_X86_64_PLT32 0000000000000000 printf - 4
Relocation section '.rela.eh_frame' at offset 0x268 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
在链接时候,所有目标文件被合并起来,链接器能够获取到全部的目标文件中定义的所有符号,链接器基于重定位信息将符号的引用修改为真实的地址,这个过程就叫做重定位。关于重定位的细节,后面会专门写文章介绍。
重定位后,内部的引用关系已经正确建立。根据链接目标的不同,静态链接可用于构造可执行程序或是可以被动态加载的动态链接库。
静态库
静态库实际上是一组 .o 文件的集合,我们可以使用 ar 命令得到静态库:
$ ar -q libswap.a swap.o
此后,在链接的时候可以使用静态库:
$ gcc main.o libswap.a -o main
动态链接
静态链接中,多个目标文件被合并在一起,这种方法很简单直接,但这种方法存在弊端。
现在几乎每一个有用的程序都会依赖标准库,有些还会依赖很多第三方库。几乎每个程序都直接或间接依赖于 C 标准库,如果每个程序都将 C 标准库与用户自己实现的模块打包在一起生成可执行文件,那么标准库就会存在很多份拷贝,这会造成空间的浪费。
很多第三方库常常会发布新的版本,但是 API 并未改变,如果采用静态链接,第三方库升级后,需要重新链接,本程序才能获得第三方库的更新。如果可以只库做更新,而无需重新链接,这就会很方便。
要解决空间浪费和更新困难的问题,可以把程序的模块分开,形成独立的可动态加载的库。程序运行的时候,进行加载,把链接过程推迟到运行时再进行。采用这种方式,很多程序可以共用同一个库,而且库升级后,程序下次启动就可以得到更新。
这种在运行时进行链接的策略就叫做动态链接。
动态链接库
动态链接的参与者为动态链接库,动态链接库是可以在运行时动态加载的目标文件。仍然以前面的程序片段为例说明如果构造与使用动态库:
构造动态库:
$ gcc -fPIC -c swap.c
$ gcc -shared swap.o -o libswap.so
链接动态库:
$ gcc main.o libswap.so -o main
上面这一步执行了链接操作,但并没有把 libswap.so 的内容包含在 main 中,而是在 main 中记录了依赖的动态库的信息。在程序启动后,就会尝试去加载该动态库。
执行程序:
$ ./main
./main: error while loading shared libraries: libswap.so: cannot open shared object file: No such file or directory
这里报了一个错误,说无法找到动态库 libswap.so,这是因为我们的动态库没有放置在动态库的搜索路径中。可以使用环境变量来指定动态库的地址:
$ LD_LIBRARY_PATH=. ./main
a: 20 b: 10
设置动态库地址为当前路径,然后执行可执行程序,就能够正常执行了。
动态加载
键入程序启动命令后,装载器会读取程序的 ELF 文件,将必要的段载入内存,而后将控制权交给动态链接器,动态链接器会获取当前程序依赖的动态库的信息,并执行动态库的加载,这部分信息可以通过 readelf -d 查看:
$ readelf -d main
Dynamic section at offset 0x2e48 contains 22 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libswap.so]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x401000
0x000000000000000d (FINI) 0x4012a4
0x0000000000000004 (HASH) 0x400300
0x000000006ffffef5 (GNU_HASH) 0x400330
0x0000000000000005 (STRTAB) 0x400400
0x0000000000000006 (SYMTAB) 0x400358
0x000000000000000a (STRSZ) 91 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x404000
0x0000000000000002 (PLTRELSZ) 72 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x4004a8
0x0000000000000007 (RELA) 0x400490
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x400470
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x40045c
0x0000000000000000 (NULL) 0x0
关于这部分信息,我会在其他文章中详细介绍。通过前两行,可以看到当前程序依赖两个动态库,一个是前面我们自己构造的 libswap.so,另外一个则是 libc.so.6。
动态库载入到内存中的址是不确定的,这引入了一个问题。main.c 中如果对一个不知道加载到何处的 swap 函数执行调用呢?
后面详述动态链接时会详细描述这里面的实现原理,此处只大概说明思路。

在 main 程序内部,维护了一个表格,这个表格中存储的是所有外部定义的函数的地址。代码中要调用这些函数,先从表格中拿到函数的地址,然后再调用函数。链接器在加载完动态库后,动态库在内存中的虚拟地址固定了,其中包含的函数的地址也都固定了,此时可以把函数地址填写到这些表格中。
动态库都加载完成后,可以跳转到 main 的程序入口处开始执行。
标准库的链接方式
前面的例子中,命令行中没有显式地链接标准库,但 GCC 默认采用动态链接的方式链接 C 标准库。
也可以使用如下命令来静态链接 C 标准库:
$ gcc main.o swap.o -static -o main.static
静态链接得到的可执行文件变大了很多:
$ ll -h
-rwxrwxr-x 1 work work 20K Aug 23 16:19 main
-rwxrwxr-x 1 work work 5.1M Aug 23 16:19 main.static
静态链接的可执行文件大小为 5.1 MB,而笔者实验环境上 libc 的静态库 libc.a 大小为 26MB。这是因为 libc.a 中包含很多 .o 文件,在链接时候,只会包含 printf 依赖的的 .o 文件。
总结
本文从宏观上描述了链接的目的,即链接整合多个分散的目标文件,以构造出可执行文件或动态库。链接分为静态链接和动态链接两种,静态链接将目标文件全部整合在一起构成一个整体,在动态链接中,可复用的目标文件被打包成动态库,链接过程被推迟到运行时。关于静态链接和动态链接还有很多细节本文并未描述,这些内容会在后续文章中展开做详细介绍。