您的位置:首页 > 其它

【转】编译,链接与库的使用(2)

2014-04-16 18:25 197 查看

静态库和动态库的混合编译

目前我们多数的库都是以静态库的方式提供,但是现在有许多地方出于运维和升级的考虑使用了许多动态链接库,这样不可避免的出现了大量的静态库与动态库的混合使用,经常会出现一些奇怪的错误,使用的时候需要有所关注
对于一般情况下,只要静态库与共享库之间没有依赖关系,没有使用全局变量(包括static变量),不会出现太多的问题,下面以出现的问题作例子来说明使用的注意事项。

baidugz与zlib的冲突

具体的说明可以参看wiki LibBaidugz baidugz 是百度早期用来解压压缩网页,可以自动识别多数的网页压缩格式具有一定的容错性,但是由于baidugz是早期zlib版本直接修改而来,出现与系统中版本不一致的时候就可能导致问题。
在 /usr/lib64/ 下可以看到 libz.so, 我们在直接使用系统zlib的时候多是在链接的时候加上 -lz 就可以了。程序在运行的时候会直接到系统的目录下去寻找libz.so,并且在运行期被载入。
早 期的zlib代码中有一部分函数和变量,虽然没有通过zlib.h对外公开,但是还是采用了extern的方式被其他的.c文件使用(这里涉及到一个问题 就是一个源码中的变量或接口要被同一个库中其它地方使用,只能被extern,但extern 后就意味着可以被其它任意使用这个库的程序看到和使用, 无论是否在对外接口中声明), 还有个别接口可以使用static但没有使用static。 这部分对内公开(实际上对外也公开了)的接口, 在baidugz的修改过程中没有被修改,在后来升级64位版本的时候,由于系统中的zlib与baidugz使用的zlib相差过大,zlib在本身的 升级过程中也没有过多的考虑这个问题(它假设不会有并存的情况), 导致在链接的过程出现错误.
在编写动态库的过程中,可以static的函数即使没有暴露在头文件也需要尽量static,避免和外界冲突。那种没有对外公开接口就无所谓加不加static的观点是存在一定风险的.
小提示
有 些程序使用 using namespace {} 这样的匿名命名空间来规避冲突的问题,从编译器角度而言,在代码中使用确实不会产生冲突。 不过采用dlopen的方式却还是可以通过强制获取符号的方式运行在共享库中使用using namespace {}包含起来的函数,但static的函数是不能被dlopen方式强制获取的。

地址无关代码

在64位下编译动态库的时候,经常会遇到下面的错误/usr/bin/ld: /tmp/ccQ1dkqh.o: relocation R_X86_64_32 against `a local symbol' can not be used when making a shared object; recompile with -fPIC提示说需要-fPIC编译,然后在链接动态库的地方加上-fPIC的参数编译结果还是报错,需要把共享库所用到的所有静态库都采用-fPIC编译一边才可以成功的在64位环境下编译出动态库。
这里的-fPIC指的是地址无关代码
这 里首先先说明一下装载时重定位的问题,一个程序如果没有用到任何动态库,那么由于已经知道了所有的代码,那么装载器在把程序载入内存的过程中就可以直接安 装静态库在链接的时候定好的代码段位置直接加载进内存中的对应位置就可以了。但是在面对动态的库的时候 ,这种方式就不行了。假设需要载入共享库A,但是在编译链接的时候使用的共享库和最后运行的不一定是同一个库,在编译期就没办法知道具体的库长度,在链接 的时候就没办法确定它或者其他动态库的具体位置。另一个方面动态库中也会用到一些全局的符号,这些符号可能是来自其他的动态库,这在编译器是没办法假设的 (如果可以假设那就全是静态库了)
基于上面的原因,就要求在载入动态库的时候对于使用到的符号地址实现重定位。在实现上在编译链接的时候不做重定位操作,地址都采用相对地址,一但到了需要载入的时候,根据相对地址的偏移计算出最后的绝对地址载入内存中。
但是这种采用装载时重定位的方式存在一个问题就是相同的库代码(不包括数据部分)不能在多个进程间共享(每个代码都放到了它自己的进程空间中),这个失去了动态库节省内存的优势。
为了解决这个问题,ELF中的做法是在数据段中建立一个指向那些需要被使用(内部的位置无关简单采用相对地址访问就可以实现)的地址列表(也被称为全局偏移表,Global offset table, GOT). 可以通过GOT相对应的位置进行间接引用.
对 于我们的32位环境来说, 编译时是否加上-fPIC, 都不会对链接产生影响, 只是一份代码的在内存中有几个副本的问题(而且对于静态库而言结果都是一样的).但在64位的环境下装载时重定位的方式存在一个问题就是在我们的64位环 境下用来进行位置偏移定位的cpu指令只支持32位的偏移, 但实际中位置的偏移是完全可能超过64位的,所以在这种情况下编译器要求用户必须采用fPIC的方式进行编译的程序才可以在共享库中使用
从理论上来说-fPIC由于多一次内存取址的调用,在性能上会有所损失.不过从目前的一些测试中还无法明显的看出加上-fPIC后对库的性能有多大的损失,这个可能和我们现在使用的机器缓存以及大量寄存器的存在相关.
小提示
-fPIC与-fpic 上面的介绍可以看到,gcc要使用地址无关代码加上-fPIC即可,但是在gcc的手册中我们可以看到一个-fpic(区别在一个大写一个小写)的参数, 从功能上来说它们都是一样的。-fpic在一些特定的环境中(包括硬件环境)可以有针对性的进行优化,产生更小更快的代码, 但是由于受到平台的限制,像我们的编译环境,开发环境,运行环境都不完全统一的情况下面使用fpic有一定未知的风险,所有决大多数情况下我们使用 -fPIC来产生地址无关代码。
共享内存效率
共享内存在只读的情况下性能和读普通内存是一样的(如果不算第一载入的消耗),而且由于是多个进程共享对cpu cache还显的相对友好。 可以参见mmap性能

同时存在静态库和动态库

前 面提到编译动态库的时候有提到编译动态库可以像编译静态库那样采用-Lpath -lxx的方式进行, 但这里存在一个问题,如果在path目录下既有动态库又有静态库的时候的行为又是什么样地? 事实上在这种情下, 链接器优先选择采用动态库的方式进行编译.比如在同一目录下存在 libx.a 和 libx.so, 那么在链接的时候会优先选择libx.so进行链接. 这也是为什么在com组维护的第三方库(third, third-64)中绝大多数库的产出物中只有.a的存在, 主要就是为了避免在默认情况下使用到.so的库, 导致在上线的时候出现麻烦(特别是一些系统中存在,但又与我们需要使用的版本有出入的库).
为了能够控制动态库和静态库的编译, 有下面的几种方式

直接使用要编译的库

在前面也提到了在编译静态库的时候有三种方式
目标文件.o 直接使用
静态库文件.a 直接编译
采用 -L -l方式进行编译
编译的时候如果不采用-Lpath -lxx的方式进行编译, 而且直接写上 path/libx.a 或者 path/libx.so 进行编译,那么在链接的时候就是使用我们指定的 .a 或者 .so进行编译不会出现 所谓的动态库优先还是静态库优先的问题. 但这个方案需要知道编译库的路径,一些情况下并不适合使用。

--static参数

在gcc的编译的时候加上--static参数, 这样在编译的时候就会优先选择静态库进行编译,而不是按照默认的情况选择动态库进行编译.
不过使用--static参数会带来另外的问题,不推荐使用,主要会带来下面的问题
如果只有动态库,而不存在同名的静态库,链接的时候也不会报错,但在运行的时候可能会出现错误 /lib/ld64.so.1: bad ELF interpreter:
由于我们程序本身在运行的需要系统中一些库的支持,包括libc, libm, phtread等库,在采用--static编译方式之后,链接的就是这些库的静态编译版本(glibc还是提供了静态编译的版本),我们等于使用的是编 译机上的库,但是我们的运行环境可能和编译机有所不同,glibc这些动态库的存在本身的目的就是为了能让在一台机器上编译好的库能够比较方便的移到另外 的机器上,程序本身只需要关注接口,至于从接口到底层的部分由每台机器上的.so来处理.不过这个问题也不是那么绝对,在一些特殊情况下(比如 glibc, gcc存在大版本差异的时候,主要是gcc2到gcc3有些地方没有做好,abi不兼容的问题比较突出,真遇到这些情况其实需要换编译器了)  --static编译反倒可以正常的运行.但是还是不推荐使用, 这些是可以采用其它方法规范在后面的第6点中有说明.另外就是glibc --stat
14dbb
ic编译可能会产生下面的warning:
warning: Using 'getservbyport_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
这个主要原因是由于getservbyport_r 这样的接口还是需要动态库的支持才可以运行,许多glibc的函数都存在这样的问题, 特别是网络编程的接口中是很常见的.
对一些第三方工具不友好,类似valgrind检查内存泄露为了不在一些特殊的情况下误报(最典型的就是strlen可以参考valgrind的 wikiValgrind运行的程序不能够使用-static来进行链接中的case3), 它需要用动态库的方式替换glibc中的函数,如果静态编译那么valgrind就无法替换这些函数,产生误报甚至无法报错. tcmalloc在这种情况下也不能支持.
我们目前64位环境中使用的pthread库,如果是使用的是动态库那么采用的是ntpl库,如果是静态库采用的linuxthread库,使用--static 会导致性能下降(可以参考32/64位性能调研)
--static之后会导致代码大小变大,对cpu代码cache不友好,浪费内存空间,不过对于小代码问题也不大.
早期使用--static的一个原因是需要使用一些第三方面库, 但是最后运行的线上机器和编译机的库存在一些行为不一致问题或者不兼容问题, 需要静态编译进去.对于这个问题,com的建议是采用third和third-64下的库, 通过我们自己编译的库来控制, com目前维护的第三方库相关资料见第三方库, 如果有新的第三方库需求可以联系com组. 采用我们自己编译的第三方库可以避免由于库底层和运行机器环境不兼容造成问题,同时也可以避免使用--static参数. 同时第三方库的编译参数也可以由我们根据实际情况进行定制, 只需要做到编译机编译的结果可以在线上机器(或者测试机)上正常运行.
早 期有一些测试表明在32位的环境下, 采用--static全部使用静态库可以使程序性能有1%~3%的提高,这个主要原因在于-fPIC产生的二次寻址问题导致(glibc那些库都是采用了 -fPIC的方式进行编译).但对于我们的很多程序本身调用glibc的地方就不是很多(如果这里会产生瓶颈,设法减少这一次函数调用效果是会更好的), 加上我们机器缓存大,代码小,这种的性能提高实在是很有限了. 在运行了多个程序的机器上反倒可能由于cache不友好有反效果
特别的是在ullib 3.1.27以后版本引入了comlog 输出网络日志,由于网络库的存在 会报出这样链接错误, 建议去除
小提示:
编译机差异
我们目前的环境的下, 编译机,开发机,线上机器都存在一些不同的地方 32位环境机器差异比较大, 开发机和编译机上是2.96 gcc, glibc版本 2.2.5 .线上32位机器现在已经很少了, 但我们经常把32位程序放到64位机器上运行, 64位机器下的32位环境中的glibc是2.2.93版本,其它各种库的版本也存在差异,使用动态库编译出问题的概率较大,特别使用了某些库的特殊接口
64位机器环境相对比较接近, 开发机上是gcc.3.4.5, 编译机是3.4.4 相差一个小版本号. 线上机器3.4.5和3.4.4都存在,内核版本也有略微不同

虽然编译机和线上机器环境存在略微的差异,不过这些差距并不大,目前的还没有因为这个原因造成问题. 对原来使用--static编译的老程序的建议 首先要确定使用--static编译的原因是什么, 根据不同的原因采用不同的方案
性能原因, 即使是经过测试认为--static可以提升性能, 提升的也是非常有限的,不建议为了这一点性能而使用--static, 另外注意就是原来在32位下有性能优势当升级到64位机器上可能就没有优势了
使用的第三方库比如-lz等, 如果出现不加--static, 会出现在线上机器不能运行. 这里建议使用由我们自己维护的第三方库第三方库,像基础库那样使用. 如果有不同版本和新库的需求请联系com组
存在动态库和静态库的混合应用,推荐使用下一章节的 -dn, -dy参数

链接参数控制

链接器中提供了-dn -dy 参数来控制使用的是动态库还是静态库,-dn表示后面使用的是静态库,-dy表示使用的是动态库
例:g++ -Lpath -Wl,-dn -lx -Wl,-dy -lpthread这样如果在path路径下有libx.so和libx.a这个时候只会用到 libx.a.
注意在最后的地方如果没有-Wl,-dy 让后面的库都使用动态库,可能会报出 "cannot find -lgcc_s" 的错误,这是由于glibc的.a库和.so库名字不同,--static会自动处理,但是 -Wl,-dy却不会去识别这个问题.
小提示
如果使用--static, 由于-dy的使用导致后面的库都是共享库(dy强制屏蔽了静态库),这个时候编译出来的程序和只有动态库的情况下强制使用--static编译一样都会报错

运行报错 "undefined reference to `xxx()' "

对 于动态链接库,实际的符号定位是在运行期进行的.在编译.so的时候,如果没有把它需要的库和他一起进行联编,比如libx.so 需要使用uldict, 但是忘记在编译libx.so的时候加上-luldict的话,在编译libx.so的时候不会报错,因为这个时候libx.so被认为是一个库,它里面 存在一些不知道具体实现的符号是合法的,是可以在运行期指定或者编译另外的二进制程序的时候指定.
如果是采用 g++ -Lpath -lx 的方式进行编译,链接器会发现所需要的uldict的符号表找不到从而报错,但是如果是程序采用dlopen的方式载入,由于是运行期,这个程序在这个地 方就直接运行报错了. 另外还有一种情况就是一个对外的接口在动态库中已经声明定义了,但是忘记实现了,这个时候也会产生类似的错误.
如果在运行期报出这样的错误,就要注意是否是由于某些库没有链接进来或者某些接口没有实现的原因产生

日志库问题

其实不只是日志库存在这样的问题,其他需要同时被多个动态库以及主程序同时使用的函数其实都存在这样的问题.这里主要以日志库的问题为例来说明这些问题.
有 一个程序,它通过dlopen的方式调用了一个.so文件. 在这个主程序中和.so中都使用了日志库,主程序中使用ul_openlog打开了日志, 在.so中没有用ul_openlog打开日志.这个时候发现,主程序中的日志正常输出,但.so中的日志却直接输出到了标准出错.
这个问 题的原因在前面的其实已经提到了,在默认情况下主程序中使用的接口对于.so是不可见的,.so所在的代码空间与主程序的代码空间是隔离的,这个时 候.so调用的ul_writelog其实是没有经过ul_openlog的那块代码空间,由于ul_log库使用了一些static变量(如果是带 comlog的ul_openlog那还有全局符号),只有在ul_writelog, ul_openlog都是在同一块空间上的时候才会起作用.
这个问题的一个最简单的解决方案是
在主程序的链接的时候加入-rdynamic,仍然链接libullib.a库
编译动态链接库时,不加链接libullib.a库
其实动态库这里是否链了libullib.a已经不重要了,在有-rdynamic的情况下,.so中如果有与主程序同名的函数那么会优先调用主程序中的函数, 动态库不链接libullib.a倒是可以省点空间
但是这种方式在某些情况还是不能完全解决问题
假 设有A.so, B.so,主程序main, 在A.so中调用了ul_openlog, B.so中没有调用ul_openlog, 但调用了ul_writelog. 在主程序中没有调用ul_log中的任何接口和使用任何变量.这种情况下即时使用了-rdynamic还是会导致在 A.so中正常输出日志,但在B.so中却把日志输出到标准出错.
这个问题的主要原因在于,gcc在链接的时候是以.o为单位的,如果一 个.o中的符号没有被外部所使用,那么在链接的时候就不会把这个.o中的符号给链接进行.so或者二进制程序中.在上面的问题中主程序里面没有调用到日志 库中的任何符号,所以在链接的时候就不会把ullib中的ul_openglog和ul_writelog给链接进行主程序中,这个时候即使有 -rdynamic也是做不到让.so中的动态链接库都使用.
这个问题一般有下面几种方案:
载入A.so的时候使用RTLD_GLOBAL参数,把A.so中的所有的符号都变成对外可见,这样A.so和B.so的ul_writelog都在一块代码空间中了
编译主程序的时候链接ullib的地方由-lullib改为 -Wl,--whole-archive -lullib -Wl,--no-whole-archive, 同时加上-rdynamic. -Wl, --whole-archive是链接参数,它表示把ullib的中所有的符号都链接进主程序中,而不管主程序是否调用了ullib中的符 号.-Wl,--no-whole-archive表示后面的链接取消--whole-archive的行为,毕竟其他的库没有必要采用这种方式全部链接 进来.
在主程序中随便调一下ul_log中的符号,比如可以先随便ul_openlog一下,然后ul_closelog, 后面再进行动态库调用.
把libullib.a用 ar x 命令还原成多个.o文件,采用直接链接的方式使用ul_log.o.
上 面的几个问题的产生主要还在于静态链接和动态链接混用,而两种链接方式又存在不一样的地方.事实上如果我们把ullib库 采用动态链接库的方式编译成 libullib.so, 采用前面的方式在编译期链接libullib.so,并且设置LD_LIBRARY_PATH, 上面的2个问题都不会存在. 编译期使用的.so,是全局可见的,不需要-rdynamic也可以被dlopen的动态库所用到,由于是动态链接库,所以 包含了所有的符号,不会像静态库那样只包含了所用到的.o中的符号. 事实上这也是多数第三方程序的解决方案
主程序中使用-rdynamic会对后续的升级造成一些麻烦:
加上-rdynamic后,像日志库这样的基础如果需要升级, 那么就必须要升级主程序, 使用的.so无论如何升级所用到的ul_writelog都是主程序中的.
主程序中除了日志库,还会有其他库或者函数的存在, 这些函数如果不是static的就有可能与 dlopen打开的so中的函数混到一起,造成困惑
如果.so中需要打印它自己的日志, 那样需要comlog本身功能的支持才可以实现,而不能简单的使用ul_writelog来实现
但是如果主程序中没有使用-rdynamic,那么又有下面的这些麻烦 dlopen打开的动态库日志是打印自己的, 不能和主程序统一在一起.
如果.so的程序和主程序open的是同一个日志,这相当于多进程打日志, 那必须要comlog的支持,ullog本身不支持多进程打同一个日志.
如果主程序中用dlopen+RTLD_GLOBAL的打开了某个.so 日志的问题就可能影响到其他的.so中的调用.
同时对于一些老程序, 升级前后-rdynamic 可能也会产生影响,比如两次ul_openlog这里对于类似日志库这种需要全局状态变量支持的库提出另外方案
1. 编译一个专门的.so, 这个.so中包括了其它.so中所需要的所有和全局量相关的接口 2. 主程序不使用-rdynamic编译, 但打开上面的.so的时候,采用RTLD_GLOBAL方式,并且是第一个打开 3. 除了打开第一个 .so, 其它的.so都不使用RTLD_GLOBAL方式, 并且在编译的时候都不把和第一.so相关的库联编 4. 第一个.so的升级需要保证没有其它.so在运行才可以dlclose, 重新dlopen
这个问题首先需要明确需求, 到底是希望每个.so打自己独立的日志,还是和主线程统一
这 里要注意另外一个问题就是目前的ullib 日志库情况比老的ullog要复杂, 在comlog中引入一些extern 出来的全局变量 在采用dlopen的时候,对于一般符号,一般都是主程序和动态库在两块空间中, 但是对于使用extern出来的变量主程序和动态库都是在一块空间中(注:由于32位下不用-fPIC也可以编译so, 在没有-fPIC的情况也是分开的, 但是由于64位一定要-fPIC所以一定会出现同一块空间的问题), 对于这个问题的解决方案是在动态库链接的时候加上 -Wl,-Bsymbolic 参数将动态库的空间和主程序的空间强行分开。
上面有提到 编译动态链接库时,不加链接libullib.a库,但主程序使用-rdynamic, 这里主要是为了避免使用到了不同的ullib导致调用了一些不同的内部符号,导致出现另外的麻烦
对于动态库中的日志建议采用下面的几个方案:
1. 动态库完全打自己的,日志,编译二进制程序不要用-rdynamic, 动态库链接的编译加上 -Wl,-Bsymbolic 参数 , 链接ullib, 在动态库中自己open,自己控制等级 1. 动态库不链接ullib, 编译二进制程序用-rdynamic , 这样可以正确的使用主程序中的日志库,也规避了版本不一致带来的问题, 但是这样失去了对于动态库日志的控制, 而且存在升级的不便, 日志的升级是由主程序控制的。
小提示
有关动态库使用的例子还可以参考 SoTips
在运行期可以通过设置环境变量LD_DEBUG查看每个符号具体链接到了什么地方,每个符号具体的查找过程和绑定过程.可以这样使用
export LD_DEBUG=help
随便运行一个程序就可以看到对于LD_DEBUG的使用说明export LD_DEBUG=files./main可以看到整个装载过程

版本管理

系 统中存在了大量的动态和静态库,并且每个库都会随着库的升级和更新,形成各种的版本,这些版本之间又存在了各种各样的兼容或者不兼容的问题.linux中 是如何维护和管理这些库的?这里介绍了linux在这方面所作的一些工作.下面的这些都是基于我们现在使用的64位开发环境中的情况, 与32位的老版本存在了一定程度上的不兼容.

命名

在linux系统中对于一个共享库的命名一般是 libname.so.x.y.z
这个与百度目前通常使用的版本项目版本是类似,我们的版本号多了一个4位版本,作为开发过程中的小版本号,最后发布的其实也是按照3位版本号进行的.
不过在linux中 也有不少不遵守上面的命名的,比如glibc的动态库叫libc-2.3.5.so, 版本号在.so前面
这里又存在了另一个问题,由于版本号和命名捆绑在一起,那么当库升级的时候有怎么把版本号给对应上呢?

编译和载入的版本

我们先看一下pthread库在系统中的情况,(下面是以64位开发机为例,32位路径有所不同其它都一样)
在 /usr/lib64/ 中我们可以找到libpthread.so, 不过可以到libpthread.so其实很下,cat 一下可以看到这其实是一个文本文件
libpthread.so的内容:/* GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. */OUTPUT_FORMAT(elf64-x86-64)GROUP ( /lib64/libpthread.so.0 /usr/lib64/libpthread_nonshared.a )这 个libpthread.so 其实并不是什么共享库,它其实是一个ld的链接脚本,这个脚本的意思是,输出的是elf64-x86-64格式,使用的动 态库是/lib64/libc.so.6 静态库是/usr/lib64/libc_nonshared.a, 这样我们在编译的时候不用考虑使用的是哪一个版本的libpthread.so,也不需要靠它的动态库叫什么,静态库叫什么,只需要都是指定  -lpthread就可以了,具体实际是哪个版本,交给脚本去处理.
我们再看/lib64/libpthread.so.0, 其实这是一个软链接,它实际指向的是 libpthread-0.10.so, libpthread-0.10.so才是它真正使用的.so. pthread它通过这样的方式进行链接有什么作用呢?
我 先随便写个使用了pthread的程序,然后用readelf -d 查看,这时候可以看到pthread那一行指向的是 libpthread.so.0!, 而不是libpthread-0.10.so. 其实pthread这样处理主要是从版本的兼容性方面进行考虑.在我们编译的阶段通过ld脚本,统一了编译时候-l使用的命字,我们编译的时候不需要去考 虑什么.so的版本号问题.编译完了实际指向的是 libpthread.so.0,而不是最后的libpthread-0.10.so, 这样的好处在下面的几个方面:
如果pthread升级了, 比如升级后叫做libpthread-0.20.so,作为开发者可以保证它和libpthread-0.10.so是兼容的,那么我们可以大胆的把 libpthread.so.0的软链接指向libpthread-0.20.so,这不会出现什么问题.
如果libpthread-0.20.so与libpthread-0.10.so是不兼容的,那么我们可以新建立一个叫做 libpthread.so.1的符号链接指向libpthread-0.20.so,这样老的程序在运行由于它认为自己使用的是 libpthread.so.0而不会指向libpthread.so.1, 而用新版本编译出来的程序会自动依赖到libpthread.so.1,而不会出现依赖了老的libpthread-0.10.so而导致不兼容.需要依 赖老版本的程序在运行的过程中也不会出现因为依赖的库替换了不兼容的库导致出现问题.

符号版本

上面的这个过程很好的解决了在同一台机器上编译和使用动态库的更新问题.但是还有一些问题上面的方式无法完全解决
比如我现在的程序是使用了libpthread-0.20.so编译出来的程序,但是在运行的机器上只有libpthread-0.10.so, 这个时候有2种选择 1. 警报,但让程序继续运行,这有可能会出core 2. 直接禁止运行
现在的问题,很有可能我虽然用了新的库进行编译,但我只用到了老的接口,用libpthread-0.10.so就可以胜任我的工作, 但无论警报也好还是直接禁止运行也好这些都显的不合适.
为 了能解决这个问题, linux在符号中引入了版本的机制.我们可以用nm /lib64/libc.so.6 查看glibc中的符号表(我们32位环境中的glibc符号表被清空了,只能通过readelf进行查看). 我们可以看有不少接口被写成类似于 tmpfile@@GLIBC_2.2.5 的形式, 在符号中带有版本号信息, 这方式可以使程序在编译的时候记录下它编译的时候使用的共享库所对应的接口的版本号,这样如果共享库升级了, 当程序载入共享库的时候会检查编译时记录下来的版本是否与当前共享库接口的版本是否兼容,如果兼容那么就可以正常运行,如果有不兼容的情况就在载入库的时 候报错. 这种机制尽可能的考虑高版本与低版本的兼容性问题,如果我们打开glibc的源码包,我们可以到许多目录下都有一个叫Versions的文件,这个文件就 是用来描述各接口的兼容版本.

共享库可执行

我们直接运行 /lib64/tls/libc.so.6 , 可以看到,在运行后出现了一段的文字,表示了库的版本,作者, 编译时间等信息.这种方式提供了一种给用户确认和了解动态库的方式. 这种方式的实现也很简单, 下面是一个demo
#include#include#include#ifdef __cplusplus extern "C" { #endif#ifndef __i386 /** * @brief 使用的动态链接库 * */#define LD_SO_PATH "/lib64/ld-linux-x86-64.so.2" #else#define LD_SO_PATH "/lib/ld-linux.so.2" #endif#if defined( __DATE__) && defined( __TIME__) /** * @brief 编译时间 * */#define BUILD_DATE ( __DATE__ "" __TIME__) #else#define BUILD_DATE "unknown" #endif/** * @brief 设置入口位置 * */constchar interp[] __attribute__((section(".interp"))) = LD_SO_PATH;                /** * @brief .so文件运行的入口函数 * **/void so_main() { //printf输出可以方便外部grep printf("Ld.so : %s\n", LD_SO_PATH); //这个可以规避由于-O2开关导致,__((section(".interp"))) 被优化没的问题 printf("Project : %s\n", "mc_pack php extension for mcpack2"); printf("Version : %s\n", VERSION); printf("CVS : %s\n", "public/php-ex/php-mcpack"); printf("CVSTag : %s\n", CVSTAG); printf("BuildDate : %s\n", BUILD_DATE); exit(0); }#ifdef __cplusplus } #endif
LD_SO_PATH 表示了所使用的加载器,这里要注意32位和64位的区别,这里把过程简化了用宏__i386来判断32位与64位
section(".interp") 设置了运行使用的载入器
so_main是运行程序的地方,这里写上共享编译的信息, 当然这个地方可以换成别的什么名字, 在编译的时候需要加上 -Wl,-e,so_main, 指明了动态库的实际的入口位置(这里是so_name, 也可以换成xxx)
采用的编译参数g++ xxx.cpp -fPIC -c -Wl, -e,so_main在把生成的.o与共享的主程序链接到一起,就可以直接运行共享库了

总结和建议

上面介绍库(包括静态库和共享库)链接的过程,并对应其实现进行了分析,并且针对编译中出现的问题进行分析和解决.
这对于我们使用库是有一定的帮助的. 这里对于库的编写和使用提出一些建议

静态库

尽量不要使用--static参数, 对于一些特殊的必须要使用.so的情况,可以考虑使用-Wl,-dn 的方式进行.
64位环境中编译加上-fPIC 虽然是静态库,但很可能会被用作共享库的一部分
链接的时候注意链接的顺序,越是基础库越是在后面

共享库

开发

对外接口尽量使用基本类型,不要使用C++类, 如果一定需要建议采用指针的方式
对外接口采用extern "C"的方式提供, 不要直接使用一般的C++接口,这主要是从接口的方便性考虑,毕竟都不希望采用"_fooIV"这种形式的接口进行访问.
共享库的生成文件需要带上我们的4位版本号, 格式与linux的格式相同, 如 mcpack.so.1.1.0.0
共享库需要能够自运行, 运行需要输出版本号, 编译时间,库的简单说明, DEMO见上文中共享库可执行部分. 输出格式建议:
采用printf输出,考虑到以后可能可以用脚本分析 输出格式参考DEMO的样式
小心使用-rdynamic参数, 能够不使用就不要使用
尽量保证主程序的编译依赖与动态库的编译依赖相同,特别是在主程序代码中使用-rdynamic.

发布

动 态库发布到output下除了要有带版本号的.so,还需要同时有一个未带版本号的.so, 比如在output/lib目录需要有mcpack.so.1.1.0.0 和一个软链接 mcpack.so 指向mcpack.1.1.0.0, 这里的mcpack.so软链接主要是给其它程序编译期使用的.

上线

严禁对于.so采取直接copy覆盖的方式
更新.so, 可以采用两种方式:
软链接方式, 程序运行使用的是确定mcpack.so软链接,上线的时候采用的是把mcpack.so链接指向mcpack.so.1.1.0.0, 由于原来的程序还在,这样不会出现问题
mv方式, 旧的.so需要先mv成另外的文件名,然后再放入新的.so
需要确认.so已经被载入, 由于.so的使用本身有2种方式,需要考虑下面两种情况 dlopen方式,这种重新载入可以适用于热切换,这个要求RD根据代码逻辑来控制, 一般可以从日志看出
LD_LIBRARY_PATH指定路径,这种情况一般第三方库的情况比较多,这个时候需要把程序重启
所有操作完毕之后,需要用/usr/sbin/lsof -p pid 查看载入的动态库是否是我们需要的不要在终端或者.bash_profile 等全局环境中加入LD_LIBRARY_PATH, 在启动需要.so的程序脚本中加入即可.

FAQ

这里收集了一些与编译,链接相关的问题, 有问题随时欢迎提问.

编译问题太复杂了,有没有简单的解决方案?

这里建议大家使用com组提供的Comake自动编译构建工具,来解决这些存在复杂依赖的情况, 在comake中 通过简单的描述就可以规避大量的编译链接问题

我是纯C程序,如何使用ullib这些用g++编译出来的库

上文已经介绍过了, 在g++的环境中直接编译的结果会导致符合表与gcc编译的结果不同导致不能混合编译.
gcc使用g++编译的库原则:
1. g++编译库的时候需要把被外界使用的接口按照纯C++可以接受的方式用extern "C" 包起来,并且加上__cplusplus宏的判断,可以参考public/mcpack, public/nshead中的写法. 对于一些特殊情况,比如已经是g++编译出来的库又不适合修改,比如ullib, 分词库等,可以自己写一个 xxx.cpp的程序,在xxx.cpp对需要使用的接口再做一次纯C接口的封装,同时用extern "C"把纯C接口导出使用.使用g++编译,并且在链接的时候加上ullib等库即可. 2. gcc编译g++库在我们的64位环境中需要在最后加上-lstdc++
gcc使用g++编译的库多见于需要将基础库与php扩展,apache mod进行联编, 这可以参考 public/php-ex/php-wordseg中的实现
g++使用gcc编译出来的库: 这个比较简单,我们使用系统的库都是这种方式,只需要gcc编译的提供的头文件采用了extern "C"封装即可.

我在同样的环境下用同样的方式编译出来的程序md5是否都一样

如果环境完全一样包括编译路径,环境变量等都是一样的,一般情况下确实是一样的,但是许多环境的情况我们很难做到一样,比如程序使用一些DATA这样与时间相关宏就会导致每次编译的结果都是不一样的,有时候甚至内存的多少也会影响编译的结果

链接和运行的时候,静态库和动态库路径的查找顺序都是什么?

链接的时候查找顺序:
-L 指定的路径, 从左到右依次查找
由 环境变量 LIBRARY_PATH 指定的路径,使用":"分割从左到右依次查找
/etc/ld.so.conf 指定的路径顺序
/lib 和 /usr/lib (64位下是/lib64和/usr/lib64)
动态库调用的查找顺序: ld的-rpath参数指定的路径, 这是写死在代码中的
ld脚本指定的路径
LD_LIBRARY_PATH 指定的路径
/etc/ld.so.conf 指定的路径
/lib和/usr/lib(64位下是/lib64和/usr/lib64)
一般情况链接的时候我们采用-L的方式指定查找路径, 调用动态链接库的时候采用LD_LIBRARY_PATH的方式指定链接路径.另 外注意一个问题,就是只要查找到第一个就会返回,后面的不会再查找. 比如-L./A -L./B -lx 在A中有libx.a B中有libx.a和libx.so, 这个时候会使用在./A的libx.a 而不会遵循动态库优先的原则,因为./A是先找到的,并且没有同名动态库存在.

我们一般使用ullib库都是直接获取.a进行编译,但是public下的库都是需要我们自己手动编译后才可以使用,为什么要这样做?

这个地方涉及到一些历史问题, lib2和lib2-64下的库都是以库的形式发布这里有两个原因:
类似ullib这样的库到处都要用,每次都编译比较花时间
类似于wordseg这样的库有保密需求,不方便向所有工程师透露原代码
一般我们把发布到lib2和lib2-64下的库称为静态发布, 发布到public下的库都是实时发布,它们的区别就在于public下的库每次都会重新进行编译.但是这种静态发布会给库的升级带来一些麻烦:
静态编译的库之间有依赖关系,一些接口性的升级会超成连锁反应,导致多个库都需要升级. 比如最简单的情况:
ullib库中有一个叫foo(char *str)的接口, 由于是老代码当时没有考虑到foo的接口上需要使用 const ,现在希望能够满足编程规范采用const接口变成foo(const char*str)的形式.这个时候假设其他静态发布的库libX也使用了这个接口. 在前面的介绍中我们提到了在c++中会根据接口类型不同导致符号不同,我们把foo(char *str)改为foo(const char*str)导致现在编译出来的ullib中符好是const char*str的形式,而不是char*str的形式, 这样在联合编译libX和ullib的时候,libX就会说foo(char *str)这个接口找不到, 这个时候一种解决方案就是libX也升一次级, 这样又带来另外的问题就是强迫大家都必须把相关的库都升上去,这个往往是不现实的, 因为某些原因这个过程往往需要分成几步走( 比如需要考虑新分词和老分词的策略区别, 版本相差太多的风险性). 目前这方面COM采用的解决方式是在.cpp文件中依然保留foo(char *str)的接口,这样可以使得老库在依赖新的ullib的时候不会出问题.但是这个方案还是不能本质解决问题, 比如新的uldict使用新的ullib编译出来, 现在需要使用一个新的dict和一个老的ullib联合编译,由于dict依赖了ullib中的const char*的接口,但老的ullib又不存在const char*这样的接口, 这个时候联编又会发现符号找不到.我们只能做到老的使用新的不出问题,但新的使用老的还是存在一些问题,目前只能建议用户升级到最新的ullib上.如果 升级存在困难请联系com组.
静态发布受到编译器和环境的限制 我们发布的是使用32位gcc 2.96和64位gcc3.4编译出来的库,如果换到另外的gcc环境中又需要重新编译,特别是存在有多个静态发布的库需要重新编译.
上面的这些问题,如果是在采用实时编译的话都可以不会出问题的,我们可以保证了代码级别上的兼容,但对于已经存在的符号表的兼容确实比较麻烦.
但是事实上这些库采用和public的类似在scmpf上进行实现编译其实也是可以做到,只是目前还是保留过去的编译方式,比较静态编译目前出现的问题都是可以规避的.
另外在third和third-64下的第三方库也是采用静态发布,这个主要考虑是第三方库的编译比较消耗时间.
使用cvs co libsrc/ullib 的方式可以直接获取到ulllib的原代码

哪些情况会出现 "undefined reference error" 的错误?

这里再总结一下这个问题可能出现的场景:
没有指定对应的库(.o/.a/.so) 使用了库中定义的实体,但没有指定库(-lXXX)或者没有指定库路径(-LYYY),会导致该错误, 比如 使用uldict, 由于uldict中使用到了md5签名需要库crypto的支持,需要在-lullib之后加上-lcrypto.
连接库参数的顺序不对 在默认情况下,对于-l 使用库的要求是越是基础的库越要写在后面,无论是静态还动态,这里可以参考上文件中静态链接的章节.
gcc/ld版本不匹配 gcc/ld的版本的兼容性问题,由于gcc2 到 gcc3大版本的兼容性存在问题(其实gcc3.2到3.4也一定程度上存在这样的问题) 当在高版本机器上使用低版本的机器就会导致这样的错误, 这个问题比较常见在我们32位的环境上, 许多32位的机器上是gcc3.2的编译环境,但我们提供的是有gcc2.96编译出来的结果,导致ullib库不能被编译器所认识. 另外就在32位环境不小心使用了64位的库或者反过来64位环境使用了32位的库.
C/C++相互依赖和链接 gcc和g++编译结果的混用需要保证能够extern "C" 两边都可以使用的接口,在我们的64位环境中gcc链接g++的库还需要加上 -lstdc++,具体见前文对于混合编译的说明
运行期报错 这个问题基本上是由于程序使用了dlopen方式载入.so, 但.so没有把所有需要的库都链接上,具体参加上文中对于静态库和动态库混合使用的说明

可以把两个.o直接合并成一个.o文件吗?

可以,命令是 ld -r a.o b.o -o x.o, 不过不推荐这样做,这样做唯一的好处是静态库在链接的时候如果使用到了a.o中的符号也可以同时把b.o中的符号链接进来,可以避免--whole-archive的应用.
但是不推荐这样做,无形中增加了对源文件维护的麻烦

为什么我使用inline,并没有把代码inline进程序?

首先加了inline的函数是否可以被inline这个是由编译器决定,很多时候即时是指定了inline但还是无法被inline
另 外注意到我们的gcc中,只有在使用-O以上的优化后inline才会起作用,没有-O, -O2, -O3这些优化手段,无论是否加上了-finline-functions gcc都是不会进行inline优化的,这个时候的inline相当于一个普通函数(其实还是有一点区别,在符号表中表示是不一样的).我们许多程序在编 译的时候加上了-finline-functions 但如果没有-OX(X>=1)的配合, -finline-functions其实是无效的,不会起作用也不会报错
gcc里面为了能够支持在不加-OX(X>=1)的情况下能够将函数inline, 提供了一个扩展always_inline, 将函数写成下面这样
__attribute__((always_inline)) int foo() { ... }
就可以在不加-OX(X>=1)的情况下把foo inline进程序,不过always_inline 这个扩展只在gcc3以后支持,我们32位环境中使用的2.96 gcc是不支持的.

64位机器上可以编译出32位程序吗?

理 论上是可以的, 在64位机器上的64位gcc中提供了-m32的参数,可以指定进行32位的编译。 但是编译问题虽然解决,但链接问题却还是存在, 我们部分64位机器上gcc2.96使用的程序覆盖了64位机器给32位程序使用的库导致链接失败, 如 果没有覆盖的机器是正常的。

我将-lub -lub_log这样连在一起使用是否有问题?

如 果确认所使用libub.a, libub_log.a它们的版本一致,这样使用是没有问题的,无论是libub中的ub_log的部分还是ub_log.a中的ub_log的部分,他 们都是属于同样的二进制代码,无论怎么链接都可以正常工作. 但是如果版本不同可能会有不可预知的后果(如最经典的baidugz和zlib的冲突).

为什么编写的动态链接库不能直接运行?

在共享库的总结中介绍了如何实现共享库可以自己运行,但是有些时候会出现undefined reference error的错误导致共享库不能被运行。
这 种情况产生的原因是:动态库中采用了类似 static int val = func(xxx);的写法, 其中val 是一个全局变量(或者静态全局变量)。 动态库被载入内存中使用的时候会直接先运行func这个函数,如果func是来自其他的库(比如一些情况下主程序使用-rdynamic编译,动态库使用 主程序的空间), 在编译动态链接的库的时候又没有被链接上, 这个时候就会出现这样的问题。
对于这样的问题主要考虑下面的解决方案:
1. 不要采用static int val = func(xxx);这种写法
将使用的静态库链接进共享库, 但这里要注意-rdynamic的影响,必要的时候需要保证和主程序使用的库版本是相同的。
让共享库不可运行也是一种解决方案

是否可以在main函数开始前就执行程序?

如果在main函数开始前执行代码,我们一般有下面的两种方法
采用 int val = func(xxx)的方式,在func(xxx)中执行
声明一个class, 把需要运行的函数写在class. 并且定义一个全局(或者static)的类变量
在实现上,编译器把它们放到一个特殊的符号 _init 中,在程序被载入内存的时候被执行但是这种方式我们不推荐使用,特别是在这些执行代码中存在库与库之间的依赖关系的时候, 比如下面的场景:
libA.cpp
class Aclass { public: Aclass() { int * u = Bfunc(); //这是另外一个库libB中的函数 int c = u[0]; }                        }                static Aclass s_test;
libB.cpp
staticint *s_test = test_init(); //初始化s_testint *Bfunc() { return s_test; }
上面的程序中有2个库,A库有一个static变量的构造函数依赖了 B库中的一个函数, B库中的这个函数又操作了一个由函数test_init初始化的static变量.按照程序的要求我们必须要让test_init()这个函数在Aclass这个函数之前运行, 但是可惜的在某些情况我们很难做到这点, 这里涉及到链接器对库链接和初始化顺序的问题.
在默认情况下, test_init()和s_test的构造函数的执行顺序是按照链接的时候-l的顺序从右到左, 比如-lB -lA 那么Aclass的构造函数会在test_init()前执行,这个时候就会出现问题,需要保证-lA -lB的顺序才可以正常.
这 里又涉及到另外一个问题, 就是 正常情况既然A依赖B, 那么在链接的时候肯定需要 保证 -lA在-lB. 但是这里我们只能说需要把越基础的库放在越后面,而不是必需放在最后面.还是上面的例子. 如果这个时候有一个test.cpp 使用了 A库, 并且在test中没有直接使用到B库中的东西, 这个时候如果-lB放在-lA前面,链接器会报错, 因为符号在从左往右展开的时候, 由于test没有使用到B的东西,所以没有做任何展开, 从这个角度而言在链接A的时候就找不到符号. 但是如果在test中有使用到B中和test_init相关联的函数,那么这个时候如果把-lB放在-lA的前面展开B函数的时候会把test_init 导出, 这样导致A会认为已经存在了test_init, 从而不报编译错误. 但是这样的结果就是test_init的初始化顺序被放到Aclass之后, 那么在程序运行的时候就可能导致错误.
对这种问题解决,主要有几种考虑
采用 单例模式, 采用类似 if (NULL == ptr) ptr = new xxx; return ptr的方式通过用户态的判断来控制
了解依赖关系, 把-lB放到-lA的后面
不允许这种方式的存在.
在使用全局变量的时候 需要特别注意这种初始化的顺序问题.小提示:
构造初始化等,是在_init中处理, 另一个方面_fini是存在在程序退出前的执行析构等操作

dlopen是否可以载入主程序的符号?

dlopen除了可以通过指定文件名载入共享库中的符号其实也是可以载入主程序的符号,只不过要注意这个时候主程需要使用-rdynamic
在dlopen的时候使用dlopen(NULL, ...) 的方式载入符号,这个时候可以载入此时运行程序中的所有全局符号,包括2个部分
dlopen具体共享的时候采用了RTLD_GLOBAL方式打开
主程序在链接的时候使用了-rdynamic参数
例:
extern "C" { externint foo(int a, int b); } int foo(int a, int b) { fprintf(stderr, "%d %d\n", a, b); } typedefint (*foo_t)(int, int);int main() { void *handle = dlopen(NULL, RTLD_NOW); char *estr = dlerror(); if (estr != NULL) { fprintf(stderr, "myerror %s\n", estr); exit(-1); } foo_t fun = (foo_t)dlsym(handle, "foo"); estr = dlerror(); if (estr != NULL) { fprintf(stderr, "error %s\n", estr); exit(-1); } fun(3, 4); return 0; }
上面的程序采用g++ -o test test.cpp -ldl -rdynamic编译后, fun可以被正常执行

有些程序对于库的编译依赖版本不一致,会有问题吗?

一个例子:
一个程序依赖A库和C库, A库依赖B库的版本1, 但C库依赖B库的版本2
几个注意: 1. 程序具体依赖到那个库是在最后链接的时候决定的 2. 在平台上SCM会选择最高版本的那个B库进行链接 3. 在编译期,库的依赖只是头文件的依赖。
严格来说这样是有一定的风险, 对于lib2和public下的库,主要由com组在升级过程中会尽可能的规避这样的风险,让大家一般情况下都不需要关心这个问题。 一般来说, com组都会保证头文件的编译依赖的向下兼容
在scmpf平台上对于编译的问题是可以强制指定编译依赖的。
另 外的一个问题就是,由于在平台上只会针对一个版本进行编译,不至于出现。 但是在线下编译的时候,对于上面的情况可能还是会出现一些链接错误, 这个主要原因是由于A库由版本1的B库编译,然后升级了, C库由版本2个B库编译, 最后编程序的时候链接是版本2的B库造成的, 遇到这种问题 , 一般建议是把A库用版本2重新编译一边。因为向下兼容一般只能保证做到高版本支持低版本,在不重新编译的情况要做到兼容比较困难,另一方面意义也不大。

参考资料

《深入理解计算机系统》
《Linkers and Loaders》
GCC online documentation : http://gcc.gnu.org/onlinedocs/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: