链接器之静态链接与动态链接

一、前言

最近在项目中需要用一个公司的CAN卡,然后到官网下载官方提供的库文件,下载下来的有3个文件.h、.lib、.dll,OK,现在主角出现了,.lib和.dll。

其实一般的库文件都包含了这三个文件,无奈自己接触的还是比较少,目前为止用了三家公司的设备,一家德国、一家日本的公司,还有一家就是CAN卡的这家,前两家外国大企业都是提供的.h和.lib,并没有.dll啊,也就是说在二次开发中,只需要将自己编写的.h、.cpp文件和官方提供的.lib文件一起编译就行可以运行了,尽管也知道.dll的存在,不就是动态链接库嘛,和.lib一样,都是别人写好的实现代码,但当时还真没有仔细深究.lib和.dll的不同之处,直到用了这家公司的CAN卡之后,遇到了问题,也就是采用之前的办法即只引用.lib文件,程序编译没有问题,但是却不能执行,当然这知识一个小问题,就是缺少相应的.dll文件嘛,加进去就可以执行了(为什么编译没问题,而运行就有问题了?后面有解答)。但作为一个程序员,就需要有一个毛病,知其然,就要知其所以然(我反正是这样的,可能有些程序员很反对这个观点,因为他们的观点就是只要会用就行了),然后就各种百度、翻书,就得到了以下的个人见解。

二、链接

在《深入理解计算机系统》一书中,P448,链接的定义是:将各种代码和数据部分收集起来并组合为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。这是定义,那么实际中呢?链接的过程就是将.cpp对应的.obj(Windows下)和.lib(其内部也是一组.obj,即一组可重定位目标文件)合并,生成一个可执行目标文件。这里首先说明,链接分为静态链接和动态链接,静态链接是由静态链接器完成,而动态链接是由动态链接器完成。

1、静态链接

静态链接分为符号解析和重定位两个步骤,其中符号解析主要是将一个模块内的符号引用与该模块的符号表中的一个符号定义关联,这里的模块就是单个可重定位目标文件,在Windows里面是.obj文件,而重定位将多个模块的符号表中的每一个符号定义以内存地址唯一关联,经过这两个步骤,就完成了符号引用到符号定义,再到内存的完整映射,因为引用一个符号,不就是为了获取或改变他在内存中的值嘛。

(1)符号解析

在一个可重定位文件(模块)中,有三种链接器符号:能被其他模块引用全局符号,引用其他模块的全局符号,以及本地符号。注意,这里的本地符号是在当前模块中所有函数定位外面的全局符号,并且带有static属性,并不是函数内部局部变量,链接器不关心函数内部的局部变量,这一部分由运行时栈管理,如下:

/*********************m.cpp*****************************/

extern int e;

int a;

static int b;

void fun(int c)

{

int d = c;

return d;

}

上面所示模块m中,符号a是能被其他模块引用的全局符号,符号e为引用其他模块的全局符号,而符号b则是目标m的本地符号,它只能在模块m内部引用,其他模块不可见,另外符号c、d都是模块m的本地过程变量,由运行时栈管理。

由于链接器上下文中有不同的符号类型(上述3种),因此链接器的符号解析对象大致分为两种情况:引用本地符号和引用全局符号。

a.引用本地符号。这个过程非常简单,直接对照关联就行

b.引用全局符号。这个过程就有点复杂了,试想,在两个模块中同时两个名字相同的全局符号,链接器该怎么处理呢?当然是定义一套自己的规则去避开一些麻烦,这些规则是建立在强符号和弱符号定义的基础之上的,强符号是指函数和已经定义的全局变量,弱符号是指未定义的全局变量,这一套规则就是:如果有多个强符号,就报错;如果有一个强符号和多个弱符号,就选择强符号;如果有多个弱符号,就随机选择一个弱符号。如下,第一种情况示例会报错

/fun1.cpp*/

int main()

{

return 0;

}

/fun2.cpp*/

int main()

{

return 0;

}

第二种情况实例不会报错,但结果与你的预期有差距

/m1.cpp*/

int x=123;

void fun();

int main()

{

fun();

cout<<x<<endl;

return 0;

}

/fun.cpp*/

int x;

void fun()

{

x=234;

}

如果运行程序,则会输出x=234,因为在模块fun.cpp中链接的是定义在m模块中的强符号x。

(2)重定位

只需要理解一点,在链接过程中,当所有符号引用与符号定位关联之后,我们仍然无法知道引用该符号所得到的内容,因为内容是存储在内存中的,这就需要重定位来完成,重定位包括重定位节和符号定义、重定位节中的符号引用两个步骤,完成重定位之后,就知道了该引用所引用的内存值。

(3)与静态库链接

静态库就是一开始提到的.lib文件,它是一个目标模块的集合,其包含一组目标模块(可重定位目标文件),当我们将自己的cpp文件与lib库文件链接生成可执行文件时,只是将静态库中被应用程序引用的模块拷贝出来,其符号解析过程还是和上述一样。比如我们自己编写了一个m.cpp文件,需要用到别人提供给的.lib中的一些函数,比如fun1.cpp,fun2.cpp,在.lib库文件中,就应该是有fun1、和fun2模块对应的可重定位模块,假设名为fun1.obj、fun2.obj,而我们自己编写的m模块被编译器和汇编器生成为m.obj的目标文件,链接过程就是合并m.obj、fun1.obj、fun2.obj的过程,即它并不是拷贝整个.lib文件,只是拷贝了fun1和fun2两个文件,这也是为了减少存储器资源消耗的方法,那么链接器是如何实现这一过程的呢?

它通过3个集合:E、U、D来完成。其中集合E表示链接器维护的一个可重定位目标文件的集合,这个集合刚开始为空,链接完成之后,链接器就合并这个集合中的目标文件,生成可执行目标文件;集合U表示为解析的符号引用,即在我们编写的.cpp文件中引用了但尚未定义的符号,刚开始也为空;集合D表示已定义的符号集合,初始时也为空。具体过程可参考《深入了解计算机系统》(原书第二版)p460内容。

总之,与静态库链接(即引用静态库)需要明白两个概念:(1)静态库就是一组可重定位目标文件集合(在Windows下是.obj文件);(2)链接过程中,应用程序只拷贝在自己源程序中引用的目标文件,并不是拷贝整个.lib库文件。

(4)动态链接共享库

静态库有自己的优点,也有缺点,缺点就是需要定期维护更新,然后引用该静态库文件的应用程序也要重新与该库文件链接才能正确运行,最主要的问题是与静态库链接需要直接拷贝库文件中的目标模块,尽管是选择性拷贝,但是对于一个小的程序来说,拷贝一些.lib文件还是会造成一些内存资源浪费。为了解决这些问题,聪明的人就创造了动态链接共享库,动态库在Windows中是用.dll扩展名的文件(好,现在就到了解释前言中提高的问题了),记住,动态库链接是由动态链接器完成,但是他也需要静态链接器(为什么呢?看后面解释)。

这里要说明,继续前言的叙述,一个动态链接库包含.h、.lib、.dll文件,而真正的可重定位目标模块是放在.dll中的。咦?刚刚讲到静态链接库的时候,可重定位目标模块是放在.lib中,这里在动态库中既然是放在.dll里面的,那么为什么还需要一个.lib文件呢?这个.lib文件里面到底放的是什么呢?这就是动态链接器的设计需要。

动态链接器的链接过程主要分为两步:生成部分链接的可执行目标文件、生成完全链接的可执行文件,这两个步骤分别有静态链接器和动态链接器完成(这个过程可参考《深入了解计算机系统》P468图7-15)。

这里生成部分链接的可执行目标文件有静态链接器完成,需要使用.lib文件。这里.lib文件中并不包含任何关于动态库文件的代码和数据内容,只是包含一些重定位和符号表信息,这使得运行时可以解析对.dll库文件中的代码和数据的引用。所以这就解释了我在前言中说明的在链接了.lib文件之后,程序编译时没有问题的。

完成部分链接之后,就已经生成了一个可执行目标文件,在Windows中是.exe文件,但是这只是一个部分链接的可执行目标文件,它没有包含任何与库有关的数据和代码内容,直接运行当然会报错,而报错信息正是缺少相应的.dll文件,这里,只要把相应的.dll文件与生成的部分链接的可执行目标文件放在同一个目录下,程序就可以运行了,完美。咦?整个动态链接的过程还是没有引用或者拷贝库文件(.dll)中的代码和数据内容啊,程序怎么又能运行了呢?那是因为你在执行.exe文件的时候,程序加载器动态的加载了.dll动态库文件,然后与.exe文件一起运行的,这是加载器帮你完成的,所以你看不到咯。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值