编译系统很早就接触了,但是编译系统中到底干了什么?
我们今天来着重看一看链接的那点事
1. 编译系统
首先,来看那一看编译系统都干了什么:
而我们今天着重来说的就是: 链接的过程
2. 链接
链接到底发生了什么?
我们都知道,在汇编器作用完成后, 汇编文件.s被转化成为目标文件(可重定位的目标文件).o
.o文件是已经编译,优化二进制文件,只要成功进行装配就可以了.
软件开发的工程浩大,如果所有的内容都是自己从头开始编写,势必力不从心,难以维系
所以,我们倡导多使用库,对于一些基础内容,已经编译好的库,我们直接去链接即可.
这也就是,我们今天来讨论链接内容的原因.
首先,我们要明确一个概念: 所谓链接,其中重要的有两点: 1.链接方式, 2. 链接时机
这两点就是链接中最核心的内容.
我们先来从链接方式说起.
3. 静态链接
C从很早就支持分离式编译,这个特性在C++中也支持,即是说,对于一个大型软件
只要分块得当,大家完全可以分离式开发,最后进行模块的装配即可
链接也是这样一个道理, 但是,链接去装拼的是函数库.
比如我们整天使用的C库,很多C库会默认链接 libc.so -> libc.so.6
因为是最基础的库,自动链接,而是我们平时不注意这些链接细节
静态链接:
实际上说的就是, 链接库, 链接时,直接将需要的库一次性装入可执行文件
这样装配成功的可执行文件中包含了所需要库中函数实现的所有内容
比如说, 我调用printf, 静态链接了C库, 于是我的a.out中便包含了printf实现的所有目标文件
那么,来分析一下静态链接的优缺点:
优点:
-
静态链接将库中所需要的内容直接装入可执行文件,使得可执行文件脱离静态库,即无需依赖库
-
因为装配的内容全部发生在链接时,所以运行时程序速度快,1% ~ 5% (相比于动态链接链接)
缺点: 尽管静态链接有优秀的地方,但它的缺点也比较严重
- 静态链接因为直接将每一个.o所需要的库文件装入, 他们相互之间不可见,于是相同的模块
甚至会悲壮在进入内存多次, 严重消耗了内存空间,举个简单的例子: printf若占0.2K, 一个大型程序
如果调用printf 2k次, 那么此可执行程序中,printf的内容就占了200KB的内容, 严重浪费内存
而且,这样的浪费情况,在早期的计算机系统中,甚至是致命的.
当然我们在分析后,发现了静态链接可能会造成在可执行文件中出现多份拷贝的问题,
早期的计算机如何如何处理呢? 是这样干的:
他们将程序对于函数库的依赖实现在内核中,woc!, 这是个很严重的问题
会造成可怕的”内核膨胀”,后果不堪设想
基于上面的问题,静态链接在特殊的历史时期还是比较有用的,但是现在,毫无疑问的过时了
所以,我们来介绍动态链接
4. 动态链接
首先,我们来说说为什么要有动态链接:
1. 为了解决内存中重复库代码的问题,内核膨胀实际上是很危险的做法
2. 还是为了实现软件库更新迭代的便捷性
我们先来说说动态链接,然后具体进行分析,并说说它如何解决上面其中的问题
动态链接:
首先,要明确的是, 动态链接是为了改进静态链接的.
我们首要解决的问题,便是多份库实现的拷贝问题,解法就是,维持一份拷贝
只要装入后在内存中维持一份库文件的拷贝,效率要好很多(隐含:链接后,a.out仅仅保留文件名)
(库文件的位置,符号表等内容,并未进行装入,推迟到运行时真正链接)
即是说,只要是在内存中维系了一份库文件拷贝的链接方式,便是动态链接
其实,我这里的说法和< C专家编程 > (“咸鱼书”) 还是不一样的.
因为下面我们就要说说链接的第二点了: 2. 装入时机
因为静态库已经过时(这说法也不全面)的问题,我们主要讨论动态链接
- 装入时动态链接
如上所说,两种装入时机,第一种便是传统的装入时(Link期间)进行动态链接,
这个时候,链接器会进行查找,他会将目标文件中的所需要的库文件装入内存
这就有一个问题了,有些模块的使用程度并不高: 尤其是错误处理模块
动态链接的方式,可以有效的减少内存压力,但是利用率还是没有明显提升
错误模块仍然被装入,会导致,多个错误模块的装入但未使用的情况.
而现在,十分流行的便是运行时动态链接(JIT Just-In-Time)的模式
- 运行时动态链接
上面说到过,装入时动态链接,它的位置是可变的,可以是内存中任意位置.但是,结构是静态的
即是说,装入多少模块,模块之间的顺序都是固定的.有的模块如果没有使用还是会浪费
因此引入了运行时动态链接的方式: 装入时,不进行链接.一直将模块的链接推迟到运行时进行
(PS: 是不是有一种静态语言和动态语言的感觉,编译时/运行时确定类型,滑稽)
这样,如果需要错误处理模块,便由OS去寻找模块并装入,可以大大提高效率
好了,常见的链接方式介绍完毕,我们再来比较一下这几种方式:
静态链接:
- 在我们需要摆脱库依赖,而且程序对此库依赖程度小时使用,也就不会造成太多内存拷贝.
动态链接:
- 如果我们要经常调用某个库,我们便使用动态链接的形式,虽然说动态链接的运行都会变慢,
尤其是运行时动态链接,将链接时机推迟,但是另一方面会提高效率,(比其他两种更快的装入速度)
可以说装入时动态链接基本上已经使用的很少了
上面是综合来讲,其实将链接方式和链接时机分开来说,也不过是这样:
-
静态链接: 可以摆脱库依赖,重复拷贝多
-
动态链接: 仅维护一份共享拷贝,无法摆脱库依赖
-
静态装入: 位置是静态的,同时会将不一定运行的模块进行装入
-
动态装入: 绝不会装入任何一个不使用的模块,不过运行速度降低(在当今的机器配置下,都不是问题)
5. 动态链接的优点
既然动态链接是目前的主流,我们就来看看动态链接的优点:
-
生成的可执行文件小得多,因为具体模块的实现,并没有装入可执行文件中,是推迟到running是链接
-
因为进行推迟,所以装入速度很快,比起它链接方式都要快,的那是缺点就是理论上运行速度慢
不过,这都不是事,在当下的计算机硬件水平下,堆配置就可以了(我游民老哥标配四路泰坦(ಡωಡ))
动态链接重要的目的之一便是: ABI(Application Binary Interface)应用程序二进制接口 历史的经验表明: 软件版本的更新迭代,往往会出现严重的问题, 不兼容,各种各样的问题 而且,每次进行新的库迭代,都要进行重新编译,(对于静态链接) 这显然是他妈的操蛋,所以动态链接出现了,它要求操作系统提供一套二进制接口 应用程序进行链接时,不去考虑器具体实现,只使用接口. 是不是进行库版本的迭代,一下简单多了,我们甚至可以同时维护多个版本的库进行选择 (PS: 是不是由了一点面向对象的感觉,ABI正是一种中间件,自然的处理了程序与库之间的关系)
一般我们约定,库文件放在指定的位置,编译系统按照默认的规定去进行编译链接.但是,
我们还可以给编译器传参,(ಡωಡ) -I,-L,-l这些可不能白瞎了,还可以-Wl,-rpath=XXX 给链接器传参
此处,我们建议尽量只使用动态链接,因为这样,我们的版本更新十分方便,并且容错率高
说了这么多,静态链接库,动态链接库实体是什么样的呢?
静态链接库成为archive,使用ar生成 “XXX.a”
动态链接库,使用gcc或者ld生成(怀疑?),”libXXX.so.x.y.z”
6. 如何生成链接库
首先,我们要严正声明,咸鱼书中的是基于SPARC上面的cc编译器
与我们日常在x86_64机器上使用的gcc/clang/msvc有很大不同,命令也是
我下面的操作是基于gcc x86_64机器进行的操作
上面我们说过静态链接库,使用ar生成,喜爱面试我们的步骤:
file: hello.c
---
#include <stdio.h>
#include "hello.h"
void hello(void)
{
printf("Hello. I'm a static link test\n");
}
file: hello.h
---
#ifndef _HELLO_H
#define _HELLO_H
void hello(void)
#endif
file: main.c
---
#include <stdio.h>
int main(void)
{
hello();
return 0;
}
上面是我们的测试文件,下面是具体的步骤:
gcc -c hello.c
ar -rcv -o libhello.a hello.o
gcc main.c -L . -lhello
#=>
[Crow@EvilCrow ~]$ ./a.out
Hello. I'm a static link test
其中需要注意的是:
-
ar中的选项,-rcv ,显示,更新,创建新的archive,即是说,对于一个archive,我们可以进行包的增删改
-
gcc中 -L 指的是链接目录,对于静态,动态都适用
-
-l说明要链接的库名, 一般都是libXXX.so XXX就是要进行链接的名字
下面是,动态链接库的创建,可以使用链接器ld,我们此处使用简单地gcc:
文件一致.
gcc -fPIC -c hello.c
gcc -shared -fPIC -o libhello.so hello.o
gcc main.c -L . -Wl,-rpath=. -lhello
#=>
[Crow@EvilCrow ~]$ ./a.out
Hello. I'm a dynamic link test
其中需要注意的是:
- 外面说的共享库和动态库是一个东西Win下DLL(Dynamic Loading Libary)
Linux下 .so(Shared Object)
- -fPIC(- file Position Independent Code) 表示与位置无关,很容易对数据进行重定位
我们的建议是: 函数库应始终使用与位置无关的代码,共享库建议使用PIC
为什么,因为共享库维护同一份拷贝,我们使用PIC,可以有效的减少换页.可以按需求任意位置装入
否则就成了静态共享库了(SVR3上一个奇葩玩意,不提)
-
-shared 这个选项一定要有啊,否则,是不能生成共享库的(动态库)
-
-L. 之后,,为什么还要有 -Wl,-rpath=. 动态库因为可以通过同时维护多个,而且运行时必须存在
所以,我们即需要指定链接目录,也需要运行目录,一般,/usr/lib64下,都是链接指向->
-Wl,rpath=XX, 纯属是gcc的命令问题,cc,只即使用-R(running dir),-Wl,…表示向链接器传参
如果我们不使用这个选项,也是可以的.将我们的共享库放在/usr/lib64, 或者/usr/local/lib64下
/usr /usr/local区别不用我说了,
另一种方法在ld.so.conf中添加我们共享库所在目录,然后
ldconfig 即可(需要sudo)
之后,动态链接,就不需要指定目录了,静态链接同理
我们下面列出一些常用库,及其链接方式
#include文件 | 库路径名 | 选项 |
---|---|---|
< math.h > | /usr/lib64/libm.so | -lm |
< math.h > | /usr/lib64/libm.a | -lm |
< stdio.h > | /usr/lib6/libc.so | 自动链接 |
…… | …… | ….. |
提取库中的符号: nm命令(我使用的不太多,大家可以去man一下)
7. 使用静态库提取符号更严格
因为共享库,推迟到运行时链接(我们现在就不考虑装入时链接了)
所以,链接命令顺序无所谓,运行时统一去链接
但是,静态库就必须在直接完成链接,所以,一旦有符号为装入,GG
而且,静态库,链接时,不是将其装入,而是装入undeference symbol
也就是说,gcc -lhello main.c -L. ,会提示找不到hello
因为先链接库, 而库中hello是已经定义了的符号,
因此,我们强烈建议,-l选项放在最后,虽然这有违, UNIX命令使用顺序,选项放在最后
不过为了程序顺利运行,这都不是事
8. Interpositioning
曾经,由很多人都干过,定义与C保留字名相同的函数
不过C标准是支持这样的行为的,与OOP中对于类成员函数的重写类似
但是,在C中,如果你重写了某一个函数
注意,注意,所有使用该保留字函数,都会被你写的函数进行替换,很严重的问题! ! !
有解吗? 有, 使用static关键字,限制命名空间, 其实OOP也就是限制了命名空间
9. 建议
说了这么多,其实也不是说静态链接就一无是处
事实上,它没有湮灭在历史长河中,就说明了它的存在性
比如说,需要给没有函数库的机器上执行,那不得需要静态编译来脱离函数库依赖么,
而且,以目前的计算机硬件水平,基本可以忽略内存多余拷贝
多说一句: 一般默认选择动态链接,找不到libXXX.so时去找libXXX.a
最后,有一个疑问: 现如今,使用的到底是装入时动态链接, 还是运行时动态链接? 可以交流一下呗,
而且这两种模式,是否可以手动指定?
最后,此次的链接知识开个小头,等CSAPP中的精华内容吧,甚至C可以直接在程序设计层面调用共享库
May 25, 2018 2:18 PM
Update: May 25, 2018 2:18 PM
更新, 今天在问了老王之后,得到两个概念:
-
gcc也只是一个driver, 其内部自行调用ccl,cpp,as,ld之类的组件 即Binuntils
-
可以确定的是链接是在运行时进行的,ld-linux-x86_64.so.2实际上可以单独运行
也就是说,在链接完成后,可执行文件中填入了链接器,证明是运行时完成动态链接
![strings a.out | grep ld](http://www.qiniu.evilcrow.site/Exceprt_C_runtime_dynamic_link.png) |