【Linux】动静态库

一、静态库与动态库的相关概念
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库

动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码

在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

二、静态库
库的名字必须以lib开头,静态库以.a结尾,库的名字则是开头和结尾中间的一段。

2.1 ar指令
ar 指令用于创建、修改和提取归档文件的工具。在软件开发中,ar 常被用来创建静态库(.a 文件),这些静态库包含了多个编译后的对象文件(.o 文件)。

格式:ar [选项] [库文件名][添加到库中的文件]

常用选项:

r:替换归档文件中的文件。如果归档文件中已经存在同名文件,则替换它。
c:创建一个新的归档文件。如果指定的归档文件已经存在,则会被覆盖。
我这里ar指令的格式并不完整,想要了解更多,大家可以去其他文章中了解。

向Makefile中写入命令,通过make快速执行多个命令,通过上图我们可以看到静态库确实被创建出来了。

2.2 创建静态库
注意:在使用静态库的那部分中,对创建静态库的方法进行了改进。

由于我的程序会使用到四种方法,这里分别创建这四个方法的.h文件和.c文件,将运算方法的函数声明写到.h文件中,将运算方法的函数体写入到.c文件中。

这里我们创建一个我们想执行的文件,这个文件中会使用到上面的四个运算方法,那么想执行这个文件,我们就需要将我们想执行的文件与四个运算方法的源文件一起编译,通过下图我们发现编译成功并且运行成功了,在gcc命令中我们并没指定方法的头文件的位置,这是因为头文件的查找是在当前目录或是指定目录。

但是这样的编译方法并不好,因为每一次编译都需要将所有的源文件重新进行一次编译过程,所以在编译多文件项目时,通常都是将所有的源文件编译成.o文件后再进行链接形成可执行程序,当我们想形成多个可执行程序,并且他们所需要的源文件分别是上面的其中几个,我们只需要将源文件编译后形成的.o文件进行组合链接就可以了。

这里我写一个Makefile帮助我们快速的形成.o文件,通过下图我们可以知道形成可执行程序Test需要依赖后面的五个.o文件,但是默认情况下当前目录并不存在这几个.o文件,所以需要根据依赖关系进行推导,这里的%.c就会将我们当前目录下的所有.c文件全部展开,%.o就会帮助我们形成同名的.o文件,$ < 指的是当前依赖的文件,每次处理一个 .c 文件时,$ < 都会被替换为当前正在处理的 .c 文件名。

我们发现我们的可执行程序是方法的.o文件与我们需要执行文件的.o文件链接而成的,若是我们将方法的.o文件全部打包,当别人想使用这些方法时,只需要将它的文件先编译成.o文件,再与打包后方法中.o文件进行链接,即可形成他所需要的可执行程序。所以我们打包所需要的并不是方法的.c文件,而是方法的.o文件,这样就可以让使用者使用方法时,就不需要从.c文件开始重新编译方法的源文件,而是直接链接方法的.o文件。

但是.o文件中全部是二进制的,用户也不知道.o文件中有什么方法,所以还需要将头文件也打包,所以用户在写程序时,就可以将方法的头文件包含在源文件中,程序中就可以使用方法了,用户直接要将自己的源文件编译成.o文件,再与方法的.o文件进行链接即可,但是如果需要的方法很多,那么链接时需要写很多的.o文件,写漏一个就可能会导致可执行程序生成失败,所以就有了库的概念,将所有方法的.o文件和.h文件全部打包到库中。

2.3 使用静态库
这里我先使用最原始的方法来使用库,当只有一个源文件时,我们想编译源文件形成可执行程序时,它提示我没有对应方法的头文件,那我将头文件拷贝到当前目录,但是光有头文件还不够,再将存储方法的库拷贝到当前目录。

当我们再次编译源文件时,它给我报了一个链接错误,我们将存有方法的库拷贝到当前目录了,gcc没有使用还报链接错误,这是因为我们写的库是第三方库,gcc默认不认识,所以这里gcc带-l选项后面皆库名称,让gcc直接链接指定的库,但是编译源文件还是报错,那我们再带一个-L选项指定库在当前路径下,再编译发现没有报错,运行程序发现是我们所需要的结果。

我们之前在编写程序时,使用了C标准库中的函数,再使用gcc的时候,并没有指定库的名字和库所在的路径,这是因为gcc是专门处理C语言的编译器,它自身就会帮你找到C标准库然后链接。

ldd命令是用来专门查询可执行程序所依赖库的,我们使用ldd查询刚刚生成的可执行程序时发现没有我们自己所写的库,这是因为ldd只能查询可执行程序的动态库,静态库在链接的时候,就已经被拷贝到可执行程序中了,所以无法被查找出来。

需要注意的是gcc默认是动态链接的,但是如果个别库只提供静态库,gcc也只能局部性的静态库进行静态链接,其他库则正常动态链接,但是gcc如果带了-static选项,则形成可执行程序所依赖的所有库必须是静态库。

上面使用编译源文件时,将方法头文件和库与源文件放在同一个目录下,但是这样使用库未免也太不方便了吧,所以我们为别人提供库时,头文件是头文件,库是库,分别放在不同的路径下,打包好再交给别人使用。

我在Makefile中添加了一个发布选项,使用该选项时,会在当前目录下创建一个目录mymath_lib,并在mymath_lib目录中子目录include和lib,创建好后将方法的头文件存放到include中,库文件存放在lib中,这样头文件和库文件就被分开,若是想被其他人使用,就可以将mymath_lib这个目录进行打包压缩,再发布到网上,使用者可以在网上下载后再解压拆包,解压拆包后就可以获得到头文件和库文件,我们可以将库进行安装,系统搜索头文件默认是在/usr/include这个路径下的,搜索库文件是在/lib64这个目录下的,所以我们的安装工作就是将头文件和库文件分别拷贝到系统指定的这两个目录中,这也就是我们常说的安装开发环境。

我这里为了不污染我的系统,我就不进行拷贝,直接使用。

首先我直接编译源程序,发现源程序找不到方法的头文件,所以gcc带-I选项后面跟头文件所在的路径,就是告诉编译器不仅仅要在系统指定的目录下进行查找,也要在我们指定的目录下进行查找,再次编译发现报了链接错误,gcc默认不认识这个库,所以这里gcc带-l选项后面皆库名称,让gcc直接链接指定的库,再次编译发现编译器找不到库,所以我们再带一个-L选项后面跟库所在路径,再编译发现没有报错,也生成了可执行程序,运行程序发现符合我们的预期。

gcc的三个选项:

-I :代表新增头文件搜索路径
-l :指明链接的库名称
-L:代表新增库文件的搜索路径
若是将库文件和头文件拷贝到系统指定目录下,则不需要带-I选项和-L选项,只需要指明库名称。

三、动态库
3.1 创建自己的动态库
创建动态库需要注意下面三点:

shared: 表示生成共享库格式
fPIC:产生位置无关码(position independent code)
动态库以.so结尾
位置无关码会在相对编址那里讲到。

示例:

[root@localhost linux]# gcc -fPIC -c Add.c Sub.c Mul.c Div.c
[root@localhost linux]# gcc -shared -o libmymath.so *.o
1
2
动态库比静态库更重要,因为我们发现形成动态库没有使用其他工具,直接使用gcc就可以形成,gcc不仅仅可以形成可执行程序还可以形成动态库,将形成动态库的方法内置到了编译器中,而形成静态库却没有,侧面印证了动态库更重要。

这里动态库的创建与静态库的创建基本没什么区别,只有生成库的命令有所不同。先是编译方法文件生成.o文件,再将.o文件打包生成库文件,再使用发布指令,将方法的头文件和库文件分别放在同一个目录下的两个子目录下,这样一个简单的动态库就制作完成了,将整个mymath_lib拷贝到使用者的源文件的同级目录下,就可以直接使用了。

3.2 使用动态库
首先我直接编译源程序,发现源程序找不到方法的头文件,所以gcc带-I选项后面跟头文件所在的路径,就是告诉编译器不仅仅要在系统指定的目录下进行查找,也要在我们指定的目录下进行查找,再次编译发现报了链接错误,gcc默认不认识这个库,所以这里gcc带-l选项后面皆库名称,让gcc直接链接指定的库,再次编译发现编译器找不到库,所以我们再带一个-L选项后面跟库所在路径,再编译发现没有报错,也生成了可执行程序,到目前位置使用动态库和静态库的操作完全是一样的。

但是当我们运行程序的时候,我们发现报错了,说找不到动态库在哪里,可是编译的时候我们不是告诉编译器动态库在哪里了吗?

这是因为静态库在编译的过程中,已经将静态库与可执行程序打包在一起了,所以执行程序的时候,则不需要找到静态库,但是可执行程序与动态库是分离的,所以在执行可执行程序的时候,必须要找到动态库。可是编译的时候我们不是告诉编译器动态库在哪里了吗?你是告诉编译器动态库在哪了,可以可执行程序加载运行的时候,编译器的工作周期就结束了,接下来就是系统的工作了,而这个库所在的位置又不在系统的默认搜索路径,所以系统找不到这个库。

3.3 使操作系统找得到动态库的方法
3.3.1 直接将头文件和库文件安装到系统指定目录下
下面我将所需要的头文件和库文件安装到指定的目录下,在指定目录下也能分别查到这些文件,当我们编译源文件时,发现只需要指定链接的库名称即可,运行可执行程序时符合预期,使用ldd指令查询可执行程序依靠的文件,也能查出来动态库所在的位置。

3.3.2 通过建立软链接
我们可以在源文件所在的目录下创建一个软链接来指向动态库,这样可执行程序就可以找到对应的动态库了。

我们还可以不将库安装到系统指定目录下,而是将库的软链接安装到指定的目录下,这样可执行程序同样可以找到动态库。

3.3.3 修改环境变量
我们知道可执行程序在运行之前系统需要找到程序所依赖的动态库在哪里,系统不仅仅会在自己默认的路径下进行查找,还会根据环境变量LD_LIBRARY_PATH提供的路径下进行查找,所以我们只需要将自己动态库所在的路径添加的环境变量中,就可以让系统找到程序所依赖的动态库。但是我们这里修改的环境变量是内存级的,所以重新启动机器后就会恢复成原样。

3.3.4 直接修改系统关于动态库的配置文件
系统中存在一个配置文件目录/etc/ld.so.conf.d/,这里面的文件都是系统管理系统加载动态库的配置文件,我这里挑一个文件查看内容,发现文件内容是一条路径,也就是说我们在这个目录中创建一个配置文件,并将我们动态库所在的路径写入到这个配置文件中,之后操作系统就可以找到可执行程序所依赖的动态库所在的位置了,下面没有找到的原因是配置文件没有更新,我们更新一下就能够找到了。通过添加配置文件的方式,就算重新打开机器,这个配置文件依旧存在,操作系统也能够找到可执行程序所依赖的动态库了。

四、动态库的加载
4.1 ELF格式
在Linux操作系统下,通常我们形成的可执行程序的格式为ELF格式,源代码在编译过程中不是简单的被编译成二进制,这些二进制都是有规则的,通常源代码被编译后会被天然的分成几个部分:代码区、全局数据区、只读数据区、所需要的属性和字段和符号表,由于堆栈是动态运行 ,所以在文件中堆区、栈区不存在。符号表中会记录可执行程序会调用哪些函数,这些函数对应在哪个库,并且在库的哪个地址中,在链接的过程中,会将对应库中方法的地址写入到符号表中,将可执行程序与库中特定的方法进行关联,这就叫做动态链接。

当我们需要运行我们的可执行程序时,需要将可执行程序加载到内存中,但是单单可执行程序并不能让程序运行起来,因为可执行程序中只记录了所需方法的地址,并没有方法的实现,所以还需要将程序所依赖的库加载到内存中。所以要运行动态链接的程序,不仅仅程序需要被加载,它所依赖的库也需要被加载。

4.2 可执行程序中的地址
在可执行程序没有被加载到内存中时,程序中就存在了地址的概念了,变量名、函数名等在通过编译以后就不存在了,全部变为了地址,既然有地址这个概念,那么他就有自己对代码编址的一套规则,这套编址规则基本上就是按照进程地址空间的方式进行编址,进程地址空间并不仅仅是操作系统中的概念,在编译器进行编译的时候,也会按照这样的规则进行编译,可执行程序在磁盘中就被按照进程地址空间的方式进行编址,当程序被加载到内存中时,操作系统会创建一个新的进程,并为其分配一个进程地址空间。然后,操作系统会将可执行文件从磁盘上读取到内存中,并将其映射到进程地址空间的相应位置。

在源文件中,若A函数调用了B函数,在编译形成可执行程序后,就会转换为call地址,这个地址就是B函数的地址,也是虚拟地址,也就是说可执行程序在未被加载到内存中的时候,就有了“虚拟地址”的概念,通常在磁盘中我们更愿意称这些地址为逻辑地址。逻辑地址是基于“基地址+偏移量”的概念来构建的,所以我们可以理解可执行程序在被编址时,就是采用了“基地址+偏移量”的方式进行编址的,只不过它的基地址为0,偏移量为[0~FFFFFFFF]。这种认为起始偏移量为0的,可执行程序的各个部分被连续地放置在虚拟地址空间的一个线性区域内,对可执行程序进行编址的方式,我们在编译语言中称之为平坦模式。

4.3 绝对编址和相对编址
绝对编址
绝对编址是指直接给出存储单元在主存中的绝对地址,即物理地址。在绝对编址方式下,每个存储单元都有一个唯一的地址码,用于标识该存储单元在内存中的位置。绝对编址的优点是地址唯一、访问直接,但缺点是灵活性较差。由于每个存储单元的地址都是固定的,因此当程序或数据需要在内存中移动时,必须重新修改所有相关的地址。这种编址方法更适合在平坦模式下对可执行程序进行编址。

相对编址
相对编址是指程序中的指令或数据在内存中的位置是相对于某个基准点(如程序起始地址、当前指令地址等)来确定的。由于地址是相对于某个基准点来确定的,因此当程序或数据在内存中移动时,只需要修改基准点的地址,而不需要修改所有相关的地址。

在创建动态库的时候,需要加fPIC选项产生位置无关码,位置无关码是指代码在内存中的位置发生变化时,仍然能够正确执行的代码,位置无关码与相对编址的基础机制相同,都采用了相对地址的概念,所以为了让动态库被加载到内存的任意位置都能够被使用,所以创建动态库的时候需要位置无关码,所以相对编址的方式更适合类似于动态库的创建。

4.4 动态库的加载过程
加载到内存中的动态库,操作系统都要为其在进程地址空间中的共享区中开辟属于它们对应的地址空间范围,再通过页表映射到物理内存中。

操作系统做不到将动态库每次都加载到共享区中的特定位置,当一个动态库在后面一段时间内不会被使用,那么这个动态库所对应的地址空间范围会被删除,当程序再一次想使用这个库时,需要将这个库再次加载到内存中,操作系统会在一次为其在共享区中为其开辟地址空间范围,但是它原来所对应的位置可能会被占用,所以操作系统做不到将动态库每次都加载到共享区中的特定位置,也不想做到,操作系统想做到让库在共享区的任意位置形成地址空间范围,都能够正常运行。

当源文件被编译成可执行程序以后,可执行程序的符号表中会存储程序使用指向动态库的库名称、函数名称的符号,代码区中调用库函数的部分转化为对应的符号,程序要运行就需要将程序和程序所依赖的库加载到内存中,操作系统会在这时候为每个动态库在进程地址空间中的共享区中开辟一个地址空间范围,操作系统可以在共享区中的任何一个地方开辟地址空间范围,但是当开辟完后这个地址空间范围的地址就会被固定,这时候符号表中的指向库名称的符号就会与地址空间范围的起始地址相关联,动态链接器根据库在内存中的加载地址计算出来的对应函数在库中的偏移量,然后将库的地址和函数的偏移量替换到可执行程序中,当代码运行到调用库函数的时候,就可以通过库的起始地址和偏移量找到库函数在进程地址空间所对应的位置,再通过页表的映射可以找到库函数在物理内存中的位置,进而使用库函数,调用完函数后,就从共享区中返回到代码区中继续执行下面的代码。

这样就能够让动态库在进程地址空间中的共享区中随意加载。动态库就是采用了相对编址的方式,才做到了与位置无关性。

我们还发现我们在调用库中的方法时,都是在进程地址空间中进行跳转,以前我们在调用我们自己写的函数时,就是在代码区中进行跳转,也就是在进程地址空间中跳转,无非现在调用库函数的时候,在进程地址空间中跳转的更远而已,所以将动态库映射到进程地址空间中以后,调用库函数与调用我们自己的函数就没有区别了。

还有一个问题,当一个可执行程序要运行时,需要将它所依赖的库加载到内存中,那么这个可执行程序是第一个依赖这些库并且还是第一个被运行的吗?并不一定,所以在这个可执行程序运行的时候,它所依赖的库就可能已经被加载到内存了,这时候就只需要将它所依赖的库映射到它形成进程的进程地址空间中,再将可执行程序中与库相关的符号替换成库的地址。无论有多少可执行程序运行时,依赖了同一个动态库,在系统中这个库也只会被加载一次,这也是动态库被叫做共享库的原因。而静态库则是在链接的时候就被拷贝到可执行程序当中,若是有很多程序依靠静态库,那么这个静态库就会在内存中存在很多套,导致内存的浪费。

4.5 再谈进程地址空间

我们知道在源程序被编译成可执行程序以后,可执行程序的内部就有了虚拟地址,当程序被加载到内存以后,里面的虚拟地址相对于整个程序来说是没有变化的,并且在加载到内存以后,程序需要占据内存空间,则程序就有了物理地址,程序每一条指令在内部都有自己的虚拟地址,而每一条指令又占据物理内存空间,则每一条指令都有自己的物理地址,那么页表中就可以将虚拟地址和物理地址映射起来了。在ELF可执行程序的头部字段entry中存放了整个程序的入口,也是程序开始执行的第一条指令的虚拟地址,CPU要先找到程序的入口,CPU中的指令寄存器会读到entry中main函数的虚拟地址,然后CPU会找到进程的PCB,在通过PCB找到进程的进程地址空间,将虚拟地址通过页表转换为物理地址,CPU就找到了程序的入口,接下来就可以执行下面的指令了,由于程序中每一条指令的地址都是虚拟地址,那么CPU在读取到每一条指令都是指令的虚拟地址,所以CPU在执行程序的时候,需要不断读取指令的虚拟地址,将虚拟地址转化为物理地址,才能够不断的执行指令。

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_55401402/article/details/143737462

版权声明:
作者:SE_Wang
链接:https://www.cnesa.cn/2917.html
来源:CNESA
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
【Linux】动静态库
一、静态库与动态库的相关概念 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking) 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。 二、静态库 库的名字必须以lib开头,静态库以.a结尾,库的名字则是开头和结尾中间的一段。 2.1 ar指令 ar 指令用于创建、修改和提取归档文件的工具。在软件开发中,ar 常被用来创建静态库(.a 文件),这些静态库包含了多个编译后的对象文件(.o 文件)。 格式:ar [选项] [库文件名][添加到库中的文件] 常用选项: r:替换归档文件中的文件。如果归档文件中已经存在同名文件,则替换它。 c:创建一个新的归档文件。如果指定的归档文件已经存在,则会被覆盖。 我这里ar指令的格式并不完整,想要了解更多,大家可以去其他文章中了解。 向Makefile中写入命令,通过make快速执行多个命令,通过上图我们可以看到静态库确实被创建出来了。 2.2 创建静态库 注意:在使用静态库的那部分中,对创建静态库的方法进行了改进。 由于我的程序会使用到四种方法,这里分别创建这四个方法的.h文件和.c文件,将运算方法的函数声明写到.h文件中,将运算方法的函数体写入到.c文件中。 这里我们创建一个我们想执行的文件,这个文件中会使用到上面的四个运算方法,那么想执行……
<<上一篇
下一篇>>