导语
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。了解它的结构并深入剖析它对于认识系统、了解背后的机理大有好处。
3.1 目标文件的格式
现有PC平台的流行可执行文件格式主要是:
- Windows下的 PE(Portable Executable)
- Linux 的 ELF(Executable Linkable Format)
它们都是 COFF(Common File Format) 格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。
其他不太常见的可执行文件格式还有 Intel/Microsoft的OMF(Object Module Format)、 Unix的a.out 和 MS-DOS .COM格式等。
不止是可执行文件(.exe或Linux的ELF可执行文件),动态链接库(DLL, Dynamic Linking Library)(.dll/.so)及静态链接库(Static Linking Library)(.lib/.a)都是按照可执行文件格式存储。
ELF文件标准里面把系统中采用的ELF文件格式归为4类:
- 可重定位文件(Relocatable File)
- 包含代码和数据,可以被用来链接成可执行文件和共享目标文件
- 静态链接库归为此类
- 实例:
Linux的.o
Windows的.obj
- 可执行文件(Executable File)
- 在权限允许的情况下,可以直接执行
- 一般都没有扩展名
- 实例:
比如/bin/bash文件
Windows的.exe
- 共享目标文件(Shared Object File)
- 包含代码和数据,可以在如下两种情况下使用:
- 链接器使用它跟其他的
可重定位文件和共享目标文件链接
,产生新的目标文件- 动态链接器可以将几个这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行
- 实例:
比如.so文件
Windows的.dll
- 核心转储文件(Core Dump File)
- 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件
- 实例:
linux下的core dump
小提示
COFF的主要贡献是在目标文件里面引入了 “段(Segment)” 的机制,不同的目标文件可以拥有不同数量及不同类型的“段”。另外,它还定义了
调试数据
格式。
3.2 目标文件是什么样的
目标文件中的内容包含有:
- 编译后的机器指令代码、数据
- 链接时所需要的一些信息,比如 符号表、调试信息、字符串等。
一般目标文件将这些信息按不同的属性,以 “节(Section)” 的形式存储,有时候也叫 “段(Segment)”。这些段都表示一个一定长度的区域,基本上不加以区分。
- 代码段(Code Section)
- 存储程序源代码编译后的机器指令
- 常见的名字由 “.code” 或 “.text”。
- 数据段(Data Section)
- 全局变量和局部静态变量数据
- 常见的名字由 “.data”。
一般 C 语言的编译后执行语句都编译成机器指令代码,保存于 .text 段;已初始化的全局变量和局部静态变量都保存在 .data 段;未初始化的全局变量和局部静态变量都放在 .bss 段里。
小提示
- .bss段只是为 未初始化的全局变量和局部静态变量预留位置而已,他并没有内容。
- BSS(Block Started by Symbol) 本意是汇编器中的一条伪指令,用于为符号预留一块内存空间。
- 未初始化的全局变量和局部静态变量默认值都为0,但是因为都是0,所以给它们在.data段分配空间并且存储数据0是没必要的。程序运行的时候它们的确还要占用内存空间,所以可执行文件使用.bss段来记录所有未初始化的全局变量和局部静态变量的大小的总和。
总体来说, 程序源代码被编译后主要分成两种段:程序指令和程序数据。代码段属于程序指令;而数据段和.bss段属于程序数据。数据和指令分开放的好处有以下几个方面:
- 当程序被装载后,数据和指令分别被映射到两个虚存区域,不同区域可以分别控制读写权限。
- 两者分离有利于提高程序的局部性,从而提高CPU的缓存的命中率。
- 当系统运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只需要保存一份该程序的指令。 这个也同样适用于 程序中的其他只读的区域段。
3.3 挖掘SimpleSection.o
除了 .text、.data、.bss 这三个常用的段之外,ELF文件也有可能包含其他的段以用于保存于程序相关的其他信息。
- .rodata1 - read only data,存放的是只读数据,比如字符串常量、全局const变量。跟 .rodata 一样。
- .comment - 存放的是编译器版本信息,比如字符串: “GCC:(GNU) 4.2.0”
- .debug - 调试信息
- .dynamic - 动态链接信息
- .hash - 符号哈希表
- .line - 调试时的行号表,即源代码行号与编译后指令的对应表
- .note - 额外的编译器信息。比如程序的公司名、发布版本等
- .strtab - String Table,字符串表,用于存储ELF文件中用到的各种字符串。
- .symtab - Symbol Table,符号表。
- .shstrtab - Section String Table,段名表
- .plt/.got - 动态链接的跳转和全局入口表
- .init/.fini - 程序的初始化和终结代码段。见“C++全局构造和析构”
GCC扩展提高了自定义段的手段:
我们在全局变量或函数定义语句之前加上
__attribute__((section("NAME")))
属性就可以把相应的变量或函数放到以“NAME”作为段名字的段中。
3.4 ELF文件结构描述
ELF目标文件格式最前部是 ELF文件头(ELF Header),他包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。
3.4.1 文件头
魔数: 0x7F454c46 - DEL E L F
- ELF文件头结构
Elf32_Ehdr(见文件/usr/include/elf.h)
3.4.2 段表(Section Header Table)
- 段表描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移位置、读写权限及段的其他属性。
- 也就是说,ELF文件的的段结构就是由段表决定的,编译器、链接器和装载器都是依赖段表来定位和访问各个段的属性的。
- 段表在ELF文件中的位置由ELF文件头的 “e_shoff” 成员决定。
- 段表中的每一项的结构由
Elf32_Shdr(见文件/usr/include/elf.h)
结构体构成。
3.4.3 重定位表(Relocation Table)
- 链接器在处理目标文件时,须要对目标文件中某些部位进行重定位,即代码段和数据段中那些绝对地址的引用的位置。这些 重定位的信息都记录在ELF文件中的重定位表里面。
- 对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。
3.4.3 字符串表(String Table)
- 把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。
- 通过这种方法,ELF文件引用字符串只需要给出一个数字下标即可,不用考虑字符串长度的问题。
- 字符串表在ELF文件中也是以段的形式保持,常见的段名:
- “.strtab”,字符串表,用于保存普通的字符串
- “.shstrtab”,段表字符串表,用于保存段表中用到的字符串,常见的是段名(sh_name)。
3.5 链接的接口-符号
链接过程的本质就是要把多个不同的目标文件之间以固定的规则相互拼装形成一个整体。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。目标文件B要用到目标文件A中的函数“foo”,那么我们成目标文件A 定义(Define) 了函数“foo”,称目标文件B 引用(Reference) 了目标文件A中的函数“foo”。在链接中,我们将函数和变量统称为 符号(Symbol),函数名和变量名就是 符号名(Symbol Name)。
符号 可以被看作是链接中的粘合剂,正是基于符号才能够正确完成整个链接过程。
- 每一个目标文件都会有一个对应的 符号表(Symbol Table),表里面记录了目标文件中所用到的所有符号。
- 每个定义的符号有一个对应的值,叫做 符号值(Symbol Value),对于变量和函数来说,符号值就是它们的地址。
- 符号表中所有的符号其类型分类如下:
- 定义在本目标文件中的全局符号,可以被其他目标文件引用。比如 定义的函数名和全局变量。
- 外部符号(External Symbol),未定义在本目标文件却在本目标文件中引用的全局符号。
- 段名,往往由编译器产生,它的值就是该段的起始地址。
- 局部符号,只可见于编译单元内部。
- 行号信息。
- 符号表可以通过
nm
命令查看。
3.5.1 ELF符号表结构
ELF文件在符号表往往是文件中的一个段,段名一般叫“.symtab”。结构体的名称为 Elf32_Sym
3.5.2 特殊符号
这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它。只有使用 ld 链接生成最终可执行文件的时候这些符号才会存在,下面是几个具有代表性的特殊符号:
__executable_start
,程序的起始地址,不是入口地址
,是程序的最开始的地址。__etext/_etext/etext
,代码段的结束地址,即代码段最末尾的地址_edata/edata
,数据段的介绍地址_end/end
,程序的结束地址- 以上地址都为程序被装载时的虚拟地址。
3.5.3 符号修饰与函数签名
为了防止符号名冲突,UNIX下的C语言就规定,C语言源代码文件中的所有全局变量和函数经过编译后,相对应的符号名前加上下划线“_”。C++ 则是通过增加 名称空间(Namespace) 来解决多模块的符号冲突问题。
C++符号修饰(Name Decoration/Name Mangling)
函数签名(Function Signature),它包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息。
在编译器和链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称(Decorated Name)。C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。所以说, 即使函数名相同,由于其编译后的函数签名不一样,它们也不是一样的函数。
小提示
c++filt
工具可以用来解析被修饰过的名称。
签名和名称修饰机制也被用于C++中全局变量和静态变量,以防止名字冲突。
3.5.4 extern “C”
- C++编译器会将在 extern “C” 的大括号内部的代码当做 C 语言代码处理。此时 C++的名称修饰机制不会起作用。
- 配合C++的宏
__cplusplus
,C++编译器会在编译C++的程序时默认定义这个宏。
3.5.5 若符号与强符号
对于C/C++语言来说,编译器默认函数和已初始化的全局变量为 强符号(Strong Symbol),未初始化的全局变量为 弱符号(Weak Symbol)。
- 注意,强符号和弱符号都是针对定义来说的,不是针对符号的引用。
- 可以通过GCC的“
__attribute__((weak))
” 来定义一个强符号为弱符号。
链接器按照如下的规则处理与选择被多次定义的全局符号:
- Rule1 不允许强符号被多次定义,即不同的目标文件中不能由同名的强符号;若有重复的多个强符号定义存在,则链接器报 符号重复定义 错误。
- Rule2 如果一个符号在某个目标文件中是强符号,在其他目标文件中都是弱符号,则选择强符号。
- Rule3 如果一个符号在所有目标文件中都是弱符号,则选择其中占用空间最大的一个。
弱引用和强引用
- 强引用(Strong Reference) 对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们必须被正确决议。若是没有找到该符号的定义,链接器就会报符号未定义错误。
- 弱引用(Weak Reference) 如该符号有定义,咋额将该符号的引用决议;若是该符号未定义,则链接器对于该引用不报错。
- 一般对于未定义的弱引用,链接器默认其为0,或者一个特殊的值,以便程序代码能够识别。
- 可以通过GCC的“
__attribute__((weakref))
” 来定义一个声明对一个外部函数的引用为弱引用。
弱引用和弱符号主要用于库的链接过程
- 比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数
- 程序可以对某些扩展功能模块的定义为弱引用,当扩展模块存在,则可以使用模块的全部功能;若扩展模块被去掉,程序也能正常链接,只是缺少了相应的功能。以便于功能的裁剪和组合。
3.6 调试信息
现代的编译器都支持源代码级别的调试,但是前提是 编译器必须提前讲源代码和目标代码之间的关系保存到目标文件里面。ELF文件采用 DWARF(Debug With Arbitrary Record Format) 的标准的调试信息格式;Windows也有自己相应的调试信息格式标准,叫 CodeView。