《程序员的自我修养:链接、装载与库》读书笔记

第二部分 静态链接

第二章 编译和链接

从.c文件到可执行.out文件,有四个步骤:预处理(预编译),编译,汇编,链接。

预处理:处理源文件中以#开头的预编译指令,比如#define,#include等。这一步会展开所有的宏定义,插入进来被include的文件。这一步产生.i文件。

gcc -E hello.c -o hello.i

当无法判断宏定义是否正确或者头文件包含是否正确的时候,可以检查预处理的文件来确定问题所在。

编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及源代码优化,生成汇编代码文件(.s文件)。以下两个命令都可以执行:

gcc -S hello.i -o hello.s
gcc -S hello.c -o hello.s

汇编:汇编是一个叫做汇编器的东西来做的,将汇编代码转变成机器可执行的机器指令,生成.o文件。以下3条指令都可以达到目的:

as hello.s -o hello.o # 调用汇编器as
gcc -c hello.s -o hello.o
gcc -c hello.c -o hello.o # 预编译,编译和汇编生成目标文件

链接:将目标文件和需要的库链接在一起,形成可执行文件。链接过程主要包括地址和空间分配、符号决议(符号绑定,symbol binding)和重定位等步骤。

第三章 目标文件里有什么

3.1 目标文件的格式

Linux系统中,目标文件(.o文件)和可执行文件的格式几乎是一样的,统称为ELF文件。

3.2 目标文件是什么样的

目标文件中按照不同的属性,分成不同的节(section),有时候也称为段(segment)。

3.4 ELF文件结构描述

ELF目标文件最前面是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件的各个段。下面是段表(Section Header Table),它描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限等。编译器、链接器和装载器都是依照段表来定位和访问各个段的属性的。段表在ELF文件中的位置由ELF文件头的“e_shoff”成员决定。

重定位表

比如.rel.text和.rel.data就是重定位表。链接器在处理目标文件时,需要对目标文件中的某些部位进行重定位(比如代码段和数据段中对绝对地址的引用的位置,需要更改)。这些重定位的信息都记录在ELF文件的重定位表中。

字符串表

.strtab是字符串表,用来保存普通的字符串,比如符号的名字;.shstrtab是段表字符串表(Section Header String Table),用来保存段表中用到的字符串,最常见的就是段名(sh_name)。Elf32_Ehdr中有一个成员“e_shstrndx”,表示.shstrtab表在段表中的下标,即段表字符串表在段表中的下标。

3.5 链接的接口——符号

在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名。符号的管理是链接中很重要的一环,每个目标文件中都有一个相应的符号表,这个表记录了目标文件中所用到的所有符号。

弱符号与强符号

编译器默认函数和初始化的全局变量为强符号,未初始化的全局变量为弱符号。链接器按照如下规则处理被多次定义的全局符号:

规则1:不允许强符号被多次定义。

规则2:如果一个符号在某个目标文件中是强符号,在其他文件中都是弱符号,那么选择强符号。

规则3:如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个。

第四章 静态链接

两步链接(Two-pass Linking),整个链接过程分两步。

第一步 空间与地址分配: 扫描所有的输入目标文件,并且获得它们的各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表。这一步中,链接器将能够获得所有输入目标文件的段长度,并且合并它们,计算出输出文件中各个段合并后的长度与位置,建立映射关系。

第二部 符号解析与重定位:读取输入文件中段的数据、重定位信息,并且进行符号解析与重定位、调整代码中的地址等。

第三部分 装载和动态链接

第七章 动态链接

7.1 为什么要动态链接

动态链接:等到程序运行时再对目标文件进行链接。可以解决静态链接造成多个目标文件副本浪费磁盘和内存空间的问题。同时动态链接对升级库和某个模块来说更友好,原理上来说只需要替换掉旧的目标文件即可。

Linux系统中,ELF动态链接文件被称为动态共享对象(DSO,Dynamic Shared Objects),简称共享对象,一般都是以.so为扩展名的一些文件。

Windows系统中,动态链接文件被称为动态链接库,通常是以.dll为扩展名的文件。

7.3 地址无关代码

装载时重定位

在链接时,对所有绝对地址的引用不作重定位,而是把这一步推迟到装载时再完成。一旦模块装载地址确定,即目标地址确定,那么系统就对程序中的所有绝对地址引用进行重定位。

装载时重定位遇到的问题:动态链接模块被装载映射到虚拟空间后,指令部分是在多个进程之间共享的,由于装载时重定位需要修改指令,所以没有办法做到同一份指令被多个进程共享,因为指令被重定位后对于每个进程来讲都是不同的。

GCC支持装载时重定位的方法,在产生共享对象时,使用-shared参数,就是使用该方法。

我们的目标很简单,希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中哪些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这种方案被称为地址无关代码(PIC,Position-independent Code)。

模块间数据访问:比如其他模块中定义的全局变量

其他模块的全局变量的地址是跟模块装载地址有关的,ELF的做法是在数据段中建立一个指向其他模块全局变量的指针数组,也被称为全局偏移表(GOT,Global Offset Table)。当代码需要引用该全局变量时,可以通过GOT中对应的表项间接引用。

7.4 延迟绑定(PLT)

在动态链接下,程序模块之间包含大量的函数引用,所以在程序开始执行前,动态链接会花费大量时间解决模块之前的函数引用的符号查找以及重定位。

延迟绑定(Lazy Binding):当函数第一次被用到时才进行绑定(符号查找、重定位等),如果没有用到则不进行绑定。可以大大加快程序的启动速度,特别有利于一些有大量函数引用和大量模块的程序。

ELF使用PLT(Procedure Linkage Table)的方法来实现延迟绑定。ELF将GOT拆分为两个表,分别是.got和.got.plt。其中.got用来保存全局变量引用的地址,.got.plt用来保存函数引用的地址。

7.5 动态链接相关结构

.interp段,interp是interpreter的缩写。.interp的内容是可执行文件所需要的动态链接器的路径。

.dynamic段,这个段里保存了动态链接器所需要的基本信息,比如依赖于哪些共享对象、动态链接符号表的位置、动态链接重定位表的位置、共享对象初始化代码的地址等。

动态符号表.dynsym,保存了与动态链接相关的符号。

动态链接重定位表

在静态链接中,目标文件里面包含专门用于重定位的重定位表,比如.rel.text表示代码段的重定位表,.rel.data表示数据段的重定位表。

动态链接的文件中,也有类似的重定位表分别叫做.rel.dyn和.rel.plt。.rel.dyn是对数据引用的修正,它所修正的位置位于.got以及数据段,.rel.plt是对函数引用的修正,它所修正的位置位于.got.plt。

7.6 动态链接的步骤和实现

动态链接的步骤基本上分为3步:启动动态链接器本身,装载所有需要的共享对象(.so文件),重定位和初始化。

后记

下面是我阅读该书的一些记录,方便多年以后的查看。
在这里插入图片描述

更多推荐