ELF文件及其加载与动态链接机制
- 一. EFL文件
- 1.1 ELF文件结构
- 二. ELF文件形成与加载
- 2.1 ELF形成可执行
- 2.2 ELF控制性文件的加载
- 2.2.1总结
- 三. ELF加载与进程地址空间
- 3.1 动态链接与动态库加载
- 3.1.1 进程如何看到动态库
- 3.2 全局偏移量表GOT(global offset table)
- 3.2.1 函数调用
- 3.2.2 GOT与动态链接的关系
- 四. 最后
一. EFL文件
ELF文件可以是可执行文件、共享库文件、目标文件或核心转储文件(core dump)。
可执行文件(Executable File)
- 可执行文件是包含程序代码的ELF文件,经过编译和链接后可以直接执行。它通常包含程序的入口点,操作系统加载它后便开始执行。
- 这类文件可以通过命令行直接运行。例如,执行一个ls命令的二进制文件就是一个可执行的ELF文件。
- 文件的类型标识通常是ET_EXEC。
目标文件(Object File)
- 目标文件是源代码编译后生成的中间文件,它包含了程序的机器代码,但还没有完全链接。目标文件通常不能单独运行,需要与其他目标文件或者库文件一起链接,生成可执行文件。
- 文件的类型标识通常是ET_REL(Relocatable)。
- 目标文件会包含符号表、重定位信息和调试信息,供链接器使用。
共享库文件(Shared Library)
- 共享库文件是一种可以被多个程序同时使用的动态链接库(.so文件)。它包含了供多个程序共享的函数和代码,通常用于提供一些常见功能的共享实现。
- 当一个程序需要某些功能时,它会在运行时加载共享库,而不是在编译时将这些功能静态链接进程序。共享库有助于节省内存和磁盘空间。
- 文件的类型标识通常是ET_DYN(Dynamic)。
1.1 ELF文件结构
ELF文件的主要结构部分包括:
- 文件头(ELF Header):描述文件的基本信息,如文件类型、机器架构等。
- 节区头表(Section Header Table):描述文件中各个节区的布局。
- 程序头表(Program Header Table):描述文件中各个段的布局。
- 节区(Section):包含数据、代码、符号表等信息。
- 段(Segment):由操作系统加载的内存区域,通常包含可执行代码、数据等。
二. ELF文件形成与加载
2.1 ELF形成可执行
- 过程:
将多份源码翻译成为.o文件,再将多个.o文件的section进行合并(也就是链接过程)。
2.2 ELF控制性文件的加载
大致流程:
- 当用户输入命令./a.out运行一个ELF文件时,操作系统执行一系列步骤来加载和执行该文件。
- 操作系统的程序加载器(如ld.so或ld-linux.so)负责加载该文件。加载器会读取ELF文件中的头部信息,确定文件类型(例如可执行文件、共享库等)以及如何在内存中组织程序的各个部分。
详细细节如下:
- ELF文件头(ELF Header)存储描述文件基本信息,如文件类型,目标架构,程序头表的位置和大小等,用于告知操作系统加载器如何处理该文件。
- 读取程序头表(Program Header Table):它描述了ELF文件中各个段的内存布局,哪些段可执行或可写,主要包括段的类型,段在文件的偏移位置和在内存中的的目标地址及段的大小,段的类型如下:代码段,数据段,BSS段,堆栈等。
- 加载器的工作:加载器根据程序头表中的信息,将需要加载的段映射到进程的虚拟地址空间中,还未堆栈已初始化或未初始化的静态数据分配内存。同时加载器根据程序头表中的偏移信息将各个段加载到进程的内存中。如代码段樱色到内存的可执行区域,数据段加载到内存的可写区域。
- 设置程序入口节点:ELF文件包含一个入口点(Entey Point),它是程序开始执行的位置。
- 执行程序:一旦所有程序的段都被加载到内存,操作系统将程序控制权交给程序的入口点。
2.2.1总结
ELF文件加载的过程可以概括为以下步骤:
- 加载器读取ELF头,判断文件类型。
- 读取程序头表,加载需要的段到内存。
- 内存映射:为代码段、数据段等分配内存。
- 动态链接(非必须):如果需要,加载和链接共享库。
- 执行程序:从程序的入口点开始执行
三. ELF加载与进程地址空间
链接过程会对程序中的方法函数进行地址重定位,所以在连接过程之前存在一个不存在的方法也不会报错。
程序有地址,使用统一编址的方式进行编址。
进程mm_struct、vm_area_struct在进程刚刚创建的时候,初始化数据从哪⾥来的?从ELF各个
segment来,每个segment有⾃⼰的起始地址和⾃⼰的⻓度,⽤来初始化内核结构中的[start, end]
等范围数据,另外在⽤详细地址,填充⻚表。
3.1 动态链接与动态库加载
3.1.1 进程如何看到动态库
在编译时,进程将符号引用保留在目标文件中,而不会包含共享库的实际代码。
在链接阶段,进程的符号会与共享库的符号进行连接,生成动态可执行文件。
在程序运行时,操作系统的动态链接器会负责加载共享库,并解析符号,确保程序可以正确调用共享库中的函数。
进程在运行时通过动态链接器映射共享库到内存,进程通过虚拟内存地址访问动态库的函数。
动态链接
_start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的
动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调
⽤和变量访问能够正确地映射到动态库中的实际地址。
程序如何调用库函数:使用起始虚拟地址+方法偏移量即可定位动态库中的任一方法。
3.2 全局偏移量表GOT(global offset table)
全局偏移量表(Global Offset Table,GOT) 是在 动态链接(Dynamic Linking)过程中用于处理 共享库(Shared Libraries)函数和数据地址的关键数据结构。在程序运行时,GOT 使得程序能够在不依赖编译时已知的物理地址的情况下,动态地访问外部共享库中的函数和数据。
3.2.1 函数调用
当程序执行到需要调用共享库中的函数时,它会通过 GOT 中存储的偏移量访问共享库中的函数地址。在第一次调用时,程序会从 GOT 中跳转到一个间接跳转的地址(通常是一个代理函数),这个代理函数会将实际的符号地址加载到 GOT 中。之后,程序就可以直接使用 GOT 中的地址来调用共享库函数。
PIC:地址无关代码
3.2.2 GOT与动态链接的关系
GOT 是实现 延迟符号解析(Lazy Binding)的关键机制之一。延迟符号解析意味着程序不在启动时就解析所有的符号,而是在需要的时候(例如,第一次调用某个外部函数时)动态地解析并填充符号地址。这使得程序启动更快,且只有在使用外部库时才进行解析。
延迟绑定技术:
由于动态链接在程序加载的时候需要对⼤量函数进⾏重定位,这⼀步显然是⾮常耗时的。为了进⼀
步降低开销,我们的操作系统还做了⼀些其他的优化,⽐如延迟绑定,或者也叫PLT(过程连接表
(Procedure Linkage Table))。与其在程序⼀开始就对所有函数进⾏重定位,不如将这个过程
推迟到函数第⼀次被调⽤的时候,因为绝⼤多数动态库中的函数可能在程序运⾏期间⼀次都不会被
使⽤到。
四. 最后
本文介绍了 ELF 文件 的结构、形成过程、加载机制以及与进程地址空间的关系。ELF文件可以是可执行文件、目标文件、共享库文件或核心转储文件,其主要结构包括 ELF 头、程序头表、节区头表、段和节区等。在 ELF文件的加载过程中,操作系统的加载器负责将文件中的各个段加载到内存,并设置程序的入口点。在进程执行期间,动态链接器负责加载和链接动态库,使用全局偏移量表(GOT) 实现延迟符号解析和函数调用。通过 延迟绑定 和 地址无关代码(PIC) 技术,ELF文件能够高效地管理共享库的函数调用。
路虽远,行则将至;事虽难,做则必成