当前位置: 首页 > news >正文

ELF文件,静态链接(Linux)

1.目标文件

在前面学习C/C++等编译语言时,都会使用编译器,或者集成开发环境,编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便。

编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运行的机器代码(也就是二进制文件)。

例如:

// test.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");
}

这里有两个文件,可以gcc进行编译:

gcc -c test.c
gcc -c code.c//形成两个文件code.o test.o

编译之后会生成两个扩展名为.o 的文件,它们被称作目标文件。最后再将两个文件进行链接在一起就可以形成可执行程序。

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

2. ELF文件

这里首先粗略理解一下ELF文件

主要有以下四种文件其实都是ELF文件:

1.可重定位文件(Relocatable File) :即 xxx.o 文件。包含适合于与其他目标文件链接来创
建可执行文件或者共享目标文件的代码和数据。

2.可执行文件(Executable File) :即可执行程序。
3.共享目标文件(Shared Object File) :即 xxx.so文件。
4.内核转储(core dumps) ,存放当前进程的执行上下文,用于dump信号触发。

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

1.ELF头(ELF header) :描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
2.程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表里
记着每个段的开始的位置和位移(offset)、长度。
3.节头表(Section header table) :包含对节(sections)的描述。
4.节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。

 ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。并且这些段,都是紧密的放在二进制文件中,需要段表(程序头表)的描述信息,才能把他们每个段分割开。

ELF格式:

 

 这里可以看到.text(代码节:用于保存机器指令,是程序的主要执行部分),.data(数据节:保存已初始化的全局变量和局部静态变量)等常见的节。

3. ELF从形成到加载大致过程

3.1 ELF形成可执行

先将多份C/C++ 源代码,翻译成为目标.o 文件(这里其实.o文件已经是一个ELF文件了)
再将多份.o 文件section进行合并

 3.2ELF可执行文件加载

通过上述查看一个ELF文件有多种不通的Section,在加载到内存时进行Section合并,形成segment

所以这里就必须要有合并原则:

相同属性合并,比如:可读,可写,可执行,需要加载时申请空间等。因此不同的的Section也就能合并到一起(也是为内存节省了空间)

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

通过这个命令可以查看ELF文件的程序头表:

readelf -l 文件(ELF格式)

Elf file type is EXEC (Executable file)

Entry point 0x4003e0

There are 9 program headers, starting at offset 64

首先看到的是,文件类型可执行文件,其次是执行的入口地址 ,段的个数。

值得注意的是这里LOAD两个段,就是要加载到内存的代码段和数据段。

通过这个命令可以查看ELF文件的Section:

readelf -S 文件(ELF格式)

 

 

可以看到第一竖列表示这一节的大小,最后的一列表示起始地址。

接下来看看段(合并节):

通过以下指令:

readelf -l 文件(ELF格式)

 这里和查看程序头表命令一样,在最后一部分就是段信息。

这里为什么要将section合并成segment:

1.Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并, 假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分 为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。

2.操作系统在加载程序时,会将具有相同属性的section合并成⼀个⼤的 segment,这样就可以实现不同的访问权限,从⽽优化内存管理和权限访问控制。

 3.3 程序头表和节头表的作用

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

链接视图:对应节头表

文件结构的粒度更细,将⽂件按功能模块的差异进行划分,静态链接分析的时候⼀般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息。

为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整成可执行的段(segment)、可读写的段、只读段等。所以,链接器趁着链接就把小块们都合并了。

从链接视图来看:

.text节:是保存了程序代码指令的代码节。

.data节:保存了初始化的全局变量和局部静态变量等数据。

.rodata节:保存了只读的数据,如一行C语言代码中的字符串。由于.rodata节是只读的,所以只能存在于⼀个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。

.BSS节:为未初始化的全局变量和局部静态变量预留位置

.symtab节 :Symbol Table符号表,就是源码里面那些函数名、变量名和代码的对应关系。

got.plt节(全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节⼀起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。

执行视图:对应程序头表

告诉操作系统,如何加载可执行文件,完成进程内存的初始化。

 从执行视图来看:

告诉操作系统哪些模块可以被加载进内存。

加载进内存之后哪些分段是可读可写,哪些分段是只读,哪些分段是可执行的。

结论:⼀个在链接时作用,⼀个在运行加载时作用。

4 链接与加载

4.1 静态链接

无论是自己的.o,还是静态库中的.o,本质都是把.o文件件进行连接的过程,所以:研究链接,就是理解.o是如何链接的。

下面通过以下代码来研究该过程:

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

首先将两个文件编程.o目标文件。

这里介绍一个命令可以将代码段(.text)进行反汇编查看:

objdump -d 文件名

 

 可以看到这里调用printf函数,和run时,call的地址为零。

其实就是在编译hello.c的时候,编译器是完全不知道printf和run函数的存在的,(他们位于内存的哪个区块,代码长什么样都是不知道的)。因此,编辑器只能将这两个函数的跳转地址先暂时设为0。

这个地址什么时候被修正呢?其实是在链接时修正。

这里可以得到一个结论:.o文件彼此是不知道对方的存在的。

为了让链接器将来在链接时能够正确定位到这些被修正的地址,在代码块(.data)中还存在⼀个重定位表,这张表将来在链接的时候,就会根据表里记录的地址将其修正。

查看符号表的指令:

readelf -s 文件名

 

 可以看到在hello.o文件中:

puts:就是printf的实现, run就是我们自己的方法在hello.o中未定义(因为在code.o中)

UND就是:undefine,表示未定义(就是本.o文件找不到)

最后将helle.o与code.o文件进行链接形成可执行程序,再来查看符号表

两个.o进行合并之后,在最终的可执行程序中,就找到了run

0000000000001149:其实是地址

FUNC:表示run符号类型是个函数

16:就是run函数所在的section被合并最终的那⼀个section中了,16就是下标

 读取可执行程序最终的所有的section清单

hello.o和code.o的.text被合并了,是main.exe的第16个section

这里再将可执行程序转到反汇编查看:

 最终得出结论:两个.o的代码段合并到了⼀起,并进行了统⼀的编址,链接的时候,会修改.o中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码调用。

统一编制:

http://www.lqws.cn/news/212491.html

相关文章:

  • 开疆智能Ethernet/IP转Modbus网关连接质量流量计配置案例
  • Redis 实现分布式锁:深入剖析与最佳实践(含Java实现)
  • 深度解析:Spring Boot 配置加载顺序、优先级与 bootstrap 上下文
  • 《JavaAI:稳定、高效、跨平台的AI编程工具优势解析》
  • RD-Agent-Quant:一个以数据为中心的因素与模型联合优化的多智能体框架
  • 408第一季 - 数据结构 - 字符串和KMP算法
  • 【Zephyr 系列 13】BLE Mesh 入门实战:构建基础节点通信与中继组播系统
  • 【C++】类型转换
  • 死锁的四个必要条件
  • HTML面试整理
  • 在Mathematica中使用Newton-Raphson迭代绘制一个花脸
  • 【判断既约分数】2022-4-3
  • Python60日基础学习打卡Day46
  • 杭州瑞盟 MS35774/MS35774A 低噪声256细分微步进电机驱动,用于空调风门电机驱动,香薰电机驱动
  • 【HarmonyOS5】UIAbility组件生命周期详解:从创建到销毁的全景解析
  • 智能手表供应链与采购清单(Aurora Watch S1)
  • 用队列实现栈
  • [TI板]MSPM0G3507学习笔记(一) 超详细keil环境配置+烧录配置+空工程迁移+vscode配置+点灯
  • 容器安全最佳实践:云原生环境下的零信任架构实施
  • 游戏(game)
  • 【RTSP从零实践】1、根据RTSP协议实现一个RTSP服务
  • compose 组件 ---无ui组件
  • SDC命令详解:使用set_propagated_clock命令进行约束
  • 路过美国Intel公司
  • 【AI论文】反思、重试、奖励:通过强化学习实现大型语言模型的自我提升
  • 计算机组成原理-存储器的概述
  • 关于datetime获取时间的问题
  • stm32内存踩踏一例
  • Doris 与 Elasticsearch:谁更适合你的数据分析需求?
  • 实战:子组件获取父组件订单信息