Linux可执行文件为ELF格式,ELF格式文件主要分为以下几类:
1. 可重定位文件(Relocatable File),这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类,如.o文件。
2. 可执行文件(Executable File),这类文件包含了直接执行的程序,如/bin/bash等。
3. 共享目标文件(Shared Object File),链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件;动态链接器可以将几个共享目标文件与可执行文件结合,作为进程映像的一部分来运行,如glibc***.so。
4. 核心转储文件(Core Dump File),当进程意外终止时,系统可以将该进程的地址空间内容及终止时的一些其他信息转储到核心转储文件。
如上图(转自51.com)所示,目标文件的开头是一个文件头,它描述了整个文件的文件属性,包括文件是否可执行,是静态链接还是动态链接及入口地址(如果是可执行文件),目标硬件,目标操作系统的信息。文件头还包括一个段表,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等。文件头后面就是各个段的内容,如代码段保存的是程序的指令,数据段保存的是程序里的一些变量等。
程序指令与数据分离成不同段的原因:
1. 当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。
2. 对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
3. 也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。
ELF文件中常见的段
常用的段名 |
说明 |
.text |
保存程序的指令序列 |
.data&.rodata |
保存初始化了的全局静态变量和局部静态变量 |
.bss |
存放未初始化的全局变量和局部静态变量 |
.rodata1 |
Read only Data,这种段里存放的是只读数据, 比如字符串常量、全局const变量。跟“.rodata”一样 |
.debug |
调试信息 |
.dynamic |
动态链接信息,详见本书第2部分 |
.hash |
符号哈希表 |
.line |
调试时的行号表,即源代码行号与编译后指令的对应表 |
.note |
额外的编译器信息。比如程序的公司名、发布版本号等 |
.strtab |
String Table.字符串表,用于存储ELF文件中用到的各种字符串 |
.symtab |
Symbol Table.符号表 |
.shstrtab |
Section String Table.段名表 |
.plt .got |
动态链接的跳转表和全局入口表,详见本书第2部分 |
.init .fini |
程序初始化与终结代码段。见“C++全局构造与析构”一节 |
l 文件头
文件头的结构定义如下所示:
typedef struct {
unsigned char e_ident[16]; /* ELF魔数,ELF字长,字节序,ELF文件版本等 */
Elf32_Half e_type; /*ELF文件类型,REL, 可执行文件,共享目标文件等 */
Elf32_Half e_machine; /* ELF的CPU平台属性 */
Elf32_Word e_version; /* ELF版本号 */
Elf32_Addr e_entry; /* ELF程序的入口虚拟地址,REL一般没有入口地址为0 */
Elf32_Off e_phoff;
Elf32_Off e_shoff; /* 段表在文件中的偏移 */
Elf32_Word e_flags; /* 用于标识ELF文件平台相关的属性 */
Elf32_Half e_ehsize; /* 本文件头的长度 */
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize; /* 段表描述符的大小 */
Elf32_Half e_shnum; /* 段表描述符的数量 */
Elf32_Half e_shstrndx; /* 段表字符串表所在的段在段表中的下标 */
} Elf32_Ehdr;
可通过readelf –h hello.o来查看ELF文件的文件头
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: 220 (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
l 段表
ELF文件中有很多段,段表就是保存这些段的基本属性的结构。段表是ELF文件中出文件头以为最重要的结构,它描述了ELF的各个段的信息,如每个段的名字,段的长度,在文件中的偏移,读写权限等。ELF文件的段结构就是由段表决定的,编译器,链接器和装载器都是通过段表来定位和访问各个段的属性的。
描述段的结构如下所示:
typedef struct
{
Elf32_Word sh_name; /* 段的名字 */
Elf32_Word sh_type; /* 段的类型,代码段,数据段,符号表等 */
Elf32_Word sh_flags; /* 段在进程虚拟地址空间中的属性 */
Elf32_Addr sh_addr; /* 段的虚拟地址 */
Elf32_Off sh_offset; /* 段在文件中的偏移 */
Elf32_Word sh_size; /* 段的长度 */
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign; /* 段地址对齐 */
Elf32_Word sh_entsize;
} Elf32;
可通过readelf –S *.o来查看ELF文件的段表
There are 11 section headers, starting at offset 0xdc:
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 00001c 00 AX 0 0 4
[ 2] .rel.text REL 00000000 000348 000010 08 9 1 4
[ 3] .data PROGBITS 00000000 000050 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000050 000000 00 WA 0 0 4
[ 5] .rodata PROGBITS 00000000 000050 00000c 00 A 0 0 1
[ 6] .comment PROGBITS 00000000 00005c 00002d 00 0 0 1
[ 7] .note.GNU-stack PROGBITS 00000000 000089 000000 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 000089 000051 00 0 0 1
[ 9] .symtab SYMTAB 00000000 000294 0000a0 10 10 8 4
[10] .strtab STRTAB 00000000 000334 000013 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
l 重定位表
从上面的输出可以看到,ELF文件中包含.rel.text的段,它是一个重定位表。链接器在处理目标文件时,需要对目标文件中某些部位进行重定位,及代码段和数据段中那些对绝对地址引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个需要重定位的代码段或数据段,都会有相应的重定位表。
l 字符串表
ELF文件中用到了很多字符串,比如段名,变量名等。因为字符串的长度往往不是固定的,故难以用固定的结构表示。常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。如下所示:
/0 |
h |
e |
l |
l |
o |
w |
o |
r |
l |
d |
/0 |
m |
y |
v |
a |
r |
i |
a |
b |
l |
e |
/0 |
|
|
|
|
|
|
|
通过该方法,在ELF文件中引用字符串只需要给出一个数字下标(偏移)即可,如上表所示:偏移0对应空字符串,偏移1对应helloworld,偏移6对应world,偏移12对应myvariable等而不用考虑字符串的长度,字符串表在ELF文件中也以段的形式保存,如.strtab(字符串表), .shstrtab(段表字符串表),前者用来保存普通的字符串,如符号的名字;后者用来保存段表中用到的字符串如段名。
一个典型的ELF目标文件的结构如下图(转自51.com)所示: