.obj是目标文件,所以可以知道目标文件是指编译后生成的文件,目标文件几乎和可执行文件相同只是稍微有点不同而已。其不同之处在于有些符号和地址没有被调整。
正是因为目标文件与可执行文件几乎相同,所以它们的存储格式是一样的,可以把它们近似看成同一种文件。
Linux下的动态链接库格式为.so,Windows和Linux下的静态链接库格式分别为.lib和.a。
静态链接库是一个文件,该文件包含了很多目标文件,它是一个整体。
Linux下的可执行文件是按照ELF格式存储的,ELF标准包含4种文件,请看P81。我所熟悉的Windows下的DLL就属于共享目标文件。
目标文件一般包含了哪些内容?编译后的机器指令代码、数据、连接所需的信息、符号表、调试信息、字符串等。
目标文件把信息按照属性的不同分段存储。写到这里我感觉这书上说的与老师课上讲的程序在内存中的分段方法有些相似。在目标文件中,编译后的机器指令代码放在代码段(Code
Section)中,段名一般为.code和.text。全局变量和静态变量放在数据段(Data
Section)中,段名一般为.data。
BSS段(Block Started By Symbol)用来存储未初始化的静态变量和全局变量。话虽如此bss中并没有这些变量的内容,它只是为这些变量按照所占空间大小预留空间而已。由于这些变量默认就是0,所以压根没必要再为它们分配一个数据0,也没有必要让它们待在data段中。因此bss的作用是为这些变量预留空间。
另外目标代码还有一个文件头用来保存该目标文件的信息,它里面还有一个段表。
源代码被编译以后生成两种段数据段和指令段,.code.text属于指令段.data.bss属于数据段。
这样分主要有3点好处:
1、防止程序被有意无意篡改。这是因为指令段只读,数据段可读写。
2、提高了缓存命中率。
3、节省内存空间。因为指令段可被多个副本共享,但是副本可以拥有自己的数据段。
原来目标文件中的段还有只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)。
从书中所给的例子来看一个ELF文件只有4个段是由内容的,即.data、.text、.rodata、.comment。
从图3-3可以看出在内存中,从低地址到高地址是按照ELF
header、text、data、rodata、comment、other
data的顺序存放的。
由本小节可知,全局变量可能因为语言和编译器的不同不一定存放在bss段,但是静态变量一定存放在bss段。
虽说bss存放的是未初始化的静态和全局变量,但是有些变量如果被初始化为0,它也会被放在bss中,这是编译器的优化,有时候这种优化会带来麻烦。
表3-2列出了其他段及意义。
此外,这个段还可以自定义。
图3-4展示了ELF的层次结构。
最重要的两个部分就是ELF文件头和段表。ELF文件头描述整个文件的基本属性,段表描述各段的信息。
清单3-2清楚地描述了ELF文件头的信息,P95黑体部分列举了ELF文件头包含的信息。
ELF文件兼容各平台,它的文件结构和相关参数定义在”/usr/include/elf.h”里,它有32位和64位两种。
表3-3展示了elf.h的自定义变量体系。
表3-4展示了ELF文件头结构成员含义。
ELF魔数:ELF文件头的第一个字段是Magic,包含16bytes,对应于Elf32_Ehdr中的e_ident成员。Magic用来表示平台的各种属性。
1~4个字节是所有ELF文件都相同的标识码,分别对应del、E、L、F,这四个字节就是ELF魔数。操作系统通过确认魔术是否正确以决定是否加载可执行文件。
第5个字节用来表示ELF文件是32位的还是64位的。
第6个字节用来表示ELF字节序。
第7个字节用来表示ELF文件版本号。
后面的9个字节用来预留,有些平台可能用来作为扩展标志。
Elf32_Ehdr中的e_type成员表示ELF文件类型,ELF总共有三种文件类型如表3-5所示。操作系统是通过判断文件类型而不是扩展名来确定ELF文件类型的。
Elf32_Ehdr中的e_machine成员表示ELF文件的平台属性。虽然ELF遵循统一标准但不代表同一ELF文件可以在不同平台上使用。
它用来表示各个段的信息,ELF文件中的段是由段表决定的。
一个ELF文件不仅仅包含像data、text、bss这样的段,还包括其他的辅助性段。
段表是一个Elf32_Shdr类型的结构体数组,元素的个数代表段的个数,每个元素对应一个段。这个Elf32_Shdr被称为段描述符。
表3-7描述了Elf32_Shdr中各字段的意义。
段的名称对于编译和链接有意义,对操作系统无意义。决定段的类型的是段的类型字段,并不是段的后缀名和名称。
段的类型和段的标志位字段决定了段的属性。表3-8展示了段的各种类型。
段的标志位表示该段在进程虚拟地址空间中的属性,如是否可读。表3-9列出了段的各种属性。
表3-10列出了系统保留段的各种属性。
段的连接信息包括sh_link和sh_info,它们与链接相关,如表3-11所示。
目标文件中有一个SHT_REL的.rel.text字段,它是重定位表。重定位发生在连接的过程中,这个在前面已经讲过,重定位表记录了重定位相关信息。
顾名思义,就是用来表示各种名称的字符串的表。它是一个装有各种字符串的表格,每个字符在表中都有一个固定的位置。
这种表在ELF文件中保存为2种形式——.strtab和.shstrtab,它们分别是字符串表和段字符串表,它们在ELF文件中都以独立的段而存在。为了轻松地找到这个段,在ELF文件头中包含了这两个段的下标,名为e_shstrndx。
链接是组合目标文件的过程,目标文件是根据彼此之间的地址相互引用,从而组合成可执行文件的。而,这个地址可以简单地理解为目标文件中的函数和变量。在这里,函数和变量统称为符号,函数名和变量名统称为符号名。
链接器的着眼点主要在定义在本目标文件和定义在其他目标文件的全局性符号,因为只有这些涉及到目标文件之间的组合。
ELF文件的符号表是一个段,段名为“.symtab”,它是一个Elf32_sym类型的数组,每个数组元素代表一个符号。
在Elf32_sym结构体中有一个32bit成员叫st_info,低4bit表示符号的类型,高28bit符号的绑定信息。绑定信息具体可见表3-15,符号类型可参见表3-16。
Elf32_sym.st_shndx:如果符号定义在本目标文件中,它表示该符号所在的段在段表中的下标,否则它具有其他意义。st_shndx具体信息可见表3-17。
Elf32_sym.st_value:每个符号都有一个对应值,它一般为变量和函数的地址。st_value的意义有如下几种:
1、如果符号定义在目标文件中,并且它不是COMMON块类型,则st_value代表符号在段中的偏移。
2、如果符号定义在目标文件中并且是COMMON块类型,则st_value表示符号的对齐属性。
3、在可执行文件中st_value表示符号的虚拟地址。
链接器本身自带的,不是你定义的,定义在链接脚本中的,但是你可以用的,这样的符号是特殊符号。它们存在的时机是链接器链接生成可执行文件时,此时链接器会将它们解析成正确的值,
书中P110举了几个具有代表性的特殊符号。
本小节明确了函数签名的概念。
函数签名:主要是指函数名和参数类型,其次是所在类和命名空间等。它用于区分不同函数。
编译器和连接器会使用名称修饰的办法加工函数签名使之成为修饰后名称,在C++中为符号名。
不同的编译器对函数签名的修饰方法不同,这导致不同种类的目标文件无法互连。
原来C++编译器已经默认定义了宏__cplusplus来兼容C语言和C++。
在不同目标文件中含有相同全局性符号定义,这种情况被称为强符号,它会引起符号重定义。
C/C++编译器认为未初始化的全局变量是弱符号。
这个强弱符号是可以被定义的,所以强弱之别是根据定义来划分的,并不针对符号的引用,P117代码说明了这一点。
链接器根据符号的强弱来处理和选择定义的全局变量:
1、不允许多次定义强符号,否则报错。
2、同一个符号在各目标文件中出现了多次,但只有一个是强符号,那么编译器选择强符号的那个。
3、如果一个符号在所有目标文件中都是弱符号,那么编译器选择占用空间最大的一个。由此可见编译器对于弱符号的选择并不明显,所以由弱符号造成的错误也相对难以发现。
强引用:目标文件对于非本目标文件的符号引用,在链接成可执行文件的过程中,如果找不到该符号的定义,就报未定义错误。
弱引用:与强引用差不多,只不过在找不到符号时不报错。
强弱引用主要用于库的链接。对于未定义的弱引用,编译器为便于识别把它看作是某一值,一般为0。
弱符号与COMMON块联系较密切。
弱引用是可以手动声明的,如P118第一段代码所示。
弱符号的作用在于提供一个默认的库符号,但是当用户想要自定义该符号的时候,该自定义符号就获得了更高的优先级。而弱引用的作用在于增强了程序的可扩展性,因为有了弱引用程序功能更强,没有弱引用程序也能正常运行。
目标文件和可执行文件中都可能保存调试信息,ELF文件采用DWARF格式保存调试信息。
由于调试信息与可执行文件最终结果无关,而且占用大量空间,所以在发布软件时应该去掉这些调试信息。