动静态库:选择与应用的全方位指南
目录
1 软链接
1.1 软链接的建立方式和观察现象
ln -s 被软链接文件 软链接文件
我们在观察下面的情况,就是软链接文件,是有自己独立的inode编号的,说明他是一个独立的文件。
1.2 软链接的原理
软链接本质上是一个独立的文件,软链接文件内容里面放的是链接文件的路径。
类似于Windows下的快捷方式。
2 硬链接
2.1 硬链接的建立方式和观察现象
ln 被硬链接文件 硬链接文件
我们观察下面,是可以发现hello.txt 和link.hard 其实上inode是一致的,这就说明link.hard其实上本质不是一个独立的文件
2.2 硬链接的本质
既然没有新建文件,那么就是一个新的文件名而已,在目录中插入了一段新的映射关系。
那么这样inode中一定有一个引用计数的变量用于记录这个inode编号有多少段映射关系。
那么硬链接的本质就是 在目录中插入了一段新的映射关系,并且让inode结构体中的引用计数++。
我们可以看到,这个查看文件信息是的这个文字我们从来没有提到过。
这个其实上就是文件的硬链接数:表明这个文件有多少个硬链接
我们回到上机目录,我们发现我们刚刚所处的目录居然有两个硬链接,但是,我们并没有给他创建硬链接啊,我们仔细对比了inode编号 目录的编号是于目录内的隐藏文件./是一致的。
所以我们得出结论,我们经常使用的 .. 和 . 其实就是目录的硬链接。
2.3 我们用户不能给目录建立硬链接
主要是因为,这样我们在查找文件的时候会陷入环路问题。
注:硬链接无法跨分区,因为只有在分区里面inode才唯一。
3. 动静态库复习
我们在编写代码的时候都使用过库。
动态库:libXXX.so 静态库:libYYY.a(库的真是名字其实是XXX和YYY那部分)
在Linux环境下,gcc默认链接的都是动态库,云服务器上其实连静态库都没有安装。
如果,要编译链接静态库,需要在后面加上 -static
4 动静态库的制作
动静态库的本质其实上就是一大堆的可执行程序。
将这些经过编译的二进制文件打包,这样就形成了库。
库的意义:这样隐藏了源代码,又提高了生产效率
4.1 静态库的制作与使用
4.1.2 打包
ar [options] archive-file object-files
指令解析:
-
archive-file是静态库名(lib库名.a);object-files是要添加到库中的对象名(.o文件)。
-
常见的选项
-c:创建库文件,如果库已存在,则会被覆盖。
-r:向库文件中添加.o文件,如果.o文件已在库中存在,则会被替换。
-t:列出库文件中包含的.o文件列表。
-v:在执行过程中显示详细的信息。
第一步:编译形成 .o 文件。
第二步:使用ar命令,将所有.o文件进行打包,形成静态库文件。
第三步:将库进行标准化。
libmyc.a:my_add.o my_sub.o //第二步:使用ar命令,将所有.o文件进行打包,形成静态库文件
ar -rc $@ $^
%.o:%.c //第一步:编译形成 .o 文件
gcc -c $<
.PHONY:clean
clean:
rm -rf *.a *.o mylib mylib.tgz
.PHONY:output //第三步:将库进行标准化
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -rf *.h mylib/include/
cp -rf *.a mylib/lib/
tar czf mylib.tgz mylib
4.1.3 静态库的使用
1.-I(i的大写):用来指定编译器搜索头文件的额外路径。
当编译器在编译过程中遇到#include指令时,它先会在标准的位置(当前目录或系统默认的头文件路径)来查找指定的头文件,如果查找不到,编译器就会使用-I指定的路径进行搜索。
2.-L:用来指定链接器搜索库文件的额外路径。
当链接器在链接中需要找到某个库文件(.so、.a),它先会在标准的位置(系统默认的库路径)中查找,如果查找不到,链接器就会使用-L指定的路径进行搜索。
3.-l(L的小写):用来指定链接器在链接过程中要链接的库。
补充:头文件的搜索路径:当前目录、系统默认的头文件路径(/usr/include、/usr/local/include)、gcc内置的标准头文件路径、命令行中通过-l选项指定的头文件路径。 库文件的搜索路径:系统默认的库文件路径(/usr/lib、/usr/local/lib)、gcc内置的库文件路径、命令行中通过-L选项指定的库文件路径、环境变量LIBARY_PATH中指定的路径。
这些选项分别用于控制编译和链接过程中的头文件、库文件的搜索路径和库文件的选择。
4.2 动态库的制作与使用
动态库制作的过程中只需要使用gcc就行了
4.2.1 打包
第一步:使用-fPIC选项,编译形成 .o 文件。
fPIC:产生位置无关码(position independent code)
- -fPIC:用于指示编译器生成与位置无关的代码,无论代码被加载到内存的哪个位置,它都能正确运行,而不依赖于它在编译或加载时的具体地址。这种特性通过使用相对寻址,而不是绝对寻址来实现的。这对于创建共享库是至关重要的,因为共享库可以在进程地址空间的任何位置被加载。
第二步:使用-shared,将所有.o文件进行打包,形成动态库文件。
- -shared:告诉编译器gcc生成一个共享库(.so或.dll文件)。
第三步:将库进行标准化
libmyc.so:my_add.o my_sub.o //第二步:使用-shared,将所有.o文件进行打包,形成动态库文件
gcc -shared -o $@ $^
%.o:%.c //第一步:使用-fPIC选项,编译形成 .o 文件
gcc -c -fPIC $<
.PHONY:clean
clean:
rm -rf *.so *.o mylib mylib.tgz
.PHONY:output //第三步:将库进行标准化
output:
mkdir -p mylib/include
mkdir -p mylib/lib
cp -rf *.h mylib/include/
cp -rf *.so mylib/lib/
tar czf mylib.tgz mylib
4.2.2 使用
动态库,再被使用的时候,是要在任何时候都要保证能被链接到的。
这个情况,就是在编译的时候,指定了路径去链接数据库。
但是,在执行程序的时候,由于没指明数据库,数据库也不再默认路径下,所以,就找不到。
4.3 动态库链接不到的四种解决方法
1 库安装
-
将库或其他软件安装到系统中,本质是把对应的文件,拷贝到系统默认的搜索路径中。
-
在64位系统,系统中库默认的搜索路径为/lib64、/usr/lib64;在32系统,系统中库默认的搜索路径为/lib、/usr/lib。
-
但是呢,这个方法是建议将别人写的成熟的库进行拷贝,如果只是自己写的,仅仅用于测试的简易的库,不建议直接拷贝进去
拷贝进去之后,这个文件立马就能运行了。
你也发现了,我一运行完就删除了,就是为了防止这种不成熟的库,污染我的库。
2 软链接
ldd Filename 可以查看文件所链接的库
3 /etc/ld.so.conf.d配置文件
- /etc/ld.so.conf.d目录下的配置文件,用来指定额外的库文件的搜索路径,以便动态链接器能够在运行时找到并加载这些库文件。
4 LD_LIBRARY_PATH环境变量
LD_LIBRARY_PATH:是一个环境变量,在linux中,为动态链接器指定额外的库搜索路径
5 动静态库链接的选择
- 如果,静态库和动态库我们都提供了,那么编译器默认会选择的是静态链接
- 如果非要使用静态链接,那么就使用static选项
- 如果我们只提供了静态库,那么编译器也没用办法,只能对这个库使用静态链接,但是整个程序不一定全是静态链接。
- 如果我们只提供动态库,那么gcc默认动态链接,如果非要静态链接,那么就会链接报错。
6 理解动态加载
6.1 在操作系统的角度去理解
一、动态库概念
动态库:也称为共享库,是一种包含代码和数据,可以在多个程序之间共享的文件,存放在磁盘上。
与静态库不同,静态库在程序编译时会被完全复制到可执行文件中,而共享库则在程序运行时被加载到内存中,如果多个程序使用同一个共享库,OS会让这些进程共享内存中的同一份库代码和数据,即:动态库的代码和数据在内存中只存在一份。
管理:系统中可以同时存在多个已经被加载的库,OS需要管理它们,先描述(包含了加载地址等信息)、再组织。
二、动态库加载的过程
检查依赖:程序启动时,动态链接器会检查该程序依赖的所有动态库。
搜索路径:动态链接器会在预设的库搜索路径中查找所需的动态库文件。
加载与映射:第一次加载、后续加载。
第一次加载:如果动态库尚未被加载到内存中,动态链接器会将该库加载到内存中,并映射到进程地址空间的共享区中。
后续加载:如果其他进程也需要共享这个库,动态链接器会检查内存中是否已存在该库;如果已存在,只需修改地址空间中共享区的映射关系,指向已存在的库副本;如果不存在,则重复第一次加载的过程。
优点:节省内存、易于更新、提高了程序的性能和安全性。
6.2 编址
其实上就是谈谈可执行程序的问题。
第一个问题:一开始我们的程序在没有加载进入内存的时候有没有地址? --- 有了
其实,我们的程序在没有加载进入内存的时候,根据类别(比如权限,访问属性等),把可执行程序划分成了几个区域 。
我们之前提到的进程地址空间,其实上本质就是一个结构体(mm_struct),我们结构体里面记载了各个区域的开始地址和结束地址(就是两个指针变量),利用这种双指针的记载方式,我们达成了分区的效果。
那么这就有一个问题?这个结构体(即进程地址空间结构体)是由谁来初始化的?每个结构体的代码大小都不同,那么正文段的大小也应该不同,结构体的数据怎么来?
---- 里面的数据很多都是从可执行程序中来的 (和操作系统,编译器都有关系)
编址:在编译和链接阶段,为程序和库中的符号(变量、函数)分配地址的过程,主要有绝对编址、相对编址两种方式。
可编址的范围:32位平台,[0, 2^32] -> [0, 4GB] ,64位平台,[0, 2^64] -> [0, 16GB]。
绝对编址:在编译和链接过程中,符号的地址是固定的,即:已经确定了符号的实际的物理内存地址。这种方式要求程序运行时,必须加载到特定的物理地址处,否则无法正确的运行。
绝对编址中的地址 == 实际的物理内存地址。
相对编制:也成为逻辑地址、虚拟地址。在编译和链接过程中,符号的地址是不固定的,而是相对于某个基地址的偏移量。这种方式允许程序在加载时动态确定实际地址,从而实现位置无关代码。
符号地址 = 基地址 + 偏移量。基地址在编译链接阶段是未知的,通常是由OS在程序加载时分配的虚拟地址,是在地址空间内的一个起始地址,如:0x400000。
回答前面提到的问题:地址空间、页表中的数据来自哪里?
- 那不就是直接用代码编址编好的地址,直接用作虚拟地址,来填充页表。
所以这里也提出一个观点:虚拟地址空间不仅仅OS系统要遵守,编译器也要遵守。
目前,使用的都是平坦模式下编址,为了兼容前面的相对编址,可以将全部代码看做一大块空间,那么,绝对编址下的代码的地址就是:0 + 偏移量。
每个可执行程序大小不同,说明了每个程序中各个区域虚拟地址范围也会不同。相应地,当这些程序被加载到内存变为进程时,则每个进程地址空间中各个区域的虚拟地址的范围也是不同的。
6.3 一般程序的加载问题(也就是静态库)
静态库,本身就是拷贝进入代码的,就按照普通代码,进行编址,然后加载到内存中,把编译出来的绝对地址,当做虚拟地址,进行页表映射。
一.先加载形成PCB和mm_struct,再对他们进行初始化
- 我们先明白一个点:在程序加载进入内存的时候是先加载进入代码还是先形成PCB ---PCB
- 就不说堆栈和共享区都是动态开辟的,在加载进入内存的时候开辟的,那么那些正文代码,初始化数据和未初始化数据,每个程序都是不同的,那么这个如何在程序未加载进入内存的时候,如何进行初始化呢? ---- 利用:可执行程序的一块特殊的文件区域,来进行对mm_struct进行初始化
二.再将整个可执行程序加载进入内存当中,在构建页表
代码也是数据,我们将代码加载进入内存的时候,那此时,这条代码既有了在代码编译时形成逻辑地址,也有了在内存上的物理地址。那么此时也就有了虚拟地址和物理地址的相互对应,这样就可以构建页表了
问:那么CPU如何找到程序的入口地址,然后运行程序的过程
用可执行程序初始化mm_struct的时候,就会顺便把头文件里的main函数的虚拟地址加载进入CPU的PC指令当中,再利用MMU和页表配合,就可以完成虚拟到物理的转化,这样就找到了程序入口。
然后,将指令读取到CPU中的指令寄存器中,指令:因为要调用函数,所以再将0x100000进行虚拟到物理的转化,找到函数,再执行函数
所以我们在这里输出一个结论:进程地址空间是一个由操作系统(创建PCB和mm_struct)+ 编译器(编译代码和形成逻辑地址)+ 计算机体系结构(虚拟地址到物理地址的转化)三者合作形成的概念。
问题:CPU如何对各式各样的指令,来做出正确的反应呢?
CPU其实就只能识别二进制,我们将所有的代码翻译成二进制文件的时候,CPU内部有一个指令集,里面记载了对于各式各样的二进制指令,CPU该如何去进行对应的动作。所以,将我们传入的指令与指令集相对应,然后做出反应
6.4 理解动静态库动态链接和加载问题
库函数和代码都加载进入内存的时候,调用库函数的方法的逻辑地址都是一致的
在编译器编译代码内部的动态库方法,并不会赋予它一个新的逻辑地址,而是会沿用动态 库内部这个方法的逻辑地址
我们知道,动态库的代码在虚拟地址映射的时候是映射到共享区的,所以在正文代码在执行的时候,执行到了动态库中的方法,此时,就要跳转到共享区,利用虚拟地址( start+偏移量)找到该方法,在共享区的位置,然后进行虚拟地址到物理地址的转化,找到该方法在内存上的位置,读取到CPU内部,开始执行代码
原文地址:https://blog.csdn.net/2301_76653277/article/details/143440991
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!