目录
一、目标文件的格式
二、目标文件是什么样的、包含什么。
三、挖掘SimpleSection.o
3.1 代码段
3.2 数据段和只读数据段
3.3 Bss段
3.4 其他段
四、ELF文件结构描述
4.1 ELF头文件
4.2 ELF段表
4.3 重定位表
4.4 字符串表
五、链接的接口-符号
5.1 ELF符号结构表
5.2 特殊符号
5.3 符号修饰与函数签名
5.4 弱符号与强符号
5.5 调试信息
六、小结
一、目标文件的格式
现在PC端的可执行文件(Executable)主要是Windows下的PE和Linux下的ELF,它们都是COFF格式的变种。具体为windows下的.obj和.dll文件,Linux下的.so和.a文件。动态连结库(ddl so)和静态链接库(lib a)。
ELF文件可被归为4类:
当我们在Linux环境下使用file命令来查看这几种类型的文件时:
分别对应relocatable、executable、shared object这几个选项。
二、目标文件是什么样的、包含什么。
目标文件包含了编译后的机器指令代码、数据、链接时必要的信息(符号表、调试信息、字符串)。以节或段的形式存储。
一般来说,编译后执行语句都编译成机器代码,保存在.text中,已初始化的全局变量和局部静态变量保存在data段中,未初始化的全局变量和静态局部变量放在bss中,默认值都是0,程序运行时需要占用内存,并且可执行文件必须记录未初始化的全局变量和局部变量的大小总和,记为.bss段,所以bss段为未初始化的变量预留位置,不占据空间 。
总体来讲,代码被编译后分为两部分:程序指令和程序数据。这样做有如下好处:
1. 数据和指令被分开后,数据区域程序可读写,指令区域可读。防止程序被有意无意的改写。
2. 现代CPU的缓存被设计为数据缓存和指令缓存,分开有助于提高缓存命中率。对于缓存命中率可看如下链接:
缓存命中_zhutaorun的博客-CSDN博客_缓存命中
3. 第三个原因最重要,系统中运行多个程序的副本时,它们的指令都是一样的,所以内存中只需保留一份改程序的指令。
三、挖掘SimpleSection.o
其代码如下:
用gcc编译后得到simplesection.o 。用binutils工具查看object内部结构,命令如下:
objdump -h simplesection.o
其中有代码段、数据段和bbs段。另三个rodata为只读数据段,comment为注视段,note.GUN.-stack为堆栈提示段。
size为占用空间大小,CONTENTS为此段是否有内容在里面,file off为此段所在位置
利用size命令查看text、data、bss段的长度,具体如下:
size SimpleSection.o
3.1 代码段
Contents of section为text的16进制内容。
其中最左侧为偏移量,中间四列为16进制内容,最后一列是text的ASCII码形式。下面的两部分为反汇编结果。
0x55正好是func1的第一句 push %ebp
0xc3是最后的指令 ret
3.2 数据段和只读数据段
.data段保存了已经初始化的全局变量和局部静态变量,分别是global_init_varabal和static_var。这两个int类型共占8字节。系统条用printf的时候,用到了一个字符串常量“%dn”,他是只读数据,他被放到了“.rodata” 段。
rodata存放只读数据,如const修饰的变量和字符串常量。单独设置rodata有很多好处,在语义上支持了c++const关键字,还保护了程序的安全性。在某些嵌入式平台,有些存储区域采用只读,如ROM,将rodata保存在此区域可以保证程序访问存储器的正确性。
我们可以看到data的由高到低四个字节54 00 00 00 ,这个刚好是十进制的84,为什么不是00 00 00 54?这设计CPU的字节序问题(Byte Order),也就是大端小端。
3.3 Bss段
bss段存放未初始化的全局变量和未初始化的局部静态变量,如global_unint_var和static_var2,其实只是给他俩存放了预置空间,但是我们可以看到该段大小只有4字节。
Quiz变量存放位置
做一个小测试
static int x1 = 0; static int x2 = 1;
我们猜一猜x1和x2会放在什么位置呢?答案是x1在bss中,x2在data中,原因是x1=0被编译器优化了,默认他是不占空间,这也节省了空间。这也出自前部分我们的叙述。
一般来说,编译后执行语句都编译成机器代码,保存在.text中,已初始化的全局变量和局部静态变量保存在data段中,未初始化的全局变量和静态局部变量放在bss中,默认值都是0,程序运行时需要占用内存,并且可执行文件必须记录未初始化的全局变量和局部变量的大小总和,记为.bss段,所以bss段为未初始化的变量预留位置,不占据空间 。
3.4 其他段
问题与答案:
自定义段
四、ELF文件结构描述
简化版的ELF结构图如下图:
4.1 ELF头文件
上图即是ELF文件的结构表,其中ELF Header描述了整个文件的基本属性(ELF的版本号、目标机器型号、程序入口地址等)。还有最重要的结构为 段表 Section header table,它描述了ELF文件中段的信息(段名、长度、在文件中的偏移、读写权限等)。
我们可以用 readelf 命令来查看ELF文件,具体示例如下图:
elf的相关参数被定义在Linux中的/usr/include/elf.h中,ELF在各平台通用,有32位版本和64位版本的。elf.h中使用typedef定义了一套自己的变量体系,如表3-3所示:
ELF魔数
最开始的四个字节是所有ELF文件的标识码,为7F 45 4c 46,第一个字节对应ASCII里的DEL控制符号,后三个字节为ELF三个字母的ASCII码。这四个字节又被称为ELF里的魔数。
接下来一个字符为ELF里的文件类。32位或64位。
第6个字节是规定ELF文件是大端还是小端。
第7个字节规定ELF文件的主版本号。
后9个字节ELF标准么有定义。填0。
文件类型
机器类型
4.2 ELF段表
通过命令来查看段信息:
readelf -s SimpleSection.o
可以看到,他是一个以结构体ELF_shrd为元素的数组,数组元素的个数等于段的个数。数组中每个元素都是段描述符。第一个描述符为无效描述符,他的类型为NULL,除此之外,每个描述符都对应一个段,共有10个有效的段。
其数据结构如下:
解释:
段的类型: 段的名字只是在链接和编译过程中有意义,但不能真正表式段的类型。主要决定段的类型的是sh_type和sh_flags。
段的标志位:表示该段在进程虚拟地址空间的属性,比如是否可写,是否可执行。
系统保留段:
段的链接信息:如果段的类型与链接相关,比如重定位表、符号表、那么sh_link和sh_info这两个成员的意义如下表。对于其他类型的段,这两个成员无意义。
4.3 重定位表
段中有rel.text的段,类行为SHT_REL,也就是一个重定位表(Relocation Table)。对于每个需要重定位的代码段和数据段,都会有一个相应的重定位表。一个重定位表同时也是ELF的一个段,sh_type为SHT_REL。
4.4 字符串表
五、链接的接口-符号
我们的函数和变量统称为符号,函数名和变量名统称为符号名。每个目标文件都有一个符号表,里面记录了目标文件所有的符号。我们将所有的符号进行分类,可能有以下几种:
其中最重要的是全局符号,在链接的过程中使用的也是全局符号,其他符号被隐藏了,外部文件看不见。
我们可以使用命令查看符号表如下:
5.1 ELF符号结构表
ELF的符号结构表也为文件中的一个段,段名叫.symtab,符号数据结构为Elf32_Sym,每个此数据结构对应一个符号。其数据结构定义如下:
这几个成员定义具体如表:
符号类型和绑定信息,低四位表示符号类型、高28位表示符号绑定信息。
符号所在段:分为符号定义在本目标文件中和不在本目标文件中。在目标文件中 ,那么这个成员表示符号所在的段在段表中的下标。如果不在本文件中,如表:
使用readelf查看更加清晰的输出:
1. func main都是在函数里的,他实在的位置都是代码段,Ndx为1,类型为FUNC,全局可见为GLOBAL,size为所占字节数,value为函数相对代码起始位置的偏移量。
2. printf仅在函数内引用,但未定义,Ndx为UND(shn_undef)
3. global_init_var为初始化的全局变量,它定义在.bss段 下标为3
4. global_uninit_var为未初始化的全局变量,他是SHN_COMMON类型符号,本身并没有在BSS段。
5. static_var.1553和static_var.1534是静态变量,它们的属性为LOCAL,是编译后被符号修饰过的变量。
5.2 特殊符号
5.3 符号修饰与函数签名
为防止函数命名冲突,C语言在编译后加 _ 。如函数“foo”,编译后_foo。但是这种方式只能解决一部分命名冲突问题,没有根本上解决符号冲突。
增加名称空间。
c++的修饰符号。
c++的类、继承、虚机制、重载、名称空间这些特性使得它的符号管理及其复杂,func(int). func(double)函数名称相同但是参数不同,为了区分人们发明了符号修饰、符号改编。我们以函数重载为例。
代码中有6个func,它们的修饰后的名称如下:
不同编译器厂商的名称修饰的方法可能不同。
extern “C”
c++为了与C兼容,有一个声明和定义C的符号 extern “C” 关键字,c++会把它当作C语言来处理。
一个简单的例子:
5.4 弱符号与强符号
pass
5.5 调试信息
在Linux中,我们可以使用strip 命令来删除调试信息
strip foo
六、小结
这一篇文章来自《程序员的自我修养》第三章。
这一章讲了目标文件ELF的构成,编译后的目标文件由各种段组成,如代码段text、数据段data、Bss段,还介绍了ELF的文件头、段表、重定位表、字符串表、符号表、调试表等相关信息。通常情况下,一个表就是一个段。



