Linux ELF 格式
本文根据/usr/include/elf.h文件来分析ELF可执行文件的文件头(ELF header)、Program header table和Section header table的构成。本文只介绍32位的ELF可执行文件:

一个segment往往会包含多个section但未必包含的每个section都是完整的
ELF Header
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
从中可知,ELF文件的主要内容——Program header (table)和Section header (table)两者的大小、在ELF文件中的位置和数量都能通过文件头来获取。而后又可以通过Program header (table)来获得每个segment的属性,通过Section header (table)来获得每个section的属性。
ELF文件头的说明如下表所示:

其中,由于每个section header的大小是固定的,而它们的名称属性不可能一样长,所以需要一个专门的(string) section来保存它们的名称属性,而这个(string) section在section header (table)中的位置就由e_shstrndx(secton header string name index)来确认(e_shoff+ e_shentsize * e_shstrndx)。这样用来达到快速查询的目的。

ELF文件头的标志位(e_ident)总共有16个字节的大小,目前只用到9个字节,剩余字节的值都为0。
标志位(e_ident)的说明如下表所示:

其中,2’s complement的意思就是补码,而1’s complement的意思为反码。
通过执行readelf命令可获得ELF可执行文件的文件头:

然而在这个ELF文件中,4520直接是section header (table)的偏移,但不是elf的最后一部分。


文件的总大小为 0x1f70 + 0x25f = 8048 + 607 =

ELF Section header (table)
typedef struct
{
Elf32_Word sh_name; /* Section name (string tbl index) */
Elf32_Word sh_type; /* Section type */
Elf32_Word sh_flags; /* Section flags */
Elf32_Addr sh_addr; /* Section virtual addr at execution */
Elf32_Off sh_offset; /* Section file offset */
Elf32_Word sh_size; /* Section size in bytes */
Elf32_Word sh_link; /* Link to 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;
在ELF文件中,每个section header都有一个索引号,从一开始依次递增。不过section header table的开头必须定义一个类型为SHT_NULL的表项,它的索引号为SHN_UNDEF(也就是0)。由于section header使用相同的数据结构来定义,并且它们的大小相同,所以所有的section header依次存储在一起看起来就是一个表,因此可以叫它section header table。类似的,所有program/segment header看起来也是一个表(program header table)。
sh_name 表示section 的名称。由于每个section 名称的长度不相同,并且为了节约空间,于是就将所有section的名称都存放在一个特定的名叫.shstrtab(section header string table)的section(文件头信息中的Elf32_Ehdr.e_shstrndx就是用来快速查找它)中,所以在这里的sh_name的值指的就是在这个 string section 中的偏移量,通过它可以获得一个字符串,也就是所需要的section名。零值表示无名称,一般用于类型为SHT_NULL的section中。
sh_type表示section的类型。它可能的取值有以下这些:
/* 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 indeces */
#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 */
其中,每个ELF文件中都有一个SHT_NULL类型的section,它只有section header (table),没有相应的section数据,section header中的数据都为0,没有意义。
SHT_PROGBITS类型的section中的内容为程序数据,如代码、全局变量等。例如 .interp section用 于保存动态链接器的绝对地址,如/lib/ld-linux.so.2;.comment section用于保存版本控制信息,如GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39);.eh_frame_hdr和.eh_frame section用于保存异常处理信息。.eh_frame_hdr是对.eh_frame的补充,可以通过执行readelf -wf tanglinux命令可以查看.eh_frame section中的内容。
NOBITS类型一般用于名叫.bss的section,该section也只有section header,没有实际的内容,不占用可执行文件的空间。
SHT_STRTAB类型的section用于保存字符串,它的格式如下例所示:

通过图中的索引Index就可以获得从Index开始到空字符为止的一段字符串。例如若Index取值为1则可获得字符串“name.”。.shstrtab就是一个典型的SHT_STRTAB类型的section,执行以下命令就可以输出该段中的内容,如下所示:

中括号[]中的数字就是用来获取其后所示字符串的索引。
可以执行hexdump -C license_1命令打印文件中每个字节的数值,然后根据文件偏移量sh_offset(如0x109d)和该section的大小sh_entsize(如0x108)来获取.shstrtab section的所有内容,如下图所示:

命令hexdump对非打印字符采用点(﹒)来代替,在上图中为了区分section名称前的点,则将空字符对应的点用空格代替。
sh_flags表示section的标志。它的可能取值如下所示:
/* Legal values for sh_flags (section flags). */
#define SHF_WRITE (1 << 0) /* Writable */
#define SHF_ALLOC (1 << 1) /* Occupies memory during execution */
#define SHF_EXECINSTR (1 << 2) /* Executable */
#define SHF_MERGE (1 << 4) /* Might be merged */
#define SHF_STRINGS (1 << 5) /* Contains nul-terminated strings */
#define SHF_INFO_LINK (1 << 6) /* `sh_info' contains SHT index */
#define SHF_LINK_ORDER (1 << 7) /* Preserve order after combining */
#define SHF_OS_NONCONFORMING (1 << 8) /* Non-standard OS specific handling
required */
#define SHF_GROUP (1 << 9) /* Section is member of a group. */
#define SHF_TLS (1 << 10) /* Section hold thread-local data. */
#define SHF_COMPRESSED (1 << 11) /* Section with compressed data. */
#define SHF_MASKOS 0x0ff00000 /* OS-specific. */
#define SHF_MASKPROC 0xf0000000 /* Processor-specific */
#define SHF_ORDERED (1 << 30) /* Special ordering requirement
(Solaris). */
#define SHF_EXCLUDE (1U << 31) /* Section is excluded unless
referenced or allocated (Solaris).*/
SHF_WRITE表示section可写;SHF_ALLOC表示在程序执行时需要占用内存空间;SHF_EXECINSTR表示可执行。
sh_addr 表示该section在进程空间中的虚拟地址(首地址)。如果该成员的值为零,则表示该 section不会出现在进程空间中。
sh_offset表示该section相对ELF文件开头的偏移量。
sh_size 表示该section的大小(字节数)。通过上面这两个成员就可以获得某个 section 的所有内容。
sh_addralign 表示该section 的地址对齐方式(也就是sh_addr对sh_addralign取模的余数为0)。它的可能值为2的倍数,0和1表示不采用对齐方式。
sh_entsize表示该section中每个表项的大小。其内容以固定大小项来存储,如符号表,否则为0。
sh_link表示链接到其他section header的索引号。
sh_info表示附加的段信息。以上这两个成员的值只对以下表中的section类型有效,其他类型的值都section为0。

通过执行readelf命令可获得可执行文件中所有的section header信息,如下所示:

只通过命令行选项 -S 执行readelf命令,Name最多只打印17个字符,会造成第[ 3]项的Name 显示不全,完全显示则须要添加-W选项。

ELF Program header (table)
section header (table) 用于描述section 的特性,而 program header (table) 用于描述segment的特性,目标文件(也就是文件名以.o结尾的文件)不存在program header,因为它不能运行。segment包含一个或多个现有的section,相当于从程序执行的角度来看待这些section。program 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表示segment的类型。它的可能值如下所示:
/* Legal values for p_type (segment type). */
#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_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 */
p_offset表示该segment相对ELF文件开头的偏移量。
p_filesz表示该segment在ELF文件中的大小。
p_memsz表示该segment加载到内存后所占用的大小。p_filesz和p_memsz的大小只有在少数情况下不相同,如包含.bss section的segment,因为.bss section在ELF文件中不占用空间,但在内存中需要占用相应字节大小的空间。通过p_offset和p_filesz两个成员就可以获得相应 segment 中的所有内容,所以这里就不再需要section header (table)的支持,但需要ELF header中信息来确定program (segment) header table的位置。因此ELFheader(它包含在第一个LOAD segment中)要加载到内存。
p_vaddr表示segment的虚拟地址。
p_paddr表示它的物理地址。在现代常见的体系架构中,很少直接使用物理地址,所以这里p_paddr的值与p_vaddr相同。
p_align表示segment的对齐方式(也就是p_vaddr和p_offset对p_align取模的余数为零)。它的可能值为二的倍数,0和1表示不采用对齐方式。p_align的对齐方式不仅针对虚拟地址(p_vaddr),在 ELF文件中的偏移量(p_offset)也要采用与虚拟地址相同的对齐方式。此外,PT_LOAD类型的segment需要针对所在操作系统的页对齐。
p_flags表示segment的标志。它的可能取值如下所示:
/* Legal values for p_flags (segment flags). */
#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 */
PF_X表示segment可执行,PF_W表示可写,PF_R表示可读。
通过执行readelf命令可获得可执行文件中所有的program header信息,如下所示:

从中可以看出section到segment的映射关系,如上图所示。