3 目标文件里有什么

来源:互联网 发布:组装机安装mac os x 编辑:程序博客网 时间:2024/04/26 05:30
目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实,它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。可执行文件格式涵盖了程序的编译、链接、装载和执行的各个方面。了解它的结构并深入剖析它对于认识系统、了解背后的机理大有好处。

3.1目标文件的格式
广义上目标文件与可执行文件格式几乎是一样的,所以我们广义地将目标文件与可执行文件看成是一种类型的文件。
动态链接库(dll和so)及静态链接库(lib和a)文件都按照可执行文件格式存储。它们在Windows下都按照PE-COFF格式存储,Linux下按照ELF存储。
静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含有很多目标文件的文件包。ELF文件标准里把系统中采用ELF文件格式的文件归为4类:
可重定位文件 Linux的.o windows的.obj
可执行文件 bin/bash文件 windows的exe
共享目标文件 Linux的so Windows的dll
核心转储文件 Linux下的core dump
我们可以在Linux下使用file命令来查看相应的文件格式。
3.2目标文件是什么样的
目标文件基本内容:机器指令代码、数据。还包括链接时所需要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”的形式存储,有时候也叫段,一般,它们都表示一个一定长度的区域,基本上不加以区别,唯一区别是在ELF的链接视图和装载视图的时候,后面会专门提到。本书默认统称为段。
机器指令通常放在代码段,代码段常见的名称有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段(data section),数据段的一般名字叫“.data”。看一个简单的程序被编译成目标文件后的结构:
这里写图片描述
未初始化的全局变量和局部静态变量一般放在一个叫“.bss”的段里。我们知道未初始化的全局变量和局部静态变量默认值都为0,在大data段分配空间并且存放数据0是没有必要的。程序运行的时候他们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bsss段。所以,.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。
总的来说,程序源代码被编译以后主要分为两种数据段:程序指令和程序数据。代码段属于程序指令,而数据段属于程序数据。
1.分别设置可读和可读写,防止程序指令被有意或无意的修改
2.指令和数据区的分离有利于提高程序的局部性,现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。
3.最重要的是,当系统中运行着多个改程序的副本时,它们的指令相同,所以内存只需要保存一份改程序的指令部分。对于指令这种只读区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它
在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。如果系统中运行了数百个进程,可以想象共享的方法可以节省大量空间。关于内存共享的更深入的内容我们将在装载这一章探讨。
3.3挖掘simplesection.o
真正了不起的程序员对自己的程序的每一个字节都了如指掌。
–佚名
$ gcc -c simplesection.c(参数-c表示只编译不链接)
.bss有大小,但是没有Comments,其实不存在;由于优化,初始化为0的仍放在.bss段。
一个ELF文件可以拥有几个相同段名的段。
但是有时候,程序员可以指定变量所处的段。
attribute((section(“name”))) void foo()
3.4ELF 文件结构描述
这里写图片描述
为了对每个成员的大小做出明确的规定以便于在不同的编译环境下都拥有相同的字段长度,“elf.f”使用typedef定义了一套自己的变量体系,如下表:
这里写图片描述
ELF文件类型:
这里写图片描述
ELF段表的这个数组的第一个元素是无效的段描述符,它的类型为“NULL”,除此之外每个段描述符都对应一个段。也就是说simplesection.o共有10个有效的段。
sh_addr:段虚拟地址,如果段可以被加载,则sh_addr为该段被加载后在进程空间中的虚拟地址;否则sh_addr为0.
sh_offset:如果段在文件中存在,表示该段在文件中的偏移;否则无意义,如对于.bss段来说就是没有意义。
3.4.3重定位表
对于每个需要重定位的代码段或数据段,都会有一个相应的重定位表,比如simplesection.o中的“.rel.text”就是针对“.text”段的重定位表,因为“.text”段中至少有一个绝对地址的引用,那就是“printf”函数的调用;而“.data”段则没有对绝对地址的引用,它只包含了几个常量,所以simplesection.o中并没有针对“.data”段的重定位表“.rel.data”。
一个重定位表也是ELF文件的一个段,那么这个段的类型(sh_type)就是“SHT_REL”类型的,它的“sh_link”表示符号表的下标,“sh_info”表示它作用于哪个段。比如,“.rel.text”作用于text段,而“text”段的下标为1,那么“.rel.text”的“sh_info”为1。
3.5链接的接口–符号
链接本质是把不同的目标文件之间相互粘到一起,或者说像积木一样,可以拼装形成一个整体。所以需要固定的规则。拼合其实是目标文件之间对地址的引用,即对函数和变量的地址的引用。在链接中我们将函数和变量统称为符号(symbol),函数名或变量名就是符号名。
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才得以正确完成。链接过程很关键的是符号的管理。每个目标文件都会有一个相应的符号表,这个表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是它们的地址。除了函数和变量外,还存在其他不常用的符号。我们将符号表中所有符号进行分类:
1.定义在本目标文件的全局符号,可以被其他目标文件引用。
2.在本目标文件中引用的全局符号,却没有定义在本目标文件,一般叫做外部符号,也就是前面所讲的符号引用,比如simplesection.c中的printf。
3.段名,这种符号往往由编译器产生,它的值就是该段的起始地址,比如simplesection.o里面的“.text”、“.data”等。
##static int static_var,static_var2
4.局部符号,这类符号只在编译单元内部可见,比如simplesection.o里面的“static_var”和“static_var2”.调试器使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。
5.行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。
我们只关心第一和第二类,其他对于其他目标文件来说都是“不可见”的,在链接过程中也是无关紧要的。
3.5.1 ELF符号表结构
符号表结构是一个Elf32_Sym结构的数组,每个对应一个符号。这个数组的第一个元素,也就是下标为0的元素为无效的“未定义”的符号。
Elf32_sym中的st_shndx:符号所在的段
符号类型和绑定信息(st_info)
这里写图片描述
符号所在的段(st_shndx)
如果符号定义在本目标文件中,那么这个成员表示符号所在段表的下标;否则,或者对于有些特殊符号,sh_shndx的值有些特殊:
这里写图片描述
这里写图片描述
readelf -s simplesection.o
3.5.2特殊符号
当我们使用ld作为链接器来链接生成可执行文件时,它会为我们定义很多特殊符号,这些符号并没有在你的程序中定义,但是可以直接声明并且引用它,我们称之为特殊符号。其实这些符号都是被定义在ld链接器的链接脚本中的,我们在后面的“链接过程控制”这一节中会再来回顾这个问题。目前只需知道这些符号是特殊的,你无需定义它们,但可以声明并使用。链接器会在将程序最终链接为可执行文件的时候将其解析为正确的值,注意,只有使用ld链接生成的最终可执行文件时这些符号才会存在。几个很具有代表性的特殊符号:
这里写图片描述
3.5.3符号修饰与函数签名
在编译器及链接器处理符号时,它们使用某种名称修饰的方法,使得每个函数签名对应一个修饰后名称。编译器在将C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以都认为是不同的函数。
3.5.4 extern “c”
3.5.5 弱符号与强符号
多个目标文件含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。比如在目标文件A和B中都定义一个全局变量global,并将它们都初始化,那么链接器将A和B进行链接时会报错:
这里写图片描述
这种符号的定义可以被称为强符号。有些符号的定义可以被称为弱符号。
3.6调试信息
如果我们在gcc编译时加上“-g”参数,编译器就会在产生的目标文件里面加上调试信息,可以通过readelf等工具可以看到,目标文件里多了很多“debug”相关的段:
这些段中保存的就是调试信息。现在的ELF文件采用一个叫DWARF的标准的调试信息格式,现在该标准已经发展到第三个版本。调试信息在目标文件和可执行文件中占用很大的空间,在Linux下,我们可以使用“strip”命令来去掉ELF文件中的调试信息。

原创粉丝点击