静态链接与动态链接
静态链接与动态链接
静态链接
编译分为3步:
1、首先对源文件进行预处理
,这个过程主要是处理一些#
定义的命令或语句,例如宏、#include
、预编译指令#ifndef
等,生成*.i
文件。
2、然后进行编译
,这个过程主要是进行词法分析
,语法分析
和语义分析
等,生成*.s
的汇编文件。
3、最后进行汇编
,这个过程就是将对应的汇编指令
翻译为机器指令
,生成可重定位的二进制目标文件
。
为什么要进行静态链接?
在实际开发中,不可能将所有的代码都放在一个源文件中,所以会出现多个源文件。而且源文件之间不是独立存在的,而存在多种依赖关系,如一个源文件要调用另一个源文件中定义的函数。
但是每个源文件都是独立编译的,及每个*.c
或*.cpp
会形成一个*.o
文件,为了满足源文件之间的依赖关系,需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
这个链接的过程就是静态链接。
静态链接的原理
由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合。
以下面这个图简单说明一下从静态链接
到可执行文件
的过程,根据在源文件中包含的头文件
和程序中使用到的库函数
,如stdio.h
中定义的printf()
函数,在libc.a
中找到目标文件printf.o
(这里暂不考虑printf()
函数的依赖关系),然后将这个目标文件与hello.o
这个文件进行链接形成我们的可执行文件。

值得注意的是,从上面的图片中可以看到静态链接库里面的一个目标文件只包含一个函数,如libc.a
里面的printf.o
只有printf()
函数,strlen.o
中只有strlen
函数。
我们知道,链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()
函数,那么链接器就会把包含printf()
函数的那个目标文件链接进来。如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库中有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间地浪费,那些没有用到地目标文件就不要链接到最终地输出文件中。
创建和使用静态链接库
Linux
下的静态链接库是以.a
结尾的二进制文件,它作为程序的一个模块,在链接期间被组合到程序中。
制作链接库的目的是希望别人使用我们已经实现的功能,但又不希望别人看到我们的源代码,这对商业机构是非常友好的。
Linux
下静态链接库文件的命名规则为:libxxx.a
,xxx 表示库的名字。例如,libc.a
、libm.a
、libieee.a
、libgcc.a
都是 Linux
系统自带的静态库。
生成静态链接库
-
首先使用
gcc
命令把源文件编译为目标文件,也即.o
文件:gcc -c 源文件列表
-c
选项表示只编译,不链接 -
然后使用
ar
命令将.o
文件打包成静态链接库,具体格式为:ar rcs libxxx.a 目标文件列表
ar
是Linux
的一个备份压缩命令,它可以将多个文件打包成一个备份文件(也叫归档文件),也可以从备份文件中提取成员文件。ar
命令最常见的用法是将目标文件打包为静态链接库。
对参数的说明:- 参数
r
用来替换库中已有的目标文件,或者加入新的目标文件。 - 参数
c
表示创建一个库。不管库否存在,都将创建。 - 参数
s
用来创建目标文件索引,这在创建较大的库时能提高速度。
- 参数
实例演示
任选一个目录,创建一个文件夹 test,将 test 作为整个项目的基础目录。在 test 目录中再创建四个源文件,分别是 add.c、sub.c、div.c 和 test.h。代码如下:
// test.h
#ifndef __TEST_H_
#define __TEST_H_
int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);
#endif
// add.c
#include "test.h"
int add(int a,int b) {
return a + b;
}
// sub.c
#include "test.h"
int sub(int a,int b) {
return a - b;
}
// div.c
#include "test.h"
int div(int a,int b) {
return a / b;
}
首先将所有源文件都编译成目标文件:
gcc -c *.c
*.c
表示所有以.c
结尾的文件,也即所有的源文件。执行完该命令,会发现 test 目录中多了三个目标文件,分别是 add.o
、sub.o
和 div.o
。
然后把所有目标文件打包成静态库文件:
ar rcs libtest.a *.o
*.o
表示所有以.o
结尾的文件,也即所有的目标文件。执行完该命令,发现 test 目录中多了一个静态库文件 libtest.a
,大功告成。
我们也可以通过以下命令查看静态链接库内的文件:
ar -t ./lib/libtest.a
输出:
add.o
div.o
sub.o
使用静态链接库
使用静态链接库
时,除了需要库文件
本身,还需要对应的头文件
:库文件包含了真正的函数代码,也即函数定义部分;头文件包含了函数的调用方法,也即函数声明部分。
为了使用上面生成的静态链接库libtest.a
,我们需要启用一个新的项目。任选一个目录,创建一个文件夹math
,将math
作为新项目的基础目录。
在比较规范的项目目录中;lib
文件夹一般用来存放库文件;include
文件夹一般用来存放头文件;src
文件夹一般用来存放源文件;bin
文件夹一般用来存放可执行文件。
为了规范,我们将前面生成的 libtest.a
放到 math
目录下的 lib
文件夹,将 test.h
放到 math 目录下的 include
文件夹。
在 math
目录下再创建一个 src
文件夹,在 src
中再创建一个 main.c
源文件。
此时 math 目录中文件结构如下所示:
|-- include
| `-- test.h
|-- lib
| `-- libtest.a
|-- src
| `-- main.c
在 main.c
中,可以像下面这样使用 libtest.a
中的函数:
#include <stdio.h>
#include "test.h" //必须引入头文件
int main() {
int m, n;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
return 0;
}
在编译 main.c
的时候,我们需要使用-I
(大写的字母i)选项指明头文件的包含路径;使用-L
选项指明静态库的包含路径;使用-l
(小写字母L)选项指明静态库的名字。所以,main.c
的完整编译命令为:
gcc src/main.c -I include/ -L lib/ -l test -o math
注意,使用-l
选项指明静态库的名字时,既不需要lib
前缀,也不需要.a
后缀,只能写 test
,GCC
会自动加上前缀和后缀。
打开 math
目录,发现多了一个 math
可执行文件,使用./math
命令就可以运行 math
进行数学计算。
root@xxx:~/projects/demo# ./math
Input two numbers: 12 23
12+23=35
12-23=-11
12÷23=0
静态链接的优缺点
- 优点
- 由于在可执行文件中已经具备所有执行文件所需要的东西,在执行的时候运行速度快。
- 缺点
- 浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件有依赖,如多个程序中都调用了
printf()
函数,则这多个程序中都包含printf.o
,所以同一个目标文件在内存中存在多个副本。 - 更新困难,因为每当库函数的代码发生修改,这个时候就需要重新编译所有依赖这个库函数的程序,形成新的可执行文件。
- 浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件有依赖,如多个程序中都调用了
动态链接
为什么会出现动态链接?
动态链接出现的原因是为了解决静态链接中存在的两个问题:空间浪费和更新困难。
动态链接的原理
动态链接的基本思想是把程序按照模块拆分为各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接那样在链接时将所有的模块组成一个可执行文件。
假设现在有两个程序 program1.o
和 program2.o
,这两者共用同一个库 libxxx.o
,假设首先运行程序 program1
,系统首先加载 program1.o
,当系统发现 program1.o
中用到了 libxxx.o
,即 program1.o
依赖于 libxxx.o
,那么系统接着加载 libxxx.o
,如果 program1.o
和 libxxx.o
还依赖于其他目标文件,则依次全部加载到内存中。当 program2
运行时,同样的加载 program2.o
,然后发现 program2.o
依赖于 lib.o
,但是此时 lib.o
已经存在于内存中,这个时候就不再进行重新加载,而是将内存中已经存在的 libxxx.o
映射到 program2
的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。
创建和使用动态链接库
创建动态链接库
我们用同样的代码来创建生成动态链接库:
gcc -shared -fPIC *.c -o libtest.so
shared
:标志着要生成动态链接库fPIC
:告诉编译器产生与位置无关的代码
这样,我们就生成了想要的动态链接库libtest.so
使用动态链接库
首先,我们需要生成可执行文件,此时的命令与静态链接相同:
gcc src/main.c -I include/ -L lib/ -l test -o math
此时,如果你直接使用./main
执行程序,你会遇到这样的报错信息:
…/bin/math: error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory
因为没有指定动态链接库的所属位置。
所以,正确的命令是:
LD_LIBRARY_PATH="./lib" ./bin/math
LD_LIBRARY_PATH=
: 告诉链接程序,从指定的目录下寻找链接库。
动态链接的优缺点
- 优点
- 节省空间资源:共享同一个依赖库
- 更新便利:只需替换相关的模块即可
- 缺点
- 性能受损:因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。 据估算,动态链接和静态链接相比,性能损失大约在5%以下。经过实践证明,这点性能损失用来换区程序在空间上的节省和程序构建和升级时的灵活性是值得的。
动态链接地址是如何重定位的呢?
虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
更多:
如果想了解更多动态链接的知识,例如如何在 Linux
中通过 dlopen
函数加载动态链接库,python
如何使用C/C++
编写的动态链接库等知识,详见参考3:深入浅出:Linux C编程中如何使用动态链接库
。
参考: