🏠关于专栏:Linux的浅学到熟知专栏用于记录Linux系统编程、网络编程等内容。
🎯每天努力一点点,技术变化看得见
文章目录
静态库
静态库的介绍及使用方法
在Linux系统中,静态库以.a
后缀结尾。我们可以进入/lib64目录下,使用ls -al | grep "\.a$"
查找当前目录下的静态库。↓↓↓
静态库的名称需要去掉前面的lib及后缀.a。如下图所示,该静态库的名称为util↓↓↓
程序在编译链接的时候把静态库库的代码链接到可执行文件中,程序运行的时候不再需要静态库。例如,我们需要使用libutil.a中的代码,在编译链接的时候,就将该库中的代码拷贝到待生成程序的代码中。
当使用静态链接生成的可执行程序被执行时,由于该程序已经包含了静态库中的代码,当其执行时就不再需要使用静态库,而是直接在该程序的正文代码段寻找对应的库代码即可。
gcc/g++编译器在默认情况下,都采用动态链接,只有用户在编译链接时显式带上-static
选项才会采用静态链接。我们对下方程序分别采用动态链接及静态链接↓↓↓
c
#include <stdip.h>
int main()
{
printf("Jammingpro\n");
return 0;
}
从上面的文件大小可以看出,静态链接的程序大小明显大于动态链接的程序大小。因为静态链接将使用到的库的可执行程序代码保存到其代码中,导致可执行程序的大小比较大。
自制静态库
在市面上,如果购买一个第三方库,我们将得到包含该库中的各个方法声明的头文件,及这些方法对应的实现生成的可执行程序(为了不让其他人知道这个方法的具体实现,故已经将这些方法的实现编译成可执行程序)。
下面,我们自制一个包含加减乘除函数的mymath静态库。我们需要包含一个mymath静态库的头文件,一个包含具体实现的mymath静态库。最终组成lib目录下保存include和mymathlib文件夹,这两文件夹内分别包含头文件及静态库↓↓↓
首先,我们需要提供包含各个函数声明的头文件(mymath.h)↓↓↓
c
#ifndef __MYMATH_H__
#define __MYMATH_H__
int add(int left, int right);
int sub(int left, int right);
int mul(int left, int right);
int div(int left, int right);
#endif
下面是上方各个函数声明的具体实现代码(mymath.c)↓↓↓
c
#include "mymath.h"
//下方代码实现不考虑除零错误及超出int表示范围的情况
int add(int left, int right)
{
return left + right;
}
int sub(int left, int right)
{
return left - right;
}
int mul(int left, int right)
{
return left * right;
}
int div(int left, int right)
{
return left / right;
}
对于上面的mymath.c源文件,我们需要使用gcc -c mymath.c
生成与源文件同名的目标文件mymath.o。
在继续做下一步前,先介绍一个命令ar -rc
,该命令用于创建归档文件,其中-c表示创建归档文件,-r表示将指定文件放入归档文件中。
所以,接下来,我们可以将mymath.o加入到libmymath.a的归档文件中,即我们需要执行ar -rc mymath.o libmymath.a
。
最后,我们需要在创建lib、include、mymathlib文件夹,并将头文件及生成的静态库文件拷贝到指定位置即可↓↓↓
bash
mkdir -p ./lib/include
mkdir -p ./lib/mymathlib
cp *.h ./lib/include
cp *.a ./lib/mymathlib
执行完上述操作后,我们就制作完属于自己的静态库了!
使用第三方提供的静态库
当我们下载某个库时,本质就是将对应的库拷贝到我们的计算机中。下面,我们使用在当前目录中创建user目录,来模拟用户下载和使用我们编写的静态库↓↓↓
下图,将lib目录拷贝到user目录下,模拟用户下载第三方静态库↓↓↓
下面,编写一个程序main.c,在该程序中调用我们编写静态库↓↓↓
c
#include <stdio.h>
#include "mymath.h"
int main()
{
printf("2 + 1 = %d\n", add(1, 1));
printf("2 - 1 = %d\n", sub(1, 1));
printf("2 * 1 = %d\n", mul(1, 1));
printf("2 / 1 = %d\n", div(1, 1));
return 0;
}
gcc/g++会在指定目录中查找静态库及其头文件,由于我们自定义的静态库并没有在指定目录中,故我们对main.c进行编译时会出错↓↓↓
当我们使用gcc [源代码文件名] -I [头文件目录] -L [库文件目录]
指名使用的库的头文件位置,库文件位置时,链接程序时仍会报错↓↓↓
除了需要指名头文件和库文件的位置,我们还需要指明使用的是哪一个库,即使用-l[库名称]
指名使用的库的名称。↓↓↓
至此,我们自定义的静态库就能够被使用了↓↓↓
动态库
动态库的介绍及使用方法
在Linux系统中,动态库以.so
后缀结尾。我们可以进入/lib64目录下,使用ls -al | grep "\.so$"
查找当前目录下的动态库。↓↓↓
动态库的名称需要去掉前面的lib及后缀.so。如下图所示,该动态库的名称为c↓↓↓
以动态链接生成的可执行文件,在被执行时,会将指定的库文件加载到内存中。并将使用到的动态库代码地址存储于程序地址空间的共享区中。当正文代码段执行到需要调用动态库的代码时,会跳转到共享区中寻找对应代码的地址,通过页表映射到物理地址,并执行对应的代码。
gcc/g++编译器在默认情况下,都采用动态链接,我们可以使用ldd查看程序的动态库链接情况(对于静态链接的程序,ldd会提示该程序不是动态链接)↓↓↓
自制动态库
与制作静态库类似,我们需要提供对应方法的声明(头文件)和实现(.o后缀的目标文件)。但动态库在生成.o的目标文件及生成库的最后一步时,与静态库制作存在差异。
制作的动态库的最终效果如下,lib目录下包含include和mymathlib两个目录,两目录分别包含头文件及动态库文件。↓↓↓
一样的,首先,我们需要提供包含各个函数声明的头文件(mymath.h)↓↓↓
c
#ifndef __MYMATH_H__
#define __MYMATH_H__
int add(int left, int right);
int sub(int left, int right);
int mul(int left, int right);
int div(int left, int right);
#endif
下面是上方各个函数声明的具体实现代码(mymath.c)↓↓↓
c
#include "mymath.h"
//下方代码实现不考虑除零错误及超出int表示范围的情况
int add(int left, int right)
{
return left + right;
}
int sub(int left, int right)
{
return left - right;
}
int mul(int left, int right)
{
return left * right;
}
int div(int left, int right)
{
return left / right;
}
但在生成.o的目标文件时,需要带上-fPIC
(用于产生与位置无关码),即gcc -c -fPIC main.c
。为什么带上-fPIC选项将于下文介绍。
所以,接下来生成动态库的操作与静态存在差异,动态库并不使用归档方式,而是使用gcc -shared -o [动态库名称] [目标文件名.o]
,即使用gcc -shared -o libmymath.so mymath.o
。
最后,我们需要在创建lib、include、mymathlib文件夹,并将头文件及生成的动态库文件拷贝到指定位置即可↓↓↓
bash
mkdir -p ./lib/include
mkdir -p ./lib/mymathlib
cp *.h ./lib/include
cp *.a ./lib/mymathlib
执行完上述操作后,我们就制作完属于自己的动态库了!
第三方动态库的使用
我们在当前目录下创建user目录,模拟用户使用动态库的过程↓↓↓
下载的本质就是拷贝,我们将自定义的动态库拷贝到user目录下↓↓↓
下面,编写一个程序main.c,在该程序中调用我们编写动态库↓↓↓
c
#include <stdio.h>
#include "mymath.h"
int main()
{
printf("2 + 1 = %d\n", add(1, 1));
printf("2 - 1 = %d\n", sub(1, 1));
printf("2 * 1 = %d\n", mul(1, 1));
printf("2 / 1 = %d\n", div(1, 1));
return 0;
}
有了静态库的使用经验,我们知道了需要告诉编译头文件、库文件位置,及使用的库文件名↓↓↓
但我们在运行可执行文件时,会报错:找不到对应的动态库↓↓↓
由于我们在编译链接时,告诉编译器库的头文件和库的位置,及使用的库文件名;但执行时,动态库代码并没有在可执行文件中,需要将对应的动态库代码加载到物理内存并映射入当前进程的共享区中。由于系统只会查找默认路径下的动态库,由于默认路径下的没有mymath动态库,故报错。
下面给出4中解决加载不到动态库的方法↓↓↓
- 由于系统默认会在/lib64目录下查找动态库,故我们可以将动态库保存在/lib64目录下↓↓↓
- 在系统默认路径下,即/lib64目录下,建立对应动态库的软链接↓↓↓
- 将自己的库所有的路径添加到系统的环境变量LD_LIBRARY_PATH中,系统在搜索动态库时,会在该环境变量中的路径中查找(export设置的环境变量仅在当前会话中有效,可以修改环境变量配置文件来永久修改环境变量的值)↓↓↓
- 在/etc/ld.so.conf.d目录中建立自己的动态库路径的配置文件,文件名称只需要以.conf即可,文件内容保存的就是我们的动态库存储位置;在创建配置文件后,需要再使用lfconfig,使得配置文件生效(这些操作均需要root权限)↓↓↓
动静态库的加载与调用原理
静态链接中,静态库中的代码被编址到程序的正文代码段中,静态库的代码称为了可执行程序代码的一部分。虽然在执行程序时不需要再链接库文件,但重复存储库代码造成了极大的浪费。例如:我们的各个程序基本都需要使用到C语言库,如果每个程序运行都要加载1份C库代码,则内存中会存在大量相同的代码。(静态链接原理在上文以有介绍)
而动态链接的代码段中并不保存库代码,而是等程序运行的时候才去链接动态库的代码,多个程序共享相同的库代码。这样可以大大提高内存的使用效率,提高代码的复用率。
一个以动态库链接的可执行文件仅仅包括包含它用到的函数的入口函数的一个表,而不是外部函数所在目标文件的整个机器码。在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,当前程序需要使用某个函数时,通过入口函数表找到对应的函数执行即可,这个过程称为动态链接。
关于动态链接,下面再做更详细的介绍:
动态链接时,在当前程序的虚拟地址空间的共享区中会保存对应动态库代码的起始地址及各个函数相对起始地址的偏移量。↓↓↓
上图中,C动态库的起始地址可以通过共享区的地址,经过页表映射找到物理地址0x11111111。当正文代码段需要调用printf函数时,会先找到C动态库在物理地址的起始存储处;然后在共享区存储的入口函数表查到printf相对于起始的地址的偏移量是8,故printf的物理地址为0x11111119。
对于静态库代码中,它的内部可以存储虚拟地址;但对于动态库代码来说,它的内部不能存储虚拟地址,而应该存储相对于起始地址的偏移量,故编译时需要带上-fPIC
(与位置无关码)选项。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
🎈欢迎进入从浅学到熟知Linux专栏,查看更多文章。
如果上述内容有任何问题,欢迎在下方留言区指正b( ̄▽ ̄)d