C专家编程5.docx

上传人:b****2 文档编号:24621521 上传时间:2023-05-29 格式:DOCX 页数:46 大小:87.28KB
下载 相关 举报
C专家编程5.docx_第1页
第1页 / 共46页
C专家编程5.docx_第2页
第2页 / 共46页
C专家编程5.docx_第3页
第3页 / 共46页
C专家编程5.docx_第4页
第4页 / 共46页
C专家编程5.docx_第5页
第5页 / 共46页
点击查看更多>>
下载资源
资源描述

C专家编程5.docx

《C专家编程5.docx》由会员分享,可在线阅读,更多相关《C专家编程5.docx(46页珍藏版)》请在冰豆网上搜索。

C专家编程5.docx

C专家编程5

对链接的思考

二凸匕

PallMallGazette于1889年3月ll日描述“托马斯爱迪生先生最近两晚都没合眼,他

在他的留声机里发现了一个‘Bu9’。

——托马簸爱遭生发现Bu9,1878

先驱的HarvardMarkIl计算机系统有一本El志,现保存在位于Smithsonian的美国国家历

史博物馆。

El=惠1947年9月9目的记录里有一只昆虫的遗蜕,可能是它偶尔飞到书页中,当

书合上时被夹在了那里。

记录里有个标签,标题是“Relay#70PanelF(飞蛾)inrelay”,

在这下面,记录了这么一句话“发现了第一个Bu9实例”。

——GrnceHopper发现Bu9,1947

当我们刚开始编程时,就惊奇地发现要让程序正确运转比想象的要难。

我们不得不使用

调试技术。

我还清楚地记得那一刻,从那时开始我就领悟到,从我自己的程序里寻找错误将

成为我生活的一个重要组成部分。

——胁luriceWilkes发现Bu9,1949

程序测试可用于发现Bu9,从来不曾有一个测试未发现Bu9。

——EdsgerW.Dijkstra发现Bu9,1972

5.1函数靡、链攘和载人

一开始,让我们回顾一下链接器(1inker)的基础知识:

编译器创建一个输出文件,这个文

件包含了可重定位的对象。

这些对象就是与源程序对应的数据和机器指令。

本章所使用的实

例就是存在于所有SRV4系统中的复杂链接形式。

C专家编程

链接器位于编译过程的哪一阶段

绝大多数编译器并不是一个单一的庞大程序。

它们通常由多达六七个稍小的程序所组成,

这些程序由一个叫做“编译器驱动器(compilerdriver)的控制程序来调用。

这些可以方便地从

编译器中分离出来的单独程序包括:

预处理器(preprocessor)、语法和语义检查器(syntacticand

semanticchecker)、代码生成器(codegenerator)、汇编程序(assembler)、优化器(optimizer)、链

接器(1inker),当然还包括一个调用所有这些程序并向各个程序传递正确选项的驱动器程序

fdriverprogram)(见图5—1)。

优化器几乎可以加在上述所有阶段的后面。

当前的SPARC编译

器在编译器的前端和后端之间的中间表示层执行绝大部分的优化措施。

C预处理器

阶段p

前端

(语法和语义分析)

阶段。

后端

(代码生成器)

阶段C

优化器

阶段2

汇编程序童

阶段8上.

1链接.载入器-

阶段l

图5.1编译器通常分割成几个更小的程序

它们之所以分成几个独立的程序,是因为在程序中如果每个具有特定功能的部分自身都

是一个完整的程序,就会更容易设计和维护。

例如,控制预处理过程的规则是预处理阶段所

独有的,它跟C语言的其他部分并没多少共同之处。

C预处理器经常(但并不总是)是一个

92

独立的程序。

如果代码生成器(又称“后端”)被编写成一个独立的程序,它很可能可以被其

他语言共享。

这种设计方法的代价是运行几个更小的程序比运行一个大型程序所花费的时间

要长(因为存在初始化进程以及在各个阶段之间传递信息的开销)。

可以使用一≠}选项查看编译

过程的各个独立阶段。

.V选项能提供版本信息。

可以通过给编译器驱动器一个特殊的一w选项(表示传递这个选项到那个阶段)向各个

阶段传递选项信息。

“W”后面跟一个字符(提示哪个阶段),一个逗号,然后就是具体的选

项。

代表各个阶段的字符也出现在图5—1中。

所以,如果要从编译器驱动器向链接器传递任何选项,必须在具体的选项前面加上“-W1”

前缀,告诉编译器驱动器这个选项是想传给链接器,而不是预处理器或编译器或汇编程序或

其他编译阶段。

下面这条命令:

CC—Wl,一mmain.c>main.上inker-map

将“.m,,选项传递给链接.载入器,要求它产生链接器映像。

你应该试上几次,看看它所

产生的是何种信息。

目标文件并不能直接执行,它首先需要载入到链接器中。

链接器确认main函数为初灼进

入点(程序开始执行的地方),把符号引用(symbolicreference)绑定到内存地址,把所有的目

标文件集中在一起,再加上库文件,从而产生可执行文件。

用于PC的链接机制与那些用于更大系统的链接机制有着巨大的差别。

PC的链接器一般

只提供几个基本的I/O服务,就是被称作BIOS的程序。

它们存在于内存中固定的地点,并

不是每个可执行文件的一部分。

如果Pc程序或程序套件需要更高级的服务,可以通过库函

数提供,但编译器必须把库函数链接到每个可执行文件中。

在MS—DOS中,没有办法推断出

函数库对其中几个程序较为常用,从而只在PC上安装一次。

UNIX系统以前也是如此。

当链接程序时,需要使用的每个库函数的一份拷贝被加入到

可执行文件中。

近几年,一种更为现代和优越的被称作动态链接的方法逐渐被采用。

动态链

接允许系统提供一个庞大的函数库集合,可以提供许多有用的服务。

但是,程序将在运行时

寻找它们,而不是把这些函数库的二进制代码作为自身可执行文件的一部分。

IBM的OS/2

操作系统具有动态链接的功能,Microsoft新型旗舰级WindowsNT操作系统也具有动态链接

功能。

最近几年,Microsoft在它的Windows桌面操作系统中也采用了动态链接。

如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;如果

可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们

称之为动态链接。

收集模块准备执行的三个阶段的规范名称是链接一编辑(1ink—editing)、载入

(10ading)和运行时链接(runtimelinking)。

静态链接的模块被链接编辑并载,z,nA便运行。

动态

链接的模块被链接编辑后载入,并在运行时进行链接以便运行。

程序执行时,在main()函数

被调用前,运行时载入器把共享的数据对象载入到进程的地址空间。

外部函数被真正调用之

前,运行时载入器并不解析它们。

所以即使链接了函数库,如果并没有实际调用,。

也不会带

来额外开销。

这两种链接方法在图5—2中作了比较。

93

C专家编程

产生

产生

一一一一一一一一一一一一一一一J

库函数在运行时被映射到进程中

注:

图中的文件大小仅用于说明的目的,与实际情况可能不同。

图5.2静态链接与动态链接

即使是在静态链接中,整个libc.a文件也并没有被全部装入到可执行文件中,所装入的

只是所需要的函数。

5曩动态链接的优点

动态链接是一种更为现代的方法,它的优点是可执行文件的体积可以非常小。

虽然运行

速度稍慢一些,但动态链接能够更加有效地利用磁盘空间,而且链接.编辑阶段的时间也会缩

短(因为链接器的有些工作被推迟到载入时)。

动态链接的目的之一是ABI

动态链接的主要目的就是把程序与它们使用的特定的函数库版本中分离开来。

取而代之

的是,我们约定由系统向程序提供一个接口,该接口保持稳定,不随时间和操作系统的后续

版本发生变化。

程序可以调用接口所承诺的服务,而不必担心这些功能是怎样提供的或者它们的底层实

现是否改变。

由于它是介于应用程序和函数库二进制可执行文件所提供的服务之间的接口,

所以称它为应用程序二进制接口(ApplicationBinaryInterface,ABI)。

94

第5章对链接的思考

统一基于AT&T的SVr4的UNIX世界的目的就是提供一个单独的ABl。

ABl保证函数

库存在于所有遵循约定的机器中,并保证接口的完整性。

动态链接必须保证4个特定的函数

库:

libc(C运行时函数库)、libsys(其他系统函数)、1ibX(Xwindowing)和libnsl(网络服

务)。

其他的函数库可以通过静态链接,但最好采用动态链接。

过去,应用程序销售商在每次新版本的操作系统或函数库出现时都必须重新链接他们的

软件。

这带来了巨大的额外工作量,因为需要照顾许多方方面面。

ABl就不需要这样做,它

保证运作良好的应用程序不会受同样运作良好的底层系统软件升级的影响。

尽管单个可执行文件的启动速度稍受影响,但动态链接可以从两个方面提高性能:

1.动态链接可执行文件比功能相同的静态链接可执行文件的体积小。

它能够节省磁盘空

间和虚拟内存,因为函数库只有在需要时才被映射到进程中。

以前,避免把函数库的拷贝绑

定到每个可执行文件的惟一方法就是把服务置于内核中而不是函数库中,这就带来了可怕的

“内核膨胀”问题。

2.所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷

贝。

操作系统内核保证映射到内存中的函数库可以被所有使用它们的进程共享。

这就提供了

更好的I/O和交换空间利用率,节省了物理内存,从而提高了系统的整体性能。

如果可执行

文件是静态链接的,每个文件都将拥有一份函数库的拷贝,显然极为浪费。

例如,如果你有八个基于XWiewTM函数库的应用程序正在运行,只需要把一个XⅥew函

数库文本段映射到内存中。

第一个进程的mmapl调用将使内核把共享对象映射到内存中。

余七个进程的mmap调用将使内核把已经映射到内存中的对象由各个进程共享。

这八个进程

的每一个都将共享内存中的同一份XⅥew函数库拷贝。

如果函数库是静态链接的,将会有八

份函数库拷贝映射到内存中,这将消耗更多的物理内存,引起更多的换页。

动态链接使得函数库的版本升级更为容易。

新的函数库可以随时发布,只要安装到系统

中,旧的程序就能够自动获得新版本函数库的优点而无需重新链接。

最后(虽然并不常见,但仍可能出现),动态链接允许用户在运行时选择需要执行的

函数库。

这就使为了提高速度或提高内存使用效率或包含额外的调试信息而创建新版本的

函数库是完全可能的,用户可以根据自己的喜好,在程序执行时用一个库文件取代另一个

库文件。

动态链接是一种“just-in.time(JIT)”链接,这意味着程序在运行时必须能够找到它们所

需要的函数库。

链接器通过把库文件名或路径名植入可执行文件中来做到这一点。

这意味着,

函数库的路径不能随意移动。

如果把程序链接至1]/user/lib/libthread.s0库,那么就不能把该函数

库移动到其他的目录,除非在链接器中进行特别说明。

否则,当程序调用该函数库的函数时,

系统调用IIllnap()把文件映射到进程的地址空间中。

这样,文件的内容可以通过读取连续的内存地址来获得。

当文件包含可执

行文件的指令时,这种方法尤为适宜。

在SVr4系统中,文件系统被当作虚拟内存系统的一部分,而mmap就是一种把文件映

射到内存的机制。

95

C专家编程

就会在运行时导致失败,给出这样一条错误信息:

ld.s0.1:

main:

fatal:

上ibthread.s0:

can’topenflle:

errno=2

当在一台机器上编译完程序后,把它拿到另一台不同的机器上运行时,也可能出现这种

情况。

执行程序的机器必须具有所有该程序需要链接的函数库,而且这些函数库必须位于在

链接器中所说明的目录。

对于标准系统函数库而言,这并不成问题。

使用共享函数库的主要原因就是获得ABl的好处——使你的软件不必因新版本函数库或

操作系统的发布而重新链接。

附带的一个好处是,它也能提高系统的总体性能。

任何人都可以创建静态或动态的函数库。

只需简单地编译一些不包含main函数的代码,

并把编译所生的.0文件用正确的实用工具进行处理——如果是静态库,使用“ar”,如果是动

态库,使用“ld”。

软件信条

只使用动态链接

动态链接现在是运行SystemVrelease4UNIX的计算机所采用的缺省设置。

从作用上

看,静态链接现已过时,只能静静躺在一边睡大觉。

使用静态链接的最大危险在于将来版本的操作系统可能与可执行文件所绑定的系统函

数库不兼容。

如果应用程序静态链接于版本N的操作系统中,当把程序运行于版本N+1的

操作系统上时,它可能会立即崩溃,也可能出现一个不明显的错误。

我们无法保证早期版本的系统函数库能够在后期版本的系统上正确地运行。

事实上,反

过来考虑倒还比较保险一点。

但是,如果应用程序动态链接到版本N的系统函数库,当它运

行于版本N+1的操作系统上时,它就会正确选取N+1版本的系统函数库。

相反,静态链接

的应用程序不得不针对每个新版本的操作系统进行重新生成以保证能够运行。

而且,有些函数库(如libai0.S0,libdl.S0,libsys.S0,libsolv.S0以及librpcsvc.S0等)只能以

动态链接的形式使用。

如果在应用程序中使用了这些函数库中的任何一个,你的程序就必须

使用动态链接。

最好的策略就是所有的应用程序都使用动态链接,这就可以避免可能产生的

问题。

静态库被称作archive,它们通过ar(用于archive的实用工具)来创建和更新。

ar工具

的名字取得不太好,如果广告学的原理也适用于软件的话,那么它应该取一个类似

glue_files_together(把文件粘在一起)的名字,或干脆就取static_library_updater(静态库更

新器)。

静态库约定在它们的文件名中使用“.a”的扩展名。

我在这里没有给出一个创建静态

库的例子,因为它们现在已经过时,我并不想鼓励任何人停留在精神世界进行交流。

 

第5章对链接的思考

在SVR3中,还存在一种中间性质的链接,介于静态链接和动态链接之间,称为“静态

共享库(staticsharedlibraries)”。

在生命期内,它们的地址始终固定,这样它们就可以直接绑

定到应用程序中,较之动态链接少了一层中间环节。

但另一方面,它们显得不是很灵活,而

且需要操作系统提供很多支持。

因此,以后不再讨论它们。

动态链接库由链接编辑器ld创建。

根据约定,动态库的文件扩展名为“.S0”,表示“shared

object(共享对象)”——每一个链接到该函数库的程序都共享它的同一份拷贝。

而静态链接

则相反,每个对象都拥有一份该函数库内容的拷贝,显得浪费。

动态链接库的最简单形式可

以通过在CC命令上加上.G选项来创建,如下所示:

%cattomat0.c

my—lib—function()(printf(”libraryroutinecalled\n”);)

%cc—olibfruit.§0—Gtomat0.c

然后,就可以利用这个动态链接库来编写程序了,并且使用下面这种方法与函数库进行

链接:

%cattest.c

main(){my—lib—function();)

%cctest.c—L/home/linden—R/home/linden—lfruit

%a.out

libraryroutinecalled

.L/home/linden和-R/home/linden选项分别告诉链接器在链接时和运行时从哪个目录寻

找需要链接的函数库。

你很可能还想使用编译器选项.Kpic来为函数库产生与位置无关的代码。

与位置无关的

代码表示用这种方法产生的代码保证对于任何全局数据的访问都是通过额外的间接方法完成

的。

这使它很容易对数据进行重新定位,只要简单地修改全局偏移量表的其中一个值就可以

了。

类似地,每个函数调用的产生就像是通过过程链接表的某个间接地址所产生的一样。

样,文本可以很容易地重新定位到任何地方,只要修改一下偏移量表就可以了。

所以当代码

在运行时被映射进来时,运行时链接器可以直接把它们放在任何空闲的地方,而代码本身并

不需要修改。

在缺省情况下,编译器并不产生与位置无关的代码,因为额外的指针解除引用操作将使

程序在运行时稍稍变慢。

然而,如果不使用与位置无关的代码,所产生的代码就会被对应到

固定的地址,这对于可执行文件来说确实很好,但对于共享库,速度却要慢一点,因为现在

每个全局引用就不得不在运行时通过修改页面安排到固定的位置,这就使得页面无法共享。

运行时链接器总能够安排对页面的引用。

但是,使用位置无关代码,任务被极大地简化

了。

当然需要权衡一下,位置无关代码与由运行时链接器安排代码相比,速度是快了还是慢

了。

根据经验,对于函数库应该始终使用与位置无关代码。

对于共享库,与位置无关的代码

显得格外有用,因为每个使用共享库的进程一般都会把它映射到不同的虚拟地址(尽管共享

97

C专家编程j

同一份物理拷贝)。

一个相关的术语是“纯代码(purecode)”。

纯可执行文件是只包含代码(无静态或初始化

过的数据)的文件。

它之所以称为“纯”是因为它不必进行修改就能被其他特定的进程执行。

它从堆栈或者其他(非纯)段引用数据。

纯代码段可以被共享。

如果生成与位置无关代码(意

味着共享),你通常也希望它是纯代码。

5.3函数库链接的s个特殊秘密

当使用函数库时,需要掌握5个基本的、不明显的约定。

绝大多数c语言书籍或手册对

此并没有作出清楚的解释。

这可能是因为编程语言的文档认为链接是操作系统的一部分。

是,设计操作系统的人们却认为链接是语言的一部分。

结果,除非是链接器开发队伍的人参

与进来,否则人们顶多也就偶尔提到它一下。

这里展示了关于UNIX链接的真实情况:

1.动态库文件的扩展名是“.80”,而静态库文件的扩展名是“.a”

按照约定,所有动态库的文件名的形式是libname.SO(可能在名字中加入版本号)。

这样,

线程函数库便被称作libthread.S0。

静态库的文件名形式是libname.a,共享archive的文件名

形式是libname.sa。

共享archive只是一种过渡形式,帮助人们从静态库转变到动态库。

共享

archive现在也已过时。

2.例如,你通过.lthread选项,告诉编译链接到iibthread.SO

传给C编译器的命令行参数里并没有提到函数库的完整路径名。

它甚至没有提到在函数

库目录中该文件的完整名字!

实际上,编译器被告知根据选项.1name链接到相应的函数库,

函数库的名字是linbname.s0——换句话说,“lib”部分和文件的扩展名被省掉了,但在前面

加一个“l”。

3.编译器期望在确定的目录找到库’

这里,你可能会疑惑,编译器是怎么知道该往什么目录寻找函数库呢?

就像存在一种特

殊的规则用于查找头文件一样,编译器也自有办法来寻找函数库。

它查看一些特殊的位置,

如在/usr/lib中查找函数库。

例如,线程库位于/usr/lib/libthread.S0。

编译器选项.Lpathname告诉链接器一些其他的目录,如果命令中加入了.l选项,链接器

就往这些目录查找函数库。

系统中存在几个环境变量,LDLIBRARYPATH和

LDRUNPATH,也是用于提供这类信息。

出于安全性、性能和创建/运行独立性方面的考虑,

使用环境变量的做法现在已经不提倡。

一般还是在链接时使用.Lpathname和.Rpanm锄e选项。

4.观察头文件,确认所使用的函数库

你有可能遇见的另一个关键问题是“我怎么知道必须链接到哪些函数库?

”答案正如

ObiWanKenobi在StarWays所清楚表达的那样(大意):

“卢克,使用源码!

”。

如果观察程序

中的源代码,就会发现自己调用了一些自己不曾实现的函数。

例如,如果程序跟三角有关,

可能会调用像sin()和cos()这样的函数,它们可以在math函数库中找到。

文档中显示了每个

函数期望接收的正确的参数类型,并说明它位于哪个函数库。

98

第5章对链接的思考

一个很好的建议就是可以观察程序所使用的#include指令。

在程序中所包含的每个头文

件都可能代表一个必须链接的库。

这个建议也适用于c++。

这里出现了一个名字不一致的大

问题。

头文件的名字通常并不与它所对应的函数库名相似。

非常遗憾!

这是你“不得不知道

的”C语言的一个混乱之处。

表5.1展示了一些常见的例子。

表.5.1Solaris2.x下的库约定

#include文件名库路径名所用的编译器选项

/usr/lib/libm.S0—lm

/usr/lib/libm.a—dn—lm

/usr/lib/libc.S0自动链接

tt/usr/openwm/include/Xll.h,,/usr/openwin/lib/libXll.SO-L/usr/openwin/lib一Ⅸll

/usr/lib/libthread.S07一ithread

/usr/lib/libIcurses.a—lcurses

/usr/lib/libsocket.S0一1socket

函数库链接所存在的另一个不一致性就是函数库包含许多函数的定义,但这些函数的原

型声明却散布于多个头文件中。

例如,在头文件中声明的函

数通常是在同一个库libc.S0中提供。

如果你不信,可以使用nm工具程序列出函数库所包含

的函数。

在下面的小启发栏目里我将详细讨论这一点。

怎样在函数库中观察一个符号

如果在链接程序时遇到下面这种错误:

ld:

underfinedsymbol

——xdr——reference

★★+Errorcode2

make:

Fatalerror:

Commandfailedfortarget7pr097

它提示找不到符号xdrreference的定义。

这里有一种方法,可以通过它找到需要链接的

库。

基本的想法是使用nln命令在/usr/lib的每个函数库中浏览所有的符号,从中寻找所丢失

的符号。

在缺省情况下,链接器会在/usr/ccs/lib和/usr/lib中查找,你也应该从这两个地方着

手,如果在那里找不到就进一步扩展查找范围(如sur/openwin/lib)。

%cd/usr/lib

99

C专家编程

%foreachi(1ib?

+)

?

echoSi

?

nm$iIgrepxdr~refrence

?

end

libC.SO

libnsl.SO

[2491】l2170281196lFUNC

1ibposix4.sO

grep—VUNDEF

IGLOBlol8{xdr—reference

这会在该目录中的所有函数库上运行“nn]”程序,它显示函数库中已知的符号列表。

过grep设定需要搜索的符号,并过滤掉标记为“UNDEF”的符号(在该函数库中有引用,

但并不是在此处定义)。

结果显示xdr—reference位于libnsl库。

需要在编译器命令行的末尾

力口上.lnsl。

5.与提取动态库中的符号相比,静态库中的符号提取的方法限制更严

最后,在动态链接和静态链接的链接语义上还存在一个额外的巨大区别,它经常会迷惑

不够仔细的用户。

archive(静态库)与共享对象‘(动态库)的动作不同。

在动态链接中,所

有的库符号进入输出文件的虚拟地址空间中,所有的符号对于链接在一起的所有文件都是可

见的。

相反,对于静态链接,在处理archive时,它只是在archive中查找载入器当时所知道

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 自然科学 > 化学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1