编译链接 | 3 目标文件里有什么 | 3.1+3.2 目标文件格式和内容
说明:重读《程序员的自我修养–链接、装载与库》
3.1 目标文件的格式
可执行文件格式:
COFF(Common file format)
PE (Portable Executable)(win)
ELF (Executable Linkable Format)(linux)
目标文件:
.obj(win)
.o(linux)
可执行文件格式及对应的文件:
由于目标文件与可执行文件的内容和结构几乎一样,故一般采用跟可执行文件相同的格式存储,
所以广义上我们可以将它们看成是一种文件格式类型–可执行文件格式类型。不止如此,动态和静态
链接库也是采用可执行文件格式存储。
具体到不同平台的可执行文件格式及类型:
PE-COFF(win):
可执行文件(.exe,Executable),目标文件(.obj,Relocatable),动态链接库(.dll,Shared Object),静态链接库(.lib)
ELF(linux):
可执行文件(无扩展名,Executable),目标文件(.o,Relocatable),动态链接库(.so,Shared Object),静态链接库(.a),核心转储文件(,Core Dump File)
注:linux下使用file
命令查看文件格式和类型。
总结和思考:
Windows和Linux操作系统中,对于目标文件(动态/静态链接库也算)和可执行文件的存储都遵从同样的格式规范–可执行文件格式。不同操作系统的格式规范大同小异,因为历史上都来自于COFF格式规范,该规范的主要贡献是为目标文件引入了“段”的机制。
目标文件和可执行文件是程序执行的实体(目标文件经过链接就是可执行文件了),知道了他们的存储格式、各部分存储的都是什么内容,对于我们理解程序运行时如何被操作和执行非常有帮助,进一步可以指导我们在编写程序时提前避免一些坑甚至帮助我们优化代码。还有当程序执行遇到问题时,可以提供一些分析方向和思路,比如linux中常见的panic和dump。
3.2 目标文件是什么样的
目标文件中的内容信息以 “段(section/segment)” 的形式存储,表示一个一定长度的区域。
注:section与segment一般不做区分,只在链接视图和装载视图中有区别。
标准的COFF 文件存储格式:
上面的有点复杂,我画了个简化版,并做了说明:
总体来说,程序源码被编译后生成的文件主要有三部分:文件头、代码段和数据段。其中数据段包含.data和.bss两种。我们通常只关心两部分 ---- 代码段和数据段。
这里有2个疑问:
疑问1:.data段和.bss段都是用来存放数据的,为何不合并成一个.data 段,而偏偏要分开增加麻烦呢?
设立.bss段的目的是为了节省目标文件的存储空间。已经初始化的全局变量和局部静态变量具体数值各不相同,需要为他们分配存储空间;而对于未初始化的全局变量和局部静态变量默认都是0,在存储时我们没必要为全0的变量特意留出存储空间,只需要象征性的为其预留位置,待到程序实际执行时再为其分配运行时内存空间即可,这样可以减少文件对存储空间的消耗。所以,.bss段只是为了未初始化的全局变量和局部静态变量预留位置而已,实际并没有内容,在文件中不占空间。
疑问2:为何要把目标文件的程序代码指令和程序数据分开存放?
- 程序安全
一方面,程序装载后,代码段和数据段分别被映射到不同的虚拟存储空间。
另一方面,程序运行时程序指令只需要读取和运行,不需要修改;而数据往往会涉及到读和写。将程序指令和程序数据分开存储,就可以专门设置代码段为只读属性,为数据段设置读写属性,这样一来可以防止程序指令被有意或无意修改,提高程序安全性。 - 提高CPU的缓存命中率
指令区和数据区的分离有利于提高程序的局部性,这对于提高CPU(特别是现代CPU)的缓存命中率是有好处的。 - 节省内存(
最重要
)
当一个程序被多个进程执行时,每个程序都会有一份该程序的副本。由于指令区只读,所以指令区副本在整个内存中只需要一份,而每个进程对数据的操作不一样,所以每个进程都有一个私有的数据区副本。只读区用于进程共享不光只在程序的指令区,对于一些图片、文本等只读资源涉及到多进程共享时也是同样的存储策略。这样可以极大的节省运行时内存消耗。
思考
感觉计算机的历史很大一部分就是安全、内存利用和性能优化的历史啊。