linux中的静态库、动态库详解
要说linux中的库使用,首先还得从gcc的编译流程说起。不然,总是说不清,理还乱,读完似懂非懂不如不看。
可执行文件的编译过程
在使用gcc
编译程序时,编译过程可以被细分为4个阶段:
- 预处理(Pre-Processing):
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
而我们通常使用gcc -o hello hello.c
编译程序实际是包含了(hello.c->hello.i–>hello.s–>hello.o–>hello)上边的4个步骤
- 关于预处理,读者可以使用
gcc -E -o hello.i hello.c
预处理后的文件. gcc -S -o hello.s hello.i
生成汇编文件gcc -c -o hello.o hello.s
生成机器码.尽管已经得到了机器码,但这个文件却还是不可以运行的,必须要经过链接才能运行- gcc hello.o。得到可执行文件。
由于链接与库的生成和使用相关,所以链接
是我们要讨论的重点。
在第三阶段汇编
中生成的二进制文件main.o我们通常叫它可重定位文件
.而在第四阶段生成的二进制文件main
我们叫它可执行目标文件
。一般我们的程序会包含很多个.c文件吧,这些.c文件经过编译,汇编后就会变成一组可重定位文件
。然后通过链接器将他们生成一个可执行目标文件
(也就是我们最终的运行程序)。当然链接器也可以将库作为输入,和可重定位文件
一起生成我们所要的可执行目标文件
。
在链接器生成可执行目标文件
过程中根据链接器链接库
的行为将其分为静态链接
和动态链接
。
静态链接
:在生成可执行目标文件
时,将程序用到的模块
从(比如函数printf()
)复制到可执行目标文件
中。之后在程序的加载、执行过程中与此库再无关系。- 优点:生成的可执行文件没有依赖,在运行时不存在缺少依赖库的问题。
- 缺点:生成的可执行文件大,浪费存储空间。此外,当库发生更新时,此程序需要重新编译链接。不方便。
动态链接
: 为解决静态链接的缺点,提出了动态链接。在生成可执行目标文件
时,将程序用到库的模块
只进行简单的标记。之后在程序的加载
或执行
过程中与程序进行链接。- 优点: 生成的可执行文件小。由于文件中并不包含库中模块的实体函数,所以当库发生更新时,不需要对程序进行重新编译、链接。
- 缺点: 存在库依赖的问题。
我们经常使用的gcc -o helloworld helloworld.o
生成的可执行文件helloworld
是动态链接的。gcc默认使用动态链接。使用-static
进行静态链接.如gcc -static -o helloworld helloworld.o
.它俩生成的文件可以做一个大小比较,会发现动态生成的要比静态的小很多。对于动态链接生成的文件,我们可以使用ldd
命令查看它的所有依赖库,如ldd ./helloword
.
那么什么是库呢?其实就是将我们用来完成某些功能的程序块被打包成一个特定格式
的文件。相应的,静态链接时用到的库格式我们叫静态库
,后缀名为.a
。而动态链接时用到的库格式我们叫动态库
(共享库),后缀名为.so
。动态库又分为加载时的动态链接库
(程序在加载时进行库链接)和运行时的动态链接库
(程序在运行时进行库链接)。千呼万唤始出来,终于见到我们的主角:静态库
、动态库
了。
linux使用lib+库名.后缀名
的格式给库命名。比如名叫mytest
的静态库应该命名为libmytest.a
.相应的动态库则命名为libmytest.so
.除此之外,常在动态库后缀名后边加版本号.x.y.z
.其中x为主版本号,y为次版本号,z为发行版本号。如libmytest.so.1.2.3
。一般的,linux系统中的库都存放在/usr/lib/
(存放系统相关的库)和/usr/local/lib/
(存放用户相关的库)下。
静态库的生成与使用
使用find /usr/lib/ -name *.a
命令可以查看/usr/lib/
下的静态库。我们最常用的printf()
等函数都包含在libc.a
的静态库中。这些是系统自带的静态库,当然我们可以生成自己的静态库。用下面的程序举个创建例程:
//建立三个文件 main.c hello.c hello.h,其中我们用hello.c创建我们的静态库libhello.a.用main.c创建我们的可执行文件main,而hello.h为libhello.a的接口。在main.c中调用了libhello.a中的模块HelloPrintf().程序如下:
//-------------------main.c文件内容
#include "hello.h"
void main(void)
{
HelloPrintf();
}
//-------------------hello.c文件内容
#include <stdio.h>
#include "hello.h"
void HelloPrintf(void)
{
printf("hello word\n");
}
//-------------------hello.h文件内容
#ifndef __HELLO__H__
#define __HELLO__H__
void HelloPrintf(void);
#endif
程序已经在上面创建好,我们现在就演示其库的生成和使用步骤:
- 使用命令
gcc -c -o hello.o hello.c
生成可重定位文件hello.o,用于后面生成libhello.a静态库。 - 使用命令
ar rcs libhello.a hello.o
生成libhello.a静态库。- 命令中
rcs
的含义:rcs中的r表示向库中添加模块,若模块已存在则替换,c表示创建库文件,s表示生成一个目标文件索引。
- 命令中
- 使用命令
gcc -o main main.c ./libhello.a
生成可执行文件main.这是一个综合命令,其实是gcc先将main.c编译、汇编成一个可重定位文件,然后将这个可重定位文件和libhello.a链接,进而生成可执行文件main。 - 执行命令
./main
来运行程序。控制台打印出hello word.
上面的1,2步为静态库的生成。3,4步为静态库的使用。我们思考一个问题:上面生成的可执行文件main是静态链接的呢,还是动态链接的呢?答案是动态链接的(用ldd main
命令可以验证)。所以我们必须要清楚一个概念:main与libhello.a之间的链接是静态链接(用nm main
可以看到HelloPrintf符号为T
,代表文本段的意思。说明HelloPrintf被复制到了可执行文件main的文本段中,所以说它俩之间是静态链接)。但是,main还有一些隐式的链接(这些隐式的链接是链接器自己完成的,用于程序的启动加载等。用nm main
可以看到属性为U
的符号,如__libc_start_main,U
代表未定义的符号,说明此函数不在main的文本段中,所以说是动态链接的)。我们前面也说了gcc默认是动态编译的。所以我们可是使用gcc -static -o main main.c ./libhello.a
来创建完全静态链接的可执行程序main。再次使用nm main
查看就会发现找不到属性为U
的符号了。说明程序是全静态编译。刚才提到的nm
命令用于列出一个目标文件的符号表中定义的符号,具体使用可以用man nm
查询。
当然的,并非一个静态库只能由一个.c文件生成。可以是多个.c文件生成一个。如使用ar rcs libhello.a hello.o hello2.o hello3.o
等。除此之外,ar
命令还有很多的功能,具体请参考man ar
.
动态库的生成与使用
上面也提到了,动态库又分为加载时的动态链接库
(程序在加载时进行库链接)和运行时的动态链接库
(程序在运行时进行库链接).不要将动态链接的库与动态加载的库混淆。前者只在程序启动时加载一次。然而,后者可以在程序执行期间的任何时候加载,它们也被称为插件。所以它们的生成和使用也存在不同,下面分别叙述:
加载时的动态链接库
我们还是用上面的程序例程来说动态库的创建与使用步骤:
- 使用命令
gcc -shared -fpic -o libhello.so hello.c
变可生成libhello.so动态库。-shared
: 生成共享库时必要的gcc选项。-fpic
: pic(position-Independent code)表示编译为位置无关代码
.共享库编译时总是使用此选项。位置无关代码
简单的理解就是可以被加载器加载到内存的任意位置。
- 使用命令
gcc -o main main.c ./libhello.so
生成可执行文件main.当然这也是一个综合命令,其实是gcc先将main.c编译、汇编成一个可重定位文件,然后将这个可重定位文件和libhello.so链接,进而生成可执行文件main。 - 执行命令
./main
来运行程序。控制台打印出hello word.
上面的第1步为动态库的生成。2,3步为动态库的使用。毋庸置疑的,生成的可执行文件main为动态链接的。可以使用nm main
查看与上边的不同处(会发现此文件的HelloPrintf符号的属性为U
,表示未定义,可见是动态链接)。现在,我们可以将刚生成的可执行文件main复制到其它目录,会发现它在执行是会报错(说加载时找不到共享库libhello.so)。当然你也可以试试上边静态库生成的main,发现在其它的目录下依旧可以执行。这就引入了一个我们现在要谈论的共享库路径问题。
解决共享库路径问题的方式如下:
- 将自己生成好的库放入gcc可以查找到的位置,有下面几种方式:
- 第一种:直接加入到系统默认的库存放路径。如
/usr/lib/
和/usr/local/lib/
。 - 第二种:修改配置文件
/etc/ld.so.conf
.将自己的库路径加入其中。 - 第三种:修改
LD_LIBRARY_PATH
环境变量。如export LD_LIBRARY_PATH=xxx/mylibPath
- 第一种:直接加入到系统默认的库存放路径。如
- 重新链接生成可执行文件
- 对于上面的第一、二种库路径。直接使用
gcc -o main main.c -lhello
就可以生成。并且在其他目录下也可执行。-lhello
为小写的L+name,其中name为libhello.so去掉前缀和后缀。它是用于指示链接器搜索此库。
- 对于第三种库路径的配置。使用
gcc -o main main.c -L/libPath -lhello
.-L/libPath
为libhello.so的路径。
- 对于上面的第一、二种库路径。直接使用
- 在其他目录下使用
./main
执行。发现上面的路径问题解决了。
运行时的动态链接库
运行时的链接库
类似用户在程序中open一个文件,然后操作文件,最后在操作完成后关闭文件。下面是相关的函数.调用下面函数需要包含头文件#include <dlfcn.h>
.
void *dlopen(const char *filename, int flags)
- 描述: 加载一个动态库,并返回它的操作指针。
- filename:共享库名称。如
/usr/lib/libhello.so
。 - flags:
- RTLD_LAZY:标志指示连接器推迟符号解析直到执行来自库中的代码。
- RTLD_NOW:立即解析对外部符号的引用。
- return:操作指针。
void *dlsym(void *handle, const char *symbol)
- 描述:查找符号(简单说就是我们库中的函数等)并返回符号指针。
- handle:dlopen()返回的操作指针。
- symbol:要查找的符号。
- return:符号指针。
int dlclose(void *handle)
- 描述:卸载打开的共享库。
- return: 0:ok -1:error.
char *dlerror(void)
- 描述:返回一个调用dlxxx()函数时最近发生的错误。
- return:一个描述错误原因的字符串。
下面用一个具体例子举例说明:
#include "hello.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
void *phandle;
void (*pHelloPrintf)(void);
char *error;
//打开libhello.so库
phandle = dlopen("libhello.so",RTLD_LAZY);
if(!phandle){
printf("%s\n",dlerror());
exit(1);
}
//获取到libhello.so中的HelloPrintf()函数指针
pHelloPrintf = dlsym(phandle,"HelloPrintf");
error = dlerror();
if(error != NULL){
printf("%s\n",error);
exit(1);
}
//调用此函数
pHelloPrintf();
//使用完卸载
int res = dlclose(phandle);
if(res < 0){
printf("%s\n",dlerror());
exit(1);
}
return 0;
}
编写好程序后,下面是使用步骤:
- 使用命令
gcc -rdynamic -o main main.c -ldl
编译上面的main.c程序. - 使用
./main
运行,终端打印hello word.
在介绍加载时的动态链接库
时已经介绍了共享库路径问题。而运行时的动态链接库
需要在此基础上再加一个步骤:在修改或者在库路径中加入新库时,使用ldconfig
命令进行更新/etc/ld.so.cache
缓冲文件。
运行时的链接库
的优点很明显,读者细细品吧。
关于技术交流
此处后的文字已经和题目内容无关,可以不看。
qq群:825695030
微信公众号:嵌入式的日常
如果上面的文章对你有用,欢迎打赏、点赞、评论。