《程序员的自我修养》-读书笔记

2019-01-29  本文已影响0人  Michale_Zuo

第1章 温故知新

操作系统
  1. CPU

 分时系统:每个程序运行一段时间以后都主动让出CPU给其他程序,使得一段时间内每个程序都有机会运行一小段时间
 多任务:以进程为单位,操作系统统一分配,安好进程优先级进行资源分配
 抢占式:运行时间过长,系统会暂停该进程,资源重新分配

  1. 存储器
  1. 地址空间不隔离
  2. 内存使用效率低
  3. 程序运行的地址不确定

==分段==:把一段与程序需要的内存空间大小的虚拟空间映射到某个地址空间。问题1,3解决了,但是问题2没有解决
==分页==:把地址空间人为的等分成固定大小的页

  1. I/O设备
线程
- 多线程可以充分利用的等待的时间
- 程序逻辑本身要求并发操作
- 全面发挥多CPU或多核计算机的计算能力
- 相对于多进程应用,多线程在数据共享方面效率高

第2章 静态链接

 程序代码到执行起来大致分为四个步骤:预编译(Prepressing),编译(Compliation),汇编(Assembly),链接(Linking)

预编译

 主要处理源代码文件中的以#开始的预编译指令

编译

 预编译处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件

汇编

 将汇编代码转变成机器可以执行的指令。输出目标文件

链接

 模块组装过程。主要包括:地址和空间分配,符号决议,重定位。

静态链接的过程


Snip20181209_1.png

第3章 目标文件

程序指令和程序数据分开的好处:

  1. 方便设置权限,防止程序指令被修改
  2. 提高缓存命中率
  3. 内存共享
ELF文件结构
Snip20181210_9.png
链接的接口--符号

 链接的过程的本质就是把多个不同的目标文件相互"黏"在一起(也就是目标文件之间对地址的引用)。函数和变量统称为符号,函数名或变量名为符号名。每个符号在符号表中都有唯一的值(符号值)。
 函数签名是用来识别不同函数的,包含函数的信息,函数名,参数类型,所在类,名称控件,其他信息。
 符号重定义:多个目标文件含有相同名字全局符号的定义。编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为若符号

调试信息

 目标文件里面可能保存调试信息
 ECL有一种DEARF的标准调试信息格式,在Xcode使用instrument的时候,如果出现了16进制的看不懂的文件或者函数名,就需要在debug里设置这个文件格式了。


第4章 静态链接

空间和地址分配

 当有多个文件的时候,在静态链接过程中采用的是两步链接的方法

  1. 空间和地址分配,相似段进行叠加
  2. 符号解析和重定位,上一步收到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址
符号解析和重定位(静态链接的核心)

 以32位Inter x86处理器来说,重定位表结构是一个结构体。包含重定位入口的偏移和重定位入口的类型和符号。

那么为什么缺少符号的定义会导致链接错误?

 重定位伴随着符号的解析过程,因为每个目标文件可能自身会定义符号或者引用其他文件的符号。重定义的入口都是对符号的引用,链接器需要确定符号的目标地址,然后去全局符号表中查找,进行重定位。如果符号在表中找不到那么就会报错啦。

静态库链接

 程序之所以有用,因为它有输入输出。一个程序如何做到输入输出了?最简单的是使用操作系统提供的API(应用程序编程接口)
 静态库可以简单的看成一组目标文件的集合。对库的链接就是将库中的目标文件进行链接,但是库里面一个目标文件只包含一个函数,只链接需要的目标文件到输出文件中,尽量减少空间的浪费。


第6章 可执行文件的装载与进程

可执行文件只有装载到内存以后才会被CPU执行,每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间。操作系统可以通过包括进程的虚拟空间监管运行中的程序。进程的虚拟空间会分为两部分:操作系统使用部分用户程序使用部分
 为了有效利用内存,我们可以将进行动态载入,把程序最常用的部分驻在内存中,不太常用的数据放在磁盘里。

覆盖装入页映射是典型的动态装载方法。

 进程的最关键特征是拥有独立的虚拟地址空间。
 一个程序执行创建新的进程的过程需要做三件事情

  1. 创建一个独立的虚拟地址空间(映射函数所需的数据结构)
  2. 读取可执行文件头,并且建立虚拟空间与可执行文件的map关系(可以理解为"装载")
  3. CPU的指令寄存器设置成可执行文件的入口地址,然后启动运行。
Linux装载ELF过程
Linux-ELF.jpg
Windows PE的装载
  1. 读取文件的第一页
  2. 检查进程地址空间
  3. map PE文件中的段
  4. 检查目标地址,不是的话就行rebaseing(基址重置)
  5. 装载所需DLL文件
  6. 解析导入符号
  7. 建立初始化栈和堆
  8. 建立并启动主线程

第7章 动态链接

 静态链接存在两个主要问题

  1. 浪费内存,磁盘空间
  2. 模块更新困难等问题
    为了解决这两个问题,出现了动态链接
动态链接

 动态链接的基本思想是把程序按照模块拆分成相对独立部分,在程序运行时才连接在一起形成一个完整的程序。
 下图是一个很简单的程序编译链接成输出文件的例子,和静态链接不同的是lib.o文件通过运行时库链接成.so对象,.so对象和.program1.o进行链接。

dynamic-load.jpg

 需要注意的是:共享对象的最终装载地址在编译时期是不确定的

 解决动态模块中绝对地址的引用问题有以下两个方法:

  1. 装载重定位,缺点是指令部分无法在多个进程之间共享。
  2. 地址无关代码,就是把指令中需要修改的部分分离出来,跟数据部分分在一起。

 动态链接相比静态有一定的性能问题
 ①动态链接对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址,程序的运行速度减慢
 ②动态链接的链接工作是在运行时完成。ELF采用了延迟绑定的技术来优化动态链接的性能。

  动态链接和静态链接一样都要经过
 ①操作系统读取可执行文件的头部,检查文件的合法性
 ②头部中的Program Header中读取每个segment的虚拟地址,文件地址和属性,并且map到进程虚拟空间的相对位置。不同的地方在于动态链接多了一个步骤,会启动一个动态连接器进行动态链接的工作。这个链接器本身是共享对象,它的位置由ELF决定。
动态链接步骤:

  1. 启动动态链接器本身
  2. 装载所有需要的共享对象
  3. 重定位和初始化(遍历重定位表,进行位置修正)

第10章 内存

 内存是承载程序运行的介质,是程序进行运算和表达的场所。
 一般来讲,应用程序有

  1. 栈:维护函数调用的上下文,地址为高地址,向低地址增长,i386下,栈顶由esp寄存器进行定位的,压栈的操作会使栈顶的地址减小,出栈是地址增大。
     栈还保存了一个函数调用所需要的维护信息(堆栈帧)
1.函数的返回地址和参数
2.临时变量
3.保存的上下文
  1. 堆:容纳应用程序动态分配的内存区域,向高地址增长(Windows里大部分使用HeapCreate产生,但是这个函数不遵守向上增长的规律),这块内存在程序主动放弃之前都会一直保持有效。现代内存管理一般都是页式(空间大小必须是页的整数倍)
     堆的空间分配算法主要分为:空闲链表和位图
  1. 可执行文件映像:存贮可执行文件在内中里的映像
  2. 保留区:内存中受到保护的区域的总称。

第11章 运行库

 一个典型的程序运行大致步骤:

  1. 创建进程,入口函数得到控制权
  2. 初始化运行库和运行环境
  3. 调用main函数
  4. 返回入口函数进行清理工作
入口函数(以MSVC为例)
  1. 初始化和OS版本有关的全局变量
  2. 初始化堆
  3. 初始化I/O
  4. 获取命令行参数和环境变量
  5. 初始化C库的一些数据
  6. 调用main并记录返回值
  7. 检查错误并将main的返回值返回
上一篇下一篇

猜你喜欢

热点阅读