吃透 Linux 静态库 / 动态库:ELF 文件、链接加载与进程地址空间详解

目录

一、什么是库?

二、静态库

2.1、静态库的生成

2.2、静态库的使用

三、动态库

3.1、动态库的生成

3.2、动态库的使用

四、目标文件

五、ELF文件

5.1、ELF文件形成可执行

5.2、理解连接与加载

[1. 静态链接](#1. 静态链接)

[2. ELF加载与进程地址空间](#2. ELF加载与进程地址空间)

​编辑

[3. 动态链接](#3. 动态链接)

5.3、总结


一、什么是库?

库是一套方法的集合,如printf,strcmp,strlen等方法(函数),只要我们包含库对应的头文件,就可以直接调用我们想要的函数。因此,有了库就可以大大提高写代码的效率,我们的程序底层可能依赖多个库。

库根据编译链接过程的不同,分为静态库和动态库。

• 静态库:Linux中以 .a 为后缀、windows中以 .lib为后缀。

• 动态库:Linux中以**.so** 为后缀、windows中以**.dll**为后缀。

一个可执行程序可能用到许多的库,这些库有的是静态库,有的是动态库,而我们的编译默 认为动态链接库,只有在该库下找不到动态.so的时候才会采用同名静态库。我们也可以使用 gcc 的 -static 强转设置链接静态库

二、静态库

静态库在编译链接时和我们自己的目标文件会进行合并,形成独立的可执行文件,所以静态链接形成的可执行文件一般比较大。

2.1、静态库的生成

静态库本质是多个目标文件(.o)的集合,假设今天我们需要用到我们自己的打印和计算字符串长度的函数。

就可以将这两个函数分别实现,然后编译为目标文件,打包形成一个静态库,当我们想要使用这两个方法时,包含对应的头文件即可。

将目标文件打包为静态库需要用到一个归档工具

bash 复制代码
ar -rc
// r --- replace
// c --- creat
bash 复制代码
// -------------------------Makefile自动化构建---------------------------------------
# 打包形成库
libmyc.a:mystrlen.o Print.o
	ar -rc $@ $^

# 编译
%.o:%.c
	gcc -c $<

.PHONY:clean
clean:
	rm -rf *.a *.o stdc*

.PHONY:output
output:
	mkdir -p stdc/include
	mkdir -p stdc/lib
	cp *.h stdc/include
	cp *.a stdc/lib
	tar -czf stdc.tgz stdc

2.2、静态库的使用

bash 复制代码
// main.c
#include "mystrlen.h"
#include "Print.h"
#include <stdio.h>
int main()
{
    char *str = "hello Linux";
    Print(str);
    int len = mystrlen(str);
    printf("strlen = %d\n", len);
    return  0;
}
bash 复制代码
// 场景1:头文件和库文件安装到系统路径下
gcc main.c -lmyc

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下
gcc main.c -L. -lmyc

// 场景3:头文件和库文件有自己的独立路径
gcc main.c -I头文件路径 -L库文件路径 -lmyc

• -L: 指定库路径
• -I: 指定头文件搜索路径
• -l: 指定库名

myc:库名,libmyc.a去掉 lib和后缀 .a即为库名。

三、动态库

• 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。

• 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码

• 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中, 这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采 用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空 间。

3.1、动态库的生成

还是以上面的代码为例,接下来我们生成动态库。

bash 复制代码
// ----------------------------Makefile自动化构建-------------------------------------
libmyc.so:mystrlen.o Print.o
	gcc -o $@ $^ -shared

mystrlen.o:mystrlen.c
	gcc -fPIC -c $<
Print.o:Print.c
	gcc -fPIC -c $<

.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.so lib/mylib
	tar -czf lib.tgz lib 

.PHONY:clean
clean:
	rm -rf *.o *.so *.tgz lib

shared: 表示生成共享库格式

• fPIC:产生位置无关码(position independent code)

• 库名规则libxxx.so

3.2、动态库的使用

与静态库相类似:

bash 复制代码
// 场景1:头文件和库文件安装到系统路径下 
$ gcc main.c -lmystdio 

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下 
$ gcc main.c -L. -lmymath // 从左到右搜索-L指定的目录 

// 场景3:头文件和库文件有自己的独立路径 
$ gcc main.c -I头文件路径 -L库文件路径 -lmymath

但是当我们运行时,还存在问题:

我们可以用 ldd a.out 指令查看a.out的链接情况:

此时我们的动态库不在库运行搜索路径中,即系统并不知道我们的动态库在哪里。

解决方案:

• 拷贝 .so 文件到系统共享库路径下, 一般指 /usr/lib、/usr/local/lib、/lib64 或者开 篇指明的库路径等

• 向系统共享库路径下建立同名软连接

• 更改环境变量: LD_LIBRARY_PATH

• ldconfig方案:配置/ etc/ld.so.conf.d/ ,ldconfig更新

四、目标文件

这里的以 .o为后缀的就是目标文件,要注意的是:如果我们 修改了一个原文件,那么只需要单独编译它这一个,而不需要浪费时间重新编译整个工程。目标文件 是一个二进制的文件,文件的格式是ELF,是对二进制代码的一种封装。

五、ELF文件

ELF (Executable and Linkable Format) 是 Linux 和其他类 Unix 系统下标准的二进制文件格式,用于存放可执行程序、目标文件(.o)、动态库(.so)和核心转储文件(core dump)

为什么要讲ELF文件呢?因为我们代码的编译链接就是ELF文件的组织结构发生变化,最终形成可执行文件。

一个ELF文件由以下四部分组成:

• ELF头(ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文 件的其他部分,还有程序的入口地址(虚拟)。

• **程序头表(Program header table) :**列举了所有有效的段(segments)和他们的属性。表里 记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中, 需要段表的描述信息,才能把他们每个段分割开。

• 节头表(Section header table):包含对节(sections)的描述。

**• 节(Section ):**ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和 数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

常见节 (Section)

  • .text:存放编译后的机器指令(代码)。
  • .data:存放已初始化的全局变量和静态变量。
  • .bss:存放未初始化的全局变量(不占文件空间,仅占位)。
  • .rodata:只读常量(如字符串常量)。
  • .symtab:符号表(函数名、变量名与地址的对应关系)。
  • .strtab:字符串表(存放符号名等字符串)。
  • .got 和 .plot:.got节保存了全局偏移表。.got节和.plt节一起提供 了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。

操作大全:

查看ELF header

bash 复制代码
readelf -h main
  • 查看Program header table
bash 复制代码
readelf -l 文件名
  • 查看Section header table
bash 复制代码
readelf -S 文件名
  • 查看Sectoins
bash 复制代码
objdump -S 文件名

5.1、ELF文件形成可执行

▶️编译阶段:形成 .o目标文件,把代码、数据、符号、重定位信息,按 Section 分开存好,等着被链接,此时以节为主。

▶️链接阶段:链接器把 多个 .o 文件 + .so/.a 库 拼成一个最终可执行 ELF。

① 合并 Section(节)为 Segment(段)

**•**把所有文件的.text 合在一起,.data 合在一起,.bss 合在一起...。

**•**合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。

**•**合并原因:减少页面碎片,高效利用空间,如页面基本单位为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分 为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个 页面。

② 符号解析

找到所有函数、变量的真实位置 。比如你调用 printf,链接器去 libc 里找到它。

③ 重定位(最关键)

把之前编译时没确定的地址,全部填上真实的虚拟地址。从此所有跳转、访问变量都有真实地址。

ELF文件提供 2 个不同的视图/视角来让我们理解Section header table 和 Program header table 两个部分:

链接视图:对应节头表 Section header table,以节为单位,用于目标文件的链接与重定位,服务于编译链接过程。

执行视图:对应程序头表 Program header table,以段为单位,用于程序加载与执行,服务于操作系统运行程序。

5.2、理解连接与加载

1. 静态链接

静态链接就是把库中的.o进行合并。

其实就是将编译之后的所有目标文件连同用到的一些静态库运行时库组合,拼装成一个独立 的可执行文件,所以此时可执行文件体积比较大。

主要包括地址修正,当所有模块组合在一起之后,链接器会根据我 们的.o文件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从而修正它们的地址。这 其实就是静态链接的过程。

通过readelf -* 指令就可以看到,形成目标文件后,我们用到的printf函数,或者自定义函数的地址是为空的,当链接后(地址修正),这些地方才会填上函数对应的入口地址(虚拟地址)。

2. ELF加载与进程地址空间

++问题:++

• 一个ELF程序,在没有被加载到内存的时候,有没有地址呢?

• 进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?

++答案:++

有地址,计算机工作时,会用平坦模式进行编址,让地址空间成为一个连续、无分段的线性地址空间,所以也要求ELF对自己的代码和数据进行统一编址,下面是 objdump -S 反汇编 之后的代码:

红色圈住的就是每条指令的地址(虚拟地址),严格来说应该是偏移量,但由于起始地址为0,偏移量即地址。也就是说,其实虚拟地址在我们的程序还没有加载到内存的时候,就已经把可执 行程序进行统一编址了。

进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪里来的?从ELF各个 segment来,每个segment有自己的起始地址和自己的长度,用来初始化内核结构中的start, end 等范围数据,另外在用详细地址,填充页表。

ELF 在被编译好之后,会把自己未来程序的入口地址记录在ELF header的Entry字段中:

3. 动态链接

我们知道 .so库文件也是ELF文件,库文件内部有相对虚拟地址(基于库自身的偏移地址),

  • 内核启动进程
  • 动态链接器 ld-linux.so 开始工作
  • 打开 .so 文件
  • 调用 mmap 把库映射到进程虚拟地址空间
  • 内核为此创建一段 VMA(vm_area_struct)

而动态库可以被多个进程共享,空间利用各高效。

动态链接将链接过程推迟至程序加载阶段。调用库函数时,由动态链接器完成符号解析与重定位,使程序中的函数调用与变量访问正确映射到动态库的实际虚拟地址。

📌 注意:

• 库已经被我们映射到了当前进程的地址空间中;

• 库的虚拟起始地址我们也已经知道了(vm_area_struct);

• 库中每一个方法的偏移量地址我们也知道(GOT表);

• 所以,访问库中任意方法,只需要知道库的起始虚拟地址+方法偏移量即可定位库中的方法 • 而且:整个调用过程,是从代码区跳转到共享区,调用完毕再返回到代码区,整个过程完 全在进程地址空间中进行的。

全局偏移量表GOT(global offset table)

📌 注意:

• 也就是说,我们的程序运行之前,先把所有库加载并映射,所有库的起始虚拟地址都应该 提前知道

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

• 等等,修改的是代码区?不是说代码区在进程中是只读的吗?怎么修改?能修改吗?

所以:动态链接采用的做法是在 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数 的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数 的地址。

• 因为.data区域是可读写的,所以可以支持动态进行修改:

💦调用printf函数过程:

⏩️进程虚拟地址空间中已通过 mmap 映射了动态库,并确定了库的基地址

⏩️调用 printf 时,通过 GOT 表获取 printf 在库内部的偏移地址 ,两者相加得到 printf最终虚拟地址

⏩️CPU 访问该虚拟地址时,再通过页表映射到对应的物理地址执行。
这种方式实现的动态链接就被叫做 PIC 地址无关代码 。换句话说,我们的动态库不需要做任何修 改,被加载到任意内存地址都能够正常运行,并且能够被所有进程共享,这也是为什么之前我们给 编译器指定-fPIC参数的原因,PIC=相对编址+GOT。

5.3、总结

• 我们知道静态链接会将编译产生的所有目标文件,和用到的各种库合并成一个独立的可执行文件, 其中我们会去修正模块间函数的跳转地址,也被叫做编译重定位(也叫做静态重定位)。

• 而动态链接实际上将链接的整个过程推迟到了程序加载的时候。比如我们去运行一个程序,操作系 统会首先将程序的数据代码连同它用到的一系列动态库先加载到内存,其中每个动态库的加载地址 都是不固定的,但是无论加载到什么地方,都要映射到进程对应的地址空间,然后通过.GOT方式进 行调用(运行重定位,也叫做动态地址重定位)。

相关推荐
AlfredZhao20 小时前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5202 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩2 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈2 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫2 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875242 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant