《程序员的自我修养--链接,装载与库》

2024-01-07 20:57:34

第一章:温故而知新

过度优化的问题:

我们知道volatile关键字可以阻止过度优化,因为它可以完成两件事:

  • 阻止编译器为了提高速度将一个变量缓存到寄存器而不写回
  • 阻止编译器调整操作volatile变量的指令顺序

然而,在优化这一块,不仅编译器会做优化,CPU也会做优化。volatile就管不着了CPU了。
经典的例子当然是单例模式。单例模式有一种常规的解决方案是DCL,也就是双重检查锁,但是在C++中new的步骤有是分为三个步骤:分配内存,调用构造函数,将内存地址用指针保存下来。CPU就要来搞怪,将第二步第三步乱个序。
一种解决方案是:调用CPU提供的barrier指令阻止将barrier指令之前的代码交换到barrier之后。但是这种方案不具有可移植性。部分实现代码如下:

if(!pInst){
	lock();
	if(!pInst){
		T* temp = new T;
		barrier();
		pInst = temp;
	}
	unlock();
}

线程:

是程序执行流的最小单元。通常意义上,一个进程由多个线程组成,各个线程之间共享程序的内部空间(包括代码段,数据段,堆等)及一些进程级的资源(如打开文件和信号)

线程的访问权限:

线程调度与优先级

  • 运行(Running):此时线程正在执行
  • 就绪(Ready):此时线程可以立刻运行,但CPU已经被占用
  • 等待(Waiting):此时线程正在等待某一件事件发生,无法执行

可抢占线程和不可抢占线程

线程在用尽时间片之后会被强制剥夺继续执行的权利,而进入就绪状态,这个过程叫做抢占,即之后执行的别的线程抢占了当前线程。

在早期的一些系统中,线程是不可抢占的。线程必须手动发出一个放弃执行的命令,才能让其他的线程得到执行。可以避免一些因为抢占式线程里调度时机不确定而产生的问题。非抢占式线程已经十分少见

写时复制(Copy on Write,COW)

指的是两个任务可以同时自由地读取内存,担任一一个任务试图对内存进行修改时,内存就会复制一份提供给修改方单独使用

线程安全

信号量:

一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行一下操作:

最后一条应该是错了,应该是信号量不小于1,唤醒一个等待中的线程?

可重入(Reentrant)与线程安全

静态链接::第二章:编译与链接

被隐藏了的过程

通常将编译和链接合并到一起的过程称为构建(Build)

一个程序从代码到可以运行经过了四个步骤:预处理(Prepressing),编译(Compilation),汇编(Assembly),链接(Linking).c -> .i -> .s ->.o -> .exe

预编译

预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include”,“#define”等

编译

编译过程就是把预处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生产相应的汇编代码文件。

实际上 gcc 这个命令只是后台程序的包装,它会根据不同的参数要求调用预编译编译程序 cc1,汇编器 as,链接器 ld

词法分析:?

首先源代码程序被输入到扫描器,进行简单的词法分析,运用一种类似于有限状态机的算法将源代码的字符序列分割成一系列的记号(一般有关键字,标识符,字面量,特殊符号)

语法分析:

语法分析器将对由扫描器产生的记号进行语法分析,产生语法树,采用了上下文无关语法的分析手段。

语义分析:

???????编译器所能分析的语义是静态语义,所谓静态语义是指在编译器可以确定的语义,与之对应的动态语义就是只有在运行期才能确定的语义

主要检查声明和定义,类型转换是否出错等。做完这一步之后,语法树的每个节点都会有对应的类型。这一步还会对符号表里的符号进行更新。

中间语言生成?

图中(2+6)被优化成 8

目标代码生成与优化

主要是生成汇编代码,以及对汇编代码的优化。比如说,在优化这个阶段,可能导致指令重排序,这就引发了单例的一些问题。

汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。

到这一步为止,输出的是目标文件(Object File)

链接

编译器前端负责生成和机器无关的中间代码;后端负责将中间代码转化为机器目标代码。这样对于一些可跨平台的编译器而言,他们可以针对不同的平台使用相同的前端,而针对不同的机器平台有数个后端。

链接是这本书的主题。语言的发展是从编写一个代码文件到多个模块文件编写的,因此要让各个模块协同工作,就需要使用链接器。重要的一点是:在未链接之前,目标文件中的一些变量、函数地址是未决的(或者说可以这么理解:编译一个文件,可能有些变量、函数的地址确定不了,是个待定值),链接器就是来干这个事的:把一些指令对其他符号地址的引用加以修正。主要包括:地址和空间分配、符号决议和重定位。

静态链接::第三章:目标文件里有什么

目标文件的格式

  1. 可执行文件:Windows下的PE、Linux下的ELF
  2. 动态链接库:Windows的.dll,Linux的.os;静态链接库:Windows的.lib,Linux的.a
  3. ELF文件类型分类:1.可重定位文件;2.可执行文件;3.共享目标文件;4.核心转储文件
  4. 已初始化的全局变量和局部变量数据经常放在数据段.data,未初始化的全局变量和局部静态变量放在.bss段;编译后的机器指令经常放在代码段.code或.text;.rodata段存放的是只读数据,一般是程序里面的只读变量和字符串常量。.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,int a=0会放在.bss段。
  5. 总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据

6.ELF文件头包含:ELF魔数(魔数--用来确认文件的类型,操作系统在加载可执行文件的时候会确认魔数是否正确,如果不正确会拒绝加载)、文件机器字节长度、段表的位置、程序头入口和长度、段的数量、文件是否可执行、目标硬件、目标操作系统等

7.数据和指令分段的好处:

  • 不同段可以设置读写权限,可以防止程序被有意或者无意的修改
  • 提高CPU的缓存命中率
  • 当系统中存在多个该程序的副本,他们的指令都一样,那么内存中只需保存一份该程序的指令

8.段表:描述文件中各个段的数组,放置段的名称、段的长度、段类型、段在文件中的偏移位置、读写权限、段的属性等。编译器,链接器和装载器都是依靠段表来定位和访问各个段的属性的

9.重定位表:在处理目标文件时,必须要对目标文件中某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置。这些重定位信息都记录在ELF文件的重定位表里面。比如说数据的重定位表对应.rel.data

10.字符串表:因为字符串长度往往是不定的,固定结构存储较为困难。所以字符串表就是解决这个问题,使用字符串在表中的偏移来引用字符串。

11.链接过程的本质就是把多个不同的目标文件之间相互”粘“到一起,实际上拼合的是目标文件之间对地址的引用,即对函数和变量的地址的引用。我们可以将符号看作是链接中的粘合剂,整个连接过程正式基于符号才能够正确完成。每一个目标文件都有一个相应的符号表,这个表里面记录了目标文件中所用的所有符号。每个定义的符号有一个对应的值,叫做符号值。对于变量和函数来说,符号之就是它们的地址。

12.对符号进行分类:全局符号、局部符号、外部符号、段名、行号。链接只关注全局符号。

13.符号表中存放的内容有:编号、值、内存大小、类型、作用域、符号所属段、符号名字

14.特殊符号:一些内置的符号,比如_executable_start表示程序起始地址

15.函数签名:函数名、参数类型、所在类和名称空间。GCC的名称修饰方法就是:_Z + N(如果是嵌套)+n(后面的字符串长度)+str+E(结尾),比如N::C::func(int)经过名字修饰之后变成:_ZN1N1C4funcEi。由于不同编译器采用不同的名字修饰方法,必然导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一。

16.对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。

17.针对强弱符号的概念,编译器就会按如下规则处理与选择多次定义的全局符号:

  • 不允许强符号被多次定义
  • 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号
  • 如果在多个文件中都是弱符号,那么选择其中占用空间最大的一个
    ?

第四章:静态链接

空间与地址分配:

对于多个输入目标文件,链接器如何将它们的各个段合并到输出文件?或者说,输出文件中的空间如何分配给输入文件?

按序叠加

最蠢的做法是顺序叠加多个目标文件的段,但是会存在内存对齐导致输出文件过大的情况;所以,大多数做法是采用相似段合并的策略。

相似段合并

将相同性质的段合并到一起,比如将所有输入文件的".text"合并到输出文件的".text"段

“链接器为目标文件分配地址和空间”这句话中的“地址和空间”有两个含义:第一个是在输出的可执行文件中的空间,第二个是在装载后的虚拟地址中的虚拟地址空间

两步链接:

第一步 空间与地址分配?

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

第二步? 符号解析与重定位

使用上面第一步中收集到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址等。事实上第二步是链接过程的核心,特别是重定位过程

符号解析与重定位

1.在未链接之前,目标文件中使用的其他编译单元的变量的地址未定义,会用0来给他占位,并且将这个未定义变量放进重定位表(专门用来保存与重定位相关的信息)中。

2.目前的链接器本身并不支持符号的类型,即变量类型对于链接器来说是透明的,它只知道一个符号的名字,并不知道类型是否一致。这就会导致由多个弱符号出现的时候的一些问题。COMMOM块来解决这些问题:当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那块为准。

3.C++中模板导致的问题:一个模板在一个编译单元内进行了实例化,在另外一个单元可能也进行了实例化,当这两个单元链接的时候就会出现重复代码,进而导致空间浪费、地址出错、指令运行效率低的问题。一个比较有效的做法是将每个模板的实例化代码都单独存放在一个段里,每个段包含一个模板实例。同理,对于虚函数,内联函数,默认构造函数,默认拷贝构造函数也会存在这样的问题,解决方案都差不多。

4.main函数之前执行全局构造,对应汇编代码.init段;main函数之后执行全局析构,对应汇编代码.fint段

5.ABI:Application Binary Interfacd,对应API,层次不同,ABI主要内容是符号修饰标准、变量内存布局、函数调用方式等内容。C++一直被人诟病的一大原因是它的二进制兼容性不好。

?静态库链接

一个静态库可以简单地看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。但事实上也是这样,比如libc.a就是由许多.o文件打包而成的。

链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所需要的文件。一般链接器有以下三种方法:

  • ld命令行指定参数,之前使用的 ld 的 -o,-e 参数就属于这类
  • 目标文件中放置指令
  • 链接控制脚本

BFD库是一个GNU项目,它的目标是希望通过一种统一的接口来处理不同的目标文件格式。

参考文章:《程序员的自我修养》笔记_程序员的自我修养 写时复制-CSDN博客

文章来源:https://blog.csdn.net/zaizai1007/article/details/135429866
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。