Linux ELF文件格式分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xj178926426/article/details/72825630

Linux ELF文件格式分析

一、ELF文件格式

概述

ELF = Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。扩展名为elf。
其主要有三种主要类型:
适于连接的可重定位文件(relocatable file),可与其它目标文件一起创建可执行文件和共享目标文件。
适于执行的可执行文件(executable file),用于提供程序的进程映像,加载的内存执行。
共享目标文件(shared object file),连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

文件格式

为了方便和高效,ELF文件内容有两个平行的视角:一个是程序连接角度,另一个是程序运行角度,如图所示。
图2
ELF header在文件开始处描述了整个文件的组织,Section提供了目标文件的各项信息(如指令、数据、符号表、重定位信息等),Program header table指出怎样创建进程映像,含有每个program header的入口,section header table包含每一个section的入口,给出名字、大小等信息。其中Segment与section的关系后面会讲到。

要理解这个图,我们还要认识下ELF文件相关的几个重要的结构体:

1,ELF文件头

像bmp、exe等文件一样,ELF的文件头包含整个文件的控制结构。它的定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
图2
其中e_ident的16个字节标识是个ELF文件(7F+’E’+’L’+’F’)。
e_type表示文件类型,2表示可执行文件。
e_machine说明机器类别,3表示386机器,8表示MIPS机器。
e_entry给出进程开始的虚地址,即系统将控制转移的位置。
e_phoff指出program header table的文件偏移。
e_phentsize表示一个program header表中的入口的长度(字节数表示)。
e_phnum给出program header表中的入口数目。类似的。
e_shoff,e_shentsize,e_shnum 分别表示section header表的文件偏移,表中每个入口的的字节数和入口数目。
e_flags给出与处理器相关的标志。
e_ehsize给出ELF文件头的长度(字节数表示)。
e_shstrndx表示section名表的位置,指出在section header表中的索引。

2,Program header

目标文件或者共享文件的program header table描述了系统执行一个程序所需要的段或者其它信息。目标文件的一个段(segment)包含一个或者多个section。Program header只对可执行文件和共享目标文件有意义,对于程序的链接没有任何意义。结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
图6
其中p_type描述段的类型;
p_offset给出该段相对于文件开关的偏移量;
p_vaddr给出该段所在的虚拟地址;
p_paddr给出该段的物理地址;
p_filesz给出该段的大小,在字节为单元,可能为0;
p_memsz给出该段在内存中所占的大小,可能为0;
p_filesze与p_memsz的值可能会不相等。

Section Header

目标文件的section header table可以定位所有的section,它是一个Elf64_Shdr结构的数组,Section头表的索引是这个数组的下标。有些索引号是保留的,目标文件不能使用这些特殊的索引。
Section包含目标文件除了ELF文件头、程序头表、section头表的所有信息,而且目标文件section满足几个条件:
目标文件中的每个section都只有一个section头项描述,可以存在不指示任何section的section头项。
每个section在文件中占据一块连续的空间。
Section之间不可重叠。
目标文件可以有非活动空间,各种headers和sections没有覆盖目标文件的每一个字节,这些非活动空间是没有定义的。
Section header结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
图7
其中sh_name指出section的名字,它的值是后面将会讲到的section header string table中的偏移,指出一个以null结尾的字符串。
sh_type是类别。
sh_flags指示该section在进程执行时的特性。
sh_addr指出若此section在进程的内存映像中出现,则给出开始的虚地址。
sh_offset给出此section在文件中的偏移。其它字段的意义不太常用,在此不细述。

文件的section含有程序和控制信息,系统使用一些特定的section,并有其固定的类型和属性(由sh_type和sh_info指出)。下面介绍几个常用到的section:“.bss”段含有占据程序内存映像的未初始化数据,当程序开始运行时系统对这段数据初始为零,但这个section并不占文件空间。“.data.”和“.data1”段包含占据内存映像的初始化数据。“.rodata”和“.rodata1”段含程序映像中的只读数据。“.shstrtab”段含有每个section的名字,由section入口结构中的sh_name索引值来获取。“.strtab”段含有表示符号表(symbol table)名字的字符串。“.symtab”段含有文件的符号表,在后文专门介绍。“.text”段包含程序的可执行指令。

Symbol Table

目标文件的符号表包含定位或重定位程序符号定义和引用时所需要的信息。符号表入口结构定义如下,可在/usr/include/elf.h中可以找到文件头结构定义:
图8
其中st_name包含指向符号表字符串表(strtab)中的索引,从而可以获得符号名。
st_value指出符号的值,可能是一个绝对值、地址等。
st_size指出符号相关的内存大小,比如一个数据结构包含的字节数等。
st_info规定了符号的类型和绑定属性,指出这个符号是一个数据名、函数名、section名还是源文件名;并且指出该符号的绑定属性是local、global还是weak。

二、结合实例分析

以一个最简单的helloworld程序为例 :

#include <stdio.h>

int main()
{
        printf("Hello World!\n");
        return 0;
}

1. ELF文件头

使用工具查看ELF文件头:readelf -h obj
图1

大小总共为64字节,换算成十六进制为0x40。在十六进制代码中找到前0x40字节,即为文件头信息部分(阅读时注意反序问题):
图3
对比上面结构体的定义,来解释下结构体各个字段的值:

e_ident : 十六个字节,可通过这个字段对ELF文件进行识别,其中包括五个部分:

第一部分:占四个字节。7f 45 4c 46,对应ASCII码.ELF,表示这是一个ELF对象。
第二部分:占一个字节。02表示是一个64位对象。
第三部分:占一个字节。01表示是小端表示法。
第四部分:占一个字节。01表示文件头版本。
其余默认为0。

e_type:两个字节,02 00表示是一个可执行文件(ET_EXEC)。

e_machine:两个字节,3e 00表示是intel80386处理器体系结构。

e_version:四个字节,01 00 00 00表示是当前版本。

e_entry:八个字节,40 04 40 00 00 00 00 00表示当前程序入口点。

e_phoff:八个字节,40 00 00 00 00 00 00 00表示程序头表的偏移地址在 00 00 00 00 00 00 00 40处(这个地址是相对于本示例中的elf文件hellowrold来说,即程序表头在helloworld文件的0x40处,前面的0x40用来存放Elf64_Ehdr结构体信息,我们正在解释的这个结构体)。

e_shoff:八个字节,c8 19 00 00 00 00 00 00表示段表的偏移地址在00 00 00 00 00 00 19 c8处。

e_flags:四个字节,00 00 00 00表示未知处理器特定标志#define EF_SH_UNKNOWN 0x0。

e_ehsize:两个字节,40 00表示elf文件头大小为00 40(64个字节)。

e_phentsize:两个字节,38 00表示重定位文件每个程序头表大小为00 38(56字节,从上面的e_phoff这个字段可以看出,程序表头是在elf文件头的后面)。

e_phnum:两个字节,09 00表示重定位文件程序头表的个数为00 09(即9个程序表头,每个程序表头56字节)。

e_ehentsize:两个字节,40 00 表示段头大小为00 40(64字节),section header table中每个header的大小。

e_shnum:两个字节,1e 00表示段表入口有30个,即段表有30段。

e_shstrndx:两个字节,1b 00 表示段表字符串在段表中的索引号,.shstrab段的段表索引号为00 1b,即27。

2,Program header

使用工具查看Program header:readelf -l obj
图2
从上图中可以知道,与我们上面对ELF文件头的分析完全对应的上,该目标文件一共有9个段,起始偏移地址是64。大小总共为56字节*9 ,换算成十六进制为0x1F8。在十六进制代码中偏移地址64字节开始找到前0x1F8字节,即为Program header信息部分(阅读时注意反序问题):
图7

这里我们要解释下上面的一个问题 :Section和Segment的区别和联系
可执行文件中,一个program header描述的内容称为一个段(segment)。Segment包含一个或者多个section,我们以我们这个例子为例,看一下section与segment的映射关系:
3
如上图映射关系可知,文本段并不仅仅包含.text节,数据段也不仅仅包含.data节,而是都包含了多个section。

3,Section Header

使用工具查看段表信息:readelf -S obj
图
图
3
在文件头中e_shoff可以找到段表偏移地址00 00 00 00 00 00 19 c8,从这个地址去查找段表。
段表长度由e_ehentsize为00 40(64字节)。
段表个数由e_shnum可知有30个。
这么多section我就不一一全部详细的列出来,我选一个text section来进行分析,上图中可以看到,text section索引序号为13,我们的段表的起始偏移地址为0x19c8,每个段长度为0x40,其前面有13个段,所以我们text section的起始地址应该是0x19c8 + (0x40*0x0d) = 0x1d08 , 我们来通过

hexdump -C helloworld

命令来验证下文件中这个地址是不是text section。
33
sh_name:四个字节,94 00 00 00表示该段名称在.shstrtab中偏移量,我们通过

readelf -x .shstrtab helloworld

来看下.shstrtab段里面偏移量为0x94处是不是.text :
图
由上图可知,偏移量为0x94处确实就是.text,看来我们上面的推算都是正确的。
sh_type:四个字节,01 00 00 00表示这个段拥有程序所定义的信息,其格式和含义完全由该程序确定,这里表示PROGBITS。
sh_flags:八个字节,06 00 00 00 00 00 00 00表示alloc和execute。
sh_addr:八个字节,40 04 40 00 00 00 00 00表示是section在内存中的虚拟地址为0x400440。
sh_offset:八个字节,40 04 00 00 00 00 00 00表示是section与文件头之间的偏移为0x0440。
sh_size:八个字节,84 01 00 00 00 00 00 00表示文件里面section占用的大小为0x0184。
sh_link:四个字节,00 00 00 00表示没有链接信息。
sh_info:四个字节,00 00 00 00表示没有辅助信息。
sh_addralign:八个字节,10 00 00 00 00 00 00 00表示字节对齐长度。
sh_entsize:八个字节,00 00 00 00 00 00 00 00表示没有入口。

我们按照上面的sh_offset和sh_size字段通过hexdump命令来看看目标文件偏移0x440处,长度为0x0184的内容:
3
然后我们通过如下命令直接把text section的具体内容可以打印出来,

readelf -x .text helloworld

跟上图的数据进行对比:
图
很明显我前面的分析是对的,其他section都可以通过上面办法来验证下,至于text section里的具体内容是什么,程序执行具体的过程,我将在后续跟进,这篇文章只是简单的认识下ELF文件。
文中有什么错误之处或者表达不明白的位置欢迎大家拍砖指出!

没有更多推荐了,返回首页