在谈论目标文件里面有什么内容之前,我们先来看一下,一个程序如何从源代码变成一个目标文件。
源代码到目标文件的过程
一个程序从源文件到可执行文件要经过四个步骤:
- 预处理
- 编译
- 汇编
- (链接)
我们简单介绍一下。(以下命令全部采用Linux上的GCC编译器命令。我们以hello.c文件为例)
预处理:gcc -E hello.c -o hello.i
1、删除所有的
#define
和注释,展开所有宏定义。
2、处理所有的条件预编译指令,如#if #ifndef
等。
3、处理#include
指令,将有关头文件插入预编译指令的位置。这个过程是递归的。
4、添加行号,保留#pragma
指令。
编译:gcc - S hello.i -o hello.s
编译过程就是将预处理完的文件进行一系列的词法、语法、语义分析和代码优化,之后生成相应的汇编代码文件。
编译这块涉及到一些编译原理的知识,太过复杂,笔者也没有学过相应知识,因此根据一些资料进行整理,有兴趣的读者可查阅相关资料。
1、词法分析:经过预处理的代码被输入到扫描器中,运用一种有限状态机的算法将源代码的字符序列分割成一系列记号。(记号为关键字、标识符、字面量和特殊符号如 =等)。
2、语法分析:有语法分析器对于上述记号进行语法解析,生成语法树。该树以表达式为结点。
3、语义分析:由语义分析器完成。分为静态语义(编译期可以确定的语义)和动态语义(只有在运行期才可以确定的语义)。静态语义一般是声明和类型的匹配,类型转换等;动态语义就是在运行时才出现的,比如0作为除数就是一个运行期语义错误。
4、优化:将语法树转化为中间代码,比如2+4会转化为6等。
最后再经过代码生成器将中间代码转换成机器代码(汇编代码),代码优化器会对其进行优化,比如选择合适的寻址方式等等。
经过这些扫描、词法、语法、语义分析、代码生成、代码优化等一系列步骤,源代码被编译成了目标文件。(再通过链接器将目标文件和库以及启动文件链接在一起就形成了可执行文件,这个后续再讲)
目标文件里有什么?
目标文件就是源代码编译以后单未进行链接的中间文件(Linux下的.o文件),它跟可执行文件(Linux下为ELF文件)的内容和结构都很相像,因此可以采用一种格式存储。(PS:其实不光是可执行文件,动态链接库即.so和静态链接库.a文件也是按照可执行文件存储)
目标文件中的内容有编译后的机器指令代码、数据,此外还有一些链接时所需要的信息(符号表等,后续再讲)。一般目标文件将这些信息按不同的属性以“段”的形式存储。(我猜想这个段应该和操作系统中内存管理的分段有联系,个人见解)
按照我们在操作系统内存管理中分段的理解,我们知道有段则必有段表来保存段的基本属性(哎 在这里我又觉得这个段和OS中分段管理的段又不同了,因为分段管理中的段表存放的是虚拟地址和物理地址的映射关系,故上面画了删除线。)好,言归正传再说回段表,它描述了ELF文件各个段的信息,比如段名、段长度以及在文件中的偏移,编译器、链接器和装载器都是依据段表来定位和访问各个段的属性的。(我们可以采用objdump - h
命令来查看.o目标文件的一些段和相应属性。也可以使用readelf
工具来查看ELF文件的各个段)
程序编译后的机器指令放在“代码段”,一般称之为“.text”;全局变量和局部静态变量数据放在“数据段”,称之为“.data”,其中未初始化的全局变量和局部静态变量放在“.bss”段中。(PS:未初始化的默认值为0,也可以放在“.data*”,但是因为他们都是0,在data段分配空间存放数据为0是没必要的。.bss段只是为未初始化的全局变量和局部静态变量预留位置,并没有内容,所以它在文件中也不占空间*)
程序运行时它们是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小总和,记为.bss段
其实这里有个问题:为什么要把程序分为数据段和代码段呢?
1、读写权限不同。当程序被装载后,数据和指令会被分别映射到两个虚存区。因为指令对于进程是只读的,而数据对于进程可读可写,因此这两个虚存区的读写权限被设置为不同。可以防止指令被改写。
2、现代CPU的缓存设计。指令缓存和数据缓存分离,以提高缓存命中率。
3、Last but not least,对于多个程序副本,指令是共享的,数据是私有的。当系统运行多个程序副本时,他们指令是一样的,内存中保存一份即可(只读),而数据区域则是每个进程私有的。
其实除了上面介绍到的.data、.text、.bss段以外,还有3个段分别是.rodata(只读数据段)、.comment(注释信息段)和堆栈提示段(.note.GUN-stack)以及字符串段等等。其中有一个“.rel.text”段,它是一个重定位表,对于每个需要重定位的代码段和数据段都会有一个重定位表,链接器在处理目标文件时通过该表对文件某些部位进行重定位。(此外还有一些重要的段属性如contents、alloc等。)
此外对于ELF文件结构的描述,有兴趣的读者可以自行阅读《程序员的自我修养—链接、装载与库》这本书,书中介绍很是详尽。
在最后介绍一下强符号与弱符号。
符号:在链接过程中,将函数和变量统称为符号。
强符号:编译器默认函数和初始化的全局变量为强符号。
弱符号:未初始化的全局变量为弱符号。
符号强弱只针对定义,并不针对符号的引用。
对于符号有如下规则:
1、不允许强符号被多次定义。(多次定义会报符号重复定义错误)
2、如果一个符号在一个文件中是强符号,在其他文件中是弱符号,那么选择强符号。
如果对外部目标文件的符号引用在最终链接成可执行文件时没有找到该符号的定义,链接器会报错,这种被称为强引用;与之对应还有一个弱引用,如果未找到符号定义,则链接器默认为0或一个特殊值,便于程序代码能够识别,强弱引用主要用于库的链接过程。