《See MIPS Run》–第二章 MIPS体系结构

Sina WeiboBaiduLinkedInQQGoogle+RedditEvernote分享




第二章 MIPS体系结构

在计算世界中, “体系结构”一词被用来描述一个抽象的机器,而不是一个具体的机器实现. 这一点非常有用的, 用来区分在市场广告上已经被滥用的”体系结构”这个术语. 读者有可能不熟悉”抽象描述”,但其概念其实很简单.

当然,如果你是一个喜欢在 滑的路上开快车的司机,前轮还是后轮驱动就很有所谓了。计算机也是如此。如果你需要高性能计算,一个计算机的具体参数与实现对你就很重要了。

一般而言,一个CPU的体系结构有一个指令集加上一些寄存器而组成。“指令集”与”体系结构“这两个术语是同义词。你经常会看见ISA(指令集体系结构–ISA)的缩写。

MIPS体系结构家族包含如下几代。每一代之间都有一些区别。

MIPS 1:32位处理器使用的指令集。仍然被广泛使用着。
MIPS II:为R6000机器所定义的,包含了一些细微的改进。后来实现在1995年的32位MIPS实现中。
MIPS III:R4xxx的64位指令集。
MIPC IV: MIPS III的一个细微的升级。定义在R10000和R5000中。

上述的MIPS体系结构等级与MIPS公司提供的文档中定义的是一致的。这些文档提供了足够的信息,以使得同一个UNIX应用程序可以在不同的MIPS体系结构等级上运行,但是在操作系统或底层相关的代码方面的移植方面,显得不足。MIPS CPU其他一些软件可见的方面都是于具体CPU实现相关的。

在本书中,我将更加慷慨大方些。有时候,我会描述一个在MIPS 体系结构手册中找不到的,但却在所有MIPS III 体系结构实现中能发现的并且你会遇到的功能。

另外,除了ISA等级,大多数MIPS CPU在实现方法上分为两大类:早期的MIRS R3000和其他所有的32位MIPS CPU;另外就是已MIPS R4000为代表的64位CPU。

有不少MIPS CPU的实现加入了一些新指令和其他一些有趣的功能。对於软件或工具( 如编译器)而言,要利用这些非标准的,依赖于具体实现的功能是不容易的。

我们可以在两种细节层次上来描述MIPS的体系结构。第一种描述(本章)是在汇编语言的层次上看待你的程序,比如,你在工作站上写一个应用程序。这也意味着CPU的所有一般的计算是可见的。

在下面章节里,我们将介绍MIPS的各个方面,包括建构在CPU之上的操作系统所掩盖的所有CPU的细节,CPU控制寄存器,中断,陷入,高速缓冲操作和内存管理。至少我们会将一个CPU分成一些小部分来学习和介绍。

2.1 MIPS汇编语言的特点

汇编语言是CPU二进制指令的可读写版本。我们在后面将有单独的一章来讲述汇编语言。从来没有接触过汇编语言的读者在阅读本书时可能会有一些迷惑 。

大多数MIPS汇编语言都是非常古板的,都是一些寄存器号码。但是工具链(toolchains)可以使得使用微处理机语言变得简单。工具链至少允许程序员引用一些助记符,而严格的汇编语言要求严格的数字编码。大多我们都是用比较熟悉的C预处理器。C预处理器会把C风格的注解去掉,而得到一个可用的汇编代码。

有C预处理器的帮助,MIPS汇编程序都是用助记符来表示寄存器。助记符同时也代表了每个寄存器的用法(我们将在2.2节介绍这一点)

对於熟悉汇编语言但不熟悉MIPS的读者,下面是一些例子。

/* this is a comment */
#so is this

entrypoint: #this’s a label

addu $1, $2, $3 # (registers) $1 = $2 + $3

与大多数汇编语言一样, MIPS汇编语言也是以行为单位的。每一行的结束是一个指令的结束,并且忽略任何“#”之后的内容,认为是注释。在一行里可以有多条指令。指令之间要用分号“;”隔开。

一个符号(label)是一个后面跟着冒号“:”的字。符号可以是任何字符串的组合。符号被用来定义一段代码的入口和定义数据段的一个存储位置。

如上所示,许多指令都是3个操作数/符(operand)。目标寄存器在左侧(注意,这一点与Inetel x86 正相反)。一般而言,寄存器结果和操作符的顺序与C语言或其他符号语言的方式是一致的。 例如:

subc $1, $2, $3

意味着:

$1 = $2 – $3;

这方面我们就先讲这么多。

2.2 寄存器

对於一个程序,可以有32个通用寄存器,分别为:$0-$31。其中,两个,也只有两个的使用不同于其他。

$0:不管你存放什么值,其返回值永远是零。

$31:永远存放着正常函数调用指令(jal)的返回地址。请注意call-by-registe的jalr指令可以使用任何寄存器来存放其返回地址。当然,如不用$31,看起来程序会有点古怪。

其他方面,所有的寄存器都是一样的。可以被用在任何一个指令中(你也可以用$0作为一个指令的目标寄存器。当然不管你存入什么数据,数据都消失了。)

MIPS体系结构下,程序计数器不是一个寄存器,其实你最好不要去那样想。在一个具有流水线的CPU中,程序计数器的值在一个给定的时刻有多个可选值。这一点有点迷惑人。jal指令的返回地址跟随其后的第二条指令。


jal printf
move $4, $6
xxx # return here after call

上述的解释是有道理的,因为紧跟踪jal指令后面的指令,由於在delay slot(延迟位置)上–请记住,关于延迟位置的规则是该指令将在转移目标(如上述的printf)之前执行。延迟位置指令经常被用来传递函数调用的参数。

MIPS里没有状态码。CPU状态寄存器或内部都不包含任何用户程序计算的结果状态信息。

hi和lo是与乘法运算器相关的两个寄存器大小的用来存放结果的地方。它们并不是通用寄存器,除了用在乘除法之外,也不能有做其他用途。但是,MIPS里定义了一些指令可以往hi和lo里存入任何值。想一想我们会发现,这是非常有必要的当你想要恢复一个被打断的程序时。

浮点运算协处理器(浮点加速器,FPA),如果存在的话,有32个浮点寄存器。按汇编语言的简单约定讲,是从$f0到$31。

实际上,对於MIPS I和MIPS II的机器,只有16个偶数号的寄存器可以用来做数学计算。当然,它们可以既用来做单精度(32位)和双精度(64位)。当你做一个双精度的运算时,寄存器$f1存放$f0的余数。奇数号的寄存器只用来作为寄存器与FPA之间的数据传送。

MIPS III CPU有32个FP寄存器。但是为了保持软件与过去的兼容性,最好不要用奇数号的寄存器。

2.2.1 助记符与通用寄存器的用法

我们已经描述了一些体系结构方面的内容,下面来介绍一些软件方面的内容。

寄存器编号 助记符 用法
0 zero 永远返回值为0
1 at 用做汇编器的暂时变量
2-3 v0, v1 子函数调用返回结果
4-7 a0-a3 子函数调用的参数
8-15 t0-t7 暂时变量,子函数使用时不需要保存与恢复
24-25 t8-t9
16-25 s0-s7 子函数寄存器变量。子函数必须保存和恢复使用过的变量在函数返回之前,从而调用函数知道这些寄存器的值没有变化。
26,27 k0,k1 通常被中断或异常处理程序使用作为保存一些系统参数
28 gp 全局指针。一些运行系统维护这个指针来更方便的存取“static“和”extern”变量。
29 sp 堆栈指针
30 s8/fp 第9个寄存器变量。子函数可以用来做桢指针
31 ra 子函数的返回地址

虽然硬件没有强制性的指定寄存器使用规则,在实际使用中,这些寄存器的用法都遵循一系列约定。这些约定与硬件确实无关,但如果你想使用别人的代码,编译器和操作系统,你最好是遵循这些约定。

寄存器约定用法引人了一系列的寄存器约定名。在使用寄存器的时候,要尽量用这些约定名或助记符,而不直接引用寄存器编号。

1996年左右,SGI开始在其提供的编译器中使用新的寄存器约定。这种新约定可以用来建立使用32位地址或64位地址的程序,分别叫 “n32″和”n64″。我们暂时不讨论这些,将会在第10章详细讨论。

寄存器名约定与使用

*at: 这个寄存器被汇编的一些合成指令使用。如果你要显示的使用这个寄存器(比如在异常处理程序中保存和恢复寄存器),有一个汇编directive可被用来禁止汇编器在directive之后再使用at寄存器(但是汇编的一些宏指令将因此不能再可用)。

*v0, v1: 用来存放一个子程序(函数)的非浮点运算的结果或返回值。如果这两个寄存器不够存放需要返回的值,编译器将会通过内存来完成。详细细节可见10.1节。

*a0-a3: 用来传递子函数调用时前4个非浮点参数。在有些情况下,这是不对的。请参阅10.1细节。

* t0-t9: 依照约定,一个子函数可以不用保存并随便的使用这些寄存器。在作表达式计算时,这些寄存器是非常好的暂时变量。编译器/程序员必须注意的是,当调用一个子函数时,这些寄存器中的值有可能被子函数破坏掉。

*s0-s8: 依照约定,子函数必须保证当函数返回时这些寄存器的内容必须恢复到函数调用以前的值,或者在子函数里不用这些寄存器或把它们保存在堆栈上并在函数退出时恢复。这种约定使得这些寄存器非常适合作为寄存器变量或存放一些在函数调用期间必须保存原来值。

* k0, k1: 被OS的异常或中断处理程序使用。被使用后将不会恢复原来的值。因此它们很少在别的地方被使用。

* gp: 如果存在一个全局指针,它将指向运行时决定的,你的静态数据(static data)区域的一个位置。这意味着,利用gp作基指针,在gp指针32K左右的数据存取,系统只需要一条指令就可完成。如果没有全局指针,存取一个静态数据区域的值需要两条指令:一条是获取有编译器和loader决定好的32位的地址常量。另外一条是对数据的真正存取。为了使用gp, 编译器在编译时刻必须知道一个数据是否在gp的64K范围之内。通常这是不可能的,只能靠猜测。一般的做法是把small global data (小的全局数据)放在gp覆盖的范围内(比如一个变量是8字节或更小),并且让linker报警如果小的全局数据仍然太大从而超过gp作为一个基指针所能存取的范围。

并不是所有的编译和运行系统支持gp的使用。

*sp: 堆栈指针的上下需要显示的通过指令来实现。因此MIPS通常只在子函数进入和退出的时刻才调整堆栈的指针。这通过被调用的子函数来实现。sp通常被调整到这个被调用的子函数需要的堆栈的最低的地方,从而编译器可以通过相对於sp的偏移量来存取堆栈上的堆栈变量。详细可参阅10.1节堆栈使用。

* fp: fp的另外的约定名是s8。如果子函数想要在运行时动态扩展堆栈大小,fp作为桢指针可以被子函数用来记录堆栈的情况。一些编程语言显示的支持这一点。汇编编程员经常会利用fp的这个用法。C语言的库函数alloca()就是利用了fp来动态调
整堆栈的。

如果堆栈的底部在编译时刻不能被决定,你就不能通过sp来存取堆栈变量,因此fp被初始化为一个相对与该函数堆栈的一个常量的位置。这种用法对其他函数是不可见的。

* ra: 当调用任何一个子函数时,返回地址存放在ra寄存器中,因此通常一个子程序的最后一个指令是jr ra.

子函数如果还要调用其他的子函数,必须保存ra的值,通常通过堆栈。

对於浮点寄存器的用法,也有一个相应的标准的约定。我们将在7.5节。在这里,我们已经介绍了MIPS引入的寄存器的用法约定。最近在约定方面有一些演化,我们将在10.8节中介绍这些变化,比如调用约定的一些新标准。

2.3 整数乘法部件与寄存器

MIPS 体系结构认为整数乘法部件非常重要,需要一个单独的硬件指令。这一点在RISC芯片里不多见。一个另外做法是通过标准的整数运算流水线部件来实现一个乘法。这意味着对於每个乘法指令,需要一段软件过程(来模拟一个乘法指令)。早期的Spacr CPU就是这样做的。

另外一个用来避免设计一个整数乘法器的做法是通过浮点运算器来实现乘法。Motorola的88000 CPU家族就是提供了这样的解决方案。这样的缺点是损失了MIPS浮点运算器是用来做浮点运算的设计初衷。

早期的MIPS乘法运算器不是特别快。它的基本功能是将两个寄存器大小的值做一个乘法并将两个寄存器大小的结果存放在乘法部件里。mfhi, mflo指令用来将结果的两部分分别放入指定的通用寄存器里。

与整数运算结果不一样的是,乘法结果寄存器是互锁的(inter-locked)。试图在乘法结束之前对结果寄存器的读操作将被暂停直到乘法运算结束。

整数乘法器也可以执行两个通用寄存器的除法操作。lo寄存器用来存放结果(商),hi寄存器用来存放余数。

MIPS CPU的整数乘法部件操作相对而言比较慢:乘法需要5-12个时钟周期,除法需要35-80个时钟周期(与具体CPU的实现有关,如操作数的大小)。相对一个同样的双精度浮点运算操作,乘法和除法操作是太慢了。乘/除法并且在内部不是靠流水线来实现的。可见相应的硬件实现是牺牲了速度以换取(指令)简单和节省芯片大小。

汇编器提供了一个合成的乘法指令用来执行乘法并将结果取出放回一个通用寄存器。MIPS公司的汇编器会通过一系列的移位和加法操作来替换(硬件)的乘法指令, 如果汇编器优化觉得这样更快的话。我对於这一点的意见是优化的工作应该有编译器来完成,而不是有汇编器来做。

乘法部件不是流水线构造的。每一次只能执行一条指令。上一次的结果将丢失如果下一条乘法指令又开始了,上一次的结果不会象流水线结构那样被写到流水线的write-back阶段。(译者注:在流水线方式下,在write-back阶段,寄存器-寄存器指令的结果将被写回到结果寄存器)。这一点如果不注意的话,将导致一些非常难理解的问题,导致你的程序的结果不对,比如中断的打扰使得你刚才的乘法结果被冲掉了。

如果一个mfhi或mflo指令在还没有走到流水线的write-back阶段而被中断或异常打断,系统将会重新启动上述读取操作,废掉上一次的读取。但是如果下一条指令是乘法指令并且完成了ALU阶段,该乘法指令会与异常处理并行的执行,并有可能覆盖掉hi和ho寄存器里的内容。那么上述mfhi或mflo的重新执行将会得到错误的结果。由於这个原因,乘法指令一般不要紧跟在mfhi/mflo指令后面,要隔开两条指令(译者着:从而防止CPU的指令预取)

2.4 加载与存储:寻址方式

如前面所言,MIPS只有一种寻址方式。任何加载或存储机器指令可以写成
lw $1, offset($2)
你可以使用任何寄存器来作为目标和源寄存器。offset偏移量是一个有符号的16位的数字(因此可以是在-32768与32767之间的任何一值)。用来加载的程序地址是源寄存器与偏移量的和所构成的地址。这种寻址方式一般已足够存取一个C语言的结构(偏移量是这个结构的起始地址到所要存取的结构成员之间的距离)。这种寻址方式实现
了一个通过一个常量来索引的数组;并足够使得可以存取堆栈上的函数变量或桢指针;可以提供一个比较合适大小的以gp为基址的全局空间以存取静态和外部数据。

汇编器提供一个简单直接存取方式的汇编格式从而可以加载一个在连接时刻才能决定地址的变量的值。

许多更复杂的方式,如双寄存器或可伸缩的索引,都需要多个指令的组合。

2.5 存储器与寄存器的数据类型

MIPS CPU可以在一个单一操作中存储1到8个字节。文档中和用来组成指令助记符的命名约定如下:

C名字 MIPS名字 大小(字节) 汇编助记符
longlong dword 8 “d”代表ld
int/long word 4 “w”代表lw
short halfword 2 “h”代表lh
char byte 1 “b”代表lb

2.5.1 整数数据类型

byte和short的加载有两种方式。带符号扩展的lb和lh指令将数据值存放在32位寄存器的低位中并剩下的高位用符号位的值来扩充(位7如果是一个byte,位15如果是一个short)。这样就正确地将一个带符号整数放入一个32位的带符号的寄存器中。

不带符号指令lbu和lhu用0来扩充数据,将数据存放纵32位寄存器的低位中,并将高位用零来填充。

例如,如果一个byte字节宽度的存储器地址为t1,其值为0xFE(-2或254如果是非符号数),那么将会在t2中放入0xFFFFFFFE(-2作为一个符号数)。t3的值会是0x000000FE(254作为一个非符号数)

lb t2, 0(t1)
lbu t3, 0(t1)

上述描述是假设一个32位的MIPS CPU。但是MIPS III或其上的体系结构实现了64位寄存器。可见所有的部分word字的加载(包括非符号数)都带符号(包括0)扩充到高32位。这看上去很奇怪但却是很有用的。这将在2.7.3节中解释这一点。

这些较小长度的整数扩充到较长的整数的细微区别是由於C语言可移植性的历史原因造成的。现代C语言标准定义了非常明确的规则来避免可能的二义性。在不能直接作8位和16位精度的算术的机器中,如MIPS,编译器对任何包含short和char变量的表达式中需要插入额外的指令以确保数据该溢出时得溢出:这一点是不希望的,程序效率非常差。当移植一个使用小整数变量的代码到MIPS CPU上的时候,你应该考虑找出那些可以安全的转换成整数的变量。

2.5.2 没对齐的加载和存储

MIPS体系结构中,正常的加载和存储必须对齐。半字(halfwords)必须从2个字节的边界加载;字(word)必须从4个字节的边界。一个加载没有对齐的地址的加载指令会导致CPU进入异常处理。因为CISC体系结构,例如MC680x0和Intel的x86确实能够处理非对齐的加载和存储,当移植软件到MIPS体系结构时,你可能会遇到这个问题。一个极端情况是你或许想安装一个异常处理程序来负责相应的加载操作从而使得地址对齐的操作对用户程序是透明的。但是这种做法使得程序效率非常慢,除非这样的异常处理非常少。

所有C语言的数据类型将严格的按照其数据类型的大小对齐。

当你不知道你要操作的数据是对齐的或者说就是不对齐的,MIPS体系结构允许通过两条指令来完成这个非对齐的存取(比通过一些列的字节的存取然后移位,加法的效率高得多)。这些代理指令的操作很隐含,比较难以掌握,通常是有宏指令ulw的产生的。详细可见8.4.1节。

MIPS另外还提供宏指令ulh(非对齐的加载半字)。这也是通过合成指令来完成的–两个加载操作,一个移位和一个位或操作。

通常,C编译器负责将所有的数据进行正确的对齐。但是在有些情况下(但从一个文件中读取数据或与一个不同的CPU共享数据)能够处理非对齐的整数数据是必须的,一些编译器允许你设定一个数据类型是非对齐的,编译器将会产生相应的特殊代码来处理。ANSI提供#progma align nn,GNU是通过更简洁packed结构属性类型来指定。

即使你的编译器实现了packd数据类型,编译器并不保证会使用特殊的MIPS指令来实现非对齐的存取。

2.5.3 内存中的浮点数据

从内存中将数据加载到浮点寄存器中不会经过任何检查–你可以加载一个非法的浮点数据(实际上,你可以加载任意的数据模式),并不会得到浮点运算错误直到对这些数据进行操作。

在32位处理器上,这允许你通过一个加载将一个单精度的数据放入一个偶数号的浮点寄存器中,你也可以通过一个宏指令加载一个双精度的数据,因此在一个32位的CPU上,汇编指令

l.d $f2, 24(t1)

被扩充为两个连续的寄存器加载:

lwc1 $f2, 24(t1)
lwc1 $f3, 28(t1)

在一个64位CPU上, l.d是机器指令ldc1的别名。ldc1完成64位数据的加载工作。

任何一个遵循MIPS/SGI规则的C编译器都将8byte的long(长整数),双精度浮点变量在8byte 的地址边界上对齐。32位硬件不需要这个要求,对齐是为了向上的兼容性:64位CPU如果加载一个没有在8byte上对奇的double变量,CPU将进入错误处理,进入异常。

2.6 汇编语言的合成指令

虽然从体系结构的原因我们不能直接用一条指令来完成将一个32位的常量取入一个寄存器中, 但是写MIPS机器码或许太沉闷了。汇编语言程序员不想每次都得考虑这些。因此MIPS公司的汇编器(和其他的MIPS汇编器)将会为你合成一些指令。你只需要写一个加载立即数指令,汇编器会知道什么时候通过两条机器指令来实现之。

显然这是很有用的,但是同时自从发明之后也就一直被乱用。许多MIPS汇编器通过将体系结构的特点掩盖起来从而使得不需要合成指令。在本书中,我们将试图尽量少用合成指令,当使用时,会给读者指出来。另外,在下面的指令列表中,我们将会指出合成指令与机器指令的区别。

我的感觉是合成指令是用来帮助程序员的,严肃的编译器应该严格的一对一的产生机器指令代码。但是在这个不尽善尽美的世界里,还是有许多编译器产生合成指令。

汇编器提供的有用的方面包括下列:

* 一个32位的立即数加载:你可以在数据码中加载任何数据(包括一个在连接阶段决定的内存地址),汇编器将会把其拆开成为两个指令,加载这个数据的前半部分和后半部分。

*从一个内存地址加载:你可以从一个内存变量来作一个加载。汇编器通常会将这个变量的高位地址放入一个暂时的寄存器中,然后将这个变量的低位作为一个加载的偏移量。当然这不包括C函数里的局部变量。局部变量通常定义在堆栈上或寄存器中。

*对内存变量的快速存取:一些C程序包含了许多对static和extern变量的存取, 对它们加载与存储用load/store两条指令开销太大了。一些编译系统避开了这一点,通过一些运行时的支持。在编译的时刻,编译器选择好一些变量(MIPS公司的汇编器缺省选择那些8或更少存储字节的变量),并将它们放在一起到一个大小不超过64K字节的内存区间。运行系统然后初始化一个寄存器–$28,或者说gp,来指向这个区域的中间位置。对这些数据的加载和存储可以通过对gp寄存器相对位置的一个加载或存储来完成。

*更多类型的跳转条件:汇编器通过对两个寄存器的算术测试来合成一系列的条件跳转。

*简单或不同形式的指令:一元操作,例如,not和neg,是通过nor或sub与永远值是零的寄存器$0来实现的。你还可以用两个操作数的方式来表示一个三个操作数的指令。汇编器将会把结果存回到第一个指定的寄存器中。

*隐藏跳转延迟槽:在正常的情况下,汇编器将不会让你接触到延迟槽。SGI汇编器非常灵巧,可以识别指令序列寻找有用的指令并将其放入到延迟槽中。一个汇编directive .set noreorder可以用来防止这一点。

*隐含加载延迟:汇编器会检测是否一个指令试图使用一个前面刚加载的数据结果。如果有这样的情况,将会对代码进行移动。在早期的MIPS CPU中(没有加载数据互锁),系统将会插入一个空指令nop。

*没对齐的移动:不对齐的数据加载和存储指令将会正确地存取半字和字数目,虽然目标地址是非对齐的。

*其他流水线矫正:一些指令(例如那些使用乘法器的指令)需要额外的限制–例如乘法器的输入寄存器在结果输出之后的第3条指令时才能复位并重新使用。你可能不想知道这方面太多的细节,汇编器会替你把补丁填好。

*其他的优化:一些MIPS指令(特别是浮点)需要花费很多的指令来产生计算结果,而且在这期间CPU是互锁的,因此你不需要考虑这些延迟对你程序正确性的影响。但是SGI的汇编器在这方面非常勇敢,会将代码挪来挪去从而提高运行速度。你有可能不喜欢这一点。

纵队,如果你想将汇编源代码(没有用.set noreorder的代码)与在内存中的指令对应起来,你需要帮助。请使用一个反汇编工具。

2.7 MIPS I 到 MIPS IV: 64位(和其他)的扩展

MIPS体系结构自从诞生以来就一直在演变,最为显著的为从32位到64位。这个扩展非常干净利索,以致在介绍MIPS体系结构时我们几乎可以按照64位的体系结构来描述,32位的结构当作是其的子集。本书没有这样做,因为如下几个原因。第一,MIPS并不是一开始就是64位的。如果一开始就按照64位来描述,可能会使得你迷惑。第二,MIPS提供给工业界的一个经验就是一个体系结构如何能够平滑的扩展。第三,本书的材料其实是为32位MIPS而准备的,当时MIPS还没有包含其64位扩展。

因此,我们介绍的方法是混合的。通常我们会先介绍32位下的□c能,当介绍到细节的时候,就会既包括32位又包括64位。在以后我们将用ISA作为指令集的缩写。

当MIPS ISA演化时,原来32位 MIPS CPU (包括R2000, R3000 和其相应的产品) ISA都相应的称为 MIPS I。另一个广泛使用的,含有许多重要改进并从而在R4000及其后续产品上提供了完整64位ISA的指令集,我们称之为MIPS III。

MIPS的一个优点是,在用户层次(当你在一个工作站上写程序时,你可见的所有代码),每个MIPS ISA都是其前一个的超集,没有任何遗漏,只有增加新的功能。

MIPS II出现过。但其第一个实现R6000马上就被MIPS III R4000取代了。除了MIPSIII的64位的整数运算, MIPS II非常接近于一个MIPS III的子集。MIPS II ISA最近又回来了,随着对32位的MIPS CPU 实现的要求的增加。

如我们已经描述过的,不同的ISA层次定义和描述了相应ISA层的内容。除去其他内容,这些ISA至少定义了在一个保护的操作系统中一个用户程序所要使用的所有的,包含浮点运算的指令。如从指令系统出发,ISA定义和描述了整数,浮点数和浮点控制寄存器。

每一个ISA定义都非常小心的将CPU控制寄存器(协处理器0),最近将所有的CPU控制寄存器都排除在外。我不知道这有什么帮助,虽然这可以创造更多的MIPS CPU咨询业的工作机会,由於ISA中隐含了很多信息。例如,如果你想要了解如何对R5000的cache编程的话,“MIPS IV指令集”的书是没有任何帮助的。

在实践中,协处理器0也伴随这正式的ISA一起演化着。与ISA的版本类似,协处理器0有两个主要的版本:一个是与R3000(MIPS CPU中最大家族MIPS 1的祖先),另一个是第一个MIPS III CPU, R4000。我将称这两个CPU家族为R3000式的和R4000式的。以后的CPU,如R5000何R10000都保留了R4000式的协处理器构造。

2.7.1 迈向64位

1990年MIPS R4000的问世,MIPS成为第一个64位的RISC芯片。MIPS III的指令集提供64位的整数寄存器。所有的通用寄存器是64位大小的。有一些CPU控制寄存器也是64位的。另外,所有的操作都产生64位的结果,虽然一些从32位的指令集继承过来的指令对64位的数据没有任何影响。对那些不能兼容的扩展到64位来处理64位的操作数的32位指令,MIPS III指令集提供了新的增加的指令。

在MIPS III中,FPA有独立的64位长的FP寄存器,因此你不再需要一对32位的寄存器来存放一个双精度的浮点运算值。这个扩展是不兼容的,因此人们可以通过设置一个控制寄存器的模式开关来使得这些寄存器的行为与MIPS I 一样从而使得旧软件也可以使用。

2.7.2 谁需要64位?

到1996年,32位CPU已经不能提供足够的地妒7d空间给一些大的应用程序。专家们认为程序的大小在指数倍的增长,每18个月就翻一番。随着这个增长速度,对地址空间的要求将是每年要增加3/4个bit。真正的32位机器(68020, i386)是在1984年取代16/20位的机器的。因此32位机器将会在2002年左右变的嫌小了。如果从这个数据让我们觉得MIPS1991年的动作太超前了,或许是对的–MIPS的最大支持者SGI直到1995年才推出其64位的操作系统。

MIPS技术早期的发展来源于操作系统的研究兴趣,希望通过使用较大的虚拟地址空间从而使得一个对象(object)可以在一段时间内通过其虚拟地址来命名。 MIPS CPU绝不是在操作系统发展中最有威望的机构。Intel占据世界市场的32位CPU等待了11年直到Windows 95操作系统将32位运算带入了巨大的市场。

64位体系结构的一个特点是计算机可以一次处理更多的位,这可以使得一些要处理大量数据的应用程序,如图形和图像,得到加快。对於多媒体指令的扩充,如Intel的MMX,士不是有必要还不是很明朗。Intel的MMX不仅提供宽广的数据通道,还能满足同时处理在其数据通道上一个字节或16位数据。

到了1996年,任何一个声称具有长远目标的体系结构都需要相应的64位的实现。或许早点实现64位计算不是一个坏事。

采用一个平面一维的线性地址空间和将通用寄存器作为指针是MIPS体系结构的特点。这意味着64位寻址和64位寄存器是相伴的。即使不考虑宽的64位地妒7d,增加了宽度的寄存器与ALU对一些处理大量数据的程序,如图形或高速通讯程序也是非常有用的。

MIPS体系结构(和其他一些RISC体系结构)带来的一个希望是体系结构朝64位的发展使得地址的段式结构(x86和PowerPC体系结构的特点)变得再没有任何必要。

2.7.3 关于64位与CPU 模式转换:数据位於寄存器中

在将一个CPU扩充到一个新的领域时,通常“标准”的做法是象很久以前DEC公司将其PDP-11挪到VAX上和Intel公司从80286升到i286和i386:他们在新的处理器中定义一个模式转换控制,当模式控制启动时,使得处理器运行得象其前代产品一样。

但是模式切换是一种组合起来的一种方法。在一个没有微代码的机器中,这种模式切换是很难实现的。因此R4000采用了一种不同的方法:

* 所有的MIPS II 指令集都保留。
* 只要你仅仅运行MIPS II指令,你的程序就是与MIPS II处理器是100%兼容的。每一个MIPS III的64位寄存器的低32位存放着相应的在MIPS II CPU时其寄存器的值。

*尽可能的定义MIPS II指令,从而使得保持兼容性并且可用在64位指令中。

在这里,重要的决定(当你清楚这个问题后,就是一个简单的问题)是,但我们将64位CPU运行在32位兼容状态下时,寄存器高32位将存放什么值?有很多种选择,但只有少数几个是简单明了的。

我们可以简单的决定寄存器高32位是没定义的。当你将CPU运行在32位兼容模式下时,寄存器的高32位可以含有任何旧的垃圾值。这个方法实现很简单但不能满足上述第三点:我们将需要32位和64位各自的测试和条件转移指令(用来测试寄存器是否相等或通过检查最高位来负数)。

第二种方案相对吸引人一点,当CPU运行在32位时,寄存器高32位保持为0。这种方法同样要求提供各自的对负数的测试指令和对负数的比较指令。另外,一个64位的异或(“nor”)指令用在两个高32位为0的值时,不能自然的产生一个高位为0的值。

第三种,也是最好的一种方法是将寄存器的高32位与第31位一样。如果(当仅仅运行32位指令时)我们确信每个寄存器存放着正确的低32位值并且高32位是第31位的复制,那么所有的64位比较和测试指令与其32位的相应指令就都是这个兼容的。所有的位操作逻辑指令也同样(任何对位31操作正确的,对位32到63也同样适用)。

这个正确的方法可以这样来加以描述,将寄存器的低32位进行带符号扩展到64位。这种方法与寄存器中的值是带符号的还是不带符号的无关。

按照这个方案,MIPS III需要新的64位简单数值计算指令(32位的addu指令,当遇到32位溢出时,将会把溢出的结果存放在低32位,并将第31位扩充至高32位—这与64位加法是不一样的!)。MIPS III还需要新的64位的存取和移位指令。在需要一个新的64位指令时,其指令助记符增加一个“d“,比如daddu, dsub, dmult和ld等。

略微不是很明显的是32位的加载指令lw。在64位下,lw更精确的意思是加载一个带符号的字(word),因此一个用在64位下的新的指令lwu被引入。lwu意味着高32位是用0来扩展。需要增加的指令的数目是由支持现有的MIPS II CPU种类的需要和(比如,按照一个常数来移位)支持使用不同的指令操作码(op-code)如何避免在32位下固定的只有5位
的移位数。

所有的MIPS指令都详细的列在了第八章。

2.7.4 MIPS III的其他一些发明
同步64位的广泛扩展提供了一个机会来增加一些非常有用的指令(与64位数值计算操作无关的)。

多处理器操作

64位MIPS提供了一对指令–加载关联(load linked)和条件存储(store conditional)。它们用来实现软件的semaphore,可用在共享内存的多处理器系统中。它们的功能与最近的CISC体系结构提供的原子性的RMW(读-改-写)指令和锁指令是一样的。但是,RMW和锁指令在一个大的多处理器系统中效率是不好的。我们将在5.8.4节中解释加载关联和条件存储的操作。在这里,下面是对它们功能的一些介绍。

ll是一个普通的加载一个word的指令,但是它在一个特殊的内部寄存器中保持这个地址的记录。sc是一个存储一个word的指令,但是它只在如下条件下才存储:

* 自从上次在这同样地址上的ll指令之后,CPU没有发生任何中断或异常,并且

* (对多处理器系统),没有别的CPU发出写操作或试图一个写操作并且写的地址包括了ll指令使用的地址。sc指令会返回一个值来告诉程序存储是否成功。

虽然ll和sc指令是为多处理器系统设计的,也可以被用在单处理器系统上。从而可以实现一个semaphore而不需要关闭中断。

封闭循环转移(可能循环)

高效的MIPS代码要求编译器能够在大多数延迟槽上安排有用的工作。在许多情况下,逻辑上在跳转指令之前的那条指令是合适的选择。显然,如果这个跳转指令是一个条件跳转并且在其之前的那个指令是计算这个跳转条件的,那么就不能把其之前的那条指令放入延迟槽中。

这种情况在包含一个循环的跳转中经常出现。循环越小,编译器就越难找到一个之前的指令并方入延迟槽中。

在一个循环里面,编译器的第二种选择是在延迟槽中存放一个跳转指令的目的地的那条指令的备份。并且将跳转目标地址提高一个word。这个调整不会使得程序变小,但确实能使程序运行加快。但是这个方法通常是不可能的。当一个循环结束时,在延迟槽里的指令将会被执行,这使得编译器很难判断这个行为是否会造成任何损害。

在这里编译器需要的是一个只有在跳转被执行时延迟槽里的指令才被执行的跳转指令。这是MIPS III指令集可以提供的功能。这些指令称之为“可能跳转”(branch likely)–这个命名非常容易迷惑人。它们的助记符是在清b有的指令助记符后面加一个”l”:因此beq产生begl指令。其他依此类推。

条件异常

随着MIPS III, 提供了一系列指令可以依据一个条件来使CPU进入异常处理:测试条件与“set if …”指令是一样的。这些指令在C语言中没有相应的语法,但是可以用来实现那种动态检查数组越界的编程语言。

扩充的浮点数

R6000将浮点寄存器扩充到了64位宽度。但是我把其当作是MIPS III扩充到64位的一部分。如果新的MIPS II有浮点处理器(不太可能),一般而言是32位的。

2.8 基本地址空间

相对於其他CISC CPU, MIPS处理器对地址空间的使用有些细微的不同。这一点有时会使人迷惑。请仔细阅读这一节的第一部分。我们将先介绍32位CPU的情况,然后再介绍64位。耐心点你将会在以后知道我为什么这样做。

下面是一些概述。在MIPS CPU里,你的程序中的地址不一定是芯片真正访问的物理地址。我们分别称之为:□
’7b序地址和物理地址。

一个MIPS CPU可以运行在两种优先级别上, 用户态和核心态。MIPS CPU从核心态到用户态的变化并不是CPU工作不一样,而是对於有些操作认为是非法的。在用户态,任何一个程序地址的首位是1的话,这个地址是非法的,对其存取将会导致异常处理。另外,在用户态下,一些特殊的指令将会导致CPU进入异常状态。

在32位下,程序地址空间划分为4个大区域。每个区域有一个传统的名字。对於在这些区域的地址,各自有不同的属性:

kuseg: 0×000 0000 – 0x7FFF FFFF (低端2G):这些地址是用户态可用的地址。在有MMU的机器里,这些地址将一概被MMU作转换。除非MMU的设置被建立好,这2G地址是不可用的。

对於没有MMU的机器,存取这2G地址的后依具体机器相关。你的CPU具体厂商提供的手册将会告诉你关于这方面的信息。如果想要你的代码在有或没有MMU的MIPS处理器之间有兼容性,尽量避免这块区域的存取。

kseg0: 0×8000 0000 – 0x9FFF FFFF(512M): 这些地址映射到物理地址简单的通过把最高位清零,然后把它们映射到物理地址低段512M(0×0000 0000 – 0x1FFF FFFF)。因为这种映射是很简单的,通常称之为“非转换的“地址区域。

几乎全部的对这段地址的存取都会通过快速缓存(cache)。因此在cache设置好之前,不能随便使用这段地址。通常一个没有MMU的系统会使用这段地址作为其绝大多数程序和数据的存放位置。对於有MMU的系统,操作系统核心会存放在这个区域。

kseg1: 0xA000 0000 – 0xBFFF FFFF(512M): 这些地址通过把最高3位清零的方法来映射到相应的物理地址上,与kseg0映射的物理地址一样。但kseg1是非cache存取的。

kseg1是唯一的在系统重启时能正常工作的地址空间。这也是为什么重新启动时的入口向量是0xBFC0 0000。这个向量相应的物理地址是0x1FC0 0000。

你将使用这段地址空间去存取你的初始化ROM。大多数人在这段空间使用I/O寄存器。如果你的硬件工程师要把这段地址空间映射到非低段512M空间,你得劝说他。

kseg2: 0xC000 0000 – 0xFFFF FFFF (1G): 这段地址空间只能在核心态下使用并且要经过MMU的转换。在MMU设置好之前,不能存取这段区域。除非你在写一个真正的操作系统,一般来说你不需要使用这段地址空间。

2.8.1 简单系统的寻址

MIPS的程序地址很少与真正的物理地址一致。但对於简单的嵌入式软件而言可以用kseg0和kesg1这两段地妒7d空间。它们朝物理地址的映射关系是非常直接了当的。

从0×20000 0000开始的512M物理地址空间在上述kseg0, kseg1 和kseg2中没有任何的映射。你可以通过设置MMU TLB的方式来访问,或者使用64位CPU的一些其他额外的空间。

2.8.2 核心与用户权限

在核心态下(CPU启动时),PU可以作任何事情。在用户态下,2G之上的地址空间是非法的。任何存取将会导致系统异常处理。注意的是,如果一个CPU有MMU,这意味着所有的用户地址在真正访问到物理地址之前必须经过MMU的转换,从而使得OS可以防止用户程序随便乱用。对於一个没有内存映射的OS,MIPS CPU的用户态其实是多余
的。

另外,在用户态下,一个指令,特别是那些CPU控制指令,是不能使用的。

要提及的是,当你作核心态和用户态切换时,并不意味着□c能的改变,只不过是意味着某些功能在用户态下不能使用了。在核心态下,与用户态一样,CPU可以存取低段地址空间。这个存取也是通过MMU的转换。这一点与用户态下一样。

另外要注意的是,虽然如果把操作系统运行在核心态下,平常的代码运行在用户态下是一种不错的选择。但如果反之也不为过。有些系统,包括□c多实时操作系统,都是全部运行在核心态下。

2.8.3 64位CPU的地址空间

MIPS地址的形成是通过一个16位的偏移量和一个寄存器。在MIPS III或更高版本的CPU里,一个寄存器是64位。因此一个程序地址是64位的。这样大的地址空间允许我们耐心的将其划分。请参阅图2.2。

首先要注意的是64位内存映象是包含在32位内存映象里面的。这是个有点奇怪的方法,就象Dr. who的“Tardis”–里面比外面要大的多。这一点是通过2.7.3节介绍的规则来实现的:当模拟一个32位指令集的适合,寄存器存放的是其32 位的带符合位扩展的64位值。因此,一个32位程序存取的是64位程序空间的最低和最高的2G。换句话说,64位CPU的地址空间的最低和最高区域是和32位情况下一样的,64位扩展的地址部分在这两者之间。

在实践中,扩展的用户地址空间和超级用户权限的地址空间一般而言没有太大的用处,除非你在写一个虚拟内存操作系统。因此许多MIPS III的使用者仍然定义32位的指针。64位下那些大块的不需要MMU转换的窗口可以克服kseg0和kseg1 512M的局限,但是我们可以通过对MMU编程来同样达到这一点。

2.8.4 流水线hazard

任何一个有流水线的CPU硬件对於那些不能满足严格的一个时钟周期规则的操作都将会存在一个延迟。体系结构的设计者要决定这些延迟中的哪一些对於编程员是可见的。将时序 上的缺点隐含起来使得程序员的编程模型简单,比如,CPU究竟在干什么。当然与此同时,这将对硬件实现引入复杂性。将调度问题留给程序员和其软件工具将简化硬件部分,但同时产生编程和移植的问题。

正如我们已经提过几次,MIPS体系结构使得其一些缺点/特点是可见的。程序员和编译器要负责配合CPU使得其正常工作。下面一些是关于流水线的方面:

* 跳转延迟:在所有的MIPS CPU里,紧跟着跳转指令的指令(在延迟槽中)会被CPU执行,即使跳转成左5c。在MIPS II指令集中引入的“可能跳转”(branch-likely)指令中,在延迟槽中的指令只会在跳转被接受的情况下被执行。详细可见8.4.4关于”可能跳转“的基本原理。程序员或编译器必须找到一个有用的,至少是无害的指令放在延迟槽中。但是,除非你指定,汇编器将会使得跳转延迟是透明不可见的。

*加载延迟:在MIPS I指令集里,load指令后面的指令(在加载延迟槽)不能使用刚用load加载的数据。一个有用的或无害的指令需要放在加载延迟槽里来将数据加载和数据使用分开。与跳转延迟一样,除非你指定,汇编器将会使得这个延迟处理对你是透明不可见的。

*整数乘法/除法问题:整数乘法部件是和ALU部件分开的,没有实现“精确异常”(请参阅5.1节关于精确异常的定义)。解决方法很简单,通常是通过汇编器–在读取上一个乘除法的结果值之后,你需要避免立刻启动下一个乘除法运算。为什么这个解决方法是必须的和足够的很负责(请参阅5.1节)。

*浮点数(协处理器1)的缺点:任何一个浮点运算几乎都要花费多个CPU时钟周期来完成,MIPS FPA通常有多个独立的流水线部件。在这种情况下,硬件可以把流水线隐含起来;FP计算可以与其后的指令并行的执行。当一个指令读取一个尚未完成的浮点计算的结果寄存器时,CPU就会停止下来。编译器需要大量的优化工作在这方面,比如重复指令比率表,各种目标CPU的延迟表等。当然,你没必要依赖这些来使得你的程序工作。

如果一个浮点计算没有流水线hazard,并不意味着浮点运算协处理器与整数运算部件的交互没有流水线hazard。这里面有两方面原因。

第一,从浮点运算器移动数据到整数寄存器的指令–mfcl, 传送数据的时刻是在下一个时钟周期,与“load”具有同样的时序要求。就象load一样,在MIPS 1 CPU中,这是个hazard,但在后来的硬件中,被利用硬件的内置锁(interlock)解决了。优化的编译器会利用延迟槽完成一些有用的工作。

第二,测试一个浮点运算的条件的指令不能直接跟在产生那个条件的浮点比较操作后面。对大多数MIPS CPU实现,需要一个指令的延迟。

* CPU控制指令问题:这个部分非常容易迷惑人。当你改变CPU状态寄存器的内容时,你潜在地在影响发生在流水线所有阶段的东西。因为关于CPU控制系统的结构描述是与具体的实现有关,因此没有ISA指令集方面的规则可以遵循。遗憾的是CPU厂商至今没有提供有关相应的文档。

请参阅第三章关于MIPS CPU控制指令的总结,然后请阅读附录A关于R4000 CPU的时序问题。

(3个打分, 平均:5.00 / 5)

雁过留声

Comments are closed.