Linux中的额动态库和静态库(.a/.la/.so/.o)
2017-12-24
概述
在windows下,一般可以通过文件的后缀名来识别类型。Linux下大致也可以,但是要明确的是,在Linux下文件的后缀与文件的类型是没有必然的联系,这只是约定俗称的习惯罢了。
在Linux下进行C/C++开发,一般都是用gcc编译器,所以本文讲解的是以gcc为主。
-
.o文件,即目标文件,一般通过.c或是.cpp文件编译而来,相当于VC编译出来的obj文件
-
.so文件,shared object共享库(对象),相当于windows下的dll
-
.a文件,archive归档包,即静态库。其实质是多个.o文件打包的结果,相当于VC下的.lib文件
-
.la文件,libtool archive文件,是libtool自动生成的共享库文件
C/C++程序编译过程
C/C++编译的过程:
-
预处理,展开头文件,宏定义,条件编译处理等。通过gcc -E source.c -o source.i 或是cppsource.c生成
-
编译。这里是一个下一的编译意义,指的是将预处理后的文件翻译成汇编代码的过程。通过gcc -S source.i生成,默认生成source.s文件。
-
汇编。汇编即将上一步生成的汇编代码翻译成对应的二进制机器码的过程。通过gcc -c source.s来生成source.o文件
-
链接。链接是将生成目标文件和其引用的各种符号等生成一个完整的可执行程序的过程。链接的时候会进行虚拟内存的重定向操作。
上面四个步骤就是C/C++程序编译的几个基本步骤。前面三个步骤都很简单,大多时候会合并为一个步骤。只有第四个步骤链接比较复杂。很多时候我们编译比较大的项目,报错的往往是链接的时候缺少某些库,或是某些符号找不到定义、重定义等。
.o文件(目标文件)
.o文件是C/C++源码编译的结果,即上面所说的C/C++编译过程的前三步。一般开发中很少将这三步分开来做,通常的做法是一步生成。
- 举例如下:
//cat atoi.h
#ifndef ATOI_H
#define ATOI_H
int atoi(const char *str);
#endif
//atoi.c
#include <stdio.h>
#include "atoi.h"
int atoi(const char *str) {
int ret = 0;
if(str != NULL) {
sscanf(str, "%d", &ret);
}
return ret;
}
- 创建atoi.o
直接使用命令gcc -c atoi.c -o atoi.o或是gcc -c atoi.c来生成目标文件。
上面我们函数中调用了sscanf这个C标准库中的函数,那么它在.o文件中会记录这个存在,我们可以使用readelf工具查看
.a文件(静态库文件)
静态库是多个.o文件的打包结果,前面已经说过,其实不一定非要多个文件,一个.o文件也可以打包成.a文件。这一步使用ar工具来操作。
- 创建atoi.a文件
使用命令:ar -r atoi.a atoi.o进行创建,其中-r表示替换已经存在的或插入新的文件到archive包中。
- 使用atoi.a
创建了atoi.a文件后,我们就可以使用它了,我们写一个main函数来调用atoi。
//main.c
#include <stdio.h>
#include "atoi.h"
int main() {
return atoi("5");
}
编译连接:
gcc -c main.c
ld main.o atoi.a -omain
上面报了一个错误,原因是atoi函数中使用未定义的引用__isoc99_sscanf,这个问题我们可以通过链接libc.a或者libc.so来解决这个问题,如果使用glibc的静态库,那么你也必须将你的程序开源,不然这应该算违反GPL协议的约定。
ld main.o atoi.a /lib64/libc.so.6 -o main
./main
上面也报了错误,正确的做法是链接上crt0.o、crti.o、crtn.o等多个文件就可以,不同的机器,需要链接的文件的位置可能不一样。这个参数可能非常长,普通人记不住,这个怎么可以得到呢?gcc编译环境知道,我们可以使用gcc来获取。
gcc -v -o main main.o atoi.a
编译之后,运行程序可以正常运行。
.so文件(共享库文件)
共享库和windows下的dll文件的概念是一样的,都是程序运行时进行链接,供程序调用的。在Linux下可以使用ldd命令来看某个可执行文件需要链接哪些共享库,并可以确定这些要链接的共享库的共享库在本机中的位置。
这里要说下动态库的查找路径。对于程序需要动态库xxx.so,如果它在当前目录下有,那么链接当前目录下的;如果没有,则连接系统/etc/ld.so.cache(可通过ldconfig更新)文件中的xxx.so的路径,如果没有,就会报错。
其实在链接的时候,我们可以通过-Wl,-rpath=sopath来指定运行时加载动态库的路径。这样做的好处是可以把一些动态库的位置信息不加入到/etc/ld.so.cache中,已经避免和系统已有和系统已有的动态库产生冲突。
注:-Wl 表示后面的参数将传递给link程序ld,gcc编译时的链接实际上是调用ld进行的。
- 创建atoi.a
这里还是使用前面创建的atoi.c文件创建atoi.so文件。实际上我们这里创建atoi.so.1文件,文件名后面的.1代表的是版本号。动态库因为使用的时候是动态链接的,而不是直接连接到目标程序文件中的。所以可能同时存在多个版本的情况,一般都会指定版本号。通常使用libxxx.so.主版本号.副版本号 的形式命名。
gcc -shared -o atoi.so.1 atoi.c
gcc -fPIC -shared -o atoi.so.1 atoi.c
-shard该选项指定生成动态链接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号,后面介绍nm工具的时候再说),不用该标志外部程序无法连接,相当于一个可执行文件。
-fPIC表示便以为位置独立的代码,不同此选项的话编译后的代码是位置相关的,所以动态载入时是通过代码拷贝的方式来满足不同进程需要,而不能达到代码段共享的目的。
第一次没有指定-fPIC的时候出错,原因是针对可迁移R_X86_64_32平台,只读数据段.rodata不能创建共享对象,原因是在动态链接动态库的时候,如果没有贬义成位置无关的代码,那么链接的时候可能因为某些代码的位置相关性,而在执行时出现错误。可执行文件在链接时就知道每一行代码、每一个变量会被放到线性地址空间的什么位置,incident这些地址可以都作为常数写到代码里面。对于动态库,只有加载的时候才知道。
如果代码中没有只读数据段,那么就不会有这个问题,例如
cat > val.c
int a = 100;
gcc -shared -o val.so val.c
- 使用atoi.so
使用.so文件的形式和使用.a也差不多,也是使用ld来进行链接。因为这个过于复杂,还是使用gcc来做这个操作(实际上gcc也是使用ld)。
gcc -o main main.o atoi.so.1
ldd main
./main
上面执行的时候报错,意思是找不到atoi.so.1这个文件。原因是共享库的查找目录没有当前目录,我们可以添加环境变量LD_LIBRARY_PATH来使系统动态载入器(dynamic linker/loader)在当前目录页查找。
export LD_LIBRARY_PATH=.
ldd main
./main
还有一种办法,比添加环境变量更好用,也更具有可移植性,那就是编译的时候指定运行的时候共享库的加载路径。gcc使用Wl,-rpath=sopath来指定,其中sopath是共享库防止的路径(可以是绝对路径也可以是相对路径)。
gcc -o main main.o -Wl,-rpath=. atoi.so.1
ldd main
./main
.la文件(libtool archive)
libtool是GNU的一个用来解决各个平台创建动态/静态库文件的不同操作的过于复杂的工具。它提供了使用抽象的结构进行动态/静态库的方法。
使用GNU Libtool可以很容易的在不同系统中建立动态链接库。它公国一个成为Libtool库的抽象,隐藏了不同系统之间的差异,给开发人员提供了一致的接口。对于大部分情况,开发人员甚至不用查看相应的系统手册,只要掌握GNU Libtool的用法就可以了。并且,使用Libtool的Makefile也只需要编写一次就可以在不同系统上使用。
- libtool的使用一般分为以下几个步骤
1 创建Libtool对象文件
2 创建Libtool库
3 安装Libtool库
4 使用Libtool库
5 卸载Libtool库
1 创建Libtool对象
ls
libtool --mode=compile gcc -c atoi.c
ls -aR
创建libtool对象文件的过程,实际上是生成.o、.so、.a的过程,同事海生成了一个.lo文件。.lo文件里描述了两个.o文件的路径。这一步已经生成了相应的动态库和静态库。
2 创建Libtool库
libtool --mode=link gcc -o libatoi.la atoi.lo -rpath /home/lib -lc
ls -aR
注意,这里使用atoi.lo作为输入文件,并指定生成的目标文件为libatoi.la。
-rpath选项是指定Libtool将这个库安装到的位置,如果省略了-rpath选项,那么不生成动态链接库。
因为在atoi函数中使用了标准C库函数sscanf,所以带上-lc选项,Libtool会记住这个依赖关系,后续在使用我们的库的时候自动将依赖的库链接进来。
可以看到这次在当前目录下生成了libatoi.la文件,而.libs目录下的那个是一个符号链接,指向当前目录下的这个文件。这其实是一个文本文件,里面内容很长。
3 安装Libtool库
libtool --mode=install install -c libatoi.la /home/lib
上述操作报了warning,提示我们运行libtool –finish /home/lib,这个操作是使用libtool进行一个正确的配置环境变量LD_LIBRARY_PATH、LD_RUN_PATH等的过程。如果不能正常的使用安装好的库,运行这个命令。
4 使用Libtool库
libtool --mode=compile gcc -c main.c
libtool --mode=link gcc -o main main.lo /home/lib/libatoi.la
./main
上面的操作默认使用了动态库,可以使用-static-libtool-libs选项来指定使用静态库
libtool --mode=link gcc -o main main.lo /home/lib/libatoi.la -static-libtool-libs
卸载Libtool库
libtool --mode=uninstall rm /home/lib