【Linux】动静态库

发布时间:2024年01月21日


一、动静态库的基本原理

动静态库的本质就是可执行程序的半成品。

为什么这么说呢?原因是在于我们的库可以存放打包的可执行程序。我们先进行讲解一下我们的可执行程序是怎么形成的吧:
我们的一堆头文件和源文件都会经历以下四个步骤:

1、预处理:完成头文件展开、去注释、宏替换、条件编译等,最终形成xxx.i文件。
2、编译:完成词法分析、语法分析、语义分析、符号汇总等,检查无误后将代码翻译成汇编指令,最终形成xxx.s文件。
3、汇编:将汇编指令转换成二进制指令,最终形成xxx.o文件。
4、链接:将生成的各个xxx.o文件进行链接,最终形成可执行程序。

想想就很复杂,如果我们是一整套的文件包呢,比如我们需要链接有5个源文件呢?这每一个都要进行上面四步的操作然后最后再将这五个.o文件进行链接,那么不就是很麻烦吗?所以我们需要将这五个.o文件先进行打包放到一个库里面,那么要用的时候去直接调用这个库,岂不是很简单?

在这里插入图片描述
实际上,所有库本质都是一堆目标文件(xxx.o)的集合,库的文件当中并不包含主函数而只是包含了大量的方法以供调用,所以说动静态库本质是可执行程序的“半成品”。

二、认识动静态库

1、全世界最简单的代码(libc-2.17.so)

我们用一个全世界最简单的代码来进行看一下,进行简单的讲解,一看就会:

#include<stdio.h>
int main()
{
    printf("hello linux\n");
    return 0;
}

最简单的代码运行结果大家都知道,就是hello linux。
在这里插入图片描述

通过这段简单的代码,我们来认识一下我们的动静态库:
大家思考过一个问题吗?就是我们的这个printf代码为什么能够这么简单就能够打印出来?我们肯定知道有一个库是C库,那么就好解释了,在这份代码当中我们可以通过调用printf输出hello world,主要原因是gcc编译器在生成可执行程序时,将C标准库也链接进来了。

我们用一个指令ldd来看一下这个可执行程序所依赖的库:
我们只用看一下我们的这个=>其链接的库的路径名为/lib64/libc.so.6
在这里插入图片描述

我们再通过ls命令看一下我们这个库是什么链接:
在这里插入图片描述
我们利用前面的知识,发现它是一个软连接,而其链接的是libc-2.17.so,而实际上该软链接的源文件libc-2.17.so和libc.so.6在同一个目录下,为了进一步了解,我们可以通过file 文件名命令来查看libc-2.17.so的文件类型。

在这里插入图片描述
此时我们可以看到,libc-2.17.so实际上就是一个共享的目标文件库,准确来说,这还是一个动态库。

在Linux当中,以.so为后缀的是动态库,以.a为后缀的是静态库。
在Windows当中,以.dll为后缀的是动态库,以.lib为后缀的是静态库。

我们再来明确一个概念,就是库的名称:
这里可执行程序所依赖的libc.so.6实际上就是C动态库,当我们去掉一个动静态库的前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。即c。

2、形成静态链接

实际上,我们的gcc和g++编译器默认都是动态链接,而我们如果想要静态链接,那么我们就需要后面加-static选项。

在这里插入图片描述

3、动静态库先驱总结

1、静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
2、动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
3、在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
4、动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

三、动静态库的特征

1、静态库

静态库是程序在编译链接的时候把库的代码复制到可执行文件当中的,生成的可执行程序在运行的时候将不再需要静态库,因此使用静态库生成的可执行程序的大小一般比较大。

优点:一次拷贝,终生收益,也就是该可执行程序就可以独自运行,不再需要库了。
缺点:使用静态库生成可执行程序会占用大量空间,特别是当有多个静态程序同时加载而这些静态程序使用的都是相同的库,这时在内存当中就会存在大量的重复代码。

2、动态库

动态库是程序在运行的时候才去链接相应的动态库代码的,多个程序共享使用库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。

在可执行文件开始运行前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。动态库在多个程序间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
在这里插入图片描述

优点:节省磁盘空间,且多个用到相同动态库的程序同时运行时,库文件会通过进程地址空间进行共享,内存当中不会存在重复代码。

缺点:依赖于这个动态库,有一天这个动态库丢失了就完了。

四、静态库的打包和使用

1、先创建两个头文件和两个源文件

创建一个sub.h和sub.c以及add.c和add.h这四个文件,我们依次进行写入讲解:

(1)add.h

#pragma once
extern int my_add(int x, int y);

(2)add.c

#include "add.h"
int my_add(int x, int y)
{
	return x + y;
}

(3)sub.h

#pragma once
extern int my_sub(int x, int y);

(4)sub.c

#include "sub.h"
int my_sub(int x, int y)
{
	return x - y;
}

在这里插入图片描述

2、打包

(1)将源文件形成目标文件

gcc -c add.c
gcc -c sub.c
在这里插入图片描述

(2)ar命令将所有目标文件打包为静态库

ar命令是gnu的归档工具,常用于将目标文件打包为静态库,下面我们使用ar命令的-r选项和-c选项进行打包。

  • r(replace):若静态库文件当中的目标文件有更新,则用新的目标文件替换旧的目标文件。
  • c(create):建立静态库文件。

[Semi@hecs-91732 class4]$ ar -rc lib.a add.o sub.o

在这里插入图片描述
我们可以用ar命令的-t选项和-v选项查看静态库当中的文件。

  • t:列出静态库中的文件。
  • v(verbose):显示详细的信息。

在这里插入图片描述

(3)将头文件和静态库文件分别组织起来

'那我们就可以创建两个文件夹,一个文件夹存放头文件,另一个文件夹存放静态库(保存着一堆目标文件)。

所以我们可以将我们的两个头文件add.h和sub.h都放进include的目录下,将生成的静态库文件lib.a放到一个名为lib的目录下,然后将这两个目录都放到mathlib下,此时就可以将mlib给别人使用了。

[Semi@hecs-91732 class4]$ mkdir -p mlib/include
[Semi@hecs-91732 class4]$ mkdir -p mlib/lib
[Semi@hecs-91732 class4]$ cp ./.h mlib/include
[Semi@hecs-91732 class4]$ cp ./
.a mlib/lib

在这里插入图片描述

(4)Makefile可以把上面所有的步骤全部完成(精选)

mylib=lib.a
CC=gcc

$(mylib):add.o sub.o
	ar -rc -o $(mylib) $^
%.o:%.c
	$(CC) -c $<
.PHONY:clean
clean:
	rm -f $(mylib) ./*.o
.PHONY:output
output:
	mkdir -p mlib/include
	mkdir -p mlib/lib
	cp ./*.h mlib/include
	cp ./*.a mlib/lib

在这里插入图片描述
make一下:
在这里插入图片描述

make output一下:
在这里插入图片描述

可视图:
在这里插入图片描述

(5)特别注意***

Makefile里面改一下(下面的库文件需要用到的):

mylib=libmymath.a
CC=gcc

$(mylib):add.o sub.o
	ar -rc -o $(mylib) $^
%.o:%.c
	$(CC) -c $<
.PHONY:clean
clean:
	rm -f $(mylib) ./*.o
.PHONY:output
output:
	mkdir -p mlib/include
	mkdir -p mlib/lib
	cp ./*.h mlib/include
	cp ./*.a mlib/lib

在这里插入图片描述

在这里插入图片描述

3、使用

我们很简单的在同一个文件中创建一个main.c的源文件,与lib这个静态库放在一起:
在这里插入图片描述

#include<stdio.h>
#include<add.h>
#include<sub.h>
int main()
{
    int a = 10;
    int b = 20;
    printf("%d + %d = %d\n", a, b, my_add(a, b));
    printf("%d - %d = %d\n", a, b, my_sub(a, b));
    return 0;
}

(1)方法1:使用-I -L -l选项

我们先介绍一下这三个选项:

  • I(i的大写):指定头文件搜索路径。

  • L:指定库文件搜索路径。

  • l(L的小写):指明需要链接库文件路径下的哪一个库。

第一个选项就是头文件的路径,我们直接用所有的头文件即可,路径只需要到倒数第二个存放全部头文件的文件即可。
第二个选项是库文件的搜索路径,是直接到库文件的那个路径即可。
第三个选项是那个我们创建的库的名字,就比如我的库文件的名字是libmymath.a,我们掐头去尾留中间,库名字就是mymath。

在这里插入图片描述

操作简明:
在这里插入图片描述
最后加上那个库即可,因为有可能该路径底下有更多的库文件不确定,所以就需要加上库的名称。

简要说明:

  • 因为编译器不知道你所包含的头文件add.h在哪里,所以需要指定头文件的搜索路径。
  • 因为头文件add.h当中只有my_add函数的声明,并没有该函数的定义,所以还需要指定所要链接库文件的搜索路径。
  • 实际中,在库文件的lib目录下可能会有大量的库文件,因此我们需要指明需要链接库文件路径下的哪一个库。库文件名去掉前缀lib,再去掉后缀.so或者.a及其后面的版本号,剩下的就是这个库的名字。
  • -I,-L,-l这三个选项后面可以加空格,也可以不加空格。

(2)方法2:把头文件和库文件拷贝到系统路径下

既然不能直接编译,那么我们就直接拷贝到系统文件不就好了吗?

[Semi@hecs-91732 friend]$ sudo cp mlib/include/* /usr/include/
[Semi@hecs-91732 friend]$ sudo cp mlib/lib/libmymath.a /lib64/

在这里插入图片描述
为什么之前使用gcc编译的时候没有指明过库名字?

因为我们使用gcc编译的是C语言,而gcc就是用来编译C程序的,所以gcc编译的时候默认就找的是C库,但此时我们要链接的是哪一个库编译器是不知道的,因此我们还是需要使用-l选项,指明需要链接库文件路径下的哪一个库。

4、补充知识

(1)erron

这个就是简单的错误码。
总而言之,当出现除零错误的时候,我们使用这个本身就在C语言中的全局变量中的erron的时候,能够看出错误码是什么,就能知道错误的点在哪。

(2)动静态库链接

  • 如果一个库中只有静态链接,那么不加-static默认是静态链接
  • 如果一个库中静态链接和动态链接都有,默认动态链接,加-static是静态链接

五、动态库的打包和使用

1、打包

(1)建立

首先先建立这些源文件和头文件,具体参考下面的代码:
在这里插入图片描述

在这里插入图片描述

(2)步骤1:让所有源文件生成对应的目标文件

  • -fPIC(position independent code):产生位置无关码。
gcc -fPIC -c add.c
gcc -fPIC -c div.c

在这里插入图片描述

  • -fPIC作用于编译阶段,告诉编译器产生与位置无关的代码,此时产生的代码中没有绝对地址,全部都使用相对地址,从而代码可以被加载器加载到内存的任意位置都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
  • 如果不加-fPIC选项,则加载.so文件的代码段时,代码段引用的数据对象需要重定位,重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的拷贝,并且每个拷贝都不一样,取决于这个.so文件代码段和数据段内存映射的位置。
  • 不加-fPIC编译出来的.so是要在加载时根据加载到的位置再次重定位的,因为它里面的代码BBS位置无关代码。如果该.so文件被多个应用程序共同使用,那么它们必须每个程序维护一份.so的代码副本(因为.so被每个程序加载的位置都不同,显然这些重定位后的代码也不同,当然不能共享)。
  • 我们总是用-fPIC来生成.so,但从来不用-fPIC来生成.a。但是.so一样可以不用-fPIC选项进行编译,只是这样的.so必须要在加载到用户程序的地址空间时重定向所有表目。

(3)步骤2:使用-shared选项将所有目标文件打包为动态库

[Semi@hecs-91732 solink]$ gcc -shared -o libmymath.so add.o div.o

在这里插入图片描述

(4)步骤3:将头文件和生成的动态库组织起来

我们将其和静态库一样分类到不同的文件路径下,头文件都分配到头文件中,生成的动态库分到目标文件中即可,也就是将add.h和div.h放到mlib/include这个路径下,我们将libmymath.so放到mlib/lib中即可。

[Semi@hecs-91732 solink]$ mkdir -p mlib/include
[Semi@hecs-91732 solink]$ mkdir -p mlib/lib
[Semi@hecs-91732 solink]$ cp ./*.h mlib/include
[Semi@hecs-91732 solink]$ cp ./*.so mlib/lib

在这里插入图片描述

(5)使用Makefile能够将上面所有步骤都囊括

mylib=libmymath.so
CC=gcc

$(mylib):add.o div.o
	$(CC) -shared -o $(mylib) $^
%.o:%.c
	$(CC) -fPIC -c $<
.PHONY:clean
clean:
	rm -rf $(mylib) ./*.o
.PHONY:output
output:
	mkdir -p mlib/include
	mkdir -p mlib/lib
	cp ./*.h mlib/include
	cp ./*.so mlib/lib

在这里插入图片描述

2、使用

(1)和静态库一样方法试一试

我们可以知道确实动静态库的使用肯定是不一样的,但是我们应该从相同的部分入手,也就是先利用静态库的那一套来对付静态库看可不可以。
main.c代码:

#include<stdio.h>
#include<add.h>
#include<div.h>
int main()
{
  int x = 10;
  int y = 0;
  printf("x + y = %d\n", Add(x, y));
  printf("x / y = %d\n", Div(x, y));
  return 0;
}

使用该动态库的方法与刚才我们使用静态库的方法一样,我们既可以使用 -I,-L,-l这三个选项来生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后仅使用-l选项指明需要链接的库名字来生成可执行程序。

[Semi@hecs-91732 friend]$ gcc main.c -I ./mlib/include -L ./mlib/lib -lmymath

在这里插入图片描述

需要注意的是,我们使用-I,-L,-l这三个选项都是在编译期间告诉编译器我们使用的头文件和库文件在哪里以及是谁,但是当生成的可执行程序生成后就与编译器没有关系了,此后该可执行程序运行起来后,操作系统找不到该可执行程序所依赖的动态库,我们可以使用ldd命令进行查看。

我们不知道还记不记得,前面的静态库是可以链接找到相对应的位置的,而这个动态库却找不到相对应的位置。
在这里插入图片描述

(2)解决方法1:拷贝.so文件到系统共享库路径下

我们直接将库文件拷贝到系统共享的库路径下,这样一来系统就能够找到对应的库文件了。

[Semi@hecs-91732 friend]$ sudo cp mlib/lib/libmymath.so /lib64

在这里插入图片描述

这样子就能跑起来(由于x/y=10我想演示一下错误的,所以出现除零错误了)。
在这里插入图片描述

(3)解决方法2:更改LD_LIBRARY_PATH

LD_LIBRARY_PATH是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH环境变量当中即可。

[Semi@hecs-91732 friend]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/Semi/linux/class4/solink/friend/mlib/lib

在这里插入图片描述
在这里插入图片描述

同样可以运行:
在这里插入图片描述

(4)解决方法3:配置/etc/ld.so.conf.d/

我们可以通过配置/etc/ld.so.conf.d/的方式解决该问题,/etc/ld.so.conf.d/路径下存放的全部都是以.conf为后缀的配置文件,而这些配置文件当中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。我们若是将自己库文件的路径也放到该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件了

首先将库文件所在目录的路径存入一个以.conf为后缀的文件当中。

[Semi@hecs-91732 friend]$ echo /home/Semi/linux/class4/solink/friend/mlib/lib > jasper.conf

在这里插入图片描述

然后将该.conf文件拷贝到/etc/ld.so.conf.d/目录下。

[Semi@hecs-91732 friend]$ sudo cp jasper.conf /etc/ld.so.conf.d

在这里插入图片描述

但此时我们用ldd命令查看可执行程序时,发现系统还是没有找到该可执行程序所依赖的动态库。

[Semi@hecs-91732 friend]$ ldd a.out

在这里插入图片描述

这时我们需要使用ldconfig命令将配置文件更新一下,更新之后系统就可以找到该可执行程序所依赖的动态库了。

[Semi@hecs-91732 friend]$ sudo ldconfig

在这里插入图片描述

而此时我们也就可以正常运行该可执行程序了。

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_70088010/article/details/135726472
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。