前言:

本人非常遗憾地告诉您。由于某些原因,更新到第三章过程(函数调用)结束就停止更新了.目前没有任何计划重新更新.懒+没时间.

YZdGij20220109time005114

Chapter 1: A Tour of Computer System (计算机系统漫游)

这一章内容主要是通过介绍 hello world 这个程序的生命周期,对计算机系统的主要概念做了一个概述.
avee8o20220115time114136
其中会涉及很多计算机领域的专有名词.对于初学者来讲,可能是第一次听到.
想要完全理解所有的知识点,需要一个较长时间的过程.因此,很长地方不理解也是很正常的.

Program Structure and Execution(程序结构和执行)

首先,我们看一下这个 hello 程序,非常简单的一个打印输出的程序.
源代码编写完成之后,保存得到一个后缀名为 .c 的文件 — hello.c
2022-01-15-11-46-05-image
接下来通过这条简单的命令gcc -o hello hell.c,即可完成对源代码的编译,生成可执行程序 hello.
这个过程虽然是通过一条命令完成的,然而编译系统的处理过程却是非常复杂的.
大致分为一下四个阶段,分别是预处理、编译、汇编以及链接

首先看一下,预处理,预处理器会根据以#开头的代码,来修改原始程序.
例如 hello 程序中引入来头文件—stdio.h 预处理器会读取该头文件中的内容.将其中的内容直接插入到源程序中,结果就得到来另外一个C程序.
这个经过预处理器处理后得到的文件通常以 .i 结尾. 这个 hello.i 仍是一个文本文件.
第二阶段是编译阶段.编译器将 hello.i 文件翻译成 hello.s 文件.这一过程我们称为编译.其中编译这一阶段包括词法分析、语法分析、语义分析、中间代码生成以及优化等等一系列的中间操作.有关编译的细节知识不会在本文中进行详细地讲解.感兴趣的同学可以了解一下《编译原理》
对于编译阶段,输入文件 hello.i 经过编译得到输出文件—hello.s
第三阶段是汇编,汇编器根据指令集将汇编程序 hello.s 翻译成机器指令.
并把这一系列的机器指令按照固定的规则进行打包,得到可重定位目标文件—hello.o.
至于可重定位是什么意思,稍后在链接阶段会讲,此时 hello.o 虽然是一个二进制文件,但是还不能执行.还要经历最后一个阶段—链接.
hello 这个程序中,我们调用了 printf 函数.这个函数是标准C库中的一个函数.
这个 printf 函数是在名为 printf.o 的文件中.这个文件是一个提前编译好的目标文件.链接器(ld)负责把 hello.oprintf.o 进行合并.
当然这个合并是需要遵循一定规则的,正是因为链接器要对 hello.oprintf.o 进行调整.
所以 hello.o 才会被称之为可重定位目标文件.最终经过链接阶段得到可执行目标文件—hello.此时得到的hello就可以被加载到内存中执行了.

image-20211208172523111
cEMnk220220115time122129
为什么要理解编译系统是如何工作的呢?
image-20211208172601327

Running Programs on a System(在系统上运行程序)

我们来看一下计算机的硬件组成.(Hardware Organization of a System)

image-20211208174610889

CPU中的PC(Program Count)就是一个4字节(32-bit)或8字节(64-bit)的存储空间(实质上是一个大小为一个字的存储区域),里面存放某一条指令的地址

寄存器文件,它就是CPU内部的一个存储设备,是由一些单字长的寄存器构成,每个寄存器都有自己唯一的一个名字,通俗来说,寄存器可以理解为一个临时存放数据的空间

(在第四章中会详细讲述处理器是如何实现的)

主存,也称为内存,处理器在执行程序时,内存主要存放程序指令以及数据,从物理上讲,内存是由随机动态存储器芯片组成,从逻辑上讲,内存可以看成一个从零开始的大数组,每个字节都有相应的地址 (关于内存更多的详细内容,在第六章中有详细的讲解)

内存和处理器之间通过总线来进行数据传递,实际上,总线贯穿了整个计算机系统,它负责将信息从一个部件传递到另外一个部件,

通常总线被设计成传送固定长度的字节块,也就是字(word),这个字取决于操作系统.

控制器与适配器的主要区别是在于它们的封装方式,他们的功能都是在I/O设备与I/O总线之间传递数据 (第六章和第十章进行详细的讲解)

image-20211208174940809

image-20211208175139961

这个结构的主要思想就是上一层存储设备是下一层存储设备的高速缓存(关于缓存的更多内容将在第六章有更加详细的讲解)

Interaction and Communication between Programs(进程间的交互和通信)

image-20211208203515825

操控硬件的是操作系统,我们可以把操作系统看作是应用程序和硬件之间的中间层

所有的应用程序对硬件的操作必须通过操作系统来完成.

这样设计的目的主要有2个:

一是防止硬件被失控的应用程序滥用

二是操作系统提供统一的机制来控制这些复杂的底层硬件.

文件是对I/O设备的抽象,虚拟内存是对内存和硬盘的I/O的抽象,进程是对处理器、内存以及I/O设备的抽象

(第八章将详细讲述关于进程的知识,在第十二章中,将讲述如何编写多线程程序)

如下图所示,为Linux系统中进程的虚拟地址空间,从下往上看,地址是增大的.最下面的是0地址,自下而上地介绍一下虚拟地址空间的分布

image-20211209102310990
一张图详解C++内存分布(通俗易懂)_Sunny的地盘的博客-CSDN博客_c++ 内存空间分配图

为什么程序内存栈是从高地址往低地址分配内存的,而其它的内存地址是从低到高分配内存?

因为栈设计为后进先出的特性(栈需要存储函数中的局部变量和参数,函数又是最后调用的最先销毁,栈的后进先出正好满足这一点)。
其次,栈是连续分配内存的,如果给一个数组或对象分配内存,栈会选择还没分配的最小的内存地址给数组,在这个内存块中,数组中的元素从低地址到高地址依次分配(不要和栈的从高到低弄混了)。所以数组中第一个元素的其实地址对应于已分配栈的最低地址。

第一个区域是用来存放程序的代码和数据的,这个区域的内容是从可执行目标文件中加载而来的,对于所有的进程来说,代码都是从固定的地址开始,(关于这个区域的详细内容会在第七章介绍)

顺着地址增大的方向往上看就是堆(heap),堆也可以在运行时动态的扩展和收缩(第九章研究虚拟内存的时候,会详细介绍堆)

接下来,就是共享库的存放区域,这个区域主要存放像C语言的标准库和数学库这种共享库的代码和数据(在第七章介绍链接时,会详细介绍共享库事如何工作的)

我们,继续往上看,这个区域被称为用户栈(user stack),函数调用的本质就是压栈,每一次当出现进行函数调用的时候,栈就会增长,函数执行完毕返回时,栈就会收缩,栈的增长方向是从高地址到低地址到的,(在第三章,会详细介绍编译器是如何使用栈来实现函数的调用的)

最后,我们看一下虚拟空间的最顶部区域,这个区域是为内核保留的区域,应用程序代码不能读写这个区域的数据,也不能直接调用内核中定义的函数,也就是说,这个区域对应用程序是不可见的(关于更多虚拟内存的知识,将在第九章进行详细的讲述)

Linux系统的哲学思想是:一切皆为文件. 如何理解呢?一切I/O设备,包含键盘,磁盘,显示器,甚至网络,这些可以看成文件,系统中所有的输入输出都可以通过读写文件来完成,虽然文件的概念非常简单,但却非常强大 (将在第十章详细讲述Unix IO)

image-20211209102516278

(在第十一章中,将介绍如何创建一个简单的web服务器)

为了定量地看系统加速比,我们首先介绍一下阿姆达尔定律,这个定律的主要思想是,当我们对系统的某一部分进行加速时,被加速部分的主要性是影响整体系统性能的关键因素image-20211209103030644

image-20211209103334421

image-20211209103607102

(第十二章会深入探讨并发的相关知识)

现代处理器可以同时执行多条指令的属性被称为指令级并行,每条指令从开始到结束大概需要20个时钟周期或者更多,但是处理器采用了非常多的技巧可以同时处理多达100条指令,因此,近几年的处理器可以保持每个周期2~4条指令的执行速率 (在第四章,将介绍流水线技术)

现代处理器拥有特殊的硬件部件,允许一条指令产生多个并行操作,这种方式称为单指令多数据(Single Instruction Multiple Data)

SIMD的指令多是为了提高处理水平、以及声音这类数据的执行速度image-20211209104414800

image-20211209104537286

Chapter 2: Representing and Manipulating Information (信息的表示和处理)

image-20211209104737367

Information Storage(信息存储)

信息在计算机系统内是如何存储的?

通常情况下,程序将内存视为一个非常大的数组,数组的元素是由一个个多字节组成, 每个字节都由一个唯一的数字来表示,我们称为地址(address),这些所有地址的集合就称为虚拟地址空间(virtual address space)image-20211209105721592

字节,信息存储的基本单元

image-20211209111858945

一个字节是由8个位(bit)组成,在二进制表示法中,每个位的值可能有两种状态,0或1,当这8个位全为0时,表示这个字节的最小值,当全为1时,表示最大值,如果用十进制来表示,那么一个字节的取值范围就在0~255(包含0和255)之间,我们把这种一位一位表示数据的方式称为位模式

使用二进制表示法比较冗长,而十进制表示法与位模式之间的切换又比较麻烦,因此,我们引入了十六进制数来表示位模式

image-20211209112856812

在C语言中,十六进制数是以0x开头,这个X可以是小写,也可以是大写,字母部分既可以全部是大写,也可以全部是小写,甚至混合都没问题

image-20211209113217396

二进制到十六进制转换:

image-20211209132636501

十进制与十六进制之间的转换需要采用除法或者乘法来处理:

用辗转相除法将一个十进制数转成十六进制数

image-20211209134034974

字长决定了虚拟地址空间的最大值的可以到多少

image-20211209134314876

对于我们需要存储的数据,我们需要搞清楚该数据的地址是什么,以及数据在内存中如何排布的,

image-20211209134850147

通常二种规则
大端法,最高有效字节存储在最前面,也就是低地址处
小端法,最低有效字节存储在最前面,也就是高地址处image-20211209135410382

大多数Intel兼容机采用小端模式,IBM和Sun公司的机器大多数采用大端法,对于很多新处理器,支持双端法,可以配置成大端或者小端运行,例如基于ARM架构的处理器,支持双端法,但是Android系统和iOS系统却只能运行在小端模式


布尔代数定义的几种运算:

image-20211209140957447

C语言中的一个特性就是支持 按位 进行布尔运算

image-20211209141231343

位运算的一个常见用法就是实现掩码运算,通俗点讲,通过位运算可以得到特定的位序列

image-20211209141707295

image-20211209141754458

除了位级运算之外,C语言还提供了一组逻辑运算,逻辑运算的运算符容易与位级运算混淆
逻辑运算认为所有非零的参数都表示true,只有参数0表示false

image-20211209142034553

逻辑运算的几个例子:
事实上,逻辑运算的结果只有两种,true或者false,而位运算只有在特殊的数值条件下才会得到0或1

image-20211209142455953

除此之外,C语言还提供了一组移位运算
左移情况比较简单,对于右移,分为逻辑右移和算术右移
逻辑右移和左移只是在方向上存在差异
算术右移:当算术右移到操作对象的最高位等于0时,算术右移和逻辑右移是一样的,没有任何差别
但当操作数的最高位为1时,算术右移之后,右端需要补1,而不是补0

image-20211209143732251

Interger Representations(整数表示)

image-20211209144741427关于long类型的大小需要注意一下,这个类型的取值范围是与机器字长相关的.当变量声明带有unsigned关键字时,限制了表示的数字只能为非负数(无符号数).

首先,我们看一下计算机是如何对无符号数进行编码的.

假设有个整数的数据类型有w位,用向量x来表示.

如果把向量x看成一个二进制表示的数.向量x中的每个元素表示一个二进制位,其中每个位的取值为0或1.

我们用一个函数B2U来表示一个长度为w的0,1串是如何映射到无符号数的.

image-20211209145706542
image-20211209145422997
如果这个映射过程不好理解,可以类比一下十进制的表示方式.其中二者之间的一个差别就是$x_i$的取值是0或1,而$y_i$是取0~9之间的整数,另外一个差别就是$x_i$对应的权重是$2^i$,而$y_i$对应的是$10^i$.

为了更加清楚地解释无符号数的表示方法.
CSAPP的原书中还介绍了一种图形化的表示方法来帮助读者理解无符号数的编码规则.image-20211209151041628
对于向量第$i$位,我们用一个长度为$2^i$的蓝色条状图来表示.每个位向量所对应的值就等于所有值为1的位,所对应条状图的长度之和.
例如编码0101,就是长度为4($2^2$)的条状图 加上长度为1($2^0$)的条状图.
因此4位编码所能表示的无符号数的取值范围是$0$~$15$.

我们可以发现这种编码方式,只能表示非负数,具有相当大的局限性,那么负数是如何编码的呢?

image-20211209151652712

关于符号位,需要理解负权重的概念,而不能简单的当成一个负号

image-20211209152248884

image-20211209152347089

在了解了无符号数和有符号数的编码规则之后,我们分别看一下不同字长可以表示整数的范围

image-20211209185833733

image-20211209190028133

无论是字长为8,还是字长为64,最小值的表示都是符号位为1,其他位等于0

image-20211209195220078

对于-1,这个需要特别注意一下,无论字长是8位,还是64位,有符号数-1的补码是一个全为1的串
-1的补码与无符号数的最大值有着相同的二进制位表示

image-20211209200256211

虽然C语言的标准中并没有要求用补码来表示有符号数
但是几乎所有的机器都是用补码来表示有符号数

为了更好地理解补码的表示,我们再看一个例子:

image-20211209200835169

其中1234被定义为short类型,将它展开为二进制表示,12345位模式表示如上图所示

根据补码的编码定义,相应矩形框内的权重值相加可以得到12345

image-20211209201046043

我们再来看一下-12345的位模式表示,这里需要注意的是最高符号位等于1
与-12345相同的位模式的无符号数又是多少呢?

image-20211209201326053

对于无符号数,最高位的1表示的不是负权重,根据无符号数的编码定义可以得到53191
对于相同的位模式,映射函数不同,得到的数值也不同

C语言允许数据类型之间做强制类型转换
例如图中的代码示例,变量a是short类型,通过强制类型转换成无符号数,那么变量b的数值是多少呢?

image-20211209202258764

我们来看一下运行结果

image-20211209202437699

-12345经过强制类型转换后得到的无符号数是53191
从十进制的表示来看,很难看出二者的关系

image-20211209202653038

将十进制表示转换成二进制表示,我们可以发现,二者的位模式是一样的
对于大多数C语言的实现,有符号数和无符号数之间的转换的规则是:
位模式不变,但是解释这些位的方式改变了
接下来我们看一下,对于相同的位模式,不同函数映射所导致的数值差异
无符号数和有符号数的函数映射关系如图所示
我们将B2U与B2T做差,得到的结果就是二者的数值的差异

image-20211209203053841

然后我们对这个式子进行移项处理
B2T从等式的左边移到等式的右边,对于相同的位模式,无符号数与有符号数的数值关系如图所示

image-20211209203528402

我们用T2U来表示有符号数到无符号数到函数映射

image-20211209204335641

当最高位Xw-1等于1时,此时有符号数x表示一个负数,经过转换后得到的无符号数等于该有符号数加上2^w^

当最高位Xw-1等于0时,此时有符号数x表示一个非负数,得到的无符号数与有符号数是相等的
以上为有符号数转无符号数,接下来介绍无符号数转有符号数

同样,还是对这个等式移项

image-20211209204738740

我们用U2T来表示无符号数到有符号数的函数映射

image-20211209205156921

当最高位等于0时,无符号数可以表示的数值小于有符号数的最大值,此时转换后的数值不变
当最高位等于1时,无符号数可以表示的数值大于有符号数的最大值,此时转换后得到的有符号数等于该无符号数减去2^w^

图中T2U和U2T这两个函数映射关系体现了有符号数与无符号数转换的数值关系
为什么要解释二者的转换关系呢?
在C语言中,在执行一个运算时,如果一个运算数是有符号数,另外一个运算数是无符号数
那么C语言会隐式地将有符号数强制转换为无符号数来执行运算

例如图中的例子,我们希望得到-1小于0的输出
但是在执行时,却得到了-1比0大的结果

image-20211209205718551

由于第二个操作数0U是无符号数,第一个操作数就隐式地转换成无符号数

image-20211209210035053

这个表达式实际上比较的是4294967295(2^32^-1)<0

C语言中还有一个常见的运算是在不同字长的整数之间进行转换

将一个较大的数据类型转换成一个较小的数据类型

image-20211209214020002

由于目标类型太小,想要保持数值不变是不可能的
然而将一个较小的数据类型转换成较大的数据类型时,保持数值不变是可以的

我们先来看一下把无符号数转成一个更大的数据类型

例如,我们将一个unsigned char类型变量,转换成unsigned short类型
变量a占8个bit位,而变量b占16个bit位
对于无符号数的转换比较简单,只需要在扩展的数位进行补零即可
我们将这种运算称为零扩展,具体表示如图所示

image-20211209215038603

根据无符号数的编码定义,零扩展之后的数值不变
与无符号数相比,将有符号数转换成一个更大的数据类型

image-20211209215408027

需要执行符号位扩展,这个符号为就是最高位,对于符号为扩展该如何理解呢?

当符号数表示表示非负数时,最高位是0,此时扩展的数位进行补零即可
当符号数表示表示负数时,最高位是1,此时扩展的数位进行补1,具体表示如图所示

image-20211209215634066

看到这里,相信有些读者会对补1的情况有些疑问
我们来看一下原书中给出的数学证明
对于一个w位的有符号数,我们用B2Tw来表示
对这个有符号数进行k位的符号扩展,具体过程如图所示

image-20211209215957516

经过扩展之后的有符号数用B2Tw+k来表示

image-20211209220842805

为了方便描述,我们将两个函数映射分别记为(1)和(2)
假如可以证明表达式(1)和(2)相等,那么经过符号位扩展确实可以保持数值不变
接下来我们的任务是证明表达式(1)和(2)相等
假如能过证明符号位扩展一位,可以保持数值不变,那么扩展任意位,就都能保持这种属性
因此,我们只要能过证明B2Tw+1等于B2Tw,就可以确定B2Tw+k等于B2Tw

根据补码的编码规则,B2Tw和B2Tw+1的展开式如图所示

image-20211209221242841

然后将二者做差,经过简单的合并之后,得到图中的表达式,由于Xw是由Xw-1扩展得到的,因此做差的结果等于0

综上所述,我们通过数学的方法证明了当有符号数从一个较小的数据类型转换成较大数据类型时
进行符号位扩展,可以保持数值不变.

看完较小数据类型到较大数据类型的转换
我们再看看较大数据类型换成较小数据类型的情况

image-20211209222016738

将int类型强制转换成short类型时,int类型高16数据被丢弃,留下底16位数据
因此截断一个数字,可能会改变它原来的值

将一个w位的无符号数,截断成k位时,丢弃最高的w-k位,截断操作可以对应取模运算
对于二进制取模运算,通俗的说法就是除以2^k^之后得到的余数
我们可以通过一个十进制的例子类比理解一下

例如十进制数123456,截取底三位数456,可以通过10^3^进行取模运算得到

image-20211209224340115

我们再来看一下截断有符号数

虽然原书中给出了以下的表达式,这个式子乍一看有点吓唬人

image-20211210204542271

其实并不难理解,我们可以分成两部分来看

image-20211210204949169

第一步,我们用无符号数多函数映射来解释底层的二进制位,这样一来我们可以使用与无符号数相同的截断方式,得到最低K位

第二步,我们将第一步得到的无符号数转换成有符号数

经过这两步,我们就得到了有符号数截断之后的值

Integer Arithmetic(整数运算)

首先,我们看一个无符号数加法的例子

image-20211210205334084

图中的两个无符号数相加,我们期望得到的结果是256,然而实际的运行结果却是零

产生这个结果的原因是因为a加b的和超过了unsigned char所能表示的最大值255

我们将这种情况称为溢出

image-20211210205424662

接下来,我们看一下无符号数的加法原理

这里我们引入一个符号来表示w位的无符号数加法,其中u是unsigned的缩写,表示无符号数

对于操作数x和y,二者的取值范围都是大于等于0,小于2^w^

image-20211210205848281

我们重点来看一下,发生溢出时,这个结果是如何得到的

首先,我们看一下变量a,b的二进制表示,具体如图所示

image-20211210210254867

当执行加法运算后,得到结果的二进制如上图所示,为了使得运算结果的数据位保持w位不变,最高位的1会被丢弃

因此得到的结果相当于减去2^w^

在C语言的执行过程中,对于溢出的情况并不会报错,但是我们希望判定运算结果是否发生了溢出

下面我们看一下C语言中是如何判断溢出的:

由于x,y都是大于等于0的,因此二者之和一定大于等于其中的任意一个数,因此,我们可以用图中的代码来判断是否发生了溢出

image-20211210210622751

如果返回值为1,表示运算结果正常,如果返回值为0,则表示发生了溢出,当然图中的if-else的书写方式是为了方便理解,实际可以简化为一句

对于这个判断条件是否正确,参考图中右边简单的数学推导,因此可以证明,当发生溢出时,得到的和小于其中的任意一个数(x或者y)

看完了无符号数加法,我们再来看一下有符号数加法

有符号数x,y,它们的取值范围如图所示

对于补码的加法运算,我们同样引入一个符号来表示,其中t就是补码的首字母缩写

想要准确地表示有符号数相加的结果需要w+1位,为了避免数据大小的扩张,最终结果将截断位w位来表示

与无符号数相加不同的是,有符号数的溢出分为正溢出和负溢出

image-20211210211657782

接下来,我们看一个代码实例,例如 图中所示的代码,x加上y,我们期望得到的结果是 128,然而运行结果却是 -128

image-20211210211836463

我们可以通过二进制的表示,来看一下结果为什么是-128

image-20211210212217441

对于x和y的二进制表示如图所示,x与y相加之和,运算结果二进制表示如图所示,根据之前学过的有符号数的表示方式,最高位的1解释为负权重,因此运行结果就等于 -128,这个运行结果与通过公式计算的结果也是一致的,对于负溢出的情况,也是类似的

对于如何检测有符号数相加是否发生了溢出比较简单

image-20211210213203250

当两个正数相加,得到的结果为负,则说明发生了正溢出,当两个负数相加,得到的结果为正,则说明发生了负溢出

看完了加法运算,我们再来看一下减法是如何实现的

CSAPP原书中提到了一个加法逆元(additive inverse)的概念

乍一听这一词比较陌生,其实也非常好理解,对于一个给定的x,存在x’,使得 x加上x’ 等于 x’加上x,并且等于0

我们称x’为x的加法逆元,实际上x与x’互为相反数,加法逆元也可以称为相反数

image-20211211211154698

对于减法运算y-x,我们可以转换成y加上x的相反数,那么我们看一下对于无符号数x,它的相反数x’一个如何表示?

根据相反数的定义需要满足x’+x=0,但是x’和x都是非负数,那么x’应该如何来表示呢?

当x’加x等于2^w^时,导致溢出,同样结果也等于0,因此对于任意x大于等于0,小于2^w^,其w位的无符号逆元x’的表示如上图所示

我们再来看一下有符号数的逆元

对于补码表示的有符号数的逆元比较简单,当x大于最小值的情况,x的逆元就是负的x

唯一需要注意的地方就是当x取最小值时,由于补码表示的最大值与最小值是非对称的,最大值的绝对值比最小值的绝对值要小

image-20211211225123590

关于最小值的逆元需要通过负溢出的方式来实现,补码最小值的逆元就是本身,具体数学公式表示如下图所示.

image-20211211225259395

接下来会讲述乘法和除法.

首先看一下无符号数的乘法运算,对于w位的无符号数x和y,二者的乘积可能需要2w位来表示

在C语言中,定义了无符号数乘法所产生的结果是w位,因此,运行结果中会截取2w位中的低w位.

我们知道,截断操作所对应的数学运算——取模

image-20211211230306603

因此,运行结果等于x与y乘积并对2^w^取模

接下来,我们看一下补码的乘法

无论是无符号数乘法,还是补码乘法,运算结果的位级表示都是一样的,只不过补码乘法比无符号数乘法多一步,需要将无符号数转换成补码(有符号数),具体数学表达式如图所示

image-20211211230633515

接下来我们看几个例子,图中展示了3位无符号数和补码的乘法示例,通过表格中所列举的三组示例,我们可以看到

虽然完整的乘积结果的位级表示可能会不同,但是截断后的位级表示都是相同的

接下来,我们通过数学的方法来证明一下

image-20211211231011667

假设x和y表示有符号数,x’和y’表示无符号数,x和x’的二进制表示相同,y与y’的二进制表示相同

根据之前我们讲过无符号数与有符号数的定义,对于相同的二进制表示,无符号数x’与有符号数x之间的关系如图所示

image-20211211231313946

同样,无符号数y’与有符号数y之间的关系也如此.

对于x’乘以y’,然后对2^w^取模运算的具体推导过程如图所示

image-20211211234220351

由于取模运算的原因,所有带有权重2^w^和2^2w^的项都丢掉了

虽然无符号数和补码两种乘积的完整位表示不同,但是截断之后结果的位级表示却相同

由于乘法指令的执行需要多个时钟周期
image-20220106172811883
很多C语言的编译器试图用移位、加法和减法来代替整数乘法的操作

首先我们看一下乘以2的幂的情况
image-20220106173322790

x乘以2^k^,就对应左移k位

相信看到这里,很多同学会有疑问,为什么乘以2的幂可以转换成左移操作?
接下来,我们看一下简单的数学证明.
例如,w位的无符号数x的二进制表示如图所示.
image-20220106175355947
根据无符号数的定义,该二进制位所对应的无符号数可以通过图中的数学表达式计算得到
我们将$x$左移$k$位,移位后的二进制数位由$w$位增加到$w+k$位.
同样根据无符号数的定义,移位后的二进制所对应的无符号数如图所示.
通过对比移位前后二者之间的变化,我们可以发现移位后,无符号数$x’$等于无符号数$x2^k$
接下来,我们通过一个例子来看看乘以任意常数的情况.
image-20220106180227795
例如,$x
14$,$14$的二进制表示如图所示.$x14$等于$x(2^3+2^2+2^1)$
根据刚才讲到的,乘以$2$的幂可以等效为左移操作
这样一来,一个乘法操作可以使用三个移位操作和两个加法操作来代替.
更好的情况,编译器甚至可以把$14$分解成$16-2.这样一个乘法操作可以用两个移位和一个减法来代替

看完了乘法运算,我们再来看一下除法.
image-20220106180644440
image-20220106180915126

对于除以$2$的幂也可以用移位来实现,不过除法的移位采用的是右移,而不是左移.
关于右移到情况.这里需要注意一下.对于无符号数采用的是逻辑右移,而有符号数采用的是算术右移 (它们的差别在2.1中有)
image-20220106181649801整数的除法,还会遇到除不尽的情况,总是朝向$0$的方向进行舍入
例如,$3.14$向零舍入的结果是$3$,$-3.14$向零舍入的结果是$-3$
对于$x\geq0,y>0$的情况,结果会向下舍入
对于$x<0,y>0$的情况,结果是向上舍入

首先,我们看一下无符号数除以$2$的幂的情况, $x$ 表示 $w$ 位的无符号数,对$x$进行右移操作$k$位的结果如图所示.
image-20220106182048131

为了方便描述,这里我们引入两个变量,$x_1和x_2$
其中$x_1$是$w-k$位的无符号数,$x_2$是$k位$的无符号数,我们将$x_1$左移$k$位
image-20220106182641778

根据前面讲到的,左移$k$位等于$x_1*2^k$
image-20220106182937854

$x_1$左移$k$位与$x_2$相加之和与$x$相等
image-20220106184157147

由于$x_2$的长度为$k$位,因此$x_2$的取值范围为$0\leq x_2<2^k$,因此$x$除以$2^k$,取整的结果就等于$x_1$,这与$x$逻辑右移k位得到的结果是一样的

看完了无符号数的除法,再看一下补码的除法,当补码的最高位等于0时,对于非负数的来讲,算术右移与除以$2^k$是一样的.
对于负数来讲,需要特别注意一下,例如对 -12340对16位表示进行算术右移不同数位的结果
image-20220106185537196

当需要舍入时,移位导致-771.25向下舍入位-772
根据整数除法向零舍入的原则,我们期望的得到的结果是-771
因此,需要在移位之前加入一个偏置,来修正这种不合适的舍入,其中偏置的值等于1左移k位减去1.
image-20220106190007960如图所示,当右移4位时,偏置量为$15((1<<4)-1)$
通过加入偏置之和,再进行算术右移,即可得到向零舍入的结果
image-20220106193958598
对于补码除以$2^k$的情况,当 $x<0$ 时,需要加上偏置,再进行算术右移.当 $x>0$ 时,可以直接进行算术右移.

不幸的是,这种方法并不能推广到除以任意常数.
与乘法不同,我们不能用除以2的幂的方法来表示除以任意常数k的除法.

Floating Point(浮点数)

理解浮点数的第一步是考虑含有小数值的二进制数

vk4MfX20220106time201005

我们先来看一下熟悉的十进制表示法,其中每个十进制位$d_i$的取值范围是$0$~$9$,这种表示的数学表达式如图所示,以小数点为分界线,小数点左边数字的权重是$10$的正幂,小数点右边数字的权重是10的负幂.

类比十进制的表示,我们看一下二进制的是如何表示小数的,其中$b_i$的取值是$0或1$.具体关于二进制权重的理解可以看一下这张图.

KbDxTg20220106time200814

对于这种定点表示的方法,并不能很有效地表示非常大的数.

接下来,我们看一下IEEE的关于浮点数的表示

表达式$V=(-1)^sM2^E$,涉及三个变量$s,E和M$

下面我们通过以单精度浮点数为例,看一下二进制位与浮点数之前的关系

例如C语言中float类型的变量占4个字节,32个比特位.这32个比特位被划分成3个字段来解释,具体表示如图所示.

DCK3ff20220106time202247

其中最高位31位表示符号位为$s$,当$s=0$时,表示正数,当$s=1$时,则表示负数
从第23位到30位,这8个二进制位与阶码的值是相关的,剩余的23位与尾数$M$是相关的.

对于64位双精度浮点数,其二进制位与浮点数的关系如图所示.
与单精度浮点数相比,双精度浮点数的符号位也是1位.但是阶码字段的长度为11位,小数字段的长度为52位.

浮点数的数值可以分为三类.
D3RBoB20220106time202455
第一类是规格化的值,第二类是非规格化的值,第三类是特殊值
其中阶码的值决定了这个数是属于其中哪一类
EYwuMe20220106time202931

当阶码字段的二进制位不全为0,且不全为1时,此时的是规格化的值
当阶码字段的二进制位全为0时,此时表示的是非规格化的值.
当阶码字段的二进制位全为1时,表示的数值为特殊值.

特殊值分为两类,一类表示无穷大或者无穷小,另外一类表示“不是一个数”

接下来,我们详细解释一下这三类数值的表示.
LIhgW520220114time225638当表示规格化的值时,其中阶码字段的取值范围如图所示.最小值是1,最大值是254.
为了方便描述,我们用小写字母e来表示这个8位二进制数.
需要注意的是阶码的值并不等于$e$(8个二进制位)所表示的值,而是$e$的值减去一个偏置量,偏置量的值与阶码的字段的位数是相关的.

当表示单精度的值时,阶码字段的长度为8,偏置量等于127.
当表示双精度的值时,阶码字段的长度为11,偏置量等于1023.

因此,对于单精度浮点数,阶码的最小值是-126,最大值是127.

我们再来看一下小数字段
aNvRc720220114time231227
尾数M被定义为1+f,尾数M的二进制表示如图所示.
因为我们可以调整E的取值,使得尾数M的取值范围为$1\leq M\leq2$
既然第一位总是1,那么就没有必要显示的表示出来.
这就是为什么尾数M的值需要加1,这个加1的地方需要特别记住
在了解了符号位、阶码字段以及小数字段所表示的数值后
就可以根据浮点数的计算公式来计算出对应的数值

接下来,我们看一下第二类数值——非规格化的值
当阶码字段的二进制位全为0时,所表示的是非规格化的值

关于非规格化的数有两个用途

一、提供了表示数值0的方法
当符号位s等于0,阶码字段全为0,小数字段也全为0时,此时表示正零.
当符号位s等于1,阶码字段全为0,小数字段也全为0时,此时表示负零.
根据IEEE的浮点规则,正零和负零只是在某些方面被认为是不同的.

二、可以表示非常接近0的数.
当阶码字段全为0时,阶码E的值等于1-bias ,而尾数的值M等于f,不包含隐藏的1.这与规格化的值的解释方法不同,这里需特别注意一下.
KtGZgy20220114time232145

我们再来看一下特殊值是如何表示的

当阶码字段全为1,且小数字段全为0时,表示无穷大的数.
无穷大也分两种,正无穷大和负无穷大
如果符号位s等于0时,表示正无穷大.
如果符号位s等于1时,表示负无穷大.
此外,还会遇到一些运算结果不为实数或者用无穷也无法表示的情况.
这里引入一个新概念——“不是一个数”(Not a Number)
例如,我们对-1进行开方运算,无穷减无穷的运算,此时得到的结果就会返回NaN.
当阶码字段全为1,且小数字段不为0时,可以表示NaN(Not a Number)

以上便是浮点数三类数值的表示规则.

为了更加直观地理解,我们看一个8位浮点数的表示例子.
这个示例假定符号位长度为1,阶码字段的长度为4,小数字段长度为3.
对于非规格化数0的表示,我们可以看到阶码字段和小数字段全都为0.
对于其他非常接近0的非规格化数,我们可以看到其中阶码字段全部为0.小数字段的取值范围从001~111,这个小数字段所对应的尾数的值如图所示
3RDyAk20220114time235404
最终,这个8位浮点数的数值是由阶码的幂与尾数相乘之后得到的

看完了非规格化的数,我们再来看看规格化的数.

首先,看一下规格化的数的最小值是如何表示的,阶码字段的二进制数值为0001,长度为4的阶码所对应的偏置量是7,根据计算公式可以得到阶码E的值为-6.
由于小数字段为0,因此尾数M等于1.
最终得到最小值$V=1*2^{-6}$
不断地增加阶码,会获得更大的规格化的值
当阶码字段为1110,小数字段为111时,此时可以得到最大的规格化的值为240.
接下来,看一下特殊值的表示.
QsyLfB20220115time000040
当阶码字段全为1且小数字段全为0时,可以表示无穷大.

接下来,我们将整型数12345转换成浮点数12345.0
Lc6mjC20220115time000251
通过转换过程,我们将会了解这段匹配数位是如何产生的

整型数12345,其二进制数表示如图所示
suv8Ve20220115time000542
虽然int类型变量占32个比特位,由于该数的高18位都等于0.
因此,我们可以将高18位忽略,只看低14位
根据规格化数的表示规则,我们可以将12345用图中的方式来表示
根据IEEE浮点数的编码规则,我们将小数点左边的1丢弃.
由于单精度的小数字段长度为23,我们还需要在末端增加10个零
ikLMJy20220115time000946
这样我们就得到了浮点数的小数字段

从12345的规格化表示可以发现阶码E的值等于13.
由于单精度浮点数的bias(偏置)等于127.
因此根据公式$E=e-bias$,可以计算出$e=140$,其二进制表示为$1000,,1100$,这样一来,浮点数的阶码字段也得到了. 最后,再加上符号位的0,整个单精度浮点数的二进制表示就构造完了.
bXEDsH20220115time001633

通过这个构造过程,我们可以发现之前提到的匹配的字段是如何产生的.由于表示方法的原因,限制了浮点数的范围和精度,所以浮点运算只能近似地表示实数运算.
对于值x,可能无法用浮点数形式来精确表示.
因此我们希望可以找到“最接近的值” $x’$ 来代替x.这就是舍入操作的任务.
一个关键的问题就是在两个可能的值中间确定舍入方向
例如一个数值1.5,想把该数舍入到最接近的整数,舍入结果应该是1还是2呢?
vNYlj120220115time003258
IEEE浮点格式定义来四种不同的舍入方式,分别是:

1.Round-to-even(向偶数舍入)

2.Round-toward-zero(向零舍入)

3.Round-down(向下舍入)

4.Round-up(向上舍入)

向下舍入和向上舍入的情况比较简单,向下舍入总是朝着小的方向进行舍入.
比如 1.4,1.5,1.6这三个数向下舍入的结果都是1.
而向上舍入总是朝着大的方向进行舍入.那么上面这三个数结果都为2.
Q2M1T720220115time003759

之前提到过向零舍入,这里不再赘述.

第四种舍入方式为 向偶数舍入,也被称为向最接近的值舍入.
例如: 1.4的舍入结果是1, 1.6的舍入结果是2.
需要注意的是当遇到两个可能结果的中间数值时
舍入的结果应该如何计算?
例如: 1.5,2.5这类数据,向偶数舍入的舍入结果要遵循 最低有效数字是偶数的规则.
因此1.5的舍入结果究竟是1还是2,取决于1和2哪个数是偶数,对于2.5同理,因此,1.5和2.5向偶数舍入的结果都是2.
nlJkbF20220115time004719
为什么要偏向取偶数呢?
如果总是才用向上舍入,会导致结果的平均值相对于真实值略高
如果总是才用向下舍入,会导致结果的平均值相对于真实值略低向偶数舍入就减少了这种统计偏差.

向偶数舍入除了十进制小数上使用也可以用在二进制小数上.
将最低有效位的值0认为是偶数,1认为是奇数
例如图中这个二进制小数,当舍入需要精确到小数点右边2位时.
D5M5o320220115time005202
由于这个数是两个可能值的中间值,根据向偶数舍入的规则
舍入结果为11.00

接下来,我们看一下浮点数加法的例子

mYJk8D20220115time005854例如图中的两个表达式,其中表达式1的计算结果等于0.0
而表达式二的计算结果等于3.14.
这是由于表达式1在计算3.14与1e10相加时,对结果进行了舍入,值3.14会丢失.
因此,对于浮点数的加法是不具有结合性的.
同样由于计算结果可能发生溢出,或者由于舍入而失去精度
导致浮点数的乘法也不具有结合性
例如图中的两个表达式,表达式3的结果为正无穷大,而表达式4的结果为$1*19^{20}$
此外,浮点乘法在加法上不具备分配性.
例如在单精度浮点的情况下,图中两个表达式的计算结果也不同
对于从事科学计算的程序员以及编译器的开发人员来说
缺乏结合性和分配性是一个比较严重的问题

C语言提供了两种不同的浮点数据类型,单精度float类型和双精度double类型.
HIEVCZ20220115time010651当int,float,double不同数据类型之间进行强制类型转换时.
得到的结果可能会超出我们的预期
当int类型转换成float类型时,数字不会发生溢出,但可能会被舍入.
这是由于单精度浮点数的小数字段是23位的,可能会出现无法保留精度的情况.
当int类型或者float类型转换成double类型时,由于double类型具有更大的范围,所以可以保留精确的数值.
从double类型转换成float类型,由于float类型所表示的数值范围更小,所以可能会发生溢出.
此外,float类型的精度相对于double较小,转换后可能被舍入.
当float类型或者double类型的浮点数转换成int类型,一种可能的情况是值会向零舍入.
例如: 1.9会被转化成1,-1.9会被转换成-1
另外一种可能的情况则是发生溢出.

Chapter 3: Machine-Level Representation of Programs (程序的机器级表示)

在第一章讲述计算机系统漫游时,我们曾提到过编译系统的工作流程.
这一节我们详细介绍一下C语言、汇编代码以及机器代码之间的关系.
理解汇编代码与原始C代码之间的关系,是理解计算机如何执行程序的关键一步.
因此,稍微花一些时间来学习一下汇编语言,对理解计算机系统是非常有必要的.
我们先来简单地看一下英特尔(Intel)处理器的发展历史。
7bTmXe20220115time131719

接下来,我们看一个C代码的例子.
bCgrRC20220115time132659
这个示例中包含两个源文件,一个是main.c,另外一个是mstore.c
我们可以通过图中的命令编译这些代码.其中gcc指的是GCC编译器,它是Linux系统上默认的编译器.其中编译选项-Og是用来告诉编译器生成符合原始C代码整体结构的机器代码.
在实际项目中,为了获得更高的性能,会使用-O1或者-O2,甚至更高的编译优化选项.但是使用高级别的优化产生的代码会严重变形.导致产生的机器代码与最初的源代码之间的关系难以理解.
这里方便理解,因此选择-Og这个优化选项,-o 后面跟的参数prog表示生成可执行文件的文件名.关于更多链接方面的知识,我们将在第七章做详细地讲解.

首先,我们以源文件mstore.c为例,看一下C代码和汇编代码之间的关系.
使用这条命令(gcc -Og -S mstore.c)可以生成mstore.c所对应的汇编文件mstore.s . 其中-S这个编译选项就是告诉编译器GCC产生的文件为汇编文件
我们可以用编辑器(vim)等打开这个汇编文件.由于篇幅限制,我们省略来部分内容.
85YVwV20220115time133123

其中以“.”开头的行都是指导汇编器和链接器工作的伪命令.也就是说我们完全可以忽略这些以“.”开头的行.
删除了无关的信息之后,剩余这些汇编代码与源文件C代码是相关的.
接下来,我们看一下这段C程序所对应的第一条汇编代码.
pushq这条指令的意思是将寄存器rbx的值压入程序栈进行保存.
相信大家对一开始的这个操作会有疑问.为什么程序一开始要保存寄存器rbx的内容?
这里我们需要简单介绍一下寄存器的背景知识。
在Intel x86-64的处理器中包含了16个通用目的的寄存器
ONeXlw20220115time134012

这些寄存器用来存放整数数据和指针.图中显示的这16个寄存器,它们的名字都是以%r开头的.在详细介绍寄存器功能之前,我们首先需要搞清楚两个概念.
调用者保存寄存器 和 被调用者保存寄存器
例如图中的这个例子
OFcfEH20220115time134639函数A中调用了函数B.因此,函数A称为调用者,函数B称为被调用者.由于调用了函数B,寄存器rbx在函数B中被修改了.逻辑上,寄存器rbx的内容在调用函数B的前后应该保持一致.
解决这个问题有两个策略.

一个是函数A在调用函数B之前提前保存寄存器rbx的内容(save register rbx).执行完函数B之后,再恢复寄存器rbx原来存储的内容(restore register rbx). 这种策略称之为 调用者保存

另外一个策略是函数B在使用寄存器rbx之前,先保存寄存器rbx的值,在函数B返回之前,先恢复寄存器rbx原来存储的内容. 这种策略被称之为 被调用者保存
eYPXho20220115time135301

对于具体使用哪一种策略,不同的寄存器被定义成不同的策略.
具体如图所示.
sCmuof20220115time135456

寄存器rbx被定义为 被调用者保存寄存器(callee-saved register)

因此,pushq就是用来保存寄存器rbx的内容.
在函数返回之前,使用了pop指令,恢复寄存器rbx的内容.
第二行汇编代码的含义是将寄存器rdx的内容复制到寄存器rbx.
根据寄存器用法的定义,函数multstore的三个参数分别保存在寄存器rdi,rsi和rdx中.
这条指令结束后,寄存器rbx与寄存器rdx的内容一致.都是desk指针所指向的内存地址.
move指令的后缀“q“表示数据的大小.由于早期的机器是16位,后来才扩展到32位.
因此,Intel用字(word)来表示16位的数据类型.
所以,32位的数据类型称为双字,64位的数据类型就称为四字.
图中的表格给出来C语言的基本类型对应的汇编后缀表示.其中需要特别注意这一列的内容
Ub4YJB20220115time210022

大多数GCC生成的汇编指令都有一个字符后缀来表示操作数的大小
例如数据传送指令就有四个变种.
分别为movb、movw、movl以及movq.分别对应Move byte的缩写,表示传送字节,Move word的缩写,表示传送字,Move double word的缩写,表示传送双字,其中l是long word的缩写,传送四字.
call指令对应于C代码中的函数调用,这一行代码比较容易理解.
该函数的返回值会保存到寄存器rax中.
因此,寄存器rax中保存来x和y的乘积的结果.
下一条指令将寄存器rax的值送到内存中保存,内存的地址就存放在寄存器rbx中.
最后一条指令ret就是函数返回.关于更多寄存器与汇编语言的知识,后续会有更加详细的介绍.

接下里,我们看一下C代码是如何翻译成机器代码的
我们只需要将编译选项-S替换成-c ,执行 gcc -Og -c mstore.c这条命令,即可产生mstore.c所对应的机器代码文件mstore.o
由于该文件是二进制格式的,所以无法直接查看.
这里我们需要借助一个反汇编工具—objdump
汇编器将汇编代码翻译成二进制的机器代码,那么反汇编器就是机器代码翻译成汇编代码.通过命令objdump -d mstore.o,我们可以查看mstore.o中的相关信息.具体内容如图所示.
xnCStx20220115time211639
通过对比反汇编得到的汇编代码与编译器直接产生的汇编代码,可以发现二者存在细微的差别.
反汇编代码省略来很多指令的后缀的“q”,但在call和ret指令添加后缀“q”.由于q只是表示大小的指示符,大多数情况下是可以省略的.

通过上面的描述,我们大概了解了C代码、汇编代码、机器代码之间的关系.这些介绍就先在这里.

寄存器与数据传送指令

在第一章计算机系统漫游时,提到过计算机系统中信息的存储部件.相对于内存和硬盘,大家对寄存器可能会陌生些.
接下来,我们详细介绍一下寄存器的相关知识

最早8086的处理器中包含八个16位的通用寄存器,具体如图所示.rwQFrG20220118time112228

每个寄存器都有特殊的功能,它们的名字就反映了不同的用途。当处理器从16位扩展到32位时,寄存器的位数也随之扩展到了32位.
直到今天64位的处理器中,原来8个16位寄存器已经扩展成了64位.
除此之外,还增加了八个新的寄存器,在一般的程序中不同的寄存器扮演着不同的角色,相应的编程规范规定了如何使用这些寄存器.
wmccnx20220118time112705
例如寄存器rax用来保存函数的返回值
寄存器rsp用来保存程序栈的结束位置
除此之外,还有6个寄存器可以用来传递函数参数。

在了解了这些寄存器的用法之后, 再去理解汇编代码就会容易多了.
接下来我们看一下指令的相关知识。
大多数指令包含两部分操作码和操作数
例如图中的这几条指令,movq、addq、subq这部分被定义为操作码.
7OTsje20220118time125037它决定了cpu所执行操作的类型.操作码之后的这部分是操作数.大多数指令具有一个或者多个操作数
不过像ret返回指令,是没有操作数的.
不同指令的操作数大致可以分为三类,分别为立即数、寄存器以及内存引用。
在AT&T格式的汇编中,立即数是以$符号开头的,后面跟一个整数.
不过这个整数需要满足标准C语言的定义.
操作数是寄存器的情况也比较容易理解,即使在64位的处理器上,不仅64位的寄存器可以作为操作数.
32位、16位甚至8位的寄存器都可以作为操作数。
需要注意的是,图中这种寄存器带了小括号的情况,它所表示的是内存引用,

接下来我们重点看一下内存引用的情况
我们通常将内存抽象成一个字节数组。当需要从内存中存取数据时,需要获得目的数据的,起始地址addr,以及数据长度b
我们使用图中的符号来表示内存引用。
SdJNK720220118time130031
为了简便,通常会省略下标b
最常用的内存引用包含四部分,分别是一个立即数、一个基址寄存器,一个变址寄存器和一个比例因子。
引用数组元素时,会使用到这种通用的形式.
有效地址是通过立即数与基址寄存器的值相加,再加上变址寄存器与比例因子的乘积,具体的计算方法如上图所示
关于比例因子s的取值必须是1、2、4或者8
看到这里,相信大家都能猜出比例因子的取值为什么是这四个数中的一个.
实际上比例因子的取值是与源代码中定义的数组的类型是相关的。编译器会根据数组的类型来确定比例因子的数值
例如定义char类型的数组,比例因子就是1,int类型,比例因子就是4,至于double类型比例因子就是8.

其他形式的内存引用都是这种普通形式的变种,省略了其中的某些部分
Y9MrQs20220118time130754
图中列出了内存引用的其他形式,需要特别注意的两种写法是不$符号的立即数和了括号的寄存器。

在前面内容中,我们提到过mov类指令.它包含movb、movw、movl以及movq这四条指令,这些指令执行相同的操作,都是把数据从源位置复制到目的位置,主要区别在于它们操作的数据大小不同,具体如图所示
gJBy8E20220118time131034

对于move类指令,含有两个操作数,一个称为源操作数,另外一个称为目的操作数
对于源操作数,可以是一个立即数、一个寄存器,或者是内存引用.
由于目的操作数是用来存放源操作数的内容
所以目的操作数要么是一个寄存器,要么是一个内存引用。
注意目的操作数不能是一个立即数。
除此之外,x86-64的处理器有一条限制
就是move指令的源操作数和目的操作数不能都是内存的地址
那么当需要将一个数从内存的一个位置复制到另外一个位置时
应该如何做呢?
此时需要两条move指令来完成.
u6Z00A20220118time131953
第一条指令,将内存源位置的数据加载到寄存器
第二条指令,再将该寄存器的值写入内存的目的位置
接下来,我们看几个关于move指令的例子。
图中的指令给出了不同类型的源操作数和目的操作数的组合
第一个是源操作数,第二个是目的操作数
move指令的后缀与寄存器的大小一定得是匹配的
例如寄存器eax是32位与双字 l 对应
寄存器al是8位,与字节 b 对应
QC7YJJ20220118time132421
除此之外,move指令还有几个特殊的情况需要了解一下
当movq指令的源操作数是立即数时,该立即数只能是32位的补码表示,然后对该数值进行符号位扩展
将得到了64位数传送到目的位置。
这个限制会带来一个问题,当立即数是64位时应该如何处理?
FQB8kn20220118time132719
这里引入了一个新的指令—movabsq
该指令的源操作数可以是任意的64位立即数
需要注意的是,目的操作数只能是寄存器。

接下来,我们通过一个例子来看一下使用move指令进行数据传送时
对目的寄存器的修改结果是怎样的?
首先使用movabsq指令将一个64位的立即数复制到寄存器rax
此时,寄存器rax内保存的数值如图所示
3LtmP920220118time133658接下来,使用movb指令将立即数-1复制到寄存器al
寄存器all的长度为8,与movb指令所操作的数据大小一致
此时,寄存器rax的低8位发生了变化,第三条指令movw是将立即数-1复制到寄存器ax
此时寄存器rax的低16位发生了改变
当指令movl将立即数-1复制到寄存器eax时
此时寄存器rax不仅仅是低32位发生了变化,而且高32位也发生了变化。
当movl的目的操作数是寄存器时,它会把该寄存器的高4字节设置为0。这是x86-64位处理器的一个规定
即任何对寄存器生成32位值的指令,都会把该寄存器的高位部分置为零

以上介绍的都是源操作数与目的操作数的大小一致的情况。
当源操作数的数位小于目的操作数时
我们需要对目的操作数剩余的字节进行零扩展或者符号位扩展

零扩展数据传送指令有5条,其中字符z是zero的缩写
z8oIGf20220118time133856
指令最后的两个字符都是大小指示符
第一个字母表示源操作数的大小
第二个字母表示目的操作数的大小

符号为扩展传送指令有6条,其中字符s是sign的缩写
lpGZkg20220118time134326
同样指令最后的两个字符也是大小指示符
对比零扩展和符号扩展,我们可以发现符号扩展比零扩展多一条4字节到8字节的扩展指令
为什么零扩展没有movzlq指令呢?
是因为这种情况的数据传送可以通过movl指令来实现
最后,符号位扩展还有一条没有操作数的特殊指令cltq
该指令的源操作数总是寄存器eax,目的操作数总是寄存器rax
cltq指针的效果与图中这条指针(movslq %eax,%rax)的效果是一致的,只不过编码更紧凑一些。

栈与数据传送指令

为了更好地理解寄存器与数据传送指令。
我们先从计算机系统的视角,看一下程序执行时数据传送的情况
在第一章,我们提到过 helloworld 程序加载运行的大致过程。
2022-01-1813.56.4720220118time140255
最初可执行文件是保存在硬盘上。通过shell程序,将可执行程序从硬盘加载的内存
此时,程序指令以及数据都保存在内存中。
CPU要执行程序,需要从内存中读取指令和数据
实际上,在一些程序的执行过程中,需要在CPU和内存之间进行频繁的数据存取
例如CPU执行一个简单的加法操作,那么首先CPU通过执行数据传送指令,将a和b的值从内存读到寄存器
寄存器就是CPU内一种数据存储的部件,只不过容量比较小
我们以x86-64位处理器为例,寄存器rax的大小是64个比特位,也就是8个字节
如果变量a是long类型,需要占用8个字节
因此,寄存器rax全部的数据位都用来保存变量a
如果变量a是int类型,那么只需要用4个字节来存储该变量
那么只需要用到寄存器的低32位就够了
如果变量a是short类型,则只需要用到寄存器的低16位
对于寄存器rax如果使用全部的64位,用符号%rax来表示
ob8PtI20220118time141103
如果只用到低32位,可以用符号%eax来表示
对于低16位和低8位,分别用%ax和%al来表示
虽然用了不同的表示符号,但实际上只是针对同一寄存器的不同数位进行操作
处理器完成加法运算之后,再通过一条数据传送指令将计算结果保存到内存。
正是因为数据传送在计算机系统中是一个非常频繁的操作
所以了解一下数据传送指令对理解计算机系统会有很大的帮助

接下来,我们看一个数据传送的代码示例。
main函数中定义了变量a,并且赋值为4,随后调用了一个exchange函数。
7TRu9E20220118time141620
该函数执行返回后,变量a的值会替换成3,变量b将保存变量a原来的值4
我们重点看一下函数exchange所对应的汇编指令
Y7CwtI20220118time141702
函数exchange由三条指令实现,包括两条数据传送指令和一条返回指令。
根据寄存器使用的惯例,寄存器rdi和rsi分别用来保存函数传递的第一个参数和第二个参数
因此寄存器rdi中保存了xp的值.寄存器rsi保存了变量y的值。
这段汇编代码并没有显式的将这部分表现出来,需要注意一下
第一条mov指令从内存中读取数值到寄存器
内存地址保存在寄存器rdi中.
目的操作数是寄存器rax,这条指令对应于图中的这行代码(x = *xp)
由于最后exchange函数需要返回变量x的值,所以这里直接将变量x放到寄存器rax中
第二条mov指令将变量y的值写到内存里。变量y存储在寄存器rsi中。内存地址保存在寄存器rdi中,也就是xp指向的内存地址,这条指令对应于函数exchange中的这行代码(*xp = y)
RtglPu20220118time142200
通过这个例子,我们可以看到C语言中所谓的指针,其实就是地址。

此外,还有两个数据传送指令需要借助程序栈
学过数据结构的同学都知道,栈是一种数据结构
2022-01-1817.05.2120220118time170646
通过push操作把数据压入栈,通过pop操作删除数据
栈具有一个特性,就是弹出的数据永远是最近被压入的且仍然在栈中。
这个程序栈本质上是内存的一个区域。
50beAp20220118time170805
在第一栈计算机系统漫游中,我们曾经提到过程序的运行时内存分布

jwtfMI20220118time171958图中的这个区域就是程序栈(User Stack)
栈的增长方向是从高地址向低地址
因此,栈顶的元素是所有栈中元素地址中最低的。
根据惯例,栈是倒过来画的.栈顶在图中的底部,栈底在顶部
例如我们需要保存寄存器rax内存储的数据0x123,可以使用pushq指令把数据压入栈内。
该指令执行的过程可以分解为两步。
首先指向栈顶的寄存器rsp进行一个减法操作
例如压栈之前,栈顶指针rsp指向栈顶的位置,此处的内存地址是0x108
压栈的第一步就是寄存器rsp的值减8,此时指向的内存地址是0x100,然后将需要保存的数据复制到新的栈顶地址
此时,内存地址0x100处将保存寄存器rax内存储的数据0x123
实际上,pushq指令等效于图中的这两条指令(subq movq)
它们之间的区别在于pushq这一条指令只需要一个字节
而图中的这两条指令需要8个字节.说到底,push指令的本质还是将数据写入到内存里。
那么,与之对应的pop指令就是从内存中读取数据,并且修改栈顶指针。例如图中这条popq指令就是将栈顶保存的数据复制到寄存器rbx中
pop指令的操作也可以分解为两步:
首先从栈顶的位置读出数,据复制到寄存器rbx
此时栈顶指针rsp指向的内存地址是0x100,然后将栈顶指针加8,pop后栈顶指针rsp指向的内存地址是0x108,因此pop操作也可以等效成图中的这两条指令(movq addq)
实际上,pop指令是通过修改栈顶指针所指向的内存地址来实现数据删除的
此时,内存地址0x100内所保存的数据0x123仍然存在,直到下次push操作此处保留的数值才会被覆盖。

至此,数据传输指令的内容就介绍完了

算术和逻辑运算指令

我们来看一下有关算术和逻辑操作的指令。
首先,我们看一下指令leaq,它实现的功能是加载有效地址
q表示地址的长度是四个字。
由于x64-64位处理器上,地址长度都是64位,因此不存在leab、leaw这类有关大小的变种
例如图中的这条指令,它表示的含义是把有效地址复制到寄存器rax中
JMg5PA20220118time172536
这个源操作数看上去与内存引用的格式类似
有效地址的计算方式与之前讲到的内存地址的计算方式是一致的
可以通过图中的公式计算得到.
假设寄存器rdx内保存的数值为x,那么有效地址的值为5x+7。
注意,对于leaq指令所执行的操作并不是去内存地址(5x+7)处读取数据
而是将有效值(5x+7)这个值直接写入到目的寄存器rax
因此,这个地方需要特别注意一下,除了加载有效地址的功能,leaq指令还可以用来表示加法和有限的乘法运算。
例如图中的这段C代码经过编译后
Zm6lim20220118time172650这段代码是通过三条leaq指令来实现的,接下来我们看一下如何通过leaq指令实现算术运算

根据计算机的使用惯例参数x,y,z分别保存在寄存器rdi,rsi以及rdx中.
还是根据内存引用的计算公式,第一条指令的源操作数就对应于x+4*y,具体过程如图所示
指令leaq将该数值保存到目的寄存器rax中

接下来,关于z*12的乘法运算会有一些复杂,需要分成两步。
4IzaOr20220118time173344
第一步首先计算3*z的数值.具体过程如图所示
第二条leaq指令执行完毕,此时寄存器rdx中保存的值是3*z
第二步再把(3*z)作为一个整体乘以4
通过这两步运算最终得到z*12
此时,相信大家会有疑惑,为什么不能使用图中的这条指令(最后一条)直接一步得到我们期望的结果
这里主要是由于比例因子的取值只能是1,2,4,8这四个数中的一个,因此需要将12进行分解。

接下来我们看一组一元操作指令
op0fkG20220118time173606这一组指令只有一个操作数,因此该操作数既是源操作数,也是目的操作数。
操作数可以是寄存器,也可以是内存地址

我们再来看一组二元操作指令
bro6Z220220119time154843这一组操作指令包含两个操作数
第一个操作数是源操作数,这个操作数可以是立即数、寄存器或者内存地址.
第二个操作数既是源操作数,也是目的操作数.这个操作数可以是寄存器或者是内存地址,但不能是立即数

接下来我们看一组例子

HsSyB320220119time155321

一开始,内存以及寄存器中所保存的数据如图所示
加法指令addq是将内存地址0x100内的数据与寄存器rcx相加,二者之和再存储到内存地址0x100处。
该指令执行完毕后,内存地址0x100处所存储的数据由0xFF变成0x100
具体过程如图所示
减法指令subq是将内存地址0x108内的数据减去寄存器rdx内的数据,二者之差存储到内存地址0x108处。
该指令执行完毕后,内存地址0x108处所存储的数据由0xAB变成0xA8
对于加一指令,就是将内存地址0x110内的存储数据加1
结果是内存地址0x100处所存储的数据由0x13变成了0x14,最后一条减法指令是将寄存器rax内的值减去寄存器rdx内的值
最终寄存器rax的值由0x100变成0xFD

更多指令的相关练习,大家可以做一做原书中的习题。

在之前的内容中,我们讲过移位操作.

R1cvBF20220119time155729

图中的这一组指令是用来进行移位运算的。
左移指令有两个.,分别为SAL和SHL
二者的效果是一样的,都是在右边填零.
右移指令不同,分为算术右移和逻辑右移.算术右移需要填符号位,逻辑右移需要填零。
这与C语言中所讲述的移位操作是一致的
对于移位量k,可以是一个立即数,或者是存放在寄存器cl中的数
对于移位指令只允许特定的寄存器cl作为操作数,其他的寄存器不行,这里需要特别注意一下。

yydYe920220119time160305

由于寄存器cl的长度为8,原则上移位量的编码范围可以达到$2^8-1(255)$
实际上,对于w位的操作数进行移位操作,移位量是由寄存器cl的低m位来决定
也就是说,对于指令salb.当前目的操作数是8位,移位量由寄存器cl的低8位来决定
对于指令salw,移位量则是由寄存器cl的低4位来决定
以此类推,双字对应的就是低5位,四字对应的就是低6位

接下来,我们通过一个例子来讲述一下移位指令的用途
4affa420220119time160506图中的这段代码,涉及了多种操作

我们重点看一下$z*48$这段代码所对应的汇编指令

E8eMVa20220119time170122这个计算过程被分解成了两步。
第一步,首先计算$3*z$,由指令leaq来实现.计算结果保存到了寄存器rax中
第二步,将寄存器rax进行左移4位,左移4位的操作是等效于乘以$2^4$,也就是乘以16
通过一条leaq指令和一条左移指令,来实现乘法操作
也许大家会有这样的疑惑,为什么编译器不直接使用乘法指令来实行这个运算的呢?
主要是因为乘法指令的执行需要更长的时间
因此,编译器在生成汇编指令时,会优先考虑更高效的方式。
此外,还有一些特殊的算术指令,这里就不一一展开讲述了感兴趣的同学可以去了解一下
Z35Pdh20220119time170307
对于汇编指令的学习,最关键的是了解指令相关的基本概念,并不需要去记住指令的细枝末节
学会查阅指令手册,能够找到需要的信息即可。

指令与条件码

在C语言中,有一类语句需要满足条件才能执行
例如条件语句,循环语句等,他们需要根据数据测试的结果来决定操作执行的顺序。
这部分内容,我们来看一下与控制流相关的指令。
在之前的内容中,我们提到过算术和逻辑运算指令
例如下图中的这条减法指令的执行需要用到算术逻辑单元。简称ALU
ALU从寄存器中读取数据后,执行相应的运算,然后将运算结果返回到目的寄存器rdx
为了方便理解,用这个简单的示意图来表示.
Gw535s20220119time171021ALU除了执行算术和逻辑运算指令外,还会根据该运算的结果去设置条件码寄存器,

接下来,我们详细介绍一下条件码寄存器的相关知识
条件寄存器,它是由CPU来维护的,长度是单个比特位,它描述了最近执行操作的属性
例如ALU执行两条连续的算术指令。$t_1$时刻执行指令1,$t_2$执行指令2
$t_1$时刻条件寄存器中保存的是指令1的执行结果的属性
$t_2$时刻,条件码寄存器中的内容将被下一条指令所覆盖。

接下来,我们介绍几个常用的条件码HTZuC320220119time172009
CF—进位标志,当CPU最近执行的一条指令最高位产生了近进位。
进位标志(CF)会被置1,它可以用来检查无符号数操作的溢出
例如,无符号数a和b相加,当a=255,b=1时,由于最高位会发生进位操作
相加的结果发生溢出,此时进位标志(CF)被置1
ZF—零标志,当最近操作结果等于零时,零标志(ZF)会被置1
SF—符号标志,当最近的操作结果小于零时,符号标识(SF)会被置1
OF—溢出标志,针对有符号数.最近的操作导致正溢出或者负溢出时,溢出标志(OF)会被置1
以上我们提到了这几个标志位是比较常用的条件码寄存器.

条件码寄存器的值是由ALU在执行算术和逻辑运算指令时写入的nu5aLQ20220119time172244
图中的这些算术和逻辑运算指令会改变条件码寄存器的内容,对于不同的指令也定义了相应的规则来设置条件码寄存器.
例如逻辑操作指令xor,进位标志(CF)和溢出标志(OF)会置0
对于加一指令和减一指令,会设置溢出标志(OF)和零标志(ZF),但不会改变进位标志
具体原因,我们就不做深入的探讨了。

除此之外,还有两类指令可以设置条件码寄存器.cmp指令和test指令7BwFqS20220119time172804
cmp指令是根据两个操作数的差来设置条件码寄存器,cmp指令和减法指令类似,也是根据两个操作数的差来设置条件码
二者不同的是,cmp指令只是设置条件码寄存器,并不会更新目的寄存器的值
test指令和and指令类似,同样test指令只是设置条件码寄存器,而不改变目的寄存器的值
讲完了条件码的设置,我们再来看一下条件码的使用

为了方便理解,我们通过一个例子来看一下。vtOrKt20220119time174718
图中C 代码的含义是比较a和b的大小
当a等于b时函数返回1,否则返回0.这段代码对应的汇编指令,如图所示
根据寄存器的使用惯例,参数a存放在寄存器rdi中,参数b存放在寄存器rsi中.指令cmp对应的操作如图所示(a-b)
根据(a-b)的结果设置条件码寄存器.当a和b的值相等时,指令cmp会将零标志位设置为1
接下来的这条指令sete看起来有点费解了,这是因为通常情况下,并不会直接去读条件码寄存器,其中一种方式是根据条件码的某种组合.通过set类指令,将一个字节设置为0或者1
在这个例子,指令sete根据零标志位(ZF)的值对寄存器al进行赋值,后缀是equal的缩写.

如果零标志等于1,指令sete将寄存器al设置为1
如果零标志等于0,指令sete将寄存器al设置为0
然后mov指令对寄存器al进行零扩展,最后返回判断结果
相等的情况比较简单,也容易理解。

接下来我们看一个稍微复杂的例子JuisKS20220119time175200
例如图中示例的判断条件由a等于b变成了a小于b
经过编译器生成的汇编指令也发生了改变.通过对比之前相等的情况,我们可以发现指令sete变成了指令setl.指令setl的含义是如果a小于b,将寄存器al设置为1
其中后缀l是less的缩写,表示“在小于时设置”,而不是表示大小long word.这里特别注意一下
相对于相等的情况,判断小于的情况要稍微复杂一点,需要根据符号标识(SF)和溢出标志(OF)的异或结果来判定。

为了方便理解,我们还是通过简单的例子来解释一下.NiSaBP20220119time185657
两个有符号数相减,当没有发生溢出时.
如果a小于b,结果为负数,那么符号标识(SF)被置为1
如果a大于b,结果为正数,那么符号标识(SF)就不会被置为1
那么是不是根据符号标志(SF)就能给出判断结论了呢?
我们来看一个例子,
当 $a=-2,b=127$ 时.由于发生负溢出,结果t=120期大于零。此时符号标志(SF)不会被置1,但溢出标志(OF)会置1,因此仅仅通过符号标识无法判断a是否小于b
我们再来看一个例子,当 $a=1,b=-128$ 时,由于发生了正溢出,结果 $t=-127$ .虽然$a>b$,但是由于溢出导致了结果$t<0$.此时符号标识(SF)和溢出标志(OF)都会被置为1.
综上所述所有的情况,根据符号标识和溢出标志的异或结果,可以对a小于b是否为真做出判断.

对于其他的判断情况,我们可以通过条件码的组合来实现。3KDlr320220119time185807
虽然看上去相对复杂一点,不过原理都是一致的。
对于无符号数的比较情况,需要注意一下.
指令cmp会设置进位标志,因而针对无符号数的比较,采用的是进位标志和零标识的组合,具体的条件码组合如图所示.7996lJ20220119time185946
关于这些条件,把它组合并不需要去记住.了解条件语句的底层实现,这对我们深入理解整个计算机系统会有一定的帮助.

跳转指令与循环

首先,我们看一段C代码.M3uI2k20220119time191050
图中的这个函数是用来计算两数之差的绝对值。这段c代码对应的汇编指令如图所示.
根据前面内容所讲的知识,条件语句x小于y由指令cmp来实现
指令cmp会根据(x-y)的结果来设置符号标志(SF)和溢出标志(OF)
图中的跳转指令jl,根据符号标志(SF)和溢出标志(OF)的异或结果来判断究竟是顺序执行,还是跳转到L4处执行
当x大于y时,指令顺序执行,然后返回执行结果,L4处的指令不会被执行
当x小于y时,程序跳转到L4处执行,然后返回执行结果

跳转指令会根据条件寄存器的某种组合来决定是否进行跳转.E5lHAo20220119time191257与前文讲的set指令的设置条件是一样的,具体如图所示,这里就不再一一展开讲述了.
对于图中的if-else语句
当满足条件时,程序沿着一条路径执行
当不满足条件是就走另外一条路径。
这种机制比较简单和通用,但是在现代处理器上,它的执行效率可能会比较低。针对这种情况,有一种替代的策略,就是使用数据的条件转移来代替控制的条件转移。

还是针对这两个数的差的绝对值问题
我们给出了另外一种实现方式,具体如图所示。T2lFmB20220119time192048
我们既要计算y-x的值,也要计算x-y的值
分别用两个变量来记录结果,然后再判断x与y的大小。根据测试情况来判断是否更新返回值。
这两种写法看上去差别不大,为什么说右侧写法的执行效率就高呢?
我们来看一下右侧的代码所对应的汇编指令czMVMR20220119time192212前面的几条指令都是普通的数据传送和减法操作,接下来我们重点看一下这条指令(cmovge)
指令cmove是根据条件码的某种组合来进行有条件的传送数据。
当满足规定的条件时,将寄存器rdx内的数据复制到寄存器rax内。
在这个例子中giJr3m20220119time192458只有当$x\geq y$时,才会执行这条指令。
更多条件传送指令,如图所示PvvC6G20220119time192548
为什么基于条件传送的代码会比基于跳转指令的代码效率高,这里涉及到现代处理器是通过流水线来获得高性能。
当遇到跳转指令时,处理器会根据分支预测器来猜测每条跳转指令是否执行。当发生错误预测时,会浪费大量的时间,导致程序性能严重下降。
关于分支的更多内容,在第四章流水线的实现中会有更加详细的讲解。
C语言中提供了三种循环结构,即do-while、while以及for语句h8xRJ220220119time193212
汇编语言中没有定义专门的指令来实现循环结构。
循环语句是通过条件测试与跳转的组合来实现的。

接下来,我们分别用这三种循环结构来实现N的阶乘。
首先看一下do-while的实现方式,具体如图所示fH6LWG20220119time193508
我们可以发现指令cmp与跳转指令组合实现了循环操作。
当n大于1时,程序跳转到L2处执行循环,直到n的值减小到1,循环结束。对比do-while循环,while循环的实现方式如图所示inf0z720220119time193644
通过C代码的对比,我们可以发现这两种循环的差别在于N大于1这个循环测试的位置不同,do-while循环是先执行循环体的内容,然后再进行循环测试。while循环则是先进行循环测试,根据测试结果是否执行循环体的内容,

接下来,我们看一下For循环的基本形式,62f9Kv20220119time193756除了一种特殊情况外,这样的for循环与图中的while循环的功能是一致的。

下面,我们看一下for循环是如何实现n的阶乘的
图中的C代码采用了最自然的方式,从2一直乘到n。这与之前do-while循环以及while循环的实现方式有较大的差别。
根据刚才提到的模板,我们将这个for循环转换成while循环,具体代码如图所示。JgYk5Q20220119time203711

对比for循环和while循环产生的汇编代码可以发现m0euwI20220119time203814
除了这一句跳转指令不同,其他部分都是一致的
需要注意一下,这两个汇编代码是采用-Og选项产生的。

综上所述,三种形式的循环语句都是通过条件测试和跳转指令来实现

C语言还提供了switch语句,它可以根据一个整数的索引值进行多重的分支。kmiqk820220119time204125
在针对一个测试有多种可能的结果时,switch语句特别有用。
switch语句通过跳转表这种数据结构,使得实现更加有效。

接下来我们看一下这段代码所对应的汇编指令。

指令cmp判断参数n与立即数6的大小,如果$n>6$程序跳转到default所对应的L8程序段
对于case 0~case 6的情况,可以通过跳转表来访问不同的分支。
C代码将跳转表声明为一个长度为7的数组,每个元素都是一个指向代码位置的指针,具体对应关系如图所示。ZhQ5ko20220119time204408

数组的长度为7,是因为需要覆盖case 0~case 6的情况,对于重复的情况case 4和case 4,使用了相同的标号
对于缺失的case 1和case 5的情况,使用默认情况的标号。
在这个例子中,程序使用跳转表来处理多重分支,甚至当switch有上百种情况时,虽然跳转表的长度会有增加,但是程序的执行只需要一次跳转也可以处理复杂的分支情况。
与使用一组很长的if-else相比,使用跳转表的优点是执行switch语句的时间与case的数量是无关的
因此,在处理多重分支时,与一组很长的if-else相比,switch语句的执行效率要高。

过程(函数调用)

在大型软件的构建中,需要对复杂的功能进行切分。
过程提供了一种封装代码的方式,它可以隐藏某种行为的具体实现,同时提供清晰简洁的接口定义。
在不同的编程语言中,过程的具体实现又是多种多样的
例如,C语言中的函数,Java语言中的方法等。
接下来,我们以C语言中的函数调用为例,介绍一下过程的机制。zglwVY20220120time221719
为了方便讨论,假设函数P调用函数Q,函数Q执行完返回函数P
这一系列操作包括图中的一个或者多个机制。

接下来,我们将详细介绍一下过程的相关内容
在之前的内容中,我们提到过程序运行时的内存分布,其中栈为函数调用提供了后进先出的内存管理机制
JVAGwx20220120time221856

在函数P调用函数Q的例子中,当函数Q正在执行时,函数P以及相关调用链上的函数都会被暂时挂起。
我们先来介绍一下栈帧的概念,当函数执行所需要的存储空间超出寄存器能过存放的大小时,就会借助栈上的存储空间
我们把这部分存储空间称为函数的栈帧,对于函数P调用函数Q的例子,包括较早的帧、调用函数P的帧,还有正在执行函数Q的帧,具体如图所示。utDtMB20220120time222412

当函数P调用函数Q时,会把返回地址压入栈中,该地址指明了当函数Q执行结束返回时,要从函数P的哪个位置继续执行。
这个返回地址的压栈操作并不是由指令push来执行的,而是由函数调用指令call来实现的
我们以main函数调用multore函数为例,来解释一下指令call和指令ret的执行情况。t8y9Ns20220120time222643
由于涉及地址的操作,我们需要查看这两个函数的反汇编代码,具体内容可以通过图中的命令得到。
我们节选了相关部分的反汇编代码,具体如图所示。

这一条call指令对于multstore函数的调用
指令call不仅仅是将函数multstore的第一条指令的地址写入到程序指令寄存器rip中,以此实现函数调用,同时还要将返回地址压入栈中,这个返回地址就是函数multistore调用执行完毕后,下一条指令的地址。
当函数multistore执行完毕,指令ret从栈中将返回地址弹出,写入到指令寄存器rip中,函数返回,继续执行main函数中的相关操作。

以上整个过程就是函数调用与返回所涉及的操作

说完了返回地址,我们再来看一下参数传递。
utDtMB20220120time222412
如果一个函数的参数数量大于6,超出的部分就要通过栈来传递。
假设参数P有n个整形参数,当n的值大于60时,参数7至参数n需要用到栈来传递。参数1至参16的传递可以使用相应的寄存器,具体如图所示。
HG9XS220220120time223206
接下来,我们看一个参数传递的示例。
图中的函数proc有8个参数,包括字节数、不同的整数以及不同类型的指针。
参数1至参数6是通过寄存器来传递的,参数7和参数8是通过栈来传递的
这里有两点,需要注意一下。第一通过栈来传递参数时,所有数据的大小都是向8的倍数对齐。虽然变量a4只占一个字节,但是仍然为其分配了8个字节的存储空间。由于返回地址占用了栈顶的位置。
所以这两个参数距离栈顶指针的距离分别为8和16
具体如图所示
oGcv1020220120time223634
另外还有一点需要注意,就是使用寄存器进行参数传递时,寄存器的使用是有特殊顺序规定的。
此外,寄存器名字的使用取决于参数传递的大小,如果第一个参数大小是4字节,需要用寄存器edi来保存。
2KhUgh20220120time223716当代码中对一个局部变量使用地址运算时,此时,我们需要在栈上为这个局部变量开辟相应的存储空间。

接下来,我们看一个与地址运算符相关的例子。函数caller定义了两个局部变量arg1和arg2。
P6qe4220220120time224032
函数swap的功能是交换两个变量的值,最后返回二者之和,我们通过分析函数caller的汇编代码来看一下地址运算符的处理方式
第一条减法指令将栈顶指针减去16,它表示的含义是在栈上分配16个字节的空间,具体如图所示。
RPUKx620220120time224327
根据这两条mov指令可以推断出变量arg1和arg2存储在函数caller的栈帧上
接下来分别计算变量的arg1和arg2存储地址。
参数准备完毕。执行call指令调用swap函数,最后函数caller返回之前。通过栈顶指针加上16的操作来释放栈帧。

我们再来看一个稍微复杂的例子,

根据图中的C代码,我们来画一个这个函数的栈帧。
根据变量类型可知$x_1$占8个字节,$x_2$占4个字节,$x_3$占2个字,$x_4$占一个字节。
因此这四个变量在栈帧中的空间分配如图所示。qHCSAJ20220120time224827
由于函数proc需要8个参数,因此参数7和参数8需要通过栈帧来传递。
注意,传递的参数需要8字节对齐,而局部变量是不需要对齐的。
从上面的例子我们可以看到,当函数运行需要局部存储空间时,栈提供了内存的分配与回收机制。
在程序执行的过程中,寄存器是被所有函数共享的一种资源。为了避免寄存器使用的过程中出现数据覆盖的问题,处理器规定了寄存器的使用惯例,所有的函数调用都必须遵守这个惯例,对于16个通用寄存器。除了寄存器rsp之外,其他15个寄存器被定义为调用者保存和被调用者保存,具体如图所示

qb5InL20220120time224920接下来我们看一个栈保存寄存器数值的例子。
FetukP20220120time225307
由于调用函数Q需要使用寄存器rdi来传递参数,因此函数P需要保存寄存器rdi中的参数x
保存参数x使用了寄存器rbq,根据寄存器使用规则寄存器rbq被定义为被调用者保存寄存器
所以便有了开头的这条指令不是pushq %rbp
至于pushq %rbx也是类似道理
在函数P返回之前使用pop指令恢复寄存器rbp和rbx的值
由于栈的规则是后进先出,所以弹出的顺序与压入的顺序相反

最后,我们再来看一个递归调用的例子。
ptEJld20220120time225417图中的这段代码是关于N的阶乘的递归实现。
我们假设n=3时,看一下汇编代码的执行情况,
由于使用寄存器rbx来保存n的值。
根据寄存器的使用惯例,首先保存寄存器rbx的值,由于n=3,所以调整指令jle不会跳转到L35处执行。
指令leaq是用来计算n-1,然后再次调用该函数。
注意,此时寄存器rbx内保存的值是3,指令pushq执行完毕后,栈的状态如图所示,继续执行,直到n=1时,程序跳转到L35处执行pop操作。
通过这个例子,我们可以看到递归调用一个函数的本身与调用其他函数是一样的,
每次函数调用都有它自己私有的状态信息,栈分配与释放的规则与函数调用返回的顺序也是匹配的。
不过当n的值非常大时,并不建议使用递归调用。至于原因应该是一目了然了。

数组的分配和访问(开始停更!)

以下内容未整理,请无视。

这一期我们主要介绍数组的相关知识,首先看几个数组的例子。数组a是由八个h r类型的元素做成,每个元素的大小是一个字节假设数组。A的其实地址是x a,那么数度元素a s地址就是x a加r I,我们再来看一个I n t类型的数组数组。B是由四个整数组成,每个元素占四个字节,因此数组臂总的大小为16个字。节假设数组臂的七始地址是x b,那么,数组元素。B,I的地址就是x b加4I,在c言中允许对指针进行运算。///huanhang//////huanhang///例如我们声明了一个指向差类型的指针。P和一个指向I n t类型的指针。Q为了方便理解,我们还是把内存抽象成一个很大的数组假设指针p和指针q都指向0x100处,现在分别对指针p和指针q进行加一的操作指针。P加一后指向0x101处,而指针q加一后指向0x104处,虽然都是对指针进行加一的运算,但是得了结果却不同。这是因为对指针进行运算时计算结果会根据该指针引用的数据类型进行相应的伸缩。接下来,我们看一个例子,///huanhang//////huanhang///我们订义了一个数组意,假设这个数组存放在内存中,对于数组的每一个元素都有两个属性,一个属性是它存储的内容,另一个属性是它的存储地址说白了就是它是啥放在哪儿?对于元素的存储地址可以通过取地址运算服务来获得具体如出所示。通常我们习惯使用数组引用的方式来访问数组中的元素,例如可以使用图中的表达式来访问数组中的元素。除此之外,还有另外一种方式,具体如图所示,其中表达是一加二表示数组第二个元素的存储地质大写字母意表示数组的起始地址,///huanhang//////huanhang///此处加二的操作与指针加二的运算类似,也是与数据类型相关,这里需要注意一下指针运算服星号可以理解成从该地指处取数据。指针是c语言中最难理解的部分。当我们理解了内存地址的概念之后,可以发现指针,其实就是地址的抽象表述。接下来,我们看一下千套数组的相关知识,千套数组也被称为二位数组。图中,我们声明了一个数组a数组a可以被看成一个五行三列的二维数组。这种理解方式与举证的排列类似具体如图所示。在计算机系统中,///huanhang//////huanhang///我们通常把内存抽象为一个巨大的数组,对于二维数组在内存中,是按照行优先的顺序进行存储的。基于这个规则,我们可以划出数组a在内存中的存储情况,关于数组的理解还有一种方式,就是可以把数组a看成一个有五个元素的数组,其中每个元素都是一个长度为三的数组,这便是千套数组的理解方式。无论用何种方式来理解数组元素在内存中的存储位置都是一致的。下面,我们来看一下。数组元素的地址是如何计算的,对于数组地任意一个元素都可以通过图中的计算公式来计算地址,///huanhang//////huanhang///其中x d表示数组的其示地址。L表示数据类型。T的大小。如果t是I n,t类型,l就等于四t是h,r类型,l就等于一。在具体的势例中,c I g都是常数。根据图中的计算公式,对于五乘三的数组a及任意元素的地址可以用x a加上四乘以三I加g来计算假设数组,其实地址x a在寄存器I d I中,所以值I和g分别在寄存器r s I和I d x中,我们可以使用图中的汇编代码,将元素a I g的值复制到寄存器。///huanhang//////huanhang///E a x中,具体如出所适,接下来我们看一下编译器对定场多位数组的优化,首先使用图中的方式将数据类型f e x m x声明为16乘以16的整形数组通过抵f I,声明将n与常数16关联到一起之后的代码中,就可以使用n来代替常数16。当需要修改数组的长度时,只需要简单的修改声明即可。图中的这段代码是用来计算举阵a的d I行与举阵b的d k列的内机,此处为了方便表述举阵的下标与代码的下标并不匹配,仅为辅助理解的示意图。///huanhang//////huanhang///接下来,我们看一下如何使用汇编代码访问数组元素。由于编译器对相关的操作进行了优化,因此这段后编代码有些晦色难懂,再进行循环操作之前,图中的这四行互编代码是用来计算三个数组元素的地址,一个是数组a d r行首个元素的地址,另外两个分别是数组b d k列的第一个元素和最后一个元素的地址,然后将这三个地址分别放到不同的寄存器中,具体如足所示,为了方便表述这里,我们引入三个指针来记录这三个地址。接下来,我们介绍一下循环的实现。///huanhang//////huanhang///首先读取指针a p t r指向元素的数据,然后,将指针a p t r指向的元素与指针。B p t r指向的元素相乘,最后将乘积结果进行累加结果保存到寄存器。E x中计算完成之后,分别移动指针a p t r和b p t r指向下一个元素。由于I n t类型占四个字界,对寄存器I d I加四的这个操作对应于移动指针。A p t r指向数组a的下一个元素。由于数组b一行元素的数量为16个,每个元素占四个字减,///huanhang//////huanhang///因此相邻猎元素的地址相差为64个字节。对寄存器r c x加64的操作对应于移动指针。B p t r指向数组b的下一个元素判断循环结束的条件是指针b p t r与指针b n的是否指向同一个内存地址,如果二者不相等,继续跳转到l七处执行,如果二者相等循环结束。通过这段汇编代码,我们可以发现编译器使用了很巧妙的方式来计算数组元素的地址。这些优化方法显著地提升了程序的执行效率。在c八九的标准中,程序员在使用变长数组时,///huanhang//////huanhang///需要使用麦l o这类函数为数组动态的分配存储空间,在I s o c九九的标准中引入了变长数组的概念,因此我们可以通过图中的方式来声明一个变长数组,它可以作为一个局部变量,也可以作为函数的参数。当变长数组作为函数参数时,参数n必须在数组a之前便长数组元素的地址计算与定常数组类似不同点在于新增了参数。N需要使用惩罚指令来计算。N乘以癌,还是举针a与举针b内积的例子,如果采用变长数组来存储举阵a和举阵b与定常数组相比,///huanhang//////huanhang///c带码的实现几乎没有任何差别,不过对比二者的汇编代码可以发现,编译器采用了不同的优化方法,无论是采用何种优化方法,都显著地提高了程序的性能。今天的内容就到这里,我们下期见。///huanhang//////huanhang///这一期视品我们来介绍一下c语言中的结构体,首先,我们来看一下结构体的声明。图中的这个结构体包含四个字段,两个I t类型的变量,一个I n t l类型的数组和一个I n t类型的指针,我们可以画出各个字段相对于结构体,其是地址处的字节偏移。从这个图上可以看出,数组a的元素是嵌入到结构体中的。接下来,我们看一下如何访问结构体中的字段,例如,我们声明一个结构体类型的指针变量,而它指向结构体的歧始地质假设而存放在寄存器。///huanhang//////huanhang///R d I中可以使用图中的汇编指令,将字段I值复制到自传这种首先读取字段癌的值。由于自段癌相对结构体其实地质的偏移量为零,所以字段癌的地址就是r的值,而子段g的偏一量为四,因此需要将r值加上偏一量四,对于结构体中的数组a可以通过图中的指令来计算任一个数组元素的地址,其中结构体指针r存放在寄存器,而d I中数组元素的索引值癌存放在寄存器r s I中最后地址的计算结果存放在寄存器r x中。综上所数,无论是单个变量,///huanhang//////huanhang///还是数组元素,都是通过其视地址加偏量的方式来访问。接下来,我们看一下关于结构体数据对齐的知识,对于图中的结构体,它包含两个I n t类型的变量和一个h r类型的变量。根据之前学过的知识,我们会直观地认为该结构体占用九个字节的存储空间,但是当使用塞造法数对该结构体的大小进行求职,使得到结果确实12个字节原因是为了提高内存系统的性能系统,对于数据存储的合法地址做出了一些限制。例如,变量g是I n t类型占四个字,///huanhang//////huanhang///简,它的起始地址必须是四则倍数,因此编译器会在变量c和变量质之间插入一个三个字节的间隙。这样变阳g相对于起始地址的偏移量就为八。整个结构体的大小就变成了12个字节。对于不同的数据类型地址对其的原则是任何k字节的基本对象的地址必须是k位数,也就是说,对于t类型,起始地址必须是二的倍数,对于占八个字节的数据类型起始地址必须是八的倍数。基于图中的规则。编译器可能需要在字段的地址空间分配时插入间隙一次,保证每个结构体的元素都是满足对其的要求。///huanhang//////huanhang///除此之外,结构体的末端可能需要填充建隙还是刚才的这个结构体我们可以通过调整自钻g和自端c的排列顺序,使得所有的字段都满足了数据对其的要求,但是当我们声明一个结构体数组织分配九个字节的存储空间是无法满足所有数组元素的对其要求,因此,编译器会在结构体的末端增加三个字节的填充,这样一来,所有的对其限制就都满足了。根据上述对其原则,我们看一个复杂的势例,对于图中的这个结构体,我们可以画出所有字段歧始地址的偏移量变量。///huanhang//////huanhang///A是一个指针变量占八个字,减变量b是邵t类型占两个字间,它其实地址的字界偏一量是八。满足对其要求,由于变量c是b类型占八个字节,因此该变量的其视地址的偏移量需要是八的倍数,所以需要在变量b之后插入六个字节的间隙,对于变量地只占一个字节顺序排列即可。由于变梁e占四个字间,它的偏移量需要是四的倍数,因此需要在变量地之后插入三个字节的间隙,具体排列情况如此所是同样,对于变量f是差,r类性顺序排列即可。由于变量g占八个字节,///huanhang//////huanhang///因此需要在变量f之后插入七个字节的间隙,最后一个变量h占四个字节,此时结构体的大小为52个字间。为了保证每个元素都满足对其要求,还需要在结构起的尾端填充四个字节的间隙,最终结构体的大小为56个字节。此外,关于更多的数据对其的情况,还需要针对不同型号的处理器以及编译系统进行具体的分析。C语言中还有一种数据类型,联合体与结构体不同联合体中的所有字段共享同一存储区域,因此,联合起的大小取决于它最大字段的大小变量,///huanhang//////huanhang///微何处左癌的大小都是八个字节,因此,这个联合体占八个字节的存储空间联合体的一种应用情况是,我们事先知道两个不同字段的使用是互斥的。那么我们可以将这两个字段声明为一个联合体,例如,我们定义一个二叉数的数据结构,这个二叉数分为内部节点和叶子节点,其中每个内部节点不含数据都有指向两个孩子。节点的指针,每个叶子节点都有两个d b o l类型的数据值,我们可以用结构体来定义该二叉数字节点。那么每个节点需要32个字间。///huanhang//////huanhang///由于该二叉数的特殊性,我们事先知道该二叉数的任意一个节点不是内部节点就是叶子节点,因此我们可以用联合体来定义节点具体如出所事,这样每个节点只需要16个字节的存储空间相对于结构体的定义方式可以节省一半的空间。不过这种编码方式存在一个问题就是没有办法来确定一个节点,到底是叶子,节点还是内部节点?通常的解决方法是引入一个枚举类型,然后创建一个结构体,它包含一个标签和一个联合体,具体如出所事,其中t b类型占四个字界联合体占16个字节,///huanhang//////huanhang///t p和联合体之间需要加入四个字节的间隙,因此,整个结构体的大小为24个字节。在这个例子中,虽然使用联合体可以节省存储空间,但是相对于给代码编写所造成的麻烦。这样的节省意义不大,因此,对于有较多自段的情况,使用联合体带来的空间节省会更吸引人。除此之外,联合体还可以用来访问不同数据类型的为模式,当我们使用简单的强制类型转换,将搭保类型的数据转换成昂塞l类行式,除了d等于零的情况,二者的二进之位表示差别很大,///huanhang//////huanhang///这时我们可以将这两种类型的变量声明为一个联合体,这样可以以一种类型来存储,以另外一种类型来访问变量,u和地就具有了相同的位。表示虽然平时编程时很少用到联合起,但是在一些特殊的场景中仍旧可以看到他的身影,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在之前的课程中,我们了解了站的相关知识战争中,会保存程序执行所需要的重要信息,例如返回地址以及保存的寄存器的值等。在c语言中对数组的引用不会进行任何的边界检查。如果对越界的数组进行写操作,就会破坏存储在战中的状态信息。当程序使用了被修改的返回地址时,就会导致严重的错误。接下来,我们通过一个代码势力看一下什么是缓冲区一出,例如图中的依靠函数声明了一个长度为八字字。负数组盖函数是c语言标准库中定义的函数,///huanhang//////huanhang///它的功能是从标准输入读入一行字符串,在遇到回车或者某个错误的情况时,停止盖次数将这个字符串复制到参数八指明的位置,并在字符串结束的位置加上那字符。注意该弹数会有一个问题就是他无法确定是否有足够大的空间来保存整个字符串。长一些的字符串可能会导致站上的其他信息被覆盖。通过汇编代码,我们可以发现,实际站上分配了24个字节的存储空间。为了方便表述,我们将占的数据分布画了出来,其中自负数组位于占顶的位置,实际上当数入字符串的长度不超过二十三十,///huanhang//////huanhang///不会发生严重的后果。超过以后返回一止以及更多的状态信息会被破坏,那么返回指定会导致程序跳转到一个完全意想不到的地方。历史上许多计算机病毒就是利用缓冲区溢出的方式,对计算机系统进行攻击。针对缓冲区溢出的攻击,现在编译器和操作系统实现了很多机制来限制入侵者。通过这种攻击方式来获得系统的控制权,例如战随机化占破坏检测以及限制可执行代码区域等。首先,我们来看一下第一种机制占随机划在过去程序的占地址非常容易预测。///huanhang//////huanhang///如果一个攻击者可以确定一个外部服务器所使用的占空间,那就可以设计一个病毒程序来攻击多台机器占随机化的思想是站在位置,在程序每次运行时都有变化。图中的这段代码只是简单打印书中局部变量l o o的地址,每次运行打印结果都可能不同,在64位零系统上,地址范围如图所示,因此采用了战随计划的机制,即使许多机器都运行相同的代码,他们的占地址也是不同的。在l x系统中占随机划已经成为了标准行为,它属于地质空间布局随机化的一种简称a s l r采用a s l r每次运行时,///huanhang//////huanhang///程序的不同部分会被加载到内存的不同区域。这类技术的应用增加了系统的安全性,降低了病毒的传播速度。接下来,我们看一下第二种保护机制占破坏检测编译器会在产生的汇编代码中加入一种战保护者的机制,来检测缓冲区域界,就是在缓冲区与占保存的状态值之间存储一个特殊值。这个特殊值被称作金丝雀值之所以叫这个名字,是因为从前煤矿工人会根据金丝雀的叫声来判断煤矿中的有毒气体的含量。吉丝雀值是每次程序运行时随机产生的,因此攻击者想要知道这个金丝雀值具体是什么并不容易在寒数返回之前检测金丝雀值是否被修改来判断是否遭受攻击?///huanhang//////huanhang///接下来,我们通过汇编代码看一下编译器是如何避免战役出攻击的图中的这两行代码是从内存中读取一个数值,然后将该数值放到站上,其中这个数值就是刚才提到的金丝雀值存放的位置与程序中定义的缓冲区是相邻的,其中指令的源操作数f s冒号40可以简单的理解为一个内存地址。这个内存地址属于特殊的段,被操作系统标记为止读,因此攻击者是无法修改金丝雀值的。函数。返回之前,我们通过指令x o r来检查金确值是否被修改,如果金丝雀值被修改,///huanhang//////huanhang///那么程序就会调用一个错误处理历程,如果没有被修改,程序就正常执行最后一种机制是消除攻击者向系统中插入可执行代码的能力,其中一种方法就是限制哪些内存区域能够存放可执行代码以前差八六的处理器将可读和可执行的访问控制合并称一位标志,所以可读的内存液也都是可执行的。由于站上的数据需要被读写,因此站上的数据也是可执行的。虽然实现了一些机制,能够限制一些业可读且不可执行,但是,这些机制通常会带来严重的性能损失,///huanhang//////huanhang///后来处理器的内存保护引入了不可执行位,将可读和可执行的访问模式分开了。有了这个特性占可以被标语成可读和可写,但是,不可执行。检查液是否可执行有硬件来完成,效率上没有损失以上我们介绍了三种常用的机制来减少针对缓冲区溢出的攻击。这三种机制都不需要程序员做任何额外的工作,都是通过编译器和操作系统来实现的。单独每一种机制都能降低漏洞的等级组合起来,使用更加有效。不幸的是,仍然有方法能够对计算机进行攻击视频。///huanhang//////huanhang///至此,第三章的内容已经接近尾声,考虑到大家可能对福典部分兴趣不大,下一期我们将进入祭四章处理器的学习,今天的视频就到这里,我们下期见。///huanhang//////huanhang///从这一期开始,我们进入第四章的学习。这章主要介绍了处理器的相关知识。首先,我们介绍一下为什么要学习处理器设计?对于软件开发人员深入理解计算机系统有注意程序的开发和优化理解处理器是如何工作的,能够帮助理解整个计算机系统。虽然真正从事处理器设计工作的人数并不多,但是许多人从事智能硬件的相关工作,对于嵌入式系统的设计者需要了解处理器是如何工作的。随着国家对集成电路的大力支持,芯片设计与制造的相关工作机会也在增加,///huanhang//////huanhang///因此,从事处理器的设计工作会是一个不错的选择。此外,处理器的设计包括了许多好的工程师践原理,它需要完成复杂的任务,而结构设计又要尽可能地简单和规则。综上所述,了解处理器的工作原理是有必要的。在剖析处理器的内部结构之前,我们先来介绍一下指令系统结构指令系统是计算机软件和硬件的交互接口。程序员根据指令系统来设计软件处理器设计人员根据指令系统实现硬件。由于差八六指令系统过于复杂,为了方便学习和理解c s a p p的元书中参照了差八六的指令系统资定义了一个相对简单的指令系统外八六。///huanhang//////huanhang///该指令系统包括定义各种状态单元指令集以及他们的编码编程规范以及异常事件处理这几部分。首先,我们来看一下什么是程序员的可见状态。这里的程序员既可以适用汇编代码写程序的人,也可以是产生机器级代码的编译器。可见状态是指每条指令都会去读取,或者修改出理器的某些部分,例如内存寄存器条件码,程序计数器以及程序状态等等。在外八六指令系统中,我们定义了15个64位的程序寄存器,相对于差八六的指令系统少了一个寄存器。///huanhang//////huanhang///R15主要是为了降低指令编码的复杂度,稍后再指令编码的部分可以体会到在外八六指令系统中,寄存器r s p也是被定义为战指针其他14个寄存器没有固定的含义。除此之外外,八六的指令系统还简化了条件。马寄存器仅保留了三个条件码,分别为零。标志符号标志(SF)和溢出标志(OF)。忘记条件码是什么的同学可以去复习一下三杠五的内容。至于程序计数器,p c是用来保存当前正在执行指令的地址。注意是指令的地址,不是指令的内容,关于程序的执行状态,///huanhang//////huanhang///我们引入状态码来表示。稍后会有详细的讲述。在目前这个状态,我们可以简单的认为虚拟内存系统向外八六程序提供了一个单一的字节数组映射第九章,我们将详细的讲述虚拟内存的相关内容。在第三章的学习中,我们介绍了一些常用的指令类比差八六的指令机外八六的指令机做了一些相应的简化,我们将插八六中的目乌q指令分成了四种不同的指令具体如图所示重定义后的数据传送指令显示的指明了源操作数和目的操作数的格式指令名字的第一个字母表明了源操作数的类型。///huanhang//////huanhang///源操作数可以是立即数寄存器或内存指令名字的第二个字目指明了目的操作书的类型。目的操作数可以是寄存器或内存,这样设计的目的主要是为了降低处理计实现的复杂度,接下来,我们对上述数据传送指令进行编码,每条指令的第一个字节表明指令的类型这个字节分为两部分,每一部分占四个,比特位高四位,表示指令代码,第四位。表示指令功能对于我们定义的数据传送指令不同的指令代码表示不同的指令指令的功能部分都为零。当指令中有寄存器类型的操作数时,///huanhang//////huanhang///会附加一个字节这个字节被称为寄存器,只示符字剪它用来指定一个或者两个寄存器,因此还需要对寄存器进行编码。在外八六的指令系统中,我们定义了15个寄存器。虽然每个寄存器定义了不同的名字,但是还需要为每一个寄存器只定一个编号。寄存器的编号可以用16进之数。零是零x e来表示具体,如出所事注意,如果指令中某个寄存器字段的值为零x f表示此处没有寄存器操作数,例如I r q指令中的第一个操作数是立即数,所以此处用零x f来填充在外八六指令系统中,///huanhang//////huanhang///我们定义了四条整数操作指令,它们只能对寄存器数据进行操作,而差八六还允许对内存数据进行操作。由于这四条指令属于同一类型,所以指令代码是一样的。不同的是功能部分,具体如图所示跳转指令一共有七条。跳转的条件与差八六中的跳转指令是一样的,都是根据条件码的某种组合来判断是否进行跳转条件传送指令有六条,它与数据传送指令,而r m q有相同的指令格式,但是只有条件码满足条件时才会更新目的寄存器的值。停止指令可以使整个系统暂停运行。///huanhang//////huanhang///N不指令表示一个空操作他们的指令编码只有一个字节比较简单,靠指令与r t指令分别实现函数的调用和返回p视指令和指令分别实现入战和出站的操作。这四条指令与差八六中的指令定义类似。综上所述外八六的指令集以及编码规则的定义就完成了。通过上述定义的指令编码规则,我们可以将外八六的汇编代码翻译成二进制表示例如图中的这条指令根据r m o q指令的编码定义指令二g制表示的第一个字结为零x四零。接下来我们再来看指令的操作数部分,///huanhang//////huanhang///根据寄存器的编号规则寄存器r s p对应的16进制数是零x四基址寄存器。R d x对应零x二,因此我们可以得到第二个字节的编码为零x四二。指令编码中偏一量占八个字节,我们需要在该片一样的前面。通过田龄来补齐八个字节。由于差八六采用小端法存储,所以需要对便一量进行字节反蓄操作,最终我们得到了长度为十个字节的二进制指令具体如途是最后我们看一下程序的状态码,他描述了程序执行的总体状态。当代码值为一是表示程序正常执行,///huanhang//////huanhang///而其他三个代码表示程序发生了某种类型的异常代码值。二表示处理器执行了一条停止指令代码值三表示程序正试图从非法地址读取数据,或者向非法地址写入数据代码值四表示程序遇到了非法指令。对于外八六。当遇到异常情况时,我们就简单的让处理器停止执行指令,然而在成熟完整的设计中,处理器遇到了一场会调用异常处理程序在第八章,我们会讲述一场处理的相关内容,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在上一期的视频中,我们讲述了资定义指令系统外八六的相关知识,包括之前的课程中,我们曾多次提到过程序寄存器。从本质上讲,它属于处理器内部的存储单元。通常我们将寄存器的集合称为寄存器文件,有的资料中也称寄存器堆在处理器内部寄存器文件和算术逻辑单元是串联的寄存器文件的输出端口与a r u的输入端口相连,例如执行图中的这条减法指令a l u从寄存器文件中读取操作数,然后执行减法操作,最后将计算结果写入到寄存器文件中。///huanhang//////huanhang///这一期视频我们以寄存器文件为例,通过剖析它的具体实现来阐述处理系设计与数字电路之间的关系,图中展示了一个寄存器文件的功能。表述它有一个独端口和一个写端口端口的数据未宽是64位,我们规定读写操作共用地址线。由于我们定义了15个程序寄存器,因此地址线的宽度设计成四位,即可满足寻止的要求。此外,还有时中信号,负位信号以及鞋使能信号根据上述的功能定义,我们可以使用硬件描述语言对寄存器文件进行行为机建模。通常的硬件描述语言有两种,///huanhang//////huanhang///最常用的是v l g,另外一种是v h d l图中这段弯l g程序就是对寄存器文件的描述,采用电子设计自动化工具,对这段程序进行逻辑综合,得到了电路,就能实现预期的功能。逻辑综合的过程与编译有点类似关于弯l g的知识,我们稍后再讲,先来看一下寄存器文件的内部结构。当执行读取操作时,使用地址线来传输寄存器的编号,多路选择器根据地址信号筛选出寄存器的值,最终数值会通过输出信号输出。为了方便后面的观看这里暂时将始中信号和负位信号省掉。///huanhang//////huanhang///执行写操作需要确定三个参数目的寄存器的I d写入的数据,以及是否能够写入。说白了,就是能不能写往哪儿写写什么?输入数据信号线与每一个寄存器单元都相连,为了方便表述这里我们忽略了数胃宽。能不能执行写入操作,由w e信号线来确定。W e是r a t I n b o e缩写这个例子中地址,信号线是独操作和写操作共用的。经过地址减析后的信号与w w e信号共同来确定对哪个寄存器执行写操作。虽然这张图对寄存器文件内部的描述已经比较想尽了,///huanhang//////huanhang///相信很多同学还会有这样的疑问,多路选择计是怎么实现的虚线框里面的这个寄存器又是如何存储数据的。为了进一步搞清楚这些模块的底层实现。接下来我们看一些数字电路的相关知识。逻辑门是数字电路中基本的计算单元,例如图中的语门,货门绯闻等,它们的输出等于输入值按慰进行相应的布尔运算,具体如图所示。逻辑们实际上是由经期管急电路实现的,在现代计算机中,珍气管通常是指基于c m s工艺的c m s有两种精气管,一种叫n g到m s经器馆简称n管,///huanhang//////huanhang///另外一种叫p构道。M s经机馆,简称屁管恩管和批馆都有三个信号端口分别为三级原级漏基,我们可以将一个批馆和一个恩管串联起来,实现非本具体实现是将二者的陋集连接在一起,作为输出三级连在一起,作为输入,然后将皮管的原籍接电员恩管的原籍接地具体如足所事,当输入为高电,平时恩管倒通批管不倒,通输出为零。当输入为低键,平时批管倒通恩管不倒通输出唯一综上所述就是非门的基本组成以及工作原理。在c m s公寓中,语门和货门实现起来,///huanhang//////huanhang///不如与飞门和霍飞门高效,所以在设计c m s电路的时候,最好使用与飞门货费们以及飞门来实现这些基本的门结构,都可以通过屁管和恩管的组合来实现。接下来我们看一下如何使用基本的门店路,实现一个多路选择器图中是一个双通道,多路选择器的门机,表示l e x等于零时数入端。A的数据可以通过该电路到达输出端,当c l a,c t等于一时输入端。B的数据会到达输出端。通常多路选择器有多条输入通道和一条输输通道,刚才我们看了寄存器文件的内部实现多路选择器可以实现控制某个特定寄存器的内容传输到a r u的输入端,///huanhang//////huanhang///它可以使用基本的门店路来实现,只不过比双通道的要复杂一点。通常情况下,寄存器文件内部的存储部件是由低出发器来实现的及电路符号。如出所视这里我们给出了一种地触发器的门迹实现具体如梭所示。图中虚线框内的部分被称为地所存器,关于地触发器的工作原理,这里就不展开。描述了通过上述的两个势例,可以发现逻辑电路可以使用基本的门店路来搭建,很早以前,电路设计是将一个个门店路绘制在图纸上,但是随着半导体技术的发展,这种方式很难高效地实现大规模的复杂电路。///huanhang//////huanhang///目前电路的逻辑设计通常采用硬件描述语言来实现,然后采用一d a工具进行综合和后端设计。硬件描述语言和综合工具的应用,使得工程师们更多关注硬件功能的设计,而不是单个精体管或者逻辑门的设计。接下来,我们看一下地出发系的w r o g实线,其中第弗利弗l表示模块的名称,第c g表示输入q表示输出图中这条o z语句表示当时中c上升眼的时候,如果g唯一就把输入d的值付给出发器的输出。Q,否则q保持不变。通过这个例子可以发现用v l g来描述电路比逻辑门要简单的多沃尔l g语言的语法跟c语言有很多类似的地方,///huanhang//////huanhang///但是表达了含义与c语言有着本质的区别,我而绕个程序是并行执行的,而c程序是串行执行的,所以硬件设计人员要从电路的角度来理解沃l o g语言,而不是软件的思维学习v l o g语言,首先要搞清楚组合逻辑电路与时序逻辑电路区别。这两种电路的主要差异在于是否还有存处单元,其中组合逻辑电路的输出值仅由当前的输入状态来决定而持续逻辑电路的输出值不仅与当前输入的状态有关,而且与原来的状态也有关。当年教我们体系结构的老师曾说过,///huanhang//////huanhang///他用窝l g做设计,只用三种语句,第一个是俄萨按语句,用于描述组合逻辑。第二个是奥位子语句,用于描述时序逻辑,其中剖在指c l k表示在时中上升。言的时候变化,最后一个是模块调用语句,学会了这三种语句,所有的设计都够了,之后我会专门开一门课来讲述处理器的实现。这里先挖个坑以后慢慢填,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在四杠一的课程中,我们介绍了外八六指令系统的相关内容。由于差八六指定系统较为复杂,为了方便学习和理解外八六指定系统做出了相应的简化处理,其中指令编码的长度从一个字简到十个字节不等,每条指令都含有一个长度为八个比特位的指令指示符,有的指令。还有一个单字节的寄存器指示符。还有的指令还有一个八字节的常数。根据图中定义的指令。通过一个例子,看一下c程序翻译成的外八六汇编代码图中这段代码用来计算一个数组的元素之和只针s t a t指向数组的起始位置。///huanhang//////huanhang///C t用来表示数组的长度。接下来我们看一下这段代码对应的外八六的汇编代码具体如图所示仅从指令的格式来看,除了数据传送指令,其它的指令与差八六指令差异不大,使用外,八六汇编器可以将图中的汇编代码翻译成二进制指令具体如图所示。Y86汇编器的翻译过程是基于外八六指令系统的,因此图中的二进支指令可以运行在外八六的处理器上。由于篇幅限制,我们这里只展示了其中的一部分。第四章的主要内容是设计一个外八六的处理器,///huanhang//////huanhang///用它来执行这些二级制指令。通常处理器执行一条指令包含很多操作实现所有的外八六指令所需要的计算可以被组织成六个基本阶段,分别为取止一码执行仿存,写回以及更新程序技术器。P c首先是取指阶段这个阶段,对于所有的指令都是需要的,在我们定义的外八六指令系统中,指令的长度并不是固定的。取指阶段还会根据指定代码来判断指令是否还有寄存器指示符是否还有常数,从而计算出当前指令的长度。下一期的视频中,我们将详细介绍取止阶段的硬件设计一码阶段比较简单,///huanhang//////huanhang///就是从寄存器文件中读取数据寄存器文件有两个读端口,可以支持同时进行两个独操作。执行阶段。算术逻辑单元主要执行三类。操作。低类操作是执行算数逻辑运算。第二类操作是计算内存引用的有效地址,第三类操作是针对布式指令和剖腹指令,稍后通过具体的指定事力来解释这三类操作。仿存阶段主要是针对内存的毒写操作,既可以从内存中读取数据,也可以将数据写入内存写回阶段与易码间断类似,都是针对寄存器文件的操作不同的是,一码阶段是读寄存器文件写回阶段是写寄存器文件更新。///huanhang//////huanhang///P c是将p c的内容设置成下一条指令的地址以上,我们大致介绍了指令执行的不同阶段,但是并不是所有的指令执行都要经历这六个阶段,接下来,通过几个例子来看一下不同的指令在各个阶段执行的操作,例如图中这条减法指令取止阶段会根据指定代码来判断该指令是否还有寄存器指示符是否包含常数,根据判断结果,可以得出该指令的长度。由于减法指令的源操作数和目的操作数都是寄存器类型的,在一码阶段会根据寄存器指是符来读取寄存器的值。///huanhang//////huanhang///执行阶段。A r u根据一码阶段独到的操作数,以及指令功能来执行具体的运算。除了输出运算结果,还会根据结果来设置条件码。寄存器忘记条件码是什么的同学可以再去复习下三杠五的内容。由于减法指令不需要读写内存,因此仿存阶段不需要任何操作写回阶段是将a r u的运算结果写回到寄存器。rbx最后对程序计数器p c进行更新以上就是减法指令的相关操作。接下来看一下数据传送指令。I r m q这条指令执行的操作是将一个立即数传送给寄存器取止阶段,///huanhang//////huanhang///根据指定代码可以判断该指令既还有寄存器指示符字解,也还有常数字段。由于这条指令不需要从寄算器文件中读起数据,所以一码阶段不需要执行任何操作。从表面上看,数据传送指令只是数据搬运,并不需要a优部件在实际的硬件中,a r u的输出端口与寄存器文件的写入端口相连。该指令在执行阶段使用a r u对立即数执行加零的操作。在写回阶段将运算结果写入寄存器文件,就完成了。数据传送的操作。由于该指令不涉及内存的毒写,///huanhang//////huanhang///所以仿存阶段不执行任何操作执行阶段使用a l u对常数进行加零的操作,需要注意一下,接下来再来看另外一条数据传送指令。R m m q举止阶段和一码阶段。跟前面讲的类似重点来看一下执行阶段a r u根据偏移量和机制寄存器来计算仿存地址仿存阶段,将寄存器r s p的数值写入内存中,内存地址由执行阶段得出。由于不需要写阶算器,所以写回阶段不进行任何操作。通过上面的描述,a r u还可以用来计算内存引用地址,///huanhang//////huanhang///接下来看一下指令p q的操作流程,例如指令p士q r d x取指阶段,根据指定代码来判断。该指令。还有寄存器指示符不含常数,因此指令长度为两个字解。需要特别注意的是,依马阶段,不仅需要读寄存器r d x值。还需要读寄存器r s p的值,这是因为指令p q要将寄存器r d x的值保存到站上执行阶段,会计算内存地址,具体方法是将寄存器r p指向的内存地址进行简八的操作,因此一码阶段还需要读寄存器s p的纸访存阶段会将寄存器r d x的值写到站上,///huanhang//////huanhang///具体如图所示。由于寄存器s p指向的内存地址发生了变化,因此写回阶段需要更新寄存器r s p的值,最后更新p c的值以上就是p q指令各个阶段所进行了操作,最后看一下跳转指令,例如j e零x040取指阶段,根据指定代码来判断该指令。还有常数字段不还有寄存器指示符字剪,因此指令的长度为九个字剪,由于不需要寄存器文件,所以依马阶段不进行任何操作执行阶段。标号为c o n d的硬件单元,根据条件码和指定功能来判断是否执行跳转这个模块,///huanhang//////huanhang///产生一个信号。C n d,如果c n d等于一执行跳转。C n d等于零不执行跳转,注意在更新p c的阶段,如果c n d等于一,就将p c的值设为零x040。如果c n d等于零p c的值,就等于当前的值加上九以上就是跳然指令的执行流程。通过这个统一的框架能够处理不同类型的外八六指令虽然指令的行为大不相同,但是我们可以将指令的处理组织成六个阶段。下一期视频,我们将详细讲述每一个阶段的硬件实现今天的视频就到这里,///huanhang//////huanhang///我们下期见。///huanhang//////huanhang///上一期的视频中,我们讲述了指令执行的基本操作。这一期将详细介绍一下各个阶段的硬件实现。首先是取指阶段取止阶段已程序技术器的值作为歧视地址举止操作,每次从指令内存中读取十个字节,很多同学可能会有疑问,为什么一次要取十个字?借?这是由于在取止操作之前,无法判断当前指令的长度外八六指定系统中最长的指令占十个字。检一次性从内存中取出十个字检可以保证一次取指操作,至少可以获取一条完整的外八六指令,接下来,将这十个字节分成两部分,///huanhang//////huanhang///一部分占一个字节,另外一部分占九个字节图中标号为s p的硬件单元处理第一部分,它将第一个字节分成两部分,每一部分占四个比特位。根据外八六指令系统的定义这两个字段分别为指令代码和指令功能。这里用I扣的和I放表示根据I q的,可以确定当前指令的状态信息。首先可以判断这条指令是否是一条合法指令,如果挨扣的在零倒币之间,那么这条指令就是一条合法指令,如果不是则表示当前指令属于非法指令。此外,根据I q的还可以判断当前指令是否包含寄存器指示符以及是否包含常处字检根据上书的判断结果,///huanhang//////huanhang///就可以算出当前指令的长度,例如,既还有寄存器指示符字剪,又还有常数字剪那么当前指令的长度就是十个字减。如果既不含寄存器,只是符字节也不含常数字剪,那么当前指令的长度就是一个字简。既然通过I扣的可以获得当前指令的长度。那么指令内存中下一调指令的地址,可以通过当前p c的值加上当前指令的长度计算出来,我们继续看一下剩余九个字节是如何处理的。图中标号为俄拉案的硬件单元,可以产生寄存器字段和常数字段。当逆的r c I d等于10,///huanhang//////huanhang///表示该指令包含寄存只是福字剪,那么第一个字节将被分成两部分,每一部分占四个比特位,然后分别装入寄存器指示符。R a和r b中当逆的r c I d等于零时,表示这条指令没有寄存器只是符字件,此时I r b这两个字段会被置为零。X f当指令中只含有一个寄存器操作数时,同样另外一个字段也会被制为零x f如果该指令还有常数而烂,按单元还会产生常数字段,同样需要根据信号逆的r g I d来判断,当你的r g I d等于10第二个字检到第九个字检表示常数字段。///huanhang//////huanhang///当逆的r个I d等于零时第一个字检到第八个字检表示常数。自短。接下来我们看一下一码阶段的硬件设计一码阶段是从寄存器文件中读取数据在外八六处理器中寄算器文件有两个读端口,它支持同时进行两个读操作两个独端口的地址输入为s r c a和s r c b从寄算器文件中读出的数值,通过v r a和v r b输出图中标号为s r z a和s c。B的原角。巨型块,可以产生寄存器的I d值产生寄存器的I d值,需要根据指令代码I q的,///huanhang//////huanhang///以及寄存器指示值。I和r b读取寄存器的数据,需要I和r b比较容易理解,那么为什么还需要指令代码I扣的,呢例如布士指令该指令的寄存器指示符中指含有目的寄存器的I d值。当执行压栈操作时,还需要获得当前占顶指针。R s p的值不仅仅是p视指令,实际上对于图中的这四条指令在一码阶段都是需要读取寄存器r s p的内容,所以一码阶段不仅需要r a和r b信号,还需要I扣的信号。执行阶段的核心部件是算术逻辑单元,///huanhang//////huanhang///简称a r u a r u,根据指定功能来判断对输入的操作数进行何种运算,每次运行时,a r u都会产生三个与条件码相关的信号。零符号溢出,不过我们只希望a r u在执行算数逻辑指令时,才会设置条件码,当a r u计算内存引用地址以及对栈进行操作时,并不会设置条件码,因此图中赛t c c模块会根据指令代码I扣d来控制是否要更新条件码。寄存器标号为c o n d的硬件单元,会根据指令功能和条件码寄存器,///huanhang//////huanhang///产生一个c n d信号,对于跳转指令,如果c n d等于一执行跳转,如果c n d等于零,则不执行跳转。对于a r u不仅可以执行算术逻辑指令。还要涉及内存地址的计算以及战指针的增加或减少的操作。仿存阶段的任务就是从内存中读取数据,或者将数据写入内存中图中的读控制块表明应该进行毒操作写控制块表明应该进行写操作。此外,还有产生内存地址和输入数据的控制块具体如出所示需要注意的是,仿存阶段的最后操作会根据图中的信号来计算状态码写回阶段是将数据写入到寄存器文件两个写端口,///huanhang//////huanhang///分别为m和e对应的地址。输入为d s t e和d s t m。这里需要注意的是,当执行条件传送指令时写入操作。还需要根据执行阶段计算出的c n d信号,当不满足条件是可以将目积寄存器设置为零x f来禁止写入寄存器文件,最后一个阶段就是更新p c的值。P c的值可能有三种情况,第一种情况,如果当前正在执行的指令是,函数调用指令靠那么新的p c,就等于靠指令的常数字段。第二种情况,如果当前正在执行的指令是函数返回指令r t指令r e在访存阶段会从内存中读出,///huanhang//////huanhang///返回地址。这个返回地址就是新的p c值第三种情况,如果当前正在执行的指令是跳转指令,当c n d信号等于一时,也就是满足跳转条件是此时新的p c值等于跳转指令的常数字段,当不满足跳转条件时,跳转指令与其他指令一样,新的p c值等于当前p c的值,加上当前指令的长度以上就是一个外八六处理器的完整设计。不过这种顺序结构存在一个问题,就是指令的执行速度太慢了,始终必须非常的慢,这样才能使得所有操作在一个时中周期内完成。///huanhang//////huanhang///下一期视频,我们将引入流水线来获得更好的性能,今天的内容就到这里,我们下期见。///huanhang//////huanhang///在讲述外八六的流水线之前,我们先介绍一下流水线的通用属性和原理。首先,我们看一个未流水画的硬件设计,在现代逻辑电路的设计中,电路的延迟用皮秒来表示一匹秒等于十的-12次方秒信号从术端经过组合逻辑电路到达。输出端中间会经过一系列的逻辑门。经过一段时间的延迟后得到输出结果,假设信号。经过图中的这个组合逻辑电路的时间是300匹秒。输出信号加载到时中寄存器保存需要20匹秒。那么,整个过程的延迟为320匹秒,///huanhang//////huanhang///我们可以将图中的整个过程抽象为指令的执行延迟,也就是一条指令从开始执行到结束所需要的时间假设三条指令顺序。通过上述组合逻辑单元可以得到一个流水图时间从左向右流动,在这个飞流水化的实现中开始。下一条指令之前,必须完成上一条指令的执行,因此指令的执行不存在相互重叠的情况。这里,为了评估上述系统的执行效率,我们以入吞吐量的概念,假如一条指令执行需要320匹秒。那么,这个系统1秒钟的时间大约可以执行3.12乘以十的九次方调指令,///huanhang//////huanhang///我们将吞吐量的单位定义为每秒千兆条指令,也就是每秒10亿条指令,因此,该系统的最大吞吐量约为3.12g I p s在之前讲述汇编代码时,我们多次提到寄存器相关的操作。这里,我们在讲述电路设计师也用到了寄存器这两种不同的情景下,寄存器一词表述的含义还是有着细微的差别。在电路设计中始终寄存器直接将它的输入和输出。连接到电路中,大多数情况下,寄存器都保持在稳定的状态,假设图中寄存器当前的状态为x。那么产生的输出也等于x当寄存器的输入端产生了一个新的输入。///huanhang//////huanhang///Y是如果时终是低电评,那么寄存器的状态不会立即发生改变,当时终信号低电平变成高店,平时输入信号外才会加载到寄存器,直到下一个时终上升。盐之前寄存器的输出一直就是外,这里需要特别注意一下,假设,我们将图中的组合逻辑单元所执行的操作分成三个阶段信号。经过每个阶段的延迟为100匹秒,然后在各个阶段之间放上流水线寄存器。寄存器的延迟是20匹秒,每条指令的执行都会经过a b c三个阶段处理,我们将十钟周期设置为120匹秒。///huanhang//////huanhang///那么,指令从开始到结束,需要三个完整的始钟周期。接下来,我们看一下指令的流水线图,只要指令一从a阶段进入b阶段之后,就可以让指令二进入a阶段。以此类推。在稳定的情况下,三个阶段都是处于运行状态之后,系统每隔120匹秒就有一条指令。离开系统一条新的指令进入,因此对于该流水化系统的吞吐量大约为8.33g I p s与非流水化的设计相比,系统的吞吐量提高了2.67倍。接下来,我们通过一个例子来看一下流水线计算的时序和操作指令,///huanhang//////huanhang///在流水线个各个阶段的转移是由时中信号来控制的。每隔120匹秒信号从零上升至一流水线开始下一个阶段的计算。接下来,我们重点看一下240匹秒至360匹秒之间的电路活动,当时克零时,此时流水线所有的部件处于空险状态,具体如出所事,首先,我们看一下时钟上升之前时刻239匹秒处的流水线状态。此时指令一经过阶段臂的计算结果已经到达第二个寄存器的输入指令。二经过阶段a的计算结果已经到达第一个寄存器的输入指令。一用青色表示指令,///huanhang//////huanhang///二用金色来表示流水线相应的部分由白色变成了青色和金色。此时第一个寄存器还保存着指令一在阶段a的计算结果,随着时间的推移,当上升时,第一个寄存器的状态有指令一的结果变成了指令。二的结果,在流水线图中,我们用金色代替青色来表示这种情况。第二个寄存器的状态用来保存指令。二在阶段b的执行结果同样,我们用青色代替白色来表示同时阶段a的输入被设置成发起指令。三的计算在经过一段时间的运行,我们看一下时刻359匹秒处的流水线状态指令一经过阶段c的计算,///huanhang//////huanhang///结果已经到达第三个寄存器的输入指令。二,经过阶段臂到达第二个寄存器的输入指令。三,经过阶段,a到达第一个寄存器的输入清色表示指令一金色表示指令。二蓝色表示指令三注意接下来当时中上升时,三个寄存器的值都会发生改变。随着时终周而复始的上升和下降不同的指令。经过流水线的三个阶段,我们可以把寄存器看作各个阶段之间的屏障,因此指应之间并不会相互干扰。上述势力是一个理想状态的流水化系统,实际上会有一些其他的因素影响流水线的效率。///huanhang//////huanhang///对硬件设计者来讲,将一个整体的设计划分成多个延迟都相等的子阶段是一个延峻的挑战。现实情况中各个阶段的延迟可能都是不等的。虽然三个阶段的延迟加起来仍旧是300匹秒,但是始终的速率是受最慢阶段的限制。始终周期需要设置成170匹秒,此时阶段a会有100匹秒的空险阶段,c会有50匹秒的空闲相对于理想情况下系统的吞吐量从8.33g I p s下降到5.88g I p s。此外,流水线还有另外一个局限性,当我们把计算过程分成更多的阶段,///huanhang//////huanhang///是系统的吞吐量也提升了。与三g流水相比,6级流水的吞吐量提升了1.71倍。虽然增加流水线的阶段数可以提升系统的吞吐量,但是过深的流水线同样会导致系统性能的下降。在之前的例子中,我们假设指令之间都是相互独立的,而实际的程序中,指令之间是有相互依赖的,例如图中的这个代码势力,第二条指令I d q的执行需要依赖第一条指令的执行结果,我们将这种情况称为数据依赖,也要数据相关同样,第三条指令也需要依赖第二条指令的计算结果。///huanhang//////huanhang///具体如图所是除了上述提到的数据依赖,还有一种依赖是由于指令控制流造成的控制依赖。例如头中的这一段汇编代码跳转指令会产生一个控制依赖,因为条件测试的结果会决定要顺序执行I q指令,还是执行h t指令在顺序结构的设计中,这些相关是通过反馈来解决的,不过在流水线的系统中引入反馈路径是非常危险的,例如指令四的输入需要依赖指令一的执行结果。为了通过刘水线技术加速系统,我们改变了系统的行为。显然,这种行为是不可接受的,///huanhang//////huanhang///我们必须以某种方式来处理指令间的数据依赖和控制依赖。关于这些问题,我们将在下一期的视频中给出相应的解决方法。今天的视频就到这里,我们下期见。///huanhang//////huanhang///在讲述外八六的流水线实现之前,我们先来复习一下外八六的顺序实现,首先是取止阶段将程序技术器p c的值作为地址,从指令内存中读取指令字节集中,第一个字节被分成两部分,分别为指令代码和指令功能。据说的指令字节中可能还有一个寄存器,只是符字节指明一个或者两个寄存器指示符,还可能还有一个四字节的长数。P j增加器用来计算下一调指令的地址,威r p v r p的值,等于p c加上当前,已经取出指令的长度,接下来是一码阶段。///huanhang//////huanhang///寄存器文件有两个读端口,a和b一次一码操作可以同时读出两个寄算器的值。寄存器文件的读出端口,与算数逻辑单元的输入相连。在执行阶段a r u会根据指令功能来执行指定的运算,从而得到运算结果,同时还会设置条件码寄存器对于跳转指令在执行阶段会根据条件码和跳转条件来产生信号。C n d再次强调一下a r u除了执行算术逻辑指令,还要计算仿存的有效地址,以及针对战指针的运算,因此,a l u的输出端口会与数据内存的逻辑地质单元相连,///huanhang//////huanhang///对于仿存阶段,可以将数据写入内存,或者从内存中读出数据写入的数据,可以由寄存器文件提供,也可以是指令中的常数字段。接下来是写回阶段寄存器文件有两个写入端口。M和e端口。E与a r u的输出端相连。A r u的计算结果可以通过端口e写回到寄存计文件端口。M与数据内存的输出端口相连。从内存中读出的数据可以通过端口m写回到寄存器文件最后一个阶段是更新p c的值,具体p c的值应该如何更新。需要根据当前执行的指令以及执行的状态来进行判断,///huanhang//////huanhang///例如当前正在执行的指令是跳转指令,那么接下来究竟是顺序之行,还是执行跳转。需要根据信号c n d来判断,例如当前正在执行函驻返回指令那么?返回地址就要从内存中得到。此外,指令在执行的过程中,可能会发生异常产生异常的原因,可能是取到了无效的指令,或者是读取内存出现了错误,这些异常信号由s t模块来处理以上就是一个外八六顺序实现的概述更多的实现细节在斯杠三和斯杠四的视频中,有过详细的介绍,这里就不再赘述了对于当前的顺序结构,///huanhang//////huanhang///所有阶段的操作都要在一个时钟周期内完成,例如取至阶段发生在时钟周期刚开始的时候,那么更新p c发生在十钟周期快要结束的时候,接下来我们稍微调整一下。当前的结构设计是更新。P c在时钟周期开始的时候,执行,而不是结束的时候,才执行具体的实现方法是,创建一个寄存器来保存指令。在执行过程中产生的信号,具体如出所事,当一个新的时钟周期开始时,根据寄存器中保存的信号值来计算当前指令的p c值。通过引入寄存器的方式,///huanhang//////huanhang///我们将更新p c的操作从时钟周期快要结束时,运行,移到了十钟,刚开始时,运行。从改造后的整体结构图来看,其它的硬件单元和控制模块都没有发生改变,我们将这种改进方法成为电路。重定时,重庆时只是改变了系统的状态。表示,但是并没有改变它的逻辑行为,接下来我们看一下如何把一个顺序结构改造成流水结构。从宏观的角度来看,改造的原理其实比较简单,我们只需要在顺序结构的各个阶段之间插入流水线寄存器,然后对信号进行重新排列,///huanhang//////huanhang///就可以得到流水结构。接下来我们看一下5级流水的整体设计。流水线寄存器用蓝色的巨型框来表示每个寄存器包含不同的字段,第一个寄存器f用来保存p c的预测值于如何预测地址之后的课程会有详细的讲解取指阶段的逻辑单元与顺序结构类似第二个寄存器d位于取止阶段和一码阶段之间,用于保存刚刚取出指令的信息,例如指令代码指令功能,寄存器指示符等等。这些信息即将进入一码阶段来处理第三个寄存器e位于一码阶段和执行阶段之间,///huanhang//////huanhang///它保存了最新一码指令的状态,以及从寄存器文件中读出的数值。上述信息都保存到了寄存器一种即将由执行阶段进行处理。第四个寄存器m位于执行阶段和仿存阶段之间,它保存了最新执行指令的结果。再次强调一下这里的结果不仅仅是执行算术逻辑单元指令的结果,还有处理跳转指令的分支条件信息最后一个寄存器打w,位于仿存阶段和反馈路径之间仿存执行的结果保存到寄存器w中反馈路径将结果写回到寄存器文件。如果当前执行的是函数返回指令r t。///huanhang//////huanhang///那么,仿存读出的数值就是下一条即将执行的指令地址,所以还需要将该数值题供给p c的选择逻辑以上就是一个5级流水线的硬件结构,这里为了方便理解我们对流水结构做了一定的简化,这个版本的流水线结构会有一些缺陷,在后续的课程中会解决这些问题,今天的视频就到这里,我们下期见。///huanhang//////huanhang///上期的视频中,我们讲述了如何将一个顺序结构改成流水结构。接下来,我们通过一个代码势力来看一下指令在流水线中的执行情况图中,这在代马序列包含五条指令,为了方便表述我们用简化的蓝色举行框表示指令执行的不同阶段,例如f表示取止阶段d表示一马阶段第一个时钟周期执行指令一的取止操作第二个时钟周期进行一码操作指令一的不同阶段将在不同的时中周期内完成。经过五个时钟周期执行完毕,具体如图索事,根据流水结构的特性,第二个始用周期进行指令二的取指操作,///huanhang//////huanhang///此时指令一已经完成了取止操作,释放了取止操作使用的硬件单元,因此可以对指令二进行取止操作,同样经过五个时钟周期后指令二执行完,毕以此类推,剩下了三条指令,按照顺序依次进入流水线执行,最终经过九个时钟周期后五条指令全部执行完毕。这个图描述了不同指令。通过流水线各个阶段的过程时间从左向右在增大。例如在第五个市中周期时,整个流水线中,同时在执行五条指令,不过五条指令都处于不同的执行阶段,具体如足所事,通常会使用指令的流水图来表示流水线的状态,///huanhang//////huanhang///因此需要大家理解这张图所表示的含义。在之前的课程中,我们提到过数据相关和控制相关。例如图中的这段汇编代码,第三条指令I d q的执行需要依赖前两条指令的执行结果。接下来,我们通过流水图来看一下这段指令在执行的过程中会出现什么样的问题。这里我们假设所有的程序寄存器的初始值都为零。图中,这段指令将立一数时和3分别放到寄存器。R d x和r a x中,然后将二者相加。最终结果存放到寄存器。I x中,我们重点看一下第四个时钟周期。///huanhang//////huanhang///此时指令q处于一马阶段,需要从寄存器文件中读取寄存器。I x和r d x的值,我们期望从寄存器r d x中独到数值时,从寄存器r x中读到数值三。然而,实际上从寄存器中读到的值却都是零之所以出现这种情况是因为此时指令一正处于仿存阶段,立即数时还未写回到寄存器。R d x同样指令二处于执行阶段,立即数三也没有写回到寄存器。R x,所以在第四个时钟周期时寄存器。R d x和r a x值还都是默认指零在这个势力中,///huanhang//////huanhang///由于指令I m q和指令I d q之间存在数据相关,导致了流水线产生了错误的计算结果,我们将这种情况称之为冒险或者冲突。与相关一样。冒险也分为两类,一类是数据冒险,另一类是控制冒险。这一器视频我们主要来看一下如何避免数据冒险,避免数据冒险,我们首先想到的是让指令I s q暂停一下。等到指令一和指令二完成写回操作时,我们在继续指令I德q的执行,这样一来就能避免数据冒险。这种解决冒险的方法看起来比较简单,///huanhang//////huanhang///非常容易想到,不过需要判断什么时候执行?暂停操作,如何实现暂停操作以及暂停多久打个比方,你开车在路上正常行驶,需要判断何时要停车,怎么停,以及停多久?判断暂停的方法是,指令在一马阶段读取寄存器时,通过读取寄存器的I d值,分别与执行阶段仿存阶段以及写回阶段所执行指令的目的寄存器进行比较。如果存在寄存器I d值相等的情况,就说明指令之间存在数据相关,那么该指令就要在一码阶段等待对于流水线的执行阶段,原本是要正常执行的指令。///huanhang//////huanhang///暂停之后,通过插入气泡来代替暂停的指令气泡不会改变寄存器内存条件码以及程序的状态图中。红色的巨型框。表示的就是气泡箭头表明执行阶段。插气泡是为了代替指令q之后的课程中,我们会详细的讲述暂停以及气泡的实现。虽然使用暂停技术可以解决数据冒险,但是机这种机制实现的流水线性能并不高,这是因为程序中数据相关的情况非常多。频繁。暂停指令的执行会严重降低流水线的吞吐量。在这个例子中,指令艾德q需要等待指令。I m q将结果写回到寄存器文件之后,///huanhang//////huanhang///再从寄存器文件中读取相应寄存器的数据。再之前的课程中,我们讲过指令I m q的执行细节指令。L m q在访存阶段并没有执行任何操作,那么有没有可能直接把运算结果传给I d q这样一来,指令I d q就不用通过暂停来等待数据写回了具体实现的方法是,可以添加一条信号线,将指令l m q经过a l u执行的结果直接传送到指令。I d q的一码阶段,我们将这种实现技术称之为数据转发,也称旁路数据转发的实现需要在基本的硬件结构中增加一些额外的数据连接和控制逻辑。///huanhang//////huanhang///由于篇幅限制,我们将图中的取止阶段略取。与之前的硬件结构相比,带有转发功能的流水线结构,增加了两个逻辑块,具体如出所视之后的课程中,我们会详细讲述转发逻辑的实现。通过添加旁碌路径能够转发前面指令的结果,这使得流水线可以不用暂停就能处理大多数情况的数据冒险,但是还有一类数据冒险不能单纯的使用转发来解决具体,我们通过一个代码式力来看一下指令。M q需要从内存中读取数据。由于独内存的操作发生在流水线的后期及时采用转发逻辑,///huanhang//////huanhang///也无法将直送回到过去的时间。为了解决这一类数据冒险,我们需要将暂停和转发结合使用指令。I d q在一码阶段暂停一个周期,等到指令。M r q访存结束后,使用旁路路径将仿存结果转发到一码阶段。指令。I d q继续执行具体足所事以上就是通过流水线硬件设计来解决指令间的数据相关问题。在保证指令正确执行的前提下,还能使得流水线保持较高的吞吐量。关于控制冒险的情况,我们将在下一期的视频中讲述今天的视频就到这里,///huanhang//////huanhang///我们下期见。///huanhang//////huanhang///这一期我们主要来看一下如何解决控制冒险。首先,我们来了解一下控制冒险出现的原因。在流水线的设计中,我们期望每个时中周期都能完成一条指令的执行,想要达到这个目的。流水线,在每个时钟周期都要取到一条指令,因此每一次取止。操作后,必须马上确定下一条指令的地址。对于外八六指令系统大多数情况下都能满足上述要求。不幸的是,当取出的指令是返回指令r e t时,下一条指令的地址需要从栈中读出,因此必须等到仿存操作结束后,///huanhang//////huanhang///才能确定下一条指令的地址。当遇到这种情况时,刘瑞宪需要进行特殊的处理来避免控制冒险。除此之外,还有一种情况。当渠道的指令是分支条件,指令是刘水县无法立即判断是否要进行跳转操作,需要经过执行阶段后才能确定是否进行跳转。同样刘瑞县也需要特殊的处理来避免控制冒险。综上所述,在关于外八六的流水线设计中,当取止阶段取到了返回指令和条件分支指令时,由于无法根据当先指令立即确定下调指令的地址,从而引发控制冒险。为了方便表述我们来看一个关于返回指令r t的势例程序图中白色的16进之数表示指令地址第一条指令I r q表示初始化战指针,///huanhang//////huanhang///第二条指令靠表示函出调用函数。P r o c包含两条指令一条返回指令。R t,另外一条是指令r q,不过这条指令并不会执行,为了方便使用流水图来表示返回指令。R e t的执行情况,我们根据指令的执行顺序来调整一下这段指令的顺序,接下来我们看一下这段指令在流水线中的执行情况,第三个时钟周期时返回指令。R e t开始进入流水线执行。由于返回指令r e t在完成仿存阶段后才能得到下一条指令的地址,所以当返回指令r t于一马执行仿存这三个阶段时,///huanhang//////huanhang///刘水县不能进行其他有用的操作,只能在流水线中插入三个气泡,具体,如出所事,直到第七个时钟周期时,返回指令r e t到达写回阶段,此时p c的选择逻辑将读出的地址作为下调指令的取指地址之后,流水线继续执行关于返回指令r t所引发的控制。冒险可以通过暂停处理新指令的方式来避免。接下来我们看另外一种情况,实际上对于条件分支的结果无非是跳转和不跳转两种情况,我们可以假定一个策略预设分支的结果总是跳转或者总是不跳转。///huanhang//////huanhang///这种猜测分支结果的方法被称为分支预测分支预测的准确性,对程序的性能有非常大的影响,这是因为当出现预测错误后,需要采取相应的方法来处理,我们还是通过一个代码式力来看一下。针对分支指令的操作,例如图中的第二条指令g n e需要根据条件码来判断是否执行跳转,如果执行跳转,刘瑞宪需要执行他g t代码断,如果不执行跳转程序顺序执行。这里我们采用一个简单而又粗暴的分支预测策略就是当遇到分支指令时,总是选择执行跳转实际中分支预测的策略要比我们假设的复杂得多。///huanhang//////huanhang///这里主要是为了讲述控制冒险而做一个简单的假设。基于上述假设的分支预测策略,在第三个是钟周期时,刘瑞宪会取出位于跳转目标处的指令,具体如图所示,在第四个时钟周期时,根据跳转后的顺序继续进行取止操作,不过在第四个时钟周期时,指令g n e到达了执行阶段,此时根据执行结果发现不应该执行跳转。显然,我们之前的分支预测发生了错误。根据程序的正常逻辑,这两条指令不应该出现在流水线中,因此应该立即终止他们继续执行。///huanhang//////huanhang///不过幸运的是,这两朝指令还没有进入执行阶段,因此并不会影响程序的正确性,所以我们只需要将这两条指令从流水线中简单地剔除即可。具体的解决方法是,在第五个时钟周期时,对于第一条指令需要在执行阶段插入气泡,对于第二条指令在一码阶段插入气泡即可,同时还要取出跳转指令后面的指令。这样一来,两条预测错误的指令就从流水线中剔除了。虽然没有引发程序的错误,但是分支预测错误导致了两个时钟周期的浪费。总上所述,对于控制冒险的处理方法,///huanhang//////huanhang///主要通过网流水线中插入气泡来解决,因此我们可以通过暂停和插入气泡来动态的调整流水线的状态,从而解决由于指定间的相关问题而导致的冒险,接下来我们看一下暂停和插入气泡是如何实现的。在之前介绍流学线的原理时,我们曾提到过流水线寄存器。流水线寄存器是通过时钟的上升盐来改变输出的值。为了实现暂停和插入气泡,我们为每个流水线寄存器。引入两个控制信号,分别为暂停信号和气泡信号在流水线正常执行时,暂停信号和气泡信号都设置为零,///huanhang//////huanhang///当遇到时中上升延时寄存器会加载它的输入来。作为新的输出。当需要暂停流水线时,需要将暂停信号设为一,此时寄存器会保持它以前的状态,即使遇到时中的上升眼寄存器的输出也不会发生改变,这样一来就可以实现指令。阻塞在流水线的某个阶段中,当需要向流水线中插入气泡时,可以将气泡信号设置为一此时寄存器的状态会设置成某个固定的负位配置,这个负位配置等效于指令。N p的状态,具体如何配置流水先寄存器可以通过一个粒子来说明一下。///huanhang//////huanhang///甲如要往寄存器易中插入气泡,我们要将I扣的字段设置为I n p,并将d s t,e d s t m,s r c以及s r c b设置为r n,这样就可以达到与指令n一样的效果。以上就是暂停和气泡的具体实现方式,今天的视频就到这里,我们下期见。///huanhang//////huanhang///从视频四杠五开始,我们开始介绍外八六的流水线设计。这一期我们重点看一下流水线的各个阶段是如何实现的。首先是流水线的第一阶段取止阶段这个阶段最复杂的地方就是如何预测下一条指令的地址,实际上,指令在具体执行时,无非就是两种情况,一种是顺序执行,另外一种是跳转执行,因此我们可以将外八六指令集所定义的指令分为两大类,假如取指阶段取出的指令属于顺序执行的情况,那么,下一调指定的地址可以通过当前指令的地址加上当前指令的长度计算得出,///huanhang//////huanhang///例如取止阶段从地址0x100处取出了指令I q根据指令集的定义指令I d q长度为两个字简,那么下一条指令的地址就是0x102。为了方便表述后面的讲述中,用v r p来代纸顺序执行时下一条指令的地址,接下来我们再来看一下跳转执行的情况。当取出的指令为函数调用指令或者跳转指令时,p c预测逻辑单元会直接将这两条指令中的长处字段作为下一条指令的地址,对于函数调用指令来说,这种处理方法非常容易理解。至于跳转指令会有跳转和不跳转两种情况,///huanhang//////huanhang///这里我们假定的分支预测策略是总是执行跳转,所以下一条指令的地址就是指令中的常数字段。采用这个策略,主要是为了简化流水线的硬件设计,方便初学者理解实际中分支预测的策略要复杂的多在之后的表述中,我们用v r c来表示跳转执行时下一条指令的地址,如果取指阶段取出的指令是返回指令下一条指定的地址需要从栈中读出。由于返回地址的范围太大,所以在我们的设计中不会尝试对返回地址做预测。既然无法预测p c预测逻辑单元,///huanhang//////huanhang///在预测返回指令的下一条指令时,采用与顺序执行的指嗯一样的方法来简单处理。综上所述,p c预测逻辑单元会根据指令类型来预测下调。指定的地址究竟是v r p还是v r c既然是预测下一条指令的地址那么,肯定会有出错的情况,对于图中的p c选择逻辑单元可以理解为纠错部件。通俗点奖就是p c预测逻辑单元出错了之后的p c选择逻辑单元会根据实际的执行情况来改正预测错误,例如当前取止阶段取道了。返回指令r t下一条指令的地址,///huanhang//////huanhang///还需要等待指令r e t经过e码执行仿存之后,才能从内存中读到正确的返回地制,也就是下一条指令的地址。改正的方法也比较简单。通过判断指令类型,如果是返回指令r e t就把流说线寄存器w中的访存结果作为下一条指令的地址,假如当前取止阶段取道了条件分支指令,那么究竟是否执行跳转,需要等待指令经过一码和执行之后才能判断。B c选择逻辑是通过信号c n d来判断是否进行跳转,如果不跳转证明我们的分支预测错了。///huanhang//////huanhang///需要改正具体改正的方法是,从流水线寄存器m中读取下调指令的地址以上就是取指阶段的硬件设计。至于取止阶段的其他模块,在讲述顺序结构时,有过详细的讲解,汪记的同学可以去复习一下思杠斯的内容。接下来我们看一下流水线寄存器的第二阶段一码阶段一马阶段,主要是根据寄存器的I d从寄存器文件中读取数据寄存器的I d,由字段r a和r b提供。经过s r c,a和s r c b逻辑单元输入到寄存器文件最终一码的结果,///huanhang//////huanhang///用v r a和v r b来表示。为了提高流水线的执行效率,我们引入了数据转发的机制。通俗点讲,数据转发可以直接使用相关寄存器的数据,而不是等企回阶段更新完寄存器的之后,再从寄存器文件中读取。忘记什么是数据转发的同学可去复习一下四杠七的内容,所以一马阶段我们需要判断究竟是直接采用转发的数据,还是需要从寄存器文件中读取数据判断的依据是,根据当前需要读取寄存器的I d值与转发的目的寄存器的I d址是否相等,///huanhang//////huanhang///对于流水线的设计一马阶段,由于加入转发功能,从而导致了硬件设计便得相对复杂。首先,我们看一下究竟是哪些数据,需要转发。第一个转发源是a r u产生的输出结果,如果按照正常指令的流程,这个a r u的输出结果,还需要经过仿存和写回阶段后才能完成寄存器的数据更新。采用数据转发的设计。A r u的输出结果可以马上作为一码阶段的结果,从而避免了等待数据写回寄存器文件后再去读的问题。第二个转发言是内存的输出数据与a r u的输出结果类似,///huanhang//////huanhang///通过转发内存的输出数据,也可以避免等待的问题。第三个转发源是缓存阶段时,对寄存器写入端口翼还没有进行写入的数据。由于篇幅限制,这里我们只画出前三个转发源的信号。第四个转发源是写回阶段时,对继存器写入端口m还没有进行写入的数据。第五个转发源是写回阶段时,对寄存器写物端口翼还没有进行写入的数据,其中每一个转发源包括两部分,一部分是寄存器的I d值用白色的信号线表示,另外一部分是转发数据,用红色的信号线表示其中这五个转发言是存在优先级的转发逻辑单元,///huanhang//////huanhang///首先会检测执行阶段的转发源,然后是仿存阶段,最后才是写回阶段。如果选择了其他的优先级顺序,对于某些程序来说会出现错误。为了方便观看这里,我们将转发信号略取,如果不满足上述所有的转发条件图中的这个逻辑单元,就选择从寄存器文件的a端口读书的数据作为输出,对于图中标号为s l x加的逻辑单元扮演了两个角色。与逻辑单元佛丁b相比,逻辑单元否定a多了一个v r p的输入,它实现了将v r p信号和v r a信号的合并功能,///huanhang//////huanhang///减少了流瑞线寄存器中状态的数量,看到这里有些同学可能会有疑问,为什么这两个信号可以合并呢v r p表示顺序执行时下一条指令的地址,而v r a表示从寄存器文件中读到的数据看起来这两个信号之间并没有什么关系。事实上只有函数调用指令和跳转指令在后面的阶段才需要用到v r p,而这两类指令并不需要从寄存器的a端口读取数据,所以可以根据指令代码I扣d来判断当前指令是否属于这两类,如果是就可以进行合并以上就是流水县一马阶段的全部设计,///huanhang//////huanhang///对于流水线的其他阶段,硬件设计与顺序实现相差不大,这里就不再赘述了今天的视频就到这里,我们下期见。///huanhang//////huanhang///这一期,我们继续讲述y八六的流水,实现为了处理流水线的冒险以及异常情况,我们需要在设计中添加控制逻辑单元。首先,我们来看一下指令执行时出现的特殊情况,以及期望得到的处理。第一种特殊情况是加载使用冒险,例如当前这两条指令顺序出现时,二者之间存在数据相关,因此会产生加载使用冒险。当指令m r q处于执行阶段时,紧随其后的I d q指令处于一马阶段,此时我们期望指令I d q可以阻塞在一马阶段等待指令。M q完成仿存操作后,///huanhang//////huanhang///通过数据转发逻辑来解决这个数据冒险,然后指令I d q继续执行流水线的控制逻辑,不仅需要监测这种冒险的发生,还要使得指令按照期望的方向去执行。具体的解决方法是保持留水线寄存器。F和地的状态不变,同时向寄存器易中插入一个气泡,这样一来,二者之间的冒险就解决了,除了指令。M r q之外,指令o q同样可能产生加载使用的冒险情况。第二种特殊情况是分支预测发生错误是当跳转指令达到执行阶段时,可以检测到预测错误,///huanhang//////huanhang///那么,下一个始终周期需要取消两条已经取道的指令,具体的解决方法是向流水线寄存器地和易中插入气泡,这样一来就可以纠正预测导致的错误。第三种情况是返回指令的处理。当取止阶段渠道返回指令r e时,指令r e t需要经过一码执行,以及仿存阶段之后才能读到下一条指令的地址。虽然取止阶段会不断地读取错误的指令,但是在一码阶段就被替换成了气泡气泡会经过剩下的流水阶段,因此,通过插入三个气泡,就可以达到期望的效果,///huanhang//////huanhang///只不过流水线连续的三个周期都取出了不正确的指令。还有一种特殊的情况是,对异常处理,这里就不展开,讲述了对于以上四种特殊情况的处理,可以通过流水线的控制逻辑来实现。控制逻辑执行的操作,就是在出现特殊情况时,通过暂停和插入气泡来保证程序正确执行。通俗点奖。暂停就是保持流水线寄存器的状态,不改变。插入气泡,就是将寄存器的状态做一个类似清零的操作,关于暂停和气泡的实现方法,忘记的同学可以去复习一下斯杠巴的内容。///huanhang//////huanhang///到目前为止,我们假设任意一个时钟周期内最多只能出现一种特殊情况,然而,实际情况中会有两种情况的组合出现,例如图中这段汇编代码包含了一个分证指令和一个返回指令当分支指令出以执行阶段返回指令处于一码阶段时,由于分支指令并不会执行跳转,因此我们期望执行的指令是I r q,而不是返回指令,因此,针对这种组合的具体解决方法是暂停寄存器f同时向寄存器d和一中插入气泡,幸运的是下一个时钟周期。P c的选择逻辑会选择跳转之后的指令,///huanhang//////huanhang///而不是从p c的预测单元读书的地址,所以无论寄存器f发生了什么都没有关系,这样一来,跳转指令与返回指令的组合冒险就解决了。接下来我们再来看另外一种组合的情况。这种组合包含一个家载使用冒险和一个返回指令,其中加载指令的目的寄存器式r s p返回指令。R t默认使用的寄存器也是r s p,此时指令m q处于执行阶段,返回指令处于一码阶段。针对加载使用冒险刘瑞线的控制逻辑会对寄存器f和d执行暂停操作,同时向寄存器一中插入气泡。///huanhang//////huanhang///针对返回指令刘水线的控制逻辑或得寄存器。F执行暂停操作,同时向寄存器地中插入气泡,从图中可以看出控制逻辑,同时对寄存器d执行了暂停和插入气泡的良操作,这显然是不合理的,实际上,针对寄存器地仅执行暂停操作即可保证程序的正确性,因此我们需要对这种组合进行特殊的处理。综上所述,在设计外八六的控制逻辑时,需要保证在出现上述特殊情况时,程序仍然可以正确运行视频,致此整个外八六的流水设计就讲完了。为了定量分析我们设计的处理器的性能,///huanhang//////huanhang///我们引入一个c p I的概念,它表示一条指令执行所需要的时终周期数,假设处理器在某个时间段内一共处理了c I挑指令和c b个气泡,那么大约一共需要c I加c b个时钟周期大约的意思是,这里忽略了启动指令。通过流水线时的时钟周期数。那么c p I的计算公式,如图所示,通过化简,可以看出c p I等于一加上c b,除以c I这里的c b除以c I可以理解为惩罚项,它表示执行一条指令平均要插入多少个气泡,///huanhang//////huanhang///根据市频开始讲述的特殊处理情况,我们可以将这个惩罚分解成三部分,其中l p表示由于加载使用冒险暂停时插入气泡的平均数。M p表示由于分支预测错误,取消指令时插入气泡的平均数。R p表示返回指令造成暂停时插入气泡的平均数。接下来,我们通过一个例子来看一下如何计算c p I,例如在一个程序中加载指令占所有执行指令的25%,其中20%会导致家载使用冒险跳转指令占所有指令的20%,其中60%会执行跳转40%不执行跳转返回指令占所有指令的2%。///huanhang//////huanhang///根据上述的数据,我们可以计算出三种处罚的总和是0.27,因此得到了c p I为1.27,也就是平均执行一条指令需要1.27个时钟周期。这与我们期望每个市中周期都能执行一条指令相比,虽然有一些差距,但整体的性能已经不错了,如果想要进一步降低c p I,提高处理器的性能,需要降低分支预测的错误,第四章的课程讲到这里,关于处理的设计就介绍完了,对于不从事处理其设计工作的同学而言,搞清楚这一章的内容就够了。///huanhang//////huanhang///针对有志于从事处理器研究的同学,后续会专门开一个关于处理器设计的课程,专门来讲述c p u的实现,今天的内容就到这里,我们下期见。///huanhang//////huanhang///从这一期视频开始,我们进入第五章的学习。这一章的主要内容是优化程序的性能,使程序运行的更快。编写高效的程序需要做到以下几点,首先是选择适当的算法和数据结构。第二点是要理解编译器的能力和局限性,有时候稍微改动一下。远代码可以导致编译器在优化方式上产生很大的改变。第三是处理晕算量特别大的问题是,可以将一个任务划分成多个部分,不同的部分,可以在不同的处理器合上并行的计算关于并行的相关知识会在第12章详细的讲述,///huanhang//////huanhang///接下来,我们通过几个例子来看一下编译器优化程序的局限性,例如图中这两个函数乍一看,二者的功能似乎是一样的,它们都是将指针外僻所指向的树两次夹到指针x p指上的树,不过还说挨二的执行效率更高一些。这是因为还说爱的一需要执行六次内存。引用,其中包含两次毒x p指向的内存位置,两次读外p指向的内存位置,以及两次写x p指向的内存位置,然而还说I的二只需要执行三次内存。引用包含一次读x p指向的内存位置,一次读外p指向的内存位置,///huanhang//////huanhang///以及一次写x p指向的内存位置,既然还数I d二的执行效率比还说爱的一高,那么能不能用函数挨二的计算方式作为函数I的一的优化版本。呢这里我们考虑一个特殊情况,当x p和外p指向的内存位置相同时,函数I z一的执行结果是x p的值增加4倍,而函数I的二的执行结果是,x p的值增加了三倍。当编译期在优化代码时,会假设x p和外p有可能指向同一个内存位置,因此不能用函数挨d二的代码作为函数I的一的优化版本,///huanhang//////huanhang///我们将两个指针可能指向同一个内存位置的情况成为内存别引用,我们再来看一个类似的例子。由于图中这断代码并没有给出指针p和指针k的初始化代码,假设指针p和指针k指向不同的内存位置是第三行代码,用外的值对指针q所指向的内存位置进行了赋值,可以推断出t e的值,就等于3000。当指针p和指针。Q指向同一个内存位置是我们再来看一下这段代码的执行结果。在执行完第三行代码后,此时指针q所指向的内存位置的数值为3000。///huanhang//////huanhang///虽然第四行代码是用x的值对指针p所指向的内存位置进行赋值,但是由于指针p和指针q指向了相同的位置。在这种情况下,t的值等于1000。通过这个例子可以看出,如果编译器无法确定两个指针是否指向同一个位置,那么编译器就会假设所有的情况都有可能发生,这就限制了可能的优化策略。另外一个妨碍优化的因素是函数调用,我们还是通过一个例子来看一下,如果忽略函数f的剧体内容。乍一看,这两个函数的执行结果是相同的,但是放开二调用了f一次,///huanhang//////huanhang///而放课一却调用了f四次。显然放开二的执行效率是高于放可一的,那么能不能把方c的代码作为放个一的优化方式,呢我们假设还说,f的内容是这样的,具体如此所是,其中康特是一个全剧变量,每次调用函数f都会改变康特的值。假设程序开始运行时,变量康特的出使值为零。当函数放个一被调用时,实际执行中对函数f发生了四次调用,每次调用函数f得到了返回值都是不同的,第一次函数f的返回之v零第二次是一,第三次是二,第四次是三,///huanhang//////huanhang///所以还数放个一执行完毕后的返回结果是六,由于函数放个二只调用了一次函数f,所以函数放和二执行的返回结果是四乘以零,因此大多数编译器不会把方和一优化成放个二的形式。由于编译器的优化并不是特别的激进,所以需要程序员花费更多的精力来编写高质量的代码。接下来,我们通过一个例子来看一下如何表示程序的性能。对于一个向量a包含n个元素,其中n是一个变量,现在我们需要计算出向量a的钱置和并且保存在向量p中,关于钱置盒子计算过程是p零等于a零p一等于a零加a一p n减一,///huanhang//////huanhang///等于a零加111直加到a n减一具体,如出所事,现在我们用一个程序来计算向量a的钱置和向量。A的元素存储在数组a中计算结果将存在数组p中,嗯n表示向量的个数函数p一通过一个f循环实现了上述功能,每一次迭代计算一个元素的值,具体如出所事,这种实现方法是比较常用的,也非常容易理解,不过还有更加高效的实现方法。具体如此所是,虽然函数披萨二也是采用了循环来实现。与p萨一不同的是,p萨二一次迭代可以计算出两个元素的值。///huanhang//////huanhang///我们将这种技术称为循环展开后面的课程中会深入讨论循环展开的好处。接下来,我们对比一下函数p萨一和p萨二的性能。当n的数量不断增大时,程序执行所需要的时间也在增大。具体如出所事,其中行坐标表示元素的个数。纵作标表示时终周期数,我们可以使用最小二乘法拟合出两条近似的直线,这两条直线近似的表示出随着数组元素n的增加,两个函数执行所需要的时终周期数的变化情况。函数披丧一的运行时间可以近似地用368加九乘以n来表示函数。///huanhang//////huanhang///P萨马二的运行时间可以金丝的。用368加六成n来表示其中六和九被称为线性因子。从图中可以看出,当恩的值交大运行时间主要由线行因子来决定。为了评估程序的性能,这里我们引入一个新的度量标准。C p e c p e表示每个元素执行所需要的周期数,而不是每次循环所需要的周期数。针对执行重复计算的程序来讲。C p e这种度量标准可以帮助我们更好地理解迭代程序的性能,根据上述度量标准p萨m二的c p e为6.0p萨一的c p e为9.0,///huanhang//////huanhang///所以我们在优化程序的性能是应该集中精力减小计算的。C p e关于优化程序的性能,就先介绍到这里更多内容,我们下期间。///huanhang//////huanhang///这一期我们继续讲述如何优化程序的性能。首先,我们给出一个向量的抽象数据类型,它有两个内存块来表示分别是向量的头部和指定长度的数组,我们可以将向量的头部声明为一个结构体,具体如足所视其中论表示向量的长度,对它更替表示向量元素的数据类型。这样的声明方式方便我们测试相同的程序在处理不同类型的数据时的性能。除此之外,我们还会分配一个长度微论数据类型为d,它杠替的数组来存放向量的元素。首先,我们看一个代码式里,///huanhang//////huanhang///图中这段代码可以实现对向量的所有元素进行求和或者求击的运算,我们可以通过底泛来确定程序执行哪一种运算,当I d n t为零o p为加法符号时,函数执行向量元素的求和运算。当I d因t v一o p为乘罚符号时,函数执行向量元素的乘积运算,其中函数y克楞s是用来获取向量的长度函数。G t m t是用来获取向量的d个元素的值结果保存在变量。V r中,在接下来的视频中,我们会对这段代码进行一系列的优化,为了评估不同版本的程序性能,///huanhang//////huanhang///我们会在一台t l酷瑞I c的机器上测试这些函数的性能。首先,我们看一下函数,肯b胺一的执行效率。实验过程中,我们分别用整型数据和浮电数据进行了加法和橙发的运算,在没有进行任何优化的情况下,我们记录了函数卡八案一的运行时间,这里用c p以来度量c p e表示每个元素执行所需要的池钟周期数,因此c p e越小。表示程序执行得越快,当我们对函数卡八按一在编译时采用杠大o e的优化选项时,通过测试数据可以发现在没有修改原代码的情况下,///huanhang//////huanhang///单纯靠编译器的优化就能够显著提升程序的性能。接下来我们看一下如何通过优化代码来提升程序的性能。在函坦一中否循环的测试条件是通过调用函数y来获取向量的长度。这样一来,每次循环迭代时都要调用这个函数进行求职,实际上向量的长度并不会随着循环的进行而改变,所以我们可以在循环开始之前就调用函数y克楞n s,然后将结果复制给局部变量。认死,这样一来就不用,每次迭代时都执行一次函数调用,我们将这种优化方式成为代码移动。///huanhang//////huanhang///接下来我们看一下经过优化后的程序的执行效率。通过测试数据可以看出,虽然代码改动非常简单,不过预算效率的提升还是比较明显的,可能很多同学会有这样的疑问,为什么编译器无法自动完成代码移动来提升程序的性能,呢这是因为编译器无法判断进行代码,移动后会不会产生副作用。编译器首先要保证程序可以得到正确的结果,其次才是优化出高性能的代码,所以为了提升程序的性能。这类优化需要程序员来完成,我们再来看一个例子。这个程序是将字符串中的所有大写字母转换成小写字母。///huanhang//////huanhang///除了把对函数s r论的调用移到了循环之外,图中。这两个函数是一样的,接下来我们看一下这两个函数运行所需要的时间。图中展示了七个不同长度的字符串的运行时间。对于函数l o一来说,字符串的长度每增加一倍,运行时间都会变为原来的4倍。对一个长度为100万的字符串。函数l w一竟然需要运行17分钟。这个例子说明了程序开发中一个常见的问题,一个看上去并不重要的代码片段却能够导致严重的性能问题,所以一个有经验的程序员应该避免这类问题的出现,///huanhang//////huanhang///既然减少函数调用的次数,可以提升程序的性能,我们可以看到函数卡班二中还有一个函数调用,如果把这个函数调用也优化了,也许性能还会有一定的提升,为了消除这个函数调用,我们引入了一个新的函数,它可以返回数组的歧始地址,经过优化后,函数卡帕三中的循环没有了函数调用,取而代之的是采用直接访问数组的方式来获取向量的元素,我们再来对比一下。函数调用与直接数据访问的性能差异令人遗憾的是,虽然消除了循环中的函数调用,///huanhang//////huanhang///但是程序的性能并没有显著的提升,甚至整数求和的性能还下降了,因此我们可以推断循环中可能存在其他的限制因素,它对性能的限制超过了函数调用,从而导致。虽然我们消除了函数调用,也于事无补。为了找出这个限制因素,我们来看一下函数扛班三的汇编代码。从这段汇编代码可以发现,每次迭代时累计变榔的数值都要从内存中读出,然后再写入内存,其中涉及两次读内存和一丝写内存的操作。实际上这样的毒写操作很浪费时间。为了消除不必要的内存饮用,///huanhang//////huanhang///我们可以引入一个临时变量a c c,它用记录累积的结果,直到循环结束,再把结果写回到目的地址,这样一来,每一次迭代只需要一次独内存的操作。接下来我们看一下。消除了内存引用之后,程序的性能是否有所提升。通过测试数据可以发现,程序的性能有了显著的提高,整数加法运算从每个元素需要7.17个时钟周期下降到了1:27个福建数的乘法也下降到了5.01个时钟周期。C s a p p元书中有一个席题,对函数卡帕三采用了杠大o二的编译选项来优化,///huanhang//////huanhang///可以得到性能与巴斯相当的效果,不过对于整数求和的情况除外感兴趣的同学可以去研究一下。综上所述,经过优化后的程序在计算每个元素时,大概需要1.25到5个十钟周期与最开始需要9到11个时钟周期相比,执行效率有了相当大的提升。那么,究竟是什么因素在制约着代码的性,程序的性能还能进一步提高吗?下一期的视频,我们将从处理器的角度去解释如何优化程序的性能,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在上一期的视频中,我们讲述了如何优化程序的性能,具体采用的优化方法,只是简单的降低函数调用的开销以及消除无必要的内存引用等。如果想进一步提升程序的性能,我们必须考虑利用处理器的微题结构来进行优化,说白了,就是搞清楚现代处理器是如何执行指令的。在第四章的课程中,我们讲述了处理器5级流水的实现。实际上,现在处理器的内部结构要复杂得多,它能同时执行多条指令,我们将这种技术称为指令级并行。接下来,我们看一下,///huanhang//////huanhang///现代处理器是如何实现多条指令并行的,它的整体设计主要分为两部分指令控制单元和执行单元指令控制单员负责从内存中读取指令序列,然后对指令进行一码,从而产生一系列的操作,相对于第四章讲述的一码。操作这里的指令一码要复杂一些。接下来,我们通过一个例子来说明一下,例如图中这条加法指令它的源操作数和目的操作数都是寄存器,因此这条指令会被转化成一个加法操作。当一条指令包含内存引用和算数运算时,例如图中这条加法指令会被一码成三个操作,///huanhang//////huanhang///分别为一个独操作一个写操作以及一个加法操作,其中读操作是从内存中读取数据到处理器中。J a v a操作是将从内存中读到的数值与寄存器中的数值进行相加写。操作是将加法运算得到的结果写回到内存里以上的一码操作对指令进行分拣。允许任务在一组专门的硬件单元之间进行分割,其中独操作由加载单员来执行,而写。操作由存储单元来执行,然后处理器的执行单元就可以并行的执行多条指令的不同部分与第四章所介绍的顺序流水线相比,///huanhang//////huanhang///现在处理器的每个时钟周期可以执行多个操作,而且是乱序执行的乱序执行的意思是允许指令的执行顺序与原始程序中的顺序不一致。当执行遇到分支时,程序有两个可能的执行方向,其中一种可能是选择分支,另外一种可能是不选择分支。现在处理器采用分支预测技术来猜测是否选择分支,同时还会预测分支的目标地址,甚至在不确定分支预测是否正确,之前就开始执行这些指令。如果之后发现分支预测错误,会将状态重新设置到分支点之前的状态,///huanhang//////huanhang///开始执行另外一个方向上的指令,我们将这种技术成为投机执行。当采用这种技术执行指令时,执行结果暂时不会存放到技存器文件或者内存中,直到可以确认应该执行这些指令时,再把结果写回到寄存器或者内存分支操作被指令控制单元送到执行单元。注意这里的分支逻辑单元是用来确定分支预测是否正确,而不是确定分支该往哪里执行,如果预测错误,执行单员会丢弃分支点之后计算出来的结果,执行单元。还会发信号告诉分支单元预测是错误的,///huanhang//////huanhang///并指出正确的分支。此时分支逻辑单元开始在新的位置取指在上一期的视频中,我们在英特尔酷卫埃期的处理器上测试了不同程序的性能。在这款处理器中,一共包含八个功能单元,我们用编号0到7来表示,例如编号唯一的逻辑单元可以执行整书运算整数乘法以及福建书的加法和乘法。这里我们提到的整数运算包含加法未及操作以及一位操作。根据图中所列举的功能。单元,我们可以统计出该处理器有四个功能,单元,可以执行整数运算。分别是单元零单元一单元五以及单元六之后,///huanhang//////huanhang///我们可以看到这些计算资源对程序获得最大的性能所带来的影响。在指令执行单元中,还有一个退役单元,它包含寄存器文件同控制着寄存器的更新指令,在一码时,指令的相关信息被放置在一个先进先出的队列中,这些信息会一直保持在队列中。直到发生下列两种情况中的一个当一条指令的操作完成了,而且所有引起这条指令的分支点也都被确认为预测正确,那么这条指令就可以退役了。所有对程序寄存器的更新也可以执行了。另外一个方面,如果引起该指令的某个分支点预测错误,///huanhang//////huanhang///也就是说,这条指令不应该被执行,那么,这条指令会被清空丢弃,所有计算出来的结果。通过这种方法即使发生预测错误,也不会改变程序的状态,我们再来看一下这款处理器的算数运算性能具体如出所事表格中的数据,有些是通过测试得到的,有些是从英特尔的数据手册中得到的。这个表格中设计了三个名词,延迟发射以及容量延迟。表示完成运算所需要的总时间。例如整数加法需要一个时钟周期,而整数乘法需要三个时钟周期发射时间表示两次运算之间间隔的最小时钟周期数,///huanhang//////huanhang///例如两次整数加法之间需要间隔一个使用周期,而除法需要间隔3到30个时钟周期容量则表示执行该运算的功能单元的数量。例如,当前这款处理器可以同时执行四个加法运算,所以整数加法的容量就是四搞清楚了。这三个名词的含义之后,我们可以发现,福建书的运算相对于整数运算需要更多的时钟周期数对于除法运算,无论是整数除法还是浮点庶除法,它的发射时间都等于延迟,这意味着在开始一条新的运算之前,除法器必须完成整个除法运算需要注意的是,///huanhang//////huanhang///除法运算的延迟和发射时间都是一个范围,而不是精确的时钟周期数,这是因为除发运算需要的时间还依赖于被处数和除数。虽然现在处理器的详细设计超出了本书的讲述范围,但是为了理解如何实现指令级并行,又不得不去了解处理器的内部设计,关于更多处理器的内容,我们后续会开一门课,专门来讲述今天的内容就到这里,我们下期见。///huanhang//////huanhang///在上一期视频的最后,我们介绍了英特尔酷瑞埃妻关于算术运算的延迟发射时间以及容量的特性。这些参数会影响到函数执行的性能。接下来,我们分别用延迟界限和吞吐量界限的c p e值来描述这种影响具体如图所示,对于任何必须严格按照顺序执行的合并运算延迟界限给出了所需要的最小的c p e的值延迟界限比较容易理解,对于吞吐量界线理解起来可能稍微有点麻烦。由于处理器只包含一个整数乘法器,它的发射时间为一个始钟周期,因此处理器不可能支持每个时钟周期大于一条乘法的速度之前的课程中,///huanhang//////huanhang///我们提到过处理器中包含四个整数加法单元。理论上每个周期有可能执行四个整数加法的操作,然而不幸的是,由于需要从内存中读取数据,这就造成了另外一个吞吐量界限,两个加载单元限制了出理器,每个时钟周期最多能够读取两个数据值,从而使得吞吐量的界限为0.5。接下来,我们会展示延迟界限和吞吐量界限,对不同版本的合并函数的影响,在五杠二这一期视频中,我们讲述了如何优化函数,扛办到目前为止。函数卡巴恩寺是运行速度最快的代码。///huanhang//////huanhang///通过图中表格可以看出除了整数j a v a的情况,其他的测量值与处理器的延迟界限是一样的,这并不是巧合,它表明了该函数的性能是有所执行的。求和或者乘积计算来决定的。通常再分析程序的性能时,我们会用到程序的数据流表示这是一种图形化的表示方法。数据流能够展示不同操作之间的数据相关是如何限制他们的执行顺序的这些限制形成了性能的关键路径。在之前的视频中,我们提到过函数卡巴斯的代码实现对于长度较大的向量来说,///huanhang//////huanhang///循环的执行是决定程序性能的主要因素,因此,我们重点看一下这段循环的执行图中,这个循环编译出的代码由四条指令组成,其中寄存器。R d x中存放着指向数组d塔d个元素的指针寄存器。R x中存放着指向数组末段的指针寄存器。差m m零中存放着累积值a c c每次执行乘罚累积之后,运算结果都会存放到寄存器。差。M m零中,接下来我们看一下这段循环代码的图形化。表示顶部的巨型框。表示循环开始时寄存器的值,而底部的巨型框。///huanhang//////huanhang///表示最后的寄存器值,例如循环开始的第一条惩罚指令被扩展成一个加载操作和一个惩法。操作具体如出所事,其中加载操作表示从内存中读取源操作数惩发操作表示执行惩法运算预算结果保存到寄存器差。M m零中,第二条加法指令对寄存器r d x执行加八的操作加v a操作的结果写回到寄存器,r d x指令c m p用来比较寄存器。R d x和寄存器,r x是否相等比较的结果会更新条件码寄存器最后指令g n e根据条件码寄存器的状态,///huanhang//////huanhang///决定循环是否继续以上就是我们根据汇编代码所画出的数据流图。在这个过程中有些操作产生的值不对应于任何寄存器,因此,基于这段循环代码,我们可以将访问到的寄存器分为四类。第一类是指毒寄存器,它可以作为数据,也可以用来计算内存地址,这些寄存器在循环中是不会被修改的,例如寄存器r x就属于只读寄存器,它始终指向数组的结束位置,第二类是只写寄存器。这类寄存器作为数据传送的目的寄存器,不过在我们的例子中并没有用到这一类寄存器。///huanhang//////huanhang///第三类是局部寄存器,这些寄存器在循环内部被修改和使用两次不同的迭代之间是不相关的。在这个例子中,条件码寄算器就属于局部寄存器。C m p操作会修改条件码寄存器的值,然后质音一操作会使用条件码寄存器的值忘记条件码。寄存器的同学可以去复习下三杠五的内容需要特别注意的是,局部寄存器的相关是存在于单次迭代之内的第四类是循环寄存器。这一类寄存器既要作为源操作数,又要作为目地值,也就是说一次迭代中产生的值会在另一次迭代中用到,///huanhang//////huanhang///例如寄存器r d x和寄存器差。M零就属于循环寄存器,因此,循环寄存器之间的操作链成为了限制程序性能的关键因素。接下来,我们对这个数据流图做出进一步的改进。调整的目标是只保留影响程序执行时间的操作以及数据相关的部分,我们对操作服进行了重新排列,调整后的数据流,更清晰地表明了数据从顶部寄存器流向底部寄存器的过程,由于比较操作和分支操作不直接影响程序的数据流,所以我们将这两个操作以及寄存器I x去掉。这样数据流图中只保留了循环寄存器以及关键操作。///huanhang//////huanhang///这样一来,剩下的部分可以看成一个抽象的模板,我们将图中的这个模板重复恩赐就可以得到函数卡巴寺内n赐迭代的数据流表示。通过这张图,我们可以看到,程序有两条数据相关链,分别对应与m u I操作对程序值,a c c的修改以及I子操作对d t加癌的操作的修改,假设。浮建树乘法的延迟为五个始终周期而整数加发的延迟为一个始终周期,那么,左边的链会成为关键路径,需要五n个时钟周期的执行时间,而右边的列只需要n个时钟周期就可以完成,///huanhang//////huanhang///所以左边的这条链是制约性能的关键路径,我们对函数卡巴斯的c p e是结果为1.27,然而根据图中左边和右边形成的相关链来预测的c p e为1.0,所以实际上测试值比预测值要慢一些。这说明了一个问题,那就是用数据流表示的关键路径所提供的指示程序执行周期数的下建,实际上还有一些其他因素会限制程序的性能,例如可用的功能单元的数量以及功能单元之间能够传递数值的数量。当合并运算的操作数为整数时,虽然数据操作足够快,///huanhang//////huanhang///但是其它操作提供数据的速度不够快。如果想要精准地确定为什么?程序中每个元素需要1:27个始钟周期还需要获得出理器更详细的硬件设计才行。接下来,我们的优化会重新调整操作的结构,通过提高指令级的并行来降低c p e今天的视频就到这里,我们下期见。///huanhang//////huanhang///在五杠一的视频中,我们提到过向量潜质和的计算问题,其中函数披上二使用了循环展开就是每次迭代可以算出两个元素的值。这样一来,所需的迭代次数就可以减半使用。循环展开,可以从两个方面提升程序的性能。首先,循环展开,可以减少与程序结果无关。操作的数量。例如当迭代次数减半是循环索引的计算以及条件分支这类操作都会减少其次。循环展开提供了一些方法,能够减少整个计算中关键路径的操作数量。接下来,我们看一下对函数扛把按使用二乘一的循环展开的代码实现,///huanhang//////huanhang///其中二表示每一次迭代处理两个数组元素,因此,每次迭代循环的索引值癌需要加二,而不是家一当向量的长度不是二的倍数时,想要使图中的代码对于任意长度的向量都能得到正确的结果,需要特别注意一下循环的界限问题。为了确保第一次循环不会超出数组的界限,对于长度为n的向量,我们将循环的界限设为n减一,然后保证只有当循环索引值I小于n减一时才会执行这个循环。那么,最大的数组索引值I加一就等于n,通过这种方法,可以解决循环越界的问题。///huanhang//////huanhang///接下来我们看一下这种变换所带来的性能提升。通过测试数据可以看出,对于整数加法,CPU从1.7降到了1.01之所以产生这样的结果是因为减少了循环开销操作,然而对于其他的情况,性能并没有提升,这是因为他们已经达到了延迟界限。简单的循环展开,无法继续降低。Cpe看到这里,相信很多同学会有这样的疑问,如果将每次迭代所能够计算的元素从两个增加到三个,那么程序的性能会不会进一步提升呢,通过测试三成一循环展开的情况,///huanhang//////huanhang///可以看到程序的性能的提升,止步于延迟界限,那么,为什么无法超越延迟界限呢?接下来,我们通过函数扛八五的汇编代码来研究一下原因,二乘一的循环展开会产生两条v m UL SD指令,第一条指令将追她I加到acc上,第二条指令将追她I加一加到acc上,其中循环的索引值癌放在寄存器r DX中,每次循环执行加二的操作。数组的起始地址追他放在寄存器I x中,循环的界限放在寄存器r BP中,在上一期的视频中,我们介绍了数据流图的相关知识。///huanhang//////huanhang///接下来,我们看一下这段汇编代码的图形化,表示每条v m u l s d指令被翻译成两个操作一个操作是从内存中加载一个数组元素。另外一个操作是把这个值乘以已有的累积至第二条微fullhd指令所执行的操作余地一条类似每一条v m u l s d指令都对寄存器叉m零执行了读操作和写操作,因此,每次迭代对寄存器叉m零一共执行了两次读操作和两次写操作。当我们对这张图进行简化以及重排列之后,可以得到如图所示的模板,///huanhang//////huanhang///关于简化的原则以及方法在五杠四的视频中有过详细的阐述,这里就不再赘述了对于一个长度为n的向量的计算,把这个模板复制2分之n次可以得到如图所示的数据流表示。通过这个图可以看出,虽然迭代次数减半,但是关键路径上还有n个乘法操作,因此,无论是否执行循环展开这条关键路径都是性能制约的主要因素。接下来,我们看一下如何通过提高并行性来提升程序的性能。图中,这段代码不仅使用了两次循环展开,而且采用了两路并行的计算方法,///huanhang//////huanhang///其中所以值为偶数的元素累积在变量acc李莹中而索引值为奇数的元素累积在变量acc1中,对于向量长度不约二的倍数。视图中,这个循环要累积剩下的数组元素,最后将acc零和acc1进行合并运算得到最终结果,我们将这种代码的实现方式称为二乘二循环展开。接下来,我们看一下采用两路并行循环展开的执行效率于只做循环展开。相比,所有的情况都得到了提升。整数乘法,福建输家法以及福建书城反的性能有了大幅度的提升,最关键的是,///huanhang//////huanhang///这种方法打破了延迟界限,设下的限制处理器不再需要延迟一个加法和乘法操作来等待前一个操作完成。要想进一步理解程序性能提升的原因,我们还是来看一下函数抗斑六的汇编代码。与函数五相比,还是扛八六的内循环也包括了两个为fullhd运算,但是它不仅用到了寄存器叉m零还使用了寄存器叉。M一接下来我们看一下函数抗斑六的数据流图。通过二乘二的循环展开,整个计算的关键路径变成了两条一条对应于计算索引值为偶数的元素的累积acc零另外一条对应于计算索引为奇数的元素的累积acc1。///huanhang//////huanhang///这样一来,每条关键路径只包含2分之n个操作,因此CPU从5.0下降到2.5,看到这里,相信很多同学会有这样的想法,如果将循环展开一次,每次并行计算k个值程序的性能会不会继续提升。通过测试数据可以发现,随着k值的增大,所有的CPU都有所改进,当k增大到十程序的性能接近吞吐量界限。与最原始的代码相比,性能提升了10到20倍。除此之外,还有另外一种方法可以使程序的性能提高到延迟界限之前,我们介绍过函数抗斑五的代码实现,///huanhang//////huanhang///现在我们调整一下括号的位置,具体如图所示,我们把这种改变称为重新结合变换这种括号顺序的改变,实际上改变,那么向量元素与累计值acc的合并顺序看上去这两个语句好像没有什么差别,但是当我们测量CPU的时候,得到结果却令人吃惊。虽然函数扛办案七的整数加法的性能与函数航班五相同,但是其他三种情况则与使用了并行累积变量的航班六相同,这说明仅仅通过括号的改变,就突破了延迟界限的限制,相信很多同学会有这样的疑问,///huanhang//////huanhang///为什么重新排列了元素的计算顺序之后,程序的性能会有显著的提升呢?接下来我们看一下函数砍半期的汇编代码,其中指令煨木SD表示从内存中加载向量元素追她I到寄存器叉m零中第一条微fullhd指令表示从内存中加载向量元素对一套I加一,然后第一个m UL操作对二者执行惩罚。操作。第二个乘法操作则是将刚才的结果乘以累计值acc与函数航班五相比,虽然加载和乘法运算的数量是相同的,但是只有一个乘法操作形成了循环寄存器之间的数据相关链,///huanhang//////huanhang///因此将简化后的模板复制2分之n次,我们可以看到关键路径上只有2分之n个操作,每次迭代内的第一个乘法操作都不需要等待前一次迭代的累计值就可以执行,因此可以大幅度降低。Cpu。综上所述,现代处理器就相当的计算能力,但是,我们可能需要按照一定的规则来编写程序,才能将这些能力展现出来,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在武杠寺的视频中,我们介绍了如何使用循环展开来提升程序的性能,其中多路并行。循环展开的实现方法,表现突出,当并行路数达到实路时,程序的c p e接近吞吐量界限,此时,如果继续增加并行路数。通过测试数据可以看出程序的性能并没有提升,反而是有所下降。接下来,我们通过二者的汇编代码来探究一下其中的原因,例如,在石乘十的循环展开中,累计变量a c c零保存在寄存器差。M零中,程序只需要从内存中读取d t I,///huanhang//////huanhang///然后与这个寄存器相成。与之相比,20乘20的循环展开,有着非常大的差别。通过这段汇编在码,可以发现,累计变量并不是保存在寄存器中,而是保存在站上,因此程序必须从内存中读取d塔癌和累计变量这两个数枝,然后再将二者相乘的结果保存回内存。看到这里很多同学可能会有这样的疑问,为什么20乘20的累计变量要保存在站上,而不是寄存器中,这是因为现代处理器有16个寄存器来保存浮电数。一旦循环变量的数量超过了可用的寄存器的数量,///huanhang//////huanhang///编译器就会在战上分配一些空间来保存部分变量。与直接使用寄存器相比,一旦将变量分配到站上,会带来额外毒写内存的开销,因此多路并行的优势很可能就会消失,这就是为什么20乘20的循环展开,比时乘时慢的原因,不过幸运的是,大多数循环在出现寄存器溢出之前就达到了吞吐量界限。当c p e达到吞吐量界限时,即使有更多的寄存器可以实现更多路数的循环展开,也是无法突破吞吐量界限的。到目前为止,我们所有的代码以及运行的测试案例,///huanhang//////huanhang///只是访问相对比较少量的内存,我们还没有看到加载操作的延迟对程序性能的影响。接下来,我们通过一个例子来看一个加载操作的延迟图中。这个函数的功能是计算一个链表的长度,在这个循环中变量l s的值依赖于l s指向n e x的值,然后通过判断变量l s是否指向链表的尾部来决定是否继续执行。虽然整个函数的实现相对比较简单,但是通过测试表明函数l t的批以为4.0。为了探究影响这个函数性能的关键因素。接下来我们看一下这个外要循环的汇编代码变量论存放在寄存器r x中,///huanhang//////huanhang///每次循环执行加一的操作变量。L s存放在寄存器I d I中,指令m q表示从内存中加载数据到寄存器。I d I,其中括号I d I表示这个加载操作的内存地址需要从寄存器I d I中读取,我们可以发现后面的寄存器。I d I的每个值都要依赖于加载操作的结果,而加载操作又以寄存器r d I中的值作为它的地址,因此直到前一次迭代的加载操作完成下一次迭代的加载操作才能开始,因此,函数l t的c p e等于4.0,///huanhang//////huanhang///是由加载操作的延迟决定的。事实上,这才测试机器的文档中给出了l一开始的访问时间是四个时钟周期。这个测试结果与文档的参考数据也是一致的。关于更多开始的内容,我们将在第六章详细介绍其今为止所有的势例中,我们所分析的大部分内存引用都是加载操作,也就是从内存读取数据到寄存器中与加载。操作对应的是存储操作,它是将一个寄存器的值写到内存存储操作,并不会影响任何寄存器的值。通常情况下,存储操作不会产生数据相关。不过当加载操作需要从存储操作写的那个内存位置读取数据时,///huanhang//////huanhang///加载操作和存储操作之间可能会相互影响。接下来,我们通过一个代码势力来看一下其中的影响,当我们对图中这个函数分别传入两组不同的参数时,通过性能测试表明视立a的c p e等于一点,三是立b的c p,e却等于7.3。由于势利臂中参数,s r c和d s t指向了同一个内存位置,使得内存毒的结果依赖于最近的内存写的操作,我们将这种情况称之为写渎相关。为了探究二者的性能差异,我们来看一下这个循环的汇变代码,其中指令木q被翻译成两个独立的操作,///huanhang//////huanhang///一个是计算存储操作的地址。S a d d r,另外一个是将数据加载到内存的操作。S d塔对于下一条m q指令所执行的加载操作需要从寄存器r d I处读取数据,我们将寄存器r s I中保存的地址计为a d e r,e寄存器。R d I中保存的地址计为a d d r,如果两个地址相同,加在操作,必须等待s d t操作完成之后才能读取数据,否则就会读到错误的数据,如果两个地址不同,两个操作就可以独立进行了,///huanhang//////huanhang///这就是写渎相关所导致的处理速度下降。总上所述关于内存操作的实现,包括许多细微之处,对于寄存器的操作在指令被一码成具体操作的时候,处理器就能确定哪些指令之间存在相关性,而对于内存操作只有当加载地址和存储地址被计算出来之后,才能确定哪些指令之间会相互影响。最后,我们总结一下程序性能优化的基本策略。第一,针对具体的问题,选择适当的算法和数据结构。第二,遵循一些基本的编码原则,例如消除连续的循环调用以及消除不必要的内存饮用。///huanhang//////huanhang///第三,根据硬件的设计,利用循环展开等技术来提高指令疾并行,不过需要特别注意的是,避免在重写程序时引入错误,尤其是引入新的变量,以及改变循环边界时很容易犯错,需要充分测试新版本的代码,确保它们与原来的代码产生一样的结果,今天的视频就到这里,我们下期见。///huanhang//////huanhang///从这一期开始,我们进入第六章的学习这一章,我们主要来介绍一下计算机系统内的存储器。作为一个程序员,我们需要理解存储器的层次结构,因为它对应用程序的性能有着巨大的影响。如果程序需要的数据存储在寄存器中,那么指令在执行时就可以立即使用这些数据,如果数据存储在c h中,那么获取这些数据需要4到75个时钟周期。当数据存储在内存中,则需要几百个始钟周期,如果数据存储在磁盘上,就需要大约几千万个始钟周期,由于不同的存储器采用了不同的存储技术,///huanhang//////huanhang///因此导致了访问速度以及价格方面的差异。接下来,我们详细介绍一下这些存储技术。首先,我们来看一下随机访问存储器,简称r r e分为两类静态r和动态r在后续的描述中,我们将采用英文缩写s r,表示静态。R d r表示动态。R m由于s r与d e的结构不同,导致了二者存在访问速度上的差异,其中s r将每个比特位的信息存储在一个双稳泰的存储器单元内,每个存储器单元需要六个晶体管来实现关于双稳胎结构,我们可以借助中百模型来理解当中柏倾斜道最左边或者最右边时,///huanhang//////huanhang///它的状态是最稳定的。理论上,钟摆也能在垂直的位置上保持平衡,不过这个状态是不稳定的,一个细微的扰动就能使它倒下,并且倒下之后无法恢复到从前的垂直状态,正是由于s r的存储单元具有双碗态子特性,所以只要有电,它就能够一直保持所存储的数据。与s r相比,d r存储信息的原理是电容充电,对于d m结构每个比特位的存储对应一个电容和一个晶体管。当然,这个电容是非常非常小的,与s不同。D的存储单元对干扰十分敏感。///huanhang//////huanhang///当电容的电压被扰乱之后,就再也无法恢复到干扰之前,虽然s r的速度比要快,但是在价格方面要更贵。处理器芯片内的开始采用的就是s,而内存采用的是d,此外,d还有另外一个缺陷,就是会有很多原因,导致漏电使得低r会在时到100毫秒内失去电荷,因此,内存系统需要不断地读出数据,然后重写来刷新内存的每一位只有通过不断的刷新才能保持数据,不过幸运的是处理器运行的市钟周期是以纳秒来衡量的,所以相对而言,这个保持时间还是比较长的。///huanhang//////huanhang///接下来,我们通过一个简单的例子来看一下d的内部结构。图中展示了一个16成八的d e芯片,其中16表示的是超单元的个数八。表示每个超单元可以存储巴比特的数据。由于存储领域从来都没有为d e的战略元素确定一个标准的名字。为了避免混淆,元书中采用超单元一词来表示低r m的存储单元。通过这张图,我们可以看到所有的超单元被组织成了一个四成四的阵列,每个超单员可以通过类似坐标的方式进行寻址,其中I表示行j表示列整个d r m芯片通过地址引角和数进引角,///huanhang//////huanhang///与内存控制器相连。简单来讲,内存控制器主要用来管理内存。第一次听到内存控制器的同学可能会感到困惑,可以通过一个通俗的例子来理解,假如我们将数据比作书内存就相当于是一个图书馆,而内存控制器可以看成图书管理员,例如我们需要从d r芯片中读出图中所示的抄单元。首先,内存控制器发送行地址二到d r芯片。D r所做出的响应,就是将整个第二行的内容全部复制到内部的航缓冲区域中,接下来内存控制器发送列地址一到d r芯片。///huanhang//////huanhang///D r m对应的操作是从这个航缓冲区中复制出对应的数据,并把它发送到内存控制器。看到这里,相信有很多同学会有这样的疑问,为什么要分两次发送地址,这样不是增加了访问的时间吗?这是因为低r目芯片的设计人员将存储单元设计成了二维阵列,而不是线性数组。这样设及到好处是可以降低芯片上地质引角的数量,如果将视力中128位d r的存储单元用线性数组来表示。那么,地质范围就是0到15,因此就需要四个地址引角,而二维阵列的组织方式只需要两个引角就可以了,///huanhang//////huanhang///不过,二维阵列的组织方式会增加数据访问的时间。接下来,我们看一下采用d芯片封装成的内存模块。图中展示了一个内存模块的基本组成。这个模块一共用了八个地r芯片,分别用编号0到7来表示每个d v m芯片的大小是八兆乘以八,也就是八兆个字节,因此,整个内存模块的大小为64。照字节。在刚才的视频中,我们提到过每个超单员可以存储巴比特的数据,那么,对于八字节的数据就需要八个抄单元来存储。不过这八个超单元并不在同一个d r芯片上,///huanhang//////huanhang///而是平均分布在八个芯片上,其中d零存储第八位d存储下一个字节,以此类推d r七存储最高八位。当处理器向内存控制器发起独数据的请求时,内存控制器将地址转换成超单元地址,然后把它发送到内存模块,然后内存模块再将行地址癌和劣地纸j广播到每个d r m,每个d r都会输出它对应的。超单员的数据最终内存模块,将所有的超单员合并成一个64比特的数据返回给内存控制器具体如头索示,为了跟上迅速发展的处理器的速度,市场上会定期推出新的。///huanhang//////huanhang///D r m,这些d r都是基于传统的d单元,然后进行一些优化,例如我们经常在配置清单上看到弟弟r三d甲四或者l p d甲r等滋样,其中d r的全称是d d r,s,d r m注意s d r m中的s表示同步,而不是静态,不要与s r混淆,这里我们只需要知道同步。D r m比一部d m速度更快就可以了,更多的技术细节就不展开了,关于地d r d j r三以d r r四这些缩写中的数字表示不同的带,例如四代的地弟r要比三寨的地雅毒写速度更快,///huanhang//////huanhang///速度的提升,主要依靠扩大欲取缓冲区的位数,例如d第四的浴区缓冲区是16个比特地,第r三是巴比特。此外,智能手机中的内存几乎全部采用的都是l p d d r,其中l p是l帕的缩写,与d j s相比,l p d j s的工号更低,不过d j s的延迟更小。目前市场上有许多商务笔记本,也开始选用l p d点作为内存,所以l p d点r更适合应用在公号敏感的设备上,今天的内容就到这里,我们下期见。///huanhang//////huanhang///上级的视频中,我们介绍了r的相关内容,无论是s r还是d e m,他们都需要在有电的情况下才能保存数据。这一期我们来看一下,在断电的情况下,也能保存数据的存储戒指。磁盘。目前市面上主流的磁盘产品有两类,分别是机械磁盘和固态硬盘。首先,我们来看一下机械磁盘,也称旋转磁盘图中给出了磁盘的内部结构示意图,它主要依靠盘片来存储数据。盘片的表面涂有磁性的记录材料。通常情况下,此番有一个或者多个盘片组成,这些盘片被固定在一个可以旋转的主轴上,///huanhang//////huanhang///主轴带动盘片以固定的旋转速理进行高速的旋转。例如图中这个磁盘包含三个盘片,一共有六个表面可以用来存储数据。具体如足所事,其中盘片的表面被划分成了一圈一圈的磁道。这里我们用同心圆来表示具体,如图所示,每个词道又被划分成了多个扇区。通常情况下,每个扇区可以存储512个字节的数据,其中山区与山区之间会有一些见隙。这些见隙是用来存放山区的标识信息,不能用来存储数据。磁盘通过读写头来读取存储在盘片表面的数据。///huanhang//////huanhang///盘片的每一个表面都对应着一个独立的毒血头,所有的毒血头连接到一个传动壁上。通过传动壁在半径方向上的移动,这样都懈头可以读取任意此刀上的数据,我们把这种机械运动称为寻道。通过传动壁的移动,可以将毒写头定位在任意此道上。在完成寻道之后,毒写头就保持不动了。如果想要完成对目标扇区的毒血操作,需要通过盘片旋转来配合毒写头可以读出相应的数据位,也可以修改相应的数据值,注意,所有的卢写头都是垂直排列一致行动的,///huanhang//////huanhang///其中毒血头距离磁番表面的高度大约是0.1微米。在这样狭小的间隙里和微笑的灰尘或者剧烈的震动都有可能导致毒血头撞向盘面,从而导致磁盘损坏。有些阿铺主对磁盘食物进行了拆检,并且录制了相关的运行视频感兴趣的同学可以去看看。通常我们关注最多的是磁盘的容量,也就是能够存多少数据,这里需要特别注意的是,磁盘制造商会使用g一笔或者t b为单位来标识磁班的容量,但是这里的一g币等于十的九次方字检一t币,等于十的12次方字。///huanhang//////huanhang///检看到这里相信有很多同学会感到困惑,一g笔不是等于二的30字方字节吗?实际上对于k m g这样的前缀,他们所表示的含义还要依赖于上下文,对于s r m和d r这一类设备。通常情况下,k等于二的14方m等于2到20次方,但是,对于像磁盘和网络这样的I o设备。通常情况下,k等于十的三次方m等于10到6次方等等,不过幸誉的是对于二的30次方,与时的九次方之间的差别不大,大约相差7%左右。除了容量之外,此番的毒写速度也是一个非常重要的指标。///huanhang//////huanhang///对山区的访问时间主要分为三部分,分别是寻道时间,旋转时间以及传送时间,我们先来解释一下什么是寻道时间当目标善区所处的词道与当前毒写头所在的词道不同。尺。那么,船动壁需要将毒血头移动到目标扇区所在的词道船动壁移动所需的时间就是寻道时间。寻闹时间主要取决于毒懈头的当前位置与目标位置的距离寻到有可能发生在两个相邻的磁道之间,此时寻到时间会比较短,也有可能是从最内侧的词道移动到最外侧的词道,遇到这种情况时,///huanhang//////huanhang///寻到时间就会比较长,因此寻到时间并不是一个固定的数值。通过对随机山区的访问测试,通常平均寻找时间在3到9毫秒左右,一旦毒懈头移动到希望的词道上,接下来还需要等待目标善区的第一个数据位旋转到读写头下才能读取数据。这个过程的性能有两个因素决定一个是当前读写头所在的山区位置与目标山区的距离,最坏的情况是毒写头刚刚错过了目标山区,所以必须等待盘片旋转一整圈才能读取数据。另外一个因素是盘片的旋转速度,例如,一个盘片的旋转速度是7200转美分,///huanhang//////huanhang///也就是盘片1分钟可以旋转7200圈,那么,旋转一圈大约需要八毫秒,所以,在最坏的情况下,最大的旋转延迟大约是八毫秒,对于一般的情况,平均旋染时间是最大旋转延迟的一半,约为四毫秒,最后当毒血头位于目标山区时,就可以开始读取数据了一个山区的传送时间,依赖于旋转速度以及每条瓷道的山区。数木假设每条瓷道的平均山区数是400个。转一圈需要八毫秒,所以转过一个扇区,大约需要0.02毫秒。也就是说,数据传送需要0.02毫秒就可以完成以上就是磁盘访问数据所花费的时间。///huanhang//////huanhang///通过这个例子,可以看出,访问一个磁盘善区,所花费的时间,主要是寻到时间和旋转时间。综上所述,机械磁盘的内部结构还是比较复杂的,不仅包含多个盘面,而且这个盘面上还有不同的记录区,但是,从操作系统的视角来看,整个磁盘被抽象成了一个个逻辑快序列,每个逻辑块的大小与磁盘扇区的容量是一致的,都是512个字节。磁盘内部有一个小的故建设备称为磁盘控制器,它维护着逻辑块与实际磁盘扇区之间的硬射关系。当操作系统执行从硬盘读取数据到内存时,///huanhang//////huanhang///操作系统会发送一个命令到磁盘控制器。这个命令就是让磁盘控制器读取特定逻辑块号的数据,根据磁盘的结构特性可以使用盘面善区。这样的一个三元组来唯一标识每个物理善区此番控制器上的故建程序的任务就是将逻辑块号翻译成对应的三言组信息,接下来,控制器会根据这个三元组的信息来执行移动毒写头以及旋转盘面的操作,然后毒角头会把独到的数据放到一个缓冲区中,最后将目标数据复造。内存里虽然越来越多的设备都采用固态硬盘来替代机械硬盘,///huanhang//////huanhang///包括笔记本电脑以及服务器等。不过机械磁盘还是会继续存在的。因此,关于机械磁盘的原理还是有必要了解一下,下一期我们将会介绍固态硬盘的相关内容。今天的视频就到这里,我们下期见。///huanhang//////huanhang///上一期的视频中,我们介绍了机械磁盘的相关内容。这一期我们来看一下顾态硬盘顾态。硬盘是由一个或者多个闪存芯片组成,它使用闪存芯片取代了传动壁加盘片。这种机械式的工作方式。除此之外,顾探硬盘还包含一个闪存转换层,它的功能与磁盘控制器类似,都是将操作系统对逻辑块的请求。翻译成对底层物理设备的访问。不同的是闪存芯片是基于n f s实现的。接下来我们来看一下陕存芯片的内部结构组成。每一颗闪存芯片是由一个或者多个大组成,///huanhang//////huanhang///每个大可以分为多个p具体,如托所事。每个普楞包含多个b l o k,需要注意的是,不l o k与逻辑块是没有关系的。B l k的内部又被分成了多个配置,对于固毯。硬盘数据是以配置为单位读写的。与机械磁斑相比,对于不同规格的闪存芯片,其中配置大小可能并不相同。在有些扇存芯片中,一个配置的大小是512字节,还有的是一k b或者两k b甚至更大传统的机械磁盘包含独和写这两个基本操作,对于固态硬盘,除了这两个基本操作之外,///huanhang//////huanhang///还多了一个擦除的操作。由于闪存编程原理的限制,只能将一改为零,而不能将零改成一,所以一个配置在写入数据之前,所有的存储位都是一对于写入操作的本质就是将某些存储位从一变成零需要特别注意的是,写入操作是以配置为单位的,在写入之前夜是需要擦除的,不能直接覆盖,对于擦除操作是以捕捞可为单位的擦除。操作的本质就是将所有的存储位都变成一当一个捕l c完成了擦除。操作,那么,这个捕l o k中所包含的所有配置都被擦除了时,///huanhang//////huanhang///所有的配置都能够执行一次写。操作,再经过一定次数的擦除之后,b l o k就会发生磨损,一旦一个不l o k发生损坏之后就不能再使用了,因此顾态硬盘中的闪存翻1层会使用平均磨损算法将擦除平均到所有的块上来最大化每个块子寿命如果平均磨损处理得好,固抬硬盘也要好多年才能磨损坏。比起机械硬盘固态硬盘有很多优点,由于它是由半导体存储器构成的,没有移动的机械部件,因此随机访问时间比机限,硬盘要快,工耗也更低,///huanhang//////huanhang///同时也更扛衰缺减就是固胎硬盘的价格要贵一些,不过随着技术的发展,它的价格也在不断的降低。从第六章开始,我们介绍了s r d r以及磁盘的相关存储技术。通过上述存储技术的介绍,可以得出以下结论第一,不同的存储技术有不同价格和性能的折中。从存取速度上来说,s r m最快低于次支磁盘最慢不过速度越快,价格也就越贵,第二个结论就是不同存储技术的价格和性能的改变速率不同。首先,我们来看一下s r自1985年至2015年期间所发生的变化。///huanhang//////huanhang///从图中这个表格可以看出访问时间以及每照自己的价格下降了大约100多倍。接下来我们再来看一下d的情况。通过图中的数据,我们可以看出,d的变化趋势更加明显,其中每照字节的价格下降了44000倍,不过访问时间只下降了10倍,看完了s m姆和d r,我们再来看一下,磁盘技术时间同样是1985年到2015年,在这30年里,机械磁盘每g b的价格下降了300万倍,不过访问速度提高得很慢,只有25倍左右。综上所数对于低润m和磁盘来说,///huanhang//////huanhang///降低要比提高性能容易的多。第三个结论是d r m和磁盘的性能滞后于c p u的性能,从1985年到2015年,c p u的时用周期提高了500倍。如果考虑多核技术,多个c p u还会并发的向d r和磁盘请求数据。C p的性能与d e m和磁磁盘性能的差距实际上在不断的加大。为了弥补处理器与内存之间的差距,处理器芯片中使用了大量的基于s r的高速缓存高速缓存之所以可以降低c p u的缓存延迟,是因为应用程序具有局部性的特点。///huanhang//////huanhang///接下来我们来看一下什么是程序的局部性,局部性。通常有两种不同的形式,分别是时间局不性和空间局步性。如果背引用过的内存位置很可能在不远的将来还会被多次引用。此时我们可以说,程序具有良好的时间局部性,如果一个内存位置被引用了一次,那么程序很可能在不远的将来以用附近的一个内存位置,此时我们可以说程序具有良好的空间局部性。接下来,我们通过几个代码式里来看一下程序数据引用的局部性。图中,这个函数的功能是对一个向量的元素求和其中变量项目在每次循环迭代中被引用一次,///huanhang//////huanhang///因此对于萨姆来说有好的时间局悟性。另一方面,由于萨姆是个标量不存在空间局部性的特点,由于向量微的元素是按照顺序一个皆一个来读取的读取的顺序与存储在内存中的顺序是一致的,因此对于变量v函数有很好的空间局步性,不过时间局部性很差,因为每个项量元素只被访问一次。综上所述,循环体中的变量要么有好的空间局,不幸要么有好的时间局不性,所以我们可以断定该函数具有好的局悟性。接下来我们再来看一个多为数组的例子。图中这个函数是对一个二维数组的元素进行求和运算,///huanhang//////huanhang///其中千套循环是按照行优先的顺序来读取。数组元素的,也就是说,内层放循环先读第一行元素。读完之后再读第二行,以此类推,由于数组在内存中的顺序也是按照航优先的顺序存储的,所以该函数访问数组元素的顺序与内存中的存储顺序是一致的,此时我们可以说,这个函数也具有良好的空间局步性。接下来,我们对这个函数作为一点小的改动,具体的改动方案是交换了I和g的循环。具体如出所事,虽然改动不大,改动之后得到的结果也是正确的,///huanhang//////huanhang///但是改动后的程序空间局步性会很差,这是因为访问顺序与存处顺序不一致了,具体如出所事一般而言,有良好,局部性的程序比局部性差的程序运行得更快。现在计算机系统各个层次的设计都用了局部性原理,例如高速缓存就是利用程序的局部性来提高c p u的缓存速度。下一期的视频中,我们将会介绍高速缓存的相关内容,今天的视频就到这里,我们下期建。///huanhang//////huanhang///在之前的视频中,我们介绍了sum地段以及磁盘等存储技术图中展示了一个典型的存储器层次结构,最高层是寄存器cpu,可以在一个时钟周期内访问他们,然后是基于SSM的高速缓存。访问他们,一般需要几个时钟周期,接下来是基于DM的内存。访问内存需要几十到几百个时钟周期,再往下是磁盘。虽然速度慢,但是容量大,最后有些系统还包括远程服务器磁盘,需要通过网络来访问存储器层次结构的中心思想是速度更快,容量更小的存储设备作为速度更慢,///huanhang//////huanhang///容量更大的存储设备的缓存缓存在现代计算机系统中无处不在,例如图中DK加1层的存储器被划分成了16个大小固定的快,每个块都有唯一的地址,这里我们用编号0到15来表示DK层的存储器有四个快的空间,每个块的大小与可以加1层的快一样。数据总是以快为单元在DK层和第k加1层之间来回复制。例如,当前DK层的存储器包含了四个快的副本具体如图所示,对于相邻层之间的块大小是固定的,然而不相邻的层次之间块的大小是不一样的例如,///huanhang//////huanhang///寄存器与l一开始之间传送的块大小。通常是一个字,啊开始与l一开始之间传送的块大小。通常是几十个字节。内存与磁盘之间则是几百个,或者几千个之间。一般来说,层次结构中离CPU越远的设备访问时间就越长。为了弥补访问过程中浪费的时间,因此倾向于使用较大的块。接下来,我们介绍几个缓存相关的概念。首先,我们来看一下什么是缓存命中当程序需要读取第k加1层的某个数据对象地是他首先从DK层的数据库中检索是否包含目标数据第的副本,///huanhang//////huanhang///如果目标数据帝刚好缓存在第k层中,我们将这种情况称为缓存命中,另一方面,如果DK层没有缓存目标数据,第我们将这种情况称为缓存不命中当发生不命中十d k层的缓存。要从d k加1层取出。包含目标数据的块,如果DK层的缓存已经满了,这时包含目标数据的快就会覆盖现存的一个块,我们把这个过程称为替换被替换的快,有时也称为牺牲快决定替换哪个快是有缓存的。替换策略来具体决定的。常用的替换策略,有随机替换以及l r u等接下来,///huanhang//////huanhang///我们重点来看一下基于SSM的高速缓存早期计算机系统的存储层次结构只有3层,分别是寄存器文件内存以及磁盘。由于CPU与内存之间的性能差距逐渐增大。于是系统设计者在寄存器文件和内存之间插入了一个小的基于SSM的高速缓存,称为l一开始。L一开始的访问速度和寄存器差不多,大约需要四个时钟周期。随着CPU和内存之间的性能差距继续增大。系统设计者有在l一开始和内存之间插入了一个更大的高速缓存,成为l开始。L二开始的访问时间大约需要十个时钟周期,///huanhang//////huanhang///还有一些计算机系统中包含了一个更大的l三开始,它位于l二开始和内存之间。为了避免歧义后面的视频中开始一词,指的是基于SSM的高速缓存,这里特别说明一下。接下来我们看一下开始的内部结构,整个开始被划分成一个或者多个。SET这里我们用变量大。S表示赛次的个数,每个SET包含一个或者多个开始烂这里我们用变量亦来表示一个site中开始烂的行数,每个开始烂由三部分组成,分别是有效,未标记数据块其中有效为的长度是一个比特,///huanhang//////huanhang///他表示当前开始按存储的信息是否有效,当外力的,等于一时表示数据有效,当外力的等于零时表示数据无效。标记是用来确定目标数据是否存在于当前的开始赖中,在后面的课程中,我们会详细介绍标记的用法。至于数据块就是一小部分内存数据的副本大小用比来表示。通常来说,开始的结构可以用元组s e b m来描述,开始大小是指所有数据块的和其中有效。为何标记为不包括在内,因此开始的容量可以通过super成意成意比计算得到,///huanhang//////huanhang///接下来我们看一下开始是如何工作的。当CPU执行数据加载指令。从内存地址a处读取数据是根据存储器层次原理,CPU将地址a发送到开始,如果开始中保存着目标数据的副本,他就立即将目标数据发回给cpu。那么,开始是如何知道自己是否包含目标数据的副本,呢假设目标数据的地址长度为IM为这个地址被参数s何必分成了三个字段。具体如图所示。首先,我们可以通过长度为super的主索引为来确定目标数据存储在哪个site中,///huanhang//////huanhang///一旦我们知道了,目标数据属于哪个set,接下来我们需要确定目标数据放在哪一行,确定具体的行是通过长度为t的标记来实现的,不过还需要注意一点40,有效为必须等于一,也就是说需要有效为和标记共同来确定目标数据属于哪一行。最后我们需要根据长度为b地块偏移量来确定目标数据。在数据库中的确切地址。通过以上三部开始就能确定是否命中可能有的同学会有这样的疑问,为什么开始要用中间的为作为主索引,而不是用高位假设高位用作组索引为那么一些连续的内存快就会映射到相同的site中,///huanhang//////huanhang///例如图中前四个内存块映射到第一个SET中,第五至第八个内存块映射到第二个赛次中,以此类推最后四个内存块映射到最后一个site中,如果一个程序有良好的空间局部性,当需要顺序读取一个数组的多个元素是此时需要不断的进行开始。烂的替换,也就是说,在任何时刻开始都只保存着一个数据块大小的数组内容,这样会导致开始的。使用率很低。如果用中间为作为索引相邻的内存块,总是映射到不同的site中,具体如图所示与高位,所以相比开始的效率会大大提高以上就是开始基本的结构组成。///huanhang//////huanhang///下一期我们将详细介绍不同类型的开始,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在上一期的视频中,我们介绍了开h的组织结构,根据每个赛t所包含的开兰的行数不同,c被分为不同的类,当每个赛t只有一个c l,也就是e等于10,我们将这种结构的c始成为直接映射。首先,我们先来看一下直接映射的c h是如何工作的假设有这样一个系统,它包含一个c p u一个寄存计文件,一个l一c h和一个内存。当c p u执行从内存中读取数据的指令时,首先从l一开始中查询是否包含目标数据的副本,如果能够在l一开始中找到目标数据,///huanhang//////huanhang///那么开始就把目标数据直接返回给c p u,这个判断是否命中获取目标数据的过程一共分为三步,分别是足选择行匹配以及自抽取。首先,我们来看一下族选择是如何实现的。这一步是根据组所运值来确定目标数据属于哪个s t。例如图中的祖缩因位的长度是五位,这些2斤之位被解释成一个无符号数,因此长度为五的阻缩韵卫最大可以检索32个赛t当s等于零时,此时足选择的结果是,s t零当s等于10,此时足选择的结果是,赛t一以此类推以上就是根据祖孙引卫进行主选择的整个过程开始执行完组选择之后,///huanhang//////huanhang///接下来开始执行行匹配,我们通过一个例子来看一下行匹配的过程。在这个例子中,每个s只有一个开始l,而且当前开始案的有效位等于一。也就是说此时开始案中的数据是有效的,然后我们需要对比开石案中的标记与地址中的标记位是否一致,如果一致表示目标数据一定在当前的开始l中,另一方面,如果不一致或者有效位等于零。表示目标数据不再呢开中,因此,航匹配最终的结果无非就是命重或者不命重。一旦命中就可以继续执行第三步自抽取这一步需要根据偏一量来确定目标数据的确切位置。///huanhang//////huanhang///通俗来讲,就是从数据块的什么位置开始抽取数据,例如图中数据块的大小为八个字节,我们用编号零到期来表示当块片移等于100时,它表明目标数据的起始地址位于字节四处。这里,我们假设目标数据的长度为四个字减,这样一来,我们就可以获得目标数据的副本了。经过以上三步开始,就可以将目标数据返回给c p u。上述过程就是开始命中的情况,如果发生了不命重,那么开始需要从存储器层次结构的下1层取出被请求的块。由于直接硬射的每个赛t只包含一行,///huanhang//////huanhang///因此替换策略十分简单,直接用新取出的行来代替当前的行就可以了。虽然上述过程并不复杂,可能大家还是会对这种处理方式感到困惑。接下来,我们通过一个具体的例子来解释一下整个过程假设我们有一个直接映射的开h,它包含四个赛t,每个赛t有一行,每个数据块包含两个字节,其中地质m是四位。虽然上书假设是不现实的,但是这种假设方式可以方便我们理解整个过程。由于地址是四位,整个地质空间可以用编号0到15来表示标记位和索引卫连起来可以唯一的标识每个内存块需要注意的是,///huanhang//////huanhang///每个内存块包含两个字节,例如块零是由地址铃和地址衣组成的。块,一是由地址二和地址三组成,以此类推,这样一来,整个地质空间被划分成了八个块,但是势例中的开势只有四t,因此就会出现两个内存块硬射到同一个赛子的情况,例如块零和块四都映射到了赛特零块一和块五都映射到了赛特一介绍完了内存和c h的基本结构,接下来,让我们来模拟一下。当c p u执行一系列读的时候,c是如何执行的。这里我们假设c p u每次读取的数据都是一个字节,///huanhang//////huanhang///最开始时整个开始都是空的,即所有的开车烂的有效位都等于零。图中表格的每一行代表一个开斯烂,第一步当c p u读地址。零的数据是经过主选择之后,发现赛特零的有效位等于零,此时不命重。接下来开始会从内存中取出包含目标数据的块,并把这个数据块放在s t零中,具体如足所事,然后开始返回位于块零处的目标数据。M0第二步,当c p u读取地址一处的字,使由于目标数据。M一已经在开始中,这次开始是命中的开始,根据地址m进行主选择航匹配以及自抽取之后,///huanhang//////huanhang///然后将目标数据m一返回给c p u,此时开始的状态没有发生变化,第三步当c p u独地址13个字时,根据地址13的组索引位进行主选择之后,发现赛特二的有效位等于零,所以发生不命重。这时开始把块六加载到塞滩二中,然后从开始案中抽取目标数据返回给c p u,第四步,当c p u读取地址八着字,根据地址八的组索引位进行主选择之后,发现赛特零的有效位是一,但是进行标记对比后发现并不匹配,此时需要块四来替代。///huanhang//////huanhang///块零替换之后,开始再返回目标数据到c p u最后一步,当c p u再去读地址。零的字又会发生不命重,这是因为前面饮用地址八的数据是我们替换了块。零,由于不同的块刚好应射到同一个s t中,虽然整个开始还有空闲的空间,当发生交替饮用时,还是会出现不命重的情况,我们把这种现象称为冲突不命重,实际上冲突不运。重在真实的程序中是很常见的,它会导致一些令人困惑的性能问题。接下来,我们通过一个代码式里来看一下冲突,///huanhang//////huanhang///不命重,对于变量x和外来说,这个函数具有良好的局悟性,因此,我们期望开始有较高的命中率,然而实际情况却并非如此。假设数阻x的第一个元素位于地址零处,每个元素的长度为四个字减,因此可以得到数组x各个元素的起始地质。数组外紧跟其后,y菱的地址从32开始具体如足所示,当程序开始运行时,循环在第一次迭代时引用了元素x零此时发生不命中开始把包含x零到x三的块加载到赛特零,接下来又立刻引用了数组元素外零又一次不命重,///huanhang//////huanhang///这时开始把包含外领到外三的块加载到赛特零这里需要注意的是,之前塞特林中存储的内容是数据块x零至x三的数据,那么这些数据会被外领至y三覆盖,具体如出所事,接下来循环继续下一次的迭代,此时,对数组元素x一的饮用又发生了不命中导致x0至3的块加载到了赛特零覆盖了y零至y三的块。看到这里,相信大家已经发现了其中的问题,实际上,后面每次对x和外的饮用都会导致开始案的替换,我们把这种现象成为抖洞。综上所述,即使程序具有良好的空间局悟性,///huanhang//////huanhang///同时我们的也有足够的空间来存放数组x和外的数据块,但是每次引用还是会产生冲突,不命重究其原因是这些块被应射到了同一个s t中,这种抖洞的出现可能会导致运行速度下降2到3倍。虽然我们给出的势力比较简单,实际上对于直接映射的开势来说,这个问题也是存在的。不过,一旦程序员意识到了抖洞的情况,那么,解决这个问题也是比较容易的,例如我们把数组x的长度由八变为12数组外,还是紧跟在x的后面。此时数组外的骑始地址发生了改变,///huanhang//////huanhang///从而就避免了x和外子元素应射到同一个s特中。这样一来,通过这种数据填充的方式就可以消除抖洞,从而解决冲突不命重的问题以上就是直接硬射开始的全部内容。下一期我们将会介绍另外两种结构的开始,今天的内容就到这里,我们下期见。///huanhang//////huanhang///在上一期的视频中,我们介绍了直接映射开始的相关内容。由于直接映射的每个赛t只有一行,因此容易发生冲突,不命重与直接映射不同阻相连开始的每个赛特允许包含多个开斯烂,也就是说e是大于一的,不过每个赛特最多不能超过c,除以b个开斯烂对于e等于c。除以b的情况我们稍后介绍,例如图中每个赛t包含两个开算,我们将这种结构称为二路组相连开始祖相连开始确定一个请求是否命重。同样需要经过主选择行匹配,以及自抽取这三步组相连开始足选择的过程与直接映射开始一样的,///huanhang//////huanhang///都是通过组缩引位来确定目标数据属于哪一个s t具体如此所是在完成足选择之后接下来执行行匹配。由于足选择c h的每个赛t包含多个开斯案,所以需要便利这个赛s中每一行来寻找一个有效位,等于一并且与地址中的标记位相匹配的开始滥,如果找到了符合条件的开始案表示命种。接下来,根据块片移,从这一行的数据块中抽取目标数据自抽取的过程与前面一样,这里就不再赘述了,如果找不到符合条件的开始案表示不命重,此时开始必须从内存中取出包含目标数据的块,///huanhang//////huanhang///不过一旦开始取出这个块应该替换哪一行,呢如果存在空行,也就是歪力的,等于零的开c l,那么这个空行就是不错的选择,但是如果这个赛s中没有空行,这时我们需要从中选择一个非空行作为被替换的对象,同时希望c p u不会很快引用这个被替换的行,这里我们介绍几个替换策略最简单的替换策略就是随机选择一行,然后进行替换其它复杂的策略就是利用局部性原理,使得在不远的将来引用被替换的行的概率最小,例如,最不长使用策略会替换在过去某个时间段内引用次数最少的那一行,///huanhang//////huanhang///最近最少使用策略会替换最后一次访问时间最久远的那一行,所有的这些策略都需要额外的时间以及硬件实现,但是越往存储期层次结构下面走一次,不命中的开销就越大,所以通过更好地替换策略来降低。不命中的机率是非常值得的,以上就是祖相连开始的内容。接下来我们来看一下全相连开始的组织结构,整个开始。只有一个赛t,也就是说,一个赛t包含了所有的开车l具体如出所事。至于具体的行书意,可以通过c除以b计算得到其中c表示开始的容量。///huanhang//////huanhang///B表示数据块大小,由于全箱开始。只有一个s t,所以默认总是选择s特零,因此不再需要组索引位进行主选择了这样一来,地址只需要划分成标记和块片宜即可。具体如出所示,关于全项链开始的航匹配和自选择与主相连开始是一样的,他们之间主要的区别就是开兰规模大小的问题。由于硬件实现以及成本的问题,全项连开始只适合做容量较小的高速缓存。例如虚拟内存系统中的t r b就可以使用这种结构视频。至此,关于不同类型开式的组织结构就讲完了。///huanhang//////huanhang///实际上开始关于毒的操作比较简单,不过写入的情况要复杂一些。当c p u需要往内存中写入数据时,需要考虑写命中和不命重两种情况。当发生血命中时,有两种策略分别是写穿透和写回写穿透是指c p u在写开始的同时写内存。这种策略的好处是内存的数据永远都是新的。开始替换时,直接扔掉旧的数据就可以写回策略是指c p u直写开始不写内存写回的好处是写开始时比较省事,不用关注是否与内存一致,只有当替换算法要驱逐这个更新的块时,///huanhang//////huanhang///再写回到内存里,不过这种策略会增加开始的复杂性。为了表明每个数据块是否被修改过,每一个开始烂,需要增加一个额外的修改位以上就是写命重时写开始的策略。当发生写不命重时,也有两种策略分别是写分配和写部分配写分配,就是把目标数据所在的块从内存加载到开始中,然后再往开始中写写部分配就是绕开开始直接把药写的内容写到内存里。通常情况下写分配与写回搭配,使用写部分配与写穿透搭配使用以上就是写开始的相关内容。这里我们仅仅做一个简要的介绍,///huanhang//////huanhang///接下来我们来看一下英特尔埃期处理器的高速缓存层次结构,其中每个处理器芯片包含四个核心,每个盒都有自己私有的。L1I c h,l一d c h和l二开始所有的核心共享芯片内的l三开始注意,所有基于s r的高速缓存都在处理器芯片内部之前的视频中,我们一直假设开始只保存程序的数据,实际上开始既保存数据,也保存指令,我们把只保存指令的高速缓存成为I c h只保存数据的高速缓存成为d k,既保存指令,又保存数据的高速缓存,///huanhang//////huanhang///称为统一的高速缓存。采用独立的I c h和d开始的好处是c p u可以同时读指令和数据。此外,还能确保数据访问不会与指令访问形成冲突不命重图中的表格列出了不同层次开始的特性,以及参数,包括访问时间容量大小以及相连的方式等等。接下来,我们分析一下不同的设计对开始性能的影响,一方面,容量较大的开始可能会提高命中率,不过使容量较大的开始运行得更快,要更难一些。最终导致的结果是,容量较大的开始可能会增加命中的时间,///huanhang//////huanhang///这就是为什么l一开始比l开始小l二开始比l三开始小的原因。数据块的大小也会影响开始的。性能较大了。块能够利用程序中可能存在的空间局部性帮助提高命中率。不过对于给定的开始大小跨越大,就意味着开始的行数越少。这样一来,虽然对空间局部性好的程序是有利的,但是对于时间局部性好的程序命中率就会受到损害。此外,当发生不命重时较大的块的处罚也会增大,因为块越大,传送的时间也就越长。在英特尔埃期处理器中,l一k始和l二是都是8路祖相连的。///huanhang//////huanhang///L三开始采用的是16路组,相连,也就是说对于l一开始和l二开始,每个赛s中有八个开始。烂。L三开始的每个赛s中有16个开始烂每个赛t中开斯案的数量越多。那么,由于冲突不命重导致的抖动现象发生的几率就越低,不过相连度越高,实现的复杂度就越高,访问速度就很难再提高了,所以最终相连度应该如何选择需要在命中时间和不命中处罚之间做一个折中的处理。最后一个因素就是写策类的影响。一般而言,l一开始与r开始之间用写穿透的要多一些。///huanhang//////huanhang///越往下层写回策略比写穿透用的要多以上就是影响开始性能的几个主要因素。关于开始对程序性能的影响,我们将在下一期重点介绍今天的视频就到这里,我们下期见。///huanhang//////huanhang///从这一期开始,我们进入第七章的学习这一章主要来介绍一下链接的相关内容。首先,我们来看一下什么是链接链接是将各种代码和数据收集并组合成一个文件的过程,最终得到的文件可以被加载到内存执行早期的计算机系统中,链接是手动执行的,到了现代系统中。链接是由链接器自动完成的。在大型应用程序的开发过程中,我们不可能将所有的功能实现都放在一个原文件中,而是将它分解为更小更容易管理的模块。当我们修改其中一个模块时,我们只需要重新编译这个修改后的模块,///huanhang//////huanhang///而其他的模块是不需要重新编译的。对于出学者由于编写的程序规模比较小。链接通常是由链接器默默处理的,所以不会觉得链接是一个重要的问题,那么,我们为什么还要学习链接的知识,呢当我们在构造大型程序时,经常会遇到由于缺少库文件或者库文件的版本不兼容而导致的链一接错误。解决这类问题,需要我们理解。链接器是如何使用库文件来解析引用的,否则对于解决这类问题,你会感到无从下手,其次离解链接器可以帮助我们避免一些难以发现的。///huanhang//////huanhang///编程错误。后面的课程中,我们会通过具体的视例来展示这类错误是如何发生的,以及如何避免第三链接,可以帮助我们理解编程语言中的作文语规则是如何实现的,例如全居变量和局部变量之间的区别是什么?当我们看到一个s属性的变量或者函数时,实际的意义到底是什么?除此之外,理解编译还可以帮助我们理解一些重要的系统概念,比如程序的加载和运行虚拟内存,内存,硬射等等最后一点理解编译可以帮助我们更好地利用共享库。随着共享库和动态链接,///huanhang//////huanhang///在现代操作系统中变得越来越重要,链接也逐渐变得复杂。因此,作为计算机相关方向的从业者,需要学习一下链接的相关内容。接下来,我们通过一个具体的事例来看一下链接做了哪些事情图中的视力是由两个原文件组成的,分别为慢点,c和点。C集中慢。函数中初始化了一个长度为二的整数数组瑞,然后调用s m函数对这个数组进行求和。在l I系统中,我们可以通过在中输入途蓉的命令来得到可执行程序p r o j,其中杠大o g表示代码优化等级。///huanhang//////huanhang///这个选项的意思是告诉编译器声称的机器代码要符合原始c代码的结构,目的是为了方便调试,在实际项目中,为了程序的性能,通常采用杠大o一或者杠大o二的优化选项杠p r o。这选项是指定生成可执行文件的名字为p I o j o是t的首字母,如果不特别指明默认生成的可执行文件名为a点t关于编译系统的整个工作流程具体如图索是接下来为了更直观的理解链接在整个编译系统的作用,我们将整个过程进行分解,采用手动链接的方式来生成可执行程序。///huanhang//////huanhang///首先是预处理阶段,我们使用图中,这条命令将原程序慢点,c翻译成慢点。I,其中c p p指的是c预处理器,当然我们也可以使用j c c来完成相同的预处理任务,不过需要添加一个杠大义的选项。这个选项是用来限制这些c只进行预处理,不做编译汇编以及链接的操作。通过上述任意一条命令都可以得到慢点。I文件,它是一个I c的中间文件。感兴趣的同学可以查看一下其中的内容。接下来是编译阶段。编译器将慢点I翻译成汇编文件。///huanhang//////huanhang///慢点,s,其中c c指的是c编译器,同样也可以使用g c c来完成这一步具体命令如出所事,其中杠大s选项表示指对文件进行编译,不做汇编链接的处理。这一步得到的汇编文件慢点,s也可以通过编译器打开,查看第三个阶段是汇编,我们使用汇编器,将慢点s翻译成一个可重定位目标文件慢点欧。至于可重定位是什么意思,后面的内容会有详细的解释。经过汇编阶段,我们可以得到了可重定位目标文件慢点,欧和s m点对于s m点的获得与慢点,///huanhang//////huanhang///欧类似,这里就不再赘述了。最后我们使用链接器来构造可执行文件。这里需要特别说明一下。当我们手动调用链接器来构造。可执行程序时,除了需要用到汇编阶段得到的慢点。O和萨点o之外,还需要这五个文件具体如途索示其中c r t是c装探姆的缩写链接就是将这些文件打包成一个可执行文件具体命令,如途索事,其中l d指的是链接器杠s表示的是采用静态链接的方式。通过这条命令,我们就可以实现手动链接的操作。关于每个c r t文件具体实现的功能。///huanhang//////huanhang///感兴趣的同学可以去看一下程序员的自我修养一书,这里就不再展开,讲述了,在上述所有的操作都完成之后,我们来测试一下得到的可执行文件。P r o j能否正确运行,我们可以在c l中输入点杠p r o j,然后回车结束关于可执行程序。P r o j的运行是通过c l调用操作系统中的加载器函数来实现的。加载器,将可执行文件p r o j中的代码和数据复制到内存中,然后将c p u的控制权转移到p r o j的程序开头。///huanhang//////huanhang///随后程序p r o j开始执行以上就是从程序的原文件到可执行文件的整个过程,我们通过不同的命令,将预处理编译汇编以及链接所执行的操作分解开来,其中链接所执行的操作就一目了然了。最后我们总结一下,关于链接就是将可重定位目标文件以及必要的系统文件组合起来,生成一个可执行目标文件的操作。对于链接是如何重新组织这些文件的,我们将在后续的课程中详细的讲解今天的内容就到这里,我们下期间。///huanhang//////huanhang///在上一期的视频中,我们介绍了如何通过手动链接可重定位目标稳件来生成可执行程序。这一期我们来详细介绍一下可重定位目标文件的相关内容。首先,我们来看一个代码实例。图中这段代码定义了两个全局变量,t和函数放个c。仅仅执行一个简单的打印功能,慢。函数内定义了两个局部静态变量。A和b。这些所有的代码都在同一个原文件慢点。C中,接下来我们使用g c c将这个原文件翻译成可重定位目标文件。具体命令如图所示其中杠c的编译选项表示只进行编译和汇编不执行链接的操作。///huanhang//////huanhang///这样一来我们就得到了可重定位目标稳健慢点,欧,我们可以使用w o d c t命令来看一下慢点。O的大小这个杠c选项表示查看文件包含多少个字节,接下来我们的任务就是看一下这1896个字简都包含了哪些信息。每一个可重定为目标文件,可以大致分为三部分,分别是e l f,h d不同的s以及描述这些s属性的表,其中e r f是可执行可链接格式的首字母缩写,接下来,我们以慢点o的e r f h为例,看一下。E f德中具体包含了哪些内容,///huanhang//////huanhang///我们可以使用图中这条命令来查看e l f h d具体内容,其中r德e r f是l I系统中提供的一个工具,它可以显示e l f文件的相关内容杠h选项表示只显示h信息。这样一来,我们就可以看到e f中的详细内容了。首先,我们来看一下开头的16个字简,分别代表什么含义最开始的四个字节被称为e l f文件的魔术分别与阿斯克马中的d e控制服字符,役字符l以及字符f对应可能大家对魔术的概念有些陌生。通俗点讲,///huanhang//////huanhang///魔术就是用来确认文件类型的操作系统在加载可执行文件的时候,会确认魔术是否正确,如果不正确,就会拒绝加载接下来的一个字节用来表示该e r f文件的类型。零x一表示32位零x二表示64位,第六字节表示字节序零x一表示小端零x二表示大端第七个字节表示e r f文件的版本号通常都是一最后九个字节一f的标准中没有定义,用名填充以上就是e r f h d最开始16字节所代表的含义,我们继续来。E r f剩下的部分根据这一行信息,///huanhang//////huanhang///我们可以看到文件的类型是可重定未文件。除此之外,还有另外两种类型,分别是可执行文件和共享文件后面的视频中,我们会详细介绍这两种类型的文件。关于e r f h的长度,这里也给出了一共十六十四个字节,根据这个信息,我们可以确定s在e r f文件中的起始位置就是零x x零具体,如图所示,关于有的书中将s翻译成节,有的书中翻译成段。为了避免混淆该视频就不对s进行翻译了。除此之外,e r f h中还给出了一些关于s t o信息,///huanhang//////huanhang///它是用来描述不同s属性的表,根据这行信息,可以确认这个表在一二f中的起始位置是1064。该表一共包含13个表象,每个表象的大小是64个字节。这样一来,我们就可以计算出整个表子,大小就是832个字节,根据这个表在e二f中的起始位置以及大小可以算出整个e r f的大小是1896。这个数值恰好于我们使用w d c o t命令统计的字节数一致以上就是一r f h d大致的内容。关于e r f d更多内容感兴趣的同学可以查看程序员的自我修养一书。///huanhang//////huanhang///通过对e f h d的减析,我们对e l f文件有了一个大致的认识,我们先来看一下描述不同s属性的表,具体命令,如出所事,其中杠大s选项表示打印整个表的信息,除了表中第一项,其他每一个表象都对应着一个s根据图中的信息,我们可以看到整个e r f文件一共包含12个,其中奥f表示每个呢起始位置s表示的大小。根据这两个参数信息,我们就可以确定每个在e r f文件中的具体位置,例如其实位置是x四零大小为84个字检在刚才的讲述中,///huanhang//////huanhang///我们知道。E r f h的大小是64个字节,所以我们可以确认,t e x是紧跟在e r f h得之后的具体如出所事这个中存放的是已经编译好的机器代码,对于查看以编译好的机器代码,我们需要使用反会编工具。O b g当铺将机器代码转换成汇编代码,具体命令,如托所是图中左边这部分是指令地址,右边这部分是具体的机器指令,其中每个字节所对应的汇编代码也可以通过返回编得到感兴趣的同学可以了解一下。通过这个例子,///huanhang//////huanhang///相信大家已经搞清楚。T x s e中所存放的内容了。根据t x呢起始位置以及大小,我们可以计算出下一个n,其实位置是零x九四。通过查表可以发现x之后的是t s e是用来存放以初始化的全居变量和静态变量的值。例如程序中,我们将全局变量。O t初始化为实静态变量。A初始化为一,接下来我们就来看一下d t s中的内容聚集命令,如出所事。由于数据存放采用小端法,所以这里存储的字节顺序与实际数直的字节顺序相反,///huanhang//////huanhang///对于未初始化的全局变量和静态变量会存放在b s s s e c中,需要特别注意的是,被初始化为零的全局变量和静态变量也存放在b s s中,用b s s表示未初始化的数据,最早源自于I b m704汇编语言,然后一直沿用至今有一个区分。D塔和b s s的简单方法就是把b s s看成更好,节省空间的缩写,还有一点需要注意的是,局部变量既不在d塔中也不再。B s s中当我们看到b s s s的起始位置和大小时,///huanhang//////huanhang///会发现两个奇怪的问题,一个是b s s的起始地址与r o d的起始地址是一样的,另外一个就是b s s的大小是四个字节,而原程序中全局变量w l和静态变量。B加起来的大小是八个字节,二者大小并不匹配,实际上b s s s并不占据实际的空间,它仅仅是一个占位符区分,以初始化和魏初始化的变量是为了节省空间,当程序运行时会在内存中分配这些变量,并把初使值设为零d t s n之后是r s,这里。R o是r缩写顾名思义,///huanhang//////huanhang///这个三就是用来存放只读数据的,例如f语句中的格式串和s语句中的跳转表就是存在这个区域内,在我们的势例中。函数放c中调用了普f函数,因此我们可以看到d这个保存了需要打印的字符串,具体如图所示综上所述,我们对t t,d塔,b,s s这几个长用的s有了一定的了解,除此之外,可重定位文件中还包含其他的来保存与程序相关的信息。剩余的部分,我们将在下期的视频中介绍今天的内容就到这里,我们下期间。///huanhang//////huanhang///在上一期的视频中,我们以慢点欧为例介绍了可重敬为目标稳件的相关内容。根据h t的信息,我们画出了不同s在e r f中的位置。关于剩余s的介绍,我们以列表的形式给出链接过程的本质就是把多个不同的目标文件黏合到一起。为了使不同的目标文件之间能够相互黏合,这些目标文件之间必须有固定的规则才行,我们可以将符号看作链接中的黏合剂。整个链接过程正是基于符号,才能够正确完成,接下来我们重点看一下符号表的详细内容集中查看符号表的内容,///huanhang//////huanhang///也需要使用瑞d e o f具体的命令,如途索示,我们可以看到整个符号表从零开始,一共包含17个符号,最后一列给出了符号的名字,可能有的同学已经忘记了慢点。C的内容在介绍符号表的内容之前,我们先来回顾一下慢点。C的内容。图中这段代码定义了两个全局变量。C t和函数放个c。仅仅执行一个简单的打印功能。慢。函数内定义了两个局部静态变量。A和b以上就是慢点。C的全部内容,整个程序代码还是比较简单的,接下来我们看一下符号表中的符号与言程序之间存在着什么样的关系?///huanhang//////huanhang///首先,我们来看一下符号慢和放在原程序中,我们定义了两个函数,分别是函数慢和函数放n,所以符号表中这二者的类型是函数,由于函数慢和放是全居可见的,因此这里的邦定字段也是全局的,关于n d x表示的是s的索引值,关于索引值与具体c的对应关系,可以查看t来确定例如所引之一。表示e x三表示d t s c,我们知道。函数慢和函数放n所在的位置是t x s c,所以符号慢和的n d x唯一y六表示函数相对于e x起始位置的偏移亮s z。///huanhang//////huanhang///表示所占字节束通过图中的信息,我们发现函数放可是从零开始大小是36个字节,这里需要注意的是歪六的。值是16尽制,所以还说慢子其实地址是零x二四。紧跟在函数放课之后,关于字段v I s在c语言中并未使用,我们可以忽略这个字段。综上所述就是函数慢和放在符号表中所对应的描述。关于符号r f虽然他也是一个函数,但是函数f只是在慢点c中被引用,由于它的定义并不在慢点c中,所以它的n d x是昂d的类型,这里需要注意一下,///huanhang//////huanhang///接下来我们看一下。关于全局变量抗t和歪的描述,在符号表中,c t和歪的类型是o t,这里的o b t表示符号是个数据对象,例如变量和数组在符号表中的类型都是用来表示虽然和都是全局变量,但是,二者的n d x值不同,也就是说,二者处于不同的s中,导致抗和位于不同,s的原因是经过了初始化而没有由于索引这三表方示d s,所以从符号表中可以看出c t是位于s中,但是y却位于c m中,不知道大家是否还记得在上一期的视频中,///huanhang//////huanhang///我们讲过未初始化的全局变量是放在b s s中,然而符号表中给出的却是另外一个s。通过查看s b,我们并没有发现的存在关于的问题,这里稍微介绍一下,实际上和b s s的区别很小,二者之间主要的区别是仅用来存放未出始化的全局变量,而b s s用来存放未出始化的静态变量,以及初始化为零的全局或者静态变量,关于为什么要这么区分,我们在后面讲述符号解析的时候再来介绍,接下来我们看一下局部静态变量。A和b与全局变量一样。///huanhang//////huanhang///二者的类型也是o b t通过帮定字段,这里我们可以看到局部静态变量与全局变量的区别。由于我们对局部静态变量a进行了初始化,所以它的n d x值13,它与全局变量c t处于同一个c,虽然我们也对b进行了初始化,但是这个初始化并没有什么用,所以符号b的n d x值等于四。与符号a不同,同样也与未初始化的全局变量歪六不同,对于变量名从a和b变成了a点2254和b点2255。这种处理方式被称为名称修饰,主要为了防止静态变量的名字冲突。///huanhang//////huanhang///除此之外,符号表中还有一些符号名没有显示的表象,实际上他们的符号名就是他们三的名称,例如索引为二的n d x等于一,那么它的符号名就是t这一类符号大致了解即可看到这里,不知道有没有同学发现我们在慢点c中还定义了一个局部变量。X这个局部变量并没有出现在符号表中,这是因为局部变量在运行时栈中被管理链接器对此类符号并不感兴趣,所以局部变量的信息不会出现在符号表中以上就是符号表的全部内容了,对于每一个可重定位目标文件都有一个符号表。///huanhang//////huanhang///这个符号表包含该模块定义和引用的符号信息,在链接器的上下文中有三种不同的符号,第一种是由该模块定义,同时能够被其它模块引用的全局符号,例如慢点c中定义的函数放可以及全局变量。C t和歪l,第二种是被其他模块定义同时被该模块引用的全局符号。这些符号称为外部符号,第三种是只能被该模块定义和引用的局部符号,区别局部符号和全局符号的关键就是s克属性。带有s属性的函数,以及变量是不能被其他模块引用的。对于c语言来说,///huanhang//////huanhang///s c属性的功能就是隐藏模块内部的变量以及函数声明这个功能类似c加加和j v中的p b和普t任何带忧属性声明的全聚变量或者函数都是模块私有的,也就是说,任何不带s属性声明的全局变量和函数都是公共的,可以被其他模块访问,因此尽可能用属性来保护你的变量和函数是一种很好的编程习惯今天的视频就到这里,我们下期见。///huanhang//////huanhang///上一期的视频中,我们介绍了符号和符号表的相关内容。这一期我们来看一下链接器是如何进行符号解析的。首先,我们看一个代码顺利图中,这段代码仅仅对函数获做了一个声明函数慢中调用了函数获。如果我们只对这个文件进行编译和汇编,而不执行链接操作此时编译和汇编是没有问题的,具体如出所事,这是因为当编译器遇到一个不是在当前模块中定义的符号时,它会假设该符号是在其他某个模块中定义的,我们可以来看一下这个原程序所对应的符号表,///huanhang//////huanhang///虽然原程序中只是声明了函数符汇编器还是为它生成了相应的符号,不过在链接生成可执行文件时,链接器在其他数模块中都找不到这个被引用符号的定义,此时就会输出一条错误信息,并且终止链接的操作具体如图所示以上是针对找不到符号定义的情况,这种情况相对容易理解,如果多个可重定位文件中定义了同名的全句符号,此时应该如何处理。呢首先,我们介绍一下强符号和弱符号的概念。在编译时,编译器向会编器输出每个全剧符号,或者是强或者是弱,///huanhang//////huanhang///接下来汇编器把这个强弱信息隐含的编码在符号表中,对于函数和以初始化的全局变量被定义为强符号未初始化的全局变量是弱符号,关于处理多重定义的符号名可以分为三种情况,第一种情况是多个同名的强符号一起出现具体,我们通过一个势力来说明一下。例如图中这两个原文件中都有函数名为慢的函数。在这种情况下,链接器将生成一条错误的信息,这是因为强符号慢被定义了两次。除此之外,具有相同的以初始化的全局变量名也会产生类似的错误。///huanhang//////huanhang///例如图中这个事例x是以初始化的全聚变料属于强符号。与刚才的情况类似,猎机器也会报错。综上所述,根据强弱符号的定义链接器不允许有多个同名的强符号一起出现第二种情况是一个强符号和多个同名。若符号一起出现具体如图所示,由于b3点c中的全局变量x并未出始化,因此x属于弱符号,那么链接器会选择在f3点c中定义的强符号,此时链接器可以生成可直行稳健,并不会提示错误或者警告在运行时还数f加x值从15213改为15212,///huanhang//////huanhang///如果这两个文件是由不同的开发人员完成的,那么慢。函数的作者会对这个结果感到十分的意外,我们再来看第三种情况,如果x都是未初始化的,那么他们都属于弱符号,此时也会发生相同的情况。对于上述两种情况会造成一些不易察觉的运行时,错误,尤其是当重复的符号。定义是不同的类型式,我们再来看一个例子,其中x在一个文件中定义为I n t类型,而在另一个文件中定义为d b l类型,在64位的机器上,b l类型占八个字减,///huanhang//////huanhang///而I n t类型占四个字减在我们的系统中,x的地址是零x601020y的地址是601024,因此这里对x负值将用负林的双精度浮点数表示覆盖x和外的位置,具体如出索示,这是一个很小并且很难被发现的错误,尤其是链接器只会发出一个警告,而且通常程序在执行很久以后才会表现出来。在一个拥用成百上千个模块的大型系统中,这种类型的错误是很难改正的,尤其是很多的开发人员并不了解链机器的工作原理。为了避免这类错误,可以在编译时添加杠f n o杠m的编译选项,///huanhang//////huanhang///这个编译选项会告诉链接器。当遇到多重定义的全局符号时,触发一个错误,或者使用杠w I r选项。这个选项会把所有的警告都变为错误以上就是链接器如何解析多重定义的全局符号的情况,相信学过c语言的同学都使用过p f函数,大多数人都知道他是c语言提供的一个库函数。接下来,我们看一下,链接器是如何使用这一类静态库的。以r s o c九九为例,它定义了标准的I o字符串操作和整数数学函数,例函数,a托I f f s n,///huanhang//////huanhang///d等。这些函数都在赖b。C的库中,可能大家对点a的文件后缀名有些陌生。在l系统中,静态库以一种称为阿k的特殊文件格式存放在磁盘上。阿k文件是一组可重定位目标文件的集合,我们可以通过o b g当铺来看一下这个静态库都包含哪些目标文件剧体命令,如托所是由于显示的内容比较多,我们将输出的内容从定向到一个文件中使用命令。格尔p搜索到普n f点o位于第6615行。打开这个文件,我们可以看到p的f被定义在了p r n f点欧中看到这里,///huanhang//////huanhang///似乎我们已经找到了最终的实现机制。这里我们也可以使用a r将l b c中所有的目标文件减压到当前目录具体命令,如出所事,我们可以统计出赖c中一共包含1690个目标文件以上就是静态库。L a p c的相关内容。接下来,我们通过一个简单的例子来看一下静态库是如何构造的。图中左边,这在代码位于爱的w c点c中。函数I的功能是实现向量元素的礼加右边这段代码位于妈t y点c中。函数。M t y的功能是实现向量元素的累积。///huanhang//////huanhang///具体如此索视根据之前所学的内容,我们可以使用知c c来编译这两个原文件,其中u c表示只进行编译和汇编不执行链接的操作。这样一来,我们就得到了可重定位目标稳件爱着y e t点o和t点。O。构造静态库文件,我们需要使用a r具体命令,如出所事。执行完这条命令,我们就可以得到一个名为赖卜歪的静态库以上就是静态库的构造过程。整个过程比较简单。为了掩饰金态库的用法,我编写了一个代码是里函数慢调用了函数爱t,///huanhang//////huanhang///其中投文件e点一之中定义了l中的函数原型。为了创建可执行文件,我们首先编译原文件慢点c,然后在链接的时候加入静态库赖具体命令。如足所事,这样一来我们就得到了可执行文件。P r o j。接下来,我们来看一下具体的链接流程。当链接器运行时,它确定了慢点。欧洲引用了I德x点欧洲所定义的I德符号,所以链接器就从中复制了I d x点o到可执性文件,因为程序中没有引用到t y x点o中所定义的符号,所以链接系就不会将这个模块复制到可执行文件。///huanhang//////huanhang///除此之外,链接器还会从l b c中复制普f点欧模块以及其他的c运行时所需要了模块,最终将这些文件一起打包生成可执行文件具体,如托所事。综上所述,关于静态库以及静态链接的相关内容就介绍完了今天的视频就到这里,我们下期间。///huanhang//////huanhang///在上一期的视频中,我们介绍了符号引用以及静态库的相关内容,并且使用a r工具,展示了如何构造一个静态库以及如何应用。接下来我们看一下。链接器是如何使用静态库来解析引用的。在符号解析阶段练接器从左到右,按照命令行中出现的顺序来扫描可重定位稳件和静态库稳件。例如针对图中这一条命令链接器先处理慢点。O在处理赖布y克特点a最后处理赖b c点a由于编译器驱动程序总会把l c点a传给链接器,所以命令中不必显示的引用l a,///huanhang//////huanhang///b c点a在扫描的过程中,链接器一共维护了三个集合,第一个是集合意在链接扫描的过程中发现了可重定位目标标文件就会放到这个集合中,在链接即将完成的时候,这个集合中的文件最终会被合并起来,形成可执行文件。第二个是集合u链接器会把引用了,但是尚未定义的符号放在这个集合里,第三个是集合地,它用来存放输入文件中,以定义的符号链接最开始时,这三个集合均为空。对于命令行上每一个输入文件。F链接器都会判断。F是一个目标文件,///huanhang//////huanhang///还是一个静态库文件,如果f是一个目标文件,那么链接器会把f添加到集合一中,同时修改集合u和d来反映f中的符号。定义和引用。针对图中的这条命令链接器会判断慢点。O是一个目标稳健,那么链接器会把慢点o放到集合意中,具体如搜所事。通过慢点,欧的符号表可以看到这个目标文件中存在两个不再当前模块中定义的符号分别是I的和普f接下来,链接器把符号I t和f放到集合幽中。此时链接系假设他们在其他模块中被定义,所以并不会报错。///huanhang//////huanhang///除此之外,漫点欧洲已经定义的全局符号,x y z以及慢会放到集合地中。此时集合地和u分别反映了慢减欧的符号定义,以及引用当文件慢减欧处理完毕之后,然后继续处理下一个输入文件。此时我们发现下一个文件是一个静态库文件,那么链接器会尝试在这个竞态库文件中寻找集合优中未解析的符号,例如静态库门件赖不x点a存在两个成员分别为I t点o和点。O当练机器发现成员x中存在未定义的符号。D定义,此时就把I x点o加到集合一中,///huanhang//////huanhang///然后将集合幽衷的符号删除。如果点欧中还定义了其他符号,还需要添加到集合地中,所以艾着c n t要被添加到集合地中。链接系处理完成员I d x点o之后还要处理t y点o对于静态库文件中的所有成员目标文件都要依次进行上述处理,直到集合优和集合地不再发生变化。此时,任何不包含在集合意中的成员目标文件都被简单地丢弃。对于这个例子,马t y x点o被丢弃,I d x点o被保留,最后练习器还要扫描l c a f点o会加入到集合一中。///huanhang//////huanhang///集合优容的符号。F被删除。上述操作。执行完毕后,如果集合优是空的链接器会合并集合异中的文件来生成可执行文件。如果链机器完成对上述命令行上所有的输入文件的扫描后,集合优势非空的,那就说明程序中使用了未定义的符号,此时练习器就会输出一个错误,并终止以上就是链接器使用静态库来解析引用的过程。不幸的是,这种算法会导致一些令人困扰的链接错误,因为命令行上的目标稳健和库文件的输入顺序非常重要,例如,我们将上述势例中的赖x点a与慢点欧的输入顺序进行调换,///huanhang//////huanhang///那么链接就会失败,这是因为当链接器处理赖不x点a时,集合优是空的,所以并没有赖不x点a中的成员目标文件会被添加到集合一种。接下来,链机器处以慢点欧之后,I的会被添加到集合优种,因此对的引用不会被解析到,所以链接器会产生一条错误信息并终止,所以在使用静态链接的过程中,文件的输入顺序十分重要。通常情况下,关于库的一般使用准则就是将他们放在命令行的结尾。如果各个裤的成员是相互独立的,也就是说,不同库的成员之间没有相互引用时,///huanhang//////huanhang///那么这些库就可以以任意的顺序放在命列行的结尾处。另一方面,如果库不是独立的,那么必须对他们进行排序,我们再来看一个例子,例如f点c调用了l b,x点a和l b这点a中的函数,而这两个库赖x点。A和l这一点a又调用了赖部外点a中的函数,那么在命令行中赖不x点a和l这点a的输入顺序必须位于赖b外点。A之前具体顺序如出所事,我们再来看另外一种情况,如果库之间需要满足依赖关系,可以在命令行上重复库,例如f d c调用了l b x点a中的函数,///huanhang//////huanhang///其中赖版x点a调用了赖部外点a中的函数,然而赖不外点。A又调用了赖b。X点一中的函数,也就是说,静态库赖板x点a和静态库赖部外点a之间存在相互引用,此时赖百x点a必须在命运行上重复出现具体命令。如此。索事,不过还有另外一种解决方法,就是把赖b x点a和赖不外点a合并成一个静态库文件以上就是符号解析以及静态链接的全部内容了,今天的视频就到这里,我们下期见。///huanhang//////huanhang///在上一期的视频中,我们介绍了符号解析的相关内容。当链机器把代码中的符号引用和对应的符号定义关联起来之后,链接器就可以确定要将哪些目标文件进行合并了,同时链接器也获得了这些目标文件的代码节和数据节的大小信息。接下来开始进行重定位的操作。在这个过程中,链接器合并输入模块,并为每个符号分配运行时,地址具体重定位的过程分为两步,第一步是重定位节和符号。定义,第二步是重定位节中的符号。引用。接下来,我们以同中的代码为例来看一下重定位的第一步做了什么?///huanhang//////huanhang///在七杠一的课程中,我们提到过这个代码式里,图中的视例有两个原文件组成,分别为慢点。C和萨姆点。C链接器把慢点,欧和萨姆典欧中所有类型相同的合并为一个新的,例如新合城的x就是可执行文件的t e x合称之前慢点,欧和萨姆典,欧洲的泰e x赛克都是从零开始的。袁书中假设合成后的,其实地址是4004d零,原因是在64位的x系统中,e r f可执行文件默认从地址零x400000处开始分配。由于一r r f的h德尔以及克之前还有一些其他的信息,///huanhang//////huanhang///所以假定t e x是从4004d零处开始。这步完成后,程序中,每条指令和全聚变量都有了唯一的运行时内存地址,第二部是重庆卫塞中的符号引用,例如还数慢中调用了函数萨姆图中这条靠指令所对应的就是函数萨姆的调用,此时,靠指令的目的地址是零,显然这不是函数萨姆的真正地址在这一步中,练机器需要修改对符号。S m的引用,使它指向正确的运行时,地址,不过要执行这一步。链接器还要依赖于可重定位条目的数据结构。接下来我们看一下什么是重定位条目,///huanhang//////huanhang///我们知道,可重定位目标文件是由汇编器产生的。当汇编器在生成可重定位目标文件时,并不知道数据和代码最终放在内存的什么位置。初次之外汇编系也不知道该模块所引用的外部定义的函数以及全局变量的位置,所以当汇编器遇到最终位置不确定的符号。引用时,它就产生一个重定位条目这个可重定位条目的功能是用来告诉链接器在合成可执行文件时,应该如何修改这个引用关于代码的重敬位条目释放在点阿。E l t x中,对于以初始化数据的重定为条目,///huanhang//////huanhang///放在点r e l点d t中。图中展示了e r f重定位条目的结构体定义每个条目由四个字段组成,其中第一个字段。奥夫赛表示,被修改的引用的截片一亮,电接器会根据第二个字段太p来修改新的引用。第三个字段表示被修改的引用是哪一个符号,最后一个的是一个常数,一些类型的重定位要使用它来做偏移调整。U f中定义了多达32种不同的重定位类型,不过我们只需要关心其中两种最基本的重定位类型即可分别是相对地址的重定位和绝对地址的重定位以上就是从定位条目的大致内容。///huanhang//////huanhang///在这个例子中,徽编系产生了两个重定位条目,一个是对萨姆的引用,另外一个是对全局变量艾瑞的引用具体如托索事,接下来我们看一下。链机器是如何使用重定位条目来进行重定位的,例如指令靠子,起始地址位于字节偏移零x e的地方。零x e八表示指令。靠着操作码在重庆位之前紧跟在操作码之后的内容被汇编器填充为零,接下来,电机器需要根据重定位条目来确定这部分的具体内容对于函数s m的重定位条目有四个字段组成具体,如图所示。///huanhang//////huanhang///首先,链接器需要根据重定位条目计算出引用的运行时,地址,具体的计算方法是通过函数慢的,其实地址与重定位条目中的偏一量字段相加。函数慢的,其实一纸在重定位的第一步可以得到原书中,假设是4004d零。这样一来,我们就得到了引用的运行时地址,然后更新这个符号。引用,使得他在运行时指向萨姆函数具体的计算方法是用函数s m的七是地址减去刚才计算。得到的运行时,地址,然后再加上一个I d的字段做一下修正原书中假设函数萨m的地址为400418I n的字段默认为负四,///huanhang//////huanhang///因此德俄结果为零x五,实际上这一步就是求两个地址之间的相对位置。经过上述计算,在最终得到的可执行程序中,靠指令的形式,如图所示,也就是说在程序运行时指定靠存放在地址4004d一处。当c p u执行靠指令时,此时p c的职为400413。至于p c的值,为什么是400413,是因为p c的值等于正在执行指令的下一条指令的地址,具体如出所示。执行这条靠指令可以分为两步,第一步。C p u先把p c的值压入战中,///huanhang//////huanhang///这一步很容易理解,因为指定靠要执行函数调用,接下来要发生跳转。函数。执行完毕后,还要继续执行这一条I的指令,所以要先把p c的值压占保存,第二步是修改p c的值,具体的修改方法是,用当前p c的值加上偏移量,根据刚才计算,得到的偏移量为零x五二者相加得到的地址为400418,恰好就是函数s m的第一条指令。综上所述就是重定位相对引用的具体过程。接下来我们看一下重定位绝对引用。例如这条木指令把艾瑞的气势地址传给了寄存器。///huanhang//////huanhang///E d I。这条木纸间的七始地址为零x九b f表示募物指令的操作码紧跟在b f之后就是对符号艾瑞引用的绝对地址对符号,艾瑞的引用也对应一条可重定位条目,其中字段告诉编译器要从偏移量零x a开始修改,这里的类型是绝对地址引用。针对这种类型的计算比较简单。I d n的字段莫认为零假设练习器已经确定埃瑞所在的位于601018处,所以这里的绝对地址引用就是601018。当执行完重定位之后,这条目物指令中的源操作数由零变成了601018。///huanhang//////huanhang///由于差八六采用小端法存储数据,所以这里要按照自检易序进行替换。具体结果如此,所事以上就是重定位绝对地址引用的计算过程。经过上述重定位之后,我们就可以确定目标文件的t e x c和d t s的内容。当程序执行加载的时候,加载器会把这些s x n中的字节直接复制到内存里,不用执行任何修改,就可以执行以上就是重定位的全部内容,今天的视频就到这里,我们下期建。 ///huanhang///有效至7-6. 重定位