自学内容网 自学内容网

动静态库:选择与应用的全方位指南

目录

1 软链接

1.1 软链接的建立方式和观察现象

1.2 软链接的原理

2 硬链接

2.1 硬链接的建立方式和观察现象

2.2 硬链接的本质

2.3 我们用户不能给目录建立硬链接

3. 动静态库复习 

4 动静态库的制作

4.1 静态库的制作与使用

4.1.2 打包

4.1.3 静态库的使用

4.2 动态库的制作与使用

4.2.1  打包

4.2.2 使用 

4.3 动态库链接不到的四种解决方法

1 库安装

2 软链接

3  /etc/ld.so.conf.d配置文件

4 LD_LIBRARY_PATH环境变量

5 动静态库链接的选择

6 理解动态加载

6.1 在操作系统的角度去理解

6.2 编址

6.3 一般程序的加载问题(也就是静态库)

6.4 理解动静态库动态链接和加载问题 


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

指令解析:

  1. archive-file是静态库名(lib库名.a);object-files是要添加到库中的对象名(.o文件)。

  2. 常见的选项

-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 库安装

  1. 将库或其他软件安装到系统中,本质是把对应的文件,拷贝到系统默认的搜索路径中。

  2. 在64位系统,系统中库默认的搜索路径为/lib64、/usr/lib64;在32系统,系统中库默认的搜索路径为/lib、/usr/lib。

  3. 但是呢,这个方法是建议将别人写的成熟的库进行拷贝,如果只是自己写的,仅仅用于测试的简易的库,不建议直接拷贝进去

 

拷贝进去之后,这个文件立马就能运行了。
你也发现了,我一运行完就删除了,就是为了防止这种不成熟的库,污染我的库。


2 软链接

 

ldd Filename 可以查看文件所链接的库 


3  /etc/ld.so.conf.d配置文件

  1. /etc/ld.so.conf.d目录下的配置文件,用来指定额外的库文件的搜索路径,以便动态链接器能够在运行时找到并加载这些库文件。


4 LD_LIBRARY_PATH环境变量

LD_LIBRARY_PATH:是一个环境变量,在linux中,为动态链接器指定额外的库搜索路径


5 动静态库链接的选择

  1. 如果,静态库和动态库我们都提供了,那么编译器默认会选择的是静态链接
  2. 如果非要使用静态链接,那么就使用static选项
  3. 如果我们只提供了静态库,那么编译器也没用办法,只能对这个库使用静态链接,但是整个程序不一定全是静态链接。
  4. 如果我们只提供动态库,那么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,再对他们进行初始化

  1. 我们先明白一个点:在程序加载进入内存的时候是先加载进入代码还是先形成PCB ---PCB
  2. 就不说堆栈和共享区都是动态开辟的,在加载进入内存的时候开辟的,那么那些正文代码,初始化数据和未初始化数据,每个程序都是不同的,那么这个如何在程序未加载进入内存的时候,如何进行初始化呢? ---- 利用:可执行程序的一块特殊的文件区域,来进行对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)!