可执行文件概述
# 一、可执行文件生成概述
# 1.1 GCC 处理 C 程序的过程
# 1)预处理
C 预处理程序为 cpp(C Preprocessor),主要用于 C 语言编译器对各种预处理命令进行处理。包括:
- 宏定义的展开:删除
#define
并展开所定义的宏 - 条件预编译的选择:处理如
#if
,#ifdef
,#endif
等 - 头文件的包含:插入头文件到
#include
处,可以递归方式进行处理 - 删除注释
- 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息
- 保留所有
#pragma
编译指令(编译器需要用)
GCC 预处理命令:
- gcc –E hello.c -o hello.i
- cpp hello.c -o hello.i
经过预编译处理后,得到的是预处理文件(如,hello.i) ,它还是一个可读的文本文件,但不包含任何宏定义。
# 2)编译
编译过程就是将预处理后得到的预处理文件(如 hello.i)进行词法分析、语法分析、语义分析、优化后,生成汇编代码文件。
用来进行编译处理的程序称为编译程序(编译器,Compiler)。
GCC 编译命令:
- gcc -S hello.i -o hello.s
- gcc -S hello.c -o hello.s
- cc1 hello.c -o hello.s
经过编译后,得到的汇编代码文件(如 hello.s)还是可读的文本文件,CPU 无法理解和执行它。
提示
gcc命令实际上是具体程序(如ccp、cc1、as等)的包装命令,用户通过 gcc 命令来使用具体的预处理程序 ccp、编译程序 cc1 和 汇编程序 as 等
# 3)汇编
汇编的功能是将编译生成的汇编语言代码转换成机器指令序列。
GCC 汇编命令:
- gcc -c hello.s -o hello.o
- gcc -c hello.c -o hello.o
- as hello.s -o hello.o (as是一个汇编程序)
汇编结果是一个可重定位目标文件(如,hello.o),其中包含的是不可读的二进制代码。
# 4)链接
预处理、编译和汇编三个阶段针对一个模块(一个*.c文件)进行处理,得到对应的一个可重定位目标文件(一个*.o文件)。
链接过程将多个可重定位目标文件合并以生成可执行目标文件。链接的本质:合并相同的节。
GCC 链接命令:
- gcc -static -o myproc main.o test.o
- ld -static -o myproc main.o test.o
–static
表示静态链接。如果不指定 -o 选项,则默认生成的可执行文件名为“a.out”。
链接的好处:
- 1:模块化
- (1)一个程序可以分成很多源程序文件
- (2)可构建公共函数库,如数学库,标准C库等
- 2:效率高
- (1)时间上,可分开编译:只需重新编译被修改的源程序文件,然后重新链接;
- (2)空间上,无需包含共享库所有代码:源文件中无需包含共享库函数的源码,只要直接调用即可(如,只要直接调用printf()函数,无需包含其源码)。
# 1.2 可执行目标文件的生成
示例:
// main.c
int buf[2] = {1, 2};
void swap();
int main()
{
swap();
return 0;
}
// swap.c
extern int buf[];
int *bufp0 = &buf[0];
static int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 每个模块有自己的代码、数据(初始化全局变量、未初始化全局变量,静态变量、局部变量);
- 局部变量temp分配在栈中,不会在过程外被引用,因此不是符号定义。
使用GCC编译器编译并链接生成可执行程序 P:
$ gcc -O2 -g -o p main.c swap.c
$ ./p
2
gcc 编译器的静态链接过程:
链接操作的步骤:
- 符号解析
- 合并相关 .o 文件
- 确定每个符号的地址
- 在指令中填入新地址
# 二、目标文件格式
# 2.1 三类目标文件
- 可重定位目标文件(.o)
- 其代码和数据可和其他可重定位文件合并为可执行文件;
- 每个 .o 文件由对应的 .c 文件生成;
- 每个 .o 文件代码和数据地址都是从 0 开始。
- 可执行目标文件(default:a.out)
- 包含的代码和数据可以被直接复制到内存并被执行;
- 代码和数据地址为虚拟地址空间中的地址
- 共享的目标文件(.so)
- 特殊的可重定位目标文件,能在装入或运行时被装入到内存并自动被链接,称为共享库文件
- Windows 中称其为 Dynamic Link Libraries (DLLs)
# 2.2 目标文件的格式
目标代码(Object Code)指编译器和汇编器处理源代码后所生成的机器语言目标代码。
目标文件(Object File)指包含目标代码的文件。
目标文件的标准二进制格式:
- DOS 操作系统:COM 格式
- System V UNIX 早期版本:COFF 格式
- Windows:PE 格式
- Linux 等类 UNIX:ELF 格式
# 2.3 ELF 目标文件格式
目标文件既可用于程序的链接,也可用于程序的执行。
有两种视图:
- 链接视图,被链接,可重定位目标文件;
- 主要由不同的节组成,不同的节描述了目标文件中不同类型的信息及特征。
- 执行视图,被执行,可执行目标文件。
- 主要由不同的段组成,描述了目标文件中的节如何映射到存储空间的段。
# 2.4 可重定位目标文件格式
可重定位目标文件主要包含代码部分和数据部分,它可以与其他可重定位目标文件链接,从而创建可执行目标文件、共享库文件。
- 可被链接(合并)生成可执行文件或共享目标文件;
- 静态链接库文件由若干个可重定位目标文件组成;
- 包含代码、数据(已初始化全局变量和局部静态变量.data和未初始化的全局变量和局部静态变量.bss);
- 包含重定位信息(指出哪些符号引用处需要重定位);
- 文件扩展名为.o(相当于Windows中的 .obj文件)
# (1)ELF 头
ELF 头位于目标文件的起始位置,包含文件结构说明信息。分32位版本和64位版本。
32位系统对应的数据结构,共占 52(0x34) 字节:
#define EI_NIDENT 16
typedef struct {
unsigned char e_ident[EI_NIDENT]; // 开始的四个字节是魔数,后面的16个字节包含一些标识信息,如字节序、32/64位、版本号等
Elf32_Half e_type; // 目标文件类型(可重定位文件、可执行文件、共享库文件还是其他类型文件)
Elf32_Half e_machine; // 机器结构类型(如 IA-32、AMD 64等)
Elf32_Word e_version; // 目标文件版本
Elf32_Addr e_entry; // 指定系统将控制权转移到的起始虚拟地址(程序入口点),如果没有关联入口点则为0,比如可重定位文件就是0
Elf32_Off e_phoff;
Elf32_Off e_shoff; // 节头表在文件中的偏移量(以字节为单位)
Elf32_Word e_flags;
Elf32_Half e_ehsize; // ELF 头的大小(以字节为单位)
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize; // 节头表中一个表项的大小,所有表项大小相同
Elf32_Half e_shnum; // 节头表项的个数
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 魔数:文件开头几个字节通常用来确定文件的类型或格式。
- a.out的魔数:01H 07H
- PE格式魔数:4DH 5AH
- 加载或读取文件时,可用魔数确认文件类型是否正确
- 仅 ELF 头在文件中具有固定位置,即总是在开头位置,其余部分的位置由 ELF 头和节头表指出。
可以用 readelf -h
命令对某个可重定位目标文件的 ELF 头进行解析。
ELF头信息举例
- 因为是可重定位文件,所以字段
e_entry
为0,无程序头表(Size of program headers = 0)。
# (2)节
节是 ELF 文件中的主体信息,包含了链接过程所用的目标代码信息,包括指令、数据、符号表和重定位信息等。
节名 | 说明 |
---|---|
.text | 已编译程序的机器代码 |
.rodata | 只读数据,如 printf 语句中的格式串、开关语句(switch-case)的跳转表等 |
.data | 已初始化的全局变量 |
.bss | 未初始化的全局和静态变量,以及所有被初始化为 0 的全局或静态变量。在目标文件中这个节不占据实际的空间,它仅仅是一个占位符,为了节省空间。在运行时,在内存中将这些变量初始化为 0。 |
.symtab | 符号表(symbol table),程序中定义的函数名和全局静态变量名都属于符号,与这些符号相关的信息保存在符号表中。每个可重定位目标文件都有一个 .symtab 节。和编译器中的符号表不同, .symtab 符号表不包含局部变量的条目。 |
.rel.text | .text 节相关的重定位信息。当链接器合并目标文件时,.text 中的代码合并后部分位置的数据需要修改。一般而言,任何调用外部函数或者引用全局变量的指令都需要修改定位信息。另一方面,调用本地函数的指令则不需要修改。 TIP:可执行文件中并不需要重定位信息。 |
.rel.data | .data 节相关的可重定位信息。一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要被修改。 |
.debug | 调试用符号表。其条目是程序中定义的全局变量和类型定义,程序中定义和引用的全局变量,以及原始的 C 源文件。 |
.line | C 源程序中的行号和 .text 节中机器指令之间的映射。 |
.strtab | 字符串表。包括 .symtab 节和 .debug 节中的符号以及节头表中的节名。字符串表就是以 null 结尾的字符序列。 |
- .bss 可以记成 "Better Save Space"。
- 只有使用 -g 选项的 gcc 命令才能得到 .debug 和 .line 表。
# (3)节头表
节头表(Section Header Table)由若干表项组成,每个表项描述相应的一个节的节名、在文件中的偏移、大小、访问属性、对齐方式等,目标文件中的每个节都有一个表项与之对应。
- 除ELF头之外,节头表是ELF可重定位目标文件中最重要的部分内容。
以下是 32 位系统对应的数据结构(每个表项占40B):
typedef struct {
Elf32_Word sh_name; // 节名字符串在.strtab中的偏移
Elf32_Word sh_type; // 节类型:无效/代码或数据/符号/字符串/…
Elf32_Word sh_flags; // 节标志:该节在虚拟空间中的访问属性
Elf32_Addr sh_addr; // 虚拟地址:若可被加载,则对应虚拟地址
Elf32_Off sh_offset; // 在文件中的偏移地址,对.bss节而言则无意义
Elf32_Word sh_size; // 节在文件中所占的长度
Elf32_Word sh_link; // sh_link和sh_info用于与链接相关的节(如.rel.text节、.rel.data节、.symtab节等)
Elf32_Word sh_info;
Elf32_Word sh_addralign; // 节的对齐要求
Elf32_Word sh_entsize; // 节中每个表项的长度,0表示无固定长度表项
} Elf32_Shdr;
2
3
4
5
6
7
8
9
10
11
12
节头表信息举例
- 从上述解析结果可以看出,该 test.o 共 11 个节。节头表从 120 字节开始。
- 其中,.text、.data、.bss 和 .rodata 节需要在存储器中分配空间。.text 是可执行的,.data 和 .bss 是可读写的,.rodata 是只读的。
- 由于在真正运行时也要对 .bss 节中的数据进行读写,所以它也要占空间。
- 根据每个节在文件中的偏移地址和长度,可以画出 test.o 的结构:
- 比较特殊的一个地方:.bss 在文件中不占用空间,但节头表记录了 .bss 节的长度为 0x0c = 12,因此需要在主存中给 .bss 节分配 12 字节空间。
整体就是通过 ELF 头连接了节头表,再通过节头表把每一个节连接起来了。
# 2.5 可执行目标文件格式
与可重定文件稍有不同:
- ELF头中字段 e_entry 给出执行程序时第一条指令的地址,而在可重定位文件中,此字段为0;
- 多一个程序头表,也称段头表(segment header table),是一个结构数组,用来说明段信息;
- 多一个.init节,用于定义_init函数,该函数用来进行可执行目标文件开始执行时的初始化工作;
- 少两个.rel节(无需重定位)。
在可执行文件中,ELF 头、程序头表、.init 节、.fini 节、.text 节和 .rodata 节合起来可构成一个只读代码段;.data 节和 .bss 节合起来可构成一个可读写数据段。显然,在可执行文件启动执行时,这两个段必须装入内存,因而称为可装入段。
# 程序头表
为了可执行文件执行时能够给在内存中访问到代码和数据,必须将可执行文件中这些连续的、具有相同访问属性的代码和数据段映射到存储空间(通常是虚拟地址空间)。程序头表就用于描述这种映射关系,一个表项对应一个连续的存储段或特殊节。程序头表表项大小分别由 ELF 头中的字段 e_phentsize 和 e_phnum 指定。
32 为系统的程序头表中每个表项具有如下数据结构:
typedef struct {
Elf32_Word p_type; // 描述存储段的类型或特殊节的类型
Elf32_Off p_offset; // 指出本段的首字节在文件中的偏移地址
Elf32_Addr p_vaddr; // 指出本段首字节的虚拟地址
Elf32_Addr p_paddr; // 指出本段首字节的物理地址,该信息通常无效
Elf32_Word p_filesz; // 指出本段在文件中所占的字节数,可以为 0
Elf32_Word p_memsz; // 本段在存储器中所占的字节数,也可为 0
Elf32_Word p_flags; // 存取权限
Elf32_Word p_align; // 对齐方式,用一个模数表示,为 2 的正整数幂
} Elf32_Phdr;
2
3
4
5
6
7
8
9
10
程序头表示例
使用 readelf -l main
命令显示的可执行文件 main 的程序头表信息:
对于任何段 s,链接器必须选择一个起始地址 vaddr,使得
这种对齐是一种优化,使得当程序执行时,目标文件中的段能够很有效率地传送到内存中。