本章将会讲解库的制作包括:静态库、动态库。他们两个的使用方法,以及原理。如果是想要了解清楚原理的话,还需要了解 ELF 文件是什么,以及他的格式,之后了解 ELF 我们才可以更加深入的了解,静态库,动态库加载的过程,这是一个循序渐进的过程!废话不多说,GOGOgo出发喽!

目录
一、静态库
1.1 静态库的制作
简单来说,静态库就是将所有的 .o 文件进行打包(专业来说是归档)变成一个 .o 文件,然后将这个 .o 文件 和对应的 .h 文件 传递给其它人进行使用。
静态库 .a[Linux]、.lib[windows]
动态库 .so[Linux]、.dll[windows]
在使用之前还需要了解几个概念:
1. ar 进行归档, ar 指令后面还需要有 -rc 表示 replease 和 creat。
2. -I 表明要连接的头文件是什么
-L 去那里找库。比如我要去当前目录下找库,就是 -L., 去根目录下寻找就是 -L /
-l(小写的 l)找什么样的库。 -lmyc
3. 静态库的命名格式:lib + 我想要的库的名字 + .a。
1.2 静态库的使用
我们一般来说对于一个打包好的库文件是具有 include 头文件,以及 mylib 目录下存放着 libmyl.a 这样的文件。
// 将当前目录下的所有 .o 打包成 libmyc.a 这个静态库文件
ar -rc libmyc.a *.o
// 当我们传递给别人进行使用时
gcc -o use use.c -I lib/include -L lib/mylib -l myc.a
二、动态库
2.1 动态库的制作
动态库与静态库的不同在于,静态库是将所有的 .o 文件进行打包 使用 ar 变成 .a 文件。而动态库是将在进行编译的时候添加 -fPIC (与位置无关码,这个在后面会进行解释)-shared , -o, mylib.dll hello.cpp, math.cpp。这样进行使用。
2.2 动态库的使用
在使用上跟静态库是一样的也是需要 -L -I -l,这个操作符。我们可以 .so 的前提是:
1.我们已经将这 .so 文件放到了操纵系统的去寻找动态库的路径下,⼀般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等。
2. 向系统共享库路径下建⽴同名软连接。
3.更改环境变量: LD_LIBRARY_PATH。
4.ldconfig⽅案:配置/ etc/ld.so.conf.d/ ,ldconfig更新。有以上四种的一种即可实现使用动态库!
// 将当前的cpp文件变成 .so 文件
gcc -fPIC -shared -o libmyc.so hello.cpp
// 进行使用不仅仅要告诉编译器,去哪里找,使用哪个头文件,使用哪个库函数,还需要告诉操作系统我的 .dll 文件在那里。
gcc main.c -I头⽂件路径 -L库⽂件路径 -lmymath
2.3 库运行的路径搜索
我来解释一下为什么要进行路径的搜索,这个是针对于动态库的。为什么呢?
因为我们的静态库进行连接的时候就是直接将对应的代码,copy 到我们的可执行文件当中,因此我们只需要告诉 gcc 去哪里找,找那个头文件,使用哪个头文件即可。但是我们的动态库文件是当我们的可执行程序加载到内存中之后去一个共享库中去找对应的函数,也需要告诉 gcc 去哪里找,找那个头文件,使用哪个头文件,单纯的这样是不够的,还需要告诉系统这个动态库在那里, os 要去那里找这个动态库,才可实现动态库的链接。这个是一个简单的解释,后面我会进行详细的解释。
三、ELF 文件格式
3.1 目标文件
.o 就是目标文件,是由gcc编译器通过 -c 指令编译源文件而形成的一个文件,这个文件的格式是 ELF 格式。与可执行程序(ELF)是类似几乎是相同的。只不过 .o 文件还没有进行连接,一些函数的地址还没有确定。

3.2 结构
具有 ELF头(ELF header):描述文件的主要特征,位于文件的开始位置,目的是为了定位文件的其他部分。
程序头表(Program header table): 所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、⻓度。主要是用来管理已将合并好的 Section。
节 (Section): 是 ELF 的基本存放单位,包含了特定类型的数据。ELF⽂件的各种信息和数据都存储在不同的节中,如代码节存储了可执⾏代码,数据节存储了全局变量和静态数据等。
节头表(Section header table) :包含对节(sections)的描述。

我们可以使用 readelf 指令来读取 elf 相关的内容。其实我们的静态库,动态库,可执行程序本质上都是 ELF 文件的格式。那么聪明的童鞋肯定就已经想到了,那可执行程序不就是多个elf 文件合并而成的吗?没错就是这样的,这个是对于链接的初步理解,不同的库在进行链接的时候使用的方法不同,但是原理都是相同的。

3.3 ELF 文件的加载过程
3.3.1 是如何进行的
简单来说就是通过文件的路径与文件名,找到这个可执行程序,然后操作读取 ELF 格式,按 program headers 将各段映射到内存、设置堆栈与进程环境,然后(若是动态链接)由动态链接器
ld.so加载并重定位所需的共享库,最后跳转到 ELF 的入口点开始执行。
3.3.2 为什么要进行合并
在这个过程当中,对于一个 ELF 格式的文件会将多个功能相同\类似的 Section 区域合并成一个 段segment。
这样做主要是为了为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。 此外,操作系统在加载程序时,会将具有相同属性的section合并成⼀个大的segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。
3.3.3 Section 与 Program 都是干啥的
需要从来个角度来说:1. 链接的角度使用 Section header table, 这个头节点里面主要包括的是那些功能相同的块可以被合并,静态链接分析的时候⼀般关注的是链接视图,能够理解 ELF ⽂件中包含的各个部分的信息。包含着 Section 的相关信息。
2. 从加载角度看,Program header table告诉操作系统,如何加载可执⾏⽂件,完成进程内存的初始化。⼀个可执⾏程序的格式中,也就是说管理已经合并好的 Segment 的相关信息。
说白了就是:⼀个在链接时作用,⼀个在运行加载时作用。
3.3 静态库原理
3.3.1 静态链接
想要理解静态库的原理我们就要理解什么是静态链接!我们可以发现,我们的库文件与 o 文件他们都是采用 ELF 的格式进行存放的,所有链接也就是合并相关的数据库,合并成一个大的 .o 文件!所有其中的静态链接,也就是说在链接时进行重定向!
我们通过查看汇编的代码可以发现,如果我们的 main 函数去调用 mylib 库里面的一个 func 函数的时候,我们的 mian 文件的 func 函数他的地址(在汇编的情况下)是没有的也就是 0000 0000 这样的方式,只有我们在链接之后才会跟新他的地址。然后我们还需要记住,我们的链接的过程当中的地址是采用平坦模式,也就是从 0 的位置开始进行编码,确定偏移地址。所有在静态链接的时候就是链接重定向在发挥作用。
3.3.2 ELF加载与进程地址空间
在这里需要考虑两个问题:1. ELF 在还没有加载到内存的时候有没有地址? 2. 我怎么知道我的虚拟进程地址空间的大小是多少?
1. 答案是肯定有的!内部采用平坦模式来进行的编码,也就是初始地址 + 偏移量,但是起始地址为 0 的方式进行的存放,也就不考虑了。因为采用这种方式我们加载到内存的时候就可以直接使用。可以使用 objdump -S。
2. 通过我们的 ELF 文件里面的各个 Section 虽然会进行合并,但是使用了平坦模式后,我们就知道各个部分的其实地址是哪里,有了起始地址也就知道了这一部分的大小是多少!也就是可以去初始化虚拟进程地址空间中的 mm_struct 与 vm_area_struct 这一部分的结构。
3.4 动态库原理
3.4.1 程序是如何使用动态库
我们的程序去使用动态库本质是使用虚拟进程地址空间,进行函数位置的跳转,所以需要把动态库映射到进程的地址空间中。
下面这张图片就是多个程序是如何访问同一个动态库的。

从这个问题我们还会衍生出几个问题:我们程序就是在 mian 函数开始的吗?不是的!我们的编译器对于我们的程序做了手脚,当我们进行汇编时可以看到我们的程序一开始的起始位置不是 mian 方法,而是 _strart 的这位置开始进行。那么为什么需要从 _strart 位置开始进行呢?
_strat 位置下的作用有设置堆栈:1.为程序创建⼀个初始的堆栈环境。2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。3. 动态链接:这是关键的⼀步, _start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。说白了就是进行加载时重定向,修改函数的调用位置那么具体是如何进行修改呢?
3.4.2 加载时重定向原理
使用一个 GOT 表(全局偏移量表),这个是动态链接时为了实现不同的虚拟进程地址空间跳转到动态库的共享的代码中,所确定的一个结构,存放在数据段中,因为代码段不可以进行修改,我们可以修改的就是 GOT 这个表里面的数组下标的位置,有一个就填写进来一个,然后根据 GOT 表的起始地址以及下标,找到对应的动态库函数,然后调用即可。
为什么需要使用 GOT 表,其实本质就是当我们 strat 识别到需要使用动态库的时候,需要使用动态库在内存当中的需要修改汇编编码的函数前面的地址,那么因为是加载到内存的时候才会修改,所有称之为加载重定向!在加载之前还是保持平坦模式进行编码,偏移量为 0 ,加载到内存是就是需要在前面加上那块共享内存的库的地址,找到动态库的区域,但是非常重要的一点在于代码在连接了之后在 ELF 文件的格式下是在代码区的部分,这一部分是不可以进行修改的!所有我们就引入了 GOT 表,将 GOT 表的首地址在进行连接时放到代码区域当中,当进行重定向时进行修改数组的下标,就可以指向不同的动态库中的函数,实现调用。这样就很好的解决了代码区不可以修改的问题。

这种⽅式实现的动态链接就被叫做 PIC 地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=相对编址+GOT。
3.4.3 优化
PLT,进行延迟绑定。这是一种优化方式,当我们需要多次的进行在库中去调用其他的库的时候就需要使用延迟绑定。简单来说就是我们只有在真正在使用这个函数的时候,才去进行GOT表的更新,也就是在我们没有使用之前我们会会将 GOT 指向一个桩代码,在我们第⼀次调⽤函数的时候,这段代码会负责查询真正函数的跳转地址。我们操作系统大部分都是采用这种方法。
4、总结
以上是对于动静态库的使用以及原理的回顾。这个文章用于我的学习记录,如果是有其他的错误还请批评指正。如果对你有帮助还请给我点个赞👍👍👍。

1万+

被折叠的 条评论
为什么被折叠?



