[toc]
下面我们介绍一种文件格式:ELF格式
,全名为可执行和可链接格式(Executable and Linkable Format)
。维基百科中这样描述:
在计算机科学中,ELF文件是一种用于可执行文件、目标文件、共享库和核心转储(
core dump
)的标准文件格式。其中核心转储是指: 操作系统在进程收到某些信号而终止时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试。
一、ELF文件类型
通俗点说由汇编器和链接器生成的文件都属于ELF文件。通常我们接触的ELF文件主要有以下三类:
- 可重定位文件(
relocatable file
) 它保存了一些可以和其他目标文件链接并生成可执行文件或者共享库的二进制代码和数据。 - 可执行文件(
excutable file
)它保存了适合直接加载到内存中执行的二进制程序。 - 共享库文件(
shared object file
)一种特殊的可重定位目标文件,可以在加载或者运行时被动态的加载进内存并链接。
总之,ELF文件是一种文件格式。但凡是一种格式,总要有一些规则,下面我们来介绍ELF文件的格式规则。
二、ELF文件结构
一个典型的ELF文件包括ELF Header、Sections、Section Header Table和Program Header Table。其位置分布如下图所示:
图1 ELF文件构成
1. ELF Header
每个ELF文件都存在一个ELF Header用来描述其结构和组成。ELF Header其实对应的是一个结构体,该结构体定义如下:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
ElfN_Addr e_entry;
ElfN_Off e_phoff;
ElfN_Off e_shoff;
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} ElfN_Ehdr;
其中
ElfN_Addr Unsigned program address, uintN_t
ElfN_Off Unsigned file offset, uintN_t
上述结构体的各成员意义如下:
e_ident
:包含一个magic number、ABI信息,该文件使用的平台、大小端规则e_type
:文件类型, 表示该文件属于可执行文件、可重定位文件、core dump文件或者共享库e_machine
:机器类型e_version
:通常都是1e_entry
: 表示程序执行的入口地址e_phoff
: 表示Program Header的入口偏移量(以字节为单位)e_shoff
: 表示Section Header的入口偏移量(以字节为单位)e_flags
: 保存了这个ELF文件相关的特定处理器的flage_ehsize
: 表示ELF Header大小(以字节为单位)e_phentsize
: 表示Program Header大小(以字节为单位)e_phnum
: 表示Program Header的数量 (十进制数字)e_shentsize
: 表示Section Header大小(以字节为单位)e_shnum
: 表示Section Header的数量 (十进制数字)e_shstrndx
: 表示字符串表的索引,字符串表用来保存ELF文件中的字符串,比如段名、变量名。 然后通过字符串在表中的偏移访问字符串。
例如,machine的值为0x3e
, 十进制为62,其代表的意义可以从文件elf-em.h中找到,如下:
#define EM_X86_64 62 /* AMD x86-64 */
表示,这个ELF文件可以运行在x86_64的机器上。
2. Section
在ELF文件中,数据和代码分开存放的,这样可以按照其功能属性分成一些区域,比如程序、数据、符号表等。这些分离存放的区域在ELF文件中反映成section
。ELF文件中典型的section
如下:
.text
: 已编译程序的二进制代码.rodata
: 只读数据段,比如常量.data
: 已初始化的全局变量和静态变量.bss
: 未初始化的全局变量和静态变量,所有被初始化成0的全局变量和静态变量.sysmtab
: 符号表,它存放了程序中定义和引用的函数和全局变量的信息.debug
: 调试符号表,它需要以'-g'
选项编译才能得到,里面保存了程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C文件.line
: 原始的C文件行号和.text节
中机器指令之间的映射.strtab
: 字符串表,内容包括.symtab
和.debug
节中的符号表
特殊的,
1)对于可重定位的文件,由于在编译时,并不能确定它引用的外部函数和变量的地址信息,因此,编译器在生成目标文件时,增加了两个section:
.rel.text
保存了程序中引用的外部函数的重定位信息,这些信息用于在链接时重定位其对应的符号。.rel.data
保存了被模块引用或定义的所有全局变量的重定位信息,这些信息用于在链接时重定位其对应的全局变量。
2)对于可执行文件,由于它已经全部完成了重定位工作,可以直接加载到内存中执行,所以它不存在.rel.text
和.rel.data
这两个section。但是,它增加了一个section:
.init
: 这个section里面保存了程序运行前的初始化代码
上述描述的各个文件中包含的这些section是必须存在的,当然除了这些section,每种文件还有一些其他的section用来存放编译器或者链接器所需要的辅助信息,详情请参考参考阅读1,在这里就不过多的讨论了。
3. Section Header Table
上述各个section的大小和位置等具体信息的存放是由Section Header Table来描述的。Section Header Table是一个结构体数组,对应的结构体定义如下:
typedef struct {
uint32_t sh_name;
uint32_t sh_type;
uint64_t sh_flags;
Elf64_Addr sh_addr;
Elf64_Off sh_offset;
uint64_t sh_size;
uint32_t sh_link;
uint32_t sh_info;
uint64_t sh_addralign;
uint64_t sh_entsize;
} Elf64_Shdr;
其中各成员的意义如下:
sh_name
:表示该section的名字相对于.shstrtab
section的地址偏移量。sh_type
:表示该section中存放的内容类型,比如符号表,可重定位段等。sh_flags
: 表示该section的一些属性,比如是否可写,可执行等。sh_addr
: 表示该section在程序运行时的内存地址sh_offset
: 表示该section相对于ELF文件起始地址的偏移量sh_size
: 表示该section的大小sh_link
: 配合sh_info保存section的额外信息sh_info
:保存该section相关的一些额外信息sh_addralign
:表示该section需要的地址对齐信息sh_entsize
:有些section里保存的是一些固定长度的条目,比如符号表。对于这些section来讲,sh_entsize里保存的就是条目的长度。
4. Program Header Table
前面讲过了,section基本是按照目标文件内容的功能来划分的一些区域,而根据其内容在内存中是否可读写等属性,又可以将不同的section划分成不同的segment
。其中每个segment可以由一个或多个section组成。其关系如图1所示。
在可执行文件中,ELF header下面紧接着就是Program Header Table。它描述了各个segment在ELF文件中的位置以及在程序执行过程中系统需要准备的其他信息。它也是用一个结构体数组来表示的。具体代码如下:
typedef uint64_t Elf64_Addr;
typedef uint64_t Elf64_Off;
typedef uint32_t Elf64_Word;
typedef uint64_t Elf64_Xword;
typedef struct {
Elf64_Word p_type; // 4
Elf64_Word p_flags; // 4
Elf64_Off p_offset; // 8
Elf64_Addr p_vaddr; // 8
Elf64_Addr p_paddr; // 8
Elf64_Xword p_filesz; // 8
Elf64_Xword p_memsz; // 8
Elf64_Xword p_align; // 8
} Elf64_Phdr;
各个字段的具体含义如下:
p_type
:描述了当前segment是何种类型的或者如何解释当前segment,比如是动态链接相关的或者可加载类型的等p_flags
:保存了该segment的flagp_offset
:表示从ELF文件到该segment第一个字节的偏移量p_vaddr
:表示该segment的第一个字节在内存中的虚拟地址p_paddr
:对于使用物理地址的系统来讲,这个成员表示该segment的物理地址p_filesz
:表示该segment的大小,以字节表示p_memsz
:表示该segment在内存中的大小,以字节表示p_align
:表示该segment在文件中或者内存中需要以多少字节对齐
三、实践
为了进一步加深对ELF文件整体结构的理解,我们取一个64bit的linux上的可执行文件hello
来验证。
首先,使用READELF
工具查看其ELF Header信息:
$ readelf -h hello
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400430
Start of program headers: 64 (bytes into file)
Start of section headers: 6616 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 28
从ELF Header中我们得到如下信息:
- ELF Header大小(
Size of this header
)为64 byte - Program Header Table的起始地址(
Start of program headers
)是64 byte,其内部共有9个(Number of program headers
)Program Header,每个大小为56 byte(Size of program headers
) - Section Header Table的起始地址(
Start of section headers
)是6616 byte,其内部共有31个(Number of section headers
)Section Header,每个大小为64 byte(Size of this header
)
其次,使用READELF
命令读取section header table信息:
$ readelf -S hello
There are 31 section headers, starting at offset 0x19d8:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
0000000000000024 0000000000000000 A 0 0 4
... snip ...
[27] .comment PROGBITS 0000000000000000 00001038
0000000000000035 0000000000000001 MS 0 0 1
[28] .shstrtab STRTAB 0000000000000000 000018cc
000000000000010c 0000000000000000 0 0 1
[29] .symtab SYMTAB 0000000000000000 00001070
0000000000000648 0000000000000018 30 47 8
[30] .strtab STRTAB 0000000000000000 000016b8
0000000000000214 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
根据上述section信息,我们得到:
- section的起始地址是
0x238
(第一个section.interp
的起始地址) - section的结束地址是
.shstrtab
的结束地址即:0x18cc + 0x10c = 0x19d8
根据我们上面的结论,我们可以得到该ELF文件的组成图如下:
name | start_addr | size | end_addr |
---|---|---|---|
ELF Header | 0 | 0x40 | |
Program Header Table | 0x40 | 0x1F8 | |
Section | 0x238 | 0x19D8 | |
Section Header Table | 0x19D8 | 0x7C0 |
我们可以计算出各部分的end_addr上述空白部分,得到:
显然,各个section正好和下一个section首尾相接。然后,通过ls -l hello
命令查看该二进制文件的大小,得到该文件大小为8600 byte
,而Section Header Table
的end_addr 0x2198
换算成十进制大小正好是8600 byte
。从而证明我们上述的分析是没问题的。
四、总结
- ELF文件包括可重定位文件、可执行文件、共享库和Core dump文件。其基本结构包括ELF Header,Program Header Table,Sections和Section Header Table。其中不同的sections组成了不同的segment。
- ELF Header描述了该ELF文件中Program Header Table的位置、大小,Section Header Table的位置、大小和程序执行的入口地址等信息。
- Program Header Table描述了各个segment的类型、虚拟地址、相对文件的偏移地址等信息。
- Section Header Table描述了各个section的名字、类型、相对文件的偏移地址等信息。
大牛,厉害!!!
非常支持,向理解计算机原理的博客真的很少,感谢笔者的分享
想问一下这里的program header table和 PCB 程序控制段这个数据结构或许有什么关联吗