【读过的书,留下的迹】深入理解计算机系统

来源:互联网 发布:windows 10通知模式 编辑:程序博客网 时间:2024/05/17 04:48

前言

  最近发现有时候看完一本书,时间久了容易忘记,看书不总结思考效果大打折扣,故打算写这一系列文章,一是为了整理书中的要点,帮助自己消化理解;二是勉励自己多看书思考。文章中不会把书中内容讲解的非常详细,只是总结概括,适合已经阅读过该书的读者。

第2章:信息的表示和处理

(1)信息存储

大多数计算机使用8位的块,或者字节(byte),作为最小的可寻址的存储器单位,而不是在存储器中访问单独的位。一个字节由8位组成,二进制中的范围是:00000000~11111111,十进制中的范围是0~255,十六进制中的范围是00~FF。在C语言中,以0x或者0X开头的都是十六进制

在C语言中,不同的数据类型分配的字节数时不同的,如下表所示:

C声明 32位机器 64位机器 char 1 1 int 4 4 short int 2 2 long int 4 8 float 4 4 double 8 8 char * 4 8

排列表示一个对象有两个通用的方法,最低有效字节在最前面的方式,称为小端法(litte endian)。最高有效字节在最前面的方式,称为大端法(big endian)。大多数Intel兼容机都采用了小端法,不同的数据值的字节表示,在不同的机器中,除了字节顺序以外,结果都是一样的。但是指针值域机器时相关的(32位、64位区别)。

在C语言中,有以下几种运算:

  • 位级运算:| 就是OR,& 就是AND,~ 就是NOT,^ 就是EXCLUSIVE-OR
  • 逻辑运算:||,&&,!
  • 移位运算:<< 左移位,>> 右移位

其中,在右移位中,分为:

  • 逻辑右移,即在左端补0
  • 算术右移,即在左端补最高位

(2)整数表示

用位来编码整数有两种不同的方式:一种只能表示非负数,即无符号数;另一种能表示负数,零和正数。C和C++均支持有符号和无符号数,Java仅支持有符号数。

  • 无符号数的编码:将位看作是二进制表达式。
  • 补码编码:同样将位看作是二进制表达式,但最高位的权重是-1。用来表示有符号数。

有符号数的其他表示方法还有反码和原码,但这两种方法对0的编码方式有两种不同的方式。C语言标准并没有要求用补码形式表示有符号数,但是几乎所有的机器都是这么做的。在Java中,单字节数据类型称为byte,而不是char,而且没有long long数据类型。这些非常具体的要求都是为了保证无论在什么机器上,Java程序运行的表现都能完全一样。

对大多数C语言的实现而言,处理同样字长的有符号数和无符号数之间相互转换的一般规则是:数值可能变化,但是位模式不变。另外,在执行一个运算时,如果它的一个运算数是有符号的,而另一个是无符号的,那么C语言会隐式地将有符号参数强制转换为无符号数。

数字的扩展和截断有以下几种方法:

  • 零扩展:将一个无符号数转换为一个更大的数据类型,只需简单地在表示的开头添加0
  • 符号扩展:将一个补码数字转换为一个更大的数据类型,是在表示中添加最高有效位的值的副本
  • 截断:丢弃最高若干位。

(3)浮点数

IEEE浮点标准用V=(-1)^s*M*2^E的形式来表示一个数

  • 符号(sign)s决定这个数是负数还是正数
  • 尾数(significand)M是一个二进制小数
  • 阶码(expoent)E的作用是对浮点数加权

浮点数的位表示划分为三个字段,分别对这些值进行编码:

  • 一个单独的符号位s直接编码符号s
  • k位的阶码子短exp=e_k-1…e_1e_0编码阶码E
  • n位小数字段frac=f_n-1…f_1f_0编码尾数M

在单精度浮点格式(C语言中的float)中,s、exp和frac字段分别为1位、k=8位和n=23位,得到一个32位的表示;在双精度浮点格式(C语言中的double)中,s、exp和frac字段分别为1位、k=11位和n=52位,得到一个64位的表示。

  • 情况1:规格化的值

    • 当exp的位模式既不全为0,也不全为1时,属于这种情况。阶码的值是E=e-Bias,其中e是无符号数,其位表示为e_k-1…e_1e_0,而Bias是一个等于2^(k-1)-1的偏置值;尾数定义为M=1+f,f=0.f_n-1…f_1f_0。
  • 情况2:非规格化的值

    • 当阶码域为全0时,所表示的数就是非规格化形式。在这种情况下,阶码值是E=1-Bias,而尾数的值是M=f。
  • 情况3:特殊值

    • 当阶码全为1的时候,小数域全为0,得到的值表示无穷;当小数域为非0时,结果被称为NaN,即不是一个数。

第3章:程序的机器级表示

程序编码

  • 假设有两个文件p1.c和p2.c。在一台IA32机器上,用gcc编译时经过以下过程。
    • C预处理器扩展源代码,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏
    • 编译器产生两个源代码的汇编代码,名字分别为p1.s和p2.s
    • 汇编器将汇编代码转化成二进制目标代码文件名为p1.o和p2.o
    • 链接器将两个目标代码文件与实现库函数的代码合并,并产生最终的可执行代码文件p。

(1)机器级代码

汇编代码表示非常接近于机器代码。与机器代码的二进制格式相比,汇编代码有一个的主要的特点,即它用可读性更好的文本格式来表示。

  • 程序计数器(在IA32中,通常称为“PC”,用%eip表示)指示将要执行的下一条指令在存储器中的地址
  • 整数寄存器(register)文件包含8个命名的位置,分别存储32位的值。这些寄存器可以存储地址或整数数据。
  • 条件码寄存器保存着最近执行的算术或逻辑指令的状态信息。
  • 一组浮点寄存器存放浮点数据。

数据格式

大多数gcc生成的汇编代码指令都有一个字符后缀,表明操作数的大小,如下图所示:



访问信息

一个IA32中央处理单元(CPU)包含一组8个存储32位值的寄存器。前6个寄存器都可以看成通用寄存器,对它们的使用没有限制。最后两个寄存器(%ebp和%esp)保存着指向程序栈中重要位置的指针。



(1)操作数指示符

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。第一种类型是立即数(immediate),第二种类型是寄存器(register),第三种类型是存储器(memory)引用。用语法Imm(Eb,Ei,s)表示的是最常用的形式,由四个组成部分:一个立即数偏移Imm,一个基址寄存器Eb,一个变址寄存器Ei和一个比例因子s,有效地址被计算为Imm+R[Eb]+R[Ei]*s。



(2)数据传送指令

将数据从一个位置复制到另一个位置的指令是最频繁使用的指令。用符号位扩展,目的位置的所有高位用源值的最高位数值进行填充。用零扩展,所有高位都用零填充。



算术和逻辑操作



控制

(1)条件码

除了整数寄存器,CPU还维护着一组单个位的条件码(condition code)寄存器。它们描述了最近的算术或逻辑操作的属性。比较和测试指令,这些指令不修改任何寄存器的值,只设置条件码



(2)访问条件码



(3)跳转指令及其编码

正常执行的情况下,指令按照它们出现的顺序一条一条地执行。跳转(jump)指令会导致执行切换到程序中一个全新的位置。下表中所示的其他跳转指令都是有条件的——它们根据条件代码的某个组合,或者跳转,或者继续执行下一条指令。跳转指令最常用的都是程序计数器相关的(PC-relative),它们会将目标指令的地址与紧跟在跳转指令后面那条指令的地址之间的差作为编码。



过程

(1)栈帧结构

大多数机器,包括IA32,只提供转移控制到过程和从过程中转移出控制这种简单的指令。数据传递、局部变量的分配和释放通过操作程序栈来实现。为单个过程分配的那部分栈称为栈帧(stack frame)。



(2)寄存器使用惯例

根据惯例,寄存器%eax、%edx和%ecx被划分为调用者保存寄存器。当过程P调用Q时,Q可以覆盖这些寄存器,而不会破坏任何P所需要的数据。另一方面,寄存器%ebx、%esi和%edi被划分为被调用者保存寄存器。这意味着Q必须在覆盖这些寄存器的值值钱,先把它们保存在栈中,并在返回前恢复它们。

(3)过程实例



(7)x86-64:将IA32扩展到64位

x86-64代码与为IA32机器生成的代码有极大的不同。主要特性如下:

  • 指针和长整数是64位长。整数算术运算支持8、16、32和64位数据类型。
  • 通用目的寄存器组从8个扩展到16个
  • 许多程序状态都保存在寄存器中,而不是栈上。整型和指针类型的过程参数(最多6个)通过寄存器传递。有些过程根本不需要访问栈
  • 如果可能,条件操作用条件传送指令实现,会得到比传统分支代码更好的性能
  • 浮点操作用面相寄存器的指令集来实现,而不是用IA32支持的基于栈的方法来实现

第4章:处理器体系结构

程序编码

一个处理器支持的指令和指令的字节级编码称为它的指令集体系结构(Instruction-Set Architecture, ISA)。下面将定义一个指令集体系结构,命名为Y86,包括定义各种状态元素、指令集和它们的编码、一组编程规范和异常事件处理。

(1)程序员可见状态

Y86程序中的每条指令都会读取或修改处理器状态的某些部分,这称为程序员可见状态,如下图。



(2)Y86指令编码

下图是Y86的指令集,是我们处理器实现的目标。



上图给出了指令的字节级编码。每条指令需要1~6个字节不等,这取决于需要哪些字段。每条指令的第一个字节表明指令的类型。这个字节分为两个部分,每个部分4位:高4位是代码(code)部分,低4位是功能(function)部分。8个程序寄存器中每个都有相应的0~7的寄存器标识符(register ID),如下图所示。



逻辑设计和硬件控制

要实现一个数字系统需要三个主要的组成部分:计算对位进行操作的函数的组合逻辑、存储位的存储器元素,以及控制存储器元素更新的时钟信号。

(1)逻辑门

逻辑门是数字电路的基本计算元素。它们产生的输出,等于它们输入位值的某个布尔函数。下图是布尔函数AND、OR和NOT的标准符号。



(2)组合电路

将很多逻辑门组合成一个网,就能构建计算块(computational block),称为组合电路(combinational circuits)。构建这些网有两条限制:

  • 两个或多个逻辑门的输出不能连接在一起。否则它们可能会使线上的信号矛盾,可能会导致一个不合法的电压或电路故障;

  • 这个网必须是无环的。

检测位相等的组合电路。

bool eq = (a && b) || (!a && !b)



单个位的多路复用器电路。

bool out = (s && a) || (!s && b)



算术/逻辑单元(ALU)也是一很重要的组合电路



Y86的顺序实现

(1)组织成阶段

通常,处理一条指令包括很多操作,将它们组织成某个特殊的阶段序列,即使指令的动作差异很大,但所有的指令都遵循统一的序列。下面是关于各个阶段以及各阶段内执行操作的简略描述:

  • 取指(fetch):取指阶段从存储器读取指令字节,地址为程序计数器(PC)的值
  • 译码(decode):译码阶段从寄存器文件读入最多两个操作数。
  • 执行(execute):在执行阶段,算术/逻辑单元(ALU)执行指令指明的操作
  • 访存(memory):访存阶段可以将数据写入存储器
  • 写回(write back):写回结果到寄存器文件
  • 更新PC(PC update):将PC设置成下一条指令的地址

下面是各个指令具体的实现:









(2)硬件实现

通过将执行每条不同指令所需的步骤组织成一个统一的流程,就可以用少量的各种硬件单元以及一个时钟来控制计算的顺序,从而实现整个处理器。



Y86的流水线实现

在各个阶段之间插入流水线寄存器,可实现流水线Y86。但是流水线存在以下问题。当相邻指令间存在相关时会导致出现问题,有两种形式:(1)数据相关,下一条指令会用到这一条指令计算出的结果;(2)控制相关,一条指令要确定下一条指令的位置。

对于数据相关的数据冒险,一般有以下的解决方法:

  • 用暂停来避免数据冒险:处理器会停止流水线中一条或多条指令,直到冒险条件不再满足
  • 用转发来避免数据冒险:将结果值直接从一个流水线阶段传到较早阶段
  • 加载/使用数据冒险:将暂停和转发结合起来
0 0