《程序员的自我修养》读书笔记5

Linux共享库的组织

一、共享库版本

1、共享库的兼容性

共享库的更新被分为两类:

       兼容更新:所有的更新只是在原有的共享库基础上添加一些内容,所有原有的接口都保持不变。

       不兼容更新:共享库更新改变了原有的接口,使用该共享库原有接口的程序可能不能运行或运行不正常。


导致C语言共享库ABI改变的行为主要有如下4个:

       导出函数的行为发生改变,也就是说该接口的功能发生改变;

       导出函数被删除;

       导出数据的接口发生变化,比如结构成员删除、顺序改变或其他引起结构体内存布局变化的行为。

       导出函数的接口发生变化,如函数返回值、参数被更改。


对linux来说,开发C++共享库为防止ABI不兼容,需要注意一下事项:

       不要在接口类中使用虚函数,万不得已要使用虚函数时,不要随意删除、添加或在子类中添加新的实现函数,这样会导致类的虚函数表结构发生变化;

      不要改变类中任何成员变量的位置和类型;

      不要删除非内嵌的public或protected成员函数;

      不要将非内嵌的成员函数改变成内嵌的成员函数;

      不要改变成员函数的访问权限;

      不要在接口中使用模板;

      不要改变接口的任何部分。


2、共享库版本命名

        Linux有一套规则来命名系统中的每一个共享库,它规定共享库的文件名规则必须如下:

        libname.so.x.y.z

        最前面使用前缀“lib”、中间是库的名字和后缀“.so”,最后面跟着三个数字组成的版本号。其中"x"表示主版本号(Major Version Number),"y"表示次版本号(Minor Version Number),“z”表示发布版本号(Release Version Number)。

        各个版本号的含义如下:

        主版本号:表示库的重大升级,不同主版本号的库之间是不保证兼容的,依赖于旧的主版本号的程序可能需要做代码改动,才能在新版的共享库中运行,或者系统必须保留旧版的共享库,使得那些依赖于旧版本共享库的程序能够正常运行。

       次版本号:表示可的增量升级,即增加一些新的接口符号,且保持原来的符号不变,在主版本号相同的情况下,高的次版本号的库向后兼容低的次版本号的库。

       发布版本号: 表示库的一些错误的修正、性能的改进等,并不添加任何新的接口,也不对接口进行任何更改。主版本号、次版本号相同,只有发布版本不同的共享库之间是完全兼容的。


3、SO_NAME

         由于不同主版本的共享库之间是完全不兼容的,因此可执行程序中必须记录它所依赖的共享库的名字和主版本号,以防止动态链接器在运行时意外将不兼容的库链接进来,从而造成程序的崩溃。

        为此,包括Solaris和Linux在内的心系统,普遍采用一种叫做SO_NAME的命名机制来记录共享库的依赖关系。

        每个共享库都有一个对应的“SO-NAME”,这个SO-NAME是共享库的文件名去掉次版本号和发布版本号,保留主版本号后的名字,比如一个共享库为:libfoo.so.2.6.1,那么它的SO-NAME即libfoo.so.2。

        此外,在linux系统中,系统会为每个共享库在它所在的目录创建一个指向SO-NAME的软链接(Symbol Link),名字跟SO-NAME相同,比如系统中存在一个共享库“/lib/libfoo.so.2.6.1”,那么Linux中共享库管理程序就会为它产生一个软链接“/lib/libfoo.so.2”并指向“/lib/libfoo.so.2.6.1”(如果没有比该次版本、发布版本更高的库)。软链接的作用:软链接会指向目录中主版本号相同、次版本号和发布版本最新的共享库,这样当所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的SO-NAME,指向该共享库主版本号相同的最新的库。

        Linux中有一个工具“ldconfig”,当系统中安装或更新一个共享库时,就需要运行该工具,它会遍历所有默认的共享库目录,然后更新所有软链接,使它们指向最新版的共享库,若新安装的库在系统中不存在旧版本,那么ldconfig会为其创建新的软链接。


二、符号版本

       当程序依赖的库的此版本较高,而系统中最新的此版本较低,这时是否要运行程序呢?不同的系统会有不同的选择,这个问题叫做“此版本交会问题(Minor-revision Rendezvous Problem)”,由于SO-NAME中仅有主版本号,所以SO-NAME机制无法解决此版本交会问题。

       一种更巧妙的来解决次版本号交会问题的方法是“基于符号的版本机制(Symbol Versioning)”。这个方案的基本思路是让每个导出和导入的符号都有一个相关联的版本号(实际做法类似于名称修饰的方法)。比如当我们将libfoo.so.1.2升级至1.3时,仍然保持libfoo.so.1这个SO-NAME,但是会给出1.3这个新版本中添加的那些全局符号打上一个标记,比如“VERS_1.3”,那么每次次版本号升级,共享库中的每个全局符号都有相应的标签。


三、共享库系统路径

       Linux遵守FHS(File Hierarchy Standard)的标准,这个标准规定了一个系统中系统文件应该如何存放。FHS规定,一个系统中主要存放共享库的位置:
       *     /lib,  主要存放系统最关键和基础的共享库,比如动态链接器、C语言运行库、数学库等;

       *     /usr/lib, 主要存放一些非系统运行时所需要的关键性的共享库,主要是一些开发时用到的共享库,一般不会被用户的程序或shell脚本直接用到;

       *     /usr/local/lib,  主要防止一些跟操作系统本身并不十分相关的库,主要是一些第三方的应用程序的库,比如python语言的解释器的库。

四、共享库查找过程

       任何一个动态链接模块所依赖的模块路径保存在“.dynamic”里面,有DT_NEED类型的项表示,动态链接器对于模块的查找有一定的规则:如果DT_NEED里面保存的是绝对路径,那么动态链接器按照该绝对路径去查找,如果DT_NEED保存的是相对路径,那么动态链接器会在/lib、/usr/lib 和由、/etc/ld.so.conf配置文件指定的目录中查找共享库。

       此外,Linux的ldconfig程序不仅为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME软链接,同时这个程序还能降这些SO-NAME收集起来,集中存放到/etc/ld.so.cache文件里面,并建立一个SO-NAME缓存。当动态链接器要查找共享库时,可以直接从/etc/ld.so.cache里面查找,这样可以大大加快共享库的查找过程。

       动态链接器先在/etc/ld.so.cache里面查找所需的共享库,如果没找到,会遍历/lib、/usr/lib这两个目录,如果还是没找到,就宣告失败。

五、环境变量

       LD_LIBRARY_PATH

       使用LD_LIBRARY_PATH环境变量,可以改变某个应用程序的共享库查找路径,而不会影响系统中其他程序。如果我们为某个进程设置了LD_LIBRARY_PATH,那么进程在启动时,动态链接器在查找共享库时,会首先查找有LD_LIBRARY_PATH指定的目录,这个环境变量可以很方便地让我们测试新的共享库或使用非标准的共享库。注意该环境变量不能随意修改,不应该被滥用。

       总体上,动态链接器查找共享库的顺序是:

       ** 由环境变量LD_LIBRARY_PATH指定的路径;

       ** 由路径缓存文件/etc/ld.so.cache指定的路径;

       ** 默认共享库目录,先/usr/lib,然后/lib。

     LD_PRELOAD

      该环境变量可以用来指定预先装载的一些共享库或者目标文件,在LD_PRELOAD里面指定的文件会在动态链接器按照固定的规则搜索共享库之前装载,它比LD_LIBRARY_PATH里面所指定的目录中的共享库还要优先。

        由于全局符号介入这个机制的存在,LD_PRELOAD里面指定的共享库或目标文件中的全局符号就会覆盖后面加载的同名全局符号,这使得我们可以很方便地做到改写标准C库中的某个或某几个函数而不影响其他函数,对于程序的调试或测试非常有用。注意,和LD_LIBRARY_PATH一样,正常情况下应该尽量避免使用LD_PRELOAD。

     LD_DEBUG

      该环境变量可以打开动态链接器的调试功能,当我们设置这个变量时,动态链接器会在运行时打印出各种有用的信息,对于我们开发和调试共享库有很大的帮助。

       LD_DEBUG可以设置成以下值:

        “bingings”显示动态链接器的符号绑定过程;

        “libs”显示共享库的查找过程;

        “versions”显示符号的版本依赖关系;

         “reloc”显示重定位过程;

         “symbols”显示符号表查找过程;

         “statistics”显示动态链接过程中的各种统计信息;

         “all”显示以上所有信息;

         “help”显示上面的各种可选值的帮助信息

六、共享库的创建和安装

      1、 共享库的创建

       使用GCC时,“-shared”参数指定输出结果是共享库类型; “-fPIC”表示使用地址无关代码技术; “-WI”参数可以指定需要传递给链接器的参数,比如使用“-WI, -soname,my_soname”时,GCC会将“-soname my_soname”传递给链接器,用来指定输出共享库的SO_NAME,如果不使用-soname来指定共享库的SO-NAME,那么该共享库默认就没有SO-NAME,ldconfig更新SO-NAME的软链接时,对该共享库也没有效果。

     2、清除符号信息

      对于发布版本来说,符号信息用处不大,并且使文件尺寸变大,使用“strip”工具可以清除共享库或可执行文件的所有符号和调试信息。也可以使用ld的“-s”和“-S”参数,使得链接器生成的输出文件时不产生符号信息,“-S”消除调试符号信息,而“-s”消除所有符号信息,在GCC中通过“-WI, -s”和“-WI, -S”给ld传递这两个参数。

     3、 共享库的安装

      当系统有root权限时,安装共享库的最简单办法是:将共享库复制到某个标准的共享库目录,如/lib、/usr/lib等,然后运行ldconfig即可。

      当系统没有root权限时,也是使用ldconfig,不过要指定共享库所在的目录:$ldconfig -n shared_library_directory,在编译程序时也需要指定共享库的位置,GCC提供了连个参数“-L”和“-I”,分别用于指定共享库搜索目录和共享库的路径。

     4、共享库构造和析构函数

       GCC提供了一种共享库构造函数,只要在函数声明时附加上“__attribute__((constructor))”的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在程序main函数之前执行,如果使用dlopen()打开共享库,共享库构造函数会在dlopen()返回之前被执行。

        在函数声明时附加上“__attribute__((destructor))”的属性,这种函数会在main()函数执行完毕后执行,如果共享库是运行时加载的,那么使用dlclose()来卸载共享库时,析构函数将会在dlclode()返回之前执行。

        声明构造和析构函数的格式如下:

        void __attribute__  ((constructor ))  init_function( void );

        void __attribute__  ((destructor ))   fint_function( void );

        当存在多个构造函数时,默认情况下,它们被执行的顺序没有规定,如果希望构造和析构函数能够按照一定的顺序执行,GCC提供了优先级参数,可以指定某个构造或析构的优先级,属性中优先级数字越小的函数会在优先级大的函数之前执行,析构函数相反。例如:

         void __attribute__  ((constructor(5)))  init_function1( void )

         void  __attribute__  ((constructor(10)))  init_function2( void )

         init_function1在init_function2之前执行。

5、共享库脚本

         通过共享库脚本,可以将几个现有的共享库通过一定的方式组合在一起,从用户的角度看就是一个新的共享库。这种共享库脚本叫做动态链接脚本,因为这个链接过程是动态完成的,也就是运行时完成的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值