目标文件
编译器编译源代码后生成的文件叫做目标文件
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程, 其中可能有些符号或有些地址还没有被调整。
目标文件的格式
现在PC平台流行的可执行文件格式主要是Windows下的PE和Linux的ELF, 它们都COFF格式的变种。
不光是可执行文件(Windows的.exe和Linux下的ELF可执行文件)按照可执行文件格式存储。动态链接库(Windows 的.dll 和 Linux的 .so) 及静态链接库(Windows 的.lib 和Linux 的.a)文件都按照可执行文件格式存储。
ELF文件类型 | 说明 | 实例 |
---|---|---|
可重定位文件(Relocatable File) | 这类文件包含了代码和数据,可以被用来 链接成可执行文件或共享目标文件,静态 链接库也可以归为这一类 | Linux的.o Windows的.obj |
可执行文件(Executable File) | 这类文件包含了可以直接执行的程序,它 的代表就是ELF可执行文件,它们一般都 没有扩展名 | 比如/bin/bash文件 Windows的.exe |
共享目标文件(Shared Object File) | 这种文件包含了代码和数据,可以在以下 两种情况下使用。 一种是链接器可以使用 这种文件跟其他的可重定位文件和共享目 标文件链接,产生新的目标文件。第二种 是动态链接器可以将几个这种共享目标文 件与可执行文件结合,作为进程映像的一 部分来运行 | Linux的.so ,Windows的DLL |
核心转储文件(Core Dump File) | 当进程意外终止时,系统可以将该进程的 地址空间的内容及终止时的一些其他信息 转储到核心转储文件 | Linux下的core dump |
我们可以在Linux 下使用 file命令来查看相应的文件格式
目标文件是什么样的
我们大概能猜到,目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了 这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字 符串等。 一般目标文件将这些信息按不同的属性,以节的形式存储,有时候 也叫段
程序源代码编译后的机器指令经常被放在代码段,代码段常见的名 字有.code或.text;全局变量和局部静态变量数据经常放在数据段,数据段的一般名字都叫.data
ELF文件的开头是一个“文件头”,它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址等信息,文件头还 包括一个段表, 段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等
一般C 语言的编译后执行语句都编译成机器代码,保存在.text段;已 初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量 一般放在一个叫.“bss”的段里。
.bss 段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于 程序指令,而数据段和.bss 段属于程序数据。
为什么把程序的指令和数据的存放分开?
- 数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,这样可以防止程序的指令被有意或无意 地改写。
- 现代CPU有着极为强大的缓存体系,这样可以提高缓存的命中率
- 最重要的原因,就是当系统中运行着多个该程序的副本时,它 们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。
挖掘SimpleSection.o
SimpleSection.c
int printf(const char* format, ...);
int global_initvar = 84;
int global_uninitvar_var;
void func1(int i)
{
printf("%d" , i);
}
int main(void)
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
使用gcc编译
gcc -c SimpleSection.c
我们可以使用binutils的工具objdump来查看object内部的结构,参数“-h”就是把ELF 文件的各个段的基本信息打印出来
objdump -h SimpleSection.o
除了最基本的代码段、数据段和BSS 段以外,还有3个段分别是只读数据段(.rodata)、注释信息段(.comment)和堆栈提示段(.note.GNU-stack), 这3个额外的段的意义我们暂且不去细究。先来看看几个重要的 段的属性,其中最容易理解的是段的长度(Size)和段所在的位置 (File Offset), 每个段的第2行中的 “CONTENTS”,“ALLOC” 等表示段的各种属性“CONTENTS” 表示该段在 文件中存在。我们可以看到 BSS 段没有 “CONTENTS”, 表示它实际上在 ELF 文件中不存 在内容。“note.GNU-stack”段虽然有 “CONTENTS”, 但它的长度为0,这是个很古怪的段, 我们暂且忽略它,认为它在ELF 文件中也不存在。那么ELF 文件中实际存在的也就是“.text”、 “.data”、“.rodata”和“.comment” 这4个段
它们在ELF中的结构
代码段
objdump的“-s”参数可以将 所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编。
objdump -s -d SimpleSection.o
最左面一列是偏移量,中间4列是十六 进制内容,最右面一列是.text段的ASCII码形式,.text 段 的第一个字节“0x55” 就是 “func1()”函数的第一条“push %ebp”指令而最后一个字节 0xc3 正是main()函数的最后条指令“ret”。
数据段和只读数据段
- .data段保存的是那些已经初始化了的全局静态变量和局部静态变量。
- SimpleSection.c里面我们在调用“printf”的时候,用到了一个字符串常量“%d\n”, 它是一种只读数据,所以它被放到了“.rodata”段
- “.rodata”段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。
- 有时候编译器会把字符串常量放到“ .data”段,而不会单独放在 “.rodata”段
BSS段
.bss段存放的是未初始化的全局变量和局部静态变量,如上述代码中 global uninit var和static var2就是被存放在.bss段,其实更准确的说法是.bss段为它们预留了空间。
有些编译器会将全 局的未初始化变量存放在目标文件.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss 段分配空间
值得一提的是编译单元内部可见的静态变量(比如给global uninit var加上 static修饰)的确是存放在.bss段的
以下代码
static int x1 = 0;
static int x2 = 1;
x1和x2会被放在什么段中呢?
x1 会被放在.bss中 ,x2会被放在.data中。为什么一个在.bss段,一个在.data段 ? 因为 x1为0,可以认为是未初始化的,因为未初始化的都是0,所以被优化掉了可以放在.bss,这样可以节省磁盘空间,因为.bss不占磁盘空间。
其他段
GCC 提供了一个扩展机制,使得程序员可以指定变量所处的段
ELF文件结构描述
ELF 目标文件格式的最前部是 ELF 文件头 (ELF Header), 它包含了描述整个文件的 基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table), 该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。
文件头
我们可以用readelf命令来详细查看ELF文件
readelf -h simpleSection.o
ELF的文件头中定义了ELF魔数文件机器字节长度、数据存储方式、版本、运行平台、ABI 版本、ELF重定位类型、硬件平台、硬件平台版本、 入口地址、程序头入口和长度、段表的位置和长度及段的数量等
ELF 文件头结构及相关常数被定义在“/usr/include/elf.h”里,ELF文件有32位版本和64位版本,它的文件头结构也有这两种版本,分别叫 做“EIf32 Ehdr”和“Elf64 Ehdr”。
- ELF魔术 Magic
这16个字节被 ELF 标准规定用来标识ELF文件的平台属性,比如这个ELF 字长(32位/64位)、字节序、ELF文件版本
段表
我们知道ELF 文件中有很多各种各样的段,这个段表 (Section Header Table)就是保存这些段的基本属性的结构,它描述了ELF的各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其他属性。段表在ELF 文件中的位置由ELF 文件头的“e shoff” 成员决定,比如 SimpleSection.o中,段表位于偏移0x118
前文“objdump-h”命令只是把 ELF 文件中关键 的段显示了出来,而省略了其他的辅助性的段,我们可以使用 readelf工具来查看ELF文件的段,它显示出来的结果才是真正 的段表结构:
readelf -S SimpleSection.o
段表的结构比较简单,它是一个以 “EIf32 Shdr”结构体为元素的数组。数组元素 的个数等于段的个数,每个 “EIf32 Shdr”结构体对应一个段。“EIf32 Shdr”又被称为段描述符,ELF段表的这个数组的第一个元素是无效的段描述符,它的类型为 “NULL”
段的位置和长度:
- 段的类型
- 段的标志位
段的标志位表示该段在进程虚拟地址空间中的属性,比如是否可写 - 段的链接信息
如果段的类型是与链接相关的(不论是动态链接或静态链接),比如重定位表、符号表等,那么sh Jlink和sh info 这两个成员所包含的意义如表图所示。对于其他类型的段,这两个成员没有意义
重定位表
我们注意到,SimpleSection.o中有一个叫做“.rel.text”的段,它的类型为 “SHT REL”,也就是说它是一个重定位表,对于每个须要重定位的代码段或数据段,都会有一个相应的重定位表。比如 SimpleSection.o中的“.rel.text”就是针对“.text”段的重定位表
字符串表
ELF文件中用到了很多字符串,比如段名、变量名等。因为字符串的长度往往是不定的, 所以用固定的结构来表示它比较困难。一种很常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串
在ELF文件中引用字符串只须给出一个数字下标即可,不用考虑字符 串长度的问题。 一般字符串表在ELF 文件中也以段的形式保存,常见的段名为“.strtab”或 “ .shstrtab” 。 这两个字符串表分别为字符串表和段表字符串表
链接的接口—符号
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用
比如目标文件B要用到了目标文件A中的函数 “foo”, 那么我们就称目标文件A 定义了函数 “foo”,称目标文件B引用了目标文件A 中的函数“foo”
在链接中,我们将函数和变量统称为符号, 函数名或变量名就是符号名。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成
每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。
我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:
- 定义在本目标文件的全局符号,可以被其他目标文件引用(“func1”、“main”和 “global init var”)
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(“printf”)
- 段名(“.text"、".data”)
- 局部符号(“static var”和"static var2")
- 行号信息
使用 “nm” 来查看 “SimpleSection.o”的符号结果
符号表结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个EIf32 Sym结构(32位ELF文件)的数组,结构如下
- 符号类型和绑定信息
- 符号所在段
如果符号定义在本目标文件中,那么这个成员表示符号所在的 段在段表中的下标;但是如果符号不是定义在本目标文件中,或者对于有些特殊符号,sh_shndx 的值有些特殊
这里使用 readelf 工具来查看 ELF 文 件的符号
readelf -s SimpleSection.o
第一列 Num 表示符号表数组的下标,从0开始,共15个符号;第二列Value 就是符号值,即st_value; 第 三列Size为符号大小,即st size;第四列和第五列分别为符号类型和绑定信息,即对应st_info 的低4位和高28位;第六列 Vis 目前在C/C++ 语言中未使用,我们可以暂时忽略它;第七 列 Ndx 即st_shndx, 表示该符号所属的段;当然最后一列也最明显,即符号名称。
特殊符号
- __executable_start, 该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址
- __etext 或 _etext 或etext, 该符号为代码段结束地址
- _edata 或edata, 该符号为数据段结束地址
- _end 或 end, 该符号为程序结束地址
- 以上地址都为程序被装载时的虚拟地址
符号修饰与函数签名
众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂,为了支持C++这些复杂的特性,人们发明了符号修饰或符号改编的机制
int func(int);
float func(float);
class C{
int func(int);
class C2{
int func(int);
};
};
namespace N{
int func(int);
class C{
int func(int);
};
}
我们引入一个术语叫做函数签名, 函数签名包含了一个函数的信 息,包括函数名、它的参数类型、它所在的类和名称空间及其他信息,函数签名用于识别不 同的函数
它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称C++编译器和链接器都使用符号来识别和处理函数和变量
弱符号与强符号
符号的定义可以被称为强符号。有些符号的定义可以被称为弱符号对于C/C++ 语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过 GCC 的“ _attribute_((weak))”来定 义任何一个强符号为弱符号。
注意,强符号和弱符号都是针对定义来说的不是针对符号的引用
链接器会按如下规则处理与选择被多次定义的全局符号:
- 不允许强符号被多次定义
- 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选 择强符号。
- 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一 个。
弱引用和强引用
如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用,与之相对应还有一种弱引用, 在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果 该符号未被定义,则链接器对于该引用不报错。一般对于未定义的弱引用,链接器 默认其为0,或者是一个特殊的值,以便于程序代码能够识别。弱引用和弱符号主要用于库的链接过程
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强 符号所覆盖,从而使得程序可以使用自定义版本的库函数
调试信息
目标文件里面还有可能保存的是调试信息
如果我们在GCC 编译时加上“-g”参数,编译器就会在产生的目标文件里面加上调试信息现在的ELF文件采用一个叫DWARF(Debug With Arbitrary Record Format) 的标准的调试信息格式在Linux下,我们可以使 用“strip”命令来去掉ELF文件中的调试信息
strip foo
参考资料<<程序员的自我修养>>