目标文件里有什么
目标文件格式
目标文件就是源代码编译后但未进行链接的中间文件(linux下的.o)。
ELF文件:从广义上看,目标文件与可执行文件的格式其实几乎是一样的,可以将目标文件与可执行文件看成是一种类型的文件(ELF文件)。
表1 ELF文件类型
ELF文件类型 | 说明 | 实例 |
可重定位文件 | 这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态库也可以归类为这一类 | Linux的.o |
可执行文件 | 这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名 | 比如/bin/bash文件 |
共享目标文件 | 这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态连接器可以将及格这种共享目标文件与可执行文件结合,作为进程映像的一部分来运行 | Linux的.so,如/lib/glibc-2.5.so |
核心转储文件 | 当进程意外终止时,系统可以将该进程的地址空间的内容及终止时的一些其他信息转储到核心转储文件 | Linux下的core dump |
目标文件是什么样的
目标文件中的内容至少有编译后的机器指令代码,数据,符号表,调试信息,字符串等。一般目标文件将这些信息按不同的属性,以“节”的形式存储,在一般情况下,它们都表示一个一定长度的区域。
程序源代码编译后的机器指令经常被放在代码段,代码段常见的名字有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段,数据段的一般名字都叫“.data”。让我们来看一个简单的程序被编译成目标文件后的结构,如下图所示。
图1
从上图可以看到,ELF文件的开头是一个“文件头”,它描述了整个文件属性,包括是否可执行,是静态链接还是动态链接以及入口地址(如果是可执行文件),目标硬件,目标操作系统等信息,文件头还包括一个段表,段表其实是一个描述文件中各个段的数组。
从上图可以看到,一般C语言的编译后可执行语句都编译成机器代码,保存在.text段;已初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量一般放在一个叫做“.bss”的段里。
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。
为什么要把程序的指令和数据的存放分开?混杂地方在一个段里面不是更加简单?其实数据和指令分段的好处有很多。主要有如下几个方面
- 一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样就可以防止程序的指令被有意或者无意的改写。
- 另外一方面是对于现代的CPU来说,它们有着极为强大的缓存体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
- 第三个原因,其实也是最重要的原因,就是当系统中运行着多个程序的副本时,他们的指令都是一样的,所以内存中只需要保存一份该程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标,图片等资源属于可以共享。当然每个副本进程的数据区域是不一样的,它们是进程私有的。
挖掘a.o
首先来看下a.c的代码清单:
图2
我们使用GCC来编译这个文件(参数-c表示只编译不链接):
gcc -c a.c
我们得到一个目标文件a.o,使用objdump来查看a.o内部的结构,该工具可以用来查看目标文件的结构和内容。运行以下命令:
objdump -h a.o
图3
从上面的结果来看,a.o的段包括了代码段(.text),数据段(.data),BSS段(.bss),只读数据段(.rodata),注释信息段(.comment)和堆栈提示段(.note.GNU-stack)等,再来看看段的属性,其中最容易理解的是段的长度(Size)和段所在的位置(File Offset),每个段的第2行的“CONTENTS”,“ALLOC”等表示段的各种属性,“CONTENTS”表示段在文件中存在。我们可以看到BSS段没有“CONTENTS”,表示它实际上再ELF文件中不存在内容,我们用下图来表示“.text”,“.data”,“.rodata”和“.comment”段在ELF文件的长度和文件中的偏移位置。
图4
代码段
挖掘各个段的内容,我们还是用objdump这个工具。Objdump的“-s”参数可以将所有段的内容以十六进制的方式打印出来,“-d”参数可以将所有包含指令的段反汇编,下图是使用objdump输出a.o中有关代码段内容的提取:
$ objdump -s -d a.o
a.o: 文件格式 elf64-x86-64
Contents of section .text(代码段):
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000c9 ................
0020 c3554889 e54883ec 10c745f8 01000000 .UH..H....E.....
0030 8b150000 00008b05 00000000 01c28b45 ...............E
0040 f801c28b 45fc01d0 89c7e800 000000b8 ....E...........
0050 00000000 c9c3 ......
Contents of section .data(数据段):
0000 54000000 55000000 T...U...
Contents of section .rodata(只读数据段):
0000 25640a00 %d..
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520342e .GCC: (Ubuntu 4.
0010 382e342d 32756275 6e747531 7e31342e 8.4-2ubuntu1~14.
0020 30342e33 2920342e 382e3400 04.3) 4.8.4.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 21000000 00410e10 8602430d ....!....A....C.
0030 065c0c07 08000000 1c000000 3c000000 .\..........<...
0040 00000000 35000000 00410e10 8602430d ....5....A....C.
0050 06700c07 08000000 .p......
Disassembly of section .text:
0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: c9 leaveq
20: c3 retq
0000000000000021 <main>:
21: 55 push %rbp(对应Contents of section .text的第一个字节)
22: 48 89 e5 mov %rsp,%rbp
25: 48 83 ec 10 sub $0x10,%rsp
29: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
30: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 36 <main+0x15>
36: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 3c <main+0x1b>
3c: 01 c2 add %eax,%edx
3e: 8b 45 f8 mov -0x8(%rbp),%eax
41: 01 c2 add %eax,%edx
43: 8b 45 fc mov -0x4(%rbp),%eax
46: 01 d0 add %edx,%eax
48: 89 c7 mov %eax,%edi
4a: e8 00 00 00 00 callq 4f <main+0x2e>
4f: b8 00 00 00 00 mov $0x0,%eax
54: c9 leaveq
55: c3 retq (对应Contents of section .text的最后一个字节)
“Contents of section .text”就是.text段的数据以十六进制方式打印出来的内容,总共0x56字节(黄色部分),跟我们上图的“.text”段长度相符合,最左边一列是偏移量,中间4列是十六进制内容,最右面一列是.text的ASCII码形式。对照下面的反汇编结果,可以很明显的看到,.text段里所包含的正是a.c里两个函数func1()和main()的指令。.text段的第一个字节”0x55”就是函数“func1()”函数的第一条“push %rbp”指令,而最后一个字节0xc3正是main()函数的最后一条指令”ret”。
数据段和只读数据段
.data段保存的是那些已经初始化了的全局静态变量和局部静态变量。从上文源代码a.c里面一共有两个这样的变量,分别是global_int_varabal和static_var。这两个变量每个4个字节,一共刚好8个字节,所以“.data”这个段的大小为8个字节(上文紫色部分),0x54是84的十六进制,0x55是85的十六进制。
- c里面我们在调用“printf”的时候,用到了一个字符串常量“%d\n”,它是一种只读数据,所以它被放到了“.rodata”段,我们可以从输出结果看到“.rodata”这个段的4个字节刚好是这个字符串常量的ASCII字节序,最后以\0结尾(蓝色部分)。
BSS段
.bss段存放的是未初始化的全局变量和局部静态变量。
ELF文件结构描述
上文通过a.o的结构大致了解了ELF文件的轮廓 ,接着就来看看ELF文件的结构格式。下图描述了ELF目标文件的总体结构。
图5 ELF结构
ELF Header |
.text |
.data |
.bss |
Other sections |
Section Header table |
String Tables Symbol Tables |
从图5可以看出ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本,目标机器型号,程序入口地址等,紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如每个段的段名,段的长度,在文件中的偏移,读写权限以及段的其他属性。
文件头
我们可以用readelf命令来详细查看ELF文件,如下图6所示
图6 ELF文件头
从上面输出的结果可以看到,ELF的文件头定义了ELF魔数,文件机器字节长度,数据存储方式,版本,运行平台,ABI版本,ELF重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度以及段的数量等。
ELF文件头结构以及相关常数被定义在”/usr/include/elf.h”中,因为ELF文件在各种版本平台下都通用,ELF文件有32位版本和64位版本,分别叫做”Elf32_Endr”和”Elf64_Ehdr”,如图7所示。
图7 ELF文件头结构体成员
段表
我们知道ELF文件中有很多各种各样的段,通过段表来保存这些段的基本属性。段表是ELF文件中除了文件头以外最重要的结构,它描述了EFL的各个段的信息,比如每个段的段名,段的长度(棕色圈矩形框),在文件中的偏移(红色圈矩形框),读写权限以及段的其他属性。也就是说,ELF文件的段结构就是有段表决定的,编译器,连接器和装载器都是依靠段表来定位和访问各个段的属性的。段表在ELF文件中的位置有EFL文件头的”e_shoff”成员决定,比如在a.o中,段表位于偏移0x190。
图8 表段信息
Readelf输出的结构就是ELF文件段表的内容,那么就让我们对着这个输出来看看段表的结构。段表的结构比较简单,它是一个以”Elf64_Shdr”(在/usr/include/elf.h,占64个字节)结构为元素的数组。数组元素的个数等于段的个数,每个”Elf64_Shdr”结构体对应一个段。”Elf64_Shdr”又称为段描述符(Section Descriptor)。对于a.o来说,段表就是有13个元素的数组。ELF段表的这个数组的第一个元素是无效的段描述符,它的类型是”NULL”,除此之外每个段描述符都对应一个段。在a.o共有12个有效的段。
到了这一步,我们才把a.o的所有段的位置和长度给分析清楚了。在下图9中,SectionTable长度为0x340,也就是832个字节,它包含了13个段描述符,每个段描述符为64个字节,这个长度刚好等于sizeof(Elf64_Shdr),符合段描述符的结构体长度,整个文件最后一个段”.real_ch_frame”结束后,长度为0x58,即1880字节,刚好等于a.o的文件长度,如图10所示。
图9 a.0的section table以及所有段的位置和长度
图10 a.o的总长度
重定位表
我们注意到,a.o中有一个叫做“.rel.text”的段,也就是说它是一个重定位表。正如我们最开始所说的,连接器在处理目标文件时,需要对目标文件某些部分进行重定位,即代码段和数据段中哪些对绝对地址的引用的位置。这些重定位的信息都记录在ELF文件的重定位表里面,对于每个需要重定位的代码段和数据段,都会有一个相应的重定位表。比如在a.o中,“.rel.text”就是针对“.text”段的重定位表,因为“.text”段中至少有一个绝对地址的引用,那就是对“printf”的调用;而“.data”段则没有绝对地址的引用,它只包含了几个常量,所以a.o中没有针对“.data”段的重定位表“.rel.data”。
链接的接口---符号
链接过程的本质就是要把多个不同的目标文件之间相互“粘”到一起。为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件B要用到目标文件A的函数“foo”,那么我们就称目标文件A定义了函数“foo”,称目标文件B引用了目标文件A的函数“foo”。这两个概念同意也适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间混淆。在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。除了函数和变量之外,还存在其他几种不常用的符号:
- 定义在目标文件的全局符号,可以被其他目标文件引用。
- 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号。
- 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
- 局部符号,这类符号往往只在编译单元内部可见。
- 行号信息,即目标文件指令与源代码中代码行的对应关系。
ELF符号表结构
ELF文件中的符号表往往是文件中的一个段,段名一般叫“.symtab”。符号表的结构很简单,它是一个Elf32_Sym(32位系统)/Elf64_Sym(64位系统)的数组,每个Elf64_Sym结构对应一个符号。
图11 符号表结构信息
st_name:符号名。这个成员包含了该符号名在字符串表的下标
st_value:符号相对应的值。这个值跟符号有关,可能是一个绝对值,也可能是一个地址等,不同的符号,它所对应的值的含义不同
st_size:符号大小。对于包含数据的符号,这个值是该数据类型的大小
st_info:符号类型和绑定信息
st_other:该成员目前位0
st_shndx:符号所在的段
特殊符号
当我们使用ld作为链接器来链接生产可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但是你可以直接声明并且引用它,我们称之为特殊符号。其实这些符号是被定义在ld链接器的链接脚本中的。目前你只需认为这些符号是特殊的,你无须定义它们,但可以声明它们并且使用。链接器会在将程序最终链接成可执行文件的时候将其解析成正确的值,注意,只有使用ld链接生产最终可执行文件的时候这些符号才会存在。几个很有代表性的特色符号如下:
- __executable_start:该符号为程序起始地址,注意,不是入口地址,是程序的最开始的地址。
- __exext或_exext或etext:该符号为代码段结束地址,即代码段最末尾的地址
- _edata或edata:该符号为数据段结束地址,即数据段最末尾的地址
- _end或end:该符号为程序结束地址
- 以上地址都为程序被装载时的虚拟地址