目录

LinuxELF文件动静态库的加载和形成

Linux:ELF文件&&动静态库的加载和形成


前言

ELF是一种文件格式的名称

ELF(Executable and Linkable Format)是Linux和其他类Unix系统中用于可执行文件、目标文件、共享库(动态链接库)和核心转储(core dumps)的标准文件格式。它定义了程序在存储和运行时的结构,是理解程序编译、链接和加载过程的关键。


一、ELF文件的类型及其组成的格式

1.1 类型

  • 可执行文件(Executable):可直接运行的程序(如 /bin/ls)。
  • 目标文件(Relocatable Object File):编译后的 .o 文件,需链接生成 可执行文件。
  • 共享库(Shared Object):动态链接库 .so,运行时被加载到进程内存。
  • 核心转储(Core Dump):程序崩溃时的内存快照,用于调试。

1.2 组成格式

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

组成部分作用
ELF Header描述文件的基本信息(架构、入口地址、段表和节表的位置等)。
Program Headers描述程序在运行时的内存布局(如代码段、数据段、栈等),用于加载和执行程序。
Section Headers描述文件的节(section)信息(如代码、数据、符号表等),用于链接和调试。
Data Sections实际的代码、数据、字符串表、符号表等内容。

https://i-blog.csdnimg.cn/direct/30d8450ffb91440c8a1b3f259c5db49c.png

常见的节

  • 代码节(.text):⽤于保存机器指令,是程序的主要执⾏部分。
  • 数据节(.data):保存已初始化的全局变量和局部静态变量。
  • Block Started by Symbol(.bss):是程序内存布局中的一个重要部分,主要用于存储未初始化的全局变量和静态变量

二、ELF文件从形成到加载轮廓

2.1 ELF可执行文件形成过程

  • step-1:将多份 C/C++ 源代码,翻译成为⽬标 .o ⽂件

  • step-2:将多份 .o ⽂件section进⾏合并

https://i-blog.csdnimg.cn/direct/b73b5e987bb449aa8dde5ea56cc22f6c.png

2.2 ELF可执行文件从磁盘加载到内存中section的变化

  1. ⼀个ELF会有多种不同的Section,在加载到内存的时候,也会进⾏Section合并,形成segment

    目的是为了节省从磁盘加载到内存过程中的内存空间,一个页大小是4kb(4096字节),如果不合并前.text大小为4097字节,.data是1字节,那么它们会占3个页面,但是合并过后总大小为4098字节,那么只需要两个页,section在链接时起作用而segment在执行时起作用,segment其实就是往下合并section,两者的关系是segment包含section

  2. 合并原则:相同属性,⽐如:可读,可写,可执⾏,需要加载时申请空间等.

  3. 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到⼀起

  4. 很显然,这个合并⼯作也已经在形成ELF的时候,合并⽅式已经确定了,具体合并原则被记录在了ELF的 程序头表(Program header table) 中

三、理解链接和加载

3.1 静态链接

研究静态链接其实就是研究不同的.o文件是如何链接到一起的,.o类型文件也叫做可重定位目标文件

以下是样例代码:

// hello.c
#include<stdio.h>
void run();
int main() {
 printf("hello world!\n");
 run();
 return 0;
}

// code.c
#include<stdio.h>
void run() {
 printf("running...\n");
}

objdump -d 命令:将代码段(.text)进⾏反汇编查看

以下图片是反汇编后的hello.o文件的汇编码

其中蓝色圈中的e8代表的就是callq的加载到内存的机械码,而后面跟着的一串0代表访问的函数地址,在链接中才会填充地址,也叫做地址重定位 https://i-blog.csdnimg.cn/direct/f3c27bca5f68468cba93f5f1123d3af1.png

静态链接就是把库中的.o进⾏合并,和上述过程⼀样

所以链接其实就是将编译之后的所有⽬标⽂件连同⽤到的⼀些静态库运⾏时库组合,拼装成⼀个独⽴的可执⾏⽂件。其中就包括我们之前提到的地址修正,当所有模块组合在⼀起之后,链接器会根据我们的.o⽂件或者静态库中的重定位表找到那些需要被重定位的函数全局变量,从⽽修正它们的地址。这其实就是静态链接的过程

https://i-blog.csdnimg.cn/direct/315c20069b9841878788d1072a92c351.png

3.2 动态链接

首先交代一个结论: 动态链接实际上将链接的整个过程推迟到了程序加载的时候。⽐如我们去运⾏⼀个程序,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。

动态库想要和运行程序关联起来需要进行两个步骤

  1. 被程序相关的进程看到:动态库地址映射到进程的地址空间

  2. 被进程调用:在地址空间中进行跳转

    https://i-blog.csdnimg.cn/direct/d89970f1c0c944e6b218d44ce0242b7c.png

    动态库(也能被叫做共享库)的意义就是将所有能重复用到的代码在程序需要时放到内存的一片区域中然后不需要再出现重复的代码。

—在C/C++程序中,当程序开始执⾏时,它⾸先并不会直接跳转到 main 函数。实际上,程序的⼊⼝点

是 _start ,这是⼀个由C运⾏时库(通常是glibc)或链接器(如ld)提供的特殊函数。

在 _start 函数中,会执⾏⼀系列初始化操作,这些操作包括:

  1. 设置堆栈:为程序创建⼀个初始的堆栈环境。

  2. 初始化数据段:将程序的数据段(如全局变量和静态变量)从初始化数据段复制到相应的内存位置,并清零未初始化的数据段。

  3. 动态链接:这是关键的⼀步, _start 函数会调⽤动态链接器的代码来解析和加载程序所依赖的动态库(shared_libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。

  4. 调⽤ __libc_start_main :⼀旦动态链接完成, _start 函数会调⽤__libc_start_main (这是glibc提供的⼀个函数)。 __libc_start_main 函数负责执⾏⼀些额外的初始化⼯作,⽐如设置信号处理函数、初始化线程库(如果使⽤了线程)等。

  5. 调⽤ main 函数:最后, __libc_start_main 函数会调⽤程序的 main 函数,此时程序的执⾏控制权才正式交给⽤⼾编写的代码。

  6. 处理 main 函数的返回值:当 main 函数返回时, __libc_start_main 会负责处理这个返回值,并最终调⽤ _exit 函数来终⽌程序。

    上述过程描述了C/C++程序在 main 函数之前执⾏的⼀系列操作,但这些操作对于⼤多数程序员来说是透明的。程序员通常只需要关注 main 函数中的代码,⽽不需要关⼼底层的初始化过程。然⽽,了解这些底层细节有助于更好地理解程序的执⾏流程和调试问题。

动态链接器:

◦ 动态链接器(如ld-linux.so)负责在程序运⾏时加载动态库。

◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

环境变量和配置⽂件:

◦ Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置⽂件(如/etc/ld.so.conf及其⼦配置⽂件)来指定动态库的搜索路径。

◦ 这些路径会被动态链接器在加载动态库时搜索。

缓存⽂件:

◦ 为了提⾼动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存⽂件。

◦ 该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先

搜索这个缓存⽂件

https://i-blog.csdnimg.cn/direct/16692c764f7a4ce2ab7c808b08dffc72.png

3.2.1 动态库中的相对地址

动态库也是elf类型的文件,当文件链接动态库的时候,发生以下过程

  1. 通过mm_struct中的变量找到有关共享区的结构体,根据里面的成员指针变量找到路径

  2. 根据路径找到磁盘中的数据块加载到内存

  3. 发生映射关系关联起来

  4. 得到库的起始虚拟地址

  5. 数据区会有一个名为.GOT的表记录库函数的偏移量,映射过后,表会根据库的起始虚拟地址进行修改得到完整的访问共享区的地址

    https://i-blog.csdnimg.cn/direct/142333df493341d6ac9a68ad7db933e9.png

总结

这篇博客是博主时隔三个多月再次恢复的博客写作,仍有诸多欠缺,希望看到这篇博客的小伙伴能一起坚持下学习和进步