Linux中的额动态库和静态库(.a/.la/.so/.o)

概述

在windows下,一般可以通过文件的后缀名来识别类型。Linux下大致也可以,但是要明确的是,在Linux下文件的后缀与文件的类型是没有必然的联系,这只是约定俗称的习惯罢了。

在Linux下进行C/C++开发,一般都是用gcc编译器,所以本文讲解的是以gcc为主。

C/C++程序编译过程

C/C++编译的过程:

上面四个步骤就是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;
}

直接使用命令gcc -c atoi.c -o atoi.o或是gcc -c atoi.c来生成目标文件。

上面我们函数中调用了sscanf这个C标准库中的函数,那么它在.o文件中会记录这个存在,我们可以使用readelf工具查看

.a文件(静态库文件)

静态库是多个.o文件的打包结果,前面已经说过,其实不一定非要多个文件,一个.o文件也可以打包成.a文件。这一步使用ar工具来操作。

使用命令:ar -r atoi.a atoi.o进行创建,其中-r表示替换已经存在的或插入新的文件到archive包中。

创建了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.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

使用.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也只需要编写一次就可以在不同系统上使用。

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

参考