ELF 结构简述
浅浅研究了一下 ELF 的文件结构。
ELF 简介
ELF(Executable and Linkable Format)是 UNIX 系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是 Linux 的主要可执行文件格式。
ELF 结构概述
ELF 文件由 4 部分组成,分别是 ELF 头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如同所示这样安排,只有 ELF 头的位置是固定的,其余各部分的位置、大小等信息由 ELF 头中的各项值来决定。
ELF 头
下面以 32 位的 ELF 为例介绍。
#define EI_NIDENT 16
typedef struct{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry;
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;
数据类型说明:
名称 | 大小 | 用途 |
---|---|---|
Elf32_Addr | 4 | 无符号程序地址 |
Elf32_Half | 2 | 无符号中等大小整数 |
Elf32_Off | 4 | 无符号文件偏移 |
Elf32_Sword | 4 | 有符号大整数 |
Elf32_Word | 4 | 无符号大整数 |
unsigned char | 1 | 无符号小整数 |
-
e_ident
:最开头是 16 个字节的 e_ident ,其中包含用以表示 ELF 文件的字符,以及其他一些与机器无关的信息。#define EI_MAG0 0 //File identification #define EI_MAG1 1 //File identification #define EI_MAG2 2 //File identification #define EI_MAG3 3 //File identification #define EI_CLASS 4 //File class #define EI_DATA 5 //Data encoding #define EI_VERSION 6 //File version #define EI_PAD 7 //Start of padding bytes #define EI_NIDENT 16 //Size of e_ident[]
开头的 4 个字节值固定不变,为
0x7f
和ELF
三个字符。e_ident[EI_CLASS]
指明文件类别:0(无效目标文件);1(32 位目标文件);2(64 位目标文件)。e_ident[EI_DATA]
指明字节序,规定该文件是大端还是小端:0(无效编码格式);1(小端);2(大端)。e_ident[EI_VERSION]
指明 ELF 文件头的版本。从
e_ident[EI_PAD]
到e_ident[EI_NIDENT-1]
之间的 9 个字节保留。 -
e_type
:文件类型。常见的:1(可重定位文件:“.o 文件”);2(可执行文件);3(共享库文件:“.so 文件”)。
#define ET_NONE 0 //No file type #define ET_REL 1 //Relocatable file #define ET_EXEC 2 //Executable file #define ET_DYN 3 //Shared object file #define ET_CORE 4 //Core file #define ET_LOPROC 0xff00 //Processor-specific #define ET_HIPROC 0xffff //Processor-specific
-
e_machine
:指定该程序在什么平台上使用。 -
e_version
:ELF 文件版本号。 -
e_entry
:程序的入口虚拟地址。对于可执行文件来说,当 ELF 文件加载完成后,将从这个地址开始执行。对于其它文件,该值为 0 。 -
e_phoff
/e_shoff
:分别指明 Program Header Table 和 Section Header Table 在文件中的字节偏移量,没有则为 0。 -
e_flags
:处理器特定的标志位,通常不怎么关心。 -
e_ehsize
:指明 ELF 文件头的字节大小(52 个字节)。 -
e_phentsize
/e_phnum
:e_phentsize
指明在 Program Header Table 中的每一项的字节大小,e_phnum
指明共有多少项。 -
e_shentsize
/e_shnum
:e_shentsize
指明在 Section Header Table 中的每一项的字节大小,e_shnum
指明共有多少项。 -
e_shstrndx
:在 Section Header Table 中,存储“节名字表”的 Section(就是 .shstrtab 节)所对应的索引。
程序头表
程序头表由多个程序头组成。
下面以 32 位为例
/* Program segment header. */
typedef struct
{
Elf32_Word p_type; /* Segment type */
Elf32_Off p_offset; /* Segment file offset */
Elf32_Addr p_vaddr; /* Segment virtual address */
Elf32_Addr p_paddr; /* Segment physical address */
Elf32_Word p_filesz; /* Segment size in file */
Elf32_Word p_memsz; /* Segment size in memory */
Elf32_Word p_flags; /* Segment flags */
Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;
-
p_type
当前 Program header 所描述的段的类型。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
在物理内存定位相关的系统中,此项是为物理地址保留。 -
p_filesz
段在文件中的长度。 -
p_memsz
段在内存中的长度。 -
p_flags
与段相关的标志。 -
p_align
根据此项值来确定段在文件及内存中如何对齐。可加载的进程段的p_vaddr
和p_offset
取值必须合适,相对于对页面大小的取模而言;- 0 和 1 表示不需要对齐;
- 其他值必须为 2 的幂次方,且必须满足
p_addr|p_aligh == p_offset|a_align
。
节头表
节头表描述了 ELF 文件中的节的基本信息。可执行文件不一定有节头表,但是一定有节,节头表可利用特殊的方式去除。
段和节的区别是:
- 段包含了程序装载可执行的基本信息,段告诉 OS 如何装载当前段到虚拟内存以及当前段的权限等和执行相关的信息,一个段可以包含 0 个或多个节;
- 节包含了程序的代码和数据等内容,链接器会将多个节合并为一个段。
typedef struct elf32_shdr {
Elf32_Word sh_name; /* Section name, index in string tbl */
Elf32_Word sh_type; /* Type of section */
Elf32_Word sh_flags; /* Miscellaneous section attributes */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Size of section in bytes */
Elf32_Word sh_link; /* Index of another section */
Elf32_Word sh_info; /* Additional section information */
Elf32_Word sh_addralign; /* Section alignment */
Elf32_Word sh_entsize; /* Entry size if section holds table */
} Elf32_Shdr;
-
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
:1 bit 位的标志位;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。
一些特殊的节
ELF 文件中有一些预定义的节来保存程序、数据和一些控制信息,这些节被用来链接或者装载程序。每个操作系统都支持一组链接模式,主要分为两类(也就是常说的动态库和静态库):
- Static:静态绑定的一组目标文件、系统库和库档案(比如静态库),解析包含的符号引用并创建一个完全自包含的可执行文件;
- Dynamic:一组目标文件、库、系统共享资源和其他共享库链接在一起创建可执行文件。当加载此可执行文件时必须使系统中其他共享资源和动态库可用,程序才能正常运行。
库文件无论是动态库还是静态库在其文件中都包含对应的节,一些特殊的节其功能如下:
.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
)和.dtors
两个节存储构造和析构相关的代码。
字符串表
字符串表是一个存储字符串的表格,而每个字符串是以 NULL
也就是 '\0'
为结尾的。字符串表格中索引为 0 处的字符串被定义为空字符串。符号表中保存的字符串是节名和目标文件中使用到的符号。而需要使用对应字符串时,只需要在需要使用的地方指明对应字符在字符串表中的索引即可,使用的字符串就是索引处到第一个 '\0'
之间的字符串。
符号表
typedef struct elf32_sym{
Elf32_Word st_name; /* Symbol name, index in string tbl */
Elf32_Addr st_value; /* Value of the symbol */
Elf32_Word st_size; /* Associated symbol size */
unsigned char st_info; /* Type and binding attributes */
unsigned char st_other; /* No defined meaning, 0 */
Elf32_Hal st_shndx; /* Associated section index */
} Elf32_Sym;
-
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))
-
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_LOCALBIND
属性;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
:每个符号都有属于的节,当前成员存储的就是对应节的索引。
参考资料:
- ELF https://baike.baidu.com/item/ELF/7120560
- ELF Format 笔记(二)—— ELF Header https://www.cnblogs.com/ilocker/p/4572846.html
- ELF文件格式简介 https://blog.csdn.net/GrayOnDream/article/details/124564129
- ELF文件格式简介 https://it.cha138.com/javascript/show-87003.html
评论区