【2020/12/4修订】【梳理】计算机组成与设计 第2章 指令(docx)

配套教材:
Computer Organization and Design: The Hardware / Software Interface (5th Edition)
这是专业必修课《计算机组成原理》的复习指引。建议将本复习指导与博客中的《简明操作系统原理》配合复习。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
计算机组成原理不是语言课,本复习指导对用到的编程语言的语法的讲解也不会很细致。如果不知道代码中的一些关键字、指令或函数的具体用法,你应当自行查找相关资料。


第二章 指令

第一节 MIPS寄存器与指令

第二节 编译与链接

第三节 ARM与x86

注意

链接:https://pan.baidu.com/s/1qLqx5Hn1fZFddn4GsFtvoQ
提取码:0000


第二章 指令

第一节 MIPS寄存器与指令
在第一章已经提到过:指令集包含了芯片支持的全部指令。
指令相当于计算机语言的词库。但是计算机语言不像不同国家的语言那样区别那么大,它们更像一个国家的不同地区的方言。如果你掌握了一门编程语言或者一种ISA的指令,那么掌握其它编程语言或其它ISA的指令的难度就要降低很多。

MIPS是最早商用的指令集,始于1980年代,现分为MIPS32和MIPS64两个版本。ARMv7指令集和MIPS比较接近,是广泛应用的指令集之一。ARMv8诞生于2013年,是ARMv7的64位版本。但ARMv8似乎同样更接近MIPS而不是ARMv7。
获得了ARM公司的授权的合作伙伴在2019Q4总共出货了大约64亿颗ARM芯片,其中约42亿芯片是Cortex-M系列微控制器。微控制器广泛应用于嵌入式市场和消费级产品。ARM授权给其它公司的常见芯片设计主要分为Cortex-A、Cortex-R和Cortex-M三个系列。ARM自己不直接销售芯片,而是将设计的架构和指令集授权给其它公司使用或改进,并将芯片成品出售。它们的指令集有些许不同,分为ARMv7-A、ARMv7-R和ARMv7-M;类似地,ARMv8指令集分为ARMv8-A、ARMv8-R、ARMv8-M。一些服务器CPU也开始使用ARMv8指令集。2020年5月,ARM还推出了Cortex-X系列,首个CPU设计为Cortex-X1,其指令集为ARMv8.2-A。

许多机构都基于ARM指令集自行研发CPU架构及产品:
高通的Krait和苹果的Swift是基于ARMv7研发的。
高通的Krait、Kryo架构;三星的Mongoose;NVIDIA的Denver、Carmel;以及苹果的Cyclone、Typhoon、Twister、Hurricane、Monsoon / Mistral、Vortex / Tempest、Lightning / Thunder、Firestorm / Icestorm,都是基于ARMv8研发的。但Apple的64位CPU无法兼容32位。
龙芯是中国科学院计算技术研究所研发的基于MIPS指令集的CPU。飞腾CPU则由国防科技大学计算机学院研制,原先基于SPARC,后续产品则基于ARMv8指令集,现服役于“天河”系列超级计算机。申威CPU由总参谋部第五十六研究所(江南计算技术研究所)研制,其“申威64”指令集来源于DEC Alpha 21164,现服役于“神威”系列超级计算机。
Intel / AMD的x86 CPU则具有x86指令集,它也是一系列指令集的集合。x86指令集及具备x86指令集的CPU主要应用于PC、工作站、服务器和超级计算机。

许多指令集具有不能忽略的相似性,因为所有的计算机基于的底层硬件技术和原理都很接近;最基本的操作,如算术运算、移位、逻辑运算、数据传输(如:读写)、控制(调用、跳转、……)等指令,也是所有计算机都必须支持的。例如,一个MIPS CPU支持的部分汇编指令和操作数如下:

计算机中,大多数指令都是无操作数(operand)(参与一个运算(操作,operation)的元素个数)、一个操作数、两个操作数或三个操作数的指令(一些指令集中的指令支持更多的操作数),而不是可变的。理由很简单:如果把许多指令设计成支持可变数量操作数的,那么硬件实现就非常复杂。
基于这点事实,我们引出设计的第一个原则:规整的设计更简单。

汇编指令是直接对应机器语言的低级指令。高级语言对操作数的数量没有限制,但汇编指令有。操作数保存在内存中或寄存器(register)中。寄存器是用于临时存储操作数(包括运算结果)的部件。寄存器的速度要快于高速缓存,但非常昂贵,因此数量很少。
8086的寄存器是16位的,MIPS32 CPU的寄存器是32位的,MIPS64 CPU的寄存器是64位的。MMX指令集用到的寄存器有8个,长度为64位:mm0到mm7,都是浮点单元(Float point unit,FPU)的80位寄存器的低64位。SSE指令集中的指令专用的寄存器包括8个128位的xmm0到xmm7,x86-64还有额外的8个寄存器xmm8到xmm15。此外还有一个32位的控制 / 状态寄存器。AVX指令集使用的寄存器则是128位的xmm0到xmm15(x86-64模式下还有xmm16到xmm31)。AVX2指令集将操作数的宽度扩展到了256位,寄存器代号从ymm0到ymm15(x86-64模式下还有ymm16到ymm31),并可以输入后缀相同的xmm寄存器来取得低128位。AVX-512指令集将操作数的宽度进一步扩展到了512位,在x86-64下,寄存器代号从zmm0到zmm31,并可以输入后缀相同的xmm、ymm寄存器来分别取得低256位、低128位。
一个字(Word)在x86中通常是16位的。但在MIPS CPU中,一个字的长度是32位。

设计的第二个原则:更小的部件更快。如果把寄存器做得很多、缓存做得很大,那么它们的速度就会变慢。因为很多时候电信号要传递更远的距离,这使得时钟周期变长,从而不利于提升CPU频率。当然,这个并不是绝对的。例如,如果寄存器的总数是31个而不是32个,那么性能未必更好。
编译器负责将最常用的变量保存到寄存器中。为了使寄存器尽可能提升性能,编译器需要正确利用寄存器,而且寄存器的数量不能太少也不能太多。许多ISA都包含16个或32个通用寄存器,以及其它一些专用的寄存器。如果寄存器数量较多,则指令中用于表示寄存器的字段(field)的长度就要增加,不利于实现定长指令(见后文)。
编译器会尽量将最常用的变量放入寄存器。当编译器产生的代码在某个时刻需要同时用到的寄存器数量多于CPU的可用寄存器数量时,不常用的变量(或稍后使用的变量)将会被重新存入内存,这称为寄存器溢出(register spilling)。

寄存器的数量实在太少,因此无法保存较多的数据或较复杂的数据结构。于是这些数据被保存在内存中。所有的CPU都包含数据传输指令(数据传送指令、数据搬移指令),用于在内存与寄存器之间搬运数据。如果需要访问内存中的内容,那么指令的其中一个操作数就需要是内存地址(address)。地址刻画了数据在内存中的位置。内存可以看成一个很大的一维数组,地址从0开始编号,并且每个字节的地址可以看成这个数组的下标。

把数据从内存读入寄存器的指令,称为读取(加载、装入,load)指令。Load指令的操作数自然是寄存器和内存地址。在MIPS架构的CPU中,任何变量的起始地址必须是4的倍数。这称为内存对齐(memory alignment)。如果尝试访问没有对齐的数据,会报错。x86架构则支持非对齐访问其实现机制是将非对齐访问指令拆分成多条指令执行,结合拼接(或者拆分)指令获取数据,缺点是牺牲性能。ARMv5不支持非对齐访问,ARMv6的部分指令支持,而ARMv7、ARMv8架构的对齐检查可以手动开启或关闭。如果使能(启用)对齐检查,那么任何指令的非对齐访问均会触发非对齐异常(exception)。注意:采用ARMv8指令集的CPU中,A64指令集的部分指令的非对齐访问在关闭对齐检查的情况下仍然会产生非对齐异常。也可以用软件等效实现非对齐访问,但会降低性能。
在编译时,可以通过调整编译选项来启用或关闭非对齐访问。
把数据从寄存器写入内存的指令,称为存储(store)指令。MIPS的Load / Store(L / S)指令一次只读一个数据、写一个数据,不进行运算。进行算术(arithmetic)时,一般都在寄存器中进行,使得运算更快。

内存中的数据有大端(big endian)和小端(little endian)两种读写模式。采用哪种模式是CPU架构决定的。MIPS是大端阵营的,一个变量的高位保存在低地址中,低位保存在高地址中;小端模式则相反。大端和小端分别也称“高尾端”和“低尾端”,其含义很清晰:大端即高尾端,意味着变量的末尾比变量的头部的地址更高;小端则相反。x86 CPU一般采用小端,IBM、Sun的CPU(SPARC)一般采用大端。有的CPU既能工作于小端也能工作于大端,如ARM、Alpha、Power PC。

立即数(immediate operand,或immediate)是常量或表达式的结果。汇编器在将汇编语言转换为机器语言时,将立即数编码到指令中,这样就避免了执行包含立即数的指令时总是要从内存或寄存器中读取常量,显著提升了性能并降低了能耗。

现代计算机一律采用二进制。事实上,最早的商用计算机是进行十进制计算的,但是效率很低。因此,后来的计算机全部采用二进制进行运算,只在输入、输出时根据需要进行二进制与十进制的互相转换。

以前的计算机采用额外的位来记录符号位(sign bit),这导致运算的时候要额外花费一步来设置符号位,而且还会出现正0和负0,程序员进行编程的时候容易出错。后来,经过大量的研究,最终将内置类型中的最高位指定为符号位,0为正,1为负。于是一个int型的数据的取值范围是这样的:

这种表示法叫做二补数(two’s complement)表示法。一个n位的二进制无符号数x与其按位取反的数~x的和是2n。也就是说x的二补数是2n-x。还有一补数(one’s complement)表示法:一个数x的一补数是x(代表按位取非,见第19点),即2^n-x-1。
对一个32位的有符号数,如果用一补数表示法,那么0x80000000到0xFFFFFFFF表示-2147483647到-0。如果采用二补数表示法表示数,可以把加减法统一按加法运算,提升性能。所以,二补数表示法很快成为全部计算机采用的表示法。
硬件在判断一个数是否为负时,只需要读取最高位(符号位)。
以int型数据为例,一个数x=x_31 x_30 x_29…x_1 x_0可以表示成:
x_31 (-2^31 )+x_30 2^30+x_29 2^29+⋯+x_1 2^1+x_0 2^0
有符号数取相反数的公式是:-x=~x+1。

溢出(overflow)是指运算结果(无论是中间结果还是最后结果)的绝对值部分大于字长能表示的最大绝对值的现象。两个正数相加,结果大于机器的字长能表示的最大正数,称为正溢。两个负数相加,结果小于机器的字长能表示的最大负数,称为负溢。

将字节数更少的变量读入寄存器时,高位要填充。对无符号数,多出来的高位直接填零;对有符号数,则进行带符号扩展(sign extension)。在C / C++中,进行类型转换(casting)时也是如此。一个有符号数转为占用字节数更多的数,则多出来的全部高位填充原数的符号位。字节数较少的有符号数转换为字节数较多的有符号数时,值不变;但是不同字节数的有符号数和无符号数互相转换时,数值就可能发生改变了。不过,当变量或常量表示地址(即变量为指针)时,没有符号位。地址总是非负的。

指令是具有一定的规范的,称为指令格式(instruction format)。MIPS的每条指令都是32位长,而x86的指令是变长的,平均长度约3个字节。这些指令是从汇编指令转换来的,对应的语言称为机器语言(machine language)。一长串这样的机器指令,也叫机器码(machine code)。

设计原则3:好的设计要求好的折中。
MIPS的32位机器指令分成了6个字段:

op:操作码(opcode),指定了操作的种类(如:加法)。
rs:第一个操作数(寄存器)。
rt:第二个操作数(寄存器)。
rd:目标操作数(寄存器)。
shamt:移位数量(这个区域不太常用)。
funct:函数(函数码),指定操作码对应的操作的变体。
到这里大家可以看出来,一个操作数只有5个bit,也就是说只能表示32种数。想表示更多的数的时候怎么办呢?MIPS的设计者们选择这样一种折中方案:保持所有的指令长度相同,但不同类型的指令采用不同的指令格式。他们又设计了另外一种格式(I-type或I-format,I = immediate),区别于上述格式(R-type或R-format,R = register):

这种格式的指令可以表示-32768到32767这65536(216)个常数(立即数),也可以表示该范围内的偏移地址。
虽然支持多种指令格式会使得硬件变得复杂,但是这两种格式的很多地方是相近的,比如前3个区域的功能和边界都一样,I型的最后一个区域的长度正好是R型的后三个区域的长度,所以实际上没有增加太多的复杂度。至于采用哪种格式,则由第一个区域的操作码决定。

逻辑运算是所有计算机必须支持的运算。部分C、Java的运算符与MIPS指令的对应关系如下表:

第一种逻辑运算是位移(shift)。位移分为左移(left shift,shift left)和右移(right shift,shift right)两种。左移把所有二进制位向左移动若干位,低位补零。如果数据类型是定长的,那么超出范围的高位自动丢失。右移把所有二进制位向右移动若干位,越过最低位的部分自动丢失。位移分为逻辑位移(logical shift)和算术位移(arithmetic shift)两种。逻辑左移和算术左移的规则是一样的;逻辑右移把空出的高位补0,算术位移把空出的高位补符号位。
MIPS的逻辑左移和逻辑右移指令分别是sll(shift left logical)和srl(shift right logical)。以左移指令为例:
sll t 2 , t2, t2,s0,4 # reg $t2 = reg $s0 << 4 bits
将其转换为机器指令是这样的:

shamt存放了位移位数。rs区域无意义。
向左位移一位,相当于把原数乘以2;向右位移一位,相当于把原数除以2(整除,并向负无穷大取整)。类比一下:把十进制数左移一位相当于乘以10;把十进制数右移一位相当于除以10(整除)。

AND或&运算符称为按位与。如果这些运算符所在的段落采用拉丁字母(英文字母)书写,那么把这些运算符大写,以免与英文连词混淆。AND对两个位数相同的数做运算,只有两个数对应的位都为1时,结果的这一位才为1。
AND可以使用掩码(mask)强制把原数指定的位设为0:掩码“隐藏”了一些值为1的位。
注意:如果将两个位数不同的数做逻辑运算,那么位数较少的数的高位自动补0直到位数相同(无符号扩展,也称零扩展(zero extension)),而不作带符号扩展(sign extension)。
OR或|运算符称为按位或。输入两个数,对应的位只要有一个为1,那么结果的对应位就为1。
NOT或~运算符是一元的,称为按位非(按位反、按位取反)。输入一个数,结果中对应的位与这个数中对应的位取值相反(0变1或1变0)。
XOR(exclusive or)或^运算符称为按位异或。输入两个数,只有输入的两数的对应位不同时,结果的对应位才为1。

所有的计算机语言都支持条件语句和循环语句。
MIPS中,if语句对应的汇编指令可能是:beq、bne,等等。两个指令分别表示branch if equal、branch if not equal。类似地,x86汇编中也有je jne jb ja jnb jna jbe jae jnbe jnae jg jng jge jnge jl jnl jle jnle等指令。其中b = below(无符号小于),a = above(无符号大于),n = not,e = equal,j = jump,g = greater(有符号大于),l = less(有符号小于)。这些语句称为条件分支(conditional branch),用于判断输入并根据输入情况执行指定语句。
条件分支的指令格式是I型的。后16个bit存储的是条件满足时跳转到的偏移地址(offset address)。基本上,所有的条件语句需要跳转的位置都离PC + 4(MIPS对所有条件分支使用PC相对寻址)不太远,所以16个bit一般足够。MIPS的每个数据的内存地址都按4的整数倍对齐,所以这16个bit可以覆盖的跳转范围实际上为218。
编译器经常创建一些在编程语言中没出现过的分支和标签。避免显式地编写这些标签和分支是使用高级语言的好处之一。
循环也使用分支指令实现,这里不再赘述。

一个基本块(basic block)是一段没有分支(结束时出现的分支除外)、没有标号(开始时出现的标签除外)的汇编语句。编译的一个早期步骤就是要把程序分成若干个基本块。

slt t 0 , t0, t0,s3,$s4 # $t0 = 1 if $s3 < s 4 s l t 表 示 s e t o n l e s s t h a n 。 该 指 令 也 有 立 即 数 版 本 s l t i 。 M I P S 用 s l t 、 s l t i 、 b e q 、 b n e 这 四 个 指 令 和 代 表 立 即 数 0 的 寄 存 器 s4 slt表示set on less than。该指令也有立即数版本slti。MIPS用sl

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值