计算机语言中的基本单词称为指令,一台计算机的全部指令称为该计算机的指令集。指令直接与计算机的运行性能、硬件结构密切相关,它是设计一台计算机的起始点和基本依据。通过对 MIPS 指令集的学习,要求掌握 MIPS 指令各字段包含的信息,掌握常用的 MIPS 指令和指令格式,深入理解指令的寻址方式,了解常见的指令功能和种类。
70 年代末期:大多数计算机的指令系统多达几百条。我们称这些计算机为复杂指令系统计算机(CISC)。但是如此庞大的指令系统难以保证正确性,不易调试维护,造成硬件资源浪费。为此人们又提出了便于 VLSI 技术实现的精简指令系统计算机(RISC)。
二、精简指令系统
精简指令集计算机 RISC 和 复杂指令集计算机 CISC 是当前 CPU 的两种架构。它们的区别在于不同的 CPU 设计理念和方法。 早期的 CPU 全部是 CISC 架构,它的设计目的是要用最少的机器语言指令来完成所需的计算任务。
(一)复杂指令集计算机 CISC
1.CISC 指令系统的主要特点
(1)指令系统复杂、庞大、指令数目多
例如:VAX11/780 机 303 条指令。
(2)指令格式多,寻址方式多
例如:VAX11/780 机 18 种寻址方式。
(3)指令字长不固定,各种指令使用频率相差很大。
2.CISC 的优点
(1)使目标程序得到优化,例如数组运算指令。
(2)给高级语言提供更好的支持;功能接近高级语言语句的指令提高编译效率。
(3)复杂指令对操作系统提供强有力的支持。
3.CISC 的缺点
CISC 的结构复杂,研制周期长,硬件成本高,硬件资源浪费。
(二)精简指令集计算机 RISC
1.RISC 指令系统的特点
(1)选用的是使用频率最高的一些简单指令。
(2)指令长度固定,指令格式及寻址方式种类少。
(3)只有取数/存数指令访问存储器,其余指令的操作都在寄存器之间进行。
(4)大多数指令可在一个机器周期内完成。
RISC 指令系统的特征:选取使用频率最高的一些简单指令,指今长度固定,指令格式种类少,寻址方式种类少,只有取数/存数指令访问存储器,其余的指令操作都在寄存器之间进行。因此,RISC 的 CPU 的寄存器较多。
RISC 的设计重点在于降低由硬件执行指令的复杂度。因为软件比硬件容易提供更大的灵活性和更高的智能,因此 RISC 设计对编译器有更高的要求;CISC 的设计则更侧重于硬件执行指令的功能,使 CISC 的指令变得很复杂。总之 RISC 注重编译器的设计,CISC 强调硬件的复杂性。
三、低级语言与硬件结构的关系
计算机语言有高级语言和低级语言之分。高级语言语句和用法与具体机器的指令系统无关。低级语言分机器语言(二进制语言)和汇编语言(符号语言),这两种语言都是面向机器的语言,它们和具体机器的指令系统密切相关。
计算机能够直接识别和执行的唯一语言是二进制语言,但人们采用符号语言或高级语言编写程序。为此,必须借助汇编程序或编译程序,把符号语言或高级语言翻译成二进制码组成的机器语言。
汇编语言依赖于计算机的硬件结构和指令系统。不同的机器有不同的指令,所以用汇编语言编写的程序不能在其他类型的机器上运行。
高级语言与计算机的硬件结构及指令系统无关,在编写程序方面比汇编语言优越。但是高级语言程序“看不见”机器的硬件结构,不能用于编写直接访问机器硬件资源的系统软件或设备控制软件。
四、MIPS 指令集
尽管机器语言种类繁多,但它们之间十分类似,其差异性更像人类语言中的“方言”,而非各自独立的语言。因此,理解了一种机器语言,其他的也就容易理解了。这种相似性一方面是因为所有计算机都是基于相似基本原理的硬件技术所构建的,另一方面是因为有一些基本操作是所有计算机都必须提供的。此外,计算机设计者有一个共同的目标:找到一种语言,可方便硬件和编译器的设计,且使性能最佳、成本和功耗最低。
用形式逻辑的方法可以很容易看到,在理论上存在着某种指令集足以控制任何的操作序列并使之执行。从当前的观点出发,在选择一个指令集时,真正的决定性因素是要更多地考虑其实际性质:指令集要求的设备简单性,它的应用对于解决实际重要问题的明确性以及它解决该类问题的处理速度。
无论是对 20 世纪 50 年代的计算机而言,还是对现代的计算机来说,“设备简单性”都是值得考虑的重要问题。我们的目的就是介绍符合此原则的一种指令集,介绍它怎样用硬件表示,以及它和高级编程语言之间的关系。
MIPS 是世界上很流行的一种 RISC 处理器。MIPS 的意思是“无内部互锁流水级的微处理器”(Microprocessor without interlocked piped stages),其机制是尽量利用软件办法避免流水线中的数据相关问题。它最早是在 80 年代初期由斯坦福(Stanford)大学 Hennessy 教授领导的研究小组研制出来的。MIPS 技术公司是美国著名的芯片设计公司,它采用精简指令系统计算结构(RISC)来设计芯片。和英特尔采用的复杂指令系统计算结构(CISC)相比,RISC 具有设计更简单、设计周期更短等优点,并可以应用更多先进的技术,开发更快的下一代处理器。
MIPS CPU 指令长度为 32 比特,按功能可以分为以下五类:加载和存储指令,算术指令,跳转和分支指令,杂类指令和协处理器指令。
一、机器指令的基本格式
计算机机器指令格式涉及多方面的因素,如指令长度、地址码结构以及操作码结构等,是一个很复杂的问题。
表示一条指令的机器字,称为指令字,通常简称指令。指令格式,则是指令字用二进制代码表示的结构形式
其中,操作码指明了指令的操作性质及功能,地址码则给出了操作数的地址。
二、地址码结构
计算机执行一条指令所需要的全部信息都必须包含在指令中。对于一般的双操作数运算类指令来说,除去操作码(OPCODE)之外,指令还应包含以下信息:
(1)第一操作数地址
(2)第二操作数地址
(3)操作结果存放地址
(4)下条将要执行指令的地址
这些信息可以在指令中显式的给出,称为显地址;也可以依照某种事先的约定,用隐含的方式给出,称为隐地址。下面从地址结构的角度来介绍几种指令格式。
操作数地址字段指示操作数或指令的地址。操作数地址字段的位数越多,访问内存的范围(寻址范围)越大。通常还包含寻址方式码。
根据一条指令中有几个操作数地址,可将该指令称为几操作数指令或几地址指令。目前二地址和一地址指令格式用的得最多。
零地址指令格式中只有操作码字段,没有地址码字段。
① 适用于不需操作数的指令。如空操作指令 NOP,停机指令 STOP 等。
② 在堆栈处理器中,若需操作数,则由堆栈指针 SP 指出。
前面介绍的操作数地址都是指主存单元的地址,实际上许多操作数可能是存放在通用寄存器里。随着计算技术和集成电路技术的迅速发展,许多计算机在 CPU 中设置了相当数量的通用寄存器,用它们来暂存运算数据或中间结果,这样可以大大减少访存次数,提高计算机的处理速度。
从操作数的物理位置来说,又可归结为三种类型: RR 型、SS 型和 RS 型三种。
RR 型指令:—两个操作数均来自寄存器的指令
SS 型指令:—两个操作数均来自内存的指令。
RS 型指令:—操作数分别来自寄存器和内存的指令。
1.规整型(定长编码)
这是一种最简单的编码方法,操作码字段的位数和位置是固定的。操作码位数固定,而且集中于第一个字段中。为了能表示整个指令系统中的全部指令,指令的操作码字段应当具有足够的位数。
假定指令系统共有 M 条指令,指令中操作码字段的位数为 n 位,则有如下关系式:
定长编码对于简化硬件设计,减少指令译码的时间是非常有利的。
变长编码的操作码字段的位数不固定,且分散地放在指令字的不同位置上。操作码的位数和位置随不同指令而不同,但指令码长度固定不变 。这种方式能够有效地压缩指令中操作码字段的平均长度,在字长较短的小、微型计算机上广泛采用。
显然,操作码字段的位数和位置不固定将增加指令译码和分析的难度,使控制器的设计复杂化。
最常用的非规整型编码方式是扩展操作码法。因为如果指令长度一定,则地址码与操作码字段的长度是相互制约的。为了解决这一矛盾,让操作数地址个数多的指令(三地址指令)的操作码字段短些,操作数地址个数少的指令(一或零地址指令)的操作码字段长些,这样既能充分地利用指令的各个字段,又能在不增加指令长度的情况下扩展操作码的位数,使它能表示更多的指令。
四、指令助记符
由于硬件只能识别 1 和 0,所以采用二进制操作码是必要的,但是我们用二进制来书写程序却非常麻烦。为了便于书写和阅读程序,每条指令通常用 3 个或 4 个英文缩写字母来表示。这种缩写码叫做指令助记符。
需要注意的是,在不同的计算机中,指令助记符的规定是不一样的。因此,指令助记符还必须转换成与它们相对应的二进制码。
任何计算机必须能够执行算术运算 MIPS 汇编语言的下述记法
add a,b,c
表示将两个变量 b 和 c 相加,并将它们的和放入变量 a 中。
这种记法的表示方式是固定的:每条 MIPS 算术指令只执行一个操作,并且有且仅有三个变量。例如,若要将变量 b、c、d、e 之和放人变量 a 中,下面的指令序列将完成此四个变量的相加:
add a,b,c # The sum of b and c is placed in a.
add a,a,d # The sum of b,c,and d is now in a.
add a,a,e # The sum of b,c,d,and e is now in a.
可知,使用 3 条指令完成了 4 个变量的相加。
上述代码每一行中,符号“#”右边的是注释,用于帮助人们理解程序,而计算机将忽略它们。
与加法类似的指令一般都有三个操作数:两个进行运算的数和一个保存结果的数。要求每条指令有且仅有三个操作数,符合硬件简单性的设计原则:操作数个数可变将给硬件设计带来更大的复杂性。这种情况说明了硬件设计四条基本原则的第一条:
设计原则 l:简单源于规整。
下面的 2 个示例程序展示了用高级编程语言编写的程序和用汇编语言编写的程序之间的关系。
2.2.2 计算机硬件的操作数
不同于高级语言程序,算术运算指令的操作数是受限的,它们必须来自寄存器。寄存器由硬件直接构建,数量有限,是计算机硬件设计的基本元素。当计算机设计完成后,寄存器对程序员是可见的。所以也可以把寄存器想象成构造计算机“建筑”的“砖块”。在 MIPS 体系结构中寄存器大小为 32 位;由于 32 位为一组的情况经常出现,因此在 MIPS 体系结构中将其称为“字”。
高级语言的变量与寄存器的一个主要区别在于寄存器的数量有限。寄存器个数限制为 32 个的理由可以表示为硬件设计四条基本原则中的第二条:
设计原则 2:越少越快。
大量的寄存器可能会使时钟周期变长,因为需要更远的电信号传输距离。当然,该原则也不是绝对的,3 1 个寄存器不见得比 32 个更快。但表象背后的物理事实值得计算机设计者认真对待。在这种情况下,设计者必须在程序期望更多寄存器和加快时钟周期之间进行权衡。
另—个不使用多于 32 个寄存器的原因是受指令格式位数的限制。
尽管可以简单使用序号 0 到 31 表示相应的寄存器,但 MIPS 约定书写指令时,用一个“$”符后面跟两个字符来代表一个寄存器。现在,我们使用$s0 $s1……来代表与 C 和 Java 程序中的变量所对应的寄存器;用$t0, $ti……来代表将程序编译为 MIPS 指令时所需的临时寄存器。
将程序变量和寄存器对应起来是编译器的工作。以我们前面讲过的 C 赋值语句 f=(g+h)-(i+j)为例:
变量 f、g、h、i 和 j 依次分配给寄存器$s0、$s1、$s2、$s3 和$s4,编译后生成的代码与前面例中的代码非常相似:
add $t0, $sl, $s2 # $to = g + h
add $t1, $s3,$s4 # $t1= i + j
sub $s0,$t0,$t1 # $s0=$t0-$t1
一、存储器操作数
编程语言中,有像上面这些例题中仅含一个数据元素的简单变量,也有像数组或结构那样的复杂数据结构。这些复杂数据结构中的数据元素可能远多于计算机中寄存器的个数。计算机怎样来表示和访问这样的结构呢?
处理器只能将少量数据保存在寄存器中,但存储器有数十亿的数据元素。因此,复杂数据结构(如数组和结构)是存放在存储器中的。
如上所述,MIPS 的算术运算只对寄存器进行操作,因此,MIPS 必须包含在存储器和寄存器之间传送数据的指令。这些指令叫做数据传送指令。为了访问存储器中的一个字,指令必须给出存储器地址。存储器就是一个很大的下标从 0 开始的一维数组,地址就相当于数组的下标。
将数据从存储器拷贝到寄存器的数据传送指令,通常叫取数指令(load)。取数指令的格式是操作码后接着目标寄存器,再后面是用来访问存储器的常数和寄存器。常数和第二个寄存器中的值相加即得存储器地址。实际的 MIPS 取数指令助记符为 lw,为 load word 的缩写。
【例】设 A 是一个含有 100 个字的数组,像前面的例一样,编译器仍然将寄存器$s1、$s2 依次分配给变量 g、h。又设数组 A 的起始地址或称基址,存放在寄存器$s3 中。编译 C 赋值语句 g=h+A[8]
解答:虽然该 c 赋值语句只有一个操作,但其中一个操作数在存储器中,所以首先必须将 A[8]传送到寄存器。其地址是$s3 中的基址加上该元素序号 8。取回的数据应放在一个临时寄存器中以便下条指令使用。第一条编译后生成的指令为:
lw $t0,8 ($s3) #将 A[8]取到寄存器$t0
因为 A[8]已取到寄存器$t0 中,下一条指令就可对$t0 进行操作。该指令将 h(在$s2 中)加上 A[8](在$t0 中),并将结果放到对应于 g 的寄存器$s1 中:
add $s1,$s2,$t0 # g=h+A[8]
数据传送指令中的常量(本例中为 8)称为偏移量(offset),存放基址的寄存器(本例中为$s3)称为基址寄存器(base register)。
除了将变量与寄存器对应起来,编译器还在存储器中为诸如数组和结构这样的数据结构分配相应的位置。然后,编译器可以将它们在存储器中的起始地址放到数据传送指令中。
很多程序都用到字节类型,且大多数体系结构按字节编址。因此,一个字的地址必和它所包括的四个字节中某个的地址相匹配,且连续字的地址相差 4。
因为 MIPS 是按字节编址的,所以字的地址是 4 的倍数:一个字有 4 个字节。
在 MIPS 中,字的起始地址必须是 4 的倍数。这叫对齐限制。许多体系结构都有这样的限制。有两种类型的字节寻址的计算机:一种使用最左边或“大端”(Big endian)字节的地址作为字地址;另一种使用最右边或“小端“(Little endian)字节的地址作为字地址。
采用 Little-endian 模式的存放方式是从低字节到高字节,而 Big-endian 模式对操作数的存放方式是从高字节到低字节。
32 位宽的数 0x12345678 在存储器中以“小端“的存放方式(假设从地址 0x4000 开始存放)为:
存储器地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x78 0x56 0x34 0x12
而在“大端”模式存放方式则为:
存储器地址 0x4000 0x4001 0x4002 0x4003
存放内容 0x12 0x34 0x56 0x78
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在 C 语言中除了 8bit 的 char 之外,还有 16bit 的 short 型,32bit 的 long 型(要看具体的编译器),另外,对于位数大于 8 位的处理器,例如 16 位或者 32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
MIPS 采用大端模式。字节寻址也影响到数组下标。在上面的代码中,为了得到正确的字节地址,与基址寄存嚣$s3 相加的偏移量必须是 4×8,即 32,这样才能正确读到 A[8],而不会错读到 A[8/4]。
与取数指令相对应的指令通常叫做存数指令(store);它将数据从寄存器拷贝到存储器。存数指令的格式和取数指令相似:操作码,接着是包含待存储数据的寄存器,然后是选择数组元素的偏移量,最后是基址寄存器。同样,MIPS 地址由常数和基址寄存器内容共同决定。实际的 MIPS 存数指令的名字为 sw,为 store word 的缩写。
试编译 A[12]=h+A[8]的 C 赋值语句。
假设变量 h 存放在寄存器$s2 中,数组 A 的基址放在$s3 中。虽然该 C 语句只有一个操作,但是有两个操作数在存储器中,因此,需要更多的 MIPS 指令。前两条指令基本上与上个例题相同,除了本例在取数指令中选择 A[8)时使用了字节寻址中正确的偏移量,并且加法指令将结果放在临时寄存器$t0 中:
lw $t0,32 ($s3)
add $t0, $s2, $t0 #Temporary reg $to gets h+A[8]
最后一条指令使用 48 (4×12)作为偏移量,寄存器$s3 作为基址寄存器,将加法结果存放到存储器单元 A[12]中。
sw $t0,48 ( $s3) #存储到 A[12]
lw 和 sw 是 MIPS 体系结构中在存储器和寄存器之间拷贝字的指令。
许多程序的变量个数要远多于计算机的寄存器个数。因此,编译器会尽量将最常用的变量保持在寄存器中,而将其他的变量放在存储器中,方法是使用取数/存数指令在寄存器和存储器之间传送变量。
二、常数或立即数操作数
程序中经常会在某个操作中使用到常数——例如,将数组的下标加 1,用以指向下一个数组元素。实际上,在运行 SPEC2006 测试基准程序集时,有超过一半的 MIPS 算术运算指令会用到常数作为操作数。
仅从已介绍过的指令看,如果要使用常数必须先将其从存储器中取出。避免使用取数指令的另一方法是,提供其中一个操作数是常数的算术运算指令。这种有一个常数操作数的快速加法指令叫做加立即数( add immediate),或者写成 addi。这样,上述操作只需写成:
addi $s3,$s3,4 #$3=$3+4
带有立即数的指令说明了硬件设计四条原则的第三条,
设计原则 3:加速执行常用操作。
常数操作数出现频率高,而且相对于从存储器中取常数,包含常数的算术运算指令执行速度快很多,并且能耗较低。
常数 0 还有另外的作用,有效使用它可以简化指令集。例如,数据传送指令正好可以被视作一个操作数为 0 的加法。因此,MIPS 将寄存器$zero 恒置为 0(此寄存器编号也为 0)
由于 MIPS 支持负常数,所以 MIPS 中不需要设置减立即数的指令。
2.3.1 进位计数制及其转换
进位计数制—按照进位计数制的方法表示数,例如十进制计数制是“逢十进一”;而二进制则是“逢二进一”。
一、常用的进位计数制
常用的进位计数制,包括十进制、二进制、八进制和十六进制。
二、不同进位制之间数的转换
转换的依据是:任何两个有理数相等,则这两个数的整数和小数部分应分别相等。
(一)十进制数转换为二进制数
1.整数部分的转换采用“除 2 取余法”(第 1 次得到的余数是二进制整数的最低位)。
小数部分的转换采用“乘 2 取整法”(第 1 次得到的整数是二进制小数的最高位)。
数值数据主要用于计算,无符号数,就是整个机器字长的全部二进制位均表示数值位(没有符号位),相当于数的绝对值。大量用到的数据还是带符号数,即正、负数,带符号数在计算机中如何表示哪?
比较 3 种机器数,主要区别有以下几点:
(一)对于正数它们都等于真值本身,而对于负数各有不同的表示。
(二)最高位都表示符号位,补码和反码的符号位可作为数值位的一部分看待,和数值位一起参加运算;但原码的符号位不允许和数值位同等看待,必须分开进行处理。
(三)对于真值 0,原码和反码各有两种不同的表示形式,而补码只有惟一的一种表示形式。
(四)原码、反码表示的正、负数范围相对零来说是对称的;但补码负数表示范围较正数表示范围宽,能多表示一个最负的数(绝对值最大的负数),其值等于一 2n(纯整数)或—1(纯小数)
2.4 计算机中指令的表示
人操作计算机的方式与计算机看到指令的方式是不同的,指令在计算机内部是以若干或高或低的电信号的序列表示的,并且看上去和数的表示是一样的。实际上,指令的各部分都可看成一个独立的数,将这些数拼接在一起就形成了指令。
因为几乎所有的指令中都要用到寄存器,所以必须有一套规定,以使寄存器名字映射成数字。在 MIPS 汇编语言中,寄存器$s0 一$s7 映射到寄存器 16—23,同时,寄存器$t0 一$t7 映射到寄存器 8 一 15。因此,$s0 表示寄存器 16,$s1 表示寄存器 17,$s2 表示寄存器 18,以此类推;
将一条 MIPS 汇编语言指令翻译成一条机器指令。对于符号表示为 add $t0,$s1,$s2 的 MIPS 指令,首先给出其十进制数表示形式,接着给出其二进制教表示形式。
第一个字段告诉 MIPS 计算机,该指令要完成加法运算。第二个字段表示加法的第一个源操作数寄存器号(17=$s1),第三个字段表示加法的另一个源操作数寄存器号(18=$s2)。第四个字段表示存放和的目的寄存器号(8=$t0)。第五个字段在这条指令中没有用到,故置为 0。这样,这条指令将寄存器$sl 和寄存器$s2 内容相加,并将和放在寄存器$t0 中。
二进制数字段组成的指令表示形式叫做指令格式。从位的数目可以看出,MIPS 指令占 32 位,与数据字的位数相等。为遵循简单源于规整的原则,所有 MIPS 指令都是 32 位长。
为了将它与汇编语言区分开来,把指令的数字形式称为机器语言。这样的指令序列叫做机器码。
为避免读写冗长乏味的二进制字串,可采用比二进制基数更大,但又易转化为二进制的表示形式来表示。由于几乎所有的计算机的数据大小都是 4 的整数倍,因此十六进制表示形式变得很流行。
为了避免处理不同进制数时产生混淆,约定十进制数加下标 10,二进制数加下标 2,十六进制数加下标 16 ,如果没有下标,那么默认为十进制。
其中各字段名称及含义如下:
op:指令的基本操作,通常称为操作码。
rs:第一个源操作数寄存器。
rt:第二个源操作数寄存器。
rd:用于存放操作结果的目的寄存器。
shamt:位移量。
funct:功能。一般称为功能码(function code),用于指明 op 字段中操作的特定变式。
当某条指令需要比上述字段更长的字段时,问题就会发生。例如,取字指令必须指定两个寄存器和一个常数。在上述格式中,如果地址使用其中的一个 5 位字段,那么取字指令的常数就被限制在 25(即 32)之内。这个常数通常用来从数组或数据结构中取元素,所以它常常比 32 大得多,5 位字段因太小而用处不大。
因此,既希望所有指令长度相同,又希望具有统一的指令格式,两者之间产生了冲突。这就引出了最后一条硬件设计原则。
设计原则 4:优秀的设计需要适宜的折中方案。
MIPS 设计者选择这样一种折中方案:保持所有的指令长度相同,但不同类型的指令采用不同的指令格式。例如,上述格式称为 R 型(用于寄存器)。另一种指令格式称为 I 型(用于立即数),立即效和数据传送指令用的就是这种格式。I 型的字段如下所示:
16 位地址字段意味着取字指令可以取相对于基址寄存器地址偏移±215 或者 32768 个字节(±213 或者 8192 个字)范围内的任意数据字。类似地,加立即数指令中常数也被限制不超过±215。
虽然多种指令格式使硬件变得复杂,但是保持指令格式的类似性仍可降低复杂度。例如,R 型和 I 型格式的前 3 个字段长度相等,并且名称也一样;I 型格式的第四个字段和 R 型后三个字段长度之和相等。
也许你会想到,指令格式可以由第一个字段的值来区分:每种格式在第一个字段(op)占有不同的值区间,以便让计算机硬件知道指令后半部分是三字段(R 型)还是一字段(I 型)。下面给出了到目前为止已使用过的 MIPS 指令的每个字段的值。
在上表中,“reg”代表寄存器的标号(从 0 到 31)。“address”表示 16 位地址,“n.a”(not applicable)表示这个字段在该指令格式中不出现。注意 add 和 sub 指令具有相同的 op 字段值,硬件根据 funct 字段的值来决定所进行的操作:add (32)或 sub (34)。
【例】将 MIPS 汇编语言翻译成机器语言。现在可以给出一个例子来描述从程序员所编程序到机器执行指令的整个转换过程。如果数组 A 的基址存放在$tl 中,h 存放在$s2 中,下面的 C 赋值语句:
A[300] =h +A[300] ;
解:被编译成如下汇编语言:
lw $t0,1200( $tl) #Temporary reg $to gets A[300]
add $t0, $s2, $t0 # Temporary reg $t0 gets h+A[300)
sw $t0,1200( $tl) # Stores h+A[300] back into A[300]
lw 指令的第一个字段(op)值为 35,在第二个字段(rs)中指定基址寄存器 9($tl),在第三个字段( rt)中指定目的寄存器 8($t0,在最后一个字段 address 中存放用于指定 A[300]的偏移量(1200=300×4)。
add 指令由第一个字段(op)值 0 和最后一个字段(funct)值 32 共同确定。第二、三、四字段中的三个寄存器(18,8 和 8)分别对应$s2、$t0 和$t0。
sw 指令由第一个字段的 43 识别。这条指令的其他部分和 lw 指令完全一样,与上述十进制形式对应的二进制机器指令如下所示(十进制数 1 200 用二进制表示为 0000 0100 1011 0000) ;
注意,第一条指令和最后一条指令的二进制表示非常相似,唯一不同的是从左边数第 3 位。
相关指令在二进制表示上的相似性可简化硬件设计。
2.5.1 逻辑操作
虽然早期的计算机仅对整字进行操作,但人们很快就发现,对字中由若干位组成的字段甚至对单个位进行操作是很有用的。于是,编程语言和指令集体系结构中增加了一些指令,用于简化对字中若干位进行打包或者拆包的操作。这些指令被称为逻辑操作。
第一类逻辑操作称为移位操作。它们将一个字里面的所有位都向左或右向移动,并在空出来的位上填充 0。与左移相对应的是右移。左移和右移这两条指令在 MIPS 中的确切名字是逻辑左移(sll)(shift left logical)和逻辑右移(srl)(shift right logical)。下面的指令完成的就是上述操作,假设源操作数在$s0 中,结果存储到$t2 中:
sll $t2, $s0,4 # $t2 =reg $s0<<4 bits
前面介绍 R 型指令格式时没有解释 shamt 字段,它在移位指令中被用于表示移位量(shift amount)。因此,上述指令对应的机器语言是:
第二类有用的操作是按位与(AND)。该操作仅当两个操作位均为 1 时结果才为 1。 AND 提供了一种将源操作数中某些位置为 0 的能力,前提是另一个操作数中对应位为 0,后一个操作数传统上被称为掩码(mask),寓意其可“隐藏”某些位。
与 AND 对偶的操作是按位或(OR)。该操作在两个操作位中任意一位为 1 时结果就为 1 。
最后一类逻辑操作是按位取反(NOT)。该操作仅有一个操作数,将 1 变成 0,0 变成 1。
为了保持三操作数的格式,MIPS 的设计者引入或非 NOR (NOT OR)指令来取代 NOT。
像在算术运算中一样,常数在 AND 和 OR 这些逻辑运算里也是很有用的,因此 MIPS 也提供了立即数与(andi)和立即数或(ori)指令。常数在 NOR 中出现的很少,因为 NOR 主要功能就是将单操作数按位取反,因此,MIPS 指令集体系结构没有设计支持 NOR 立即数的版本。
MIPS 指令全集也包括异或(XOR)。当 2 个操作数对应位不同时置 l,相同时置 0。
计算机与简单计算器的区别在于决策能力。根据输入数据和计算过程中产生的值,它可以执行不同的指令。
一、if- then-else 语句
程序语言通常使用 if 语句描述决策,有时也使用 go to 语句和标签。 MIPS 汇编语言中有 2 条类似 if 和 go to 语句功能的指令。第一条是
beq register1,register2 ,L1
该指令表示:如果 register1 和 register2 中的数值相等,则转到标签为 L1 的语句执行。助记符 beq 代表如果相等则分支(branch if equal)。第二条指令是
bne register1,register2 ,L1
该指令表示:如果 register1 和 register2 中的数值不相等,则转到标签为 L1 的语句执行。助记符 bne 代表如果不相等则分支(branch if not equal)。
有时候另一种更有效的方法是将多个指令序列分支的地址编码为一张表,即转移地址表格,这样程序只需索引该表即可跳转到恰当的指令序列。转移地址表是一个由代码中标签所对应地址构成的数组。程序需要跳转的时候首先将转移地址表中适当的项加载到寄存器中,然后使用寄存器中的地址值进行跳转。为了支持这种情况,MIPS 计算机提供了寄存器跳转指令 jr(jump register),用来无条件地跳转到寄存器指定的地址。
2.6 计算机硬件对过程的支持
过程或函数是程序员进行结构化编程的工具,两者均有助于提高程序的可读性和代码的可重用性。过程允许程序员每次只需将精力集中在任务的一部分,参数担任过程与其他程序、数据之间接口的角色,因为它们能传递数值并返回结果。过程(procedure)根据提供的参数执行一定任务的存储的子程序。
你可以将过程想象成一个侦探,他离开时带着一项神秘的计划,为了完成该计划,需要获得资源、执行任务并隐匿行踪,最后带着预期的结果返回起点。一旦任务完成将不再对系统产生任何其他干扰。
在过程运行期间,程序必须遵循以下六个步骤:
1)将参数放在过程可以访问到的位置。
2)将控制转移给过程。
3)获得过程所需的存储资源。
4)执行请求的任务。
5)将结果的值放在调用程序可以访问到的位置。
6)将控制返回初始点,因为一个过程可能由一个程序中的多个点调用。
如上所述,寄存器是计算机中保存数据最快的位置,所以我们希望尽可能多地使用寄存器。MIPS 软件在为过程调用分配 32 个寄存器时遵循以下约定:
•$a0 一$a3:用于传递参数的四个参数寄存器。
•$v0 一$v1:用于返回值的两个值寄存器。
•$ra:用于返回起始点的返回地址寄存器。
除了分配这些寄存器之外,MIPS 汇编语言还包括一条过程调用指令:跳转到某个地址的同时将下一条指令的地址保存在寄存器$ra 中。这条跳转和链接指令。格式为:
jal ProcedureAddress
指令中的链接部分表示指向调用点的地址或链接,以允许过程返同到合适的地址。存储在寄存器$ra(31 号寄存器)中的链接部分称为返回地址。返回地址是必需的,因为同一过程可能在程序的不同部分调用。返回地址:指向调用点的链接,使过程可以返回到合适的地址,在 MIPS 中它存储在寄存器$ra 中。
为了支持这种情况,类似 MIPS 的计算机使用了寄存器跳转(jump register)指令 jr,用于 case 语句,表示无条件跳转到寄存器所指定的地址:
jr $ra
寄存器跳转指令跳转到存储在$ ra 寄存器中的地址——这正是我们所希望的。因此,调用程序或称为调用者,将参数值放在$a0 一$a3 然后使用 jal x 跳转到过程 x(有时称为被调用者)。被调用者执行运算,将结果放在$v0 和$vl,然后使用 jr $ra 指令将控制返回给调用者。
在存储程序概念中,使用一个寄存器来保存当前运行的指令地址是绝对必要的。尽管这个寄存器更为合理的名字可能应该是指令地址寄存器(instruction address register),但是出于历史原因,这个寄存器通常称为程序计数器,在 MIPS 体系结构中缩写为 PC。 jal 指令实际上将 PC+4 保存在寄存器$ra 中,从而将链接指向下一条指令,为过程返回做好准备。
一、过程调用
假设对于一个过程,编译器需要使用多于四个参数寄存器和两个返回值寄存器。由于在任务完成后必须消除踪迹,调用者使用的任何寄存器都必须恢复到过程调用前所存储的值。这种情况可以看成是需要将寄存器换出到存储器的一个例子。
换出寄存器的最理想的数据结构是栈一种后进先出的队列。栈需要一个指针指向栈中最新分配的地址,以指示下一个过程放置换出寄存器的位置,或是寄存器旧值的存放位置。栈指针按照每个被保存或恢复的寄存器以字为单位进行调整。 MIPS 软件为栈指针保留了第 29 号寄存器,并将其命名为$sp 由于栈的应用十分广泛,因此向栈传递数据或从栈中取数都有专用术语:将数据放人栈中称为压栈,从栈中移除数据称为出栈。
按照历史惯例,栈“增长”是按照地址从高到低的顺序进行的。这意味着将值压栈时,栈指针值减小;而值出栈时,栈长度缩短,栈指针增大。
【例】编译一个不调用其他过程的 C 过程(无嵌套调有)
int leaf_ example (int g,int h,int i,int j )
{int f;
f=(g+h) -(i+j);
reture f;
}
编译后的 MIPS 汇编代码是什么呢?
解:参数变量 g、h、i 和 j 对应参数寄存器$a0、$a1、$a2 和$a3,f 对应$s0。编译后的程序是以如下标号开始的过程:
leaf_ example:
下一步是保存过程中使用的寄存器。过程实体中的 C 赋值语句与前面例子相同,使用了两个临时寄存器。因此,需要保存三个寄存器:$s0、$t0 和$t1。我们将旧值“压栈”,也就是在栈中建立三个字的空间并将数据存入:
leaf_example:
addi $sp, $sp, -12 #调整栈指针,准备存入 3 个数
sw $t1, 8($sp)
sw $t0,4($sp)
sw $s0, 0($sp)
add $t0, $a0, $a1 #过程实体,计算 f=(g+h) -(i+j)
add $t1, $a2, $a3
sub $s0, $t0, $t1
add $v0, $s0, $zero #将计算结果传送的返回值寄存器
lw $s0, 0($sp) #恢复三个数据
lw $t0, 4($sp)
lw $t1, 8($sp
addi $sp, $sp, 12
jr $ra #返回
图 2.6 过程调用栈变化图
$t0–$t9 是临时寄存器,过程调用不必保存;
$s0–$s7 是保留寄存器,过程调用必须保存。
二、嵌套过程
不调用其他过程的过程称为叶过程(leaf procedure)。如果所有过程都是叶过程,那么情况就很简单,但实际并非如此。就像一个侦探其任务的一部分是雇佣其他侦探一样,被雇佣的侦探进而雇佣更多的侦探,某个过程调用其他过程也是这样。更进一步的是,递归过程甚至调用的是自身的“克隆”。
就像在过程中使用寄存器需要十分小心一样,在调用非叶过程时需要更加小心。
例如,假设主程序将参数 3 存人寄存器$a0,然后使用 jal A 调用过程 A。再假设过程 A 通过 jal B 调用过程 B,参数为 7,同样存入$a0。由于 A 尚未结束任务,所以在寄存器$a0 的使用上存在冲突。同样地,在寄存器$ra 保存的返回地址上也存在冲突,因为它现在保存着 B 的返回地址。除非我们采取措施阻止这类问题发生,否则这个冲突将导致过程 A 无法返回其调用者。
一个解决方法是将其他所有必须保存的寄存器压栈,就像将保留寄存器压栈一样。调用者将所有调用后还需要的参数寄存器($a0 一$a3)或临时寄存器($t0~$t9)压栈。被调用者将返回地址寄存器$ra 和被调用者使用的保留寄存器($s0~$s7)都压栈。栈指针$sp 随着栈中寄存器个数调整。到返回时,寄存器会从存储器中恢复,栈指针也随着重新调整。
2.7 人机交互
计算机发明是为了数字计算,不过很快被用于商业方面的文字处理。今天大多数计算机使用 8 位的字节来表示字符,也就是几乎每个人都遵循的 ASCII(American Standard Code for Information Interchange)码。
二进制对人类来说不是自然计数方法的,我们有 10 个手指头,所以我们自然的采用十进制数。
为什么计算机不使用十进制呢?事实上,第一台商用计算机确实提供了十进制算术。问题在于计算机仍然采用开关信号,所以一个十进制数将由几个二进制数来表示。十进制被证明效率很低,所以后来的计算机都转向了二进制,只有在相对很少发生的 I/O 事件中才将数据转换成十进制。
我们可以使用一串 ASCII 码而不用整数来表示数字。如果用 ASCII 码表示 10 亿这个数将比用 32 位整数表示增加多少存储呢?
10 亿就是 l 000 000 000,需要使用 10 位 ASCII 码表示,每一个 ASCII 码都是 8 位长。所以存储将增长到(10×8)/32 即 2.5 倍。除了存储空间要增加外,用于对这些十进制数字进行加法、减法、乘法和除法的硬件的设计也是困难的。这些困难解释了为什么计算专家越来越相信使用二进制的计算机是自然的,而偶然出现的十进制计算机则是奇怪的。
图 2.7 字符的 ASCII 表示
注意所有大写字母和对应小写字母的差均为 32。这个观测结果可以得到一条检查和切换大小写的捷径。没有给出的 ASCII 值包括格式化字符。例如 8 代表退格,9 代表 tab 字符,而 13 代表回车。另外一个有用的值 0 表示 null。C 编程语言用这个来标记字符串的结尾。这些内容也可以在本书文前的 M[PS 参考数据的第 3 列中找到。
可以使用一一系列指令从一个字中提取出一个字节,所以字的读取和存储同样可以完成对字节的传输。然而,由于在某些程序中对文本的操作十分普遍,所以 MIPS 还提供字节传输指令。
字节读取 lb (load byte)指令从内存中读出一个字节,并将其放在一个寄存器最右边的 8 位。字节存储 sb(store byte)指令把一个寄存器最右边的 8 位取出来然后写到内存中。这样,我们可以按下面的顺序复制一个字节:
lb $t0 ,0($sp) # Read byte from source
sb $t0,0($gp) # write byte to destination
和算术运算一样,对取数指令来说有符号数和无符号数是有区别的。取回有符号数后需要使用符号位填充寄存器的所有剩余位,称为符号扩展,但其目的是在寄存器中放入数字正确的表示方式。取回无符号数只是简单地用 0 来填充数据左侧的剩余位,因为这种表示形式的数是没有符号的。
当把 32 位的字加载到 32 位的寄存器中,上面的讨论是没有意义的,因为无符号数和有符号数的加载是完全一样。MIPS 提供了两种字节加载的方法:一种是用于字节加栽的 lb (load byte)。 lb 将字节看做有符号数,使用符号扩展来填充寄存器的左侧 24 位;另一种是用于无符号整数加载的 lbu(load byte unsigned)。由于 C 程序几乎都是使用字节来表示字符,很少用来表示有符号短整数(short signed integers),所以实际中几乎所有的字节加载都是使用 lbu。
字符通常被组合为字符数目可变的字符串。表示一个字符串的方式有三种选择:(I)保留字符串的第一个位置用于给出字符串的长度;(2)附加一个带有字符串长度的变量(如在结构体中);(3)字符串最后的位置用一个字符来标识其结尾。 C 语言使用第三种选择,用一个值为 0(ASCII 码中的 null)的字节来结束字符串。所以,字符串“Cal”在 C 中用 4 个字节表示,用十进制表示分别为:67、97、108、0。
2.8 MIPS 中 32 位立即数和地址的寻址
虽然保持所有 MIPS 指令为 32 位长简化了硬件,但有时使用 32 位常量或 32 位地址更加方便。
本节先介绍使用较大常量的一般解决方法,然后描述了用于分支和跳转指令寻址的优化措施。
一、32 位立即数
尽管常数往往比较短而且适于 16 位字段,但有时它们会更大。MIPS 指令集中的读取立即数高位指令 lui (load upper immediate)专门用于设置寄存器中常数的高 16 位,允许后续指令设置常数的低 16 位。
加载下面这个 32 位常量到寄存器$so 的 MIPS 汇编代码是什么?
首先,我们使用命令 lui 加载高 16 位,十进制表示是 61:
lui $s0,61 # 61 decimal= 0000 0000 0011 1101 binary
执行上面的指令后,寄存器$s0 的值为 0000 0000 0011 1101 0000 0000 0000 0000
下一步是插入低 16 位,十进制表示是 2304:
ori $s0,$s0,2304 #2034=0000 1001 0000 0000
寄存器$s0 中的最终值就是所需要的值:
0000 0000 0011 1101 0000 1001 0000 0000
编译器或汇编程序必须把大的常数分解为若干小的常数然后再合并到一个寄存器中。正如你想象的那样,立即数字段大小的限制,无论在取/存数指令中对存储器的地址还是在立即数指令中对常数都可能带来问题。如果这项工作由汇编程序来做,如 MIPS 软件,那么汇编程序必须有一个可用的临时寄存器来创建长整数值。这是给汇编程序保留$at 寄存器的一个原因。
因此,MIPS 机器语言的符号表示不再受到硬件限制,但仍受汇编程序构造者选择包括的内容所限。我们以靠近硬件层的方式解释计算机的体系结构,需要注意的是,我们所使用汇编程序的增强扩展语言,在实际处理器中是不存在的。
二、分支和跳转中的寻址
MIPS 跳转指令寻址采用最简单的寻址方式。它们使用最后一种 MIPS 指令格式,称为 j 型。j 型除了 6 位操作码之外,其余位都是地址字段。所以,可以汇编为下面的格式(实际中要更加复杂一些,正如我们后面将看到的那样:
其中跳转操作码的值为 2,跳转地址为 100000 和跳转指令不同,条件分支措令除了规定分支地址之外还必须指定两个操作数。因此
bne $s0,$s1,Exit #go to Exit if $s0 $s1
被汇编为下面的指令,只保留了 16 位用于指定分支地址:
如果让程序地址适应该 16 位字段,则意味着任何程序都不能大于 216,这在今天来说太小,因此是一种很不现实的选择。另一个可选的办法是指定一个总是加到分支地址上的寄存器,这样分支指令可能按如下方式计算:
程序计数器=寄存器+分支地址
这个求和结果允许程序的大小达到 232。并且仍能使用条件分支,从而解决了分支地址大小的问题。随之而来的问题是使用哪个寄存器呢?
答案取决于条件分支是如何使用的。条件分支在循环和 if 语句中都可以找到,它们倾向于转到附近的指令。例如,在 SPEC 基准测试程序中,大概一半条件分支的跳转距离小于 16 条指令。因为程序计数器(program counter,PC)包含当前指令的地址,如果我们使用 PC 来作为增加地址的寄存器,我们可转移到离当前指令距离为±215 个字的地方。几乎所有循环和 if 语句都远远小于 216 个字,因此 PC 是一个理想的选择。
这种分支寻址形式称为 PC 相对寻址。正如在第 4 章中将会看到的那样,提前递增 PC 来指向下一条指令会对硬件带来很多方便。所以,MIPS 寻找实际上是相对于下一条指令的地址(PC+4),而不是相对于当前指令(PC)
像近期的大多数计算机一样,MIPS 对所有条件分支使用 PC 相对寻址,因为这些指令的跳转目标很可能接靠近其分支地址。另一方面,跳转链接指令并非总是靠近调用者的过程,所以它们通常使用其他寻址方式。因此,MIPS 体系结构通过使用跳转和跳转链接指令的 J 型格式来为过程调用提供长地址。
既然所有 MIPS 指令都是 4 字节长,所以在 PC 相对寻址时所加的地址被设计为字地址而不是字节地址。相对于 16 位的字节地址,16 位的字地址跳转范围扩大了 4 倍。同样地,跳转指令的 26 位字段也是字地址,它可以表示 28 位的字节地址 a
因为 PC 是 32 位,所以有 4 位必须来自于跳转指令之外的其他地方。 MIPS 跳转指令仅仅代替 PC 的低 28 位,而高 4 位保持改变。装载器和链接器必须十分小心以避免程序超过 256 MB 的寻址界限;否则,该跳转必须替换为寄存器跳转指令,并在执行前使用其他指令将完整的 32 位地址加载到一个寄存器中。
大多数条件分支都转移到一个附近的位置,但有时也会转移很远,距离超过条件分支指令的 16 位可以表示的范围。汇编器的解决方法就像处理对大地址或大常数的方法一样:插入一个跳转到分支目标的无条件跳转,并将条件取反以便由分支决定是否跳过该无条件跳转指令。
假设在寄存器$s0 与寄存器$s1 值相等时需要跳转,可以使用如下指令:
beq $s0,$sl,L1
用两条指令替换上面的指令,以获得更远的转移距离。
可用下面的指令替换短地址的条件分支措令:
bne $s0,$s1,L2
j L1
L2 :
三、MIPS 寻址模式总结
多种不同的寻址形式一般统称为寻址模式。图 2.8 给出了每种寻址模式的操作数如何识别。
MIPS 寻址模式如下所示:
1)立即数寻址(immediate addressing),操作数是位于指令自身中的常数。
2)寄存器寻址(register addressing),操作数是寄存器。
3)基址或偏移寻址(base or displacement addressing;),操作数在内存中,其地址是指令中基址寄存器和常数的和。
4)PC 相对寻址(PC-relative addressing),地址是 PC 和指令中常数的和。
5)伪直接寻址(pseudodirect addressing),跳转地址是指令中 26 位字段和 PC 高位相连而成。
图 2.8 MIPS 5 种寻址模式
一台计算机中所有机器指令的集合,称为这台计算机的指令系统。指令系统是表征一台计算机性能的重要因素,它的格式与功能不仅直接影响到机器的硬件结构,而且也影响到系统软件。
指令格式是指令字用二进制代码表示的结构形式,通常由操作码字段和地址码字段组成。操作码字段表征指令的操作特性与功能,而地址码字段指示操作数的地址。MIPS 采用三地址 32 位长度的单字长形式。
形成指令地址的方式,称为指令寻址方式。有顺序寻址和跳跃寻址两种,由指令计数器来跟踪。
形成操作数地址的方式,称为数据寻址方式。操作数可放在专用寄存器、通用寄存器、内存和指令中。数据寻址方式有隐含寻址、立即寻址、直接寻址、间接寻址、寄存器寻址、寄存器间接寻址、相对寻址、基值寻址、变址寻址、块寻址、段寻址等多种。
MIPS 具有 5 种寻址方式。
RISC 指令系统是目前计算机发展的主流,也是 CISC 指令系统的改进,它的最大特点是:① 指令条数少;② 指令长度固定,指令格式和寻址方式种类少;③ 只有取数/存数指令访问存储器,其余指令的操作均在寄存器之间进行。
存储程序计算机的两个准删是指令的使用与数字没有区别,以及使用可修改的存储器。这些准则使一台计算机可以在不同的领域辅助环境科学家、经济顾问和小说家。选择机器可以理解的指令集需要精妙的平衡程序执行需要的指令数目、指令执行所需的时钟周期数和时钟的速度。在做精妙平衡时有四条准则可以指导设计者:
1)简单源于规整。规整性使 MIPS 指令集具有很多特点:所有指令长度统一、算术指令总是需要三个寄存器操作数和寄存器字段在每种指令格式的位置相同。
2)越小越快。对速度的要求导致 MIPS 只有 32 个寄存器而不是更多。
3)加速常用操作。MIPS 加速常用操作的例子包括条件分支中 PC 相对寻址和为大的常数操作数使用立即数寻址。
4)优秀的设计需要好的折中。一个 MIPS 例子是在指令中提供更大地址与常数,并且保持所有的指令具有相同的长度之间的折中。