`
myoldman
  • 浏览: 84905 次
  • 性别: Icon_minigender_1
  • 来自: 福建福州
最近访客 更多访客>>
社区版块
存档分类
最新评论

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

 
阅读更多
009-08-28 03:27 P.M. 地址无关代码,在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是存在在程序退出前的执行析构等操作
分享到:
评论

相关推荐

    已经编译好的NTL 静态链接库

    在这个已经编译好的NTL静态链接库中,用户可以直接使用预编译的库文件,无需自行编译源代码,简化了项目集成过程。 首先,我们要理解什么是静态链接库。静态链接库是将库函数编译进目标程序中,使得程序运行时不再...

    NTL库包括编译好的静态链接库

    在本资源中,我们得到了NTL库的静态链接库版本,这意味着这些库文件已经编译完成,可以直接与我们的应用程序静态链接,无需在运行时寻找和加载对应的动态库文件。 静态链接库的优势在于简化了部署过程,因为所有...

    gcc编译stm32f103+freeROTS代码

    7. 使用编译命令(如`arm-none-eabi-gcc`)进行编译和链接,生成`.elf`文件。 8. 使用工具(如`arm-none-eabi-objcopy`)将`.elf`转换为`.hex`或`.bin`,便于烧录到STM32F103的闪存中。 在压缩包中,提供的文件可能...

    编译原理试卷(期末考试)

    4. 解释语义规则,并解决与类型系统、作用域和链接相关的问题。 5. 分析并优化给定的中间代码,提出改进方案。 6. 理解并解释目标代码生成过程,包括寄存器分配策略和指令调度。 对这些主题的深入理解和应用是编译...

    可以同时编译成Sys和Dll的库文件示例

    在本示例中,`ZZ_TEST`可能是包含源代码、编译脚本或其他相关资源的压缩包。解压后,我们可以看到实现这一功能的具体代码结构、编译脚本和测试用例。这些文件将有助于理解如何实际操作和应用上述理论知识。 总结...

    zz.rar_visual c

    《Visual C++编程基础与实践——基于"zz.rar"案例解析》 Visual C++作为Microsoft公司推出的集成开发环境,是Windows平台上进行C++程序开发的重要工具。它集成了编译器、调试器以及资源编辑器等,使得C++开发者能够...

    android应用源码zz-doctor中医大夫助理信息系统.rar

    【标签】"android应用源码zz-do android应用源码"表明这是与Android应用开发相关的代码,"zz-do"可能是项目或应用的简写,可能代表“中医助手”或类似的含义。标签强调了这是可以被开发者参考、学习或修改的源代码,...

    编译原理专栏程序输入文件

    这个压缩包文件包含了与编译原理相关的程序输入文件,可能是用于教学、实验或项目开发的资源。让我们深入探讨一下编译原理中的关键知识点。 1. **词法分析(Lexical Analysis)**:这是编译过程的第一步,也称为...

    VC++动态链接库(DLL)编程深入浅出(zz).doc

    在静态链接库中,`add`函数的实现会被编译进`.lib`文件,使用者只需包含对应的头文件并链接库即可使用。但在DLL中,`add`函数的实现存在于DLL文件中,程序运行时动态加载并调用。 为了使用DLL,调用方需要通过`...

    android应用源码zz-doctor中医大夫助理信息系统.zip

    4. zz_doctor - 这可能是项目的主代码目录,包含了所有与“zz-doctor”应用相关的源文件。这些文件可能包括Java或Kotlin类、XML布局文件、资源文件(如图片、字符串、样式)等,它们一起构成了Android应用的核心部分...

    windows动态链接文件

    与msvcp120d.dll一样,这里的“d”表示调试版本,它包含了一些额外的调试信息和检查,便于开发者定位程序中的错误。 这两个DLL文件通常与使用Visual Studio 2013编译器开发的应用程序一起使用。当一个应用程序需要...

    VS2010下的NTL静态库以及示例

    4. 编译并链接NTL库(静态库或动态库,本例为静态库)。 5. 使用NTL的类和函数编写代码,并参考提供的示例进行调试和学习。 通过学习这些示例,开发者不仅可以掌握NTL的基本用法,还能了解到如何在实际项目中有效地...

    NTL库VC6.0测试工程

    `test`文件可能是这个测试工程的主要源代码文件,可能包含了如何初始化NTL库,创建和操作大整数,以及执行其他相关运算的示例代码。分析和理解这个文件,可以帮助你更好地掌握NTL库的实际使用。 在密码学中,NTL库...

    高精度计算库_接口及静态库

    这个库由Visual Studio 2005(VS2005)编译,相较于之前的版本,它解决了与旧版VC6编译的库不兼容的问题,特别是解决了缺少`libcd.lib`依赖的问题。 NTL库提供了丰富的接口,允许开发者轻松地进行高精度算术操作,...

    明靓正则表达式工具易语言源码.rar

    3. 动态链接库(DLL):ml_zz.dll 是一个动态链接库文件,它包含了一些预编译的函数或模块,可供其他程序调用。在这个案例中,可能包含了正则表达式相关的函数实现,供主程序使用。 4. 正则表达式工具.e:这是...

    centos6.6离线手动安装gcc和prel

    在Linux系统中,CentOS 6.6是一个广泛使用的版本,尤其在服务器环境中。...如果你遇到任何问题,记得查阅相关文档或在线论坛,寻求解决方案。离线安装虽然复杂,但它能让你在不具备网络条件的情况下依然保持开发能力。

    NOI2019基础知识题库.pdf

    比如Pascal中的integer和longinteger类型的长度与编译选项有关,而NOI对C++语言模板的使用没有限制。选手在提交答案时如果包含NOI考试明确禁止使用的代码,则会受到成绩以0分计算的惩罚。 以上知识点是NOI2019基础...

    c++ 第三方日志库,纯源码非dll

    3. **编译与链接**:编译你的项目时,编译器会一起处理你的代码和plog的源码,确保所有依赖项都被正确链接。 4. **使用API**:在你的代码中,通过plog提供的API来记录日志,例如设置日志级别、指定输出目标、控制...

    zz.rar_OpenGL_C++_

    从`zz.vcproj`和`.user`文件来看,这个项目是使用Visual Studio编译的,`.vcproj`是项目的配置文件,包含了编译器设置、链接器设置以及源文件列表。而`.user`文件则是个人用户设置,可能包含了一些调试或构建路径的...

Global site tag (gtag.js) - Google Analytics