动静态库的基本原理
静态库原理:
静态库是一组预先编译好的目标文件(通常是 .o 文件)的集合,它们被打包成一个单独的文件。在链接阶段,编译器会将静态库中的目标文件直接复制并链接到可执行文件中。因此,可执行文件中包含了静态库中的函数和数据的副本。
动态库的原理:
动态库是一组目标文件的集合,它们被编译成共享对象文件(Shared Object,通常是 .so 文件)。在链接阶段,编译器只在可执行文件中包含对动态库的引用,而不包含实际的库函数和数据。在程序运行时,操作系统会将动态库加载到内存中,并将程序的调用转发到动态库中的函数。
所以在程序编译的时候,编译器要能找到库的位置;而在程序运行的时候,操作系统要能找到库的位置。(二者缺一不可!)
总结:
在链接阶段,编译器会将编译后的目标文件和库文件链接成一个可执行文件。这时候库文件才发挥作用,它们被链接到可执行文件中,使得可执行文件包含了库中的函数和数据。对于静态库,链接器会将库中的目标文件直接复制并链接到可执行文件中;对于动态库,链接器只会在可执行文件中包含对动态库的引用,而不包含实际的库函数和数据。
认识动静态库
我们先来认识一下动静态库:
首先先写一段简单的代码:
1 #include<stdio.h>
2 int main()
3 {
4 printf("Hello Linux\n");
5 return 0;
6 }
通过这个代码来理解一下库的作用。
这段代码要想生成可执行文件,则需要经过下面几个阶段:
- 预处理阶段(Preprocessing):
在预处理阶段,编译器会处理预处理指令,如#include <stdio.h>
,将stdio.h
头文件中的内容包含到源文件中。stdio.h
包含了printf
函数的声明,使得在源文件中调用printf
函数时能够正确识别。 - 编译阶段(Compilation):
在编译阶段,编译器会将源文件编译成汇编代码,然后汇编成目标文件。在这个过程中,编译器会根据printf
函数的声明生成相应的机器代码,但实际的printf
函数实现并不在源文件中,而是在标准C库中,因此编译器只需生成对printf
函数的调用。 - 链接阶段(Linking):
在链接阶段,链接器会将目标文件与库文件链接在一起,生成可执行文件。在Linux系统中,printf
函数是位于标准C库(libc)中的,因此链接器会将你的目标文件与标准C库进行链接,以解析对printf
函数的调用。这样,生成的可执行文件中包含了对标准C库中printf
函数的引用,使得程序能够正确地调用并输出 “Hello Linux”。
结果如下:
在Linux下,我们可以通过ldd 文件名
来查看一个可执行程序所依赖的库文件。
-
linux-vdso.so.1 => (0x00007fff633bc000)
:linux-vdso.so.1
:这是一个虚拟动态共享对象,通常称为 VDSO,用于在用户空间和内核空间之间进行系统调用的高效接口。=>
:表示指向动态链接库文件的符号链接。(0x00007fff633bc000)
:表示动态链接库加载到内存中的地址。
-
libc.so.6 => /lib64/libc.so.6 (0x00007f591f9a3000)
:libc.so.6
:这是标准C库(libc)的文件名。=>
:表示指向动态链接库文件的符号链接。/lib64/libc.so.6
:表示标准C库的路径。(0x00007f591f9a3000)
:表示动态链接库加载到内存中的地址。
-
/lib64/ld-linux-x86-64.so.2 (0x00007f591fd71000)
:/lib64/ld-linux-x86-64.so.2
:这是动态链接器(ld-linux-x86-64.so.2)的文件路径。(0x00007f591fd71000)
:表示动态链接器加载到内存中的地址。
这其中的libc.so.6就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6实际上只是一个软链接。
libc-2.17.so
和libc.so.6
在同一个目录下。我们可以使用file 文件名
命令来查看libc-2.17.so
的文件类型。
/lib64/libc-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked (uses shared libs), BuildID[sha1]=9470e279388f7f9cb2ed3b2872d0c2095b191ff4, for GNU/Linux 2.6.32, not stripped
/lib64/libc-2.17.so
:这是一个共享对象文件,通常称为共享库文件,位于 /lib64 目录下。ELF 64-bit LSB shared object, x86-64
:这个描述说明了该文件是一个 64 位的 ELF 格式的共享对象文件,适用于 x86-64 架构的处理器。version 1 (GNU/Linux)
:这个描述说明了该文件的版本是 1,用于 GNU/Linux 系统。dynamically linked (uses shared libs)
:这个描述说明了该文件是动态链接的,并且使用了其他共享库。BuildID[sha1]=9470e279388f7f9cb2ed3b2872d0c2095b191ff4
:这个描述是一个构建标识符,使用 SHA1 哈希算法生成的唯一标识符,用于标识该文件的构建版本。
-for GNU/Linux 2.6.32
:这个描述说明了该文件适用于 GNU/Linux 内核版本 2.6.32 及以上。not stripped
:这个描述说明了该文件没有经过剥离(stripped),即未去除调试符号和符号表等信息。
注意:
- 在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
- 在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。
这里可执行程序所依赖的libc.so.6
实际上就是C动态库,当我们去掉一个动静态库的前缀lib
,再去掉后缀.so
或者.a
及其后面的版本号,剩下的就是这个库的名字。
因为我们这里是直接使用gcc
进行编译处理的,所以 gcc
默认使用的是C标准动态库来进行链接的。
去除掉动静态库的前缀lib
,再去掉后缀.so
或者.a
和版本号 才是这个库的名字,比如:libc.so.6 -> c.so
gcc/g++编译器默认是使用动态库来进行链接的,如果想使用静态库链接,则需要使用 -static
参数,这个在我们之前的 gcc 参数编译讲过,具体可见动态库和静态库
gcc .c文件名 -static -o 可执行文件名
动静态链接的最明显的差异就是文件大小了,因为静态链接的可执行文件中包含了静态库中的函数和数据的副本,所以文件比动态链接的大得多。
静态链接的可执行程序没有所依赖的库文件,所以 ldd
为空。
静态库的打包与使用
接下来我们来手动写一个静态库,这个库里面包含了四个文件,分别是两个头文件
add.h
和 sub.h
两个源文件add.c
和 sub.c
。
//add.h
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
//add.c
#include "add.h"
int add(int a, int b)
{
return a + b;
}
//sub.h
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
//sub.c
#include "sub.h"
int sub(int a, int b)
{
return a - b;
}
打包
-
根据库的特点,我们需要先将add.c文件和sub.c文件编译成目标文件,使用带-c选项的gcc指令:
-
使用
ar
命令来讲目标文件打包成静态库。
ar
是一个用于创建静态库(archive)的命令行工具,通常用于将一组相关的目标文件打包成一个单独的静态库文件。下面是关于 ar 命令的一些基本用法和示例:
-r:表示将目标文件插入(或替换)到静态库中。
-c:表示创建一个静态库,如果库文件不存在则创建,如果存在则添加目标文件到现有库中。
[qq@iZ0jl65jmm6w9evbwz2zuoZ code]$ ar -rc libcal.a add.o sub.o
此外,我们也可以用ar命令的-t选项和-v选项查看静态库当中的文件
- -t:列出静态库中的文件。
- -v(verbose):显示详细的信息。
使用
我们先使用我们写的静态库来链接这个test.c文件,我们可以使用下面这个语句:
gcc main.c -I ./mylib/include/ -L ./mylib/lib -lmyc
-I
(大写I) 选项指定了用户自定义的有头文件路径-L
选项指定了用户自定义的库文件路径-l
(小写L)选项指定了第三方库的名称
因为我们没有吧头文件单独放在一个路径里面,所以这里只用指定库的路径即可,也能找到头文件。
为什么我们要使用
-l
来指定库的位置呢?
因为对于gcc/g++ 编译器来说,他们只会去 系统标准库和环境变量里面去找,如果想要使用自定义的第三方库的话就需要吧其添加到标准库或者系统变量中去,但这不太建议,因为将第三方自己写的库加入到系统库目录和环境变量中存在一些潜在的问题和风险。所以这里一般建议使用-L
来指定库搜索路径。
方法二:把头文件和库文件拷贝到系统路径下
sudo cp ./libcal.a /lib64/
gcc 编译
gcc test.c -lcal -o test
此时不用再指定位置,但是还得要去指定库的名字。
动态库的打包与使用
在我们了解动态库的打包和使用前先了解一下位置无关代码。
什么是位置无关代码(PIC)?
位置无关代码意味着编译生成的机器代码可以被加载到内存的任意位置,并且能正确执行。这与传统的绝对代码不同,绝对代码通常在编译时就已经决定了代码执行的具体地址。
为什么动态库需要位置无关代码?
- 共享和重用:动态库的主要目的是可以被多个不同的程序共享。为了让同一个库文件能被不同的程序同时使用,库中的代码必须能在不同的地址空间中运行而不产生冲突。
- 内存效率:如果动态库的代码是位置无关的,操作系统可以将库的同一份拷贝映射到多个程序的地址空间中。这样做节省了内存,因为不需要为每个使用库的程序都创建一个库的副本。
- 动态加载:动态库可以在程序运行时被加载。由于加载的具体地址在编译时是未知的,因此库必须使用位置无关代码来适应任意的加载地址。
打包
我们还是利用这四个文件进行打包演示:
第一步:让所有源文件生成对应的目标文件
此时用源文件生成目标文件时需要携带-fPIC选项
:
第二步:使用-shared选项将所有目标文件打包为动态库
我们需使用gcc的-shared选项将所有目标文件打包为动态库。
gcc -shared -o libcal.so add.o sub.o
使用
首先我们先回顾下,在静态库的使用时我们只需在gcc的时候指定库的路径即可,
但是动态库这样时不行的!
因为动态库和静态库的运行机制有些许不同。
不同于静态库,动态库在程序运行时被加载。因此,除了编译时指定库的位置外,还需要确保在程序运行时操作系统能找到这些库。
使用这段代码直接编译时,在运行时会发生错误:
gcc test.c -o mytest -L /home/qq/bt111/Linux/4_28/code -lcal
与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
解决该问题的方法有以下三个:
方法一:拷贝.so文件到系统共享库路径下
既然系统找不到我们的库文件,那么我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。
sudo cp /home/qq/bt111/Linux/4_28/code/libcal.so /lib64
可执行程序也就能够顺利运行了。
方法二:将静态库文件所在的目录添加到
LD_LIBRARY_PATH
环境变量中。
LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径。
将静态库文件所在的目录添加到 LD_LIBRARY_PATH
环境变量中。
export LD_LIBRARY_PATH=/path/to/your/library:$LD_LIBRARY_PATH
在运行时,如果你的程序依赖于动态链接库(例如 .so 文件),操作系统会根据 LD_LIBRARY_PATH 环境变量的设置,在指定的路径中搜索依赖的动态链接库。如果找不到库文件,程序可能无法运行,或者会抛出动态链接错误
查看库的消息
方法三:配置/etc/ld.so.conf.d/
我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了。
首先将库文件所在目录的路径存入一个以.conf
为后缀的文件当中,再吧该文件拷贝到/etc/ld.so.conf.d/
目录下。
这时我们需要使用ldconfig命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。