对于C/C++程序,编译主要分为三个步骤:
- 源文件进行预处理,这个过程主要是处理一些#号定义的命令或语句(如宏、#include、预编译指令#ifdef等),生成
*.i
文件 - 进行编译,这个过程主要是进行词法分析、语法分析和语义分析等,生成
*.s
的汇编文件; - 进行汇编,这个过程比较简单,就是将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件。
通常情况下,对函数库的链接是放在编译时期(compile time)完成的。所有相关的对象文件 (object file)与牵涉到的函数库(library)被链接合成一个可执行文件 (executable file)。 静态链接和动态链接两者最大的区别就在于链接的时机不一样,静态链接是在形成可执行程序前,而动态链接的进行则是在程序执行时。下面来详细介绍这两种链接方式。
静态链接
为什么要进行静态链接
在我们的实际开发中,不可能将所有代码放在一个源文件中,所以会出现多个源文件,而且多个源文件之间不是独立的,而会存在多种依赖关系,如一个源文件可能要调用另一个源文件中定义的函数,但是每个源文件都是独立编译的,即每个*.c
文件会形成一个*.o
文件,为了满足前面说的依赖关系,则需要将这些源文件产生的目标文件进行链接,从而形成一个可以执行的程序。这个链接的过程就是静态链接。
此时程序在运行时,与函数库再无瓜葛,因为所有需要的函数已拷贝到自己下面。所以这些函数库被成为静态库(static libaray),通常文件名为libxxx.a
的形式。
静态链接的原理
由很多目标文件进行链接形成的是静态库,反之静态库也可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件,如下图,使用ar
命令的-a
参数查看静态库的组成:
这里的*.o
目标文件在博客从编写源代码到程序在内存中运行的全过程解析中已经讲的很清楚了,可以参考一下。
以下面这个图来简单说明一下从静态链接到可执行文件的过程,根据在源文件中包含的头文件和程序中使用到的库函数,如stdio.h中定义的printf()函数,在libc.a中找到目标文件printf.o(这里暂且不考虑printf()函数的依赖关系),然后将这个目标文件和我们hello.o这个文件进行链接形成我们的可执行文件。
这里有一个小问题,就是从上面的图中可以看到静态运行库里面的一个目标文件只包含一个函数,如libc.a里面的printf.o只有printf()函数,strlen.o里面只有strlen()函数。
我们知道,链接器在链接静态链接库的时候是以目标文件为单位的。比如我们引用了静态库中的printf()函数,那么链接器就会把库中包含printf()函数的那个目标文件链接进来,如果很多函数都放在一个目标文件中,很可能很多没用的函数都被一起链接进了输出结果中。由于运行库有成百上千个函数,数量非常庞大,每个函数独立地放在一个目标文件中可以尽量减少空间的浪费,那些没有被用到的目标文件就不要链接到最终的输出文件中。
静态链接的优缺点
静态链接的缺点很明显,一是浪费空间,因为每个可执行程序中对所有需要的目标文件都要有一份副本,所以如果多个程序对同一个目标文件都有依赖,如多个程序中都调用了printf()函数,则这多个程序中都含有printf.o,所以同一个目标文件都在内存存在多个副本;另一方面就是更新比较困难,因为每当库函数的代码修改了,这个时候就需要重新进行编译链接形成可执行程序。但是静态链接的优点就是,在可执行程序中已经具备了所有执行程序所需要的任何东西,在执行的时候运行速度快。
动态链接
为什么会出现动态链接
动态链接出现的原因就是为了解决静态链接中提到的两个问题,一方面是空间浪费,另外一方面是更新困难。下面介绍一下如何解决这两个问题。
动态链接的原理
动态链接的基本思想是把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。下面简单介绍动态链接的过程:
假设现在有两个程序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的虚拟地址空间中,从而进行链接(这个链接过程和静态链接类似)形成可执行程序。
动态链接的优缺点
动态链接的优点显而易见,就是即使需要每个程序都依赖同一个库,但是该库不会像静态链接那样在内存中存在多分,副本,而是这多个程序在执行时共享同一份副本;另一个优点是,更新也比较方便,更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会被自动加载到内存并且链接起来,程序就完成了升级的目标。但是动态链接也是有缺点的,因为把链接推迟到了程序运行时,所以每次执行程序都需要进行链接,所以性能会有一定损失。
据估算,动态链接和静态链接相比,性能损失大约在5%以下。经过实践证明,这点性能损失用来换区程序在空间上的节省和程序构建和升级时的灵活性是值得的。
动态链接地址是如何重定位的呢?
前面我们讲过静态链接时地址的重定位,那我们现在就在想动态链接的地址又是如何重定位的呢?虽然动态链接把链接过程推迟到了程序运行时,但是在形成可执行文件时(注意形成可执行文件和执行程序是两个概念),还是需要用到动态链接库。比如我们在形成可执行程序时,发现引用了一个外部的函数,此时会检查动态链接库,发现这个函数名是一个动态链接符号,此时可执行程序就不对这个符号进行重定位,而把这个过程留到装载时再进行。
对比
简单的说,静态库和应用程序编译在一起,在任何情况下都能运行,而动态库是动态链接,顾名思义就是在应用程序启动的时候才会链接,所以,当用户的系统上没有该动态库时,应用程序就会运行失败。再看它们的特点:
动态库:
- 类库的名字一般是 libxxx.so
- 共享:多个应用程序可以使用同一个动态库,启动多个应用程序的时候,只需要将动态库加载到内存一次即可;
- 开发模块好:要求设计者对功能划分的比较好。
- 动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。
静态库:
- 类库的名字一般是libxxx.a
- 代码的装载速度快,执行速度也比较快,因为编译时它只会把你需要的那部分链接进去。
- 应用程序相对比较大,如果多个应用程序使用的话,会被装载多次,浪费内存。
- 如果静态函数库改变了,那么你的程序必须重新编译。
如果你的系统上有多个应用程序都使用该库的话,就把它编译成动态库,这样虽然刚启动的时候加载比较慢,但是多任务的时候会比较节省内存;如果你的系统上只有一到两个应用使用该库,并且使用的API比较少的话,就编译成静态库吧,一般的静态库还可以进行裁剪编译,这样应用程序可能会比较大,但是启动的速度会大大提高。
环境变量
这里附一个附录,是关于怎么添加各种环境变量的
1 | #(动态库搜索路径) 程序加载运行期间查找动态链接库时指定除了系统默认路径之外的其他路径 |
需要注意的是export PATH="/data2/zhaodali/software/protobuf/bin:$PATH"
会先从前面加载protobuf,但是若设置为:export PATH=$PATH:/data2/zhaodali/software/protobuf/bin
,则会先从系统路径中加载protobuf!!!
LD_LIBRARY_PATH
LD_LIBRARY_PATH
默认值是什么呢?Linux上常见的动态链接器使用缓存来查找它的库。缓存存储在/etc/ld.so.cache
中,并由ldconfig
更新,该指令查找在/etc/ld.so.conf
中提供的路径(目前通常是/etc/ld.so.con.d
中的文件)。/etc/ld.so.cache
的内容可以通过运行ldconfig-p来列出。
因此,LD_LIBRARY_PATH
没有缺省值,默认库查找根本不需要它。如果定义了LD_LIBRARY_PATH
,则首先使用它,但不会禁用其他查找(还包括一些默认目录)。
If LD_LIBRARY_PATH is not set or is empty, it is ignored. If it is set to empty values (with LD_LIBRARY_PATH=: for example), those empty values are interpreted as the current directory.
跨机器运行
在一台机器上设置好动态、静态链接库的环境变量,然后编译出可执行文件后。直接拷贝该执行文件到另一台机器上,少什么动态链接库就使用上面的export
指令设置其路径即可。
但是需要注意,若报错提示找不到符号,可能是由于编译环境不同,导致C++编译出来的库符号表不同,此时就不能直接拷贝动态链接库,得重新编译。
参考
静态链接和动态链接区别
静态链接库、动态链接库、导入库的区别
What is the default value of LD_LIBRARY_PATH? [duplicate]