表1. ELF文件类型分类

ELF文件类型 说明 实例
Relocatable File 可重定位文件 未链接之前的ELF文件,可用于链接可执行文件或静态链接库 Linux下的".o"文件,Windows下".obj"文件
Executable File 可执行文件 最终的可执行程序 如Linux下"/bin"目录下文件,Windows的".exe"文件
Shared Objected File 共享目标文件 一种是可用于静态链接文件,另一种是程序运行中被动态链接文件 如Linux下的".a"和".so"文件,Windows的dll文件
Core Dump File 核心转储文件 进程意外终止时,保存进程地址空间的内容以及其他信息 Linux下的core dump

一、前言

在早期的UNIX中,可执行文件格式为a.out格式,由于其格式简单,随着共享库概念的出现被COFF格式取代,后来Linux和Windows基于COFF格式,分别制定了ELF和PE格式,我们日常使用的".exe"文件".lib",".dll"文件就属于PE文件的一种;Linux平台下的可执行文件,中间目标文件".o"以及静态库".a"和动态链接库".so"文件属于ELF文件,本节主要讲解中间目标文件(Relocatable File in ELF)这一ELF类型的文件结构,因为这是编译器将源代码经过预编译,编译,汇编后得到的第一层ELF文件,目标文件经过链接后才能成为真正在Linux下运行的可执行文件,这一类型会在后续blog中讲解。

本文所有测试结果在一下平台得出 ,不同的软件系统与硬件架构在输出结果上会稍有不同,但原理一致。

1 qi@qi:~$ uname -a
2 Linux qi 5.4.0-89-generic #100~18.04.1-Ubuntu SMP Wed Sep 29 10:59:42 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

Reference: 

[1] 《程序员的自我修养,链接、装载与库》,俞甲子 石凡 潘爱民


二、测试用例

我们以一段具有代表性的C文件来作为测试源码,如下,该程序中包含了各种类型的变量以及函数,这些变量或者函数符号经过汇编生成中间目标文件后将被保存在不同的ELF段里。

 1 int printf( const char* format, ... ); // 外部函数声明
2
3 int global_init_var = 84; // 全局已赋值变量
4 int global_uninit_var; // 全局未初始化变量
5
6 void func1( int i ) {
7 printf("%d\n", i);
8 }
9
10 int main(void) {
11 static int static_var = 85; // 局部静态已赋值变量
12 static int static_var2; // 局部静态未初始化变量
13 int a = 1; // 局部已赋值变量
14 int b; // 局部未初始化变量
15
16 func1( static_var + static_var2 + a + b);
17 return a;
18 }

我们使用gcc编译出汇编文件,中间目标文件,以及最终可执行文件,最后使用"file"工具查看文件类型,使用"tree"查看生成的文件大小 : 

 1 $ ls
2 SimpleSection.c
3 $ gcc -S SimpleSection.c && gcc -c SimpleSection.c -o SimpleSection.o && gcc SimpleSection.c -o SimpleSection
4 $ tree -sp
5 .
6 ├── [-rwxrwxr-x 8512] SimpleSection
7 ├── [-rw-rw-r-- 395] SimpleSection.c
8 ├── [-rw-rw-r-- 1936] SimpleSection.o
9 └── [-rw-rw-r-- 1336] SimpleSection.s
10
11 $ file *
12 SimpleSection: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=da891be2d625e27300a0a9682a57fb6cf6563d82, not stripped
13 SimpleSection.c: C source, ASCII text
14 SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
15 SimpleSection.s: assembler source, ASCII text

可以看到,文件"SimpleSection"和"SimpleSection.o"均为ELF-64文件,但是类型不一样,"SimpleSection.o"文件大小为1936个字节。后文使用sublime text以16进制查看ELF目标文件。

三、目标文件ELF结构

ELF文件总体结构可以用图1表示,图左为"SimpleSection.o"文件的前一部分以十六进制表示的内容,图中间一层层的字段(定义:每种字段存储不同类型的内容)就是ELF结构的内容层次了,在目标文件的开头为一个长度为64(0x40)字节的ELF头,只要分析ELF表头内存储的信息,可以得出段表"Section Header table"(在图的最顶层的那个段)在整个目标文件中的偏移,而段表是一个元素为"Elf64_Shdr"结构体类型的数组,它的元素的数量正好是图中间那些字段的数量,也就是它的每个元素存储了中间一些字段如".text",".symtab"等字段的信息,这些信息包括字段名,大小等等属性。所以概括的来说,通过读取ELF头,可以得到段表,然后通过读取段表中各个字段元素,就可以得出各个字段的信息了。那么读到这里,ELF结构轮廓已然清晰,接下来就是分析ELF文件各个字段的具体用途,以及某些字段是具体如何关联才使得链接器能够完全理解这个文件。

图1 ELF文件结构图

1 .text字段:用于保存程序中的代码片段

2 .data字段:用于保存已经初始化的全局变量和局部变量

3 .bss字段:用于保存未初始化的全局变量和局部变量

4 .rodata:顾名思义,保存只读的变量

5 .comment:保存编译器版本信息

6 .symtab:符号表,各个目标文件链接的接口

7 .strtab:字符串表,保存符号的名字,因为各个字符串大小不一,所以统一把所有字符串放到这个段里,后续其他段通过某个符号在字符串标中的偏移可以取到符号。

8 .rela.text:因为程序声明使用了未在程序内部定义的函数或者变量,所以需要等到链接时(定义在别的目标文件或者库里)对这个符号的地址进行重新定位,不然会引用到错误的地址。

9 .shstrtab:和strtab类似,不过保存是段名,也就是说里面保存的字符串是所有段的名字

10 Section Header Table:段表,保存了所有段的信息,本身通过Elf头找到,可以解析出所有段的位置。

在你还没掌握肉身解码ELF文件之前,你可能需要一些工具才能得出目标文件中有什么字段,每个字段有多少字节等等信息,我们可以借助"objdump"和"readelf"来查看目标文件内的细节 : 

  • 使用readelf工具,我们可以很容易得到 "ELF Header前16个字节(可用sublime查看文件实际内容验证)" 为 "7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00" ,这16个字节被称为为Elf文件的魔数。其中前四个字节为所有ELF固定的格式,第5个字节为0x02代表的是64位Elf,如果你的机器是32位的话,那么这个字节就是0x01,第6个字节为字节序,0x01为little endian,0x02为big endian,第7个字节为Elf文件的主版本号,后面9个字节一般为0。
  • 另外,这个shell工具还给我们输出了各个字段的信息,和图1中各个字段一一对应,我们可以看到各个字段在文件中的偏移和占用的字节,如字段".text"的offset为0x40(这里同时验证了Elf文件头为0x40个字节大小,和readelf返回的"Size of this header: 64 (bytes)"契合),占用了0x57个字节。其他字段的位置和大小以此类推。
  • 除此之外,我们还可以看到 "Start of section headers: 1104 (bytes into file)" 这一信息,这表示段表的位置在文件的1104(0x450)个字节的偏置的地方,而我们的Elf一共有13个段,因为段表中的每个元素表示的各个段的属性而不是内容,所以段表中每个元素的大小应该一样,事实上,每个元素都是一个 "Elf64_Shdr" 结构体类型的变量,该结构体和其它如表示Elf头的结构体 "Elf64_Ehdr" 都可以在文件 "/usr/include/elf.h" 中找到,每一个结构体有64个字节(也可以从readelf的输出得出: Size of section headers: 64 (bytes)),那么段表将在1104+64x13=1936 Bytes结束,正好是我们用 "ls -l"得出来的目标文件大小,这表明在我这个平台下,段表位于目标文件的最后。

段表元素类型:Elf64_Shdr

 1 typedef struct
2 {
3 Elf64_Word sh_name; /* Section name (string tbl index) */
4 Elf64_Word sh_type; /* Section type */
5 Elf64_Xword sh_flags; /* Section flags */
6 Elf64_Addr sh_addr; /* Section virtual addr at execution */
7 Elf64_Off sh_offset; /* Section file offset */
8 Elf64_Xword sh_size; /* Section size in bytes */
9 Elf64_Word sh_link; /* Link to another section */
10 Elf64_Word sh_info; /* Additional section information */
11 Elf64_Xword sh_addralign; /* Section alignment */
12 Elf64_Xword sh_entsize; /* Entry size if section holds table */
13 } Elf64_Shdr;

Elf文件头类型:Elf64_Ehdr

 1 typedef struct
2 {
3 unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
4 Elf64_Half e_type; /* Object file type */
5 Elf64_Half e_machine; /* Architecture */
6 Elf64_Word e_version; /* Object file version */
7 Elf64_Addr e_entry; /* Entry point virtual address */
8 Elf64_Off e_phoff; /* Program header table file offset */
9 Elf64_Off e_shoff; /* Section header table file offset */
10 Elf64_Word e_flags; /* Processor-specific flags */
11 Elf64_Half e_ehsize; /* ELF header size in bytes */
12 Elf64_Half e_phentsize; /* Program header table entry size */
13 Elf64_Half e_phnum; /* Program header table entry count */
14 Elf64_Half e_shentsize; /* Section header table entry size */
15 Elf64_Half e_shnum; /* Section header table entry count */
16 Elf64_Half e_shstrndx; /* Section header string table index */
17 } Elf64_Ehdr;
  • 上文中我们说到可以通过Elf头得到段表在文件中的位置从而可以找到段表,保存这个偏置的信息就保存在结构体 "Elf64_Ehdr"中的成员 "e_shoff" 中,我们回过头来看Elf表头的内容,使用sublime text打开目标文件,观察文件头前64个字节也就是Elf文件头的大小:
1 7f45 4c46 0201 0100 0000 0000 0000 0000
2 0100 3e00 0100 0000 0000 0000 0000 0000
3 0000 0000 0000 0000 5004 0000 0000 0000
4 0000 0000 4000 0000 0000 4000 0d00 0c00

可以看到,在第41个字节开始的8个字节(成员e_shoff为Elf64_Off类型,是uint64_t的typedef)为 "50040000",由于是little endian,所以转化成十进制就是0x0450=1104,正好和readelf输出的偏移一样。

  • 我们再来观察由Elf头得到的段表,也就是文件从第1105个字节开始的段,前128个字节(包含了NULL和.text字段的信息)如下:
1 0000 0000 0000 0000 0000 0000 0000 0000
2 0000 0000 0000 0000 0000 0000 0000 0000
3 0000 0000 0000 0000 0000 0000 0000 0000
4 0000 0000 0000 0000 0000 0000 0000 0000
5
6 2000 0000 0100 0000 0600 0000 0000 0000
7 0000 0000 0000 0000 4000 0000 0000 0000
8 5700 0000 0000 0000 0000 0000 0000 0000
9 0100 0000 0000 0000 0000 0000 0000 0000

因为第一个字段为NULL,所以64个字节全为0,接下来的一个字段就是.text字段,对比一下 "Elf64_Shdr"结构体的定义,刚好是64个字节,其中成员 "sh_offset" 表示该段在文件中的偏移,该结构体的第25-32这8个字节为0x40,是"sh_offset"的值,而该结构体的第23-40这8个字节为0x57,是 "sh_size" 的值,正好是偏置0x40,大小0x57,和readelf工具的输出一致。

等等,我为什么知道这个字段是叫".text",请看 "Elf64_Ehdr" 结构体的第一个成员,该成员的值表示字段名在段表字符串表中的下标,为0x20,我们根据0x20,到字段 ".shstrtab" 中找到第33个字节开始,将16进制码转化为ASCII码,就可以知道该段的名字了,说干就干,但是现在每一个字段的名字都还不知道,也就不知道这12个字段里哪一个才是 ".shstrtab", Elf Header结构体 "Elf64_Ehdr" 的最后一个变量 "e_shstrndx" 告诉了我们该段在段表数组中的下标,我们看目标文件前64个字节中的最后两个字节为0x000c=12,也就是说,段表数组的最后一个元素就是我们要找的 ".shstrtab" 字段了,这个元素应该是目标文件的最后64个字节,而这64个字节中的第25-32这8个字节为0x03e8,也就是说 ".shstrtab" 字段的内容从第0x3e8+1个字节开始,我们用sublime text打开目标文件,找到第1001个字节,往后走0x20=32个字节:

1 **** **** **** **** 002e 7379 6d74 6162
2 002e 7374 7274 6162 002e 7368 7374 7274
3 6162 002e 7265 6c61 2e74 6578 7400 2e64
4 6174 6100 2e62 7373 002e 726f 6461 7461
5 002e 636f 6d6d 656e 7400 2e6e 6f74 652e
6 474e 552d 7374 6163 6b00 2e72 656c 612e
7 6568 5f66 7261 6d65 00** **** **** ****

后面的5个字节为 "2e 74 65 78 74",转换为ASCII正好为 ".text",OK,大功告成。

  • 使用objdump工具,我们可以看到各个字段的内容,上文中objdump输出了".text"等字段的内容,如".text"字段输出了0x57个字节,与readelf的输出一致。

四、不放过ELF文件的每一个字节

再更...

Linux可执行文件格式-ELF结构详解的更多相关文章

  1. Linux文件系统的目录结构详解

    Linux文件系统的目录结构详解   一.前 言 文章对Linux下所有目录一一说明,对比较重要的目录加以重点解说,以帮助初学者熟练掌握Linux的目录结构. 二.目 录 1.什么是文件系统 2.文件 ...

  2. Linux下的文件目录结构详解

    Linux下的文件目录结构详解 / Linux文件系统的上层根目录 /bin 存放用户可执行的程序 /boot 操作系统启动时所需要的文件 /dev 接口设备文件目录,例如:had表示硬盘 /etc ...

  3. Linux内核源代码目录结构详解

    http://blog.csdn.net/u013014440/article/details/44024207

  4. Linux目录结构(目录结构详解是重点)

    1.Linux目录与Windows目录对比 1.Windows目录结构 2.Linux目录结构 深刻理解Linux 树状文件目录是非常重要的,只有记住他们,你才能在命令行中任意切换,想去哪里去哪里 2 ...

  5. Android系统目录结构详解

    Android系统基于linux内核.JAVA应用,算是一个小巧精致的系统.虽是开源,但不像Linux一般庞大,娇小可亲,于是国内厂商纷纷开发出自己基于Android的操作系统.在此呼吁各大厂商眼光放 ...

  6. Android逆向之旅---SO(ELF)文件格式详解(转)

    第一.前言 从今天开始我们正式开始Android的逆向之旅,关于逆向的相关知识,想必大家都不陌生了,逆向领域是一个充满挑战和神秘的领域.作为一名Android开发者,每个人都想去探索这个领域,因为一旦 ...

  7. Android逆向之旅---SO(ELF)文件格式详解

    第一.前言 从今天开始我们正式开始Android的逆向之旅,关于逆向的相关知识,想必大家都不陌生了,逆向领域是一个充满挑战和神秘的领域.作为一名Android开发者,每个人都想去探索这个领域,因为一旦 ...

  8. [r]Ubuntu Linux系统下apt-get命令详解

    Ubuntu Linux系统下apt-get命令详解(via|via) 常用的APT命令参数: apt-cache search package 搜索包 apt-cache show package ...

  9. Linux进程上下文切换过程context_switch详解--Linux进程的管理与调度(二十一)

    1 前景回顾 1.1 Linux的调度器组成 2个调度器 可以用两种方法来激活调度 一种是直接的, 比如进程打算睡眠或出于其他原因放弃CPU 另一种是通过周期性的机制, 以固定的频率运行, 不时的检测 ...

随机推荐

  1. vue 移动端项目切换页面,页面置顶

    之前项目是pc端是使用router的方式实现置顶的 //main.js router.afterEach((to, from, next) => { window.scrollTo(0, 0) ...

  2. Java中的引用类型和使用场景

    作者:Grey 原文地址:Java中的引用类型和使用场景 Java中的引用类型有哪几种? Java中的引用类型分成强引用, 软引用, 弱引用, 虚引用. 强引用 没有引用指向这个对象,垃圾回收会回收 ...

  3. 使用CEF(二)— 基于VS2019编写一个简单CEF样例

    使用CEF(二)- 基于VS2019编写一个简单CEF样例 在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进 ...

  4. Python异常代码含义对照表

    Python常见的异常提示及含义对照表如下: 异常名称 描述 BaseException 所有异常的基类 SystemExit 解释器请求退出 KeyboardInterrupt 用户中断执行(通常是 ...

  5. Kubernetes-Service介绍(三)-Ingress(含最新版安装踩坑实践)

    前言 本篇是Kubernetes第十篇,大家一定要把环境搭建起来,看是解决不了问题的,必须实战. Kubernetes系列文章: Kubernetes介绍 Kubernetes环境搭建 Kuberne ...

  6. 【c++ Prime 学习笔记】第7章 类

    类的基本思想是数据抽象和封装 数据分离抽象是一种依赖于接口和实现分离的编程/设计技术.接口包括用户能执行的操作,实现包括类的数据成员.接口实现的函数体.定义类所需的各种私有函数 封装实现了类的接口和实 ...

  7. 全连接层dense作用

    参考来源

  8. LeetCode:并查集

    并查集 这部分主要是学习了 labuladong 公众号中对于并查集的讲解,文章链接如下: Union-Find 并查集算法详解 Union-Find 算法怎么应用? 概述 并查集用于解决图论中「动态 ...

  9. 安装多个版本的 JDK

    安装多个版本的 JDK 刚刚开始学 Java 的时候安装了 JDK9 版本,后续发现还是 JDK8 使用的多些,而又不想删除原先版本 因此安装两个版本的 JDK 在需要是切换一下即可 1. 安装第一个 ...

  10. [no_code][Beta]设计和计划

    2020春季计算机学院软件工程(罗杰 任健) 2020春季计算机学院软件工程(罗杰 任健) 作业要求 Beta设计和计划 我们在这个课程的目标是 远程协同工作,采用最新技术开发软件 这个作业在哪个具体 ...