Library学习

本文详细介绍了计算机科学中库的基本概念,包括其历史发展、静态链接库与动态链接库的区别、工作原理等内容。并通过实例讲解了Linux环境下库的具体应用。

                            Library初探
                                                     --Nowarter 2008-12-15
前言
   在做LFS的过程中由于要进行那个对LFS工具链的制作,出现了很多关于库的知识,所以经过搜索整理得出下面的文章,在文章的最后有组成文献而不是参考文献,因为基本上是将别人的东西整理了一下,加上了一点点自己的理解和对内容的组织安排,以便能更全面的了解库这个概念。
一、概念与历史(wiki)
   1、概念
   在计算机科学中,库是用于开发软件的子程序或类的集合。库和可执行文件的区别是,库不是独立程序,他们是向其他程序提供服务的代码和数据。这样就使得可以采用一种模块化的编程方式。
   库链接是指把一个或多个库包括到程序中,即可执行程序和库通过“链接”(links)这个动作使得双方可以互相引用(linking)(或也成为链接起来),一般完成“链接”这个动作的是一个叫做“链接器”(linker)的程序。
   通俗版解释:
   我们可以创建一种文件里面包含了很多函数和变量的目标代码,注意是目标代码,即是经过编译的产生,链接的时候只要把这个文件指示给链接程序就自动地从文件中查找符合要求的函数和变量进行链接,整个查找过程根本不需要我们操心。这个文件叫做 “库(Library)”,平时我们把编译好的目标代码存储到“库”里面,要用的时候链接程序帮我们从库里面找出来。
   2、库的简史
   最早的类似于库的编程概念是为了实现将数据定义和程序本身分离开而出现的。由JOVIAL在1959年提出的COMPOOL(通信池)概念被广泛关注, 尽管这个概念是借鉴于SAGE软件。COMPOOL这个概念的目的是用来使数据可以用一种集中化的方式来表示,进而使得许多不同的程序可以共享系统的数据,从而达到符合计算机科学中的问题分解和信息隐藏这一原则。
   在1959年COBOL也包含了一些简单的能力(或特性)用来应付库,但是在Jean Sammet回顾往事的时候把他们(COBOL)描述成“不完整的库”。
   另一个对现代库概念作出贡献的是FORTRAN的子程序的概念的产生。FORTRAN的子程序能够独立编译而不影响各自的独立性,但是FORTRAN的编译器缺少一个链接器,所以在子程序之间进行类型那个检查是不可能的。
   最后,对于计算机科学很有影响的Simula 67不能不提。Simula是第一个面向对象的编程语言,他的类的概念和现在用在Java,C++和C#中的类的概念几乎一致。Simula的类的概念也是Ada中包和Modula-2中模块的概念的起源。在1965年Simula被创建之初,它的类就能被包含在库文件中,并且在编译时被加载了。
   注:上述是wiki中找到的关于库的概念,翻译的比较烂,但是作为一个了解应该是够了。从上面的简史中我看到了库产生的原因和不断发展变化的过程,从而也对库的分类、优缺点和工作原理等有了一个历史背景的印象。
二、分类与区别
   1、分类
   关于分类想事先说明一点,看网上很多介绍文章根本不把库的分类和库的链接方式的分类区分开来,虽然大多数时候这两个概念我们可以自动的进行区分,不过在有些时候还是会给人造成误解,比如,下面的分类中有人将加载时链接描述为静态绑定,这样描述从他们的工作原理上来说都无可厚非,不过多了静态绑定中静态两个字,好像总在时不时提醒我们链接的是静态链接库,而其实静态绑定是动态链接的一种,链接的当然是动态链接库。
     (1)、库的链接方式的分类
     库有两种链接形式:静态链接和动态链接。动态链接又根据载入程序何时确定动态代码的逻辑地址分为运行时链接(runtime link)(或动态绑定dynamic binding)和加载时链接(loadtime link)(或静态绑定static binding)。
     (2)、库的分类  
     相应的,静态链接的库叫做静态链接库,动态链接的叫做动态库(不论是运行时链接还是加载时链接)。
   2、区别
     (1)静态链接和动态链接的优缺点和却别
     静态链接是由链接器在链接时将库的内容加入到可执行程序中的做法。链接器是一个独立程序,将一个或多个库或目标文件(先前由编译器或汇编器生成)链接到一块生成可执行程序。
     早期库的组织形式相对简单,里面的目标代码只能够进行静态链接,所以我们称为“静态链接库”,静态链接库的结构比较简单,其实就是把原来的目标代码放在一起,链接程序根据每一份目标代码的符号表查找相应的符号(函数和变量的名字),找到的话就把该函数里面需要定位的进行定位,然后将整块函数代码放进可执行文件里,若是找不到需要的函数就报错退出。
     静态链接的主要缺点:
     #1链接后产生的可执行文件包含了所有需要调用的函数的代码,因此占用磁盘空间较大。
     #2如果有多个(调用相同库函数的)进程在内存中同时运行,内存中就存有多份相同的库函数代码,因此占用内存空间较多。
     #3由于以上原因在载入静态链接生成的可执行文件时,系统整体的运行时间也会加大
     静态链接的主要有点:
     #1静态链接产生的可执行程序自称一体,所有需要用到的目标文件和库都包含在可执行文件中,不受本地库的影响。
 
     动态链接就是为了解决这些问题而诞生的技术,顾名思义,动态链接的意思就是在程序装载内存的时候才真正的把库函数代码链接进行确定它们的地址。
     动态链接的有点是就算有几个程序同时运行,内存也只存在一份函数代码。内存中的动态代码只有一份副本,但动态库的数据仍然可能有多份副本,因为每一个链接到动态的进程都可能会修改库的数据,每当出现这种情况的时候,操作系统就复制出一份数据副本,然后修改进程的地址空间映射,使它指向新的数据副本,于是进程最后修改的只是属于自己的那份数据。
    由于动态链接生成的库文件是预先编译链接好的,存储在计算机的硬盘上。而不是链接进可执行程序中的,所以动态链接的最大缺点就是可执行程序依赖存储在硬盘上的库文件才能正确执行。如果库文件被删除了,移动了,重命名了或者被替换为不兼容的版本了或是移植到另一个系统中了,那么可执行程序就可能工作不正常。这就是常说的DLL-hell。
   
    另外静态链接库基本上就是一个文档文件(archive)文件,而动态链接库的代码必须满足这样一种条件:能够被加载到不同进程的不同地址,所以代码要经过特别的编译处理,我们把这种经过特别处理的代码叫做“位置无关代码(Position independed Code .PIC)”.

    (2)运行时链接(runtime link)(或动态绑定dynamic binding)和加载时链接(loadtime link)(或静态绑定static binding)的区别
    #1 静态绑定(static binding)
    使用静态绑定的程序一开始载入内存的时候,载入程序就会把程序所有调用到的动态代码的地址算出确定下来,这种方式使程序刚运行的初始化时间较长,不过旦完成动态装载,程序的运行速度就很快。
    #2动态绑定(dynamic binding)
    使用这种方式的程序并不在一开始就完成动态链接,而是直到真正调用动态库代码时,载入程序才计算(被调用的那部分)动态代码的逻辑地址,然后等到某个时候,程序又需要调用另外某块动态代码时,载入程序又去计算这部分代码的逻辑地址,所以,这种方式使程序初始化时间较短,但运行期间的性能比不上静态绑定的程序。
三、工作原理
    1、静态链接的工作原理
    库的静态链接的工作原理比较简单,就是当建立一个可执行程序时,链接器将预先制作好的且可执行程序需要的静态库和编译程序编译出来的目标文件链接成最后的可执行文件,在这个过程中,静态库的地址,或静态库中被可执行程序需要用到的函数的地址(这里地址泛指一切能找到它们的标志)是以相对地址或符号链接的形式“硬”编码进可执行程序里面的。用更专业的话说是在静态链接系统中,生成的代码会持有对某个函数的引用。链接器使用加载该函数的真实地址去替换这个引用,以便生成的二进制代码在适当的位置会有正确的地址。
    当可执行程序要运行时,就要将所有的程序全部装入内存中才能找到需要的库,因为只有程序全部装入内存之后,相对地址或符号链接的地址才能确定下来。(或更细微的说就是当程序的代码都装入到分配给这个程序的段之后,才能确定段首地址和库相对于段首的相对地址)。
    2、动态链接的工作原理
    这个概念理解说起来也很简单。拥有一个程序库;然后共享这个程序库。但是,当您的程序尝试调用动态链接库中的某个函(比如linux系统中共享库的C函数库的printf()函数)时,也就是说实际操作的时候,具体发生的事情却稍微有点复杂。   
    动态链接在可执行文件装载时(loadtime)或运行时(runtime)(就是上面说的两种动态链接方式),由操作系统的装载程序(loader)加载库
    大多数操作系统将解析外部引用(比如库)作为加载过程的一部分。在这些系统上,可执行文件包含一个叫做import directory的表,该表的每一项包含一个库的名字。根据表中记录的名字,装载程序在硬盘上搜索需要的库,然后将其加载到内存中预先不确定的位置,之后根据加载库后确定的库的地址更新可执行程序。可执行程序根据更新后的库信息调用库中的函数或引用库中的数据。这种类型的动态加载称为装载时加载 ,被包括Windows和Linux的大多数系统采用。
    其他操作系统可能在运行时解析引用。在这些系统上,可执行程序调用操作系统API,将库的名字,函数在库中的编号和函数参数一同传递。操作系统负责立即解析然后代表应用调用合适的函数。这种动态链接叫做运行时链接 。因为每个调用都会有系统开销,运行时链接要慢得多,对应用的性能有负面影响。现代操作系统已经很少使用运行时链接。
    而不论是loadtime链接还是runtime链接装载程序(loader)都是不可少的,loader在加载应用软件时要完成的最复杂的工作之一就是加载时链接。loader必须为每个被链接的库做相当多的工作,所以大部分loader都是不积极的。只有在库中的函数被调用时,它们才实际做一些工作。 实现此过程的是一个被称为过程链接表(Procedure Linkage Table)(PLT)的数据块,它是程序中的一个表,列出了程序所调用的每一个函数。当程序开始运行时,PLT包含每个函数的代码,以便查询运行期链接器,从而获得已加载某个函数的地址。然后它会在表中填入这个条目并跳转到那个已加载函数。当每个函数被调用时,它的PLT中的条目就会被简化为一个到那个已加载函数的直接跳转。

四、linux中静态链接库与动态链接库的区别及动态库的创建
    见《2008-12-15-转linux静态链接库与动态链接库的区别及动态库的创建》(http://nowarter.blog.sohu.com/106549648.html)
五、linux下动态链接库的搜索路径
    见《2008-12-15-转Linux动态库(.so)搜索路径》(http://nowarter.blog.sohu.com/106550235.html)
六、修改动态链接器的搜索路径
    当链接某个程序时,在运行期您可以指定另外的搜索路径。在gcc中,其语法是-Wl,-R/path。如果程序已经被链接,那么您也可以设置环境变量LD_LIBRARY_PATH来改变这一行为。通常只是在应用程序需要搜索的路径不是系统级默认路径的一部分时才需要这样做,对大部分 Linux系统来说,这种情况很少见。
    设置程序库路径可以为两个应用程序需要同一程序库的不兼容版本的这种罕见情况提供一个迂回解决方案。可以使用包装器脚本使某一应用程序在使用特殊版本程序库的目录中进行搜索。这称不上是一个完美的解决方案,但是在某些情况下,这是您能采用的最佳方法。
    如果出于不得已的原因需要为很多程序添加某个路径,那么也可以修改系统的默认搜索路径。通过 /etc/ld.so.conf 控制动态链接器,该文件包含默认搜索路径的列表。对LD_LIBRARY_PATH中指定的任何路径的搜索都要先于ld.so.conf中列出的路径,所以用户可以覆盖这些设置。
    大部分用户没有理由修改系统默认程序库搜索路径;通常环境变量更适用于修改搜索路径,比如连接某个工具包中的程序库,或者使用某个程序库的较新版本的测试程序。
   使用 ldd
   ldd 是调试共享程序库问题的一个实用工具。其名称来自 list dynamic dependencies。这个程序会查看某个给定的可执行程序或者共享程序库,并指出它需要加载哪些共享程序库以及要使用哪些版本。输出类似如下:
   清单 1. /bin/sh 的依赖   
$ ldd /bin/sh
        linux-gate.so.1 =>  (0xffffe000)
        libreadline.so.4 => /lib/libreadline.so.4 (0x40036000)
        libhistory.so.4 => /lib/libhistory.so.4 (0x40062000)
        libncurses.so.5 => /lib/libncurses.so.5 (0x40069000)
        libdl.so.2 => /lib/libdl.so.2 (0x400af000)
        libc.so.6 => /lib/tls/libc.so.6 (0x400b2000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
    看到一个“简单的”的程序使用了这么多个程序库,可能会有些令人惊讶。或许是 libhistory 需要 libncurses。为了查明真相,我们只需要运行另一个 ldd 命令:
   清单 2. libhistory 的依赖
$ ldd /lib/libhistory.so.4
        linux-gate.so.1 =>  (0xffffe000)
        libncurses.so.5 => /lib/libncurses.so.5 (0x40026000)
        libc.so.6 => /lib/tls/libc.so.6 (0x4006b000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
    在某些情况下,可能需要为应用程序指定另外的程序库路径。
    清单 3. 运行 dll 查找不在搜索路径中的 程序库的结果   
$ ldd /opt/mozilla/lib/mozilla-bin
  linux-gate.so.1 =>  (0xffffe000)
  libmozjs.so => not found
  libplds4.so => not found
  libplc4.so => not found
  libnspr4.so => not found
  libpthread.so.0 => /lib/tls/libpthread.so.0 (0x40037000)
    为什么找不到这些程序库?因为它们不在常见的程序库搜索路径中。实际上,它们在 /opt/mozilla/lib 中,所以,解决方案之一是将这个目录添加到 LD_LIBRARY_PATH 中。
    另一个选项是将路径设置为 .,并在这个目录下运行 ldd,尽管这样做更危险 —— 将当前目录添加到程序库路径中与将它添加到可执行程序路径中一样有着潜在的危险。
    在这种情况下,将这些程序库所在的目录添加到系统级搜索路径中显然不是一个好办法。

七、linux的Library的版本号
    当出现新版本的程序库时会怎样?特别是新版本改变了某个给定函数的调用次序时,又会怎样?
    版本号可以解决这个问题 —— 共享的程序库将拥有一个版本号。当一个程序链接到某个程序库时,程序中会存储一个它计划支持的版本号。如果更改程序库,那么版本号就会不匹配,程序也就不会被链接到较新版本的程序库。
    不过,动态链接的可能优势之一在于修正缺陷。如果可以修正程序库中的缺陷,而且不必重新编译上千个程序,就可以利用这一修正功能,这将是非常令人愉快的。有时,需要链接到某个较新的版本。 不幸的是,这会导致在某些情况下,您希望链接到较新的版本,而在另外一些情况下,您宁愿坚持使用较老的版本。不过,有一个解决方案 —— 使用两类版本号:
    主版本号表明程序库版本之间的潜在不兼容性。
    次要版本号表明只是修正了缺陷。
    这样,在大部分情形下,加载具有相同主版本号和更高次要版本号的程序库是安全的;而加载主版本号更高的程序是不安全的行为。为了让用户(和程序员)不必追踪程序库版本号和更新,系统提供了大量的符号链接。通常,其模式是:
      libexample.so
      将是一个指向
      libexample.so.N 的链接,其中 N 是在系统中可以找到的最高的 主 版本号。
      对受支持的每一个主版本号而言,
      libexample.so.N
      将是一个指向
      libexample.so.N.M 的链接,其中 M 是最高的 次要 版本号。
      这样,如果为链接器指定了 -lexample,那么它会去寻找 libexample.so,这是一个符号链接,指向某个指向最新版本的符号链接。另一方面,当加载某个现有程序时,它将尝试去加载 libexample.so.N,其中 N 是它先前链接的版本。各得其所!

八、 加载库的步骤
   在传统的静态程序库中,生成的代码通常封装在一个程序库文件中(其名称以 .a 结尾),然后传递给链接器。在动态程序库中,程序库文件的名称通常以 .so 结尾。文件结构稍有不同。
   常规的静态程序库的格式是 ar 工具(一个非常简单的存档程序,类似于 tar,但是更简单)所创建的那种格式。相反,共享程序库通常以更复杂的文件格式存储。
   在现代 Linux 系统中,这一格式通常是 ELF 二进制格式(可执行与可链接格式(Executable and Linkable Format))。在 ELF 中,每个文件的组成包括:一个 ELF 头,随后是零或者一些段(segments),以及零或者一些区段(sections)。 段 中包含文件的运行时执行所需要的信息,而 区段 中包含用于链接和重定位的重要数据。整个文件中的每个字节每次只能由一个区段使用,不过可以存在不被任何区段所包含的孤立字节。通常,在 UNIX 可执行文件中,一个或多个区段会封装在一个段内。
   ELF 格式中包含用于应用程序和程序库的规范。但程序库格式要复杂得多,不仅仅是对象模块的简单存档。
   链接器将所有对符号的引用进行分类,标识出它们是在哪个程序库中找到的。将静态程序库的符号添加到最终的可执行文件中;然后将共享程序库的符号放入 PLT 中,最后创建对 FLT 的引用。在完成这些任务之后,生成的可执行文件会拥有一个列表,该列表列出了计划从运行期将加载的程序库中找出的那些符号。
   在运行期间,应用程序将加载动态链接器。实际上,动态链接器本身使用与共享程序库相同种类的版本号。例如,在 SUSE Linux 9.1 中, /lib/ld-linux.so.2 文件是一个指向 /lib/ld-linux.so.2.3.3 的符号链接。另一方面,寻找 /lib/ld-linux.so.1 的程序不会尝试使用新的版本。
   然后动态链接器开始进行所有有趣的工作。它会查明某个程序先前链接到了哪些程序库(以及哪个版本),然后加载它们。加载程序库的步骤包括:
     找到程序库(它可能在系统中若干个目录中的任意一个目录中)。
     将程序库映射到程序的地址空间。 
     分配程序库可能需要的由零填充的内存块。
     添加程序库的符号表。
   调试这一过程可能会比较困难。您可能会遇到多种问题。例如,如果动态链接器不能找到某个给定的程序库,那么它将停止加载程序。如果它找到了所有需要的程序库,但却无法找到某个符号,那么它也可能会因此而停止加载操作(但是可能直到真正尝试去引用那个符号时才会发生这种情形) —— 这是一种很少见的情况,因为通常如果不存在某个符号,那么在初始化链接的时候就会被警告。


组成文献:
 1、csdn 风生水起’s blog  静态链接库和动态链接库(转)
 2、剖析共享程序库  PeterSeebach  IBM developerwork
 3、wiki百科
 4、csdn windone's blog  http://www.cnitblog.com/windone0109/archive/2008/04/23/42653.html
 5、csdn blog lwhsyit的专栏 http://blog.youkuaiyun.com/lwhsyit/archive/2008/09/01/2860964.aspx

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值