[转载]UNIX程序的链接

来源:互联网 发布:网络剧最好的我们 编辑:程序博客网 时间:2024/05/22 15:20
转载地址:http://bbs.chinaunix.net/archiver/?tid-779785.html
UNIX链接处理

我们已经知道链接实际上是指将在一个模块中引用的符号与它在另一个模块中的定义相链接的过程。并且我们还知道链接分为动态链接和静态链接两种方式。不论是 对哪一种方式。链接程序都将搜索程序中的每一个模块,包括所用到的每一个库文件,以在这些文件中寻找在某个模块中没有定义的外部符号的定义。如果没有找到 某个被引用的符号的定义,链接程序将报告错误。此时可执行文件的创建将会失败。

对于静态链接和动态链接,其区别主要在于搜索到某个符号的定义后链接程序所做的不同工作:

静态链接,链接程序将把静态链接库(档案库)中哪些被用户程序所引用的符号定义的目标代码,拷贝到最终生成的可执行文件中。这种情况下,程序中的外部符号引用同其定义的链接是在可执行文件被建立的时候完成的。

动态链接,共享对象(动态链接库)中的内容在运行时被映射到用户进程的虚地址空间。链接程序所作的仅仅是在最终生成的可执行文件中记录下到哪里去找外部符号定义的目标代码。这种情况下符号的外部引用与其定义的链接是在程序运行时完成的。

在这一节中,我们将详细地讨论链接过程。如编译系统的一些缺省设置、用户如何生成自己的动态库或静态库、如何在程序中链接这些库文件,以及动态链接库是如何实现的,等等问题。在掌握了这些内容之后,读者将能够高效地组织自己的源文件,提高开发的效率和程序的可维护性。

缺省设置

前面一节中,我们使用:

$ cc -o myprg myprog.c myfunc.c

命令来生成可执行文件。此时cc将生成同每一个C源程序对应的目标文件,并把它们彼此链接,以生成一个可执行的程序文件。对于所生成的每一个目标文件,我们称之为可重定位的目标文件,因为这些目标文件中含有尚未同其定义相链接的符号引用,也即尚没有在内存中分配地址。

但我们可以注意到,myprog.c中所调用的printf()和myfunc.c中所调用的isdigit(),这两个函数是我们在自己的程序中所没有 定义的。这两个函数是标准C库所提供的。缺省情况下链接程序将自动地道标准C库中去查找哪些被用户程序所调用的函数的定义。

标准的C函数库有动态和静态两个不同的版本,其文件名分别是libc.so和libc.a, 分别用于动态链接和静态链接。缺省情况下,链接程序将对标准C库函数调用进行动态链接(使用libc.so),也即所调用的函数将在运行时与程序相链接。 但有些标准函数由于设计上的一些遗漏,在libc.so中并没有其定义。对这些函数链接程序将使用libc.a进行静态链接,也即将这些函数的代码拷贝至 可执行文件中。但对哪些既可以静态链接,又可以进行动态链接的符号引用,程序员可以自主选择到底使用哪种链接方式。后面我们将介绍如何完成这一点。

标准C函数库中所包含的函数有如下几类:

标准I/O函数,如fopen,fread,fwrite,fclose等;
字符串操作函数,如strcat,strcmp,strcpy等;
对八位字符编码的整数值分类的函数,如isalpha,isupper,islower,isdigit等;
字符、整数或字符串转换函数,如atoi,atol,strtoul,atof等;
用库函数形式实现的系统调用,如open,read,write,close等等。

关于其他未列出的函数及据说明读者可参考有关的手册。
cc自动对标准C库的动态链接是下述约定为基础的:
根据约定,共享对象或动态链接库的名称,前缀是lib而后缀是.so;档案库或者静态链接库,前缀为lib而后缀是.a。因此标准C库的共享版本的名称将是libc.so;而静态版本的名称则是libc.a。

cc命令的-l选项能够识别上述约定。也就是说:

$ cc ... -lx

将使链接程序搜索动态库libx.so或静态库libx.a。缺省情况下,cc将自动地把-lc选项传给链接程序。因此,从实际看-l选项是一个链接程序选项

缺省情况下,链接程序将优先查找同一目录下的动态库版本libx.so,不成功时才选择静态库libx.a。
缺省情况下,链接程序将在系统的标准位置(/usr/ccs/lib和/usr/lib目录下)按上述顺序搜索所需的库。由编译系统提供的标准库一般被放在/usr/ccs/lib目录下。

在这些约定的基础上,我们可以更明确地讲,缺省的cc命令行将引导链接程序搜索/usr/ccs/lib/libc.so,而不是它所对应的静态库。后面 我们将要讲到如何将用户的程序同某个库的静态版本相链接(如同libc.a相链接而不是同libc.so相链接),以及用户如何建立起自己的静态库或动态 库,并使程序与这些库相链接。当然,如果缺省的cc命令行能够满足编译要求的话,那么就用不着进行多余的链接处理了。

标准库函数的链接

libc.so只是一个目标文件,其中包含有标准C库中每一个函数的代码。当某个程序调用该库中的某个函数并且动态地将自己的程序相链接时,libc.so的全部内容将被映射到运行时与该程序相对应的进程虚地址空间中。

对于静态库(档案库)则不是这么回事,每一个函数或者一组相关的函数代码被保存到它们自己的目标文件中。然后这些目标文件被收集在一个档案库中。但程序员 在命令行中指定对标准C库进行静态链接的时候,链接程序将在档案库中搜索被调用函数的代码并将其拷贝到最终的可执行文件中。这里我们看到使用静态链接时最 终可执行文件中只包含有哪些所需的代码。

那么如何使链接程序改变缺省的动态链接方式而进行静态链接呢?方法是在cc命令行中加上-dn选项,如下所示:
$ cc -o myprog -dn myprog.c myfunc.c

这样对于myprog.c和myfunc.c中的标准C库函数调用,链接程序将在libc.a中去搜索目标代码并且将其拷贝到最终可执行的文件中。

在程序比较复杂的情况下,一个程序可能就不是仅仅调用了标准C库中的函数了。例如,对于用到了数学运算sin(),cos()这类函数的程序,它就可能需 要同数学函数库链接。由于编译系统只提供了libc和libdl(对动态链接进行控制的函数调用集合)的动态版本,因此,除非是用户自己在标准位置安装了 数学函数库的动态版本libm.so,那么链接程序将在标准位置查找libm.a库函数。当然这需要在cc命令行中加上一个-l选项,如下所示:
$ cc file.c file2.c   -lm

注意在上述命令行中我们并没有指定-dn选项。这样链接程序对每一个库函数调用,将仍然试图去进行动态链接。例如,对于file1.c和file2.c中 的标准C库函数调用,链接程序将在libc.so中搜索其定义。后面我们将看到对每一个库,如何指定是同静态版本相链还是同动态版本相链。

另外还要注意的是链接程序对于静态库的搜索仅仅是为了解决以前发现的、尚未定义的外部引用。因此,-l选项在命令行中的位置很重要。例如:

$ cc -dn file1.c -lm file2.c

这样链接程序对libm.a的搜索只是为了解决file1.c中对数学函数的调用。因此如果在file2.c中调用了某个数学函数,链接程序将无法找到该函数的定义,因而链接也将失败。这种情况下,

除非是特别清楚每个C文件中都调用了哪些函数,否则还是将-l选项放在命令行的最后比较好,并且-l选项可以出现多次,以指定多个不同的库文件。

下一部分:静态库及动态库的建立 ......

UNIX系统开发-静态库和动态库的建立

UNIX系统及各种软件包为开发人员提供了大量的库文件。但一般情况下这些库文件还不能足以满足用户的所有需求。开发人员大多会根据他们自己的开发、研究 要求编写出许多函数。对于这些函数,如果都用在命令行中指定源文件的方法同调用它们的的程序链接起来,虽然也是可以的,但也有一些缺点:

对每一个调用了这些函数的程序,在编译时都需要将这些函数的代码分别重新编译,这实际是对计算时间的大量浪费。
一个文件中通常都不止包含有一个函数的定义。使用上述编译方法将使得大量无关函数的代码被拷贝到最终的可执行文件中,无端加大对存储资源的占用量,使运行时装载变慢。
维护上的诸多不便。由于一个源文件供多个程序使用,当由于某个程序的需要面对此源文件进行了某种修改时将引起诸多意想不到的麻烦。等等。

所有这些原因,使得我们想到能否将自己编写的函数也作成库文件供多个程序调用,就如同那些标准的库函数那样。事实上在UNIX系统中提供了这方面的工具。借助于这些工具我们不光是能将函数放到静态库,而且能够将其作成动态库。

下面来看看如何生成静态库。

我们知道静态库也称档案库,在此档案文件中实际上是收集了一系列的目标文件。这些目标文件就是由CC对函数的源代码编译生成的。因此,静态库的生成方法实际上可分成两步:

  1.将各函数代码所在地源文件编译成目标文件。例如,对于前面的myfunc.c,可以用如下命令将其编译成目标文件:

  $ cc -c myfunc.c

  当然在有多个源文件时,只需在cc命令行中将其分别列上就可以了。

  经此一步我们将能够得到各源文件的目标文件。对上例将得到myfunc.o。

  2.将各目标文件收集起来放到一个静态库文件中。这主要借助于ar命令完成,如:

  $ ar r $HOME/lib/libtest.a myfunc.o
  
ar:creating /home/yxz/libtest.a (屏幕输出)

这 里-o $HOME/lib/libtest.a是生成的静态库的全路径名。其中我们假定$HOME/lib目录已经存在。注意对静态库的命名要遵循libx.a 的原则,便于以后能够在cc命令行中用-l选项指定之。后面的myfunc.o则是待收集到档案库中的目标文件名。有多个目标文件时只需分别列上即可。

这生成了libtest.a档案库之后,再编译myprog.c时,便可使用下面的办法:

$ cc -L $HOME/lib -o myprog myprog.c -ltest

这里-L选项指示链接程序在$HOME/lib目录下去搜索有关的库文件(当然它还会自动搜索标准位置)。下一节我们对此将进行更详细的说明。最后的- ltest选项指示链接程序在libtest.so或libtest.a中去搜索myprog.c中对TestInput()的引用。当然由于我们并没有 生成libtest.so文件,故链接程序将只能搜索libtest.a。另外,由于我们在命令行中并未指定-dn选项,故对于缺省的-lc选项,链接程 序将搜索libc.so而不是libc.a。

静态链接库的生成虽然比较简单,但此种链接方式也正因为其简单而在某些情况下达不到比较高的效率。同动态链接方式相比,这种链接方式具有如下一些明显的不足:
静态库的不足之处:
1:由于在生成的可执行文件中包含有函数代码的单独拷贝,这些重复的代码会消耗掉大量的磁盘空间。 (磁盘空间不是问题吧?)
2:运行时各进程单独在自己的地址空间中装入它所调用的每一个函数的代码,这样在有多个进程都调用了同一个函数时,内存中将会有此函数代码的多个拷贝,无端地占用比较多的内存。
3:由于对符号引用的确定是在编译链接时完成的。故以后对函数的定义进行更新的时候,必须重新链接调用这些函数的程序。

在虚拟存储管理方案的基础上,实现的动态链接方式克服了静态链接上述不足,而使整个系统能够获得比较高的效率。因此缺省情况下,链接程序只要有可能就要试图进行动态链接(找库函数的动态版本)。

进行动态链接的核心问题是要生成动态链接库(共享对象)。下面我们介绍如何生成动态链接库,然后讨论建立动态链接库的一些原则。

建立动态链接库并不需要用到其他的工具,借助于cc命令即可完成。此时需在命令行中加上-K PIC和-G这两个选项,如下我们可以建立libtest的动态版本:

$ cc -K PIC -G -o $ HOME/lib/libtest.so myfunc.c

这里-o $ HOME/lib/libtest.so指定待生成的动态链接库的全路径名。同静态库一样,动态库的命令应遵循libx.so约定。-G选项指示cc按动态链接库的格式将其各文件的目标代码组织起来。

-K PIC 选项是生成动态链接库所必须的。我们已经知道,动态链接时建立在页式虚拟管理方式的基础上的。在这种需存管理方式下,进程之间内存的共享是以页为 单位的。只要运行时内存页不改变,它们就能够被共享。但是这些共享的页在不同的进程中。可能会具有不同的虚地址。因此这些代码的物理地址只是在运行时才能 得到。(这个过程称为地址的重定位。)如果在重定位某个共享对象的引用时,某个进程写了一个共享页,此时操作系统将为该进程生成改页的一个专用拷贝。这种 情况下,页面共享的好处就没有了。因此程序必须尽可能减少对页面的修改的次数,减少此修改次数的方法就是使用地址浮动的代码。

地址可浮动的代码将能够被装入到进程地址空间的任何地方。由于此种代码不依赖于绝对地址,故这些代码将在使用它的每一个进程中在不同的虚地址正确的运行, 而且在运行过程中是没有页面修改的。-K PIC选项的作用,就是指示编译系统生成地址可浮动的目标文件。此时在目标文件中,可重定位的引用将从所在地正文段被移动到数据段的表中。

在生成了动态库之后,就可以在cc命令行中使用它了。如:

$ cc -L $ HOME/lib -o myprog myprog.c -l test

这时虽然在$HOME/lib目录下也具有test库的静态版本libtest.a,但链接程序将优先搜索libtest.so。

在搞清楚如何生成动态库之后,下面我们来看一看建立动态库的一些原则。所有这些原则都是为了性能的改善而提出来的。

动态库性能的改进:
性能的改善主要涉及两方面的问题。
其一是尽量减少动态链接库的数据段。我们知道,所谓共享,共享的只是代码。而对于动态库的数据段却是无法供多个进程共享 的。系统将为每个共享该库文件的进程都分配该库整个数据段的一份内存拷贝。因此,要想真正实现动态链接库少占用内存的目的,必须尽可能地减小共享对象的数 据段大小。归纳起来,大致有以下四种方法:

(1)尽量使用自动(堆栈)变量。如果自动变量行的通,就不要使用全局变量或者静态变量。
(2)尽量使用函数接口而少使用全局变量进行参数的传递。这样还能够提高程序的可维护性。
(3)将那些大量使用全局变量的函数排除在动态链接库之外。对于此类函数,将其放到静态链接库中比较合适。
(4) 动态链接库应是自包含的。也就是说在生成某个动态链接库时,对其他库的函数调用不要使用动态链接而应使用静态链接。因为在此种情况下使用动态链接时,调用 该动态库中函数的进程将除了得到该动态库的数据段的拷贝以外,还将得到该数据库说链接的其他动态库的数据段拷贝。这实际上是得不偿失的了。

动态链接性能的改善所涉及到的第二个问题,尽量减少内存页面的交换动作。虽然使用共享库的进程并不会写共享页面,但它们仍然可能引起页面失效而导致动态链接的性能降低。对这个问题可以使用以下两种方法加以解决:

(1)改进对符号引用的定位性 这包括两方面的含义。其一在共享库中排除哪些很少用到的、库本身不依赖于它们的那些函数定义。如果共享库中装有许多不相关的函数,而且只是一些不相关的进 程偶尔调用这些函数,那么定位性将降低,页面的交换将会变得频繁。其二是要尽可能把相关的函数组合在一起,放到同一个页面中以改进引用的定位性。

例如:假定func1()调用了func2()和func3(),并且这三个函数的代码被放到同一页中。那么在执行func1()的代码时,func2 ()和func3()地代码也将被同时装入内存。而假如func2()的代码与func1()的代码不在同一页上。那么在执行func1()的过程中可能 还需要去调页,从而系统的效率将受到影响。

(2)调整页面安排 这主要是指要对共享库的目标文件进行整理,使那些频繁使用的函数的代码不要越过页边界被分到不同的页上去了。完成这个工作首先要搞清楚系统内存页面的大 小。然后用nm命令显示出目标文件中各符号的偏移值。然后据此可对各函数的位置进行调整,使那些必须跨越页边界的是那些使用不太频繁的函数,以尽可能提高 页面的命中率。

上述对动态链接性能改善的分析实际上也是动态链接的一些不足。因此并不是说在什么情况下都应使用动态链接而排除静态链接。理想情况下,对同一个库应提供其 静态和动态两个不同的版本。这主要是由于一些用户可能找不到适合自己应用程序的动态库,另一方面有些UNIX系统并不支持共享对象。从上面我们对静态库和 动态库建立方法的介绍,知道完成这一点并不是什么特别费力的事。

 
原创粉丝点击