2016.05.05 –
《程序员的自我修养 —— 链接、装载与库》
- 余甲子 石凡 潘爱民编
个人选读笔记 - 学点表皮。
05.05
PART I 静态连接
IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建。
编译器就是将高级语言翻译成机器语言的一个工具程序。
05.07
预编译过程主要处理源代码文件中的以“#”开始的预编译指令。处理/替换掉编译后期不需要的所有信息(如#define、#include、条件预编译);保留/添加编译后期所需要的信息(如#pragma、添加行号和文件名标识)。
编译过程把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编指令都对应一条机器指令。(目标文件)
(静态)链接器将各个目标文件拼接在一块形成可执行文件。
源代码文件到目标代码文件的过程。
扫描器/词法分析器将源代码字符序列分割成一系列的记号。该记号一般可以分为这几类:关键字、标识符、字面量(包括数字、字符串等)和特殊符号(如加号)。在识别记号的同时,扫描器也进行了其它诸如将标识符放到符号表、将数字、字符串常量放到文字表等的工作,以备后面的步骤使用。
语法分析器 对扫描器产生的记号进行语法分析(如’+’记号应有两个操作数)从而产生语法树(表达式为节点的树)。
语义分析器完成由语法分析器产生的语法树中的表达式是否具有意义的分析(如分析出两指针做乘法不合法)。经过语义分析器后,整个语法树的表达式会被标识类型,如果有些类型需要做隐式转换,语义分析程序会在语法树中插入相应的转换节点。
源码级优化器对源代码进行优化输出中间代码(中间代码是经优化的语法树的顺序表示,它跟目标机器和运行时环境无关)。
代码生成器将中间代码转换成目标机器代码(这个过程十分依赖于目标机器,因为不同的机器有着不同的字长、寄存器、整数数据类型和浮点数数据类型等)。
目标代码优化器对代码生成器生成的目标代码进行优化(如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等)得到最终的目标代码。[源码级和目标代码级都有优化]
编译器的编译过程图简示。
这里的目标机器代码指得是由汇编指令组成的代码(即汇编.s文件)。
程序设计的模块化是程序员一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成小的系统以达到各个突破的目的。一个复杂的软件也如此,人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。这个过程主要包括地址和空间分配、符号决议(绑定)和重定位等步骤。
最基本的静态链接过程。
库其实是一组目标文件的包,就是一些常用的代码编译成目标文件后打包存放。
比如在程序模块main.c中使用另外一个模块func.c中的函数foo()。在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo()指令的目标地址搁置(由foo声明告知编译器foo函数在其它文件中被定义),等待链接的时候由链接器去将这些指令的目标地址修正(当foo函数的地址发生改变后,重新链接一次即可)。这就是静态链接最基本的过程和作用。
目标文件中的内容由经编译的程序内容(和调试信息)组成。
目标文件指汇编器输出还未经过链接的文件(如window下的.obj、linux下的.o),其内是高级语言源程序对应的机器指令/二进制/高低电平。PC平台流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。目标文件跟可执行文件的内容和结构相似,所以目标文件一般跟可执行文件格式一起采用一种格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)及静态链接库(Static Linking Library)(Windows的.lib和Linux的.a)文件都按照可执行文件格式存储。
目标文件中有编译后(预处理过程到汇编过程)的指令代码、数据,还包括链接是所需要的信息(符号表、调试信息、字符串等)。一般目标文件将内容按不同的属性,以“节”(Section)(或段-Segment)的形式组织。
“文件头”描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(可执行文件)、目标硬件、目标操作系统等信息,文件头还包括一个段表(Section Table)[见2.4下的段表 – ELF段表不在ELF文件头中,文件头记录段表在文件中的偏移],它可被看作是描述文件中各个段的数组。段表描述了文件中各个段在文件中的偏移位置及段的属性等,从段表里面尅得到每个段的所有信息(这些信息由编译器生成)。文件头后面就是各个段的内容。
一般C语言的编译后执行语句都编译成机器代码,保存在.text段;已经初始化的全局变量和局部静态变量都保存在.data段;未初始化的全局变量和局部静态变量一般放在一个叫“.bss”的段里(未初始化的全局变量和静态局部变量默认值都为0,本来它们也可以被放在.data段里,但是因为它们都是0,就专门设立一个“.bss”段来记录所有未初始化的全局量和静态变量以及它们大小的总和,“.bss”段只是为记录未初始化全局变量和局部静态变量预留空间而已,除了这些记录外这些变量并不占用空间(指存储目标文件的空间),载入程序时/程序运行时根据该记录为这些变量分配内存空间)。
05.08
[objdump的参数](利用这些参数可以查看目标文件中各段的内容)
[“-h”参数把ELF文件的各个(关键)段打印出来]
[“-s”参数可以将所有段的内容以16进制的方式打印出来]
[“-d”参数可以将所有包含指令的段反汇编]
[“-x”参数会显示所有头(all – headers,段表、符号表、重定位表)]
目标文件内容/段分布
编译器(汇编器)以段(section)的形式将源码编译后的内容输出/组织到目标文件中。
摘书中的SimpleSection.c。
在Linux平台下将SimpleSection.c转换为目标文件,并查看目标文件内容。
关注“.text”、“.data”、“.rodata”和“.comment”这4个段,并将它们的大小和在文件中的偏移位置标识在下图。
[目标文件SimpSection.o文件大小可用“ls –l”命令查看]
目标文件各段内为源程序对应的二进制内容。
(Linux下)ELF目标文件的生成过程•简。
ELF头和各个段表中的属性由“/usr/include/elf.h”中的Elf32_Ehdr结构体描述。
[readelf参数]
[“-h”参数显示ELF头]
[“-S”参数显示ELF段表]
(1) ELF头
以SimpleSection.o为例,查看其ELF头。
不用关心目标文件内程序入口等信息,关注下“ELF魔数”、“ELF文件类型”、“ELF所在的机器类型”。
ELF魔数(Magic)
最开始的4个字节是所有ELF文件都必须相同的标识符,分别为0x7F、0x45、0x4C、0x46,(ASCII字符里的DEL控制符、E、L、F) [这4个字节被称为魔数,用来确认文件的类型,OS在加载可执行文件的时候会确认魔数是否正确]。接下来的一个字节用来标识ELF文件类型,0x01/0x02表示32/64位;第6个字是字节序,规定该ELF文件是大端还是小端的;第7个字节规定ELF文件的主版本号;后面9个字节ELF标准没有定义,一般填0,有些平台会使用这9个字节作为扩展标志。
文件类型(Type)
表示ELF文件类型:目标文件(可重定位文件)、可执行文件还是共享目标文件。
机器类型(Machine)
表示ELF文件的平台属性(该ELF文件只能在兼容Intel 80386的平台上使用)。
(2) 段表(Section Header Table)
段表描述了ELF各个段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限以及段的其他属性。编译器、链接器和装载器都是依靠段表来定位和访问各个段(的属性)的。段表在EFL文件中的位置有ELF文件头的start of section headers(e_shoff成员)值决定。
用“readelf –S SimpleSection.o”命令查看SimpleSection.o段表信息。
每一个段表由“/usr/include/elf.h”内的Elf32_Shdr结构体描述,其每一列对应该结构体内的一个元素。由该段表可得到SimpleSection.o的段表和所有段的位置和长度如下图。
[有的段会跟前一个段相隔一两个字节是因为对齐的原因]各个段的大小可有偏移地址计算得来。
05.09
(3) 重定位表
在SimpleSection.o中有一个名为“.rel.text”的段,它的类型(Type – Elf32_Shdr.sh_type)为重定位表(REL – SHT_REL)。对于每个需要重定位(修改对绝对地址引用)的段都会有一个重定位表,在SimpleSection.o中,只有“.text”中有一个对“printf”函数的绝对引用,故而只有一个“.rel.text”重定位表。重定位表也是ELF的一个段,其Type - Elf32_Shdr.sh_type为REL – SHT_REL类型,其Lk - Elf32_Shdr.sh_link表示符号表(.symtab)的下标,其Inf - Elf32_Shdr.sh_info表示它作用于哪个段(.rel.text作用于.text段,.text段的下标为1,所以Inf的值为1)。
(4) 字符串表
ELF文件中用到了很多字符串,比如段名、变量名等。因字符串的长度往往是不定的,所以用固定的结构来表示它比较困难。一种常见的做法是把字符串集中起来存放到一个表,然后使用字符串在表中的偏移来引用字符串。一般字符串表在ELF文件中也以段的形式保存,常见的段名为“.strtab”或“.shstrtab”,这两个字符串表分别为字符串表和段表字符串表,它们分别用来保存普通的字符串(如符号名)和段表中用到的字符串(如段名)。
每一个目标文件都会有一个相应的符号表,该表里记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值 —— 符号值,对于变量和函数来说(还有编译器自身产生的符号),符号值就是他们的地址(虚拟地址)。
(1) ELF符号表
ELF文件中的符号表往往是文件中的一个段 —— “.symtab”。其结构由“/usr/include/elf.h”中的Elf32_Sym结构体数组描述。
(2) (ld)链接器的链接脚本中的特殊符号
当使用ld作为链接器来链接生成可执行文件时,它会为我们定义很多特殊的符号,这些符号并没有在你的程序中定义,但可以在程序中直接声明并引用它们。其实这些符号是被定义在链接器的链接脚本中的,链接器将在最终生产可执行文件的时候将这些特殊符号解析成正确的值。ld中几个具有代表性的特殊符号如下。
(3) 强符号和弱符号
强弱符号是对定义而非引用来说的。对于C/C++来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。也可以通过特殊的方法如gcc的“__attribute _((weak))”(两个下划线)来定义任何一个强符号为弱符号。
目标文件里面还有可能保存的是调试信息。几乎所有现代的编译器都支持源代码级别的调试,比如可以在函数里面设置断点,可以监视变量变化,可以单步运行等,前提是编译器必须提前将源代码与目标代码之间的关系对应,比如目标代码中的地址对应源代码中的哪一行、函数和变量的类型、结构体的定义、字符串保存到目标文件里。(gcc编译器加“-g”参数就会在目标文件中加入调试信息)。
PART II 装载与动态链接
PART III 库与运行库
[2016.05.05 - 23:03]