作为一名C/C++程序员,对于编译链接的过程要了然于胸。首先大概介绍一下,编译分为3步,首先对源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成*.i文件;然后进行编译,这个过程主要是进行词法分析、语法分析和语义分析等,生成*.s的汇编文件;最后进行汇编,这个过程比较简单,就是将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。以上就是编译的过程,下面主要介绍两种链接方式–静态链接和动态链接。
静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时,下面来详细介绍这两种链接方式。
静态链接
1.静态链接对象
多个可重定位模块(.o) +静态库(标准库、自定义库, .a文件,其中包括多个.o模块)关于静态库后面会详细说明
2.引入静态库这个概念之前的说明
如何打包编程者经常使用的功能函数? 在静态库提出之前有2种方法:
- Option 1: 将所有函数写入一个源代码文件
- 编程者需要将一个很大的目标文件链接到其程序
- 空间和时间性能都低
- Option 2: 每一个函数形成一个单独的源文件
- 编程者需将自己的程序显示地链接到相应的二进制文 件
更有效,但对于编程者是额外负担 - 以上两种库处理方式并不能很好处理库链接
3.静态库
- **静态库是什么?**一组连接起来的可重定位目标文件集合,有一个头部用来描述每个 成员目标文件的大小和位置。
我们可以用指令来查看一些常用的静态库是由哪些东西组成的(以c标准库为例子:libc.a ): - 静态库 (static library) , 它可以用做链接器的输入。当链接器构造一个输出的可执行文件时,它只拷贝静态库里被应用程序引用的目标模块。(链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来)
比如,使用标准 C 库和数学库中函数的程序可以用形式如下的命令行来编译和链接,编译器只会拷贝被程序引用的目标模块:
unix> gcc main.c /usr/lib/libm.a /usr/lib/libe.a
- 静态库以一种称为存档 (archive) 的特殊文件格式存放在磁盘中。存档文件是一组连接起来的可重定位目标文件的集合,有一个头部用来描述每个成员目标文件的大小和 位置。存档文件名由后缀 .a 标识。
- 增强了链接器,其可以通过在一个或多个存档中查找符号来解析外 部引用
- 若一个存档成员文件解析了引用,则可将其连接到可执行文件
4.实例
构造一个名字叫libvector.a 的静态库,里面有两个成员目标文件构成:mulvec.c addvec.c
- mulvec.c:
/*mulvec.c*/
void mulvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]*y[i];
}
- addvec.c:
/* addvec.c*/
void addvec(int *x,int *y,int *z,int n)
{
int i;
for(i=0;i<n;i++)
z[i]=x[i]+y[i];
}
使用ar工具创建库:
gcc -c addvec.c multvec.c
ar rcs libvector.a addvec.o multvec.o
可以看到文件夹中已经生成了这个库
编写main2.c, 调用 addvec :
#include <stdio.h>
#include "vector.h"
int x[2]={1,2};
int y[2]={3,4};
int z[2];
int main()
{
addvec(x,y,z,2);
printf("z= [%d,%d]\n",z[0],z[1]);
return 0;
}
编译和链接输入文件 main.o 和 libvector.a,执行指令:
gcc -O2 -c main2.c//生成.o文件
gcc -static -o p2 main2.o ./libvector.a//再用./p2指令可以可以运行可执行文件p2
- -static 参数告诉编译器驱动程序,链接器应该构建一个完全链接的可执行目标文件,它可以加载到存储器并运行,在加载时无需更进一步的链接。
- 当链接器运行时,它判定 addvec.o 定义的 addvec 符号是被 main.o 引用的,所以它拷贝 addvec.o 到可执行文件。因为程序不引用任何由 multvec.o 定义的符号,所以链接器就不会拷贝这个模块到可执行文件。
- 链接器还会拷贝 libc.a 中的 printf.o 模块,以及许多 C 运行时系统中 的其他模块。链接库图形表示如下:
5.静态库的缺点
- 在存储中的可执行文件中有多个副本 (每一个函数均需要静态库文件 )
- 在运行中的可执行文件中存在多个副本
- 即便是对系统库进行小bug的修复,也需要对使用到这个库的所有应用显示地重 新链接
动态链接共享库
1.简介
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。下面简单介绍动态链接的过程:
假设现在有两个程序program1.o和program2.o,这两者共用同一个库lib.o,假设首先运行程序program1,系统首先加载program1.o,当系统发现program1.o中用到了lib.o,即program1.o依赖于lib.o,那么系统接着加载lib.o,如果program1.o和lib.o还依赖于其他目标文件,则依次全部加载到内存中。当program2运行时,同样的加载program2.o,然后发现program2.o依赖于lib.o,但是此时lib.o已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的lib.o映射到program2的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。
共享库 (shared library) 是致力于解决静态库缺陷的一个现代创新产物。共享库是一个目标模 块,在运行时,可以加载到任意的存储器地址,并和一个在存储器中的程序链接起来。这个过程 称为动态链接 (dynamic linking), 是由一个叫做动态链接器 (dynamic linker) 的程序来执行的。
- 在 Unix 系统中通常用 .so 后缀来表示。
- Windows操作系统大量地利用了共享库,它们称为 DLL (动态链接库)
2.两种共享方式:
- 在任何给定的文件系统中,对于一个库只有一个 .so 文件。所有引用该库的可执行目标文件共享这个 .so 文件中的代码和数据,而不是像静态库的内容那样被拷贝和嵌人到引用它们的可执行的文件中。
- 其次,在存储器中,一个共享 库的 .text 节的一个副本可以被不同的正在运行的进程共享。
3.创建共享库并完成链接
- 创建共享库libvector.so:
- fPIC 选项指示编译器生成与位置 无关的代码
- shared 选项指示链接器创建一个共享的目标文件
gcc -shared -fPIC -o libvector.so addvec.c multvec.c
- 一旦创建了共享库,我们将他链接到示例程序中:
-这样创建了一个可执行目标文件p2(部分链接的可执行目标文件)。
-此文件的形式使得它在运行时可以和libvector.so链接。
-基本思路:创建可执行文件时,静态执行一些链接,然后程序加载时,动态完成链接过程
-创建可执行文件时,没有任何libvector.so的代码数据节被真正拷贝到执行文件p2中。反之连接器拷贝了一些重定位和符号表的信息,在运行时可以解析对libvector.so中符号的引用。
unix> gcc -o p2 main2.c ./libvector.so
- 当加载器加载和运行可执行文件 p2 时,加载部分链接的可执行文件 p2。接着,它注意到 p2 包含一个 .interp 节,这个节包含动态链接器的路径名,动态链接器本身就是一个共享目标(比如,在 Linux 系统上的 LO-LINUX.SO)。
- 加载器不再像它通 常那样将控制传递给应用,而是加载和运行这个动态链接器。
- 然后,动态链接器通过执行下面的重定位完成链接任务: ·重定位 libc.so 的文本和数据到某个存储器段。 ·重定位 -libvector.so 的文本和数据到另一个存储器段。 ·重定位 p2中所有对由 libc.so 和 libvector.so 定义的符号的引用。
最后,动态链接器将控制传递给应用程序。从这个时刻开始,共享库的位置就固定了,并且在程 序执行的过程中都不会改变