编译器编译源代码后生成的文件叫做目标文件。
3.1目标文件的格式
windows下的可执行格式是PE文件,Linux则是ELF,它们都是COFF格式而来。
目标文件就是源代码编译后但未进行链接的那些中间文件。
动态链接库(.ddl)(.so)和静态链接库(.lib)(.a)都按照可执行文件存储。
静态链接库可理解成多个目标文件捆绑在一起的包。
下面是ELF文件相关的一个总结图,建议看看。
随后书下面就讲了一些 目标文件和可执行文件的小历史 这里不做概括
COFF的主要贡献就是在目标文件中引入了段的机制。
3.2目标文件是什么样的?
目标文件其实已经是二进制文件了。里面有机器指令代码和数据。
当然它还有链接时需要的一些信息,比如符号表,调试信息,字符串等。
一般目标文件将这些信息按照不同的属性,以“节”的形式存储,有时候也叫做“段”。节与节之间唯一的区别就是ELF的链接视图和装载视图的时候,此处节和段都叫段。
编译后的机器指令通常放在代码段(.text),全局变量和局部静态变量放在数据段(.data)。
从上图可以看到,ELF文件开头是“文件头”,它描述了整个文件的属性(包括是否可执行、静态链接还是动态、入口地址、目标硬件、目标操作系统)其中还包括一个段表,是描述文件中各个段的数组(描述各个段在文件的偏移和属性)。
值得一提的是,初始化的全局变量和局部静态变量保存在data段;未初始化的全局变量和局部静态变量保存在bss段。但是本来它们应该在data段的,但是因为没初始化,都是0,程序不给存放数据0预留空间,所以此时bss段只是给它们预留位置而已。
总的来说,程序指令去了text代码段,程序数据去了data和bss段。
把文件分开来放的好处:
- 程序被装载后,数据和指令将分别被映射到两个虚拟内存区域
- 对于CPU来说,它们有着极为强大的缓存体系(有必要提高缓存的命中率)。
- 当程序运行多个该程序的副本时,指令只用在一个地方读取,程序的其他数据就可以共享,以用来节省空间。
3.3挖掘SimpleSection.o
这里是用readelf来分析文件的格式,笔者只是简单总结一下学到的东西,就不做赘述,建议跟着原文看一遍(P61开始)
1 | objdump -h SimpleSection.o |
-h是把ELF各个段的基本信息打印出来,-x可以把更多信息打印出来。
这里多了打印出来除了上面提到的段的话,还有三个段(.rodata)只读数据段,(.comment)注释信息段,(.note.GNU-stack)堆栈提示段(此处是0,暂且忽略它)。此ELF文件事实上存在的段就只有text,data,rodata,comment这四个段了。
3.3.1 代码段
主要是对着格式分析,此处不再赘诉
3.3.2数据段和只读数据段
主要是对着格式分析,此处不再赘诉
“rodata”存放只读数据,语义上支持了C++的const关键字,又保证了程序安全性。另外再某些嵌入式平台下,有些存储区域是采用只读存储器的,如ROM,这样将“.rodata”段放在该存储区域中就可以保证程序访问存储器的正确性。
另外值得一提的是,有时候编译器会把字符串常量放到data段,可以试试把文件名改成.cpp,然后用各种MSVC编译器编译一下
然后后面根据它展示的内容,小小讲到了大小端序,在本书的附录有详细介绍,到时候去看。
3.3.3BSS段
上面说过,bss段存放未初始化的全局变量和局部静态变量。
通过bss段的符号是否被定义,是否存放在目标文件的BSS段,引出了“弱符号和强符号和common块”这两个概念,在下一章会讨论这个问题。
编译器优化有时候会给我们分析系统软件背后的机制带来很多在障碍,使很多问题不能够一目了然,本书尽量避开优化过程,还原机制和原理本身
3.3.4其他段
这张图片帮忙说了很多,一下子概括了整整一小节的内容,后仅做补充
自定义段
有些时候我们可能希望变量或者某部分代码放到指定的段去,实现某些特定功能。比如为了满足某些硬件的内存和I/O地址布局,或者是像Linux操作系统内核中用来完成一些初始化和用户空间复制时出现页错误异常等。
GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:
1 | __attribute__((section("FOO"))) int global=42 |
我们在全局变量或者函数之前加入“attribute((section(“name”)))”属性,就可以把相应的变量和函数放到以“name”作为段名的段中。
3.4ELF文件结构描述
先放ELF文件结构预览图
ELF目标文件格式最前面是ELF文件头(包含了整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等)紧接着是ELF各个段。
其中与ELF中和段的重要结构就是段表(SHtable),表中描述了(段名,段长度,偏移,读写权限以及其他)段的属性。
3.4.1文件头
直接上图
ELF文件头定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。
ELF文件头结构以及相关常数被定义在“/usr/include/elf.h”里,ELF有32和64位的版本。不同版本的成员大小不一样,但是ELF文件头内容基本上一样,它们文件头分别叫做“Elf32_Ehdr”和“Elf64_Ehdr”。“elf.h”使用typedef定义了一套自己的变量体系。
一下是Elfxx_Ehdr的结构体图
然后较详细介绍了ELF头部结构体一些成员的含义,比如ELF魔数、ELF_type,系统一般通过ELF_type来判断它是ET_REL,ET_EXEC,ET_DYN这三种之一的文件。
ELF头部结构体一些成员的含义
3.4.2段表
段表存放段的基本属性的结构,位置由ELF文件头的“e_shoff”成员决定。
我们可以看ELF文件段表的基本内容
段表的结构比较简单,它是一个以“Elf32_Shdr”(也叫段描述符)结构体为元素的数组。每个结构体意味着一个段的信息。ELF段表第一个元素是无效的段描述符。
“Elf32_Shdr”被定义在”/usr/include/elf.h”,如图:
各个成员含义如下
这里简单说一下段的标志位,表示该段在进程虚拟地址空间的属性,比如是否可写,是否可执行等。
然后再简单说一下段的链接信息(sh_link、sh_info)
3.4.3重定位表
如果一个段的类型是”SHT_REL”,也就是说它是一个重定位表,对于每个需要重定位的段都有一个相应的重定位表
比如”.rel.text”就是对”.text”的重定位表。此时sh_link表示符号表的下标,sh_info表示它作用域哪个段。
下一章静态链接过程的时候,再细细分析。
3.4.4字符串表
一般字符串表在ELF文件中也以段的形式存储,常见的段名是“.strtab”或者“.shstrtab”。分别代表“字符串表”和“段表字符串表”。
3.5链接的接口——符号
在链接中,目标文件的相互拼接实际上是目标文件之间对地址的引用,就是对函数和变量地址的引用,我们将函数和变量统称为符号,函数名和变量名就是符号名。
每个目标文件都有一个相应的符号表(Symbol Table),每个符号都有一个值,叫符号值,对于函数和变量来说,这就是它们的地址。
还有几种不常用到的符号:定义在本目标文件的全局符号、段名(它的值就是段的起始地址)、本目标文件引用的全局符号、局部符号、行号信息(目标文件指令和源代码中代码行对应的关系)。
3.5.1 ELF符号结构
elf符号表往往是个段,段名“.symtab”。
符号表结构是一个Elf32_Sym结构的数组
符号绑定st_info,低4位表示符号类型。高28位表示符号绑定信息,有局部,全局符号还有弱引用。
符号所在段st_shndx,定义在本目标文件中,表示符号所在段 在段表 中的下标。
其他详细建议看书
3.5.2 特殊符号
使用ld作为链接器来链接生成可执行文件时,它会为我们定义很多特殊符号,这些符号不是我们定义的,但我们可以使用它,这些符号叫做特殊符号。详细看书
3.5.3 符号修饰和函数签名
相当于同一个函数名在不同函数位置中,或者不同返回类型,或者我们的一些函数和库重名,会有一些修饰区分它们,大概就是这样,详细看书。
3.5.4 extern“C”
c++为了和C兼容,C++有一个声明和定义C符号的关键字“extern”
详细看书
3.5.5 弱符号和强符号
对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
也可以用GCC的“attribute((weak))”
注意:强符号和弱符号都是针对定义来说的,不是针对符号的引用
1 | extern int ext; |
这里,weak和 weak2 是弱符号, strong和 main 是强符号,而ext 既非强符号也非弱符号,因为它只是一个外部变量的引用。
针对强弱符号的概念,链接器会按如下规则处理和选择被多次定义的全局不好:
- ** 规则1:**不允许强符号被多次定义(即不同的目标文件不能有同名的强符号);如果有多个强符号定义,则链接器包符号重复定义错误。
- ** 规则2:** 如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。
- ** 规则3:** 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。
同样对于符号名的引用也分为强引用和弱引用,强引用表示如果找不到符号定义会报错,弱引用不报错,默认为0或某个特殊值。
链接:https://www.jianshu.com/p/31108b62f81d
同样对于符号名的引用也分为强引用和弱引用,强引用表示如果找不到符号定义会报错,弱引用不报错,默认为0或某个特殊值。
3.6 调试信息
如果我们GCC编译的时候加上“-g”参数,编译器产生的目标文件里面加上调试信息,可以用readelf工具查看,目标文件里面多了很多“debug”的段
我们可以用“strip”命令来去掉ELF的调试信息