当前位置: 首页 > news >正文

轻松Linux-8.动静态库的制作及原理

来啦来啦


1.动静态的制作和使用

动静态库,相信我们都有接触到,但什么是库呢?库其实就是,成熟的、已经写好的、可复用的代码,我们只要引用头文件即可。其中动静态库的区别,是代码链接运行时的区别(后面会介绍)。没有库上来就是现成的,都是大佬们一次次迭代优化出来,所以库的意义非同寻常。

在Linux中:静态库-- .a(都是文件后缀),动态库-- .so。

在windows中:静态库-- .lib,动态库-- .dll。

↓我们先来看看长什么样↓

1.1动静态库的制作

动静态库的制作其实并不复杂(实则不然,底层过程还是复杂的),只要在指令上操作一下即可。

静态库制作的指令:

不懂Makefile的可以去看第三篇拓展语法
也可以去查资料问Ai
libmystdio.a:mystdio.oar -rc $@ $^
%.o:%.cgcc -c $<.PHONY:clean
clean:rm -rf *.a *.o stdc*.PHONY:output
output:mkdir -p stdc/includemkdir -p stdc/libcp -f *.h stdc/includecp -f *.a stdc/libtar -czf stdc.tgz stdc

ar是gun的归档工具,rc表示replace 与 create。

这里的-tv

t:表示列出静态库中的文件

v:表示详细信息

动态库的制作指令:

不懂Makefile的可以去看第三篇拓展语法
也可以去查资料问Ai
libmyc.so:mystdio.ogcc -shared -o $@ $^
mystdio.o:mystdio.cgcc -fPIC -c $<.PHONY:output
output:mkdir -p stdc/includemkdir -p stdc/libcp -f *.h stdc/includecp -f *.so stdc/libtar -czf stdc.tgz stdc.PHONY:clean
clean:rm -rf *.so *.o stdc*

-shared:表示生成共享库格式

-fPIC:表示产生位置无关码(position independent code),在后面会讲

对于动态库,只有程序运行的时候才会去链接动态库的代码,并且多个程序可以共享同一个动态库。一个与动态库链接的可执行程序一般不会包含外部函数所在目标的整个机器码,而是会包含一个它要用到的函数入口地址的表

在程序运行之前,外部函数的机器码会由操作系统从磁盘的目标动态库中复制到内存上,然后由动态链接器解析程序库相关的代码,再修改程序中的重定位表,填充共享库相关函数在内存中的地址,这个过程就是动态链接。

因为动态库可以被多个程序共享,所以动态链接可以使文件更小,更节省磁盘空间。

1.2库的使用

头⽂件和库⽂件安装到系统路径下
gcc main.c -lmystdio头⽂件和库⽂件和我们⾃⼰的源⽂件在同⼀个路径下
gcc main.c -L. -lmystdio头⽂件和库⽂件有⾃⼰的独⽴路径
gcc main.c -I头⽂件路径 -L库⽂件路径 -lmystdio-L: 指定库路径
-I: 指定头⽂件搜索路径
-l: 指定库名库⽂件名称和引⼊库的名称:去掉前缀lib,动态库去掉后缀.so,静态库去掉后缀.a,
如:libmystdio.so -> mystdio

静态库的使用:

对应静态链接的程序,即使静态库被删除,也能正常运行。

动态库的使用:同静态库一样。

使用ldd 可以查看库或可执行程序的依赖。


2. .o文件

我们在开始学习编程时,大多时间都是在windows上练习的,而windows上强大的IDE封装已经非常完美,我们一般编译代码时也是一条龙服务,一键构建虽然方便,可是一旦出现链接问题,大部分人就不知所措了。

在学习Linux时,我们基本都会练习使用gcc去编译文件,相信也不会太过陌生,我们在这就再探编译和链接。

在我们使用gcc -c 编译一个或多个.c文件时,会产生相应的.o文件,这个就是目标文件。如果我们有某一个文件需要修改时,我们只需单独编译那个被修改的文件,这样就不需要重新编译整个项目,编译产生的目标文件是一种文本格式为ELF二进制文件,ELF格式是对二进制代码的一种封装。如图↓


3.ELF文件

3.1ELF是什么

要想深入了解编译和链接,我们就必须来了解ELF文件。

对于常见的ELF文件有一下几种:

1.可重定位文件(Relocatable File):.o文件。也包含适合于其它目标文件链接,来创建可执行文件或者共享目标文件的代码与数据。

2.可执行程序(Executable File):可执行程序本身也是ELF文件。

3.共享目标文件(动态库,Shared Object File):.so文件。

4.核心转储文件(Core Dump File):存放当前进程的执⾏上下⽂,⽤于dump信号触发、程序崩溃时生成的内存快照,用于调试。

ELF文件的构成:

1.ELF头(ELF Header):位于文件的开头,用于定义文件的基本属性,已经定位文件的其它部分。

2.程序头表(Program Header Table):列举了所有有效的段(segments)以及它们的属性,记录了每个段的起始位置、偏移和长度,而这些段都是在紧密得放在二进制文件中,需要有段表的索引信息,来将它们分开。

3.节头表(Section Header Table):描述节(Sections)信息,供链接器和调试工具使用。

4.节(Section)/段(Segment):在连接器的视角下是节,在加载器的视角下是段,包含了特定类型的数据信息。ELF文件的各种数据和信息都存储在不同节中,例如代码节中存储着可执行代码。

其中最常见的节有:

代码节(.text):记录了机器指令,是程序执行的主要部分。

数据节(.data):记录着已初始化的全局变量和局部静态变量。

3.2ELF合并

在链接时,将每个ELF文件的section合并,以及各个部分进行合并,还有与库的合并,因为涉及到的东西过多,也不是简单的合并,不作过多解释,可以去看看其他大佬的相关博客,或者问问ai。

一个ELF有多个且不同section,在文件加载到内存的时候也会进行section的合并,形成segment。

具体的合并原则在链接成ELF文件的时候已经记录在ELF的程序头表(Program Header Table)里了。合并的原则:相同的属性,例如可写、可读、可执行、要申请空间等等。

section为什么要合并成segment呢?

主要是为了减少页面碎片,提高内存使用效率,想象一下,如果一页的大小为4096字节,而一个section大小恰好为4097字节,那就要浪费几乎一整页。另外,将相同属性的section合并在一起,还可以优化权限访问控制。

接下来用用链接视图(Linking view)和执行视图(Execution view)来理解程序头表和节头表:

链接视图------对应节头表(Section Header Table)------在链接时起作用:

文件结构粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件里的各个部分的信息。

.text节:保存了程序代码指令的代码节。

.data节:保存了已初始化的全局变量和局部静态变量。

.rodata节:保存了只读数据,例如常量字符串,因为是只读数据,所以只能在text段内找到rodata。

.BSS节:为未初始化的全局变量以及局部静态变量预留出位置。

.symtab节:Symbol Table也就是符号表,记录了源码里变量名、函数名和代码的对应关系。

.got(全局偏移表) .plt(过程链接表)节:.got保存了全局偏移表。.got与.plt共同提供了对导入的共享库函数的访问入口,由动态链接器进行在运行时进行修改。

执行视图------对应程序头表(Program Header Table)------在程序加载运行时起作用:

操作系统根据程序头表来加载可执行文件,完成进程内存的初始化。可以说,一个可执行程序一定有Program Header Table。

所以,静态链接基本上将库和.o文件合并到一起,不同.o文件合并时,地址变量表也会进行修正,链接器会根据我们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,这就是静态链接的过程。更多细节由于涉及到汇编(up的汇编水平比较低),便不作过多介绍。

3.3ELF加载与进程地址空间

3.3.1虚拟地址(逻辑地址)

我们都知道一个程序想要运行,就要先加载进内存,但是怎么知道ELF程序加载进入内存时的地址呢?

答案是:现代计算机都用“平坦模式”进行工作,所以会让ELF文件对自己的代码和数据进行统一编址,下面用objdump -S来简单看一段汇编代码↓

这是部分复制出来的代码
0000000000001160 <_start>:1160:	f3 0f 1e fa          	endbr64 1164:	31 ed                	xor    %ebp,%ebp1166:	49 89 d1             	mov    %rdx,%r91169:	5e                   	pop    %rsi116a:	48 89 e2             	mov    %rsp,%rdx116d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp1171:	50                   	push   %rax1172:	54                   	push   %rsp1173:	4c 8d 05 36 04 00 00 	lea    0x436(%rip),%r8        # 15b0 <__libc_csu_fini>117a:	48 8d 0d bf 03 00 00 	lea    0x3bf(%rip),%rcx        # 1540 <__libc_csu_init>1181:	48 8d 3d c1 00 00 00 	lea    0xc1(%rip),%rdi        # 1249 <main>1188:	ff 15 52 2e 00 00    	callq  *0x2e52(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>118e:	f4                   	hlt    118f:	90                   	nop0000000000001249 <main>:1249:	f3 0f 1e fa          	endbr64 124d:	55                   	push   %rbp124e:	48 89 e5             	mov    %rsp,%rbp1251:	48 83 ec 10          	sub    $0x10,%rsp1255:	48 8d 35 a8 0d 00 00 	lea    0xda8(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>125c:	48 8d 3d a3 0d 00 00 	lea    0xda3(%rip),%rdi        # 2006 <_IO_stdin_used+0x6>1263:	e8 94 00 00 00       	callq  12fc <MyFopen>1268:	48 89 45 f8          	mov    %rax,-0x8(%rbp)126c:	48 8b 45 f8          	mov    -0x8(%rbp),%rax1270:	48 89 c7             	mov    %rax,%rdi1273:	e8 77 01 00 00       	callq  13ef <MyFclose>1278:	b8 00 00 00 00       	mov    $0x0,%eax127d:	c9                   	leaveq 127e:	c3                   	retq   

这里最左侧的一列就是ELF的虚拟地址,严格来讲严格叫逻辑地址,即起始地址 + 偏移量,但我们认为的起始地址是0,也就是说,虚拟地址在还没加载到内存的时候,系统就已经把可执行程序进行统一编码了。

并且进程在创建的时候mm_struct、vm_area_struct就从ELF中的各个segment中读取到地址数据,因为每个segment都有自己的起始地址和长度,所以可以用来初始化内核结构中的[start, end]等的范围数据,之后再用详细地址,去填充进程的页表。

因此,不光要OS支持虚拟地址,编译器也要支持虚拟地址。

再看看图,理解一下↓

3.3.2动态链接

↓先来点网图,进程如何看动态库↓

进程间如何共享库的

动态链接相对于静态链接,远要更加常用,我们用ldd来看一下a.out 这个可执行程序

我们发现这里有一个箭头,这里它调用了一个c的动态库。

那么为什么编译器不使用静态链接呢,静态链接可以链接各个库,不用产生更多的依赖关系,照理应该会更加方便。

没错的确是很方便,但是静态链接产生的文件体积太大了,并且非常浪费资源。随着软件的功能越来越多,程序会变得越来越臃肿,并且可能不同的软件包了相同的库,这样就会浪费大量的磁盘空间。

这时候,动态链接的优势就体现出来了,我们将可以共享的代码提取出来,做成一个独立的动态链接库,在它们运行时再把动态库加载到内存中,这样只同一个模块只需要在内存留一个副本即可,可以节省大量内存资源和磁盘空间。

那动态链接是如何工作的呢?

动态链接实际上,将整个链接的过程推迟到程序运行的时候才进行。我们运行一个程序时,操作系统首先会将程序的代码和它要用到的一系列运行库先加载到内存中,因为每个动态库加载时的地址都是随机的,所以系统会根据当前地址空间的使用情况来给它们分配一段内存。待动态库加载完毕,就可以重定向可执行程序中的函数跳转地址表了。

在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到main 函数。实际上,程序的入口是_start ,这是一个由C运行时库(通常是glibc)或链接器(如ld)提供的特殊函数。

在执行_start函数时,会进行一系列初始化操作:

1. 设置堆栈:为程序创建一个初始的堆栈环境。

2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位
置,并清零未初始化的数据段。

3. 动态链接:这是关键的一步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的
动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调
用和变量访问能够正确地映射到动态库中的实际地址。

4. 调用 __libc_start_main :一旦动态链接完成, _start 函数会调用__libc_start_main (这是glibc提供的一个函数)。__libc_start_main 函数负责执行一些额外的初始化工作,比如设置信号处理函数、初始化线程库(如果使用了线程)等。

5. 调用 main 函数:最后, __libc_start_main 函数会调用程序的main 函数,此时程序的执
行控制权才正式交给用户编写的代码。

6. 处理main 函数的返回值:当main 函数返回时, __libc_start_main 会负责处理这个返回
值,并最终调用 _exit 函数来终止程序。

动态链接器:

动态链接器(如ld-linux.so)负责在程序运行时加载动态库。
当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。


环境变量和配置文件:

Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置文件(如/etc/ld.so.conf及其子配置文件)来指定动态库的搜索路径。这些路径会被动态链接器在加载动态库时搜索。
缓存文件:为了提高动态库的加载效率Linux系统会维护一个名为/etc/ld.so.cache缓存文件
该文件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会首先搜索这个缓存文件

进程找到动态库本质是文件操作,不过我们访问库函数,通过虚拟地址进行跳转访问的,所以需要把动态库映射到进程的地址空间中

局部小结

库已经被映射到内存中了,并且库的虚拟地址我们也知道,库内每一个函数的偏移量我们也知道,所以想要访问库中的某个函数,我们只需要用库的起始地址 + 偏移量就可以访问每一函数。并且访问的时候是从代码区跳转到共享区,调用完毕再回到代码区,整个过程都发送在地址空间中。

3.4全局偏移表(Global Offset Table)

我们前面说到,在运行程序之前,要先把要用到的库先映射到内存中,并且知道每个库的起始地址和库内每个函数的偏移量。

然后对我们加载到内存中的程序的库函数调用进行地址修改,在内存中二次完成地址设置(这个叫做加载地址重定位)。

我们知道代码区.text是只读状态,是不允许修改的,所以动态链接并不会在这里修改,而是会在.data中进行操作。

动态链接的做法是,在.data(可执行程序或者库自己)中专门预留一片空间来存放函数的跳转地址,它就是全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数的地址。因为.data区域是可读写的,所以可以支持动态进行修改。

1.由于代码段只读,我们不能直接修改代码段。但有了GOT表,代码便可以被所有进程共享。但在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独立的GOT表,所以进程间不能共享GOT表。
2. 在单个.so下,由于GOT表与.text 的相对位置是固定的,我们完全可以利用CPU的相对寻址来找
到GOT表。
3. 在调用函数的时候会首先查表,然后根据表中的地址来进行跳转,这些地址在动态库加载的时候会被修改为真正的地址。
4. 这种方式实现的动态链接就被叫做PIC 地址无关代码。换句话说,我们的动态库不需要做任何修
改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给
编译器指定-fPIC参数的原因,PIC = 相对编址 + GOT

3.5库间依赖

因为库也是ELF文件,所以库与库之间也是可以相互依赖的,如果想更深入去了解GOT表中地址变化可以去看看这篇博客:通过GDB学透PLT与GOT_plt got-CSDN博客。

由于动态链接在程序加载的时候需要对大量函数进行重定位,这一步显然是非常耗时的。为了进一
步降低开销,我们的操作系统还做了一些其他的优化,比如延迟绑定,或者也叫PLT(过程连接表
(Procedure Linkage Table))
。与其在程序一开始就对所有函数进行重定位,不如将这个过程
推迟到函数第一次被调用的时候在进行重定向,因为绝大多数动态库中的函数可能在程序运行期间一次都不会被使用到。

上面思路是:GOT中的跳转地址默认会指向一段辅助代码,它也被叫做桩代码/stup。在我们第一次调用函数的时候,这段代码负责查询真正函数的跳转地址,并且去更新GOT表。于是我们再次调用函数的时候,就会直接跳转到动态库中真正的函数实现

注:在解析依赖关系的时候,就是加载并完善程序间的GOT表的过程。

总而言之,动态链接实际上将链接的整个过程,比如符号查询、地址的重定位从编译时推迟到了程序的运行时,它虽然牺牲了一定的性能和程序加载时间,但绝对是物有所值的。因为动态链接能够更有效的利用磁盘空间和内存资源,以极大方便了代码的更新和维护,更关键的是,它实现了二进制级别的代码复用。


不容易的一篇,有些部分up也是参照网上资料的说法,并且很多图也是网图,如有错误请指出。


文章转载自:

http://zSg2YDQC.ckLLd.cn
http://r5EeixGy.ckLLd.cn
http://yBXrlqZK.ckLLd.cn
http://bfCvVMjC.ckLLd.cn
http://2GgR431A.ckLLd.cn
http://XjFHRmqm.ckLLd.cn
http://NKYSq52i.ckLLd.cn
http://SWbInShP.ckLLd.cn
http://b2qncG7b.ckLLd.cn
http://HJkSJTS4.ckLLd.cn
http://qUAf7l3L.ckLLd.cn
http://qCbNXCvH.ckLLd.cn
http://WwiCNszf.ckLLd.cn
http://QqFC16Sn.ckLLd.cn
http://nEtCtz77.ckLLd.cn
http://rnDiGTfZ.ckLLd.cn
http://bpgwcBma.ckLLd.cn
http://nhd8gXHU.ckLLd.cn
http://BRheFc5p.ckLLd.cn
http://ahqQJ5JD.ckLLd.cn
http://7P4sv91L.ckLLd.cn
http://3XXy73ZG.ckLLd.cn
http://hwBNprZb.ckLLd.cn
http://VNOGaQGD.ckLLd.cn
http://luVvGmVM.ckLLd.cn
http://7ghChGvR.ckLLd.cn
http://jxtN3vTs.ckLLd.cn
http://qsTga3Sf.ckLLd.cn
http://ic3cU38M.ckLLd.cn
http://KlKYy1Qp.ckLLd.cn
http://www.dtcms.com/a/371593.html

相关文章:

  • LeetCode 面试经典 150 题:移除元素(双指针思想优化解法详解)
  • 【TypeScript】闭包
  • 后端(fastAPI)学习笔记(CLASS 1):扩展基础
  • Spring Boot @RestController 注解详解
  • 腾讯云语音接口实现会议系统
  • ESP32与SUI-101A实现用电器识别
  • Wan2.2-S2V - 音频驱动图像生成电影级质量的数字人视频 ComfyUI工作流 支持50系显卡 一键整合包下载
  • 开始 ComfyUI 的 AI 绘图之旅-图生图(二)
  • VS2017安装Qt插件
  • ZYNQ FLASH读写
  • 容器元素的滚动条回到顶部
  • 【音频字幕】构建一个离线视频字幕生成系统:使用 WhisperX 和 Faster-Whisper 的 Python 实现
  • ncnn-Android-mediapipe_hand 踩坑部署实录
  • java面试中经常会问到的mysql问题有哪些(基础版)
  • SoundSource for Mac 音频控制工具
  • Unity学习----【进阶】Input System学习(一)--导入与基础的设备调用API
  • 第11篇:降维算法:PCA、t-SNE、UMAP
  • 【Leetcode100】算法模板之二叉树
  • 深入理解假设检验:从抛硬币到药物实验的全景讲解
  • JavaScript笔记之JS 和 HTML5 的关系
  • 第4篇 conda install pytorch==2.0.0报错
  • 基于Echarts+HTML5可视化数据大屏展示-学生综合成绩评价系统大屏
  • 探索OpenResty:高性能Web开发利器
  • Lua 核心知识点详解
  • 26考研——内存管理_内存管理策略(3)
  • MySQL索引和B+Tree的关系
  • 《云原生配置危机:从服务瘫痪到韧性重建的实战全解》
  • 论文阅读-SelectiveStereo
  • 架构思维:重温限流算法原理与实战
  • 【面试题】关于RAG的五道题