前言
这本书是关于MIPS处理器的。MIPS处理器是八十年代中期RISC CPU设计的一大热点。
MIPS是卖的最好的RISC CPU,可以从任何地方,如Sony, Nintendo的游戏机,Cisco的
路由器和SGI超级计算机,看见MIPS产品在销售。目前随着RISC体系结构遭到x86芯
片的竞争,MIPS有可能是起初RISC CPU设计中唯一的一个在本世纪盈利的。
RISC是一个很有用的简写,并不仅仅是一个市场广告用词。RISC概括了在80年代出
现的,采用流水线结构设计的一系列计算机体系结构的许多共同的特徵。CISC这个
术语就有点不清晰,它指的是所有非RISC的技术或芯片。在本书中,CISC指的是非
RISC的68000,x86和其他一些1982年以前的通过微代码技术的体系结构。
这本书是写给程序员的。这也是我们考虑书中应该包括什么的准则--是否程序员会
接触到一个问题,或是否会对一个问题感兴趣。这意味着我们不会在这里讨论已经
折磨了两代硬件工程师的MIPS奇怪的系统接口。操作系统可能已经将我们在这里将
要讨论的许多细节隐含了起来;许多优秀的程序员认为C语言在层次上已经足够低了,
与具体的体系结构不相关性因此具有极好的可移植性。但是在有些时候你确实需要
具体的细节,并且人对事物是如何工作的总是充满了好奇的。
当描述一些软件工程师可能不是很熟悉的部分的时候,特别是CPU的内部工作细节,
我们将通过非正式的方式讲述。但当遇到程序员曾经遇到过的问题时,如寄存器,
指令和数据如何在内存中保存,我们将会非常简洁地和技术性地解释。
我们假设读者对C语言有一定的熟悉和接触过。本书中的大多数例子使用C语言作为
简单紧凑的例子,特别是在关于指令集与汇编语言的章节中。
本书的一些部分是面向那些曾经用过CISC处理器(比如,680x0或x86)的读者。这样
MIPS处理器的灵巧性和独特性才能充分的体现出来。当然如果你不熟悉CISC的汇编
语言,也没太大关系。
大多数情况下,需要了解一个CPU细节的或者是操作系统方面的高手,或者是在嵌入
式系统方面工作人。”嵌入式“是一个很广的定义,其含义是一个不象通常计算机
的计算机。这样一个系统的共同的特徵是一个操作系统(如果存在的话)并不将CPU的
工作与程序员隔离。MIPS处理器被使用在从游戏到工业控制等非常广泛的领域中。
本书并不是一本MIPS体系结构的参考手册:要掌握一个体系结构意味着要去实践。
我希望本书会对那些希望了解现代CPU体系结构的学生有所帮助。
如果你计划从前往后的通读这本书,你可以从一般的阅读到精读来一步一步的取得
进展。你不会失望的。你也可以通过历史的顺序来阅读。每次谈论一个新概念时,
我们通常会着重于其第一个版本。Hennessy和Patterson称这种方法为“从演化中学
习”。这种学习方法对他们是不错的,对於我,当然也是好的。
因此在第一章,我们将会介绍一些历史和背景知识,并通过讨论MIPS发明者当初所
最关心的技术问题和想法来引入MIPS处理器。接着在第二章我们讨论按照他们想法
所设计的MIPS机器语言的特徵。
MIPS体系结构很小心的将与浮点数处理的指令分开. 这种分开可以使得MIPS CPU与
不同层次的浮点支持结合在一起, 比如,从可以没有浮点运算, 部分支持,到最新的
浮点硬件技术. 因此在讲述中我们也将暂时浮点运算部分分离出去, 将其放在第七
章.
第八章里我们会讲解整个机器的指令系统. 我们的目的是要尽可能的精确, 但要比
MIPS标准的参考归约要简单的多--相对其他文档或书籍用了一百多页,我们用了十页
. 第九章描述了MIPS汇编语言编程,所以该章更象一个编程手册. 这与书中其他章节
的风格有点不一样, 但迄今为止一直没有一个很好的关于MIPS的汇编语言手册. 在
汇编语言层次编程的读者会发现书中其他部分也是相关的.
第十章是为那些已经熟悉C语言编程而准备的. 这一章着重与MIPS体系结构相关的C语
言部分. 提供的例子中包括MIPS编译器实现的内存结构和参数传递. 第十一章提供
了一个对在从事把一个软件从其他CPU移植到MIPS CPU 的人们非常有帮助的各种注
意事项.
第十二章是一些加了注释的与本书内容相关的软件. 理解真正的软件代码有可能要
费点事, 但是准备从事一个具有挑战性的MIPS项目的读者会发现这一章是很有用的
, 提供了一个编程风格指南和一些相关材料列表.
附录A(指令时序), 附录B(汇编语言语法),和附录C(目标代码)包含了一些我认为不
应该被完全忽视的非常技术性的内容, 虽然不会有很多人需要去参考这些材料. 附
录D中你可以得到一些MIPS体系结构的最新消息, 比如MIPS16, MDMX和MIPS V的指令
集扩展.
在本书的最后, 是词汇表. 从中你可以查找那些特殊的或不熟悉的术语. 另外, 还
列出了一些给读者进一步学习所用的书籍, 文章和在网上的一些参考文献.
风格与局限
每本书都反映了一些作者自己的看法. 所以我们要预先认识这一点.
虽然形式不同, 这本书其实已经存在七年了. 作者从1986年就一直从事MIPS体系结
构的工作. 从1988年起, 我开始给一些客户作一些关于MIPS体系结构方面的培训.
讲座的部分材料成为该书的一些章节。1993年,我将所有的材料收集起来,并制作
成一个软件手册作为IDT的MIPS文档的一部分。但是这个手册是专门为IDT的R3051系
列的,因此有很多重要的细节没有在手册中讲述。在1995和1996年,这本书增加了
64位CPU的部分和其他相关细节。
MIPS还在一直成长;否则我们写MIPS的书只能给历史学者看了。Morgan Kaufmann也
不会有兴趣来出版该书。
因为该书的写作和评阅过程非常繁长,我们不得不选择一个合适的时间点。在其后
的MIPS发布的新的功能就没有包括在本书中。但是我们在附录D中尽可能的反映了这
些最新的发展。
约定
下面是关于本书的一些字体格式约定:
//在此略去
感谢
//在此略去
第一章 RISCs与MIPS
MIPS是高效精简指令集计算机(RISC)体系结构中最优雅的一种;即使连MIPS的竞争对手也这样认为,这可以从MIPS对于后来研制的新型体系结构比如DEC的Alpha和HP的Precision产生的强烈影响看出来。虽然自身的优雅设计并不能保证在充满竞争的市场上长盛不衰,但是MIPS微处理器却经常能在处理器的每个技术发展阶段保持速度最快的同时保持设计的简洁。
相对的简洁对于MIPS来说是一种商业需要,MIPS起源于一个学术研究项目,该项目的设计小组连同几个半导体厂商合伙人希望能制造出芯片并拿到市场上去卖。结果是该结构得到了工业领域内最大范围的具有影响力的制造商们的支持。从生产专用集成电路核心(ASIC Cores)的厂家(LSI Logic,Toshiba, Philips, NEC)到生产低成本CPU的厂家(NEC, Toshiba,和IDT),从低端64位处理器生产厂家(IDT, NKK, NEC)到高端64位处理器生产厂家(NEC, Toshiba和IDT).
低端的CPU物理面积只有1.5平方毫米(在SOC系统里面肉眼很难找到).而高端的R10000处理器,第一次投放市场时可能是世界上最快的CPU,它的物理面积几乎有1平方英寸,发热近30瓦特.虽然MIPS看起来没什么优势,但是足够的销售量使其能健康发展:1997年面市的44M的MIPS CPU,绝大多数使用于嵌入式应用领域.
MIPS CPU是一种RISC结构的CPU, 它产生于一个特殊的蓬勃发展的学术研究与开发时期.RISC(精简指令集计算机)是一个极有吸引力的缩写名词,与很多这类名次相似,可能遮掩的真实含义超过了它所揭示的.但是它的确对于那些在1986到1989年之间投放市场的新型CPU体系结构提供了一个有用的标识名,这些新型体系结构的非凡的性能主要归功于几年前的几个具有开创性的研究项目所产生的思想。有人曾说:"任何在1984年以后定义的计算机体系结构都是RISC";虽然这是对于工业领域广泛使用这个缩写名词的嘲讽,但是这个说法也的确是真实的-1984年以后没有任何一款计算机能够忽视RISC先驱者们的工作。
在斯坦福大学开展的MIPS项目是这些具有开创性的项目中的一个。该项目命名为MIPS(主要是无内锁流水段微型计算机的关键短语的缩略)同时也是"每秒百万条指令数"的双关语。斯坦福研究小组的工作表明虽然流水线已经是一种众所周知的技术,但是以前的体系结构对它研究的远远不够,流水线技术其实能够被更好的利用。尤其是当结合了1980年的硅材料设计水平时。
1.1 流水线
从前在英格兰北部的一个小镇里,有一个名叫艾薇的人开的鱼和油煎土豆片商店。在店里面,每位顾客需要排队才能点他(她)要的食物(比如油炸鳕鱼,油煎土豆片,豌豆糊,和一杯茶)。然后每个顾客等着盘子装满后坐下来进餐。
艾薇店里的油煎土豆片是小镇中最好的,在每个集市日中午的时候,长长的队伍都会排出商店。所以当隔壁的木器店关门的时候,艾薇就把它租了下来并加了一倍的桌椅。但是这仍然不能容纳下所有的顾客。外面排着的队伍永远那么长,忙碌的小镇居民都没有时间坐下来等他们的茶变凉。
他们没办法再另外增加服务台了;艾薇的鳕鱼和伯特的油煎土豆片是店里面的主要卖点。但是后来他们想出了一个聪明的办法。他们把柜台加长,艾薇,伯特,狄俄尼索斯和玛丽站成一排。顾客进来的时候,艾薇先给他们一个盛着鱼的盘子,然后伯特给加上油煎土豆片,狄俄尼索斯再给盛上豌豆糊,最后玛丽倒茶并收钱。顾客们不停的走动;当一个顾客拿到豌豆糊的同时,他后面的已经拿到了油煎土豆片,再后面的一个已经拿到了鱼。一些穷苦的村民不吃豌豆糊-但这没关系,这些顾客也能从狄俄尼索斯那里得个笑脸。
这样一来队伍变短了,不久以后,他们买下了对面的商店又增加了更多的餐位。
这就是流水线。将那些具有重复性的工作分割成几个串行部分,使得工作能在工人们中间移动,每个熟练工人只需要依次的将他的那部分工作做好就可以了。虽然每个顾客等待服务的总时间没变,但是却有四个顾客能同时接受服务,这样在集市日的午餐时段里能够照顾过来的顾客数增加了三倍。图1.1说明了艾薇的方法,是由她那很少涉猎非虚现实问题的儿子爱因斯坦绘制的。
如果将程序看成是内存中存储的一堆指令的话,一个即将运行的程序看起来和排着队等待接受服务的顾客没什么相似之处。但是如果从CPU的角度来看,就不一样了。CPU从内存中提取每条指令,进行译码,确定需要的操作数,执行相应操作,并存储产生的任何结果-然后再次重复同样的工作。等待执行的程序就是一个等待一次一个的流过CPU的指令队列。
由于每条指令都要做不同的工作,因此在CPU内部已经配有各种不同的专用的大块逻辑电路,所以构造一个流水线并没有使CPU复杂度增加多少;只是让CPU工作负载更重一些而已。
对于RISC微处理器来说使用流水线技术不是什么新鲜事儿。真正重要的在于完全的重新设计-从指令集开始-目的是使流水线更加高效。因此,怎样才能设计一个高效的流水线实际上可能是一个错误的问题。正确的提问应该是,是什么使得流水线效率低下?
第二章 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实清b相关的。
在本书中,我将更加慷慨大方些。有时候,我会描述一个在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 子函数的返回地□
'7d
虽然硬件没有强制性的指定寄存器使用规则,在实际使用中,这些寄存器的用法都
遵循一系列约定。这些约定与硬件确实无关,但如果你想使用别人的代码,编译器
和操作系统,你最好是遵循这些约定。
寄存器约定用法引人了一系列的寄存器约定名。在使用寄存器的时候,要尽量用这
些约定名或助记符,而不直接引用寄存器编号。
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取代了。除了MIPS
III的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: 0x000 0000 - 0x7FFF FFFF (低端2G):这些地址是用户态可用的地址。在
有MMU的机器里,这些地址将一概被MMU作转换。除非MMU的设置被建立好,这2G地址
是不可用的。
对於没有MMU的机器,存取这2G地址的后依具体机器相关。你的CPU具体厂商提供的
手册将会告诉你关于这方面的信息。如果想要你的代码在有或没有MMU的MIPS处理器
之间有兼容性,尽量避免这块区域的存取。
kseg0: 0x8000 0000 - 0x9FFF FFFF(512M): 这些地址映射到物理地址简单的通过
把最高位清零,然后把它们映射到物理地址低段512M(0x0000 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空间。它们朝物理地址的映射关系是非常直接了当的。
从0x20000 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的时
序问题。
See MIPS Run 第三章
翻译:张福新
系统结构实验室
中国科学院计算技术研究所
2003 年8 月8 日
1
第三章
协处理器0 : MIPS 处理器控
制
除了通常的运算功能之外,任何处理器都需要一些部件来处理中断,提供可
选项配置方法以及某种观察或控制诸如高速缓存(cache) 和时钟等片上功能的
途径。但要用一个干净的、和具体实现无关的方法来描述这些东西很难,不象指
令集中表示运算功能那么简单。
为了更便于读者理解,我们会把不同的功能分成几章来介绍。这一章里我
们先介绍用来实现这些特色功能的公共机制。在读后续的三章之前,您应该先
读本章的前面部分,特别要注意“协处理器”(下面将有解释)一词的含义。
那么, MIPS CPU 的协处理器0 (以下简称CP0 )做些什么工作呢?
配置: MIPS 硬件常常是很灵活的,您可能可以选择一些很根本的CPU 特
性(例如大尾端/小尾端,参见第11章)或者改变系统接口的工作方式。这
些选项的控制和可见性通常由一个(一些)内部寄存器决定。
高速缓存控制: MIPS CPU 总是集成了高速缓存控制器,(除了最古老的芯
片)也都集成了高速缓存本身。连最早期的MIPS CPU 都在状态寄存器里
有高速缓存控制的字段。R4000 以后,就有专门的CP0 指令来操纵高速
缓存的每一项了。我们将在第4 章讨论高速缓存。
例外/中断控制: 象中断或者例外时发生什么,您应该做什么来处理它等事情都
由一些CP0 控制寄存器和特殊指令来定义和控制。这会在第5 章讨论。
存储管理单元控制: 第6 章讨论这个话题。
杂项: 总是有更多的东西:时钟、事件计数器、奇偶校验错误检测等等。无论
什么时候额外的功能被集成到CPU 里边,不再能方便地当作外设访问
时,这里就要增加一些东西。
2
MIPS对协处理器一词的特殊用法协
处理器一词通常用来表示处理器的一个可选
部件,负责处理指令集的某个扩展。MIPS
MIPS 标准指令集缺少很多实际CPU 需要的
功能,但是它预留了多达4 个的协处理器操
作码和相应的指令域。其中一个(协处理器1
)时浮点协处理器,这的确是通常意义上的协
处理器—原文: which really is a coprocessor
in anyone’s language 。
另一个(协处理器0 或者说CP0 )是MIPS 所
谓的系统控制协处理器,协处理器0 指令是
处理所有标准指令集范围之外的功能所必须
的。这也是本章描述的对象。
协处理器0 不能独立存在而且也绝不是可选
的—例如,您不可能做一个没有状态寄存器
的MIPS CPU 。但它的确规定了访问状态寄
存器的指令的编码方式。所以,虽然R3000
和R4000 家族的状态寄存器的定义发生了变
化,您还是能用同样—译者:所谓同样,大
概是指用同样的指令,具体的处理一般有所
不同—的汇编程序来处理两种CPU 。
协处理0 的功能被有意地从MIPS 指令集圈
离开来,原则上是实现相关的。实际情况
是这些功能和常规的指令集是配对发展的。
例如,到目前为止制造的MIPS III CPU 的
CP0 功能都非常相象,以致同样的操作系统
二进制代码可以在整个家族的处理器上跑(可
能需要稍微处理一下)。
四个协处理器中, MIPS III ,尤其是MIPS
IV 以后的“标准”指令集已经侵占了CP3 。
只有CP2 还可以给一些片上系统应用使用。
我们会在本章后半部分总结所有在“标准” CPU 能找到的东西。但是让我
们暂时别管我们想作到什么功能,先看看我们用什么机制吧。MIPS CPU 里只
有为数不多的几个CP0 指令—只要可能,对CPU 的底层控制都是对一些特殊
CP0 寄存器某些位的读写。
表3.1 介绍了那些已经成为事实标准的控制寄存器功能描述。表中第一组
的寄存器(及其功能)是到今天为止每个MIPS CPU 都实现了的;第二组是自
R4000 (它代表着一次改善CP0 部件组织方式的尝试)以后的MIPS CPU 都实现
了的。
这不是一个完整的列表;在讲到存储管理和高速缓存控制的时候我们将会
看到更多一些控制寄存器。另外,一些MIPS CPU 已经有一些和具体实现相关
的寄存器—这也是往MIPS CPU 里增加特色功能的标准方法。请参考您的特定
CPU 的手册。
为了防止这时候就用一堆的细节把您搞晕,我们把对CP0 寄存器一位一位
的描述放到不同的小节里:3.3 小节放所有CPU 都有的寄存器;3.4 放R4000
以后的CPU 都有的寄存器。如果您对下面的章节感兴趣,现在可以暂时跳过
那些小节。
我们列这些寄存器的时候, K0 和K1 值得一提。那是两个由软件约定预留
下来的通用寄存器,用在例外处理程序中。预留至少一个通用寄存器是非常必
要的1;预留哪一个是硬性指定的,但必须保证所有的MIPS 工具包和二进制程
序都遵循同一约定。2
1译者:否则保存上下文时会有困难,因为RISC 结构中所有的load/store 都要通过通用寄存
器执行,而且例外处理程序不能假定某个通用寄存器的值有效
2译者:这一段话多少有点跑题的感觉,不过考虑到K0,K1 也是为系统控制服务的,也说的过
去。要记住所谓CP0 寄存器和一般可以参与运算的通用寄存器不同就是了。
3
表3.1: 常见的MIPS CPU 控制寄存器(不包括MMU )
寄存器助
记符
CP0寄
存器标
号
描述
PRId 15 识别这个处理器类型的一个标志符,带着更新版本
号信息。这个ID 原则上是应由MIPS 公司控制的,
指令集或者CP0 寄存器集发生了改变的时候必须变
化。到97 年年中为止用过的值列表可以参见下面的
表3.2 。
SR 12 状态寄存器,罕见地由大部分可写的控制位域组成。
包括决定CPU 特权等级,哪些中断引脚使能和其它
的CPU 模式等位域。
Cause 13 什么导致异常或者中断?
EPC 14 例外程序计数器:处理完例外/中断后从哪里重新开
始执行。
BadVaddr 8 导致最近的地址相关例外的程序地址。各种地址错例
外都会设置它,即使没有MMU 。
Index 0
所有这些都是MMU 操纵相关的寄存器,在第6章描
述。EntryLo1 和Wired 是R4000 引入的。
Random 1
EntryLo0 2
EntryLo1 3
Context 4
EntryHi 10
PageMask 1
Wired 1
R4000 引入的寄存器
Count 9 这两个寄存器一起形成了一个简单但是很有用的高精
度时钟,频率为CPU 流水线频率的一半。Compare 11
Config 16 CPU 参数设置,通常是系统决定;一些域可写,一
些只读。
LLAddr 17 最近一次ll(load-linked) 指令的地址。只用于诊断错
误。
WatchLo 18 用于设置硬件数据观测点。可以在CPU 存取这个地
址时发生例外—可能对调试有用。WatchHi 19
CacheERR 27 当CPU 在其数据通路上支持校验时,用于分析(甚至
可能从中恢复)一个内存错误。详细信息参见图4.4 和
它的解释。
ECC 26
ErrorEPC 30
TagLo 28 用于高速缓存操纵的寄存器,详见4.10 小节。
TagHi 29
4
3.1 CPU 控制指令
有几条CPU 控制指令用于实现存储管理,但我们把它留给第6 章。MIPS
III CPU 有个多功能的cache 指令来做所有对高速缓存的操作,第4 章会进一
步说明。但除此之外, MIPS CPU 控制还需要少数几个指令。首先看看用来访
问刚刚我们列出的那些寄存器的指令:
mtc0 rs, <nn> # 把数据送到协处理器0
dmtc0 rs, <nn> # 把双字数据送到协处理器0
这些指令把通用寄存器rs 的内容装到协处理器0寄存器nn ,数据分别位32 位
和64 位(即使在64 位的CPU 里,很多CP0 寄存器也是32 位的)。这是设置
CPU 控制寄存器的唯一方法。
直接在汇编程序里使用控制寄存器的编号来引用它们是不良习惯;通常您
应该使用如表3.1 中的助记符。大多数工具链把这些名字定义在一个C 风格
的include 文件里,然后用C 的预处理器作为汇编器的前端;您的工具包文档
会告诉您如何做。虽然原始的MIPS 标准有很强的影响,但是(不同的工具链
中)这些寄存器的命名还是有所差别。我们将一直使用表3.1 中的助记符。
与之相反的是从CP0 控制寄存器中取出数据:
mfc0 rd, <nn> # 从协处理器0取出数据
dmfc0 rd, <nn> # 从协处理器0取出双字数据
在两种情况下通用寄存器rd 都被装入CPU 控制寄存器nn 的值。这是查看一
个控制寄存器值的唯一方法。因此,如果您想要更新控制寄存器的某个域,比
如说状态寄存器SR 吧,您写的代码将是这个样子:
mfc0 t0, SR
and t0, <要清掉的位的补码>
or t0, <要设置的位>
mtc SR, t0
控制指令集的最后一个关键成员是一种取消例外效果的方法。我们会在第5章
详细讨论例外的问题,但基本的问题是每个实现任何一种安全操作系统的CPU
都要面对的;那就是例外可以在运行在用户态(低特权级)时发生,而例外处理
程序运行在高特权级。因此当返回用户态时,CPU 需要避开两种风险:一方
面,如果在返回用户程序之前特权级降低了,您马上就会得到一个致命的特权
级违反例外3;另一方面,如果先回到用户态再降低特权级,那么一个恶意的程
序就有可能有机会用高特权级运行指令。所以返回到用户程序和降低特权级必
须是从编程的角度不可分的操作(或者用体系结构术语说,原子的(操作))。
在R3000 和类似的CPU 中,这个工作是由一个延迟槽放一条rfe 指令的
跳转指令来完成的;但从R4000 以后,eret 完成整个事情。第5 章里我们会更
详细的谈到它们。
3译者:因为至少还有一些属于例外处理程序的特权级指令需要运行
5
3.2 起作用的寄存器及其时机
有些寄存器您需要在下面这些情况和它们打交道:
² 加电后: 您需要设置SR 来使CPU 进入正确的引导状态。
绝大部分的MIPS CPU (除了最古老的一些)都有Config 寄存器,它可能
包含一些需要在很早的时候设置的选项。请和您的硬件工程师商量,确
认CPU 和系统关于配置的问题足够一致,至少能启动到让您写这些寄存
器!
² 处理任何例外: 任何MIPS 例外(除了一个特别的MMU事件4)都调用一
个固定入口地址的“通用异常处理程序”。
在入口处程序的寄存器并没有被自动保存,只有返回地址被存在EPC 寄
存器。MIPS 硬件没有任何关于栈的知识。在任何情况下一个安全操作系
统的特权级例外处理程序不能假定用户级代码的任何完整性—特别地,
它不能假定栈指针有效或者栈空间可用。
您需要用K0 和K1 中至少一个来指向为例外处理程序预留的一些内存
空间。然后您就可以保存东西,必要时还可以用另一个来访问控制寄存
器。
通过Cause 寄存器,您可以找出例外的类型,再分别处理。
² 从异常处理返回: 控制最终必须返回到刚近入例外时保存的EPC指向的
地方。不管发生的是什么例外,您返回时都要把SR 寄存器设置会原来的
值,恢复用户特权级设置,使能中断,也就使要消除例外的影响。
在R3000 中特殊指令rfe 做这件事情,但是请注意它本身并不转移控制
流。要跳回去,您要把原来的EPC 值装到一个通用寄存器,然后用一个
jr 操作。
在R4000 和目前为止所有的64位CPU中,“从例外返回”指令eret 接合了
返回到用户空间和重新设置SR 寄存器两个功能。
严格地说, CP0 指令集,包括rfe 和eret ,都是实现相关的。但没有一
个CPU 用了第三种方法来做这个事情,假定以后也没有人会是相当安全
的。然而,以后您可能会看到一个32位的CPU ,它的CP0 设计是基于
R4000 的5。
² 中断: SR 用来调整中断掩码,即决定哪些(如果有的话)中断被赋于比
当前优先级更高的优先级。硬件没有提供中断优先逻辑,但是软件可以
随便干。
² 总是触发例外的指令: 这些指令很常用(系统调用,断点以及模拟一些指
令等)。所有的MIPS CPU 都实现了break 和syscall ;有一些还实现了
额外的一些指令。
4译者:指TLB refill 例外,实际上后来的CPU 还有几个特殊入口,不过用得不多,可以不管
5译者:龙芯-1就是,呵呵
6
控制寄存器编码: 关于保留域的一个说
明现在有必要了。许多不用的控制寄存器域
被标记为“0”;在这样的域里的位保证读出为
0,写它也没有什么害处(虽然写入的值会被
丢弃)。另一些被标记为“x”;您应该小心,
保证总是写入0,而且不应该假设读回的值
是0 或者其它任何特殊值。
3.3 标准CPU 控制寄存器编码
这一节告诉您控制寄存器的格式以及各个域的一个概要功能描述。多数情
况下,关于这些东西如何工作的更多内容在后面几节可以找到。但我们把有关
存储管理的寄存器留到第6 章。
3.3.1 处理器ID(PRId) 寄存器
图3.1 显示了PRId 寄存器的内容。它是一个标志CPU 类型的只读寄存
器。只要指令集或者控制寄存器定义发生改变,“Imp” 就会改变。“Rev” 完全
取决于制造者,只是用来帮助CPU 厂家跟踪芯片版本,用做其它任何用途都
是不可靠的。我们所知道的一些设置列在表3.2 中。
如果您想打出这些值,打成“x.y”的形式比较方便(其中x,y 分别为Imp 和
Rev 的十进制值)。尽量不要依赖这个值来获得一些参数(例如高速缓存大小,
速度等等)或者获得某项特性是否存在的信息;用一些代码序列来探测各种特性
的存在性,它将使您的软件更加可移植和健壮。很多情况下您会在本书找到(关
于探测的)例子或者建议。
3.3.2 状态寄存器(SR)
MIPS CPU 有少数几个模式位,它们在状态寄存器中定义,如图3.2 。我
们显示了“标准”的R3000 和R4000 CPU 的寄存器定义;其它CPU 偶然也用其
它域,或者改变一些域的含义,通常它们并不实现所有的域。
我们再次强调, MIPS CPU 里没有“nontranslated”(不经过TLB地址翻
译)或者“noncached”(不缓存)模式;所有的是否翻译,是否缓存都由程序的
地址决定。
绝大部分MIPS CPU 都提供R3000 和R4000 所公有的那些域。
R3000 和R4000 公有的关键域
这是关键的公有域;把这些域重用为其它任何目的都是非常不好的想法,
在可以预见的将来这些域的用法很可能都不会变化。
CU1 协处理器1 可用:如果有浮点处理部件的话,设成1 表示可以使用它;0
表示禁止使用。当值为0 时,所有浮点指令导致例外。没有浮点硬件时把
它设为1 显然不行;但有浮点硬件时(用0 )关掉它有时会有用。6
6为什么要关掉一个好好的浮点部件呢?有些操作系统对所有的新任务禁止浮点指令;如果该任
务试图使用浮点时,操作系统会捕获到例外并为它使能浮点部件。这样,我们可以分出那些从不使
用浮点的任务。在任务切换时,我们不需要为那些任务保存和恢复浮点寄存器,这样可以节省上下
文切换的时间。
7
31 16 15 8 7 0
reserved Imp Rev
图3.1: PRId 寄存器各个域
表3.2: MIPS CPU 的RPID(Imp) 值
CPU 类型Imp 值
R2000 1
R3000,IDT R3051,R3052,R3071,R3081. 绝大多数是早期32位MIPS CPU 2
R6000 3
R4000,R440 4
一些LSI Logic 的32位CPU 5
R6000A 6
IDT R3041 7
R10000 9
NEC Vr4200 10
NEC Vr4300 11
R8000 16
R4600 32
R4700 33
R3900和其变种34
R5000 35
QED RM5230,RM5260 40
位31 和30 分别控制协处理器3 和2 的可用性;可能被一些想定义更多指
令的CPU 使用。CP2 指令可能出现在一些(用于SOC 的?)处理器核的实
现中。
BEV 启动时例外向量:当BEV==1 时, CPU 用ROM(KSEG1) 空间的例外
入口(参见5.3节)。正常运行中的操作系统里,BEV 一般设置为0 。
IM 中断屏蔽:8 位,定义那些中断源有请求时可以触发一个例外。八个中断
源中6 个是CPU 核外面的信号产生的(其中一个可以被浮点部件使用;它
虽然在片上,逻辑上是外部的);其它两个是Cause 寄存器中软件可写的
中断位。
有浮点部件的32位CPU 用CPU 中断之一来发出浮点例外7;MIPS III和
以后的处理器协处理器0 中通常有个内部时钟,时钟中断信号通过最高的
中断位来发出。其它情况,中断从CPU 片外发出。
这里没有为您提供中断优先逻辑:硬件对所有中断位一视同仁。详细信
息参见5.8 节。
7译者:也不尽然,龙芯-1就不是这样
8
R3000(MIPS I) 状态寄存器
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 0 1 2 3 4 5 6 7 8
0 CU1CU0 0 RE 0 BEV TS PE CM PZ SwC IsC IM 0 KUo IEo KUp IEp KUc IEc
R4000(MIPS III) 状态寄存器
0 CU1CU0 RP FR RE 0 BEV TS SR 0 CH CE DE IM KX SX UX KSU ERLEXL IE
图3.2: status寄存器各个域
不那么明显的公有域
这些域比较生僻,通常不用,但不好随便改变,因此目前为止都一致。
CU0 协处理器0 可用:设成1 允许用户态下使用一些特权指令。您不会想这
么干的。协处理器0 的指令在内核态总是可用的,不管这个位设为什么
值。
RE 反转用户态下的尾端设置: MIPS 处理器可以在复位时配置为任何一种
尾端(如果不明白什么意思请参见11.6 节)。由于人们总是很固执,现在
MIPS 实现分成了两个世界: DEC 和Windows NT 是小尾端的; SGI 和
它们的UNIX 世界是大尾端的。嵌入式应用最初显得倾向于大尾端,但
现在已经彻底混淆了。
这个“世界”的操作系统能运行另一个“世界”来的软件可能会是一个有用
的特性; RE 位使得这成为可能。当RE 设为1 时,用户态的软件运行起来
就好象CPU 是配置为相反的尾端一样。然而,真的达到跨世界运行会需
要软件上很大的努力,到目前为止还没有干过。
TS TLB 关闭:详细的信息参见第6 章。如果一个程序地址同时匹配两个TLB
表项(这是操作系统软件出了某种严重错误的标志), TS 位就会被置1 。
在一些实现中,在这种状态下继续操作有可能导致内部竞争损坏芯片,
所以TLB 停止匹配任何地址。TLB 关闭是一个终结性的过程,一旦置
上只有硬件复位才能清除。
一些MIPS CPU 的TLB 硬件可以防止出现这种情况,因而可能并不实现
这一位。
在IDT R3051 系列CPU 中,您可以在硬件复位后查看这一位,它当且仅
当CPU 没有TLB(存储管理硬件)的时候置位。但这种测试并不是总是可
靠的(即有些硬件实现可能并不是这样做)。
状态寄存器中R3000 专有的域:日常使用的
Swc,IsC 交换高速缓存和隔离(数据)高速缓存:这些是为了高速缓存管理和诊
断用的高速缓存模式位;详细信息参见4.9 节。简单地说,当SR(IsC)
置位时,所有的load 和store 只访问高速缓存,绝不访问内存;在这种模
式下一个部分字的store 操作将使相应的高速缓存表项无效。
当SR(SwC) 置位时,指令高速缓存和数据高速缓存的角色互换,这样
您可以访问和无效指令高速缓存的内容。
9
KUc,IEc 这是两个基本的CPU 保护位。
以内核优先权运行时, KUc 设成1 ,用户模式下设成0。在内核模式下
您可以访问整个程序地址空间以及使用特权(协处理器0 )指令。在用户
模式下您只能存取0—0x7FFFFFFF 之间的程序地址,不能使用特权指
令;试图违反规则会导致例外。
IEc 设置为0 阻止CPU 响应中断,1 使能中断。
KUp,IEp 上一个KU,上一个IE:例外时,硬件把KUc 和IEc 的值保存在这
儿,再把它们设置为[1,0] (内核模式,禁止中断)。rfe 指令可以用于把
KUp,IEp 拷贝回KUc,IEc 。
KUo,IEo 老的KU,老的IE:例外时,硬件把KUp,IEp 的值保存在这儿。效果
上这六个KU/IE 位构成了一个三项每项两位的栈,例外时压栈,rfe 时弹
栈。这个过程在第5章描述并展示在图5.1 中。
如果在一个例外处理程序保存SR 寄存器之前又发生了例外,这种机制就
使得我们有可能干净地处理嵌套的那个例外。这种情况下能做的事情是
很有限的,很可能它只是对把TLB 重填的代码写短些有用;更多的信息
请参见6.7节。
生僻的R3000 专有位
PE 当一个高速缓存奇偶校验错误发生时置位。这种情况下不发生例外,只是
对诊断问题有用。之所以MIPS 体系结构有高速缓存诊断的设施是因为早
期的CPU 使用片外的高速缓存,而高速缓存总线上的信号时序已经接近
当时工艺水平的极限。对那些实现来说,高速缓存的奇偶校验位是很必
要的调试工具。
对拥有片上高速缓存的CPU 来说,这个特性很可能已经过时。
CM 这显示了数据高速缓存“隔离”以后最后一个load 操作的结果(关于“隔
离”的意思,请参见IsC 位的解释或者4.9.1 节)。如果高速缓存真的包含
被访问地址的数据(也就是说,即使数据高速缓存没有被“隔离”,访问也
将命中),那么CM 将被置位。
PZ 当它置位时,高速缓存奇偶校验位被写为0 ,不再进行奇偶校验。这是使
用片外高速缓存的CPU 用的老古董了。它可以让有信心的设计者省去保
存奇偶校验位的外部存储器,节约一点钱。如果CPU 有片上高速缓存,
您用不着这一位。
R4x00 CPU 中常见的域
请记住,这些域原则上是完全CPU 相关的;然而, MIPS III 以上的CPU
都有很多相同的地方。
FR 一个模式开关:设成1 使得所有32个双字大小的浮点寄存器对软件可见;
设成0 使它们象在R3000 上那样工作8。
8译者: 32 位MIPS 处理器用一对32 位寄存器来存一个双精度浮点数,参见第7 章
10
为什么有个管理态呢? R3000 CPU
只提供两个特权级,这已经能满足绝大部分
UNIX 实现的要求,也是任何MIPS 操作系
统真正用到过的。那么为什么R4000 的设计
者要费这功夫去设计一个从来没有人用过的
特性呢?
在1989-90 年的时候, MIPS 最大的成功之
一就是在DEC 公司的DECstation 产品线上
使用了R3000 CPU , MIPS 公司想让R4000
被选为DEC 将来的工作站的CPU 。竞争
者是DEC 公司内部开发的后来发展成Alpha
体系结构的CPU ,但那是从后面赶上来
的; R4000 大概比Alpha 早18个月面世。不
管DEC 选择什么CPU ,它必须不仅能够运
行UNIX ,而且要能运行DEC 的小型机操
作系统VMS ;而显然VMS 的体系结构设计
师声称只有两个特权级不可能实现VMS 。
Alpha 的基本指令集和MIPS 几乎完全相
同;它的最大不同是试图取消子字存取操
作,后来的Alpha 指令集又重新加回了那些
指令。
最后,看起来VMS 软件组选择了Alpha 而
不是R4000 ,因为它坚持认为某些指令集和
CPU 控制结构的不同会使得移植到R4000 慢
很多。我很怀疑这个说法(and put the choice
down to NIH(not invented here)—不会翻。
DEC 相信控制它自己的处理器开发很重要,
这很可能是对的,但猜猜如果DEC 采用了
R4000 事情会怎样发展也很有趣。
我也怀疑卖出的基于Alpha 的VMS 几乎可
以忽略,但那是另一回事了。
SR 发生了软复位: MIPS CPU 提供了几个不同等级的复位,用硬件信号区
分。SR(SR) 域在硬复位(这时所有的参数都重新设置)后被清掉,在一个
软复位或者不可屏蔽例外后置位。特别地,配置寄存器Config 在软复位
期间维持原值,但硬复位后必须重新编程。
DE 禁止高速缓存和系统接口的数据检查:一些硬件系统可能没有在高速缓存
重填的路径上提供奇偶校验(虽然硬件设计者可以选择把返回给CPU 的数
据标记为没有校验位—这很可能是更好的方法9,这时您可能要设置这一
位。对没有实现高速缓存奇偶校验的CPU ,您也应该设置这一位。
UX,SX,KX 这些用于支持R3000 兼容的和一些扩展的地址空间:三个不同
的特权级各有一位;当相应的位置位时,最常见的内存地址翻译例外(即
TLB 不命中例外)被重定向到不同的入口,那里的软件将处理64位的地
址。
同时,当SR(UX) 置成0 时CPU 将不在用户态下运行MIPS III 中的64
位指令。
KSU CPU 特权等级: 0 是核心态,1 是管理态,2 是用户态。不管这个域是
什么值,只要EXL 或者ERL 被例外置位了, CPU 就自动处在核心态。
管理态是R4x00 引入的,但从来没有被用过。(猜测的)原因可以参见边
栏。
ERL 错误级:当CPU 响应一个奇偶校验或者ECC 校验错误例外时被置位。
之所以这个要用一个单独的位是因为一个可以纠正的ECC 错误可以在
任何地方发生—包括最敏感的一般例外处理代码—如果系统想修正ECC
9译者:大概是指这样高速缓存部件可以自动禁止检查奇偶检验
11
31 30 29 28 27 16 15 8 7 6 2 1 0
BD0 CE 0 IP 0 ExcCode 0
图3.3: Cause 寄存器各个域
错误并继续运行,它必须不管例外发生在哪里都可以修复。这是有挑
战性的,因为例外处理程序没有一个可以安全使用的寄存器;而没有
一个寄存器用做指针,它就无法开始保存寄存器。为了跳出这个死圈,
SR(ERL) 有一个很彻底的效果;所有对正常用户地址空间对访问消失
了,从0 到0x7FFF.FFFF 的地址变成一个映射到相同物理地址的不经
过高速缓存的窗口。目的是高速缓存错误例外处理过程可以用0 号寄存
器(值永远为0 )来做基地址,用基址+偏移的方式来获得一块可以用来保
存寄存器的内存空间。
EXL 例外级:被任何例外置位,这强制进入核心态并禁止中断;目的是把
EXL 维持足够长的时间以便软件决定新的CPU 特权级和中断屏蔽位该设
成什么。
IE 全局的中断使能位:请注意不管这怎么设, EXL 或ERL 总是禁止所有的
中断。
R4x00 CPU 里的CPU 相关域
RP 减小功耗:降低CPU 的操作频率,通常是把它除以16 .在很多R4x00
CPU 里这不起作用;即使起作用,它也要求系统接口也能对付这种要
求。具体情况请阅读CPU 手册,咨询系统设计人员。
CH 高速缓存命中指示:只用于诊断。
CE 高速缓存错误:这只对诊断和错误恢复过程有用,错误恢复也应该依赖
ECC 寄存器里的内容而不是这。
3.3.3 原因寄存器(Cause)
图3.3 显示了Cause 寄存器各个域,这是您想找出发生了什么例外,决定
如何处理时应该看的东西。Cause 寄存器是例外处理的一个关键寄存器,在我
所知道的MIPS CPU 中定义都一样,只是其中例外类型的列表有所增长。
BD 转移延迟:EPC 寄存器作用是保存例外处理完之后应该回到的地址。正
常情况下,这指向发生例外的那条指令。但是如果发生例外的指令是在
一条转移指令的延迟槽里,EPC 得指向那条转移指令;重新执行转移指
令没有什么害处,但如果您返回到延迟槽指令,转移指令将没法跳转从
而这个例外将破坏程序的执行。
Cause(BD) 只当发生例外的指令在转移指令延迟槽时置位。如果您想分
析发生例外的指令,只要看看Cause(BD) (如果它为1 ,那么该指令是
EPC+4 )。
12
CE 协处理器错误:如果例外是由于一个协处理器格式的指令没有被相应的
SR(CUx) 位使能引起的,那么Cause(CE) 保存这条指令的协处理器
号。
IP 待决的中断:展示想要发生的中断。第7到2 位随着CPU 六个中断输入的
电平变化。第8位和第9位可读可写,保存您最后写入的值。当这8 位任何
一个活跃而且被SR(IM) 位和全局中断标志SR(IEc) 使能时,一个中断
将被触发。
Cause(IP) 和Cause寄存器其它域有微妙的不同:它不是告诉您当例外
发生时发生了什么事情,而是告诉您现在正在发生什么事情。
ExcCode 这是一个5位的代码,告诉您哪种例外发生了,如表3.3所示。
3.3.4 例外返回地址(EPC)
这只是一个保存例外返回点的寄存器。一般等于导致(或者遭受)例外的指
令地址,除非Cause 寄存器的BD 位置位了—这种情况下EPC 指向前一条(转
移)指令。如果CPU 是64 位的那么EPC 也是。
3.3.5 无效虚地址寄存器(BadVaddr)
这个寄存器保存引发例外的地址;在任何MMU 相关的例外里设置,原因
包括一个用户程序试图访问kuseg 以外的地址,或者地址没有正确对齐。在其
它任何例外之后它的值没有定义。请注意,特别地,总线错例外并不设置它。
如果CPU 是64 位的那么EPC 也是。
13
表3.3: ExcCode 值:不同种类的例外
值助记符描述
0 Int 中断
1 Mod TLB 修改:试图写一个经过TLB 映射的程序地址,但
TLB 表项说那是只读的—译者:原书似乎有误。
23
TLBL
TLBS
TLB load/TLB store:读/写使用的程序地址在TLB 里
没有匹配的项。这个例外有一个专门的入口,用来处理
大部分的地址翻译(它们就是从R3000 到R4000 的改变中
获得特殊对待的例外)。
45
AdEL
AdES
地址错(分别是取指/ load 操作和store 操作引起):要么
是在用户态下试图访问kuseg 以外的段,或者是试图访
问一个双字、字或者半字而地址不相应对齐。
67
IBE
DEB
总线错误(分别是在取指或读数据时发生):外部硬件指
示发生了某种错误;您该怎么做是系统相关的。存数操
作引起的总线错只能间接地反应出来,表现为读入想写
的高速缓存块时的结果。
8 Syscall 由一个syscall 指令无条件产生。
9 Bp 断点:由break 指令产生。
10 RI 保留指令:一条本CPU 没有定义的指令。
11 CpU 协处理器不可用:一种特殊的未定义指令例外。指令属
于某个协处理器或者协处理读写指令。特别地,这是当
浮点部件可用位SR(CU1) 没有置位时浮点指令引起的
例外,因此它也就时浮点模拟开始的地方。
12 Ov 算术溢出:请注意无符号类的指令(如addu )从不引起这
个例外。
13 Trap 这个来自MIPS II 新增的条件陷阱指令。
14 VCEI 指令高速缓存中的虚地址一致性错误:这个只和有二级
高速缓存并且使用二级高速缓存的tag 位来检查高速缓
存别名的R4000 以后的CPU 相关。4.14.2 节有相关解
释。
15 FPE 浮点例外:只在MIPS II 和它以上的CPU 中发生。在
MIPS I CPU 中,浮点例外作为中断发出。
16 C2E 协处理器2 例外:还没有一个R4x00 CPU 有协处理器2
,所以不必管它。
17-22 - 预留作将来的扩展。
23 Watch load/store 的物理地址和WatchLo/WatchHi 寄存器
中的值匹配。
24-30 - 预留作将来的扩展。
31 VCED 数据虚地址一致性错误:和VCEI 一样。
14
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
CM EC EP SB SS SW EW SC SM BE EM EB 0 IC DC IB DB CU K0
图3.4: Config 寄存器各个域
3.4 R4000 以后的CPU 专有的控制寄存器
R4000 (第一个实现了64 位MIPS III 指令集的CPU )是一个相当大胆的尝
试。它试图把当时已经有些控制不住的各种实现方式规则化,并给一些不可避
免的特色功能(的实现)提供一个规则的结构。
最明显的改变是高速缓存现在是由一条叫cache 的新指令控制(实际上是
一组指令);其它而外的特色功能包括CPU 内带的时钟,一些调试设施和处理
高速缓存的可恢复位错误的机制。同时提供一个Config 寄存器来允许一些关
键特性的参数化(高速缓存总容量, cache 行大小等),软件可以通过它来进行
相应控制。
我们将在第4 章介绍那些只用于高速缓存管理的寄存器,在第6 章介绍
MMU/TLB 寄存器。
3.4.1 Count/Compare 寄存器: R4000 时钟
这些寄存器提供了一个简单的连续运行的通用时钟,可以编程来发出中
断。在大部分的CPU 里,这个时钟是不是连线到一个中断是复位时的一个选
项。时钟中断总是使用Cause(IP7) (通常这使得硬件输入Int5*多余了–译者:
应该是不能用了?)。
Count 是一个32位的计数器,它精确地以CPU 流水线频率的一半向上
加(即每两拍加1) 。当它达到最大的32位整数值时,直接溢出回0 。您可以读
Count 寄存器来获取当前时间。您也可以随时写Count 寄存器,但实践上还
是不这么做为好。
Compare 32位可读可写的寄存器。当Count 寄存器增长到等于Compare
寄存器时,中断就回发出。这个中断一直维持到下一个对Compare 寄存
器的写为止。
要产生一个周期的中断,中断处理程序应该总是用一个固定数量来递增
Compare 寄存器(不是Count ,因为那样的话中断处理的延迟会稍微增加周
期时间)。软件需要看看一个中断是不是来迟了,以避免把Compare 寄存器设
置成一个Count已经经过的值。通常,它写完Compare 后再重新读Count
以检查这个问题。
3.4.2 Config 寄存器: R4x00 配置
CPU 配置毫无疑问地是CPU 相关的,但所有R4x00 家族地成员都有
Config 寄存器并共享其中的许多域。图3.4显示了最初的R4000 CPU 提供的标
志位集合。
图3.4中的域如下:
15
CM 设为1 表示主设备/检查器(?) 模式—只用于容错系统。在复位时设置,
只读。
EC 三位,用于表示时钟分频:内部流水线时钟和系统接口时钟的比率。在一
些CPU 里,系统接口时钟等于输入时钟,然后这作为乘数(倍频后)提供
给内部时钟;在老一些的CPU 里,流水线频率总是等于输入时钟的两
倍,然后这作为被除数(分频后)算出系统接口时钟。
对于R4000 ,当这个域的值等于n 时,比率是(n+2) 。但后来的CPU 中
诸如1.5 和2.5 这样的比率使得编码不得不改变。请参照具体的CPU 手
册。
这个域(到目前为止)在复位时设置,只读。
EP 四位,用于表示数据传输模式。R4000 和以后的许多处理器的系统接口都
没有为高速缓存写回时的多块数据传输提供外部握手信号。CPU 能够每
拍发送一个宽度等于总线宽度的数据。因为这有时对接口来说太快了,
所以数据传输的速率和节拍在这里编程控制。
下面的表显示“D” 时表示一个发送了一个数据字的拍,显示“x” 时表示
系统接口歇一个时钟周期。
EP值数据模式EP值数据模式
0 D 8 Dxxx
1 DDx 9 DDxxxxxx
2 DDxx 10 Dxxxx
3 Dx 11 DDxxxxxxx
4 DDxxx 12 Dxxxxx
5 DDxxxx 13 DDxxxxxxxx
6 Dxx 14 Dxxxxxx
7 DDxxxxx 15 DDxxxxxxxxx
短的模式必要时重复,因此一个8个字(4个双字)的高速缓存块,当Con-
fig(EP) 等于5时将是“DDxxxxDD”。(还是正确的写法是“DDxxxxDDxxx”
,表示总线上有三个空闲周期?)我们的经验是许多CPU 在写结
束是不实现无用的周期(dead time),但有一些确实这样做了。如果这对
您很重要,去问您的CPU 提供商吧。
大部分CPU 只支持这些值的一个子集。有些使用不同的编码。这个域有
时在复位时设置并只读,有时是可编程的。
SB 片外二级高速缓存块大小(或者说行大小)。这个域通常由硬件决定,是只
读的。R4000 的编码是:
SB值块大小(32位字)
0 4
1 8
2 16
3 32
16
SS 在R4000 CPU 中,片外的二级高速缓存可以是分立的(指令和数据分别使
用不同的高速缓存位置,不管地址是什么)或者统一的(指令和数据根据地
址统一对待)。1 表示分立,0 表示统一。
SW 在R4000 (也许还有其它)CPU中,如果二级高速缓存是和原始的R4000SC
一样位128 位宽则设置为1 ,0 表示64 位宽。
EW 系统接口宽度:0 表示64 位,1 表示32 位。
SC 在R4000 和R5000 以及它们的直接后代中,这个域是可写的,作为软件控
制的二级高速缓存使能位;它对诊断问题非常有用。如果有一个片上控
制的二级高速缓存,这个设置为1 ,否则为0 。
后来的一些带二级高速缓存的单处理器在另外的域(利用R4000 用于多处
理器的一些域)中报告二级高速缓存的大小。然而,通常这些大小域的值
只是机械地传递加电配置时收到的信息,并没有硬件上的影响。10
SM 多处理器高速缓存一致性协议配置。
BE CPU 尾端(参见11.6 节): 1 表示大尾端,0 表示小尾端。在(至少) NEC
Vr4300 这个域时软件可写的,但在大部分CPU 上它是硬件配置的一部
分。
EM 数据校验模式: 1 表示ECC 校验,0 表示每字节的奇偶校验。
EB 一定是0 。曾经想提供一个硬件接口选项来用顺序次序进行高速缓存重填
和写回操作,而不是子块次序;这个选项从来没有实现过。
IC/DC 一级指令/数据高速缓存的大小:一个二进制值n 表示高速缓存大小
为212+n 字节。
IB/DB 一级指令/数据高速缓存的行(块)大小:0 表示4x32 位字, 1 表示8x32
位字。
CU 另一个多处理器高速缓存一致性协议配置位。
K0 这是一个可写的域,用来配置KSEG0 段访问的高速缓存行为。使用的编
码和MMU 表里控制每一页的缓存行为用的EntryLo(c) 位一样。除了
多处理器一致性用的值之外,我们感兴趣的只有3 =缓存, 2 =不缓存。
R4000 以后的不提供多处理器高速缓存支持的CPU 已经用一些其它值来
配置不同的高速缓存行为,例如写穿透和写分配—它们的含义请参见4.3
节。
3.4.3 Load-linked Address (LLAddr)寄存器
这个寄存器保存最近运行的load-linked 操作的物理地址,用来监控可能
导致后来的一个条件存(store conditional) 失败的访问;参见5.8.4 节。软件对
LLAddr 的访问只用于诊断目的。
10译者:我不明白这一段和SC 有什么关系。
17
31 3 2 1 0
MatchAddr[31..3] 0 RW
图3.5: WatchLo 寄存器各个域
3.4.4 调试观测点(WathLo/WatchHi) 寄存器
这对寄存器实现了一个观测点:它们包含一个物理地址,每个读数据或者
存数据操作都跟它比较,如果地址匹配则发生一个陷阱例外。目的是给调试软
件提供帮助。
WatchLo 显示在图3.5中。观测点的地址只维护到最近的双字( 8 字节),
所以只有第三位以上的地址位需要保存。WatchHi 保存地址高位。其它
的WatchLo 位如下:如果WatchLo(R) 等于1 ,读操作参与检查,如果
WatchLo(W) 等于1 ,存数操作参与检查。您完全可以同时使能读操作和存
数操作的检查。
有些调试器使用硬件观测点,有些则不用。提供观测点(有时叫数据断
点)功能的调试器通常允许您设置任意多个这类断点,很可能只有您指定的调试
点正好是一个时才会使用WatchLo/WatchHi 寄存器。
第四章 Cache for MIPS
没有Cache的MIPSCPU不能称为真正的RISC。可能这样说不公平。但为了一些特殊的目的,你可以设计一个含有小而紧密内存的MIPSCPU,而这些内存只需要固定个数的流水线步骤(最好是一个)就可以被访问到。但绝大部分MIPS CPU都是含有cache的。
这一章将介绍MIPS的cache怎样工作和软件应该怎么做才能使它可以被使用而且是可靠的。MIPSCPU重新启动后,cache的状态是不确定的,所以软件必须非常小心。你有一些线索知道cache的大小(如果你直接知道cache的大小后去初始化,这是一个不好的软件习惯。)。对于诊断程序员,我们将讨论怎样测试cache和获取特殊入口。
对于实时应用程序的程序员,希望在CPU运行时能够正确地控制cache。我们也将讨论怎么做,虽然我对使用一些窍门方式有怀疑。
当然这些也随着MIPSCPU的发展而进步。对于早期的32位MIPS处理器,初始化cache或者使其无效,首先让cache进入一种特殊的状态,然后通过普通的读写操作来完成。对于后来的处理器,一些特殊的指令被定义出来做这些相关的操作。
4.1 cache和cache的管理
cache的工作就是将内存中的一部分数据在cache中保留一个备份,使这些数据能一个固定的极短的时间内被快速的存取并返回给CPU,这样能保证流水线的连续运行。
绝大部分MIPSCPU针对指令和数据有其各自的cache(分别称为Icache和Dcache),这样读一条指令和一个数据的读操作或者写操作就能同时发生。
老的CPU家族(象x86)为了保证被写入CPU的代码的一致性,所以没有cache。现在的x86芯片拥有更灵活的硬件设计,从而保证软件没有必要从更本上了解cache(如果你正在装一台机器跑MS/DOS,它将在本质上提供一致性)。
但因为MIPS机器有各自的cache,所以就没有必要那么灵活。cache对于应用程序来说必须是透明的,除了除了能感觉到运行速度的增加。但对于系统程序或者驱动程序,拥有cache的MIPSCPU并没有尝试cache对它们也是透明的。cache仅仅使CPU跑得更快,而不能给系统程序员有所帮助。在象Unix一类的操作系统中,操作系统能对应用程序完全隐藏cache,当然对于更多不能的胜任的操作系统,其也能很好的隐藏大部分cache的处理,但你可能必须知道在什么时候需要调用适当的子程序来对cache做一些必要操作。
4.2 cache怎样工作
从概念上讲,cache是一个相连内存(associative memory),当数据被写入时用数据的一部分作为关键字来标志的一块存储区域。在cache中,关键字是整个内存的地址。提供一个相同的关键字给相连内存,你将得到相同的数据。一个真实的相连内存在存入条目时,将完全按照它们的关键字,除非它已经满了。然而,由于需要这个当前的关键字必须和所有被存的关键字同时比较,因此任何大小的真实相连内存不是效率低或速度慢,或者就是两者都有。
怎样我们才能设计有用的高速缓存,使其不仅效率高而且速度快呢?图4.1展示了一种最简单高速缓存的基本设计方案,直接映射(direct-mapped)高速缓存。它被1992年以前的MIPSCPU广泛使用。
直接映射cache由许多块简单的高速缓存排列构成(通常每一块称之为一line),通过地址低位在整个范围内做索引。cache的每一条line都包含一个字或者几个字的数据和一个标签(tag)区域,tag记录着数据所在内存的地址。
当一个读操作时,每一条line都可以被访问到,tag将和内存地址的高位做比较;如果匹配的话,我们知道是找到正确的数据了,这被称之为命中(hit)。如果在这一块中有超过一个字的数据,对应的那个字的数据通过地址的最低几位来选择出来。
如果tag没有匹配,这称之为没有命中(miss),那么数据需要从内存中读入,然后复制到cache对应的line中。这对应line中原来的数据将会被抛弃,如果CPU又需要被抛弃的数据时,需要再次从内存中取得。
这样的直接映射cache有一个特征,就是对于任何一个内存地址,在高速缓存中只有唯一的一条line可以用来保存其数据。这样有好处也有坏处。好处就是这样的架构简单,可以使CPU跑得更快。但简单也有其不好的一面:如果你的程序要不停地交替使用两个数据,而它们刚好要对应高速缓存中的同一块(可能是它们对应内存地址的低位刚好一样),这样这两个数据就会不停的将对方替换出高速缓存,以至高速缓存的效率被彻底的降下来。
而真正的相连内存将不会遇到这样的折腾,但对于任何合理大小,它将是难以想象的复杂、昂贵和速度缓慢。
折衷的办法就是使用two-way set-associative cache,其实就是两个direct-mapped cache并联,在它们中同时匹配内存位置。如图4.2。这时对应一个地址将有两次机会命中。Four-way set-associative cache (就是有四个直接映射的子高速缓存)在cache的设计中也是很平常的。但是这是有惩罚的。一个set-associate cache比起直接映射cache来需要更多的总线连接,所以cache太大以至于很难在一块芯片上构造直接映射。
不过也有巧妙的地方,由于直接映射cache对于你需要的数据只有唯一的候选者,所以把一些东西放到tag匹配前运行是可能的(只要CPU不做和着个数据有关的操作)。这样可以提高每一个时钟利用率。
由于当运行一段时间后cache会被装满,所以当再次存放从内存读来的数据时,就会抛弃一些cache内原有的数据。如果你知道这些数据在cache和内存中是一致的,那么你可以直接把cache中的备份抛弃;但如果cache中的数据更新的话,你就需要首先把这些数据存回到内存中。
这就给我们带来一个问题,cache怎样处理写操作?
4.3 Write-Through Caches in Early MIPS CPUs
CPU不能仅仅是读数据(就象上面的讨论),它们也要写数据。由于cache只是将主存中的一部分数据做一个备份,所以有一个显而易见的方法来处理CPU的写操作,被称之为Write-Through cache。
对于Write-Through cache,写操作时CPU总是将数据直接写到主存中去;如果对应主存位置的数据在cache中有一个备份,那么cache中的那个备份也要被更新。如果我们总是这样做的,那么cache中的任何数据将和主存中的保持一致,所以只要我们需要我们就可以抛弃任何一条cahce line的数据,并且除了消耗时间不会丢失任何东西。
当然这也是有危险的,当我们让处理器等待写操作结束时,处理器的运行速度将彻底的降下来,不过我们能修复这个问题。可以将要写入主存的数据及其地址先保存在另一边,然后有主存控制器自己取得这些数据并完成写操作。这个临时保存写操作内容的地方被称之为写操作缓冲区 (write buffer),它是先入先出的(FIFO)。
早期的MIPS CPU有一个直接映射的write-through cache和一个写操作缓冲区,还有一个R3000的激发设置。它在同一芯片上构造cache控制器,但需要额外的高速存贮器芯片来存贮tag和数据。只有CPU跑一些特殊的程序很平均地产生的写操作,主存系统在这种工作方式下才能很好的消化这些写操作并工作的很好。
但CPU运行速度的增长比存贮器块得多。某些时候当32位的MIPS让位给64位R4000后,MIPS的速度就已经超过存贮器系统可以合理消化所有写操作的临界点了。
4.4 Write-Bach Cache in Recent MIPS CPUs
早期的MIPS CPU 使用简单的write-through cache。后来的MIPS CPU由于速度太快而不能适用这种方法,它们会陷入存储系统的写操作中,速度慢得像爬行。
解决的方法就是把要写的数据保留在cache中。要写的数据只写到cache中,并且对应的那条cahce line要做一个标记,使我们肯定不会忘记在某个时候把它回写到内存中(一条line需要回写,称之为dirty)。
Write-back cache还可以分成几种不同的子处理方式。如果当前cache中没有要写地址所对应的数据,我们可以直接写到主存中而不管cache,或者可以用特殊的方式把数据读入cache,然后再直接写cache,后面这种方式被称之为写分配(write allocate)。用一种自私的观点来看一个程序运行在一个CPU上,写分配(write-allocate)看起来象浪费时间;但是它可以使整个系统的设计变得简单,因为在程序运行时读写内存都读或者写都是以一条cache line大小为单位的块进行操作。
从MIPS R4000 开始,MIPS CPU在芯片内拥有cache,而且都支持write-through和write-allocate两种工作模式,line的大小也是支持16byte和32byte两种。
MIPS cache的这些工作模式可以被应用到使用sillicon Graphics设计R4000和其他大型CPU,其他计算机系统也因为多处理器系统而被这些cache工作模式影响到。
4.5 Cache设计的其他选择
在上个世纪八十和九十年代针对怎样设计cache,做了很多工作和研究。所以下面还有许多其它的设计选择。
Physically addressed/virtually addressed:
当CPU在运行成熟的操作系统时,数据和指令在程序中的地址(程序地址或虚拟地址)会被转换成系统内存使用的物理地址。
如果cache纯粹地在物理地址方式下工作,将很容易被管理(我们将在后面讨论为什么)。但合法的虚拟地址可以让cache更早地开始查询匹配工作,这样可以使系统跑的稍微块一点。
但虚拟地址有什么问题呢?它们不是唯一的;当许多不同的程序在CPU不同的地址空间中运行,它们可能会共享同样的虚拟地址而使用不同的数据。当我们切换不同的地址空间时,每次都需要重新初始化cache;这种方式在很多年前被使用,可以作为针对非常小的cache的一种合理解决方法。但针对大的cahce这种方式不仅可笑而且效率低下,我们需要一块区域来辨别cache tag中的地址空间,以至我们不被它们混淆。
这儿还有其它关于虚拟地址更细致的问题:相同的物理地址可以在不同的任务中被不同的虚拟地址描述。这就会导致相同物理地址的内容会被映射到不同的cache条目中(因为它们对应不同的虚拟地址,所以会被不同的索引所选中)。这样的情况必须被操作系统的内存管理所避免掉。详细的情况将在4.14.2节介绍。
从R4000起,MIPS的主cache都使用虚拟地址索引,从而提供快速的cache索引。但对于作为标记符来标记每一个cache-line,物理地址比虚拟地址更好。物理地址是唯一的而且效率更高,因为这样的设计显示出CPU在做cache索引的同时可以把虚拟地址转换成物理地址。
line大小的选择(Choice of line size):
line的大小是对应每一个tag可以存贮多少字的数据。早期的MIPS的cache对应一个tag只能存贮一个字的数据。但对应一个tag能存贮多个字的数据更好,尤其是内存系统支持快速的burst read。现代的MIPS cache趋向于使用四个或者八个字大小的line,并且更大的第二层和第三层cache使用更大的line。
当cache miss发生时,整个一条line的数据都要从内存中获得。但很可能会取来几line的数据;一个字的cache line的MIPS CPU经常是一次就取多个字的数据。
分开/统一(Split/unified):
MIPS的主cache总是分成I-cache和D-cache,取指令时察看I-cache,读写数据时察看D-cache。(顺便说一下,如果你想执行CPU刚刚拷贝到内存的代码,你必须不仅仅要是D-cache一部分无效使这些代码数据在D-cache中不再存在,而且还要保证它们被装入I-cache)
但是不在同一块芯片上的第二层cache很少也按这种方式来分成两块。这样就没有什么真的优势可言了。除非你能针对两种cache提供分开的数据总线,但这又会需要太多的管脚。
4.6 Cache管理(Magaging Caches)
Cache系统在系统软件的帮助下,必须保证任何应用程序数据的一致性,和它们在没有cache的系统下一样,尤其是DMA I/O控制器(直接从内存中取得数据)取得程序认为已经写过的数据。
对于CISC CPU,通常都不需要系统软件对cache的帮助;因为它会花费额外的内存空间、silicon area、时钟周期来使得cache变得真正的透明。
在系统启动的时候MIPS CPU需要初始化它的cache;这是一个十分复杂的过程,下面有关于它的几点建议。但当系统启动后运行到三种情况CPU必须加以干涉。
.在DMA设备从内存取数据之前:
如果一个设备从内存中取得数据,它必须取得正确的数据。如果D-cache是write-back,并且程序已经写了一些数据,那么很可能其中一些正确的数据还保留在D-cache中而没有写回到主存中去。CPU当然不可能看到这个问题;如果CPU需要这些数据,它会从cache中得到正确的数据。
所以在DMA设备开始从内存中读数据前,任何一个将被读数据如果还保留在D-cache中,必须被写回到内存中。
. DMA设备写数据到内存:
如果一个设备要将数据存贮到内存中,要使cache中任何对应将要写入内存位置的line都无效化,这是非常重要的。否则,CPU读这些位置的数据,将得到错误的数据。cache应该在数据通过DMA写入内存之前将对应的cache line无效化。
. 拷贝指令:
当CPU自己为了后面的执行而写一部分指令到内存中,你首先必须保证这些指令会被回写到内存中,其次保证I-cache中对应这些指令的line会被无效化。在MIPS CPU中,D-cache和I-cache是没有任何联系的。(当CPU自己写指令到内存中时,这时候指令是被当作数据写的,很可能只被写到cache中,所以我们必须保证这些指令都会被回写到内存中;为什么要使I-cache无效化,这和数据通过DMA直接写入内存中要无效cache一样的原因。)
如果你的软件需要解决这些问题,就需要针对cache line的两个独特的操作。
第一个操作被称之为回写操作。CPU必须能够针对地址在cache中查找对应的cache line。如果找到,并且对应line是dirty,就需要把这条line的数据写回到内存中。
CPU增加了其他不同层次的cache(速度和大小),来减少miss的处理。所以设计者可以使内层的cache机构简单,从而使它能在很高的时钟频率上作查询。这样很显然越往内层的cache就会越小。从1998年开始,许多高速的cpu都在同一块芯片上采用第二级cache,主cache的大小变小,双重16K的主cache受到青睐。
不在同一块芯片上的cache通常都是直接映射的,因为组相连的cache系统需要更多的总线从而需要更多的管脚来连接。这还是一个值得研究的领域;MIPS R10000采用