ELF(Executable and Linkable Format)文件是一种用于二进制文件、可执行文件、目标代码、共享库和core转存的格式文件,是UNIX系统实验室作为应用程序二进制接口(ABI)而开发和发布的,也是Linux的主要可执行文件格式。
可执行文件(.out): 包含代码和数据,可以直接运行,其代码和数据都有固定的地址或基地址偏移。系统可以根据这些地址加载程序
可重定位文件(.o): 包含基础代码和数据,但它们没有指定绝对地址,所以需要和其他目标文件链接来创建.o或者.so
共享目标文件(.so):包含代码和数据,这些数据是在链接时被链接器(ld)和运行时动态链接器(ld.so.1、libc.so.1、ld-linux.so.1)使用
内核转储(core dump):存放当前进程的执行上下文
这个ELF文件是属于上面类型,存在ELF头部信息中
ELF文件格式提供了2种视图:链接视图和执行视图
顾名思义,链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图,链接视图以节(section)为单位,执行视图以段(segment)为单位。
本文主要分析链接视图,继续细分一下
如上图,ELF可分为4大部分:
ELF头(ELF Header)——定义全局属性信息,比如幻数、目标体系结构、节头表地址偏移等;
程序头表(Program Header Table)——举了所有有效的段(segments)和它们的属性、程序表头需要加载器将文件中的节接在到虚拟内存中,对于可链接文件来说,程序表头可能为空;
节区(Section Table)——ELF文件中的数据和代码以节区的形式存储,不同类型的节区包含了不同的信息,如代码区、数据区、符号表区等;
节头表(Section Header Table)——包含对节(section)的描述,记录了ELF文件中各个节的起始偏移、大小、标志等信息;
描述ELF文件的主要特性(/usr/include/elf.h)
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf64_Half e_type; /* Object file type */
Elf64_Half e_machine; /* Architecture */
Elf64_Word e_version; /* Object file 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; /* Processor-specific flags */
Elf64_Half e_ehsize; /* ELF header size in bytes */
Elf64_Half e_phentsize; /* Program header table entry size */
Elf64_Half e_phnum; /* Program header table entry count */
Elf64_Half e_shentsize; /* Section header table entry size */
Elf64_Half e_shnum; /* Section header table entry count */
Elf64_Half e_shstrndx; /* Section header string table index */
} Elf64_Ehdr;
e_ident:幻数
e_type:目标文件类型(上面第一张图)
e_machine:体系结构类型(比如183是AARCH64架构)
还有一些数据不一一描述
下面的图是32位系统的,只有占用字节数不同
是一个结构体数组,每一个元素类型都是Elf64_Phdr(64位编译器)、Elf32_Phdr(32位编译器)描述了一个段或者其他系统在准备程序执行时所需要的信息,其中 ELF 头中的 e_phentsize 和 e_phnum 指定了该数组每个元素的大小以及元素个数,一个目标文件的段包含一个或者多个节。可以说程序表头就是专门为ELF文件运行时中的段所准备的
typedef struct
{
Elf64_Word p_type; /* Segment type */ 该字段为段的类型
Elf64_Word p_flags; /* Segment 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 该字段给出了文件镜像中该段的大小,可能为 0*/
Elf64_Xword p_memsz; /* Segment size in memory 该字段给出了内存镜像中该段的大小,可能为 0*/
Elf64_Xword p_align; /* Segment alignment 可加载的程序的段的 p_vaddr 以及 p_offset 的大小必须是 page 的整数倍*/
} Elf64_Phdr;
一个段可以包含一个或多个节,但是不同的段可能会有重合,即一个节在不同段里
这个数据结构位于ELF文件的尾部,具体位置在ELF头中的e_shoff项给出了偏移,e_shnum告诉我们节头表中包含的节数,e_shentsize给出了每一节的字节大小(比如64位系统就是64字节)
typedef struct {
Elf64_Word sh_name;
Elf64_Word sh_type;
Elf64_Xword sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
Elf64_Xword sh_size;
Elf64_Word sh_link;
Elf64_Word sh_info;
Elf64_Xword sh_addralign;
Elf64_Xword sh_entsize;
} Elf64_Shdr;
成员 |
说明 |
sh_name |
节名称,是节区头字符串表节区中(Section Header String Table Section)的索引,因此该字段实际是一个数值。在字符串表中的具体内容是以 NULL 结尾的字符串。 |
sh_type |
根据节的内容和语义进行分类,具体的类型下面会介绍 |
sh_flags |
每一比特代表不同的标志,描述节是否可写,可执行,需要分配内存等属性。 |
sh_addr |
如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应该在进程镜像中的位置。否则,此字段为 0 |
sh_offset |
给出节区的第一个字节与文件开始处之间的偏移。SHT_NOBITS 类型的节区不占用文件的空间,因此其 sh_offset 成员给出的是概念性的偏移。 |
sh_size |
此成员给出节区的字节大小。除非节区的类型是 SHT_NOBITS ,否则该节占用文件中的 sh_size 字节。类型为 SHT_NOBITS 的节区长度可能非零,不过却不占用文件中的空间。 |
sh_link |
此成员给出节区头部表索引链接,其具体的解释依赖于节区类型 |
sh_info |
此成员给出附加信息,其解释依赖于节区类型。 |
sh_addralign |
某些节区的地址需要对齐。例如,如果一个节区有一个 doubleword 类型的变量,那么系统必须保证整个节区按双字对齐。也就是说sh_addr%sh_addralign = 0。 目前它仅允许为 0,以及 2 的正整数幂数。 0 和 1 表示没有对齐约束 |
sh_entsize |
某些节区中存在具有固定大小的表项的表,如符号表。对于这类节区,该成员给出每个表项的字节大小。反之,此成员取值为 0 |
一些系统预定的固定section
sh_name |
sh_type |
说明 |
.text |
SHT_PROGBITS |
代码段,包含程序的可执行指令 |
.data |
SHT_PROGBITS |
包含初始化了的数据,将出现在程序的内存映像中 |
.bss |
SHT_NOBITS |
未初始化数据,因为只有符号所以 |
.rodata |
SHT_PROGBITS |
包含只读数据 |
.comment |
SHT_PROGBITS |
包含版本控制信息 |
.dynsym |
SHT_DYNSYM |
此节区包含了动态链接符号表 |
.shstrtab |
SHT_STRTAB |
存放section名,字符串表。Section Header String Table |
.strtab |
SHT_STRTAB |
字符串表 |
.symtab |
SHT_SYMTAB |
符号表 |
.text代码段可以通过objdump -d反汇编查看ELF文件代码段内容
ELF文件中有很多字符串,比如section name、变量名等,ELF会集中放到一起(.strtab、.shstrtab),每一个字符串以'\0'分割,然后保存首字符偏移进行引用字符串,可以使用readelf -S xxx.o查看所有section信息
假设一段代码
#include
unsigned char s_muse[]={0x12,0x34,0x56,0x78,0x90};
int main()
{
int a = 10;
printf("a=%d\n",a);
}
aarch64-xxx-gcc -c 5.c -o 5.o
readelf -h 5.o
可以知道架构是aarch64、elf头64字节、节头每个节64字节、elf有13个section、secton name在第12节中,
节头表偏移在848(0x350)这个位置上,我们可以从0x350开始解析所有节,从0x350到结尾都是节头表,总共64*13=832字节,可以通过readelf -S 5.o或许section信息
上图的所有信息其实都是在节头表每64个字节的节头表结构体中解析出来的,从节信息中又可以得到对应节的起始偏移、占用大小、一些flag等。
比如代码中定义了一个全局数组(0x12,0x34,0x56,0x78,0x90),它是保存在.data节中
可以看到起始位置是0x70、占用5个字节,首地址8字节对齐,使用16进制格式打开5.o
可以看到0x70开始的5个字节刚好就是数组数据了
ELF所有信息都可以通过偏移和结构体获得