2010年8月11日星期三

编译,链接和库

编译,链接和库
2010-01-14 23:58
      在膜拜了辉哥牛x的总结文档后,摘录了一些重点以加深自己的印象。当然,当务之急是好好学习一把两本神作《Linkers and Loaders》和《Computer Systems A Programmer's Perspective》。

1、.h文件在预编译的时候就展开到.c/.cpp里,现代编译器一般同时进行预编译和编译。

2、只声明函数,在没有调用的情况下,编译和链接都不会有问题;只声明函数,在没有static关键字的时候,可以编译(默认函数位置在链接的时候可以找到),链接时出错;如果使用了static关键字,则只有声明的函数无法通过编译,因为对于static的函数,编译器只在本地查找对应的符号表。

3、链接包含两部分工作:符号解析和重定位。他们的作用是把各个.o中在编译时未确定的函数位置与其实现的真实位置产生对应关系。

4、使用-L -l的方式定位库,有个问题,就是不区分动态和静态库,默认情况下,使用的是动态库。

5、写依赖库的时候,越基础的库越需要放在后面,这是因为符号表的解析和查找利用一个类似堆栈的尾递归实现。可以使用以下链接参数实现循环自动查找编译关系
-Xlinker "-(" -la -lb -Xlinker "-)"
-Xlinker有时候也简写成"-Wl, ",它的意思是 它后面的参数是给链接器使用的.-Xlinker 和 -Wl 的区别是一个后面跟的参数是用空格,另一个是用"," 

6、gcc和g++的几个区别
a) gcc和g++在编译时产生的符号不同。C++由于要支持函数重载,命名空间等特性,其编译出的符号比较奇特。可以使用c++filt查看符号表对应函数的真实原型。由于符号表的差异,如果C++库需要被C程序调用,则需要用 extern "C" 方式,然而extern "C"只有g++认识,所以还需要配合__cplusplus宏一起使用。注意,即使使用 extern "C"的方式,gcc还是不能编译通过带默认参数的接口。
b) gcc不会主动链接-lstdc++,g++会
c) const的全局变量,g++会自动将其加上static属性,而gcc不会。(假设一个全局的const int a = 0;使用gcc编译后,nm后可知,符号a的属性是R,全局只读符号;使用g++编译后,nm可知,符号a的属性是r,局部只读符号)

7、注意只有用gcc编译.c文件才会真正把源文件当做c程序来编译,使用gcc编译.cc、.cpp或者使用g++编译.c都会被当做cpp程序来编译


8、a虽然只是.o的打包,但是使用.a与直接使用.o有几个不同之处:
a) 使用.o链接的时候,所有.o包含的符号表都会被载入最后的可执行程序,而是用.a时,如果某个.o未被使用,则它不会被载入。
b) 在链接的时候,如果两个.o包含相同的符号,编译器会报redefination error,但是如果将这两个.o分别打包成.a,连接时会选择第一个库中的现实。

9、对于静态库的使用,有以下几方面问题:
a) 库的更新将导致程序的重新编译
b) 程序运行时将会载入内存,有可能出现同一个库,被不同的程序同时载入内存,即浪费了内存,又影响cpu cache
c) 不能在运行时动态更新库
d) 静态编译后的程序copy到不同环境可能会出问题,但如果底层库使用动态库,则能规避环境不同造成的影响

10、动态库默认查找路径 /etc/ld.so.conf,也可使用环境变量LD_LIBRARY_PATH,注意环境变量LD_LIBRARY_PATH的优先级要更高一些

11、为什么动态库的接口一般都要导出成C的格式?因为gcc和g++符号表不同的缘故,如果没有使用extern "C"导出,则在使用dlsym接口载入接口实现时,需要提供非常奇怪的符号名(由g++产生)。

12、/usr/sbin/lsof -p pid 可以查看到由pid在运行期所载入的所有共享库

13、dlopen的RTLD_GLOBAL参数的作用:
我们有一个main.cpp ,调用了两个动态 libA, 和 libB, 假设A中有一个对外接口叫做 testA, 在main.cpp可以通过dlsym获取到testA的指针,进行使用.但是对于libB 中的接口,它是看到不libA的接口,使用testA 是不能调用到libA中的testA的,但是如果在dlopen 打开libA.so的时候,设置了RTLD_GLOBAL这个选项,就可以把libA.so中的接口升级为全局可见, 这样在libB中就可以直接调用libA中的testA,如果在多个共享库都有相同的符号,并且有RTLD_GLOBAL选项,那么会优先选择第一个。

另外这里注意到一个问题, RTLD_GLOBAL使的动态库之间的对外接口是可见的,但是动态库是不能调用主程序中的全局符号,为了解决这个问题, gcc引入了一个参数-rdynamic,在编译载入共享库的可执行程序的时候最后在链接的时候加上-rdynamic,会把可执行文件中所有的符号变成全局可见,对于这个可执行程序而言,它载入的动态库在运行中可以直接调用主程序中的全局符号,而且如果共享库(自己或者另外的共享库 RTLD_GLOBAL) 加中有同名的符号,会选择可执行文件中使用的符号,这在一些情况下可能会带来一些莫名其妙的运行错误。 

14、使用namespace只能在编译期隐藏接口,动态库仍可以通过符号名的暴力方式获取入口指针,但static的接口无法通过这种方式获得。

15、-fPIC参数:Position Independent Code
这里首先先说明一下装载时重定位的问题,一个程序如果没有用到任何动态库,那么由于已经知道了所有的代码,那么装载器在把程序载入内存的过程中就可以直接安装静态库在链接的时候定好的代码段位置直接加载进内存中的对应位置就可以了。但是在面对动态的库的时候,这种方式就不行了。假设需要载入共享库A,但是在编译链接的时候使用的共享库和最后运行的不一定是同一个库,在编译期就没办法知道具体的库长度,在链接的时候就没办法确定它或者其他动态库的具体位置。另一个方面动态库中也会用到一些全局的符号,这些符号可能是来自其他的动态库,这在编译器是没办法假设的(如果可以假设那就全是静态库了)

基于上面的原因,就要求在载入动态库的时候对于使用到的符号地址实现重定位。在实现上在编译链接的时候不做重定位操作,地址都采用相对地址,一但到了需要载入的时候,根据相对地址的偏移计算出最后的绝对地址载入内存中。

但是这种采用装载时重定位的方式存在一个问题就是相同的库代码(不包括数据部分)不能在多个进程间共享(每个代码都放到了它自己的进程空间中),这个失去了动态库节省内存的优势。

为了解决这个问题,ELF中的做法是在数据段中建立一个指向那些需要被使用(内部的位置无关简单采用相对地址访问就可以实现)的地址列表(也被称为全局偏移表,Global offset table, GOT). 可以通过GOT相对应的位置进行间接引用. 

在gcc的手册中我们可以看到一个-fpic(区别在一个大写一个小写)的参数,从功能上来说它们都是一样的。-fpic在一些特定的环境中(包括硬件环境)可以有针对性的进行优化,产生更小更快的代码, 但是由于受到平台的限制,像我们的编译环境,开发环境,运行环境都不完全统一的情况下面使用fpic有一定未知的风险,所有决大多数情况下我们使用 -fPIC来产生地址无关代码。

16、在Linux环境下,很多时候都是同时存在.so和.a,此时连接时使用-l -L的话,链接器会默认选择动态库进行链接。如果需要使用静态库,一个方法是使用path/lib.a的方式;或者使用--static参数。注意,如果使用--static,但只有.so库没有同名的.a库,编译和链接时不会有错,但在运行时可能出现错误,如:/lib/ld64.so.1: bad ELF interpreter

17、使用--static参数有以下几个问题:
a) 如glibc中getservbyport_r这样的接口需要动态库支持才能运行,静态编译会导致运行时错误。
b) 第三方工具不友好,类似valgrind检查内存泄露为了不在一些特殊的情况下误报(最典型的就是strlen可以参考valgrind的 wikiValgrind运行的程序不能够使用-static来进行链接中的case3), 它需要用动态库的方式替换glibc中的函数,如果静态编译那么valgrind就无法替换这些函数,产生误报甚至无法报错. tcmalloc在这种情况下也不能支持.
c) --static之后会导致代码变大,对cpu代码cache不友好,浪费内存。

18、使用链接参数 -dn -dy参数来控制使用的是动态库还是静态库,-dn表示后面使用的是静态库,-dy表示后面使用的是动态库,例:
g++ -Lpath -Wl,-dn -lx -Wl,-dy -lpthread

没有评论: