ELF 文件,大名叫 Executable and Linkable Format。
作为一种文件,那么肯定就需要遵守一定的格式。
从宏观上看,可以分成四个部分:
图中的这几个概念,如果不明白的话也没关系,下面我会逐个说明的。
在 Linux 系统中,一个 ELF 文件主要用来表示 3 种类型的文件:
1. 可执行文件
2. 目标文件
3. 共享文件
既然可以用来表示 3 种类型的文件,那么在文件中,肯定有一个地方用来区分这 3 种情况。
在我的头部内容中,就存在一个字段,用来表示:当前这个 ELF 文件,它到底是一个可执行文件?是一个目标文件?还是一个共享库文件?
另外,既然我可以用来表示 3 种类型的文件,那么就肯定是在 3 种不同的场合下被使用,或者说被不同的家伙来操作我:
可执行文件:被操作系统中的加载器从硬盘上读取,载入到内存中去执行;
目标文件:被链接器读取,用来产生一个可执行文件或者共享库文件;
共享库文件:在动态链接的时候,由 ld-linux.so 来读取;
就拿链接器和加载器来说吧,这两个家伙的性格是不一样的,它们看我的眼光也是不一样的。
链接器看ELF文件,看不见 Program header table.
加载器看ELF文件,看不见 section header table, 并将section改个名字叫segment;
可以理解为:一个 Segment 可能包含一个或者多个 Sections,就像下面这样:
其实只要掌握到 2
点内容就可以了:
一个 ELF 文件一共由 4 个部分组成;
链接器和加载器,它们在使用我的时候,只会使用它们感兴趣的部分;
还有一点差点忘记给你提个醒了:在 Linux
系统中,会有不同的数据结构来描述上面所说的每部分内容。
描述 ELF header 的结构体:
描述 Program header table 的结构体:
描述 Section header table 的结构体:
ELF header(ELF 头)
头部内容,就相当于是一个总管,它决定了这个完整的 ELF 文件内部的所有信息,比如:
这是一个 ELF 文件;
一些基本信息:版本,文件类型,机器类型;
Program header table(程序头表)的开始地址,在整个文件的什么地方;
Section header table(节头表)的开始地址,在整个文件的什么地方;
为了方便描述,我就把 Sections
和 Segments
全部统一称为 Sections
在一个 ELF 文件中,存在很多个 Sections,这些 Sections 的具体信息,是在 Program header table
或者 Section head table
中进行描述的。
就拿 Section head table
来举例吧:
假如一个 ELF 文件中一共存在 4
个 Section: .text、.rodata、.data、.bss
,那么在 Section head table
中,将会有 4
个 Entry(条目)来分别描述这 4 个 Section 的具体信息(严格来说,不止 4 个 Entry,因为还存在一些其他辅助的 Sections),就像下面这样:
用一个具体的代码示例来描述,看实实在在的字节码。
程序的功能比较简单:
-
// mymath.c
-
int my_add(int a, int b)
-
{
-
return a + b;
-
}
-
// main.c
-
#include <stdio.h>
-
extern int my_add(int a, int b);
-
int main()
-
{
-
int i = 1;
-
int j = 2;
-
int k = my_add(i, j);
-
printf("k = %d \n", k);
-
}
从刚才的描述中可以知道:动态库文件 libmymath.so
, 目标文件 main.o
和 可执行文件 main
,它们都是 ELF 文件,只不过属于不同的类型。
这里就以可执行文件 main 来拆解它!
首先用指令 readelf -h main
来看一下 main 文件中,ELF header
的信息。
readelf 这个工具,可是一个好东西啊!一定要好好的利用它。
这张图中显示的信息,就是 ELF header
中描述的所有内容了。这个内容与结构体 Elf32_Ehdr
中的成员变量是一一对应的!
有没有发现图中第 15 行显示的内容:Size of this header: 52 (bytes)
。
也就是说:ELF header
部分的内容,一共是 52
个字节。那么我就把开头的这 52
个字节码给你看一下。
这回用 od -Ax -t x1 -N 52 main
这个指令来读取 main 中的字节码,简单解释一下其中的几个选项:
-Ax: 显示地址的时候,用十六进制来表示。如果使用 -Ad,意思就是用十进制来显示地址;
-t -x1: 显示字节码内容的时候,使用十六进制(x),每次显示一个字节(1);
-N 52:只需要读取 52 个字节;
这 52
个字节的内容,你可以对照上面的结构体中每个字段来解释了。
首先看一下前 16 个字节。
在结构体中的第一个成员是 unsigned char e_ident[EI_NIDENT];
,EI_NIDENT
的长度是 16
,代表了 EL header
中的开始 16
个字节,具体含义如下:
0 - 15 个字节
官方文档对于这部分的解释:
关于大端、小端格式,这个 main
文件中显示的是 1
,代表小端格式。啥意思呢,看下面这张图就明白了:
那么再来看一下大端格式:
好了,下面我们继续把剩下的 36
个字节(52 - 16 = 32),也以这样的字节码含义画出来:
16 - 31 个字节:
32 - 47 个字节:
48 - 51 个字节:
字符串表表项 Entry
在一个 ELF
文件中,存在很多字符串,例如:变量名、Section名称、链接器加入的符号等等,这些字符串的长度都是不固定的,因此用一个固定的结构来表示这些字符串,肯定是不现实的。
于是,把这些字符串集中起来,统一放在一起,作为一个独立的 Section
来进行管理。
在文件中的其他地方呢,如果想表示一个字符串,就在这个地方写一个数字索引:表示这个字符串位于字符串统一存储地方的某个偏移位置,经过这样的按图索骥,就可以找到这个具体的字符串了。
比如说啊,下面这个空间中存储了所有的字符串:
在程序的其他地方,如果想引用字符串 “hello,world!”,那么就只需要在那个地方标明数字 13
就可以了,表示:这个字符串从偏移 13 个字节处开始。
那么现在,咱们再回到这个 main
文件中的字符串表,
在 ELF header
的最后 2 个字节是 0x1C 0x00
,它对应结构体中的成员 e_shstrndx
,意思是这个 ELF 文件中,字符串表是一个普通的 Section,在这个 Section 中,存储了 ELF
文件中使用到的所有的字符串。
既然是一个 Section
,那么在 Section header table
中,就一定有一个表项 Entry 来描述它,那么是哪一个表项呢?
这就是 0x1C 0x00
这个表项,也就是第 28
个表项。
这里,我们还可以用指令 readelf -S main
来看一下这个 ELF
文件中所有的 Section
信息:
其中的第 28
个 Section,描述的正是字符串表 Section:
可以看出来:这个 Section
在 ELF
文件中的偏移地址是 0x0016ed
,长度是 0x00010a
个字节。
下面,我们从 ELF header
的二进制数据中,来推断这信息。
读取字符串表 Section 的内容
来演示一下:如何通过 ELF header
中提供的信息,把字符串表这个 Section
给找出来,然后把它的字节码打印出来给各位看官瞧瞧。
要想打印字符串表 Section
的内容,就必须知道这个 Section
在 ELF
文件中的偏移地址。
要想知道偏移地址,只能从 Section head table
中第 28
个表项描述信息中获取。
要想知道第 28
个表项的地址,就必须知道 Section head table
在 ELF
文件中的开始地址,以及每一个表项的大小。
正好最后这 2
个需求信息,在 ELF header
中都告诉我们了,因此我们倒着推算,就一定能成功。
ELF header
中的第 32
到 35
字节内容是:F8 17 00 00
(注意这里的字节序,低位在前),表示的就是 Section head table
在 ELF 文件中的开始地址(e_shoff
)。
0x000017F8 = 6136
,也就是说 Section head table
的开始地址位于 ELF
文件的第 6136
个字节处。
知道了开始地址,再来算一下第 28
个表项 Entry 的地址。
ELF header
中的第 46、47
字节内容是:28 00
,表示每个表项的长度是 0x0028 = 40
个字节。
注意这里的计算都是从 0
开始的,因此第 28
个表项的开始地址就是:6136 + 28 * 40 = 7256
,也就是说用来描述字符串表这个 Section
的表项,位于 ELF
文件的 7256
字节的位置。
既然知道了这个表项 Entry 的地址,那么就扒开来看一下其中的二进制内容:
执行指令:od -Ad -t x1 -j 7256 -N 40 main
其中的 -j 7256
选项,表示跳过前面的 7256
个字节,也就是我们从 main
这个 ELF
文件的 7256
字节处开始读取,一共读 40
个字节。
这 40
个字节的内容,就对应了 Elf32_Shdr
结构体中的每个成员变量:
这里主要关注一下上图中标注出来的 4
个字段:
sh_name: 暂时不告诉你,马上就解释到了;
sh_type:表示这个 Section 的类型,3 表示这是一个 string table;
sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x000016ed = 5869,意思是字符串表这个 Section 的内容,从 ELF 文件的 5869 个字节处开始;
sh_size:表示这个 Section 的长度。0x0000010a = 266 个字节,意思是字符串表这个 Section 的内容,一共有 266 个字节。
还记得刚才我们使用 readelf
工具,读取到字符串表 Section
在 ELF 文件中的偏移地址是 0x0016ed
,长度是 0x00010a
个字节吗?
与我们这里的推断是完全一致的!
既然知道了字符串表这个 Section
在 ELF
文件中的偏移量以及长度,那么就可以把它的字节码内容读取出来。
执行指令: od -Ad -t c -j 5869 -N 266 main
,所有这些参数应该不用再解释了吧?!
看一看,瞧一瞧,是不是这个 Section
中存储的全部是字符串?
刚才没有解释 sh_name
这个字段,它表示字符串表这个 Section
本身的名字,既然是名字,那一定是个字符串。
但是这个字符串不是直接存储在这里的,而是存储了一个索引,索引值是 0x00000011
,也就是十进制数值 17
。
现在我们来数一下字符串表 Section
内容中,第 17
个字节开始的地方,存储的是什么?
不要偷懒,数一下,是不是看到了:“.shstrtab” 这个字符串(\0是字符串的分隔符)?!
读取代码段的内容
从下面的这张图(指令:readelf -S main
):
可以看到代码段是位于第 14
个表项中,加载(虚拟)地址是 0x08048470
,它位于 ELF
文件中的偏移量是 0x000470
,长度是 0x0001b2
个字节。
那我们就来试着读一下其中的内容。
首先计算这个表项 Entry
的地址:6136 + 14 * 40 = 6696
。
然后读取这个表项 Entry
,读取指令是 od -Ad -t x1 -j 6696 -N 40 main
:
同样的,我们也只关心下面这 5
个字段内容:
sh_name: 这回应该清楚了,表示代码段的名称在字符串表 Section 中的偏移位置。0x9B = 155 字节,也就是在字符串表 Section 的第 155 字节处,存储的就是代码段的名字。回过头去找一下,看一下是不是字符串 “.text”;
sh_type:表示这个 Section 的类型,1(SHT_PROGBITS) 表示这是代码;
sh_addr:表示这个 Section 加载的虚拟地址是 0x08048470,这个值与 ELF header 中的 e_entry 字段的值是相同的;
sh_offset: 表示这个 Section,在 ELF 文件中的偏移量。0x00000470 = 1136,意思是这个 Section 的内容,从 ELF 文件的 1136 个字节处开始;
sh_size:表示这个 Section 的长度。0x000001b2 = 434 个字节,意思是代码段一共有 434 个字节。
以上这些分析结构,与指令 readelf -S main
读取出来的完全一样!
PS: 在查看字符串表 Section
中的字符串时,计算一下:字符串表的开始地址是 5869
(十进制),加上 155
,结果就是 6024
,所以从 6024
开始的地方,就是代码段的名称,也就是 “.text”。
知道了以上这些信息,我们就可以读取代码段的字节码了.使用指令:od -Ad -t x1 -j 1136 -N 434 main
即可。
内容全部是黑乎乎的的字节码,我就不贴出来了。
Program header
文章的开头,我就介绍了:我是一个通用的文件结构,链接器和加载器在看待我的时候,眼光是不同的。
为了对 Program header
有更感性的认识,我还是先用 readelf
这个工具来从总体上看一下 main
文件中的所有段信息。
执行指令:readelf -l main
,得到下面这张图:
显示的信息已经很明白了:
这是一个可执行程序;
入口地址是 0x8048470;
一共有 9 个 Program header,是从 ELF 文件的 52 个偏移地址开始的;
布局如下图所示:
从图中还可以看到,一共有 2
个 LOAD
类型的段:
我们来读取第一个 LOAD 类型的段,当然还是扒开其中的二进制字节码。
第一步的工作是,计算这个段表项的地址信息。
从 ELF header
中得知如下信息:
字段
e_phoff
:Program header table 位于 ELF 文件偏移 52 个字节的地方。字段
e_phentsize
: 每一个表项的长度是 32 个字节;字段
e_phnum
: 一共有 9 个表项 Entry;
通过计算,得到可读、可执行的 LOAD
段,位于偏移量 116
字节处。
执行读取指令:od -Ad -t x1 -j 116 -N 32 main
:
按照上面的惯例,我还是把其中几个需要关注的字段,与数据结构中的成员变量进行关联一下:
p_type: 段的类型,1: 表示这个段需要加载到内存中;
p_offset: 段在 ELF 文件中的偏移地址,这里值为 0,表示这个段从 ELF 文件的头部开始;
p_vaddr:段加载到内存中的虚拟地址 0x08048000;
p_paddr:段加载的物理地址,与虚拟地址相同;
p_filesz: 这个段在 ELF 文件中,占据的字节数,0x0744 = 1860 个字节;
p_memsz:这个段加载到内存中,需要占据的字节数,0x0744= 1860 个字节。注意:有些段是不需要加载到内存中的;
经过上述分析,我们就知道:从 ELF
文件的第 1
到 第 1860
个字节,都是属于这个 LOAD
段的内容。
在被执行时,这个段需要被加载到内存中虚拟地址为 0x08048000
这个地方,从这里开始,又是一个全新的故事了。
再回顾一下
其实只要抓住下面 2
个重点即可:
ELF header 描述了文件的总体信息,以及两个 table 的相关信息(偏移地址,表项个数,表项长度);
每一个 table 中,包括很多个表项 Entry,每一个表项都描述了一个 Section/Segment 的具体信息。
链接器和加载器也都是按照这样的原理来解析 ELF 文件的,明白了这些道理,后面在学习具体的链接、加载过程时,就不会迷路啦!
- EOF -
�6�9�6�9简单了解下ELF文件的格式。
1 简介
�6�9�6�9可执行与可链接格式 (Executable and Linkable Format,ELF),常被称为 ELF格式,是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式,一般用于类Unix系统,比如Linux,Macox等。ELF 格式灵活性高、可扩展,并且跨平台。比如它支持不同的字节序和地址范围,所以它不会不兼容某一特别的 CPU 或指令架构。这也使得 ELF 格式能够被运行于众多不同平台的各种操作系统所广泛采纳。
�6�9�6�9ELF文件一般由三种类型的文件:
- 可重定向文件:文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。比如编译的中间产物
.o
文件; - 可执行文件:一个可执行文件;
- 共享目标文件:共享库。文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接。比如linux下的
.so
文件。
2 ELF文件格式
�6�9�6�9在编译过程中ELF文件格式在链接和程序的运行阶段的格式不同。链接阶段每个.o
文件都是一个独立的ELF文件,为了效率和便利性他们的段需要进行合并才能生成对应的可执行文件。
�6�9�6�9ELF文件包含一个Header描述文件的基本信息;程序头表告诉徐彤如何构建进程的内存镜像,因此只有可执行文件由程序头表;Sections描述了链接过程中的需要的符号表、数据、指令等信息,而在可执行文件中是Segments,是经过合并的Secitons;节/段头表指明了对应section/segment在文件中的偏移,链接阶段的ELF文件必须包含该表头;而每个节/段头描述了对应的section/segment的大小,入口等基本信息。
�6�9�6�9下图是32bit系统下面使用的字段的数大小,64bit系统类似,之后不在赘述。
2.1 ELF Header
�6�9�6�9ELF文件头描述了ELF文件的基本类型,地址偏移等信息,分为32bit和64bit两个版本,定义于linux源码的/usr/include/elf.h
文件中。
#define EI_NIDENT 16
typedef struct elf32_hdr{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
typedef struct elf64_hdr {
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
�6�9�6�9从上面的结构中能够看出32bit和64bit的区别仅仅是字长的区别,字段上没有实际上的差别。每个字段的含义如下:
e_ident
:ELF文件的描述,是一个16字节的标识,表明当前文件的数据格式,位数等:- [0,3]字节为魔数,即
e_ident[EI_MAG0-EI_MAG3]
,取值为固定的0x7f E L F
,标记当前文件为一个ELF文件; - [4,4]字节为
EI_CLASS
即e_ident[EI_CLASS]
,表明当前文件的类别:- 0:表示非法的类别;
- 1:表示32bit;
- 2:表示64bit;
- [5,5]字节为
EI_DATA
即e_ident[EI_DATA]
,表明当期那文件的数据排列方式:- 0表示非法;
- 1表示小端;
- 2表示大端;
- [6,6]字节为
EI_VERSION
即e_ident[EI_VERSION]
,表明当前文件的版本,目前该取值必须为EV_CURRENT
即1; - [7,7]字节为
EI_PAD
即e_ident[EI_PAD]
表明e_ident
中未使用的字节的起点(值是相对于e_ident[EI_PAD+1]
的偏移),未使用的字节会被初始化为0,解析ELF文件时需要忽略对应的字段;
- [0,3]字节为魔数,即
�6�9�6�9EI_MAG0,EI_MAG1,EI_MAG2,EI_MAG3,EI_CLASS,EI_DATA,EI_VERSION,EI_OSABI,EI_PAD是linux源码中定义的宏,取值分别为0-7,分别对应各个字段的下标;下面的宏定义将采用类似EI_MAG0(0)的方式,表示EI_MAG0的值为0。
e_type
:文件的标识字段标识文件的类型;ET_NONE(0)
:未知的文件格式;ET_REL(1)
:可重定位文件,比如目标文件;ET_EXEC(2)
:可执行文件;ET_DYN(3)
:共享目标文件;ET_CORE(4)
:Core转储文件,比如程序crash之后的转储文件;ET_LOPROC(0xff00)
:特定处理器的文件标识;ET_HIPROC(0xffff)
:特定处理器的文件标识;[ET_LOPROC,ET_HIPROC]
之间的值用来表示特定处理器的文件格式;
e_machine
:目标文件的体系结构(下面列举了少数处理器架构,具体ELF文件支持的架构在对应的文件中查看即可);ET_NONE(0)
:未知的处理器架构;EM_M32(1)
:AT&T WE 32100;EM_SPARC(2)
:SPARC;EM_386(3)
:Intel 80386;EM_68K(4)
:Motorola 68000;EM_88K(5)
:Motorola 88000;EM_860(6)
:Intel 80860;EM_MIPS(7)
:MIPS RS3000大端;EM_MIPS_RS4_BE(10)
:MIPS RS4000大端;- 其他,预留;
e_version
:当前文件的版本;EV_NONE(0)
:非法的版本;EV_CURRENT(`)
:当前版本;
e_entry
:程序的虚拟入口地址,如果文件没有对应的入口可以为0;e_phoff
:文件中程序头表的偏移(bytes),如果文件没有该项,则应该为0;e_shoff
:文件中段表/节表的偏移(bytes),如果文件没有该项,则应该为0;e_flags
:处理器相关的标志位,宏格式为EF_machine_flag
比如EF_MIPS_PIC
;e_ehsize
:ELF文件头的大小(bytes);e_phentsize
:程序头表中单项的大小,表中每一项的大小相同;e_phnum
:程序头表中的项数,也就是说程序头表的实际大小为ephentsize x e_phnum
,如果文件中没有程序头表该项为0;e_shentsize
:节表中单项的大小,表中每一项的大小相同;e_shnum
:节表中项的数量;e_shstrndx
:节表中节名的索引,如果文件没有该表则该项为SHN_UNDEF(0)
。
2.2 程序头表(Program Header Table)
�6�9�6�9可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段 或者系统准备程序执行所必需的其它信息。程序头表描述了ELF文件中Segment在文件中的布局,描述了OS该如何装载可执行文件到内存。程序头表的表项的描述如下,类似于ELF Header也有32和64位两个版本。
typedef struct elf32_phdr {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
typedef struct elf64_phdr {
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
p_type
:当前Segment的类型;PT_NULL(0)
:当前项未使用,项中的成员是未定义的,需要忽略当前项;PT_LOAD(1)
:当前Segment是一个可装载的Segment,即可以被装载映射到内存中,其大小由p_filesz
和p_memsz
描述。如果p_memsz>p_filesz
则剩余的字节被置零,但是p_filesz>p_memsz
是非法的。动态库一般包含两个该类型的段:代码段和数据段;PT_DYNAMIC(2)
:动态段,动态库特有的段,包含了动态链接必须的一些信息,比如需要链接的共享库列表、GOT等等;PT_INTERP(3)
:当前段用于存储一段以NULL为结尾的字符串,该字符串表明了程序解释器的位置。且当前段仅仅对于可执行文件有实际意义,一个可执行文件中不能出现两个当前段,如果一个文件中包含当前段。比如/lib64/ld-linux-x86-64.so.2
;PT_NOTE(4)
:用于保存与特定供应商或者系统相关的附加信息以便于兼容性、一致性检查,但是实际上只保存了操作系统的规范信息;PT_SHLIB(5)
:保留段;PT_PHDR(6)
:保存程序头表本身的位置和大小,当前段不能在文件中出现一次以上,且仅仅当程序表头为内存映像的一部分时起作用,它必须在所有加载项目之前;[PT_LPROC(0x70000000),PT_HIPROC(0x7fffffff)]
:该范围内的值用作预留;
p_offset
:当前段相对于文件起始位置的偏移量;p_vaddr
:段的第一个字节将被映射到到内存中的虚拟地址;p_paddr
:此成员仅用于与物理地址相关的系统中。因为 System V 忽略所有应用程序的物理地址信息,此字段对与可执行文件和共享目标文件而言具体内容是指定的;p_filesz
:段在文件映像中所占的字节数,可能为 0;p_memsz
:段在内存映像中占用的字节数,可能为 0;p_flags
:段相关的标志;p_align
:段在文件中和内存中如何对齐。可加载的进程段的p_vaddr
和-p_offset
取值必须合适,相对于对页面大小的取模而言;- 0和1表示不需要对齐;
- 其他值必须为2的幂次方,且必须 p _ a d d r ∣ p _ a l i g n = = p _ o f f s e t ∣ p a l i g n p\_addr|p\_align==p\_offset| p_align p_addr∣p_align==p_offset∣pa�6�7lign。
2.3 节头表(Section Header Table)
�6�9�6�9节头表描述了ELF文件中的节的基本信息。可执行文件不一定由节头表但是一定有节,节头表可利用特殊的方式去除。
�6�9�6�9段和节的区别是:
- 段包含了程序装载可执行的基本信息,段告诉OS如何装载当前段到虚拟内存以及当前段的权限等和执行相关的信息,一个段可以包含0个或多个节;
- 节包含了程序的代码和数据等内容,链接器会将多个节合并为一个段。
typedef struct elf32_shdr {
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
typedef struct elf64_shdr {
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
sh_name
:值是节名称在字符串表中的索引;sh_type
:描述节的类型和语义;SHT_NULL(0)
:当前节是非活跃的,没有一个对应的具体的节内存;SHT_PROGBITS(1)
:包含了程序的指令信息、数据等程序运行相关的信息;SHT_SYMTAB(2)
:保存了符号信息,用于重定位;- 此种类型节的
sh_link
存储相关字符串表的节索引,sh_info
存储最后一个局部符号的符号表索引+1;
- 此种类型节的
SHT_DYNSYM(11)
:保存共享库导入动态符号信息;- 此种类型节的
sh_link
存储相关字符串表的节索引,sh_info
存储最后一个局部符号的符号表索引+1;
- 此种类型节的
SHT_STRTAB(3)
:一个字符串表,保存了每个节的节名称;SHT_RELA(4)
:存储可重定位表项,可能会有附加内容,目标文件可能有多个可重定位表项;- 此种类型节的
sh_link
存储相关符号表的节索引,sh_info
存储重定位所使用节的索引;
- 此种类型节的
SHT_HASH(5)
:存储符号哈希表,所有参与动态链接的目标只能包含一个哈希表,一个目标文件只能包含一个哈希表;- 此种类型节的
sh_link
存储哈希表所使用的符号表的节索引,sh_info
为0;
- 此种类型节的
SHT_DYAMIC(6)
:存储包含动态链接的信息,一个目标文件只能包含一个;- 此种类型的节的
sh_link
存储当前节中使用到的字符串表格的节的索引,sh_info
为0;
- 此种类型的节的
SHT_NOTE(7)
:存储以某种形式标记文件的信息;SHT_NOBITS(8)
:这种类型的节不占据文件空间,但是成员sh_offset
依然会包含对应的偏移;SHT_REL(9)
:包含可重定位表项,无附加内容,目标文件可能有多个可重定位表项;- 此种类型节的
sh_link
存储相关符号表的节索引,sh_info
存储重定位所使用节的索引;
- 此种类型节的
SHT_SHLIB(10)
:保留区,包含此节的程序与ABI不兼容;[SHT_LOPROC(0x70000000),SHT_HIPROC(0x7fffffff)]
:留给处理器专用语义;[SHT_LOUSER(0x80000000),SHT_HIUSER(0xffffffff)]
:预留;
sh_flags
:1bit位的标志位;SHF_WRITE(0x1)
:当前节包含进程执行过程中可写的数据;SHF_ALLOC(0x2)
:当前节在运行阶段占据内存;SHF_EXECINSTR(0x4)
:当前节包含可执行的机器指令;SHF_MASKPROC(0xf0000000)
:所有包含当前掩码都表示预留给特定处理器的;
sh_addr
:如果当前节需要被装载到内存,则当前项存储当前节映射到内存的首地址,否则应该为0;sh_offset
:当前节的首地址相对于文件的偏移;sh_size
:节的大小。但是对于类型为SHT_NOBITS
的节,当前值可能不为0但是在文件中不占据任何空间;sh_link
:存储节投标中的索引,表示当前节依赖于对应的节。对于特定的节有特定的含义,其他为SHN_UNDEF
;sh_info
:节的附加信息。对于特定的节有特定的含义,其他为0
;sh_addralign
:地址约束对齐,值应该为0或者2的幂次方,0和1表示未进行对齐;sh_entsize
:某些节是一个数组,对于这类节当前字段给出数组中每个项的字节数,比如符号表。如果节并不包含对应的数组,值应该为0。
2.3 一些特殊的节
�6�9�6�9ELF文件中有一些预定义的节来保存程序、数据和一些控制信息,这些节被用来链接或者装载程序。每个操作系统都支持一组链接模式,主要分为两类(也就是常说的动态库和静态库):
- Static:静态绑定的一组目标文件、系统库和库档案(比如静态库),解析包含的符号引用并创建一个完全自包含的可执行文件;
- Dynamic:一组目标文件、库、系统共享资源和其他共享库链接在一起创建可执行文件。当加载此可执行文件时必须使系统中其他共享资源和动态库可用,程序才能正常运行。
�6�9�6�9库文件无论是动态库还是静态库在其文件中都包含对应的节,一些特殊的节其功能如下:
.bss
,类型SHT_NOBITS
,属性SHF_ALLOC|SHF_WRITE
:存储未经初始化的数据。根据定义程序开始执行时,系统会将这些数据初始化为0,且此节不占用文件空间;.comment
,类型SHT_PROGBITS
,属性none
:存储版本控制信息;.data
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_WRITE
:存放初始化的数据;.data1
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_WRITE
:存放初始化的数据;.debug
,类型SHT_PROGBITS
,属性none
:存放用于符号调试的信息;.dynamic
,类型SHT_DYNAMIC
,属性SHF_ALLOC
,是否有属性SHF_WRITE
屈居于处理器:包含动态链接的信息,.hash
,类型SHT_HASH
,属性SHF_ALLOC
:.line
,类型SHT_PROGBITS
,属性none
:存储调试的行号信息,描述源代码和机器码之间的对应关系;.note
,类型SHT_NOTE
,属性none
:.rodata
,类型SHT_PROGBITS
,属性SHF_ALLOC
:存储只读数据;.rodata1
,类型SHT_PROGBITS
,属性SHF_ALLOC
:存储只读数据;.shstrtab
,类型SHT_STRTAB
,属性none
:存储节的名称;.strtab
,类型SHT_STRTAB
:存储常见的与符号表关联的字符串。如果文件有一个包含符号字符串表的可加载段,则该段的属性将包括 SHF_ALLOC 位; 否则,该位将关闭;.symtab
,类型SHT_SYMTAB
,属性``````:存储一个符号表。如果文件具有包含符号表的可加载段,则该节的属性将包括 SHF_ALLOC 位;否则,该位将关闭;.text
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储程序的代码指令;.dynstr
,类型SHT_STRTAB
,属性SHF_ALLOC
:存储动态链接所需的字符串,最常见的是表示与符号表条目关联的名称的字符串;.dynsym
,类型SHT_DYNSYM
,属性SHF_ALLOC
:存储动态链接符号表;.fini
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储有助于进程终止代码的可执行指令。 当程序正常退出时,系统执行本节代码;.init
,类型SHT_PROGBITS
,属性SHF_ALLOC|SHF_EXECINSTR
:存储有助于进程初始化代码的可执行指令。 当程序开始运行时,系统会在调用主程序入口点(C 程序称为 main)之前执行本节中的代码;.interp
,类型SHT_PROGBITS
:保存程序解释器的路径名。 如果文件有一个包含该节的可加载段,则该节的属性将包括 SHF_ALLOC 位; 否则,该位将关闭;.relname
,类型SHT_REL
:包含重定位信息。如果文件具有包含重定位的可加载段,则这些部分的属性将包括 SHF_ALLOC 位;否则,该位将关闭。通常,名称由 重定位适用的部分。因此.text
的重定位部分通常具有名称.rel.text
或.rela.text
;.relaname
,类型SHT_RELA
:同relname
。- 其他:对于C++程序有些版本会有
.ctors
(有时也会是.init_array
,见Can’t find .dtors and .ctors in binary)和dtors
两个节存储构造和析构相关的代码。
�6�9�6�9带有点 (.) 前缀的部分名称是为系统保留的,但如果它们的现有含义令人满意,应用程序可以使用这些部分。 应用程序可以使用不带前缀的名称以避免与系统部分冲突。 目标文件格式允许定义不在上面列表中的部分。 一个目标文件可能有多个同名的部分。
2.4 字符串表
�6�9�6�9字符串表是一个存储字符串的表格,而每个字符串是以NULL也就是\0
为结尾的。字符串表格中索引为0处的字符串被定义为空字符串。符号表中保存的字符串是节名和目标文件中使用到的符号。而需要使用对应字符串时,只需要在需要使用的地方指明对应字符在字符串表中的索引即可,使用的字符串就是索引处到第一个\0
之间的字符串。
2.5 符号表
�6�9�6�9目标文件的符号表包含定位和重定位程序的符号定义和引用所需的信息。符号表索引是该数组的下标。索引0既指定表中的第一个条目,又用作未定义的符号索引。
typedef struct elf32_sym{
Elf32_Word st_name;
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
typedef struct elf64_sym {
Elf64_Word st_name; /* Symbol name, index in string tbl */
unsigned char st_info; /* Type and binding attributes */
unsigned char st_other; /* No defined meaning, 0 */
Elf64_Half st_shndx; /* Associated section index */
Elf64_Addr st_value; /* Value of the symbol */
Elf64_Xword st_size; /* Associated symbol size */
} Elf64_Sym;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
st_name
:存储一个指向字符串表的索引来表示对应符号的名称;st_value
:存储对应符号的取值,具体值依赖于上下文,可能是一个指针地址,立即数等。另外,不同对象文件类型的符号表条目对 st_value 成员的解释略有不同:- 在重定位文件中在可重定位文件中,
st_value
保存节索引为SHN_COMMON
的符号的对齐约束; - 在可重定位文件中,
st_value
保存已定义符号的节偏移量。 也就是说,st_value
是从st_shndx
标识的部分的开头的偏移量; - 在可执行文件和共享对象文件中,
st_value
保存一个虚拟地址。 为了使这些文件的符号对动态链接器更有用,节偏移(文件解释)让位于与节号无关的虚拟地址(内存解释)。
- 在重定位文件中在可重定位文件中,
st_size
:符号的大小,具体指为sizeof(instance)
,如果未知则为0;st_info
:指定符号的类型和绑定属性。可以用下面的代码分别解析出bind,type,info
三个属性:
#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))
- 1
- 2
- 3
BIND
STB_LOCAL(0)
:局部符号在包含其定义的目标文件之外是不可见的。 同名的本地符号可以存在于多个文件中,互不干扰;STB_GLOBAL(1)
:全局符号对所有正在组合的目标文件都是可见的。 一个文件对全局符号的定义将满足另一个文件对同一全局符号的未定义引用;STB_WEAK(2)
:弱符号类似于全局符号,但它们的定义具有较低的优先级;[STB_LOPROC(13),STB_HIPROC(15)]
:预留位,用于特殊处理器的特定含义;
TYPE
:STT_NOTYPE(0)
:符号的类型未指定;STT_OBJECT(1)
:符号与数据对象相关联,例如变量、数组等;STT_FUNC(2)
:符号与函数或其他可执行代码相关联;STT_SECTION(3)
:该符号与一个节相关联。 这种类型的符号表条目主要用于重定位,通常具有STB_LOCAL
BIND属性;STT_FILE(4)
:一个有STB_LOCAL
的BIND属性的文件符号的节索引为SHN_ABS
。并且如果存在其他STB_LOCAL
属性的符号,则当前符号应该在其之前;[STT_LOPROC(13),STT_HIPROC(15)]
:预留位,用于特殊处理器的特定含义;
INFO
:SHN_ABS
:符号有一个绝对值,不会因为重定位而改变;SHN_COMMON
:该符号标记尚未分配的公共块。 符号的值给出了对齐约束,类似于节的 sh_addralign 成员。 也就是说,链接编辑器将为符号分配存储空间,该地址是 st_value 的倍数。 符号的大小表明需要多少字节;SHN_UNDEF
:此节表索引表示该符号未定义。 当链接编辑器将此对象文件与另一个定义指定符号的文件组合时,此文件对符号的引用将链接到实际定义;
st_other
:该成员当前持有 0 并且没有定义的含义;st_shndx
:每个符号都有属于的节,当前成员存储的就是对应节的索引。
3 ELF文件示例
�6�9�6�9下面是使用下面的代码编译生成动态库libadd.so
作为示例:
//add.h
int add(int a, int b);
static int mult(int a, int b);
- 1
- 2
- 3
//add.c
//编译命令gcc add.c -shared -o libadd.so
extern int extern_value;
static int static_value = 1;
static int static_value1;
int add(int a, int b){
return 0;
}
static int mult(int a, int b){
return 1;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
3.1 ELF Header
�6�9�6�9使用命令readelf -h <ELF文件名>
查看ELF文件的Header。
//readelf -h libadd.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4a0
Start of program headers: 64 (bytes into file)
Start of section headers: 6000 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 24
Section header string table index: 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
�6�9�6�9从上面的Magic Number中能够看出:当前文件类型为64bit的共享库,小端存储,版本为1,机器架构为x86-64,程序头表项有7项,节头表项有24项。
3.2 Program Header Table
�6�9�6�9使用命令readelf -l <ELF文件名>
查看程序头表;
//readelf -l libadd.so
Elf file type is DYN (Shared object file)
Entry point 0x4a0
There are 7 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000674 0x0000000000000674 R E 0x200000
LOAD 0x0000000000000e80 0x0000000000200e80 0x0000000000200e80 0x00000000000001a4 0x00000000000001b0 RW 0x200000
DYNAMIC 0x0000000000000e90 0x0000000000200e90 0x0000000000200e90 0x0000000000000150 0x0000000000000150 RW 0x8
NOTE 0x00000000000001c8 0x00000000000001c8 0x00000000000001c8 0x0000000000000024 0x0000000000000024 R 0x4
GNU_EH_FRAME 0x00000000000005a8 0x00000000000005a8 0x00000000000005a8 0x000000000000002c 0x000000000000002c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000000e80 0x0000000000200e80 0x0000000000200e80 0x0000000000000180 0x0000000000000180 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .rela.dyn .init .plt .plt.got .text .fini .eh_frame_hdr .eh_frame
01 .init_array .fini_array .dynamic .got .got.plt .data .bss
02 .dynamic
03 .note.gnu.build-id
04 .eh_frame_hdr
05
06 .init_array .fini_array .dynamic .got
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
�6�9�6�9从上面看出上半部分的内容基本和程序头表项的每个字段基本对应。从下面的Segment Sections可以看出一个Segment是多个Section的集合。
3.3 Section Header Table
�6�9�6�9使用命令readelf -S <ELF文件名>
查看节头表的内容。