Linux系统ELF可执行文件格式详解
前言
本文主要讲解ELF文件格式分解讲解。并且会使用c代码将信息打印出来方便读者理解elf格式,并且手写一个ELF文件解析工具。文章中的执行环境为ubuntu 22.04 x64操作系统中编译的ELF文件,因此我们按照x64的来解析文件。
一、ELF文件类型
- ET_NONE: 未知类型。该标记的文件类型不确定或者还未定义。
- ET_REL: 重定位文件(目标文件, xxx.o )。可重定位文件是还未链接到可执行文件的独立代码,即编译后的 .o 文件。
- ET_EXEC: 可执行文件。这类文件也成为程序,是一个进程开始的入口。
- ET_DYN: 共享目标文件,即动态链接文件,.so 文件。在程序运行时被装载并链接到程序的进程镜像中。
- ET_CORE: 核心文件。在程序崩溃或者进程传递了一个SIGSEGV信号(分段违规)时,会在核心文件中记录整个进程的镜像信息。可以使用GDB读取这类文件来辅助调试并查找程序崩溃的原因。
二、ELF文件解析
其实我的理解,ELF文件主要分为四个部分(个人理解)1. ELF头 2. program 头表。3. section头表 4.section 段
然后还有一个program段其实是由多个section组成,如下图(这个图很重要):
然后对于ELF文件我们可以使用两种不同的视图来分析ELF文件,1. 链接视图。2. 执行视图。两个表示的是同一个文件,只是看数据的角度不一样而已。
2.1 ELF Header解析
我们先看看ELF 头部的数据结构。
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 , Program 头表的偏移位置,可以通过这个偏移获取 Program的表数据*/
Elf64_Off e_shoff; /* Section header table file offset , Section 头表的偏移位置,可以通过这个偏移获取 Section 的表数据*/
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 ,Program 头部表的大小*/
Elf64_Half e_phnum; /* Program header table entry count ,Program 有多少项数据*/
Elf64_Half e_shentsize; /* Section header table entry size ,Section 头部表的大小 */
Elf64_Half e_shnum; /* Section header table entry count ,Section 有多少项数据*/
Elf64_Half e_shstrndx; /* Section header string table index , 字符串的索引,表示字符串在第几个Section段中*/
} Elf64_Ehdr;
2.2 Section视图解析
2.2.1 视图表结构如下
typedef struct
{
Elf64_Word sh_name; /* Section name (string tbl index) */
Elf64_Word sh_type; /* Section type */
Elf64_Xword sh_flags; /* Section flags */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Section size in bytes */
Elf64_Word sh_link; /* Link to 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;
2.2.2 sh_name(节名字)
表示节区的名字。
获取节区名字的方法步骤如下:
- 获取ELF Header中的e_shstrndx,字符串节区的索引
- 中Section Header(e_shstrndx)中以及拿到对应节区
- 找到对应strtab区的表头,
- 从表头的偏移量中拿到真正的字符串存储区域。
- 根据sh_name的便宜两种拿到对应的字符串。
对应的获取代码如下:
// 获取shdr表头
Elf64_Shdr *shdr = reinterpret_cast<Elf64_Shdr *>(fileBuffer + ehdr->e_shoff);
// shdr[ehdr->e_shstrndx] 获取对应字符串节
// shdr[ehdr->e_shstrndx].sh_offset 获取该节的偏移量量拿到真正的字符串存储节
char *strtab = reinterpret_cast<char *>(fileBuffer + shdr[ehdr->e_shstrndx].sh_offset);
// 根据字符串存储节拿到对应的数据
std::string res(strtab + item.sh_name);
2.2.3 sh_type(节类型)
类型有以如下,具体的每总类型的节点分析在第三章中讲解。
* Legal values for sh_type (section type). */
#define SHT_NULL 0 /* Section header table entry unused */
#define SHT_PROGBITS 1 /* Program data */
#define SHT_SYMTAB 2 /* Symbol table */
#define SHT_STRTAB 3 /* String table */
#define SHT_RELA 4 /* Relocation entries with addends */
#define SHT_HASH 5 /* Symbol hash table */
#define SHT_DYNAMIC 6 /* Dynamic linking information */
#define SHT_NOTE 7 /* Notes */
#define SHT_NOBITS 8 /* Program space with no data (bss) */
#define SHT_REL 9 /* Relocation entries, no addends */
#define SHT_SHLIB 10 /* Reserved */
#define SHT_DYNSYM 11 /* Dynamic linker symbol table */
#define SHT_INIT_ARRAY 14 /* Array of constructors */
#define SHT_FINI_ARRAY 15 /* Array of destructors */
#define SHT_PREINIT_ARRAY 16 /* Array of pre-constructors */
#define SHT_GROUP 17 /* Section group */
#define SHT_SYMTAB_SHNDX 18 /* Extended section indices */
#define SHT_NUM 19 /* Number of defined types. */
#define SHT_LOOS 0x60000000 /* Start OS-specific. */
#define SHT_GNU_ATTRIBUTES 0x6ffffff5 /* Object attributes. */
#define SHT_GNU_HASH 0x6ffffff6 /* GNU-style hash table. */
#define SHT_GNU_LIBLIST 0x6ffffff7 /* Prelink library list */
#define SHT_CHECKSUM 0x6ffffff8 /* Checksum for DSO content. */
#define SHT_LOSUNW 0x6ffffffa /* Sun-specific low bound. */
#define SHT_SUNW_move 0x6ffffffa
#define SHT_SUNW_COMDAT 0x6ffffffb
#define SHT_SUNW_syminfo 0x6ffffffc
#define SHT_GNU_verdef 0x6ffffffd /* Version definition section. */
#define SHT_GNU_verneed 0x6ffffffe /* Version needs section. */
#define SHT_GNU_versym 0x6fffffff /* Version symbol table. */
#define SHT_HISUNW 0x6fffffff /* Sun-specific high bound. */
#define SHT_HIOS 0x6fffffff /* End OS-specific type */
#define SHT_LOPROC 0x70000000 /* Start of processor-specific */
#define SHT_HIPROC 0x7fffffff /* End of processor-specific */
#define SHT_LOUSER 0x80000000 /* Start of application-specific */
#define SHT_HIUSER 0x8fffffff /* End of application-specific */
获取方法如下:
#define SHT_CASE_TYPE(XX) case SHT_##XX: res=#XX;break;
std::string ELFRead::getSectionType(Elf64_Shdr item)
{
std::string res = "";
switch(item.sh_type){
SHT_CASE_TYPE(NULL)
SHT_CASE_TYPE(PROGBITS)
SHT_CASE_TYPE(SYMTAB)
SHT_CASE_TYPE(STRTAB)
SHT_CASE_TYPE(RELA)
SHT_CASE_TYPE(HASH)
SHT_CASE_TYPE(DYNAMIC)
SHT_CASE_TYPE(NOTE)
SHT_CASE_TYPE(NOBITS)
SHT_CASE_TYPE(REL)
SHT_CASE_TYPE(SHLIB)
SHT_CASE_TYPE(DYNSYM)
SHT_CASE_TYPE(INIT_ARRAY)
SHT_CASE_TYPE(FINI_ARRAY)
SHT_CASE_TYPE(PREINIT_ARRAY)
SHT_CASE_TYPE(GROUP)
SHT_CASE_TYPE(SYMTAB_SHNDX)
SHT_CASE_TYPE(NUM)
SHT_CASE_TYPE(LOOS)
SHT_CASE_TYPE(GNU_ATTRIBUTES)
SHT_CASE_TYPE(GNU_HASH)
SHT_CASE_TYPE(GNU_LIBLIST)
SHT_CASE_TYPE(CHECKSUM)
SHT_CASE_TYPE(LOSUNW)
SHT_CASE_TYPE(SUNW_COMDAT)
SHT_CASE_TYPE(SUNW_syminfo)
SHT_CASE_TYPE(GNU_verdef)
SHT_CASE_TYPE(GNU_verneed)
SHT_CASE_TYPE(GNU_versym)
SHT_CASE_TYPE(LOPROC)
SHT_CASE_TYPE(HIPROC)
SHT_CASE_TYPE(LOUSER)
SHT_CASE_TYPE(HIUSER)
}
return res;
}
2.2.4 sh_link和sh_info
这两字段比较特殊需要根据sh_type的类型他们的作用是不一样的。
sh_type | sh_link | sh_info |
---|---|---|
SHT_DYNAMIC | 表示这个节中的字符串节区的索引 | 0 |
SHT_HASH | 哈希表所应用的符号表的节头索引 | 0 |
SHT_REL | ||
SHT_RELA | ||
SHT_SYMTAB | ||
SHT_DYNSYM |
2.2.4 sh_flags (节权限标识)
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
std::string ELFRead::getSectionFlags(Elf64_Shdr item)
{
std::string res = "";
if(item.sh_flags & SHF_WRITE){
res += "W";
}
if(item.sh_flags & SHF_ALLOC){
res += "A";
}
if(item.sh_flags & SHF_EXECINSTR){
res += "X";
}
if(item.sh_flags & SHF_MERGE){
res += "M";
}
if(item.sh_flags & SHF_STRINGS){
res += "S";
}
if(item.sh_flags & SHF_INFO_LINK){
res += "S";
}
if(item.sh_flags & SHF_LINK_ORDER){
res += "S";
}
if(item.sh_flags & SHF_OS_NONCONFORMING){
res += "O";
}
if(item.sh_flags & SHF_GROUP){
res += "G";
}
if(item.sh_flags & SHF_TLS){
res += "T";
}
if(item.sh_flags & SHF_COMPRESSED){
res += "C";
}
if(item.sh_flags & SHF_MASKOS){
res += "o";
}
if(item.sh_flags & SHF_MASKPROC){
res += "P";
}
return res;
}
2.2.4 sh_addr
略
2.2.5 sh_offset (节偏移量)
我们可以根据这个偏移量找到对应的节的信息。
2.2.6 sh_size( 节的大小)
表示该节的总大小。
2.2.7 sh_entsize(节的item数量)
如果这个节是一个列表节点那么就会告诉我们在这个节区数据中有多少个item子项。
2.3 Program视图解析
Program各个字段如下:
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 */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
- p_type:类型有以下内容,常见的程序头类型有五种:PT_LOAD, PT_DYNAMIC, PT_NOTE, PT_INTERP, PT_PHDR。
注意其中PT_LOAD一个可执行文件至少包含一个 PT_LOAD 类型的段。这类段是可装载的段,将会被装载或映射到内存中。例如,存放程序代码的text段和存放全局变量的动态链接信息的data段会被映射到内存中,根据p_align中的值进行内存对齐。text 段(代码段)权限一般设置为PF_X| PF_R(读和可执行),通常将data段的权限设置为`PF_W | PF_R)(读和写)。
#define PT_NULL 0 /* Program header table entry unused */
#define PT_LOAD 1 /* Loadable program segment */
#define PT_DYNAMIC 2 /* Dynamic linking information */
#define PT_INTERP 3 /* Program interpreter */
#define PT_NOTE 4 /* Auxiliary information */
#define PT_SHLIB 5 /* Reserved */
#define PT_PHDR 6 /* Entry for header table itself */
#define PT_TLS 7 /* Thread-local storage segment */
#define PT_NUM 8 /* Number of defined types */
#define PT_LOOS 0x60000000 /* Start of OS-specific */
#define PT_GNU_EH_FRAME 0x6474e550 /* GCC .eh_frame_hdr segment */
#define PT_GNU_STACK 0x6474e551 /* Indicates stack executability */
#define PT_GNU_RELRO 0x6474e552 /* Read-only after relocation */
#define PT_GNU_PROPERTY 0x6474e553 /* GNU property */
#define PT_LOSUNW 0x6ffffffa
#define PT_SUNWBSS 0x6ffffffa /* Sun Specific segment */
#define PT_SUNWSTACK 0x6ffffffb /* Stack segment */
#define PT_HISUNW 0x6fffffff
#define PT_HIOS 0x6fffffff /* End of OS-specific */
#define PT_LOPROC 0x70000000 /* Start of processor-specific */
#define PT_HIPROC 0x7fffffff /* End of processor-specific */
#define PT_CASE_TYPE(XX) case PT_##XX: res=#XX;break;
std::string ELFRead::getSegmentType(Elf64_Phdr item)
{
std::string res="";
switch (item.p_type) {
PT_CASE_TYPE(NULL);
PT_CASE_TYPE(LOAD);
PT_CASE_TYPE(DYNAMIC);
PT_CASE_TYPE(INTERP);
PT_CASE_TYPE(NOTE);
PT_CASE_TYPE(SHLIB);
PT_CASE_TYPE(PHDR);
PT_CASE_TYPE(TLS);
PT_CASE_TYPE(NUM);
PT_CASE_TYPE(LOOS);
PT_CASE_TYPE(GNU_EH_FRAME);
PT_CASE_TYPE(GNU_STACK);
PT_CASE_TYPE(GNU_RELRO);
PT_CASE_TYPE(GNU_PROPERTY);
PT_CASE_TYPE(SUNWBSS);
PT_CASE_TYPE(SUNWSTACK);
PT_CASE_TYPE(HISUNW);
PT_CASE_TYPE(LOPROC);
PT_CASE_TYPE(HIPROC);
}
return res;
}
p_offset: 该segment的数据在文件中的偏移地址(相对文件头)
p_vaddr: segment数据应该加载到进程的虚拟地址
p_paddr: segment数据应该加载到进程的物理地址(如果对应系统使用的是物理地址)
p_filesz: 该segment数据在文件中的大小
p_memsz: 该segment数据在进程内存中的大小。注意需要满足p_memsz>=p_filesz,多出的部分初始化为0,通常作为.bss段内容
p_flags: 进程中该segment的权限(R/W/X)
#define PF_X (1 << 0) /* Segment is executable */
#define PF_W (1 << 1) /* Segment is writable */
#define PF_R (1 << 2) /* Segment is readable */
#define PF_MASKOS 0x0ff00000 /* OS-specific */
#define PF_MASKPROC 0xf0000000 /* Processor-specific */
p_align: 该segment数据的对齐,2的整数次幂。即要求p_offset % p_align = p_vaddr。
三、ELF各个节分析
根据不同类型的节他的存储的内存是不一样的。我们可以通过Section header中获取到该节的内容(通过Section Header中提供的sh_offset和sh_size,即通过偏移量和所占大小就能够拿到文件中某一段的数据这个数据就是整个Section的数据)
3.1 .text 节
该节保存的是代码指令。因此节的类型为SHT_PROGBITS。该类型官方定义为
3.2 .rodata 节
.rodata 保存了只读的数据。例如代码中的字符串。
printf("Hello world\n");
因为.rodata节是只读的,它存在于只读段中。因此,.rodata是在text段而不是data段。该节类型同样也是SHT_PROGBITS。
3.3 .plt 节
该节保存的是动态链接器从共享库导入的函数所必须的相关代码。因为保存的是代码,同样也存在与text段中,且节类型为SHT_PEOGBITS
3.4 .data 节
.data 节存在于data段,保存的是初始化的全局变量等数据,节类型也为SHT_PROGBITS。
3.5 .bss 节
.bss节保存的是未经初始化的全局变量数据,也是data段的一部分。占用空间不超过4字节,仅表示这个节本身的空间。程序加载时数据被初始化为0,在程序执行期间可以进行赋值。由于该节没有保存实际的数据,因此节类型为SHT_NOBITS。
3.6 .got.plt节
.got 节保存了全局偏移表。.got 节和.plt 节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。该节与程序执行有关,因此节类型为SHT_PROGBITS。
3.7 .dynsym 节
该节保存的是动态链接相关的导入导出符号,该节保存在text段中,节类型被标记为SHT_DYNSYM。
3.8 .dynstr 节
该节保存的是动态符号字符串表,是三种字符串表之一。表中的字符串以空字符为终止符,代表了符号的名称。
三种字符串表
1. .dynstr
2. .shstrtab
3. .strtab
elf load
https://github.com/malisal/loaders/blob/master/elf/elf.c