第3章
目标文件的格式
目前PC平台流行的可执行文件格式主要是Win下的PE和Linux下的ELF格式,这两个均为COFF格式的变种。目标文件就是源代码编译后未进行链接的中间文件(Win下是.obj,Linux下是.o),其实内容结构和可执行文件很相似。除此之外,不太常见的可执行文件格式有OMF、Unix a.out格式和MS-DOS .COM格式等等。
不光是可执行文件按照可执行文件格式存储,动态链接库(Win下.dll,Linux下的.so)和静态链接库(Win下的.lib和Linux下的.a)亦是如此。其中静态链接库有些不同,它是把多个目标文件捆绑起来形成的一个文件,再加上一些索引查找,可以看作一个包含有很多目标文件的文件包。
ELF文件分为以下几类:可重定位文件(Relocatable File)、可执行文件(Executable File)、共享目标文件(Shared Object File)、核心转储文件(Core Dump File)。
目标文件是什么样子的
目标文件中的内容除了汇编后的机器指令代码、数据,应该还有链接需要的一些信息,比如符号表、调试信息、字符串等等。这些信息在文件中咦节的方式存储,有的时候也会称为段(记得好像当时在B站看小甲鱼c的时候也是这么定义的)。
顾名思义,代码段一般存放的是源代码编译后的机器指令,代码段常见的名字有".code"或".text",数据段一般存放了全局变量和局部的静态变量,数据段的名字一般叫".data"。
可以看到FLF的文件的开头是应该"文件头",和做misc题目时用010打开图片文件看到的开头类似,其中主要包括了文件的属性、文件是否可执行、静态链接还是动态链接及入口地址、目标硬件等信息。当然文件头还包括了一个段表描述的是各个段在文件中的偏移地址及段的属性
可以看到除了数据段(.data)和代码段(.text),还有一个叫".bss"的段,一般存放未初始化的全局变量和局部静态变量,一般数据都是在数据段存放的,这些未初始化的数据默认值都为0,放在数据段是没有必要的。而在程序运行的时候确实要占用空间,并且可执行文件必须记录所有未初始化的变量的大小总和,记作".bss"段。故bss段只是给未初始化的变量预留了位置,并没有内容,在文件中不占据空间。
对目标文件挖掘一下
稍微修改一下上一章的程序
使用gcc编译目标文件并用objdump查看一下
gcc -c hello.c
objdump -h hello.o
其中有几个之前没见过的为只读数据段(.rodata)、注释信息段(.comment),堆栈提示段(.note.GNU-stack),这几个现在也不清楚啥用法,但是看名字也猜出个大概了。第一行可以看到段的长度(size)和段所在的位置(File off),每个段的偏移地址知道了,我们就知道了它们在ELF中的结构分布。
附:
可以使用size
命令,用来查看ELF文件的代码段、数据段和BSS段的长度
size hello.o
代码段
使用objdump的"-s"参数可以将所有段的内容以16进制的方式打印出来,"-d"参数可以将所有包含指令的段反汇编。
objdump -s hello.o
.text是代码段,对照着下面的反汇编代码可以看到与上面的机器码是一一对应的。.text中最后的c3对应的正好是下面反汇编代码最后一个ret
数据段和只读数据段
.data段保存的是那些已经初始化的全局静态变量和局部静态变量。
.rodata段存放的是只读数据,一般是程序里面的只读变量(如const修饰的变量)和字符串常量。只读数据不允许修改,在一定程度上保证了程序的安全性。
有的时候编译器会把字符串常量放到".data"段,而不是单独的放在".rodata"段。
利用objdump查看存放的情况
objdump -x -s -d hello.o
可以看到,数据段中的前4个字节是0x55,0x00,0x00,0x00对应的正好是static_var的值85,这之中涉及到一个端序的问题,就是存放的顺序,在计算机组成原理的课上老师已经讲过了。
示例代码
.bss段存放的是未初始化的全局变量和局部静态变量
objdump -x -s -d SimpleSection.o
可以看到bss段上的大小只有4个字节,本来应该有8个字节的哦(因为未初始化的全局变量和局部静态变量有global_uninit_var和static_var2),因为global_uninit_var没有存放在任何段上,而是应该未定义的"COMMON 符号"。在上面的图片可以看到.bss段为他们预留了空间。
其他段
除了.data段、.text段、.bss段这3个常用的段之外,ELF文件还可能包含其他的段,用来保存于程序相关的其他信息。
ELF文件结构
通过这个ELF文件结构的剖析图,我们可以看到最前部的是文件头(包含了文件的属性比如文件版本、目标机器型号、程序入口地址等),接下来是.text(代码)、.data(数据)、.bss(预留)、其他段,接下来就是段表(与段联系密切),段表描述了文件中所有段的信息(比如每个段的段名、段长、在文件中的偏移、读写的权限及其他属性)
文件头
用readelf命令查看ELF文件
readelf -h SimpleSection.o
可以看到,文件头定义了ELF魔数、文件机器字节长度、数据存储方式、ABI版本、ELF重定位类型、硬件平台、程序入口和长度等等。
文件头包含的东西很多,最让我感兴趣的就是ELF魔数。在最前面的"Magic"中可以看到16个字节,这16字节被规定用来标识ELF文件的平台属性。
最开始的4个字节是所有ELF文件都必须相同的标识码:0x7f、0x45、0x4c、0x46,第一个对应ASCII里面的DEL控制符,后面3个对应E、L、F这3个字符的ASCII值。这4个字节又被称为ELF文件的魔数,几乎所有的可执行文件格式的最开始几个字节都是魔数。比如a.out格式最开始的2个字节是0x01、0x07;PE/COFF文件最开始的2个字节是0x4d、0x5a,即ASCII的字符MZ。魔数用来确认文件类型,操作系统在加载可执行文件的时候会确认魔数是否正确,不正确会拒绝加载。
段表
段表是保存一些段的基本属性的结构(比如段名、段长、在文件中的偏移、读写权限等等)
objdump -h SimpleSection.o
这只是展示了几个关键段,可以用readelf查看全部的段
readelf -S SimpleSection.o
可以看到,ELF段表的第一个元素是无效的描述符,类型为"NULL",其他每个描述符对应一个段,即有13个有效的段。
段表的结构是一个以"Elf32_Shdr"结构体为元素的数组。这个结构体被定义在"/usr/include/elf.h"
可以清晰的看到存储的内容(●'◡'●)
决定段的属性的不是段名,而是段的类型(sh_type)和段的标志位(sh_flags),在elf.h的文件往下即可看到相关的常量定义
重定位表
上面打印出来所有的段中的".rela.text"就是属于重定位表。因为源码中调用了printf函数,引用了绝对地址。如果数据段中也有引用绝对地址的话(示例程序没有用到),就会有一个".rela.data"段。
符号(链接的接口)
链接的过程像把各个目标文件"粘在一起"。在链接的过程中,把变量和函数均称为符号,其变量名和函数名就是符号名。符号可以看作链接的"粘合剂",故每一个目标文件里面都会有一个相应的符号表用来记录目标文件中用到的符号。每个符号均有一个对应的值称为符号值,对于变量和函数来说就是它们的地址。可以使用"nm"查看目标文件中的符号结果
nm SimpleSection.o
符号表的结构如下(还是在elf.h文件里哦)
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;
存放的信息依次为符号名、符号相对应的值、大小、符号的类型和绑定的信息、所在的段
可以使用readelf查看文件的符号
readelf -s SimpleSection.o
与上面的结构体里面的结构一一对应
有一些特殊的符号
可以在程序中直接使用这些符号
示例程序(SpecialSymbol.c)
调试信息