分享

Linux静态链接(库)、动态链接(库)、可执行文件加载相关问题(创建、选项、环境变量等)

 北漂之邬 2014-03-17


参考:

http://www.cnblogs.com/hanyan225/archive/2010/10/01/1839906.html

http://www./info/html/wangzhanyunying/jianzhanjingyan/20080417/70218.html

http://www.cnblogs.com/lidp/archive/2009/12/02/1697479.html

GNU Binutils:http://en./wiki/GNU_Binutils


Linux静态链接(库)、动态链接(库)、可执行文件加载相关问题(创建、选项、环境变量等)

(1) 概念
函数库一般分为两种,静态链接库(static linking lib)和动态链接库(dynamic linking lib),无论是windows还是linux,都是如此,当然,其组织形式和调用方式可能会有所不同,这里只针对Linux来说明。那么,动态链接库和静态链接库都是编译得到的,所以,编译器得到静态链接库的链接过程就是静态链接,动态链接同理。
动态链接库一般是.so为后缀,静态链接库以.a为后缀。
为了在同一系统中使用不同版本的库,可以在库文件名后加上版本号为后缀,但由于程序连接默认以.so为文件后缀名。所以为了使用这些库,通常使用建立符号连接的方式。如下:
ln -s libtest.so.1.0 libtest.so


(2) 静态链接库和动态链接库的使用区别
首先,回顾一点,一个程序,从源代码到运行,包括:编译(compile)、链接(link)、加载(load)、运行(execute),对应的GNU工具一般为:编译器compiler(gcc)、链接器linker(ld)、加载器loader(其中动态链接库加载器为ld.so(ld-linux.version.so),在/lib目录中,如 /lib/ld-linux.so.2,所以不能直接在命令行下运行ld-linux.so,需要完整路径,加载器一般不需要我们直接运行,在运行可执行程序的加载过程中包含有动态加载的过程)。经常会将编译和链接统称为编译,期间为编译时(compile time);而加载和运行统称为运行,期间为运行时或执行时(runtime/execution time)。
静态链接库:编译时(compile time)被使用(更详细的是链接的时候)。在链接静态库的时候,链接器会在其中找到所需要链接的函数,然后将它们拷贝到执行文件,这种拷贝是完整的拷贝,所以在链接成功后,程序运行不需要静态库的参与。
动态链接库:编译时和运行时都被使用。在编译时,链接器在其中找到所需要的函数(或其他对象文件),生成地址/位置无关代码(Position Independent Code (PIC)),并没有真正的实现拷贝;在运行时(runtime/execution-time),某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了,如果有,则让其共享那一个拷贝;只有没有才链接载入。
说明:Linux下进行链接的缺省操作是先考虑动态链接库,即如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。


(3) 如何生成静态库和动态库和nm命令了解
代码如下(这里test1和test2都是foo函数,打印的内容不一样,下面的内容会使用这两个链接库来学习其他内容):
  1. // File: test1.cpp  
  2. #include <stdio.h>  
  3.   
  4. void foo()  
  5. {  
  6.     printf("This is foo in test1\n");  
  7. }  
  1. // File: test1.h  
  2. #ifndef TEST1_H  
  3. #define TEST1_H  
  4.   
  5. void foo();  
  6.   
  7. #endif  
  1. // File: test2.cpp  
  2. #include <stdio.h>  
  3.   
  4. void foo()  
  5. {  
  6.     printf("This is foo in test2\n");  
  7. }  
  1. // File: test2.h  
  2. #ifndef TEST2_H  
  3. #define TEST2_H  
  4.   
  5. void foo();  
  6.   
  7. #endif  


1. 静态链接.a:

首先,先编译得到中间文件.o:

  1. #ls  
  2. test1.cpp  test1.h  test2.cpp  test2.h  
  3. #gcc -c test1.cpp -o test1.o  
  4. #gcc -c test2.cpp -o test2.o  
  5. #ls  
  6. test1.cpp  test1.h  test1.o  test2.cpp  test2.h  test2.o  
  7. #  
然后,利用ar命令打包得到.a文件:

  1. #ar crv libtest1.a test1.o  
  2. a - test1.o  
  3. #ls  
  4. libtest1.a  test1.cpp  test1.h  test1.o  test2.cpp  test2.h  test2.o  

(libtest2.a可以同理得到,说明:这里是由一个.o得到一个.a的情况)

命令:ar

说明:对于需要将多个.o打包成一个.a的情况,可以直接在后面列举所有的.o文件即可。另外,ar命令有很多选项,对于.a已经存在,往其中插入模块(.o),如何处理已经存在的.o等等其它的处理情况,这里不详细讨论。一般的使用是crv选项,c表示不管.a是否已经存在创建新的.a文件,r表示在库中插入模块(替换)。当插入的模块名已在库中存在,则替换同名的模块。假如若干模块中有一个模块在库中不存在,ar显示一个错误消息,并不替换其他同名模块。默认的情况下,新的成员增加在库的结尾处,能够使用其他任选项来改变增加的位置。v表示显示详细操作信息。具体ar的使用参考:http://www./info/html/wangzhanyunying/jianzhanjingyan/20080417/70218.html

关于nm命令,用来列出目标文档的符号清单,不仅仅适用于.a,也用于.so等。也不更多说明,一般常用的就是nm file或者nm -o file(信息会更详细),如下:

  1. #ar crv libtest1.a test1.o  
  2. a - test1.o  
  3. #ar crv libtest2.a test2.o  
  4. a - test2.o  
  5. #ar crv libtest12.a test1.o test2.o   
  6. a - test1.o  
  7. a - test2.o  
  8. #nm libtest1.a   
  9.   
  10. test1.o:  
  11. 0000000000000000 T _Z3foov  
  12.                  U puts  
  13. #nm libtest2.a   
  14.   
  15. test2.o:  
  16. 0000000000000000 T _Z3foov  
  17.                  U puts  
  18. #nm libtest12.a   
  19.   
  20. test1.o:  
  21. 0000000000000000 T _Z3foov  
  22.                  U puts  
  23.   
  24. test2.o:  
  25. 0000000000000000 T _Z3foov  
  26.                  U puts  
  27. #  

2. 动态链接得到.so:
首先,使用gcc的-fPIC选项编译得到PIC(位置无关代码)的中间文件。

然后,使用gcc的-shared选项对.o文件进行链接。

如下:

  1. #gcc -c -fPIC test1.cpp -o test1.o  
  2. #gcc -c -fPIC test2.cpp -o test2.o  
  3. #gcc -shared test1.o -o libtest1.so  
  4. #gcc -shared test2.o -o libtest2.so  
  5. #gcc -shared test1.o test2.o -o libtest12.so  
  6. test2.o: In function `foo()':  
  7. test2.cpp:(.text+0x0): multiple definition of `foo()'  
  8. test1.o:test1.cpp:(.text+0x0): first defined here  
  9. collect2: ld 返回 1  
  10. #  
(从这里也可以看到,gcc生成动态链接库会检查是否有重复定义的情况,上面的静态打包ar命令就不会检查。)

说明:必须使用-fPIC选项编译,否则使用-shared生成动态链接库的时候会出现链接错误。当然,上面是编译和链接分开的,也是可以一起完成,就不需要-c选项了,如gcc -fPIC -shared test.c -o libtest.so等。

关于nm在查看.so时候常见的使用方式:

上面有关于nm用于查看.a文件的说明,对于.so,通常使用下面的选项查看:

(参考:http://blog.sina.com.cn/s/blog_5dc87df60100bike.html 

http://www./noflybird/archive/2010/05/06/114618.aspx

 http://apps.hi.baidu.com/share/detail/20625788

-C:用C/C++的方式显示函数名(函数名在编译后会稍微改变一下,demangle和mangle)。

-g:仅显示外部符号。

我一般使用如下来查看动态链接库里面定义的函数(也可以用这个查看.a更好):

  1. nm -C -g libtest1.so -A  
或者进一步用T过滤结果:

  1. nm -C -g libtest1.so -A | grep T  
如下:

  1. #nm -C -g libtest1.so -A | grep T  
  2. libtest1.so:000000000000057c T foo()  
  3. libtest1.so:00000000000005cc T _fini  
  4. libtest1.so:0000000000000460 T _init  


总结:

1. 生成静态链接库的方式:先gcc编译得到.o文件,使用ar命令打包.o文件即可(ar crv xxx.a xxx.o xxx1.o xxx2.o...)。

2. 生成动态链接库的方式:用gcc -fPIC编译得到.o文件,然后用-shared选项进行链接即可。

3. 查看静态链接库和动态链接库的符号文件:nm,也可以查看其它文件的符号文件。比如:nm -C -g libtest1.so/libtest1.a -A | grep T的形式。

4. 经过上面的折腾,得到了如下的文件(下面会用到):

  1. #ls  
  2. libtest12.a  libtest1.a  libtest1.so  libtest2.a  libtest2.so  test1.h  test2.h  
  3. #  

(4) 静态链接库和动态链接库的“原始”使用

测试程序:

  1. // File: main.cpp  
  2. #include "test1.h"  
  3.   
  4. int main()  
  5. {  
  6.     foo();  
  7.     return 0;  
  8. }  
1、和静态库链接:

直接参与链接即可:

  1. #ls  
  2. libtest12.a  libtest1.a  libtest2.a  main.cpp  test1.h  test2.h  
  3. #gcc main.cpp libtest1.a   
  4. #./a.out   
  5. This is foo in test1  
  6. #  
当然,也可以把编译和链接分开,那么就先gcc -c main.cpp -o main.o得到main.o,然后gcc main.o libtest1.a即可。

这是最简单的和静态库链接的方式,直接作为gcc的输入参数(输入文件)参与链接。更复杂的情况是,要链接的库不在当前目录下,那么当然,需要指定文件的路径(相对路径或绝对路径)如下:

  1. #ls  
  2. lib  main.cpp  test1.h  test2.h  
  3. #gcc main.cpp ./lib/libtest1.a  
  4. #./a.out   
  5. This is foo in test1  
  6. #  

2、和动态库链接:

直接参与链接即可,和静态链接库一样,gcc xxx.cpp xxx.so libs/abc/xxx.so.....,将要链接的库作为输入文件参数传递给gcc即可。

  1. #ls  
  2. lib  main.cpp  test1.h  test2.h  
  3. #gcc main.cpp lib/libtest1.so   
  4. #./a.out   
  5. This is foo in test1  
  6. #  


总结:上面这是最“原始"的方式连链接静态库和动态库,即把库文件作为gcc的输入文件,规则当然和源文件和目标文件类似了,不管是使用相对路径还是绝对路径,总之是必须能让gcc直接找到的。对于这种方式,使用-L或者把libtest1.so复制到/lib、/usr/lib这些方式,都不能简化为"gcc main.cpp libtest1.so”(当然是libtest1.so不在当前目录的情况下)。说明:这一点是很基础的,但是时间久了反而犯迷糊了,以为-L也能对这种最原始的情况起到作用(gcc main.cpp libtest1.so -L./lib,其中libtest1.so在lib文件夹中)。


(5) 使用-l和-L链接静态库(-static)和动态库

上面的最原始的方式链接到动态库的方式有一个缺点在于:需要指定要链接的库的相对或绝对路径。那么,一个情况是,如果我们的程序需要链接到一些库,比如linux提供了很多库如pthread库等,如果需要链接到它们,当然,使用完整的路径是可以的。如下:

  1. #gcc /lib64/libpthread.so.0 main.cpp lib/libtest1.so   
  2. #  
这样的缺点是很明显的。为了改进,gcc提供了-l的方式来简化这个问题,在gcc选项中使用-lxxxx指定要链接的库的库名,对应的库名为libxxx.so或libxxx.so,同时,这个库是存在于“系统库目录”中。对于Linux而言,系统库目录默认为/lib和/usr/lib(或者/lib64和/usr/lib64,这里不讨论关于32和64位的问题)。另外,-L选项则用于将一个指定的目录加入到”系统库“的范畴,其实就是加入到链接器对库的搜索路径了。

所以,对于上面的例子,可以:

A、把libtest1.so复制到/lib中或者/usr/lib中,就可以直接使用:gcc -ltest1 main.cpp编译了。

B、如果libtest1.so不在搜索路径(/lib和/usr/lib中),那么编译会得到类似于:/usr/bin/ld: cannot find -ltest1的错误。可以使用-L将指定的目录加入到搜索路径,如:

  1. #ls  
  2. lib  main.cpp  test1.h  test2.h  
  3. #gcc -ltest1 main.cpp -Llib  
  4. #gcc -ltest1 main.cpp -L./lib  
  5. #  

至于静态库,其规则和上面的一样,同样有"系统库目录"和-l的简写以及-L添加搜索路径。但是,默认情况下,gcc是进行动态链接的。如果需要使用静态链接,需要使用-static选项,使用了-static选项后,所有的-l指定的库都会去搜索路径中查找对应的.a静态库。而且,需要注意的是,-static也会改变gcc的默认链接的库的链接方式,也会链接静态库(关于gcc默认链接的库,比如libc等,默认都是动态库的,使用了-static后,会链接到libc.a静态库,关于哪些库是gcc的默认链接库下面会介绍)。


(6) gcc的-include和-I参数

这个内容本来和这里的内容无关,但是一般总是和-L一起讨论,所以就这里说明一下。-include用于包含头文件,功能和#include一样,一般很少用,一般都是在代码里用#include了。-I用于指定额外的“系统头文件”的搜索路径,对应于-L选项类似的功能。

对于使用<>的#include,头文件会去系统头文件(/usr/include)搜索,如果某些头文件不在系统头文件路径中,就需要使用-I指定了。

PS:-I和-L都可以在一个命令行下指定多个,增加多个路径,如gcc -lxxx foo.cpp -Llib1/ -Llib2/ -Llib3/等。


(7) 可执行文件的加载和运行和ldd命令

上面的内容都是关于如何编译和链接的问题,得到可执行文件,事实上,上面的例子中,有些可执行文件是不能执行的。

对于链接静态库的程序,由于静态链接会将代码直接拷贝到可执行文件中,链接后的可执行文件是可以不依赖于静态库.a直接运行的,所以问题主要是关于使用了动态库的程序的运行问题。对于使用动态库的程序,在程序加载过程中,有一个重要的一步是动态库的加载(有时候也说成动态库的动态链接),loader为/lib/ld-linux.so(参考http://blog.csdn.net/gengshenghong/article/details/7096511的(3)理解)。

 动态库的搜索路径搜索的先后顺序是:
1.编译目标代码时指定的动态库搜索路径;
2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
3.配置文件/etc/ld.so.conf中指定的动态库搜索路径;//只需在在该文件中追加一行库所在的完整路径如"/root/test/conf/lib"即可,然后ldconfig是修改生效。
4.默认的动态库搜索路径/lib;
5.默认的动态库搜索路径/usr/lib。

先看下面的例子,说明一个问题:

  1. #tree  
  2. .  
  3. ├── lib  
  4. │   ├── libtest1.so  
  5. │   └── libtest2.so  
  6. ├── libtest1.so  
  7. ├── libtest2.so  
  8. ├── main.cpp  
  9. ├── test1.h  
  10. └── test2.h  
  11.   
  12. 1 directory, 7 files  
  13. #gcc main.cpp libtest1.so   
  14. #./a.out   
  15. ./a.out: error while loading shared libraries: libtest1.so: cannot open shared object file: No such file or directory  
  16. #gcc main.cpp ./libtest1.so   
  17. #./a.out   
  18. This is foo in test1  
  19. #gcc main.cpp lib/libtest1.so   
  20. #./a.out   
  21. This is foo in test1  
  22. #gcc main.cpp /home/sgeng2/labs_temp/temp/libtest1.so   
  23. #./a.out   
  24. This is foo in test1  
这里,使用gcc main.cpp libtest1.so编译成功了,居然运行说找不到共享库,使用./就可以了。当然,对于上面的任意一种情况,链接后删除了.so肯定也是运行不了的。那么,如果是下面的情况呢:

  1. #ls  
  2. lib  libtest1.so  libtest2.so  main.cpp  test1.h  test2.h  
  3. #gcc main.cpp ./libtest1.so   
  4. #./a.out   
  5. This is foo in test1  
  6. #../temp/a.out   
  7. This is foo in test1  
  8. #cd ..  
  9. #temp/a.out   
  10. temp/a.out: error while loading shared libraries: ./libtest1.so: cannot open shared object file: No such file or directory  
  11. #  
会发现,使用gcc mai.cpp ./libtest1.so编译后,运行时,这个"./libtest1.so“的信息是保存了的,所以,只能在当前目录运行a.out,如果cd切换到其它目录,那么它仍然是去寻找./libtest1.so,自然就找不到了。那么,如果这里使用的是lib/libtest1.so链接,也是一样的,运行的时候,当前工作目录下需要有一个lib/libtest1.so的文件,如果使用的是绝对路径,那么当然,只要.so没有删除,都是可以运行的。可见,这种最“原始"的链接方式,它还是很傻的,局限性比较大。

总结:这种使用"原始"的方式来链接得到可执行文件,.so的路径信息是保存在可执行文件中的,在加载的时候需要查找对应的.so是否存在。上面的提到了"动态库的搜索路径的搜索顺序",这是用于使用-l和-L来链接的动态链接库的情况,对于这里的"原始"的方式并无改变,比如使用gcc main.cpp ./libtest1.so编译后,哪怕将libtest1.so复制到了/usr/lib中也没用。(个人理解,这种原始的指定路径的编译方式,应该属于"编译目标代码时指定的动态库搜索路径”,当然,一般说到这个编译目标代码时指定的动态库搜索路径,是指另外一个选项,下一部分说明)。

ldd命令:ldd命令用于显示一个可执行文件中依赖的所有.so在当前系统中的位置,简单理解,就是它会模拟可执行文件的加载,输出各个.so文件的路径,比如上面的所有路径的顺序,在不同的路径中放入.so,ldd得到的路径会显示实际loader查找到的路径。如果找不到.so或者.so有问题,ldd也会报错。


(8) 编译目标代码时指定的动态库搜索路径"-Wl,-rpath,"

gcc提供了-Wl,-rpath,xxx来指定程序运行时候搜索动态库的路径,和-L没有任何关系,-Wl,-rpath是在编译时指定,但是其用于运行时的loader,所以是一个比较特殊的选项,容易和-L混淆。说明:-Wl,-rpath,xxx指定的路径,如果使用的是相对路径,那么运行的时候,也是根据相对路径来查找.so,所以其实和上面说的"原始"方式进行链接,应该本质上是一回事。

这个选项可以指定多个路径,用":"分开即可。如:gcc -Wl,-rpath,path1:path2:path3 main.cpp -ltest -Llib/等。


总结:

1. 对于静态库,可以使用"原始"方式(指定路径作为gcc输入参数)链接静态库,也可以使用-l&&-L的方式,但是,需要使用-static选项。另外,默认的gcc链接方式是动态的,所以使用了-static,会让gcc默认链接的库也链接静态版本。

2. 对于使用"原始"方式(指定路径作为gcc输入参数)链接动态库的情况,运行时候限制太多,必须是怎么编译链接的,就要保证运行时候按照当时的路径能找到动态链接库,当然,这种方式很少用,一般都是用-l&&-L来链接动态链接库。

3. 对于使用-l来链接.so的情况,编译器会到/lib,/usr/lib和-L指定的路径下查找-l指定的.so文件来进行链接。搜索先后顺序为:-L指定的路径;/usr/lib;/lib。

4. 对于使用-l链接.so的情况,在运行的时候。会根据相应的路径搜索.so文件来进行加载运行,加载的时候,动态库的搜索路径搜索的先后顺序是:编译目标代码时指定的动态库搜索路径;环境变量LD_LIBRARY_PATH指定的动态库搜索路径;配置文件/etc/ld.so.conf中指定的动态库搜索路径;默认的动态库搜索路径/lib;默认的动态库搜索路径/usr/lib。

5. -Wl,-rpath用于编译目标代码时指定的动态库搜索路径,和-L没有关系,一个指定的路径用于运行时加载.so的搜索,一个指定的路径用于编译时链接.so中符号的搜索。


(9) 链接多个库的问题(多个库都有同一函数的定义)

比如:两个.a文件都定义了函数foo,然后都参与链接,实际使用的是哪一个?如果是两个.so呢?一个.a和一个.so呢?


(10) 补充:

1. 关于搜索路径

上面已经总结了相关的头文件和库文件的搜索路径问题,需要说明的是,实际的GCC的搜索路径还可能会和其他因素有关,比如一些环境变量的设置,还有gcc会将自己的安装目录下的lib文件夹或者include文件夹等等加入搜索路径,包括可以通过sysroot等选项改变默认的设置,具体参考http://blog.csdn.net/gengshenghong/article/details/7108634的总结。当然,为了更好的了解这些信息,gcc也提供了几个相关命令来查看相关信息。

gcc -print-search-dirs:打印安装、程序和库的搜索路径。输出示例如下:

  1. #gcc --print-search-dirs  
  2. 安装:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/  
  3. 程序:=/usr/libexec/gcc/x86_64-redhat-linux/4.6.0/:/usr/libexec/gcc/x86_64-redhat-linux/4.6.0/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/:/usr/lib/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../x86_64-redhat-linux/bin/x86_64-redhat-linux/4.6.0/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../x86_64-redhat-linux/bin/  
  4. 库:=/usr/lib/gcc/x86_64-redhat-linux/4.6.0/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../x86_64-redhat-linux/lib/x86_64-redhat-linux/4.6.0/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../x86_64-redhat-linux/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../x86_64-redhat-linux/4.6.0/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../lib64/:/lib/x86_64-redhat-linux/4.6.0/:/lib/../lib64/:/usr/lib/x86_64-redhat-linux/4.6.0/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../../x86_64-redhat-linux/lib/:/usr/lib/gcc/x86_64-redhat-linux/4.6.0/../../../:/lib/:/usr/lib/  
  5. #  
gcc -v file.c:显示编译file.c时候的详细信息(其中会显示一些搜索头文件的顺序的信息和LIBRARY_PATH、COMPILER_PATH等等。也可以直接gcc -v,显示的就是gcc的基本配置信息。

gcc -dumpspecs:显示所有内建 spec 字符串。

gcc -dM -E file.c:显示预处理file.c中所有的宏定义。

总之,gcc搜索路径还是有些复杂的,不需要所有细节全部记清楚,但是至少要知道会有哪些内容需要被考虑。


2. GCC的--sysroot选项

这个选项一般用于交叉编译,用于指定编译所需要的头文件,及链接库(的root目录)等。


3. GCC环境变量和配置选项等

http://www./bbs/thread304996.html

http://blog.csdn.net/yangruibao/article/details/6869323

http://jimobit.blog.163.com/blog/static/28325778200991524826935/


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多