Linux ELF 格式

黎 浩然/ 22 5 月, 2022/ 操作系统/OPERATINGSYSTEM/OS, 本科/UNDERGRADUATE, 计算机/COMPUTER/ 0 comments

本文根据/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的映射关系,如上图所示。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*