《程序员的自我修养》(一)——编译与静态链接

简介

温故而知新

  • 计算机科学领域的任何问题都可以通过增加一个间接地中间层来解决。
  • 在UNIX中,硬件设备的访问形式跟访问普通的文件形式一样;在Windows系统中,图形硬件被抽象成了GDI,声音和多媒体设备被抽象成了DirectX对象,磁盘被抽象成了普通文件系统,等等。
  • 如何将计算机上有限的物理内存分配给多个程序使用。整个想法是这样的,我们把程序给出的地址看作是一种虚拟地址,然后通过某些映射的方法,将这个虚拟地址转换成实际的物理地址。
  • 进程的映射方式:

    • 分段,基本思路是把一段与程序需要的内存大小的虚拟空间映射到某个地址空间。
    • 分页,基本方法是把地址空间人为地等分成固定大小的页,每一页的大小又硬件决定,或硬件支持多种大小的页,由操作系统选择决定页的大小。目前几乎所有的PC上的操作系统都使用4KB大小的页。几乎所有的硬件都是采用一个叫MMU的部件来进行页映射。
    • 在页映射模式下,CPU发出的Virtual Address,即我们的程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。一般MMU都集成在CPU内部了,不会以独立的部件存在。

静态链接

编译和链接

  • 预编译(预处理):预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。C文件预编译后形成.i文件,C++文件编译后扩展名是.ii。
  • 编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,编译后生成.s文件。
  • 汇编:汇编器是将汇编代码转变成机器可以执行的指令,经过汇编后生成.o文件。
  • 链接:链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。链接过程主要包括了地址和空间分配、符号决议和重定位等步骤。最终生成可执行文件。

《程序员的自我修养》(一)——编译与静态链接_第1张图片

  • 编译过程一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。

    • 词法分析:首先源代码程序被输入到扫描器,扫描器的任务很简单,它只是简单地进行词法分析,运用一种类似于有限状态机的算法可以很轻松地将源代码的字符序列分割成一系列的记号。
    • 语法分析:接下来语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树(以表达式为节点的树)。
    • 语义分析:编译器所能分析的语义是静态语义,所谓静态语义是指在编译期可以确定的语义,静态语义通常包括声明和类型的匹配,类型的转换。
    • 中间语言生成:现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。源代码优化器往往将整个语法树转换成中间代码,它是语法树的顺序表示,其实它已经非常接近目标代码了。
    • 目标代码生成与优化:代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等。最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。

目标文件里有什么

  • 现在PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。
  • 目标文件中包括机器指令代码、数据、符号表、调试信息、字符串等。一般目标文件将这些信息按不同属性,以“段”的形式存储。
  • 程序源代码编译后的机器指令经常被放在代码段里,代码段常见的名字有“.code”或“.text”;全局变量和局部静态变量数据经常放在数据段,数据段的一般名字都叫“.data”。
  • .bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间

《程序员的自我修养》(一)——编译与静态链接_第2张图片

  • ELF目标文件的文件头描述了整个文件的基本属性,包括文件是否可执行、是静态链接还是动态链接及入口地址、目标硬件、目标操作系统、程序入口地址等。紧接着就是ELF文件各个段,后面是与段有关的重要结构——段表,段表其实是一个描述文件中各个段的数组。段表描述了文件中各个段的段名、段的长度、在文件中的偏移位置、读写权限及段的其他属性等。

  • 编译器还会将一些辅助性的信息,诸如符号、重定位信息等也按照段的方式存放在目标文件中。

《程序员的自我修养》(一)——编译与静态链接_第3张图片

静态链接

  • 现在的链接器空间分配的策略都是采用一种叫两步链接的方法。

    • 第一步,空间与地址分配。扫描所有的输入目标文件,获得它们的各个段的长度、属性和位置,并且将输入目标中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。
    • 第二步,符号解析与重定位。使用上面第一步中收集到的所有信息,读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。这一步是链接过程的核心,特别是重定位过程。
  • 对于可重定位的ELF文件来说,它必须包含有重定位表,用来描述如何修改相应的段里的内容。对于每个要重定位的ELF段都有一个对应的重定位表,而一个重定位表往往就是ELF文件中的一个段,所以其实重定位表也可以叫重定位段。
  • 链接器的COMMON块机制解决同一个符号定义在多个文件中的问题(目前的链接器本身并不支持符号的类型,即变量类型对于链接器来说是透明的,它只知道一个符号的名字,并不知道类型是否一致)。这个问题解决的规则是,如果是弱符号(未初始化的全局变量)则在最终链接后的输出文件中,该符号所指的变量大小以输入文件中最大的那个为准。如果其中有一个符号为强符号,那么最终输出结果中的符号所占空间与强符号相同。
  • 如果要使两个编译器编译出来的目标文件能够互相链接,那么这两个目标文件必须满足下面这些条件:采用同样的目标文件格式、拥有同样的符号修饰标准、变量的内存分布方式相同、函数的调用方式相同,等等。其中我们把符号修饰标准、变量内存布局、函数调用方式等这些跟可执行代码二进制兼容性相关的内容称为ABI(Application Binary Interface)。
  • 一个静态库可以简单地看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。
  • VISUAL C++允许使用脚本来控制整个链接过程,这种控制脚本叫做模块定义文件,它们的扩展名一般为.def。

Windows PE/COFF

  • 在Windows平台,VISUAL C++编译器产生的目标文件使用COFF格式,而可执行文件为PE格式。微软对64位Windows平台上的PE文件结构稍微做了一些修改,这个新的文件格式叫做PE32+。新的PE32+并没有添加任何结构,最大的变化就是把那些原来32位的字段变成64位。
  • COFF文件是由文件头及后面的若干个段组成,再加上文件末尾的符号表、调试信息的内容,就构成了COFF文件的基本结构。COFF文件的文件头部包括了两部分,一个是描述文件总体结构和属性的映像头,另外一个是描述该文件中包含的段属性的段表。
  • “.drectve 段”实际上是“Directive”的缩写,它的内容是编译器传递给链接器的指令,即编译器希望告诉链接器应该怎样链接这个目标文件。
  • COFF文件中所有以“.debug”开始的段都包含着调试信息。比如“.debug$S”表示包含的是符号相关的调试信息段;“.debug$P”表示包含预编译头文件相关的调试信息段;“.debug$T”表示包含类型相关的调试信息段。
  • PE文件是基于COFF的扩展,它比COFF文件多了几个结构。最主要的变化有两个:第一个是文件最开始的部分不是COFF文件头,而是DOS MS可执行文件格式的文件头和桩代码;第二个变化是原来的COFF文件头中的“IMAGE_FILE_HEADER”部分扩展成了PE文件头结构“IMAGE_NT_HEADERS”,这个结构包括了原来的“Image Header”及新增的PE扩展头部结构。

《程序员的自我修养》(一)——编译与静态链接_第4张图片

你可能感兴趣的