GCC编译器原理(二)------编译原理一:ELF文件(1)
二、ELF 文件介绍
2.1 可执行文件格式综述
相对于其它文件类型,可执行文件可能是一个操作系统中最重要的文件类型,因为它们是完成操作的真正执行者。可执行文件的大小、运行速度、资源占用情况以及可扩展性、可移植性等与文件格式的定义和文件加载过程紧密相关。研究可执行文件的格式对编写高性能程序和一些黑客技术的运用都是非常有意义的。
可执行链接格式 ( Executable and Linking Format)最初是由 UNIX 系统实验室 ( UNIX System Laboratories, USL)开发并发布的, 作为应用程序二进制接口 ( Application Binary Interface, ABI)的一部分。
不管何种可执行文件格式,一些基本的要素是必须的,显而易见的,文件中应包含代码和数据。因为文件可能引用外部文件定义的符号(变量和函数),因此重定位信息和符号信息也是需要的。一些辅助信息是可选的,如调试信息、硬件信息等。基本上任意一种可执行文件格式都是按区间保存上述信息,称为段(Segment)或节(Section)。不同的文件格式中段和节的含义可能有细微区别,但根据上下文关系可以很清楚的理解,这不是关键问题。最后,可执行文件通常都有一个文件头部以描述本文件的总体结构。
相对可执行文件有三个重要的概念:编译(compile)、连接(link,也可称为链接、联接)、加载(load)。源程序文件被编译成目标文件,多个目标文件被连接成一个最终的可执行文件,可执行文件被加载到内存中运行。
ELF全称Executable and Linkable Format,可执行连接格式,ELF 格式的文件用于存储 Linux 程序。
2.2 LINUX平台下ELF文件加载过程的简单描述
- 内核首先读 ELF 文件的头部,然后根据头部的数据指示分别读入各种数据结构,找到标记为可加载(loadable)的段,并调用函数 mmap() 把段内容加载到内存中。在加载之前,内核把段的标记直接传递给 mmap() ,段的标记指示该段在内存中是否可读、可写,可执行。显然,文本段是只读可执行,而数据段是可读可写。这种方式是利用了现代操作系统和处理器对内存的保护功能。
- 内核分析出 ELF 文件标记为 PT_INTERP 的段中所对应的动态连接器名称,并加载动态连接器。现代 LINUX 系统的动态连接器通常是 /lib/ld-linux.so.2。
- 内核在新进程的堆栈中设置一些标记-值对,以指示动态连接器的相关操作。
- 内核把控制传递给动态连接器。
- 动态连接器检查程序对外部文件(共享库)的依赖性,并在需要时对其进行加载。
- 动态连接器对程序的外部引用进行重定位,通俗的讲,就是告诉程序其引用的外部变量/函数的地址,此地址位于共享库被加载在内存的区间内。动态连接还有一个延迟(Lazy)定位的特性,即只在 "真正" 需要引用符号时才重定位,这对提高程序运行效率有极大帮助。
- 动态连接器执行在 ELF 文件中标记为 .init 的节的代码,进行程序运行的初始化。在早期系统中,初始化代码对应函数 _init(void)(函数名强制固定),在现代系统中,则对应形式为
void __attribute((constructor)) init_function(void)
{
……
}
其中函数名为任意。
8. 动态连接器把控制传递给程序,从 ELF 文件头部中定义的程序进入点开始执行。在 a.out 格式和 ELF 格式中,程序进入点的值是显式存在的,在 COFF 格式中则是由规范隐含定义。
- 从上面的描述可以看出,加载文件最重要的是完成两件事情:
- 加载程序段和数据段到内存;
- 进行外部定义符号的重定位。
重定位是程序连接中一个重要概念。我们知道,一个可执行程序通常是由一个含有 main() 的主程序文件、若干目标文件、若干共享库(Shared Libraries)组成。(注:采用一些特别的技巧,也可编写没有 main 函数的程序)
一个 C 程序可能引用共享库定义的变量或函数,换句话说就是程序运行时必须知道这些变量/ 函数的地址。在静态连接中,程序所有需要使用的外部定义都完全包含在可执行程序中,而动态连接则只在可执行文件中设置相关外部定义的一些引用信息,真正的重定位是在程序运行之时。
- 静态连接方式有两个大问题:
- 如果库中变量或函数有任何变化都必须重新编译连接程序;
- 如果多个程序引用同样的变量/函数,则此变量/函数会在文件/内存中出现多次,浪费硬盘/内存空间。
比较两种连接方式生成的可执行文件的大小,可以看出有明显的区别
三、 文件格式分析
3.1 a.out 文件格式分析
a.out 格式在不同的机器平台和不同的 UNIX 操作系统上有轻微的不同,下面我们讨论的是最 "标准" 的格式。
a.out 文件包含 7 个 section,格式如下:

执行头部的数据结构(/usr/include/x86_64-linux-gnu/a.out.h):
struct exec {
unsigned long a_info; /* 魔数和其它信息 */
unsigned long a_text; /* 文本段的长度 */
unsigned long a_data; /* 数据段的长度 */
unsigned long a_bss; /* BSS段的长度 */
unsigned long a_syms; /* 符号表的长度 */
unsigned long a_entry; /* 程序进入点 */
unsigned long a_trsize; /* 文本重定位表的长度 */
unsigned long a_drsize; /* 数据重定位表的长度 */
};
文件头部主要描述了各个 section 的长度,比较重要的字段是 a_entry(程序进入点),代表了系统在加载程序并初试化各种环境后开始执行程序代码的入口。
由 a.out 格式和头部数据结构我们可以看出,a.out 的格式非常紧凑,只包含了程序运行所必须的信息(文本、数据、BSS),而且每个 section 的顺序是固定的。这种结构缺乏扩展性,如不能包含 "现代" 可执行文件中常见的调试信息,最初的 UNIX 黑客对 a.out 文件调试使用的工具是 adb,而 adb 是一种机器语言调试器!
a.out 文件中包含符号表和两个重定位表,这三个表的内容在连接目标文件以生成可执行文件时起作用。在最终可执行的 a.out 文件中,这三个表的长度都为 0 。
a.out 文件在连接时就把所有外部定义包含在可执行程序中,如果从程序设计的角度来看,这是一种硬编码方式,或者可称为模块之间是强藕和的。
a.out 是早期UNIX系统使用的可执行文件格式,由 AT&T 设计,现在基本上已被 ELF 文件格式代替。a.out 的设计比较简单,但其设计思想明显的被后续的可执行文件格式所继承和发扬。
2.2 COFF 文件格式分析
COFF 格式比 a.out 格式要复杂一些,最重要的是包含一个节段表(section table),因此除了 .text,.data,和 .bss 区段以外,还可以包含其它的区段。另外也多了一个可选的头部,不同的操作系统可一对此头部做特定的定义。
COFF 文件格式如下:

- 结构体位于文件:
- ubuntu中:/usr/include/linux/coff.h
- Linux 内核中:include/uapi/linux/coff.h
文件头部的数据结构:
struct COFF_filehdr
{
unsigned short f_magic[]; /* 魔数 */
unsigned short f_nscns[]; /* 节个数 */
long f_timdat[]; /* 文件建立时间 */
long f_symptr[]; /* 符号表相对文件的偏移量 */
long f_nsyms[]; /* 符号表条目个数 */
unsigned short f_opthdr[]; /* 可选头部长度 */
unsigned short f_flags[]; /* 标志 */
}
COFF 文件头部中魔数与其它两种格式的意义不太一样,它是表示针对的机器类型,例如 0x014c 相对于 I386 平台,而 0x268 相对于 Motorola 68000系列等。当 COFF 文件为可执行文件时,字段 f_flags 的值为 F_EXEC(0X00002),同时也表示此文件没有未解析的符号,换句话说,也就是重定位在连接时就已经完成。由此也可以看出,原始的 COFF 格式不支持动态连接。为了解决这个问题以及增加一些新的特性,一些操作系统对 COFF 格式进行了扩展。Microsoft 设计了名为 PE(Portable Executable)的文件格式,主要扩展是在 COFF 文件头部之上增加了一些专用头部,某些 UNIX 系统也对 COFF 格式进行了扩展,如 XCOFF(extended common object file format)格式,支持动态连接。
紧接文件头部的是可选头部,COFF 文件格式规范中规定可选头部的长度可以为 0,但在 LINUX 系统下可选头部是必须存在的。下面是 LINUX 下可选头部的数据结构:
typedef struct
{
char magic[]; /* 魔数 */
char vstamp[]; /* 版本号 */
char tsize[]; /* 文本段长度 */
char dsize[]; /* 已初始化数据段长度 */
char bsize[]; /* 未初始化数据段长度 */
char entry[]; /* 程序进入点 */
char text_start[]; /* 文本段基地址 */
char data_start[]; /* 数据段基地址 */
} COFF_AOUTHDR;
字段 magic 为 0413 时表示 COFF 文件是可执行的,注意到可选头部中显式定义了程序进入点,标准的 COFF 文件没有明确的定义程序进入点的值,通常是从 .text 节开始执行,但这种设计并不好。
前面我们提到,COFF 格式比 a.out 格式多了一个节段表,一个节头条目描述一个节数据的细节,因此 COFF 格式能包含更多的节,或者说可以根据实际需要,增加特定的节,具体表现在 COFF 格式本身的定义以及稍早提及的 COFF 格式扩展。下面将简单描述 COFF 文件中节的数据结构,因为节的意义更多体现在程序的编译和连接上,所以本文不对其做更多的描述。此外,ELF 格式和 COFF 格式对节的定义非常相似,在随后的 ELF 格式分析中,将省略相关讨论。
struct COFF_scnhdr
{
char s_name[]; /* 节名称 */
char s_paddr[]; /* 物理地址 */
char s_vaddr[]; /* 虚拟地址 */
char s_size[]; /* 节长度 */
char s_scnptr[]; /* 节数据相对文件的偏移量 */
char s_relptr[]; /* 节重定位信息偏移量 */
char s_lnnoptr[]; /* 节行信息偏移量 */
char s_nreloc[]; /* 节重定位条目数 */
char s_nlnno[]; /* 节行信息条目数 */
char s_flags[]; /* 段标记 */
};
LINUX 系统中头文件 coff.h 中对字段 s_paddr 的注释是 "physical address" ,但似乎应该理解为 "节被加载到内存中所占用的空间长度" 。字段 s_flags 标记该节的类型,如文本段、数据段、BSS 段等。在 COFF 的节中也出现了行信息,行信息描述了二进制代码与源代码的行号之间的对映关系,在调试时很有用。
GCC编译器原理(二)------编译原理一:ELF文件(1)的更多相关文章
- 使用gcc不同选项来编译查看中间生成文件
gcc编译C程序的总体流程如下图 用到的命令如下: .c---> .i gcc -E hello.c .c--->.s gcc -S hello.c .c--->.o gcc -c ...
- Knowledge Point 20180303 对比编译器、解释器与Javac编译原理
编译器与Javac编译原理 在前文我们知道了Java是一种编译语言和解释语言,它的源代码经过编译器Javac编译为能够被JVM识别的二进制语言,然后JVM将其解释为能够被平台识别的机器语言.那么什么是 ...
- 前端与编译原理——用JS写一个JS解释器
说起编译原理,印象往往只停留在本科时那些枯燥的课程和晦涩的概念.作为前端开发者,编译原理似乎离我们很远,对它的理解很可能仅仅局限于"抽象语法树(AST)".但这仅仅是个开头而已.编 ...
- 扒一扒ELF文件
ELF文件(Executable Linkable Format)是一种文件存储格式.Linux下的目标文件和可执行文件都按照该格式进行存储,有必要做个总结. 目录 1. 链接举例 2. ELF文件类 ...
- GCC编译器原理(二)------编译原理一:ELF文件(2)
四. ELF 文件格式分析 ELF文件(目标文件)格式主要四种: 可重定向文件: 文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件.(目标文件或者静态库文 ...
- gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解
摘自http://blog.csdn.net/elfprincexu/article/details/45043971 gcc/g++等编译器 编译原理: 预处理,编译,汇编,链接各步骤详解 C和C+ ...
- GCC编译器原理(三)------编译原理三:编译过程---预处理
Gcc的编译流程分为了四个步骤: 预处理,生成预编译文件(.文件):gcc –E hello.c –o hello.i 编译,生成汇编代码(.s文件):gcc –S hello.i –o hello. ...
- GCC编译器原理(三)------编译原理三:编译过程(3)---编译之汇编以及静态链接【2】
4.1.2 符号解析与重定位 (1)重定位 在完成空间和地址的分配步骤之后,链接器就进入了符号解析和重定位的步骤,这是静态链接的核心部分. 先看看 a.o 的反汇编文件: objdump -d a.o ...
- 跟vczh看实例学编译原理——二:实现Tinymoe的词法分析
文章中引用的代码均来自https://github.com/vczh/tinymoe. 实现Tinymoe的第一步自然是一个词法分析器.词法分析其所作的事情很简单,就是把一份代码分割成若干个tok ...
随机推荐
- [WC2005]双面棋盘(并查集+分治)
题目描述 题解 唉,还是码力不行,写了一个多小时发现想错了又重构了一个多小时. 这道题意图很显然,动态维护联通块,有一个经典做法就是用LCT维护按照删除时间维护的最大生成树. 网上还有一种神奇的做法, ...
- [SCOI2016]萌萌哒(倍增+并查集)
一个长度为n的大数,用S1S2S3...Sn表示,其中Si表示数的第i位,S1是数的最高位,告诉你一些限制条件,每个条件表示为四个数,l1,r1,l2,r2,即两个长度相同的区间,表示子串Sl1Sl1 ...
- poj 3279 Fliptile(二进制搜索)
Farmer John knows that an intellectually satisfied cow is a happy cow who will give more milk. He ha ...
- springboot 后台运行
https://zhuanlan.zhihu.com/p/25102504?refer=dreawer 酱油一篇,整理一下关于Spring Boot后台运行的一些配置方式.在介绍后台运行配置之前,我们 ...
- QML学习笔记(二)-纯qml画图实现canvas画板-鼠标画图
作者: 狐狸家的鱼 Github: 八至 版权声明:如需转载请获取授权和联系作者 用纯qml实现canvas画板功能,用鼠标进行画图,可以画直线,画圆,画矩形,画弧线. 由于canvas画图会有延迟和 ...
- electron入门笔记(三)- 引入bootstrap
源码:https://github.com/sueRimn/electron-bootstrap 当引入jQuery和bootstrap文件时,会报错,原因是:electron 的 Renderer ...
- 洛谷P3620 数据备份
好吧,我一开始说这是个神级数据结构毒瘤题,后来改成神题了. 主要是贪心做法的巧妙转化: 首先发现选择的一对必须相邻,于是我们搞出差分. 然后考虑选取最小值时,最小值两侧的数要么同时选,要么都不选. 然 ...
- [bzoj3524][Couriers]
题目链接 思路 观察这个\((r - l + 1)/2\),很容易证明,如果一个数出现次数大于\((r - l + 1) / 2\),那么这个区间内第\((r - l + 1) / 2 + 1\)大一 ...
- Go数据类型和变量
一:Go数据类型 1.1 Go语言按照分类有以下几种数据类型 布尔型 布尔型的是一个常量true或者false 数字类型 整型int和浮点型 float32, float64 字符串类型 字符串就是一 ...
- Go GraphQL初学者教程
Go GraphQL初学者教程 https://tutorialedge.net/golang/go-graphql-beginners-tutorial/ https://tutorialedge. ...