《程序员的自我修养》-读书笔记
第1章 温故知新
操作系统
-
操作系统的两个主要功能:提供抽象接口和管理硬件资源
-
硬件资源
- CPU
分时系统:每个程序运行一段时间以后都主动让出CPU给其他程序,使得一段时间内每个程序都有机会运行一小段时间
多任务:以进程为单位,操作系统统一分配,安好进程优先级进行资源分配
抢占式:运行时间过长,系统会暂停该进程,资源重新分配
- 存储器
- 内存
早期的计算机,程序是直接运行到物理内存上的,这样产生了三个问题
- 地址空间不隔离
- 内存使用效率低
- 程序运行的地址不确定
==分段==:把一段与程序需要的内存空间大小的虚拟空间映射到某个地址空间。问题1,3解决了,但是问题2没有解决
==分页==:把地址空间人为的等分成固定大小的页
- I/O设备
线程
- 线程是程序执行流的最小单元。包含线程ID,当前指令指针(PC),寄存器集合和堆栈。一般情况下,一个进程包含一个或多个线程,线程之间共享内存空间,和进程级别的资源。
- 为什么使用多线程
- 多线程可以充分利用的等待的时间 - 程序逻辑本身要求并发操作 - 全面发挥多CPU或多核计算机的计算能力 - 相对于多进程应用,多线程在数据共享方面效率高
- 线程调度:就是不断在处理器上 切换不同的线程的行为分为运行,就绪,等待三种状态,运行中的线程有一段可以执行的时间,这叫做时间片。
- 线程安全:单指令操作->原子性。比较常见的做法是加锁lock
第2章 静态链接
程序代码到执行起来大致分为四个步骤:预编译(Prepressing),编译(Compliation),汇编(Assembly),链接(Linking)
预编译
主要处理源代码文件中的以#开始的预编译指令
编译
预编译处理完的文件进行一系列词法分析,语法分析,语义分析及优化后生成相应的汇编代码文件
汇编
将汇编代码转变成机器可以执行的指令。输出目标文件
链接
模块组装过程。主要包括:地址和空间分配,符号决议,重定位。
- 重定位:重新计算各个目标的地址过程
静态链接的过程
Snip20181209_1.png
第3章 目标文件
- 格式:Windows为PE,Linux为ELF。另外动态链接库和静态链接库文件都按照可执行文件格式存储
- 目标文件将信息按照不同的属性按照段(Segment)形式存储.
- 总体来讲,程序源代码经过编译以后主要分为两种段:程序指令和程序数据。
- 程序指令:代码段,常见的名字有.code或.text
- 程序数据:数据段和.bss段
程序指令和程序数据分开的好处:
- 方便设置权限,防止程序指令被修改
- 提高缓存命中率
- 内存共享
ELF文件结构
Snip20181210_9.png链接的接口--符号
链接的过程的本质就是把多个不同的目标文件相互"黏"在一起(也就是目标文件之间对地址的引用)。函数和变量统称为符号,函数名或变量名为符号名。每个符号在符号表中都有唯一的值(符号值)。
函数签名是用来识别不同函数的,包含函数的信息,函数名,参数类型,所在类,名称控件,其他信息。
符号重定义:多个目标文件含有相同名字全局符号的定义。编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为若符号
调试信息
目标文件里面可能保存调试信息
ECL有一种DEARF的标准调试信息格式,在Xcode使用instrument的时候,如果出现了16进制的看不懂的文件或者函数名,就需要在debug里设置这个文件格式了。
第4章 静态链接
空间和地址分配
当有多个文件的时候,在静态链接过程中采用的是两步链接的方法
- 空间和地址分配,相似段进行叠加
- 符号解析和重定位,上一步收到的所有信息,读取输入文件中段的数据,重定位信息,并且进行符号解析与重定位,调整代码中的地址
符号解析和重定位(静态链接的核心)
- 重定位
链接器在完成地址和空间分配以后就已经可以确定所有符号的虚拟地址,根据这个地址对每个需要重定位的指令进行地位修正,在ELF文件中有一个重定位表来进行重定位相关信息的保存。
以32位Inter x86处理器来说,重定位表结构是一个结构体。包含重定位入口的偏移和重定位入口的类型和符号。
- 符号解析
在编程过程中经常会遇到类似undefined reference to xxx
的错误,就是链接时符号未定义。导致这个问题可能会是没有导入某个库,输入目标文件路径不正确或者接口与实现不一致。
那么为什么缺少符号的定义会导致链接错误?
重定位伴随着符号的解析过程,因为每个目标文件可能自身会定义符号或者引用其他文件的符号。重定义的入口都是对符号的引用,链接器需要确定符号的目标地址,然后去全局符号表中查找,进行重定位。如果符号在表中找不到那么就会报错啦。
静态库链接
程序之所以有用,因为它有输入输出。一个程序如何做到输入输出了?最简单的是使用操作系统提供的API(应用程序编程接口)
静态库可以简单的看成一组目标文件的集合。对库的链接就是将库中的目标文件进行链接,但是库里面一个目标文件只包含一个函数,只链接需要的目标文件到输出文件中,尽量减少空间的浪费。
第6章 可执行文件的装载与进程
可执行文件只有装载到内存以后才会被CPU执行,每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间。操作系统可以通过包括进程的虚拟空间监管运行中的程序。进程的虚拟空间会分为两部分:操作系统使用部分和用户程序使用部分。
为了有效利用内存,我们可以将进行动态载入,把程序最常用的部分驻在内存中,不太常用的数据放在磁盘里。
覆盖装入和页映射是典型的动态装载方法。
- 覆盖装入:是将不会相互调用的模块进行内存共享。目前基本已经淘汰。
- 页映射:将内存和所有磁盘中的数据和指令按照页为单位来划分,所有的装载和操作的单位就是页。操作系统管理内存的调配。
进程的最关键特征是拥有独立的虚拟地址空间。
一个程序执行创建新的进程的过程需要做三件事情
- 创建一个独立的虚拟地址空间(映射函数所需的数据结构)
- 读取可执行文件头,并且建立虚拟空间与可执行文件的map关系(可以理解为"装载")
- CPU的指令寄存器设置成可执行文件的入口地址,然后启动运行。
Linux装载ELF过程
Linux-ELF.jpgWindows PE的装载
- 读取文件的第一页
- 检查进程地址空间
- map PE文件中的段
- 检查目标地址,不是的话就行rebaseing(基址重置)
- 装载所需DLL文件
- 解析导入符号
- 建立初始化栈和堆
- 建立并启动主线程
第7章 动态链接
静态链接存在两个主要问题
- 浪费内存,磁盘空间
- 模块更新困难等问题
为了解决这两个问题,出现了动态链接
动态链接
动态链接的基本思想是把程序按照模块拆分成相对独立部分,在程序运行时才连接在一起形成一个完整的程序。
下图是一个很简单的程序编译链接成输出文件的例子,和静态链接不同的是lib.o文件通过运行时库链接成.so对象,.so对象和.program1.o进行链接。
需要注意的是:共享对象的最终装载地址在编译时期是不确定的
解决动态模块中绝对地址的引用问题有以下两个方法:
- 装载重定位,缺点是指令部分无法在多个进程之间共享。
- 地址无关代码,就是把指令中需要修改的部分分离出来,跟数据部分分在一起。
动态链接相比静态有一定的性能问题
①动态链接对于全局和静态的数据访问都要进行复杂的GOT定位,然后间接寻址,程序的运行速度减慢
②动态链接的链接工作是在运行时完成。ELF采用了延迟绑定的技术来优化动态链接的性能。
动态链接和静态链接一样都要经过
①操作系统读取可执行文件的头部,检查文件的合法性
②头部中的Program Header中读取每个segment的虚拟地址,文件地址和属性,并且map到进程虚拟空间的相对位置。不同的地方在于动态链接多了一个步骤,会启动一个动态连接器进行动态链接的工作。这个链接器本身是共享对象,它的位置由ELF决定。
动态链接步骤:
- 启动动态链接器本身
- 装载所有需要的共享对象
- 重定位和初始化(遍历重定位表,进行位置修正)
第10章 内存
内存是承载程序运行的介质,是程序进行运算和表达的场所。
一般来讲,应用程序有
- 栈:维护函数调用的上下文,地址为高地址,向低地址增长,i386下,栈顶由esp寄存器进行定位的,压栈的操作会使栈顶的地址减小,出栈是地址增大。
栈还保存了一个函数调用所需要的维护信息(堆栈帧)
1.函数的返回地址和参数 2.临时变量 3.保存的上下文
- 堆:容纳应用程序动态分配的内存区域,向高地址增长(Windows里大部分使用HeapCreate产生,但是这个函数不遵守向上增长的规律),这块内存在程序主动放弃之前都会一直保持有效。现代内存管理一般都是页式(空间大小必须是页的整数倍)
堆的空间分配算法主要分为:空闲链表和位图
- 可执行文件映像:存贮可执行文件在内中里的映像
- 保留区:内存中受到保护的区域的总称。
第11章 运行库
一个典型的程序运行大致步骤:
- 创建进程,入口函数得到控制权
- 初始化运行库和运行环境
- 调用main函数
- 返回入口函数进行清理工作
入口函数(以MSVC为例)
- 初始化和OS版本有关的全局变量
- 初始化堆
- 初始化I/O
- 获取命令行参数和环境变量
- 初始化C库的一些数据
- 调用main并记录返回值
- 检查错误并将main的返回值返回