目录
动静态库的基本原理
动静态库是软件开发中用于代码重用的两种主要形式。它们都有助于组织代码、减少冗余并提高开发效率。下面是动静态库的基本原理及其区别:
静态库(Static Library)
基本原理:
- 静态库是将一组目标文件(object files)打包成一个单一的归档文件(通常扩展名为
.a
或.lib
)。 - 编译时,链接器(linker)将静态库中所需的代码复制到可执行文件中。
- 静态库在编译阶段就被绑定到可执行文件,因此在运行时不需要额外的库文件支持。
优点:
- 独立性高: 可执行文件在运行时不依赖外部库,因此分发和部署更方便。
- 速度快: 由于所有代码都在一个文件中,不需要在运行时加载外部库,启动速度更快。
缺点:
- 占用空间大: 由于每个可执行文件都包含所需的库代码,可能会导致文件体积较大。
- 更新不便: 若需要更新库的代码,必须重新编译所有依赖该库的可执行文件。
动态库(Dynamic Library)
基本原理:
- 动态库(也称共享库,通常扩展名为
.so
或.dll
)在编译时并不复制到可执行文件中,而是在运行时加载。 - 可执行文件在运行时通过操作系统的动态链接器加载动态库,从而使用其中的代码。
- 多个可执行文件可以共享同一个动态库,从而节省内存和磁盘空间。
优点:
- 占用空间小: 可执行文件不包含库的代码,多个程序可以共享同一个动态库,节省空间。
- 易于更新: 更新动态库无需重新编译依赖该库的可执行文件,只需替换动态库文件即可。
缺点:
- 依赖性高: 可执行文件运行时依赖动态库,分发时必须确保动态库随之提供或在系统路径中可用。
- 加载开销: 在运行时需要加载动态库,可能会增加启动时间。
动静态库的使用场景
- 静态库适合: 需要独立分发的可执行文件,要求在没有外部依赖的情况下运行的场景。
- 动态库适合: 需要节省空间和内存的场景,或是需要频繁更新库的环境,如操作系统和大型软件系统。
动静态库在生成和使用过程中,涉及到预处理、编译、汇编和链接的四个步骤。以下是每个步骤中涉及动静态库的详细解释:
静态库
1. 预处理
- 预处理器处理源文件,展开头文件、宏替换、去注释、条件编译等。生成的文件是预处理后的
.i
文件。
2. 编译
- 编译器将预处理后的
.i
文件进行词法分析、语法分析、语义分析和符号表生成,最终生成汇编代码.s
文件。
3. 汇编
- 汇编器将
.s
文件转换成机器指令,生成目标文件.o
。
4. 链接
- 链接器将多个目标文件
.o
和库文件(静态库.a
)合并成一个可执行文件。 - 静态库
.a
文件包含多个.o
文件,可以通过ar
工具打包。 - 链接时,链接器从静态库中提取需要的目标文件,将其直接复制到最终的可执行文件中。
总结
- 静态库在链接阶段被绑定到可执行文件中,形成一个完整的可执行文件,不再需要动态库的支持。
动态库
1. 预处理
- 与静态库相同,预处理器处理源文件,生成预处理后的
.i
文件。
2. 编译
- 编译器将
.i
文件编译成汇编代码.s
文件。
3. 汇编
- 汇编器将
.s
文件转换成目标文件.o
。
4. 链接
- 创建动态库时,链接器将多个目标文件
.o
链接成共享对象文件.so
(在Windows上为.dll
)。- 创建动态库命令示例(Linux):
gcc -shared -o libmylib.so file1.o file2.o ...
- 创建动态库命令示例(Linux):
- 链接可执行文件时,链接器将动态库
.so
文件的符号表链接到可执行文件,但不复制代码。- 链接可执行文件命令示例(Linux):
gcc -o myprogram main.o -L. -lmylib
- 链接可执行文件命令示例(Linux):
运行时
- 在运行时,操作系统的动态链接器负责加载动态库
.so
文件,并解析可执行文件中的符号。 - 动态库的代码在运行时加载,允许多个程序共享同一个动态库实例。
总结
- 动态库在运行时被加载和链接,可执行文件在启动时依赖动态库。
对比
- 静态库 在链接阶段将库代码直接复制到可执行文件中,生成的可执行文件独立,便于分发和部署,但占用空间大。
- 动态库 在运行时加载和链接,节省空间和内存,便于更新和维护,但运行时依赖性较强,需要确保动态库的可用性。
静态库的打包和试用
为了更容易理解,下面演示动静态库的打包与使用时,都以下面的四个文件为例,其中两个源文件add.c
和sub.c
,两个头文件add.h
和sub.h
。
打包
下面我们就利用这四个文件打包生成一个静态库:
使用Makefile
编写Makefile后,只需一个make
就能生成所有源文件对应的目标文件进而生成静态库。
一个make output
就能将头文件和静态库组织起来。
当我们把自己的库给别人用的时候,实际上需要给别人两个文件夹,一个文件夹下面放的是一堆头文件的集合,另一个文件夹下面放的是所有的库文件。
使用
创建源文件main.c
,编写下面这段简单的程序来尝试使用我们打包好的静态库。
#include <stdio.h>
#include <add.h>
int main()
{
int x = 20;
int y = 10;
int z = my_add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
现在该目录下就只有main.c
和我们刚才打包好的静态库。
此时使用gcc编译main.c生成可执行程序时需要携带三个选项:
-I
:指定头文件搜索路径。-L
:指定库文件搜索路径。-l
:指明需要链接库文件路径下的哪一个库。
gcc -o mymath main.c -I ./mylib/include -L ./mylib/lib -l mymath
此时就可以成功使用我们自己打包的库文件并生成可执行程序。
动态库的打包和使用
打包
动态库的打包相对于静态库来说有一点点差别,但大致相同,我们还是利用这四个文件进行打包演示:
第一步:让所有源文件生成对应的目标文件
此时用源文件生成目标文件时需要携带-fPIC
选项:
-fPIC
(position independent code):产生位置无关码。
说明一下:
1.-fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
2.如果不加-fPIC选项,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so文件代码段和数据段内存映射的位置。
3.不加-fPIC编译出来的.so是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。
4.我们总是用-fPIC来生成.so,但从来不用-fPIC来生成.a。但是.so一样可以不用-fPIC选项进行编译,只是这样的.so必须要在加载到用户程序的地址空间时重定向所有表目。
第二步:使用-shared选项将所有目标文件打包为动态库
与生成静态库不同的是,生成动态库时我们不必使用ar命令,我们只需使用gcc的-shared选项即可。
gcc -shared -o libcal.so my_add.o my_sub.o
第三步:将头文件和生成的动态库组织起来
与生成静态库时一样,为了方便别人使用,在这里我们可以将add.h
和sub.h
这两个头文件放到一个名为include的目录下,将生成的动态库文件libcal.so
放到一个名为lib的目录下,然后将这两个目录都放到mlib下,此时就可以将mylib给别人使用了。
[yxh@iZbp1clsle0rodd65nbnswZ test5_16]$ mkdir -p mylib/include
[yxh@iZbp1clsle0rodd65nbnswZ test5_16]$ mkdir -p mylib/lib
[yxh@iZbp1clsle0rodd65nbnswZ test5_16]$ cp ./*.h mylib/lib
[yxh@iZbp1clsle0rodd65nbnswZ test5_16]$ cp ./*.so mylib/lib/
使用
我们还是用刚才使用过的main.c
来演示动态库的使用。
#include <stdio.h>
#include <add.h>
int main()
{
int x = 20;
int y = 10;
int z = my_add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
现在该目录下就只有main.c
和我们刚才打包好的动态库。
说明一下,使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序,下面我们仅以第一种方法为例进行演示。
此时使用gcc编译main.c生成可执行程序时,需要用-I选项指定头文件搜索路径,用-L选项指定库文件搜索路径,最后用-l选项指明需要链接库文件路径下的哪一个库。
[cl@VM-0-15-centos project]$ gcc main.c -I./mylib/include -L./mylib/lib -lcal
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
需要注意的是,我们使用-I
,-L
,-l
这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd
命令进行查看。
我们可以拷贝.so文件到系统共享库路径下
既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
sudo cp mylib/lib/libcal.so /lib64
可执行程序也就能够顺利运行了。