【Linux】动静态库、ELF文件、链接与加载
目录
一、前言
本文介绍了Linux下动静态库的制作和使用方法,而后谈到可执行文件、动静态库等文件的ELF格式,介绍ELF文件的结构和作用,最后重新理解程序链接与加载的整个过程,从静态链接到动态链接的不过和详细过程。
二、动静态库
1.原理及区别
什么是库?是写好的、现成的、可复用的代码。现实中的程序都要依赖很多基础的底层库,不可能每次的代码都从零开始写,因此库的意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行,库有两种:静态库和动态库
| 维度 | 静态库 | 动态库 |
| 文件拓展名 | .a(Linux) 、.lib(Windows) | .so(Linux)、dll(Windows) |
| 原理 | 编译时静态库代码被完整地复制到最终的可执行文件中 | 程序运行时由操作系统动态加载到内存中 |
| 性能 | 启动速度快(无需加载外部库) | 启动稍慢(需加载库) |
| 内存占用 | 每个程序独立加载库代码副本,内存占用高 | 多个程序共享同一内存副本,节省资源 |
| 依赖性 | 无运行时依赖,可独立运行 | 依赖库文件的存在 |
2.静态库的制作
我们自己写的程序,通常含有.h头文件,.c实现文件,我们知道多文件编译形成.o文件后同一进行链接即可形成可执行程序。若我们不想让人看见我们函数的实现,就可以发送.o文件。
多个.o文件可以形成一个静态库:
ar -rc libmyc.a *.o
其中lib表面其是一个库,.a是静态库后缀,myc则是该静态库的真正名字。若把头文件和静态库打包在一起,可以如下使用:

3.动态库的制作
gcc -fPIC -c math.c -o math.o # 生成位置无关的目标文件
gcc -shared -o libmath.so math.o # 生成动态库 libmath.so
使用方法有以下几种:
• 拷贝 .so 文件到系统共享库路径下, 一般指 /usr/lib、/usr/local/lib、/lib64 或者开篇指明的库路径等
• 向系统共享库路径下建立同名软连接
• 更改环境变量: LD_LIBRARY_PATH
• ldconfig方案:配置/ etc/ld.so.conf.d/ ,ldconfig更新
三、ELF文件
ELF是Linux中用于可执行文件、目标文件(.o)、共享库等文件的标准二进制格式,它的核心结构如下:

多份.o文件形成可执行程序时(链接)会将各个ELF文件格式的.o文件的section进行合并:

然后在ELF可执行文件加载到内存时形成segment,合并的原则是合并相同的属性,例如可读、可写、可执行等,这个合并的原则被记录在ELF的程序头表中。
那么这样合并section为segment的好处是什么呢?
- 减少页面碎片,提供内存使用效率。假设页面(操作系统内存管理的基本单位)大小为4KB,若.text一部分为1KB,另一部分为2KB,合并前需要2个页面,合并后只需要1个
- 优化内存管理和权限访问控制。OS加载程序时,具有相同属性的section合并在一起,实现不同的访问权限,方便管理
节头表是链接时起作用,描述文件中所有节section的详细信息,包括名称、类型、大小等属性;程序头表则是执行时起作用,描述如何将文件内容加载到内存,包括段的类型、权限等。
四、链接与加载
1.静态链接
假设你写了一个hello.c的文件,调用了code.c中的run函数,编译时生成了hello.o和code.o的目标文件,此时若对其进行反汇编查看,就能发现调用run函数的地址为0,也就是说hello.o并不知道run函数在哪,在链接之后,两个.o的代码段合并在一起,进行统一编址,此时hello中调用的run函数就有了地址,因此.o文件也叫做可重定位目标文件。
静态库就是一堆.o文件和集合,所谓静态链接也就是上述过程而已,就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立的可执行文件。

2.虚拟地址/逻辑地址
一个ELF程序,在没有被加载到内存的时候就有地址,当代计算机工作的时候,都采用"平坦 模式"进行工作。所以也要求ELF对自己的代码和数据进行统一编址。

最左侧的就是ELF的虚拟地址,其实,严格意义上应该叫做逻辑地址(起始地址+偏移量),但是我们 认为起始地址是0。也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执行程序进行统一编址了。进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?就是从ELF各个segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的[start, end] 等范围数据,另外在用详细地址,填充页表。
ELF 在被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry字段中:

接下来用一张图来理解程序加载到内存的整个过程:

首先可执行程序在加载前就已经拥有虚拟地址了,加载到物理内存后,每个虚拟地址的一行都会占据一个物理地址,这样的映射关系被填充在页表中,同时操作系统创建task_struct,用ELF文件中的虚拟地址来初始化mm_struct的start和end,例如图中代码段开始于1060,结束于108f,这就用来初始化mm_struct的text段,并且此时ELF文件中保存的程序入口信息也被放在CPU的EIP中,随后CPU就可以执行该程序了,注意CPU使用的都是虚拟地址,不过可以通过MMU-CR3来转换为物理地址。
在这里我们就可以从OS的三个核心管理内容:文件管理、内存管理、进程管理整体上结合来认识一下了:首先必然要先从文件系统中查找到inode找到对应数据块,然后用虚拟地址初始化mm_struct,随后加载物理内存,建立页表映射

3.动态链接、动态库加载
动态库也是ELF文件格式,和其他可执行程序一样会加载到物理内存,页表建立映射,但是存在于mm_struct的共享区,且动态库在物理内存中仅有一份,多个程序可以共享:

动态链接其实远比静态链接要常用得多,gcc、g++默认使用动态链接。动态链接实际上是将链接整个过程推迟到程序加载的时候。动态库也是ELF文件,采用统一编址,也就是起始地址(0)+ 偏移量,例如动态库中有偏移量为0x123的函数,那么该函数映射到可执行文件中时就是:可执行文件中动态库的起始地址(如0x1000)再加上偏移量0x123。
在C/C++程序中,当程序开始执行时,它首先并不会直接跳转到 main 函数。实际上,程序的入口点是 _start ,会执行一系列初始化操作,就包括动态链接,_start 函数会调用动态链接器的代码来解析和加载程序所依赖的动态库。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调用和变量访问能够正确地映射到动态库中的实际地址。

所以动态库加载的整个过程其实是:可执行程序执行_start时,需要执行动态链接器依赖的方法,识别到程序依赖c标准库,从路径(动态编译时提供)找到磁盘文件中的动态库数据块,加载进内存,而库中所有方法都存在偏移量,接下来进程中再建立共享区vm_area_struct,初始化库的起始虚拟地址,建立页表和物理地址的映射,这样下来,我们就能使用起始虚拟地址+偏移量直接访问库的内容了。

也就是说我们的程序在运行之前,先把所有库加载并映射,库的起始虚拟地址我们就都应该提前知道了,然后对加载到内存的程序的库函数调用进行地址修改,在内存中二次完成地址设置,这个叫做加载地址重定位。但修改的是代码区吗?代码区可是只读的,不能修改,那么如何修改呢?
.data区域是可读写的,支持动态进行修改,因此需要使用GOT表来实现:

更多推荐

所有评论(0)