ImageVerifierCode 换一换
格式:DOCX , 页数:46 ,大小:87.28KB ,
资源ID:24621521      下载积分:3 金币
快捷下载
登录下载
邮箱/手机:
温馨提示:
快捷下载时,用户名和密码都是您填写的邮箱或者手机号,方便查询和重复下载(系统自动生成)。 如填写123,账号就是123,密码也是123。
特别说明:
请自助下载,系统不会自动发送文件的哦; 如果您已付费,想二次下载,请登录后访问:我的下载记录
支付方式: 支付宝    微信支付   
验证码:   换一换

加入VIP,免费下载
 

温馨提示:由于个人手机设置不同,如果发现不能下载,请复制以下地址【https://www.bdocx.com/down/24621521.html】到电脑端继续下载(重复下载不扣费)。

已注册用户请登录:
账号:
密码:
验证码:   换一换
  忘记密码?
三方登录: 微信登录   QQ登录  

下载须知

1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。
2: 试题试卷类文档,如果标题没有明确说明有答案则都视为没有答案,请知晓。
3: 文件的所有权益归上传用户所有。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 本站仅提供交流平台,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

版权提示 | 免责声明

本文(C专家编程5.docx)为本站会员(b****2)主动上传,冰豆网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。 若此文所含内容侵犯了您的版权或隐私,请立即通知冰豆网(发送邮件至service@bdocx.com或直接QQ联系客服),我们立即给予删除!

C专家编程5.docx

1、C专家编程5对链接的思考二凸匕早 Pall Mall Gazette于1889年3月l l日描述“托马斯爱迪生先生最近两晚都没合眼,他在他的留声机里发现了一个Bu9。” 托马簸爱遭生发现Bu9,1878 先驱的Harvard MarkIl计算机系统有一本El志,现保存在位于Smithsonian的美国国家历史博物馆。El=惠1947年9月9目的记录里有一只昆虫的遗蜕,可能是它偶尔飞到书页中,当书合上时被夹在了那里。记录里有个标签,标题是“Relay#70 Panel F(飞蛾)in relay”,在这下面,记录了这么一句话“发现了第一个Bu9实例”。 Grnce Hopper发现Bu9,194

2、7 当我们刚开始编程时,就惊奇地发现要让程序正确运转比想象的要难。我们不得不使用调试技术。我还清楚地记得那一刻,从那时开始我就领悟到,从我自己的程序里寻找错误将成为我生活的一个重要组成部分。 胁lurice Wilkes发现Bu9,1949 程序测试可用于发现Bu9,从来不曾有一个测试未发现Bu9。 Edsger WDijkstra发现Bu9,197251函数靡、链攘和载人 一开始,让我们回顾一下链接器(1inker)的基础知识:编译器创建一个输出文件,这个文件包含了可重定位的对象。这些对象就是与源程序对应的数据和机器指令。本章所使用的实例就是存在于所有SRV4系统中的复杂链接形式。C专家编程

3、链接器位于编译过程的哪一阶段 绝大多数编译器并不是一个单一的庞大程序。它们通常由多达六七个稍小的程序所组成,这些程序由一个叫做“编译器驱动器(compiler driver)的控制程序来调用。这些可以方便地从编译器中分离出来的单独程序包括:预处理器(preprocessor)、语法和语义检查器(syntactic andsemantic checker)、代码生成器(code generator)、汇编程序(assembler)、优化器(optimizer)、链接器(1inker),当然还包括一个调用所有这些程序并向各个程序传递正确选项的驱动器程序fdriverprogram)(见图51)。优

4、化器几乎可以加在上述所有阶段的后面。当前的SPARC编译器在编译器的前端和后端之间的中间表示层执行绝大部分的优化措施。C预处理器阶段p 前端(语法和语义分析)阶段。 后端(代码生成器)阶段C优化器阶段2 汇编程序 童阶段8上1 链接载入器 - 阶段l图51 编译器通常分割成几个更小的程序 它们之所以分成几个独立的程序,是因为在程序中如果每个具有特定功能的部分自身都是一个完整的程序,就会更容易设计和维护。例如,控制预处理过程的规则是预处理阶段所独有的,它跟C语言的其他部分并没多少共同之处。C预处理器经常(但并不总是)是一个92叠独立的程序。如果代码生成器(又称“后端”)被编写成一个独立的程序,它

5、很可能可以被其他语言共享。这种设计方法的代价是运行几个更小的程序比运行一个大型程序所花费的时间要长(因为存在初始化进程以及在各个阶段之间传递信息的开销)。可以使用一选项查看编译过程的各个独立阶段。V选项能提供版本信息。 可以通过给编译器驱动器一个特殊的一w选项(表示传递这个选项到那个阶段)向各个阶段传递选项信息。“W”后面跟一个字符(提示哪个阶段),一个逗号,然后就是具体的选项。代表各个阶段的字符也出现在图51中。 。所以,如果要从编译器驱动器向链接器传递任何选项,必须在具体的选项前面加上“-W1”前缀,告诉编译器驱动器这个选项是想传给链接器,而不是预处理器或编译器或汇编程序或其他编译阶段。下

6、面这条命令: CCWl,一m maincmain上inker-map 将“m,选项传递给链接载入器,要求它产生链接器映像。你应该试上几次,看看它所产生的是何种信息。 目标文件并不能直接执行,它首先需要载入到链接器中。链接器确认main函数为初灼进入点(程序开始执行的地方),把符号引用(symbolic reference)绑定到内存地址,把所有的目标文件集中在一起,再加上库文件,从而产生可执行文件。 用于PC的链接机制与那些用于更大系统的链接机制有着巨大的差别。PC的链接器一般只提供几个基本的IO服务,就是被称作BIOS的程序。它们存在于内存中固定的地点,并不是每个可执行文件的一部分。如果Pc

7、程序或程序套件需要更高级的服务,可以通过库函数提供,但编译器必须把库函数链接到每个可执行文件中。在MSDOS中,没有办法推断出函数库对其中几个程序较为常用,从而只在PC上安装一次。 UNIX系统以前也是如此。当链接程序时,需要使用的每个库函数的一份拷贝被加入到可执行文件中。近几年,一种更为现代和优越的被称作动态链接的方法逐渐被采用。动态链接允许系统提供一个庞大的函数库集合,可以提供许多有用的服务。但是,程序将在运行时寻找它们,而不是把这些函数库的二进制代码作为自身可执行文件的一部分。IBM的OS2操作系统具有动态链接的功能,Microsoft新型旗舰级Windows NT操作系统也具有动态链接

8、功能。最近几年,Microsoft在它的Windows桌面操作系统中也采用了动态链接。 如果函数库的一份拷贝是可执行文件的物理组成部分,那么我们称之为静态链接;如果可执行文件只是包含了文件名,让载入器在运行时能够寻找程序所需要的函数库,那么我们称之为动态链接。收集模块准备执行的三个阶段的规范名称是链接一编辑(1inkediting)、载入(10ading)和运行时链接(runtime linking)。静态链接的模块被链接编辑并载,z,nA便运行。动态链接的模块被链接编辑后载入,并在运行时进行链接以便运行。程序执行时,在main()函数被调用前,运行时载入器把共享的数据对象载入到进程的地址空间

9、。外部函数被真正调用之前,运行时载入器并不解析它们。所以即使链接了函数库,如果并没有实际调用,。也不会带来额外开销。这两种链接方法在图52中作了比较。93C专家编程产生产生一一一一一一一一一一一一一一一J 库函数在运行时被映射到进程中注:图中的文件大小仅用于说明的目的,与实际情况可能不同。 图52静态链接与动态链接 即使是在静态链接中,整个libca文件也并没有被全部装入到可执行文件中,所装入的只是所需要的函数。5曩动态链接的优点 动态链接是一种更为现代的方法,它的优点是可执行文件的体积可以非常小。虽然运行速度稍慢一些,但动态链接能够更加有效地利用磁盘空间,而且链接编辑阶段的时间也会缩短(因为

10、链接器的有些工作被推迟到载入时)。 动态链接的目的之一是ABI 动态链接的主要目的就是把程序与它们使用的特定的函数库版本中分离开来。取而代之的是,我们约定由系统向程序提供一个接口,该接口保持稳定,不随时间和操作系统的后续版本发生变化。 程序可以调用接口所承诺的服务,而不必担心这些功能是怎样提供的或者它们的底层实现是否改变。由于它是介于应用程序和函数库二进制可执行文件所提供的服务之间的接口,所以称它为应用程序二进制接口(Application Binary Interface,ABI)。 94第5章对链接的思考 统一基于AT&T的SVr4的UNIX世界的目的就是提供一个单独的ABl。ABl保证函

11、数库存在于所有遵循约定的机器中,并保证接口的完整性。动态链接必须保证4个特定的函数库:libc(C运行时函数库)、libsys(其他系统函数)、1ibX(X windowing)和libnsl(网络服务)。其他的函数库可以通过静态链接,但最好采用动态链接。 过去,应用程序销售商在每次新版本的操作系统或函数库出现时都必须重新链接他们的软件。这带来了巨大的额外工作量,因为需要照顾许多方方面面。ABl就不需要这样做,它保证运作良好的应用程序不会受同样运作良好的底层系统软件升级的影响。 尽管单个可执行文件的启动速度稍受影响,但动态链接可以从两个方面提高性能: 1动态链接可执行文件比功能相同的静态链接可

12、执行文件的体积小。它能够节省磁盘空间和虚拟内存,因为函数库只有在需要时才被映射到进程中。以前,避免把函数库的拷贝绑定到每个可执行文件的惟一方法就是把服务置于内核中而不是函数库中,这就带来了可怕的“内核膨胀”问题。 2所有动态链接到某个特定函数库的可执行文件在运行时共享该函数库的一个单独拷贝。操作系统内核保证映射到内存中的函数库可以被所有使用它们的进程共享。这就提供了更好的IO和交换空间利用率,节省了物理内存,从而提高了系统的整体性能。如果可执行文件是静态链接的,每个文件都将拥有一份函数库的拷贝,显然极为浪费。 例如,如果你有八个基于XWiewTM函数库的应用程序正在运行,只需要把一个Xew函数

13、库文本段映射到内存中。第一个进程的mmapl调用将使内核把共享对象映射到内存中。其余七个进程的mmap调用将使内核把已经映射到内存中的对象由各个进程共享。这八个进程的每一个都将共享内存中的同一份Xew函数库拷贝。如果函数库是静态链接的,将会有八份函数库拷贝映射到内存中,这将消耗更多的物理内存,引起更多的换页。 动态链接使得函数库的版本升级更为容易。新的函数库可以随时发布,只要安装到系统中,旧的程序就能够自动获得新版本函数库的优点而无需重新链接。 最后(虽然并不常见,但仍可能出现),动态链接允许用户在运行时选择需要执行的函数库。这就使为了提高速度或提高内存使用效率或包含额外的调试信息而创建新版本

14、的函数库是完全可能的,用户可以根据自己的喜好,在程序执行时用一个库文件取代另一个库文件。 动态链接是一种“just-intime(JIT)”链接,这意味着程序在运行时必须能够找到它们所需要的函数库。链接器通过把库文件名或路径名植入可执行文件中来做到这一点。这意味着,函数库的路径不能随意移动。如果把程序链接至1userliblibthreads0库,那么就不能把该函数库移动到其他的目录,除非在链接器中进行特别说明。否则,当程序调用该函数库的函数时,系统调用IIllnap()把文件映射到进程的地址空间中。这样,文件的内容可以通过读取连续的内存地址来获得。当文件包含可执行文件的指令时,这种方法尤为适

15、宜。在SVr4系统中,文件系统被当作虚拟内存系统的一部分,而mmap就是一种把文件映射到内存的机制。 95C专家编程就会在运行时导致失败,给出这样一条错误信息: lds01:main:fatal:上ibthreads0:cant open flle:errno=2 当在一台机器上编译完程序后,把它拿到另一台不同的机器上运行时,也可能出现这种情况。执行程序的机器必须具有所有该程序需要链接的函数库,而且这些函数库必须位于在链接器中所说明的目录。对于标准系统函数库而言,这并不成问题。 使用共享函数库的主要原因就是获得ABl的好处使你的软件不必因新版本函数库或操作系统的发布而重新链接。附带的一个好处是

16、,它也能提高系统的总体性能。 任何人都可以创建静态或动态的函数库。只需简单地编译一些不包含main函数的代码,并把编译所生的0文件用正确的实用工具进行处理如果是静态库,使用“ar”,如果是动态库,使用“ld”。软件信条只使用动态链接 动态链接现在是运行System V release 4 UNIX的计算机所采用的缺省设置。从作用上看,静态链接现已过时,只能静静躺在一边睡大觉。 使用静态链接的最大危险在于将来版本的操作系统可能与可执行文件所绑定的系统函数库不兼容。如果应用程序静态链接于版本N的操作系统中,当把程序运行于版本N+1的操作系统上时,它可能会立即崩溃,也可能出现一个不明显的错误。 我们

17、无法保证早期版本的系统函数库能够在后期版本的系统上正确地运行。事实上,反过来考虑倒还比较保险一点。但是,如果应用程序动态链接到版本N的系统函数库,当它运行于版本N+1的操作系统上时,它就会正确选取N+1版本的系统函数库。相反,静态链接的应用程序不得不针对每个新版本的操作系统进行重新生成以保证能够运行。 而且,有些函数库(如libai0S0,libdlS0,libsysS0,libsolvS0以及librpcsvcS0等)只能以动态链接的形式使用。如果在应用程序中使用了这些函数库中的任何一个,你的程序就必须使用动态链接。最好的策略就是所有的应用程序都使用动态链接,这就可以避免可能产生的问题。 静

18、态库被称作archive,它们通过ar(用于archive的实用工具)来创建和更新。ar工具的名字取得不太好,如果广告学的原理也适用于软件的话,那么它应该取一个类似glue_files_together(把文件粘在一起)的名字,或干脆就取static_library_updater(静态库更新器)。静态库约定在它们的文件名中使用“a”的扩展名。我在这里没有给出一个创建静态库的例子,因为它们现在已经过时,我并不想鼓励任何人停留在精神世界进行交流。第5章对链接的思考 在SVR3中,还存在一种中间性质的链接,介于静态链接和动态链接之间,称为“静态共享库(static shared libraries

19、)”。在生命期内,它们的地址始终固定,这样它们就可以直接绑定到应用程序中,较之动态链接少了一层中间环节。但另一方面,它们显得不是很灵活,而且需要操作系统提供很多支持。因此,以后不再讨论它们。 动态链接库由链接编辑器ld创建。根据约定,动态库的文件扩展名为“S0”,表示“sharedobject(共享对象)”每一个链接到该函数库的程序都共享它的同一份拷贝。而静态链接则相反,每个对象都拥有一份该函数库内容的拷贝,显得浪费。动态链接库的最简单形式可以通过在CC命令上加上G选项来创建,如下所示:cat tomat0c mylibfunction()(printf(”library routine ca

20、lledn”);)cco libfruit0G tomat0c 然后,就可以利用这个动态链接库来编写程序了,并且使用下面这种方法与函数库进行链接:cat testc main()mylibfunction();) cc testcLhomelindenRhomelindenlfruit aout library routine called Lhomelinden和-Rhomelinden选项分别告诉链接器在链接时和运行时从哪个目录寻找需要链接的函数库。 你很可能还想使用编译器选项K pic来为函数库产生与位置无关的代码。与位置无关的代码表示用这种方法产生的代码保证对于任何全局数据的访问都是通

21、过额外的间接方法完成的。这使它很容易对数据进行重新定位,只要简单地修改全局偏移量表的其中一个值就可以了。类似地,每个函数调用的产生就像是通过过程链接表的某个间接地址所产生的一样。这样,文本可以很容易地重新定位到任何地方,只要修改一下偏移量表就可以了。所以当代码在运行时被映射进来时,运行时链接器可以直接把它们放在任何空闲的地方,而代码本身并不需要修改。 在缺省情况下,编译器并不产生与位置无关的代码,因为额外的指针解除引用操作将使程序在运行时稍稍变慢。然而,如果不使用与位置无关的代码,所产生的代码就会被对应到固定的地址,这对于可执行文件来说确实很好,但对于共享库,速度却要慢一点,因为现在每个全局引

22、用就不得不在运行时通过修改页面安排到固定的位置,这就使得页面无法共享。 运行时链接器总能够安排对页面的引用。但是,使用位置无关代码,任务被极大地简化了。当然需要权衡一下,位置无关代码与由运行时链接器安排代码相比,速度是快了还是慢了。根据经验,对于函数库应该始终使用与位置无关代码。对于共享库,与位置无关的代码显得格外有用,因为每个使用共享库的进程一般都会把它映射到不同的虚拟地址(尽管共享97C专家编程 j同一份物理拷贝)。 一个相关的术语是“纯代码(pure code)”。纯可执行文件是只包含代码(无静态或初始化过的数据)的文件。它之所以称为“纯”是因为它不必进行修改就能被其他特定的进程执行。它

23、从堆栈或者其他(非纯)段引用数据。纯代码段可以被共享。如果生成与位置无关代码(意味着共享),你通常也希望它是纯代码。53函数库链接的s个特殊秘密 当使用函数库时,需要掌握5个基本的、不明显的约定。绝大多数c语言书籍或手册对此并没有作出清楚的解释。这可能是因为编程语言的文档认为链接是操作系统的一部分。但是,设计操作系统的人们却认为链接是语言的一部分。结果,除非是链接器开发队伍的人参与进来,否则人们顶多也就偶尔提到它一下。这里展示了关于UNIX链接的真实情况: 1动态库文件的扩展名是“80”,而静态库文件的扩展名是“a” 按照约定,所有动态库的文件名的形式是libnameSO(可能在名字中加入版本

24、号)。这样,线程函数库便被称作libthreadS0。静态库的文件名形式是libnamea,共享archive的文件名形式是libnamesa。共享archive只是一种过渡形式,帮助人们从静态库转变到动态库。共享archive现在也已过时。 2例如,你通过lthread选项,告诉编译链接到iibthreadSO 传给C编译器的命令行参数里并没有提到函数库的完整路径名。它甚至没有提到在函数库目录中该文件的完整名字!实际上,编译器被告知根据选项1name链接到相应的函数库,函数库的名字是linbnames0换句话说,“lib”部分和文件的扩展名被省掉了,但在前面加一个“l”。 3编译器期望在确定

25、的目录找到库 这里,你可能会疑惑,编译器是怎么知道该往什么目录寻找函数库呢?就像存在一种特殊的规则用于查找头文件一样,编译器也自有办法来寻找函数库。它查看一些特殊的位置,如在usrlib中查找函数库。例如,线程库位于usrliblibthreadS0。 编译器选项Lpathname告诉链接器一些其他的目录,如果命令中加入了l选项,链接器就往这些目录查找函数库。系统中存在几个环境变量,LD LIBRARY PATH和LD RUN PATH,也是用于提供这类信息。出于安全性、性能和创建运行独立性方面的考虑,使用环境变量的做法现在已经不提倡。一般还是在链接时使用Lpathname和Rpanm锄e选项

26、。 4观察头文件,确认所使用的函数库 你有可能遇见的另一个关键问题是“我怎么知道必须链接到哪些函数库?”答案正如ObiWan Kenobi在Star Ways所清楚表达的那样(大意):“卢克,使用源码!”。如果观察程序中的源代码,就会发现自己调用了一些自己不曾实现的函数。例如,如果程序跟三角有关,可能会调用像sin()和cos()这样的函数,它们可以在math函数库中找到。文档中显示了每个函数期望接收的正确的参数类型,并说明它位于哪个函数库。98 第5章对链接的思考 一个很好的建议就是可以观察程序所使用的#include指令。在程序中所包含的每个头文件都可能代表一个必须链接的库。这个建议也适用

27、于c+。这里出现了一个名字不一致的大问题。头文件的名字通常并不与它所对应的函数库名相似。非常遗憾!这是你“不得不知道的”C语言的一个混乱之处。表51展示了一些常见的例子。 表51 Solaris 2x下的库约定 #include文件名 库路径名 所用的编译器选项 usrliblibmS0 lm usrliblibma dnlm usrliblibcS0 自动链接ttusropenwmincludeXllh, usropenwinliblibXllSO -Lusropenwinlib一ll usrliblibthreadS0 7一ithread usrliblibIcursesa lcurses

28、 usrliblibsocketS0 一1socket 函数库链接所存在的另一个不一致性就是函数库包含许多函数的定义,但这些函数的原型声明却散布于多个头文件中。例如,在头文件、和中声明的函数通常是在同一个库libcS0中提供。如果你不信,可以使用nm工具程序列出函数库所包含的函数。在下面的小启发栏目里我将详细讨论这一点。 怎样在函数库中观察一个符号 如果在链接程序时遇到下面这种错误: ld:underfined symbol xdrreference +Error code 2 make:Fatal error:Command failed for target 7 pr09 7 它提示找不到

29、符号xdr reference的定义。这里有一种方法,可以通过它找到需要链接的 库。基本的想法是使用nln命令在usrlib的每个函数库中浏览所有的符号,从中寻找所丢失 的符号。在缺省情况下,链接器会在usrccslib和usrlib中查找,你也应该从这两个地方着手,如果在那里找不到就进一步扩展查找范围(如suropenwinlib)。 cdusrlib 99C专家编程foreach i(1ib?+)?echo Si?nm$i I grep xdrrefrence?endlibCSOlibnslSO2491】l 217028 1 196 l FUNC1ibposix4sO grepV UNDE

30、FI GLOB l o l 8xdrreference 这会在该目录中的所有函数库上运行“nn”程序,它显示函数库中已知的符号列表。通过grep设定需要搜索的符号,并过滤掉标记为“UNDEF”的符号(在该函数库中有引用,但并不是在此处定义)。结果显示xdrreference位于libnsl库。需要在编译器命令行的末尾力口上lnsl。 5与提取动态库中的符号相比,静态库中的符号提取的方法限制更严 最后,在动态链接和静态链接的链接语义上还存在一个额外的巨大区别,它经常会迷惑不够仔细的用户。archive(静态库)与共享对象(动态库)的动作不同。在动态链接中,所有的库符号进入输出文件的虚拟地址空间中,所有的符号对于链接在一起的所有文件都是可见的。相反,对于静态链接,在处理archive时,它只是在archive中查找载入器当时所知道

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

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