一些与编译,链接相关的问题(zz)

2019-04-15 16:20发布

 地址无关代码,在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还显的相对友好。

同时存在静态库和动态库

前面提到编译动态库的时候有提到编译动态库可以像编译静态库那样采用-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:
由于我们程序本身在运行的需要系统中一些库的支持,在采用--static编译方式之后,链接的就是这些库的静态编译版本,等于使用的是编译机上的库,但是我们的运行环境可能和编译机有所不同,glibc这些动态库的存在本身的目的就是为了能让在一台机器上编译好的库能够比较方便的移到另外的机器上,程序本身只需要关注接口,至于从接口到底层的部分由每台机器上的.so来处理.不过这个问题也不是那么绝对,在一些特殊情况下(比如 glibc, gcc存在大版本差异的时候,主要是gcc2到gcc3有些地方没有做好,abi不兼容的问题比较突出,真遇到这些情况其实需要换编译器了) --static编译反倒可以正常的运行.但是还是不推荐使用, 这些是可以采用其它方法规范在后面的第6点中有说明.另外就是glibc --static编译可能会产生下面的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检查内存泄露为了不在一些特殊的情况下误报, 它需要用动态库的方式替换glibc中的函数,如果静态编译那么valgrind就无法替换这些函数,产生误报甚至无法报错. tcmalloc在这种情况下也不能支持.
64位环境中使用的pthread库,如果是使用的是动态库那么采用的是ntpl库,如果是静态库采用的linuxthread库,使用--static 会导致性能下降
--static之后会导致代码大小变大,对cpu代码cache不友好,浪费内存空间,不过对于小代码问题也不大.

链接参数控制
链接器中提供了-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的方式载入,由于是运行期,这个程序在这个地方就直接运行报错了.另外还有一种情况就是一个对外的接口在动态库中已经声明定义了,但是忘记实现了,这个时候也会产生类似的错误.

如果在运行期报出这样的错误,就要注意是否是由于某些库没有链接进来或者某些接口没有实现的原因产生

=================================================

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

纯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进行联编;

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是先找到的,并且没有同名动态库存在.
哪些情况会出现 "undefined reference error" 的错误?

这里再总结一下这个问题可能出现的场景:

没有指定对应的库(.o/.a/.so) 使用了库中定义的实体,但没有指定库(-lXXX)或者没有指定库路径(-LYYY),会导致该错误,
连接库参数的顺序不对 在默认情况下,对于-l 使用库的要求是越是基础的库越要写在后面,无论是静态还动态
gcc/ld版本不匹配 gcc/ld的版本的兼容性问题,由于gcc2 到 gcc3大版本的兼容性存在问题(其实gcc3.2到3.4也一定程度上存在这样的问题) 当在高版本机器上使用低版本的机器就会导致这样的错误, 这个问题比较常见在32位的环境上, 另外就在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位的机器上可以用进行链接的库主要有2个一个是供64位程序使用的,另外一个供gcc2.96编译程序在64位机器上运行的,这两个库都不能给gcc -m32出来的结果提供链接环境(32位库不能连接64位库,给gcc2.96的库太老的不兼容), 所以在编译机器环境上是不能直接编译出可执行的32位程序(编译成.o文件还是可以的)

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

在共享库的总结中介绍了如何实现共享库可以自己运行,但是有些时候会出现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

static int *s_test = test_init(); //初始化s_test

int *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是存在在程序退出前的执行析构等操作
原文地址:http://hi.baidu.com/graphchen/blog/item/e5f997c996a2141e7f3e6f06.html
谢谢