2.3 ELF文件格式
ELF文件格式描述了Linux下可执行文件、可重定位目标文件、共享目标文件、核心转储文件的存储格式。本书设计的编译系统只关心可执行文件和可重定位目标文件的格式,如果要设计动态链接器的话,则还需要了解共享目标文件的内容。
ELF文件信息的一般存储形式如图2-11所示。
在Linux下,可以使用readelf命令查看ELF文件的信息。如果要查看1.3.3节生成的hello.o的信息,可以使用如下命令查看ELF的所有关键信息:
readelf –a hello.o
在ELF文件中,最开始的52个字节记录ELF文件头部的信息,通过它可以确定ELF文件内程序头表和段表的位置及大小。以下列出了hello.o文件头信息。
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 224 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 11
Section header string table index: 8
紧接着文件头便是程序头表,它记录程序运行时操作系统如何将文件加载到内存,因此只有可执行文件包含程序头表。使用readelf查看1.3.4节静态链接生成的hello文件,可以看到它的程序头表,类型为LOAD的表项表示需要加载的段。以下列出它的程序头表信息。
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x84fd2 0x84fd2 R E 0x1000
LOAD 0x085f8c 0x080cdf8c 0x080cdf8c 0x007d4 0x02388 RW 0x1000
NOTE 0x0000f4 0x080480f4 0x080480f4 0x00044 0x00044 R 0x4
TLS 0x085f8c 0x080cdf8c 0x080cdf8c 0x00010 0x00028 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x085f8c 0x080cdf8c 0x080cdf8c 0x00074 0x00074 R 0x1
ELF文件最关键的结构是段表,这里的段表示文件内的信息块,与汇编语言内的段并非同一个概念。段表记录了ELF文件内所有段的位置和大小等信息。在所有的段中,有保存代码二进制信息的代码段、存储数据的数据段、保存段表名称的段表字符串表段和存储程序字符串常量的字符串表段。符号表段记录汇编代码中定义的符号信息,重定位表段记录可重定位目标文件中需要重定位的符号信息。hello.o的段表如下:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[0] NULL 00000000 000000 000000 00 0 0 0
[1] .text PROGBITS 00000000 000034 00001d 00 AX 0 0 4
[2] .rel.text REL 00000000 000350 000010 08 9 1 4
[3] .data PROGBITS 00000000 000054 000000 00 WA 0 0 4
[4] .bss NOBITS 00000000 000054 000000 00 WA 0 0 4
[5] .rodata PROGBITS 00000000 000054 00000d 00 A 0 0 1
[6] .comment PROGBITS 00000000 000061 00002c 01 MS 0 0 1
[7] .note.GNU-stack PROGBITS 00000000 00008d 000000 00 0 0 1
[8] .shstrtab STRTAB 00000000 00008d 000051 00 0 0 1
[9] .symtab SYMTAB 00000000 000298 0000a0 10 10 8 4
[10].strtab STRTAB 00000000 000338 000015 00 0 0 1
符号表段是按照表格形式存储符号信息的,我们可以看到主函数和printf函数的符号项。
Symbol table '.symtab' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FILE LOCAL DEFAULT ABS hello.c
2: 00000000 0 SECTION LOCAL DEFAULT 1
3: 00000000 0 SECTION LOCAL DEFAULT 3
4: 00000000 0 SECTION LOCAL DEFAULT 4
5: 00000000 0 SECTION LOCAL DEFAULT 5
6: 00000000 0 SECTION LOCAL DEFAULT 7
7: 00000000 0 SECTION LOCAL DEFAULT 6
8: 00000000 29 FUNC GLOBAL DEFAULT 1 main
9: 00000000 0 NOTYPE GLOBAL DEFAULT UND printf
重定位表也是按照表格形式存储的,很明显,printf作为外部符号是需要重定位的。
Relocation section '.rel.text' at offset 0x350 contains 2 entries:
Offset Info Type Sym.Value Sym.Name
0000000a 00000501 R_386_32 00000000 .rodata
00000012 00000902 R_386_PC32 00000000 printf
从ELF文件格式的设计中可以看出,可执行文件其实就是按照一定标准将二进制数据和代码等信息包装起来,方便操作系统进行管理和使用。从文件头可以找到程序头表和段表,从段表可以找到其他所有的段。因此,在汇编语言输出目标文件的时候,就需要收集这些段的信息,并按照ELF格式组装目标文件。这样做不仅有利于使用操作系统现有的工具调试文件信息,也为后期链接器的实现提供了方便。
另外需要说明的是,对于ELF文件格式的定义,Linux提供了头文件描述。在系统目录/usr/include/elf.h提供的elf.h头文件中描述了标准ELF文件的数据结构的定义,在实现汇编器和链接器的代码中都使用了该头文件。