hello~ 很高兴见到大家! 这次带来的是Linux系统中关于库制作与原理这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、ELF格式
- 二、重谈虚拟空间地址
-
- 2.1逻辑地址与虚拟地址
- [2.2 可执行程序的加载和执行过程](#2.2 可执行程序的加载和执行过程)
- 执行
一、ELF格式
1.1 介绍
- .o 文件(目标文件)、动态库(.so)、静态库(.a)和可执行文件,本质上都是二进制文件 。我们知道.o 文件会在链接阶段整合到一起 ------ 但二进制文件看起来像一团乱码,为什么能精准整合?核心原因是:这些文件都遵循统一的二进制文件格式规范,在 Linux 系统下,这个规范就是ELF(Executable and Linkable Format可执行与可链接模式)。因为他们都是ELF格式,所以.o文件才能够形成库文件和可执行文件。

- 一个标准的 ELF 文件由四个部分组成,它们分别是:ELF 头、程序头表、节头表和节。
- 一般.o 文件(目标文件)没有程序头表,.o 文件在整合时,是把相同类型的节分别合并到一起。
节
- 节(Section)是 ELF 文件最基本的组成单位,不同的节按数据类型 / 用途分类存储不同内容:比如 .data 节(数据节)存储已初始化的全局变量和静态变量,.text 节(代码节)存储函数的二进制执行代码等。这是两个最常见的节。
节头表
- 节头表本质是一个结构体数组,数组中的每一个元素对应 ELF 文件里的一个节,它会记录每个节的全部核心信息(只记录,不直接管理)------ 相当于给每个节都贴了一张包含完整信息的身份卡片,工具(如链接器)通过这张卡片能快速找到、识别并处理对应的节,而无需遍历整个 ELF 文件。



- 通过命令 readelf -S ELF 文件名(-S 为大写),可以查看 ELF 文件的节头表。节头表中清晰记载了每个节的核心信息:包括节的名称、类型、虚拟地址、文件偏移量、大小、权限标志等。其中权限标志里的字符含义可先简单记:R 代表只读,W 代表可写,X 代表可执行;A(Alloc)暂时无需深入理解,核心知道它表示该节需要分配内存空间即可。
ELF头
- ELF头是整个ELF文件的身份证和总目录,里面包含这个ELF文件的核心基础属性,例如文件类型、节头表/程序头表的位置和大小等等。

- 我们可以使用命令 readelf -h elf文件名 来查看ELF文件的ELF头的信息。图中标注的是文件入口地址,文件运行时将这个入口地址提供给OS,OS就知道应该从什么地方开始执行代码。
程序头表
- 要知道,进程是从可执行程序加载来的,OS在为进程创建虚拟地址空间时,是怎么知道要划分多少个区域的?又怎么知道应该给这些区分配多少的空间才合适。要解答这些问题就得提到 ELF 文件里面的段概念了。
- ELF 文件会将功能、权限相近的多个节整合为不同的段;OS 加载程序时,会按照段的定义,将不同段精准映射到进程虚拟地址空间的对应区域。

- 程序头表存储了这些段的内存划分核心信息------ 包括每个段的类型、映射到虚拟地址空间的起始地址、文件大小、内存大小、权限等(OS 加载程序时的核心依据);可以通过命令 readelf -l(小写L) ELF文件名查看 ELF 文件的程序头表。如上图,该文件一共划分了9个段,不同文件划分的段数不同。
- 为什么要划分成段?想想,OS在磁盘里存取的单位是块,大小4KB,而节的大小不一,有的可能满了4KB,有的又不足4KB,如果按节来存取,那么是不是就会有很多空间就被浪费掉,而且效率还低(IO次数多);整按照权限整合成段就不同了,因为段会按照4KB进行对齐,既避免空间碎片化又能减少IO次数,提升效率。
- 所以加载可执行文件的时候,主要就是看段而非节。
- 多个节的合并,是加载的时候加载器帮我们进行的。
- .o文件和.a文件没有程序头表,核心原因是无需被操作系统直接加载------ 它们的唯一作用是被链接器解析、合并节,最终生成可执行文件 /.so 动态库。
二、重谈虚拟空间地址
2.1逻辑地址与虚拟地址
- 先创建一个进程,是先创建内核进程相关的数据结构?还是先加载ELF格式的二进制文件?当然是先创建PCB再加载ELF格式的二进制文件。
- 传统分段模式下逻辑地址 = 段地址 + 偏移量。现代 32/64 位系统采用平坦内存模式,已摒弃传统分段的段地址概念(段基址默认为 0),逻辑地址通常等同于虚拟地址 ;编译器生成.o 文件时,分配的是逻辑地址,链接器生成可执行文件时才分配虚拟地址。32 位 / 64 位系统的地址空间范围,分别对应二进制从 0000 ... 0000 开始到 1111 ... 1111 结束的区间(实际可用空间受内核占用限制)。
- 所以逻辑地址和虚拟地址的分配都不是文件加载进入内存之后才分配,而是在编译链接阶段就已基本完成:编译器生成.o 文件时会分配逻辑地址,链接器生成可执行文件 /.so文件时会分配虚拟地址;前面提到的 ELF 头中的文件入口地址就是虚拟地址。
2.2 可执行程序的加载和执行过程

加载
- 创建了PCB,谁来初始化这个结构体里面的mm_struct(管理虚拟地址空间的结构体)?内核会以 ELF 文件的静态信息(如程序头表)为蓝图,结合系统内存分配规则动态生成,从而完成它的初始化。
- 该可执行程序加载后,其代码段(及数据段)的内容会按内存页(如 4KB)为单位映射到进程的虚拟地址空间(虚拟地址由链接器预分配),内核会在页表中创建对应的页表项,将这些虚拟页与物理内存中的物理页建立映射关系。
执行
- 首先内核会读取 ELF 文件中由链接器预置的程序入口虚拟地址,将其写入该进程的 CPU 上下文;当 CPU 调度执行该进程时,会从寄存器中读取这个入口虚拟地址。由于程序执行过程中所有内存访问(如函数调用、数据读写)使用的都是虚拟地址(或基于虚拟地址的偏移量),CPU 会交由内置的 MMU 硬件查询页表,将虚拟地址转换为物理地址,最终 CPU 凭借该物理地址定位到对应的物理内存单元,完成指令执行或数据读写操作,如此反复。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!