探索静态库与动态库的“奥秘”
什么是库?
所谓“库”,就是稳定成熟的可以复用的代码;库从本质上来说是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
库有两种:静态库(.a(linux)、.lib(windows))和动态库(.so(linux)、.dll(windows))。
所谓静态、动态是指链接,可以看下编译链接的过程:
库与可执行文件区别:
- 库文件无法直接执行,从生产库的源码中可以查看出,源码是没有
main
函数,都是一些函数模块的定义和实现,由于没有主入口,所以无法直接运行库。
静态库是什么?
静态库实际就是一些目标文件(一般以.o
结尾)的集合,静态库一般以.a
结尾,只用于链接生成可执行文件阶段。这里注意区分下linux
环境下是以.a
结尾的,而windows
环境下是以.lib
结尾的(我这里就使用linux
版本进行,文章就主要以linux
环境进行撰写)
在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中。这种库称为静态库;
特点:
- 可执行文件中包含了库代码的一份完整拷贝
缺点:
- 缺点就是被多次使用就会有多份冗余拷贝。
- 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库
libxxx.lib
更新了,所以使用它的应用程序都需要重新编译、发布给用户;
静态库生成
创建静态库的步骤:
- 写源文件,通过
g++ -c xxx.cpp
生成目标文件。 - 用
ar
归档目标文件,生成静态库。(ar cr libhello.a hello.o) - 使用静态库中函数的头文件。
- 使用静态库时,在源码中包含对应的头文件,链接生成的静态库。
生成静态库案例
文件:aLibTest.h aLibTest.cpp main.cpp
root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# ls
aLibTest.cpp aLibTest.h main.cpp
aLibTest.h
#include <iostream>
using namespace std;
int getParam();
aLibTest.cpp
#include "aLibTest.h"
int getParam()
{
int iparam = 10;
iparam++;
return iparam;
}
制作动态库:
- 先编译生成
.o
链接文件
root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# g++ aLibTest.cpp -c
root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# ls
aLibTest.cpp aLibTest.h aLibTest.o main.cpp
-
归档,生成静态库
root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# ar cr libaLibTest.a aLibTest.o root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# ls aLibTest.cpp aLibTest.h aLibTest.o libaLibTest.a main.cpp
-
main
函数调用main.cpp
#include "aLibTest.h" int main() { int res = getParam(); cout<<"res:"<<res<<endl; return 0; }
-
链接
root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# g++ main.cpp -L. -laLibTest root@iZuf67on1pthsuih96udyfZ:~/GDB/20201019# ./a.out res:11
-
通过使用makefile做到自动化
CXX=g++ build:libaLibTest.a libaLibTest.a:aLibTest.o ar crv $@ aLibTest.o aLibTest.o:aLibTest.cpp $(CXX) -c aLibTest.cpp target:aLibTest aLibTest:main.cpp $(CXX) main.cpp -o $@ -L. -laLibTest
使用
make build
会构建出libaLibTest.a
,使用make target
生成可执行文件;
动态库是什么?
Linux
下动态库文件的文件名形如 libxxx.so
,其中so是 Shared Object 的缩写,即可以共享的目标文件。
动态库**在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。
特点:
- 使用动态库的优点是系统只需载入一次动态库,不同的程序可以得到内存中相同的动态库的副本
优点:
- 节省了内存
动态库生成
创建动态库的步骤:
- 编写源文件。
- 将一个或几个源文件编译链接,生成共享库。
- 通过
-L<path> -lxxx
的gcc
选项链接生成的libxxx.so
。 - 把
libxxx.so
放入链接库的标准路径,或指定LD_LIBRARY_PATH
,才能运行链接了libxxx.so
的程序。
生成动态库案例
还是上面的代码,这里直接使用上面的代码进行动态库的生成和使用
制作动态库:
-
先编译生成
.o
链接文件-fPIC
创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# g++ -fPIC -c aLibTest.cpp root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# ls aLibTest.cpp aLibTest.h aLibTest.o main.cpp
-
生成动态库(链接器选项-shared)
root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# g++ -shared -o libaLibTest.so aLibTest.o root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# ls aLibTest.cpp aLibTest.h aLibTest.o libaLibTest.so main.cpp
其实可以一步就生成:
g++ -fPIC -shared -o libaLibTest.so aLibTest.cpp
-
main
函数调用root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# g++ main.cpp -L. -o main -laLibTest root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# ls aLibTest.cpp aLibTest.h aLibTest.o libaLibTest.so main main.cpp root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# ./main ./main: error while loading shared libraries: libaLibTest.so: cannot open shared object file: No such file or directory
我们发现,明明我们的动态库就在当前目录下,为什么会找不到呢?
-
当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。
-
对于elf格式的可执行程序,是由ld-linux.so*来完成的,它先后搜索elf文件的 DT_RPATH段—环境变量LD_LIBRARY_PATH—/etc/ld.so.cache文件列表—/lib/,/usr/lib 目录找到库文件后将其载入内存。
如何让系统能够找到它:
-
如果安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
-
如果安装在其他目录,需要将其添加到/etc/ld.so.cache文件中,步骤如下:
-
编辑/etc/ld.so.conf文件,加入库文件所在目录的路径
-
运行ldconfig ,该命令会重建/etc/ld.so.cache文件
-
-
我们直接将我们生成的文件拷贝至/usr/lib
下,再次运行:
root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# cp libaLibTest.so /usr/lib
root@iZuf67on1pthsuih96udyfZ:~/GDB/sharedtest# ./main
res:11
当然,加载动态库不止这一种方式,我们代码中往往使用的是一些函数,具体会出一篇文章,先做个插曲~
小插曲(常用参数)
- -l参数就是用来指定程序要链接的库,-l参数紧接着就是库名,拿数学库来说,它的库名是m,它的库文件名是
libm.so
,很容易看出,把库文件名的头lib
和尾.so
去掉就是库名了。 - -L参数跟着的是库文件所在的目录名。比如我们把
libtest.so
放在/tmp
目录下,那链接参数就是-L/tmp -ltest
另外,大部分libxxxx.so
只是一个链接; -include
用来包含头文件,但一般情况下包含头文件都在源码里用#include xxxxxx
实现,-include
参数很少用。-I
参数是用来指定头文件目录,/usr/include
目录一般是不用指定的,gcc
知道去那里找,但是如果头文件不在/usr/include
里我们就要用-I
参数指定了,比如头文件放在/myinclude
目录里,那编译命令行就要加上-I/myinclude
参数了,如果不加你会得到一个"xxxx.h: No such file or directory"
的错误。-I
参数可以用相对路径,比如头文件在当前目录,可以用-I.
来指定;
往期精彩汇总
欢迎关注公众号—后台服务器开发,更多精彩等你来看~