摘抄--------See mips RUN中文版



See MIPS Run
(中文版)

 



中国Linux论坛CPU板, 中国XTRJ.ORG, 中国AKA.ORG

译者的话

我们来自 中国Linux论坛(www.linuxforum.net), 系统软件研究所(www.xtrj.org)  和  AKA信息技术组织(www.aka.org.cn) . 我们在网络上相遇;我们在网络上相知. 共同的技术爱好将我们联系在一起.

系统软件与硬件技术是不可分的. 了解和掌握一种典型的CPU技术细节才能使得对系统软件技术的理解更加深刻.

随着MIPS CPU及相关技术在中国的普及, 我们在学习的同时,将这本著名的关于MIPS CPU的书"See MIPS Run"翻译出来作为非营利性质的学习与教学使用和参考. 希望能对在此相关领域的同仁有所帮助.

我们深知, 由於自己的理论和经验的不足, 我们的工作必定会有许多不精确或错误之处. 希望读者能及时给我们指出. 我们定将及时的改进.

谢谢,

全体译者

7/18/2003


前言

这本书是关于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采用只有一个数据总线的二路组相连cache,如果命中的不是希望的那一组,通过一段延时后在返回数据来实现(两个组共用一个数据总线)。
在cache的发展过程中,产生了两类主要的软件接口来针对cache。从软件的观点来看,一类是建立在以R3000为代表的32位MIPS CPU的基础上;另一类是建立在以R4000为代表的64位MIPS CPU上的。R3000这一类型的MIPS CPU的cache是write-through,直接映射的,物理地址为索引。cache访问的最小单位是一个字,所以写一个字节(或者是写小于一个字)的操作必须被特殊的处理。在读写这一类数据是cache管理采用特殊的模式。

为什么不通过硬件来管理cache?
通过硬件来管理cache通常被称为“爱管闲事”。当另一个cpu或者是DMA设备访问内存时,被访问地址对应的内容对于cache来说是可以看到的。

4.7 第二层和第三层cache
在大型的系统中,通常需要一个嵌套的多层cache。一个小而快的主cache最接近cpu。访问主cache出现miss时,不是直接从内存中查找而是从第二层cache中查找。第二层cache在速度和大小上是介于主cache和内存之间。cache层次的数目可以通过内存速度和cpu最快访问速度比较来决定;由于cpu速度发展比内存的发展快得多,在过去的12年里桌上型电脑系统从没有cache发展到有两层cache。九十年代后期的最快cpu速度大约可以达到500MHz,拥有三层cache。




4.8 MIPS CPU cache的构造
通过观察cache采用模式和层次的发展(看表4.1),我们可以将MIPS CPU分成两类,古老的和现代的。
当时钟的速度变得越快,我们就能看到越多得cache构造,因为设计者为了应付CPU跑得速度比内存系统越来越快。为了保证运行的顺畅,cache必须提高运行速度,保证提供数据的速度比外围得存贮器要快,同时也要保证尽可能多命中。相比较R4000类型的CPU,主cache是write back类型,是write allocate ,virtually indexed,physically tagged, 二路或四路组相连的cache。
许多R4x00和其后续cpu在同一块上拥有第二层cache的控制器,1998年出现了这样的第一块cpu。
由于两种产生的不同,我们将分两节来详细介绍。

注意!一些系统的第二层cache不是由mips cpu内部的硬件来控制的,而是建立在内存的总线上。对于这类cache的软件接口将具有系统特殊性,和象这章介绍的由cpu内部控制的cache的软件接口相比,可能有很大的不同。

4.9 Programming R3000-Style Caches
MIPS R2000打破了芯片内cache控制器的基础,将cache额外的分成I-cache和D-cache。这是一个后见之明,不会让人感到惊讶,就是这样一个先驱者的冒险导致了后面很多事端。cache有一个特殊的软件访问缺点。
为了节省芯片管脚,cache将不能拥有不同的闸门来执行字节、半个字和其他小于一个字单位的写操作。所以在R2000系列中对cache执行一个小于字单位的写操作时,会回写到主存中,并将cache中这个字所在的Line无效化。这样针对cache管理,提供了一个使cache无效的方法:只用写一个字节就行了。
你可以看到支持这些简化的观点。R2000设计者提出理由小于字的操作通常用于字符操作,字符操作总是由库函数提供,而这些库函数用整个字的操作来重写。这些假设总是被认为对对错错,或者半对半错。
直到认识到不是所有系统都能用相同的函数库,而且每个字节写操作都使所在cache无效也不是一个好主意,这些争论才没有继续下去。因为这是不能被容忍的,所以出现了一个很大的改动,R3000系列的cpu通过一个RMW(read-modify-write)序列来执行小于字单位的写操作。这个RMW出现在所以的32位的mips cpu中,并增加了一个 时钟周期来作为这样一个写操作的延时。
这样cache无效的机制被带入困境;R2000因为它的奇怪习惯而有一个优点,可以通过字节的写操作来使cache无效化。而R3000 cache 需要用一个叫isolation的模式来挽救,原来这种模式只是用于cache诊断的。RMW队列因为这种模式而受到压制,在那种状态下小于一个字单位的写操作还是会让该字所处的line无效化。这是不幸的但不是悲惨(灾难)的,对于一些运行着的系统做一些事有着更有益的地方。显著的就是当cache在isolation模式时的时候,cache将没有读写操作,任何读写操作将直接和内存打交道。

4.9.1 Using Cache Isolation and Swapping
所有的R3000系列cpu的cache都是write-through模式的,这就是说cache中不会拥有比内存中更新的数据。也就是说cache中的数据从来都不需要回写到内存,所以我们只需要能使D-cache和I-cache无效就行了。
只需要不同的cache操作按照内存顺序来做cahce的管理,并且cache的管理没有必要通过特殊的内存地址空间。所以这儿有一个状态寄存器有一个SR位能够使D-cache关闭isolation模式;在这种模式下读写操作只影响着cache,读还是会命中但不管tag是不是相等。当D-cache处于isolation模式时,小于一个字单位的写操作会使对应cache Line被无效化。

CAUTION!!!
当D-cache处于isolation模式,任何读写操做不会受其对应地址或TLB条目的影响而按照非cache的情况操作。这样的结果就是cache管理程序必须保证有些数据是不可以被访问的;如果你能通过你的编译器做到很好的控制,并且能过保证所有你用的变量都保存在寄存器中,你才能在很高级别的语言中写它们。还必须保证运行这些程序时屏蔽中断。

I-cache在通常运行模式下也是完全不可访问的。所以CPU提供了另一种模式,cache交换(swapped),通过设置状态寄存器的SwC位;这时D-cache可以担当I-cache,I-cache可以担当D-cache。当cache是交换模式时,isolated的I-cache条目可以被读、写和无效化。
D-cache可以完美的充当I-cache使用(可能I-cache也可以通过初始化使之象D-cache一样工作),但I-cache不能完全的充当D-cache。这也是靠不住的,当cache是交换模式时有用,isolation却没有用。
如果你需要使用交换的I-cache来存储字单位的数据(和以前一样小于字单位的数据写操作会使该数据对应的line被无效化),你必须保证在返回到正常模式时对应的cache line必须被无效化。

4.9.2 Initializing and Sizing 初始化和判断大小
当机器启动时cache的状态是不确定的,所以这时读cache结果也是不可预知的。你也应该认识到机器重起后状态寄存器的SwC位和IsC位也是不确定的,所以在对cache读写前(即使在非cache的情况)启动软件最好能将这些状态设为可知的。
不同的MIPS CPU,cache有不同的大小。为了保证你软件的可移植性,最好能在初始化的时候计算出D-cache和I-cache的大小。这样比直接配置一个给定的值好。
下面将介绍怎样获得cache大小的值:
a. Isolated cache,让I-cache处于交换模式。
b. 在R3000系列CPU中,cache的大小可能是256K,128K,64K,32K,16K,8K,4K ,2K,1K和0.5K(K等于1024,单位是字节)。将这些可能的值n(上面那些值中的一个)写到物理地址等于它们本身的地方(有大到小)。最简单产生物理地址是用Kseg0段地址(n+0x80000000)。因为cache地址是重叠循环的,那么如果n是cache大小的倍数,那么它就会被后面小的值所覆盖。
c. 所以读物理地址零(也就是0x80000000),就能得到cache大小的值。

初始化cache,你必须保证每一个cache条目都被无效化,而且正确对应一个内存位置,所含的之值也是正确的:
a. 检查状态寄存器SR的PZ位是不是位零(为1的话,关闭奇偶位,对于同一个芯片上的 cache这不是一个好主意)。
b. isolated D-cache,并使它和I-cache交换。
c. 对于cache的每一个字,先写一个字的值(使cache的每条line的tag、数据、和奇偶位都正确),然后再写一个字节(使每条line都无效)。
不过要注意当对于每条line有四个字的I-cache,这样做效率就很低;因为只要写一个字节就足够使每条line无效了。当然除非你要经常调用这个使cache无效程序,否则这个问题是不会表现的很明显。不过如果你想根据实际情况来优化cache无效化程序,就需要在启动的时候确定cache的结构。


4.9.3 cache无效化(Invalidation)
使cache无效,按照下面的流程:
a. 计算出你使cache失效所需的地址范围。使用超过cache大小的范围是浪费时间。
b. 使D-cache孤立。一旦被孤立后你就不能读写内存,所以你必须花费所有的代价来防止异常的产生。关闭所有的中断并保证后面的程序都不会导致内存访问异常。
c. 如果你还想使I-cache失效,使cache处于交换模式。
d. 在刚才计算出的地址范围内针对每一条line写一个字节的内容。
e. 关闭cache的交换和孤立模式。
通常你应该在I-cache打开的模式下运行使cache失效的程序。这听起来是混乱和危险的,但事实上你没有必要花费额外的步骤去跑cache。一个使cache失效的程序在cache关闭的情况下运行要慢4到10倍。
当你的CPU去设IsC位时,本质上必须关闭所有的中断,因为这时是不能访问内存的。

4.9.4 测试和探察
在测试、调试或profiling时,画一个cache条目的示意图是很有帮助的。 你不能直接的读tag的值,但对于合法的line有详尽的方法得到:
a. Isolate the cache.
b. 通过每条line的起始地址从每条line里取得(低位地址匹配,高位地址包括你系统的内存的物理地址范围)。每一次读取都要参考状态寄存器的CM位,只有该位为零时,取得的tag值才是正确的。

这需要很多个计算机的周期,不过对于在20MHz的处理器,1K的D-cache对应4MB的物理内存,做整个查询就只需要几秒钟。

4.10 Programming R4000-Style Caches
R4000修改了早期cpu cache不合适的地方。但R4000成功的地方就在于cache有多种工作模式(write-back , write-allocate),以及拥有更长的line。因为有write-back工作模式,当被cpu写时每一条line都需要一个状态位来标志这条line为dirty(因此来表示很内存中的数据不同)。
对于这类cache,我们需要invalidate和write-back操作:而且还必须保证任何cpu写到cache中的数据必须被回写到内存中。
对于诊断和维护的目的,tag将更容易的被读写;R4000增加了一对寄存器TagLo和TagHi用来在cache tag和系统管理软件之间中转数据。对于R4000没有直接方式读取cache line内的数据,当然你还是可以通过cache 命中的方式来访问数据。CPU可以通过执行cache指令来从cache tag内取数据到32-bit的TagLo和TagHi寄存器中,或者是将这些寄存器的内容写到cache tag。图4.3显示这些寄存器的详细内容。
cache的地址tag存有除了用来查询cache index的其他所有位;因此主cache tag的长度会因为最大物理地址(R4x000是36bit)和用来索引cache位数不同而不同。13bit用来作为最初R4000的8KB大小主cache的索引,从此就再也没有小于这个位数。这样tag长度就有23bit,并且TagLo是24bit;在目前的cpu内TagHi总是零。这对于最小cache大小或是可以支持的最大物理地址是很重要的。对于R4000,现在TagHi是多余的;把它设为零并忘了它。
所以TagLo寄存器内包含对应cache line的tag的所有位。TagLo(Pstate)还包含状态位。在绝大多数情况下(多处理器)这将变得非常复杂,但对所有cache的管理和初始化它足够表明当Pstate为零是一个合法的值来对应一条无效的cache entry。
这个区域被后来的cpu占用,用来储存第二层cache的状态信息,但这成了一个惯例值为零是安全和合适的对于初始化。
最后,TagLo(p)是一个奇偶位,设一为整个cache tag偶校验。为全零的TagLo表明正确地偶校验。一些cpu忽视这个bit,而不去检查它,而且也没有危害。

4.10.1 CacheERR, ERR, and ErrorEPC Register:Cache ErrorHandling
CPU的cache是内存系统的至关重要的一部分,对于高效的实用或正确系统可以发现用额外的位来表明存储在这儿的数据的完整性是值得的。
内存系统的校验将首尾相接理想化地被执行下去;当数据一被产生或被传入系统校验位就会被计算,随着数据被存放,并在数据被使用前被检查。That way the check catches faults not just in the memory array but in the complex buses and gizmos that data passes through on its way to the CPU and back. 这样检查的方法将不在内存队列中抓住错误而在总线上,并通过这样的方式转送到cpu和返回。
因为这个原因,R4x000地CPU(设计应用于大型计算机)在cache里提供错误校验。和主存系统一样,你既可使用简单的奇偶校验或者使用错误纠正码(ECC)。
奇偶校验是简单的使用一个额外的bit来对应内存中每一个byte。一个奇偶错误可以告诉系统这个数据是不可靠的,并允许有些控制停止来代替creeping随机错误。奇偶校验的一个至关重要的任务就是系统开发的过程中提供巨大的帮助,因为它不能明确的指出由于内存数据完整性导致的问题。
但一个byte的废物将有百分之五十的机会有一个正确地奇偶校验,并且在72位的数据总线上的随机垃圾256次中将有一次没有被发现。一些系统可能会好一点。
错误纠错码计算起来将更加复杂,因为对于一个64位的数据将有8个bit的校验位。这将是十分的彻底,一个bit的错误将被唯一的指出并纠正,任何两个bit的错误也不会被忽视。在非常大的内存队列中ECC将从本质上排除随机错误。
因为纠错码一次可以检查整个64位的数据,所以使用纠错码的内存不能进行小于一个字的数据的写操作,被选中的小于一个字的数据必须被并入新的数据并重新计算纠错码。MIPS cpu在cache关闭的情况下需要内存系统能进行小于一个字数据的写操作,这将使事情变得复杂。内存系统硬件必须将一个小于一个字数据的写操作转变为先读然后合并,接着重新计算,最后在写入的操作序列。
对于简单的系统一般的选择是奇偶校验位,而不是其他的。让采用校验方式变成可选择的这是很有意义的,这样在设计研发过程中将有利于诊断,而在成为产品时却不用付出相应的代价。
无论检查机制是运行在内存系统中还是在R4x00的cache中,cpu将提供一个字节对应的奇偶校验位,或是对应64位的8bit纠错码,或者就是干脆没有保护。
当支持错误检测时,数据的检测位在cache填充时通常是直接通过系统接口到cache内存放而不被检查。只有在数据被使用时才检查,这样可以保证任何cache奇偶异常都被转交给引起它的指令,而不是转交给使用共同的cache line。当作一个退化的情况,一个在非cache的取指错误会被标志成cache奇偶错误,这种情况会使你很混乱。
注意,系统接口标志进来的数据为没有合法的检查位是有可能的。在这种情况下,cpu会为它内部的cache重新产生检查位。
如果一个错误发生了,cpu会产生一个特殊的错误陷阱。这个vector会直达一个非cache的位置(如果cache内是错误的数据,它会很愚蠢地去执行cache内地代码)。如果系统采用ECC,当写操作时硬件会产生纠错位,当错误时会检查得到。硬件并不知道怎么纠错;这将是软件的工作。
ERR寄存器(如图4.4)的格式如下:
a. ER/ED/ET/EE/EB: 这些位将区别是什么cache(主cache 或第二层cache,指令cache还是数据cache)发生了错误,或是它在系统接口之外。
b. PidX: 给出错误位置的cache index。你可以取得这儿的内容用于index类型的cache操作;它将得到正确的line,而不管cache是直接映射还是组相连的。

当错误发生了,ErrorEPC寄存器指向发生错误的指令位置。ERR寄存器保存着ECC位,你需要用它来纠正可以改正的错误,但在这儿我们将不再讲如何做--因为这需要很大的篇幅,你将需要密切联系处理器手册。你将可以得到一些简单的针对mips运算规则的代码。
4.10.2 The Cache Instruction cache 指令
Cache指令有着和MIPS存储指令类似的格式(拥有通常寄存器再加上十六位有符号的偏移地址),但表示数据寄存器的值会被译成选择区表示是什么cache指令。这儿没有标准的名字来表示这些cache操作;在这儿我就武断地使用来自SDE-MIPS算法库的名字,这些名字依次是基于SCI/MIPS的一个头文件(include files)。选择区不是完全的位编码,但是几乎是;看表4.2。
Cache选择区可以让你做下面这些选择:
a. 那类cache: 选择是icache还是dcache,是主cache还是第二层cache。因为没有多余的位存在,所以还没有提供的三层cache的选择。但这儿我要提醒你这是和CPU密切有关的,在R4000之后的64位CPU提供对R4000兼容性是非常有帮助的。
b. How cache is addressed:有两种不同的类型。如果是命中方式,你需要提供正常的程序地址(虚拟地址),其必须被转化。如果提供的地址确实在cache中,则该操作会对应相应的cache line被完成;如果不再cache中,那就什么也不用做。
另一种是index方式。地址低位用来直接选中某一个cache line,而不管这条line现在有什么内容。这显现出cache内部组织的没有原则性。
cache的维护通常是需要命中方式,而初始化时就需要index方式。
c. 回写(write back):如果对应的line是dirty,就将数据回写到内存中去;如果不是,就像是一条nop指令。
d. invalidate(使无效):将这条line标志为无效,使其的数据不能在被使用。同时做回写和无效是可能的;但这不是自动的,如果你需要你可以使一条dirty的line无效。所以一些应用程序可能会丢数据。
e. Load/store tags:这些操作是将对应line内的tag内容存到TagLo和TagHi寄存器中或从这两个寄存器读到对应line的tag内。
存储tag使用比较过时的方式(TagLo和TagHi寄存器要预先设为零),是cache初始化的一部分。
f. Fill:这是仅仅为I-cache设计的,这个操作通过特殊的内存地址来填充一条cache line。对于填充dcache是没有必要的,因为当cache打开的时候读取没有命中时就会达到同样的效果。
g. Create data:这类操作是能够让用户能以很高的速度来写内存的排列,而避免任何的cache重新填充。除非你能保证在数据被使用或被回写到内存中之前覆盖这些所有的数据。
这个特性对于初始化和诊断很有帮助(你将在后面初始化第二层cache的代码例子中看到,并知道如何清除第二层cache的数据)。

4.10.3 计算cache的大写和决定怎样配置
对于R4x00 CPU(和绝大多数后面的CPU)主cache大小和line的大小会同过CP0的Config寄存器可靠的给出。
但要得出你的cache是直接映射的还是组相连的却是相当的困难。但对于正在运行的cache这就不难测试出来了,你只要参考两个不可能同时出现在直接映射的cache中的地址,然后使用index类的操作去检查看它们是不是同时在cache内存在;当然如果你还没有初始化cache,这是没有用的。幸运的是你可以只写同一个程序来初始化直接映射cache或是组相连cache。

4.10.4 初始化程序
这儿将介绍一个很好的方法:
1.开辟一些内存对应任意的数据,但如果你的系统使用奇偶校验码或是纠错码,你必须保证这是正确的,并用这些数据填充cache。(在算法库程序中我们保留至少32K的系统内存一直到cache初始化的时候;只要在cache关闭的时候去写这些内存,就能得到正确的奇偶码。)还需要一个足够的空间来初始化的二层cache;我们将采用迂回的方式来处理。
2.将TagLo寄存器设为零,这样能保证对应line有效的那一位没有被置起来并且tag的奇偶码是一致的。
TagLo寄存器被cache Store_Tag指令使用,强制使对应的line无效和清除tag的奇偶码。
3.屏蔽中断,不然会有一些意外发生。
4.先初始化Icache,然后是Dcache。下面是初始化Icache的C代码。(你必须相信像Index_Store_Tag_I()这样的函数或是宏能作底层的操作;它们或是琐碎的汇编代码子函数,能够运行在相应指令的机器上,或是对应GUN C用户通过宏调用一个C嵌入汇编。)

for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
{
/* clear tag to invalidate */
Index_Store_Tag_I(addr);
/* fill so data field parity is correct */
Fill _I (addr);
/* invalidate again – prudent but not strictly necessay */
Index_Store_Tag_I();
}

5.Dcache的初始化相对来说要复杂一些,因为没有对应Dcache的Index_Fill_D操作;我们只能通过从cache读取数从而依靠通常的没有命中过程来达到目的。依次当cache填充指令对应index操作时,读取工程会依靠内存地址通过tag来命中一条line。你必须非常小心tag;对应two-way的cache,用初始化Icache的循环来初始化Dcache会将Dcache的一半初始化两次,因为清除PTagLo会重新设置用来决定下一次没有命中时是那一组cache Line的位。下面是正确的方法。

/* clear all tags */
for (addr = KSEG0; addr <KSEG0 + size; addr += lnsize)
Index_Store_Tag_D (addr);
/* load from each line (in cached space) */
for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
junk = *addr;
/* clear all tags */
for (addr = KSEG0; addr < KSEG0 + size; addr += lnsize)
Index_Store_Tag_D (addr);


4.10.5 Invalidating or Writing Back a Region of Memory in the Cache
对于对应一些I/O空间的程序或物理地址的范围,用于无效或回写的参数是不变的。
你几乎总是用命中类型的cache指令来使cache内需要的位置来使其无效或回写。如果你需要将内存一个巨大范围使其无效或回写,使用index类型的指令来使整个cache无效或回写会比较快,虽然这是一个最优化的方法,但你很可能会忽视。
我们有足够的理由这样做:

PI_cache_invalidate(void *buf, int nbytes)
{
char *s;
for (s = (char*)buf; s < buf + nbytes; s += lnsize)
Hit_Invalidate_I (s);
}

注意这儿没有必要产生特殊的地址,只要buf是程序地址就足够了,但像下面的例子,如果p是物理地址,你就必须将其加上一个常量转化成kseg0范围内的地址。

PI_cache_invalidate (p + 0x80000000, nbytes);


4.11 cache 效率
从九十年代早期cache设计在cpu同一块芯片上,高速cpu的性能很大程度上是有他们的cache系统性能决定的。现在许多的系统(尤其是嵌入式系统,需要节约cache的大小和内存的性能),CPU有50-60%的时间时在等待cache的再次填充。就这点来说将CPU性能翻倍将增加应用程序15-25%的性能。
cache性能取决于系统在等待cache再次填充的总时间。你可以将之归结为两个参数产生的结果:
a.cache没有命中的几率:是CPU(取指令或数据存取)在cache中没有命中,从而需要内存中取的比例。
b.cache没有命中需要替换造成的延时:这个延时是从内存中取数替换后到CPU的流水线继续下去的时间。

当然这没有必要很好的测量。举个例子,x86的CPU的寄存器个数很少,所以同样的程序编译给x86使用会比给MIPS使用多很多的数据存取。当然x86使用堆栈来代替寄存器给这些额外存取使用;而这堆栈位置将是内存中使用非常频繁的区域,对应cache的话使用效率会非常高。通过大块特殊的程序,我们就能获得cache没有命中的次数。
上面提到的意见将给下面指出的几种提高系统运行速度的显而易见的方法很有帮助。

a. 减少cache没有命中的几率:
1.让cache变得更大。这是最有效的,当然也算是最昂贵的。在1996年,64K的cache会占据高速嵌入CPU一半的硅面积甚至超过,所以要使cache的大小翻倍,你只有等待摩尔理论发明在相同的面积上做更多的门。
2.增加cache的组相连。值得增加到四组,再增加下去提高就几乎看不到了。
3.增加另外层次的cache。当然这将使计算变得更加复杂。除了多义子系统的复杂外,第二层cache的非命中率会被抑制很多;主cache已经可以撇去CPU重复访问数据行为的cream。为了使其物有所值,第二层cache必须比主cache大得多(一般来说是八倍或者更大),并且第二层cache的访问时间必须比内存快(两倍或者更快)。
4.优化你的程序从而减少非命中率。如果工作处于实践阶段,这就很难说清楚了。对于小的程序优化就比较简单,但对于琐碎的程序就很费力了。但迄今为止还没有人做出一个工具可以优化任何程序。看4.12节。

b. 减少cache替换的处罚:
1.加快CPU获得第一个字的数据。DRAM内存系统需要必须做许多发动工作,才能提供数据很快。使内存和CPU之间更加紧密,减短它们之间的路径,这样数据在它们之间来回才会更快。
注意这是唯一可以在便宜的系统中应用,并且效果不错。不过荒谬的是它很少被注意到,也许是因为它需要在CPU接口和内存系统设计之间考虑更多的综合因素。当CPU设计者在设计芯片的接口时不情愿处理这些问题,很可能因为他们的工作已经够复杂的了!
2.增加内存burst的宽度。这是传统上通常被应用的,是昂贵的技术,用两个或者更多的内存系统来交替存储数据;当初始化完后你就可以交替着从每一个内存系统中取数据,这样带宽好像翻倍了。第一个这样内存技术的应用是,是在1996年出现的同步DRAM(SDRAM)。SDRAM修改了DRAM的接口,提供更大的带宽。

c. 尽早的重新起动CPU:这个简单的方法是在排列cache替换的数据时,从CPU没有命中的数据开始,当数据一到就马上重新启动CPU。cache的替换可以和CPU运行并行。MIPS CPU从R4x000开始就应用这项技术,用一个子缓冲区来存放cache要替换进来的数据,并能那个字的数据是最先需要的。但只有R4600和其派生出来的CPU才体现出这项技术的好处。
激进的方式是让CPU绕过取数操作继续执行下去;取数的操作由总线接口控制器控制,CPU继续运行直到它需要的数据被存放到相应的寄存器重。这被称之为没有阻碍取数,从R10000和RM7000开始被应用。
更激进的方法是,可以执行任何后续代码,只要它不依赖于还没有取来的数据,R10000就可以不按照指令的顺序来执行。这类CPU应用这项技术非常彻底,不仅仅是应用到取数还应用到计算和跳转指令。


4.12 修改软件使Influence Cache效率更高
很多时候我们都在程序访问可知地方的基础上工作,并且我们是公平的在不强制的工作方式上操作。对于绝大多数目的我们同样可以假设,访问是恰当的随机分布式。对于工作站就必须能够支持执行足够的应用程序,这是一个公平的假设。但当一个嵌入式系统运行一个简单的应用程序,没有命中的清况好像是因为特殊的程序经过特殊的编译造成的。这是很有诱惑的,如果我们能是应用程序代码能在系统的方式下提高cache的效率。为了了解这是怎样处理的,你将cache没有命中分类,按照它们产生的原因:
a. 第一次访问:所以任何数据必须从内存中读入。
b. 替换:cache的大小是有限的,所以当你的程序没有运行多久就会出现cache没有命中,需要替换掉一部分合法的数据。当程序运行时,cache就会不停的替换掉数据然后在取。你可以通过使用大的cache和减小程序的大小来使替换和没有命中减到最少(其实就是程序大小和cache大小的比例)。
c. 从实际来讲,cache通常没有超过四路组相连的,所以对于任何程序地址在cache中最多有四个位置可以存放;对应直接映射就只有一个,二路组相连的cache就有两个(和组相连比较thrashing会丢失减少速度的可能;但绝大多数研究者建议一个四路组相连的cache在way的选择上几乎不回丢失性能)。
如果你的程序会非常频繁的使用n段空间的数据,而这n段空间的地址的低位又非常接近,那么它们就会使用相同的line。如果n大于cache的路数,那么cache的没有命中也会非常频繁,因为每一段空间的数据会不停的将cache内的其他空间的数据挤出去。

明白上面的知识,那么怎样针对程序做变化使它针对cache运行的更好?
a. 使程序更小:如果你能做到的话,这是个很好的主意。你可以使用适当的编译器优化(外来的优化通常是程序更大)。
b. 让程序中经常执行的那部分更小:访问密度在一个程序中不总是平均分布的。通常会有相当一部分代码几乎不被用到(错误处理,不明系统的管理),或者只被用到一次(初始化代码)。如果你能将这些很少用到的代码剥离出来,对于剩余的程序你就能得到一个很好的cache命中率。
资格访问的方式有利于将一些频繁使用的程序区分出来,并固定放在内存的某一位子,以减少要运行时的放置。这样至少这些经常使用的程序不会因为cache的位置而相互碰撞。
c. 强迫一些重要的代码或数据常驻cache:一些机器的能允许一部分cache保护它们所拥有的数据不被替换。这部分代码一般是中断处理或是其他自关重要的软件中确定要执行的。这些代码或数据一般被保留在二路组相连cache的其中一组中(这样当系统重其后,cache就像是一个直接映射的cache).
我很怀疑这个方法的生存能力,我也不知道有那些研究支持它的有效性。系统重起后的损失很可能大于执行那些保留代码的获利。cache上锁很可能就像一个不确定的市场工具来限死顾客对cache启发式特性的渴望。这个渴望是可以理解的,随着程序越快越复杂越大,但cache毕竟只是影响结果的一部分因数。
d. 安排程序避免碰撞:上面提到的让程序的执行部分变的更小,这对于我来说太难维护所以不是个好的主意。而且对于组相连的cache(尤其是两路的)使这个方式更加没有意义。
e. 让那些很少用到的数据和代码不经过cache:这看起来很有吸引力,让cache只给那些重要的代码或数据服务,排除那些只用一次或很少使用的代码和数据。
但这几乎总是一个错误。如果数据真的很少使用,那么它们不可能一开始就在cache中。因为cache取数时总是以一条line的长度4或16字为单位,所以即使是传送只使用一次的数据也能有很高的速度;burst替换比一个字的访问几乎不多花时间,并且能给你免费提供另外的3或5个字的数据。

简而言之,我们将介绍下面的内容作为一个起点(除非你已经有很多实践和很深的想法,你才可以放弃)。开始我们先认为除了I/O寄存器cache都是打开的,并且很少使用远程内存。在你试着做预测前,先搞明白cache对你的应用程序有什么启发。第二在硬件上排除任何问题。也没有任何软件辅助收回因为高的cache替换率和小的内存带宽而损失的性能。尝试从新组织软件而降低cache的非命中率,而不能增加其长度和使其变复杂,但也要明白一开始收获是很小并且来之不易。也试着在硬件上做优化。



4.13 Write Buffers and When You Need to Worry
通常使用write-though cache的32位MIPS CPU,其每一个写操作都会立刻直接写到主存中,如果CPU要等待每一个写操作完成才能继续,这将是是一很大的性能瓶颈。
C语言程序编译给MIPS使用,平均有10%的指令是存储指令;但这些操作可能可以趋向于合并为burst,举个例子在一个函数开始时的现场保护(保存一些寄存器)。DRAM内存通常有这样的特征,一组中第一个写的通常会花费很长时间(这些CPU一般是5至10个时钟周期),而第二个和后边的就会相应很快。
如果CPU简单地等待每一个写操作完成,这对性能地打击很大。所以通常会提供一个回写地缓冲区,先入先出地存入要写地数据和地址。
使用write-through cache的32位MIPS CPU很倚重回写缓冲区。在这些CPU中,在CPU的时钟频率达到40MHz时能缓冲四次的队列将很难提供很好的缓冲。
后来的CPU(有write-back cache)的缓冲区能直接保存需要回写的line,并且提高非cache写的时间。
许多回写缓冲区操作的时间对于软件来说是透明的。但有时候编程人员要注意下面的情况:
a. I/O寄存器访问的时间:这对所有MIPS CPU都有影响。当你执行一个向I/O寄存器的写操作,这会有一个不能确定的延时。和I/O系统之间的其他通讯可能会很快,举个例子在你告诉设备不要再产生中断后,你还是可能会看到一个活跃的中断。在其他例子中,如果I/O寄存器在一个写操作后需要一定时间来恢复,那么在你开始计算这个延时前,你必须保证回写缓冲区是空的。这儿你必须保证CPU等待直到回写缓冲区腾空。定义子程序来做这项工作是个好习惯;子程序叫wbflush(),这是个传统。看后面的4.13.1节,如何实现。

上面描述了在任何MIPS R4x000(MIPSIII ISA)上可能发生的。还针对整个IDT R3051家族,和绝大多数流行的嵌入式CPU。但在一些早期的32位系统中,更怪的事可能发生:
a. 读操作赶上写操作:当一条取数指令(非cache或是没有命中cache)执行时,回写缓冲区不是空的。CPU需要选择:是等写操作完成还是将内存接口给取操作使用?让取操作先做将提高效率,因为CPU需要等待直到取的数据到来。这是一个好的机会,写操作被压倒,但它后面还是可以和CPU并行。
最初的R3000硬件将这个选择留给系统硬件来决定。从IDT开始的绝大多数MIPSI CPU不允许读操作压倒写操作,写操作有没有条件的优先权。绝大多数MIPSIII CPU不允许读操作被压倒,但软件也没有必要对此进行过多的考虑。看8.4.9节关于sync指令的描述。
如果你确认你的MIPS I CPU没有无条件的写的优先权,那么当你在处理I/O寄存器时,必要的地址检测也不能帮到你;因为早期的一个向不同地址的写操作还没有完成,那么这时候的取操作就会产生错误。在这中情况下你就需要调用wbflush()。
b. 字节合并:当缓冲区注意到一些部分字的写操作是写向相同的字地址,缓冲区会将这些写操作合并成一个简单的写操作。这并没有被所有的R3051家族的CPU所采用,因为它可能在对I/O寄存器的写操作时产生错误。
如果将你的I/O寄存器映射成每个寄存器对应一个独立的字地址,这就不是一个坏注意了。但你不可能总这样做。

4.13.1 执行wbfush
除非你的CPU是上面提到的特殊类型的一种,你能保证在执行针对任何地址的取操作时,回写缓冲区是空的(这会暂停CPU直到写操作完成,取操作也完成)。但这样效率不高;你可以通过使用最快的内存来最小限度来克服一些。
对于那些从来都不想考虑这些的人,一个写操作后面紧跟着一个针对相同地址的取操作(如果你是运行在MIPS III或其后面的CPU,需要在两个指令间加入sync指令),需要先清空回写缓冲区(很难看到如果CPU没有这个动作也能执行正确)。
一些系统通过硬件信号来指明FIFO是不是空的,连到输入接口让CPU能立刻得知。但不是到目前为止的CPU都这样做。

CAUTION!一些系统通常在CPU外面也有写操作的缓冲区。任何总线或内存接口自夸的写加速也会出现相同的特征。写缓冲区在CPU外和在CPU内一样,也会给你带来相同的问题。



4.14 其他的关于MIPS CPU
虽然你可能永远也不用知道这些,但我们还是有许多理由要谈到这些。


4.14.1 多处理器的cache特征
这个书的讨论将仅仅针对单CPU的系统。感兴趣的可以读相应的文档(Sweazey和Smith著,1986)。

4.14.2 Cache Aliases
这个问题只会影响这样一类的cache,用于产生index的地址和存放在tag内的地址不一样。在R4000类型CPU的主cache中,index是由虚拟地址长生而tag内存放的是物理地址。这对性能很有好处,因为cache查询和地址转化可以并行,但这也可能导致aliases。
绝大多数这些CPU可以按照4KB一页的大小来转化地址,而cache大小是8KB或者更大。两个不同的虚拟地址可以映射成对应一个物理地址,而且这是两个连续页,我们可以假设它们开始地址分别是0KB和4KB。如果程序访问地址0KB,那么数据会被读入到index为0的cache中。我们在假设要地址4KB来访问相同的数据,cache就又会从内存中取数并保存到cache中,但这次index却为4KB。这时在cache中针对相同数据有两个备份,一个被改变了,另一个却不会受到影响。这就是cache的alias。
MIPS第二层cache是物理地址来产生index和被存放到tag中,所以它们不会产生上面的问题。
不过,避免这个问题比改正它更容易。如果任意两个不同的虚拟地址能产生相同的index,那么这个问题就不会出现了。对于4KB一页,只要保证用来产生index的最低12位地址一致;只要保证不同虚拟地址对应的物理地址页大小等于主cache大小的模。如果你能保证虚拟地址是64KB的倍数,这就不可能产生这个问题,你也不会遇到麻烦。

第五章 异常,中断,初始化

在MIPS体系结构中,中断,陷入,系统调用,以及其他中断程序正常执行的事情统统被称为异常.异常在MIPS体系结构中被同一种机制处理.异常包括:

外部事件.包括中断,读总线错.在有外部事件时,中断被用来引起CPU的注意.使用中断比使用CPU轮询机制来得快且更有效.
中断是唯一由CPU执行以外的事件引起的异常.因为我们不能通过注意来避免中断,在必要是,我们只能用一种软件的办法来禁止中断.

内存翻译异常.当没有合适的物理地址对应虚拟地址时,或当写一个有写保护的页时,会发生此种异常.操作系统会监查内存翻译异常的具体原因.某些异常是由于应用程序访问了非法内存.操作系统会终止应用程序的执行以保护其他应用程序.良性的内存翻译异常可以触发操作系统执行从复杂到简单的一系列操作:操作可以复杂到装入一个需要时调入内存的虚拟页,或简单到扩大栈的空间.

其他需要内核更正的不寻常情况.一个例子是浮点指令:当硬件无法处理某些困难和少见的操作符和操作数的组合时,硬件会产生一个异常,寻求软件模拟.这类情况比较模糊,因为不同的对这类情况会有不同的处理意见.未对齐的装入在某些操作系统中被由软件处理,在另外一些操作系统中被当做错误.

程序或硬件检查出的错误.包括非法指令,在不正确的用户权限下执行的指令,在相应SR位被禁止时执行的协处理器指令,整数溢出,地址对齐出错,用户模式下访问超出用户段(KUSEG)地址.

数据完整性错误.很多MIPS CPU不断对来自总线和缓存的数据作字节校验或字校验.校验错在R4000及以后的CPU上产生一个特殊异常.

系统调用和陷入.某些指令只是用来产生异常.它们提供了一种进入操作系统的安全机制(系统调用,条件陷入,断点).

某些事件不产生中断,尽管大家认为它们会.比如写总线错.CPU把数据和地址放入写缓冲中,然后继续执行.写操作可能在几个时钟周期后发生.在这种情况下,很难判断到底是哪条指令产生写错误.某些系统利用外部机制来解决这个问题.这种外部机制有可能会产生异常来引起CPU注意.

更为有意思的是,在大多数32位的CPU上,缓存中的校验错不产生异常.错误被放在一个状态寄存器位SR(PE)里,你必需自己去察看它.在R3000(32位的CPU)里,缓存校验在以后加入用于调试目的.

在这一章里,我们将要讨论:CPU如何决定产生异常,软件如何正确处理异常,为什么MIPS的异常叫做精确异常,异常入口点,以及一些软件编程约定.

在嵌入式系统中,来自CPU外部的硬件中断最常见的异常异常,要求得到及时处理,并很容易导致不易觉查的错误.异常嵌套---在一个异常的处理中发生另一个异常---可能引起一些特殊的错误.

系统重置(RESET)后,MIPS CPU靠异常启动,所以CPU启动也在这一章中讨论.在这一章末尾,我们会讨论一些相关的话题,如软件模拟机器指令,构造一个信号量用以提供任务到任务的通信.第十二章还包含一个有关异常处理的有注释的程序.
 

内存管理与TLB

我们倾向于直接从最底层引入本书中的大部分主题进行探讨,对于一本关注计算机底层体系结构的书而言,这似乎是自然而然的。然而,为了说清楚内存管理硬件,我们得从MIPS R2000所寻求实现的unix风格的虚拟存储系统开始讲起。本章的后面我们还会讨论一下相同的硬件如何在其他环境下工作。

早期的MIPS CPU定位于支持运行在UNIX工作站与服务器上的应用程序,因此内存管理硬件被构想为一个最小化的能帮助BSD UNIX——一个经过完善设计并拥有充分多虚拟存储需求的操作系统的典型——提供内存管理功能的硬件。很明显的是,这些设计者们十分熟悉DEC VAX小型机,并且在从这种体系结构中获取了众多思路的同时,也摒弃了许多复杂设计。尤其是许多VAX使用微代码来解决的问题,在MIPS中被交由软件处理。

本章中我们将从MIPS的设计起点开始,面对着一个unix类型的操作系统以及它的虚存系统的众多需求。我们将会展示一下MIPS的硬件是如何满足这些需求的。结尾时,我们会讨论一下在不能像通常一样使用内存管理硬件的嵌入式系统中,您可以采取的几种使用方式。

内存地址转译硬件(下面我们将称其MMU,全称为memory management unit)有几类不同用途:

n         重定位(Relocation):程序的函数方法和预先声明的数据地址均在编译期间决定,MMU允许程序在任何物理地址运行。

n         为程序分配内存:MMU可以从物理内存里许多零散的页中创建连续的程序空间,使我们能从一个充满固定大小页面的池里分配内存。如果我们不停分配释放大小不一的内存块,就会碰上内存碎片问题:我们不得不止步于一个布满“小孤岛”的内存空间,无法满足对较大块内存的申请要求,哪怕此时所有的空闲空间之和是足够的。

n         隐藏和保护:用户级程序只能访问kuseg内存区域(较低的程序地址)内的数据。这类程序只能在操作系统所许可的内存区域中取得数据。

此外,每一页可以独立的指定为可写权限或者写保护权限;操作系统甚至可以停止一个意外的写覆盖代码空间的应用程序。

n         扩展地址空间: 有些CPU不能直接访问它们拥有的全部物理空间。尽管MIPS I 系列CPU是真正的32位体系结构,它们却布局了地址映射,使得未被映射的地址空间窗口kseg0kseg1(它们不依赖MMU进行地址转换)落在了物理内存的开头的512M内。如果你要访问更高地址,则必须通过MMU

n         内存映射对程序的适应化:在MMU的帮助下,你的程序能够去使用适合它的地址。同一段程序的许多份拷贝可能会同时运行在一个庞大的操作系统里,令它们去使用相同的程序地址变得更容易。

n         调页能力:程序可以好像已经得到它们所申请分配的所有资源一样正常的运行,而操作系统实际上只分配给它们当前所需的资源。访问未分配空间的程序会导致一个交由操作系统处理的异常(exception),操作系统此时才在这块内存中装入适当数据并令应用程序继续运行。

 

UNIX内存管理工作的本质是为了能运行众多不同的任务(即multitasking——多进程),并且每个任务各自拥有自己的内存空间。如果这个工作圆满完成,那么各任务的命运将彼此独立开来(操作系统自身也因此得以保护):一个任务自身崩溃或者错误的做某些事不会影响整个系统。显然,对一个使用分布终端来运行学生们程序的大学而言,这是一个很有用的特性;然而不仅如此,甚至是要求最严格的商业系统环境也需要能够在运行的同时支持实验软件或原型软件一并进行调试和测试。

MMU并不仅仅为了建立巨大而完备的虚拟存储系统,小的嵌入式程序也能在重定位和更有效的内存分配里受益。如果能把应用程序观念上的地址映射到任何可获得的物理地址,系统在不同时刻运行不同程序就会更加容易。

多进程和隔离不同进程地址空间一直都在向更小的计算机上移植,目前在个人电脑以及英特网服务器端都已经十分普通。

嵌入式应用中常常会明确的运用多进程机制,但几乎没有多少嵌入式操作系统使用隔离的地址空间。或许这归咎于这种机制在嵌入式CPU以及它们上面的操作系统上用处不大并且带来不稳定性,因而显得不那么重要。

MIPS这种如此之必要以致于导致在1986年时工作站CPU变的廉价起来的简单机制,或许也可以被证实跟90年代后期嵌入式系统的兴起有一定关系。甚至是很小的应用,也被迅速增长的代码大小所困扰,需要使用所有已知的手段来控制软件的复杂度;这种由MIPS首创的灵活的基于软件的方法看来能提供任何所需空间。仅仅几年前,CPU的厂商们在定位嵌入式市场时还很难确定MMU是否值得包括进去;然而到1997年,微软推出的无法在没有内存管理硬件的环境下运行的Windows/CE,已被视为针对嵌入式所面对的各种困难的一个成功解决方案。

6.1 大型计算机上的内存管理

或许从一个类似unix系统的内存管理系统的整个工作开始讨论是最容易的(选择unix作为研究是因为:尽管它体积庞大,却比PC上的操作系统简单的多)。在图6-1中展示了其典型特征。

6.1.1 基本的进程空间布局和保护

6-1中最宽的分隔线是在低半部分——标明“用户程序可访问”的那部分——以及剩余部分之间的。程序的空间中的用户可访问部分就是我们在2.8节所描述的通常在MIPS内存映射中称为“kuseg”的部分。所有的高位地址内存都保留给操作系统。从操作系统的角度来看,地址低半部分是一个用户







     Assembler language programming

这一章将告诉你如何阅读并编写MIPS体系下的汇编代码。MIPS汇编代码看上去与实际的代码差异很大,这主要是因为以下原因:

1,      MIPS汇编编译器(assembler)提供了大量的已经预定义的宏指令(extra macro-instruction)。所以编译器的指令集(instruction set)要比CPU实际提供的指令集大的多。

2,在MIPS汇编代码中有许多伪操作符,放在代码开始和结束的地方,用来预定义常用数据,控制指令排列顺序,以及控制对代码的优化。通常它们被称为“directives”或“pseudops”。

3,实际应用中,汇编代码往往要经过C语言预处理器(C preprocessor)的处理后,才被提交给assembler进行编译。C语言预处理器将汇编代码中的宏,用它自己的头文件中的定义进行替换。这可以使汇编代码书写起来稍微方便一点。

在你继续看下去之前,最好先回去温习一下Chapter-2的内容,包括低层机器码的构造,数据类型,寻址方式。($:流水线pipeline的知识很值得温习一下,主要看一下那些该死的延迟点delay-slot)

 

 

9.1      A Simple Example

我们仍然采用在Chapter-8见过的那个例子: C库函数strcmp(1)。这一次我们的演示的重点是: 汇编语法所必需的符号,以及一些人工优化(hand-optimized)并重排序(hand-scheduled)的代码。

Int

Strcmp(char* a0, char* a1)

{

char t0, t1;

while(1){

         t0 = a0[0];

         a0 += 1;

         t1 = a1[0];

         a1 += 1;

         if( t0 == 0 )

                 break;

         if( t0 != t1 )

                 break;

}

return ( t0 - t1 );

}

 

这段代码的运行速度因为以下原因而比较低:

1,           每个循环都会经过两个条件分支(conditional branch)和两个提取指令(load),而我们没有在分支延迟点(branch delay-slot)和提取延迟点(load delay-slot)上放置足够的指令($:相当于cpudelay-slot处做nop动作,从而影响了效率,参见1.5.5 programmer-visible pipeline effects)

2,           每次循环只比较一个字节,使得循环过于频繁而效率低下($:因为分支(b*)及跳转(j*)指令会造成流水线的刷新,后续指令被失效)。

我们来修改这段代码:首先把循环展开,每次循环比较2个字节;把一个load指令调整到循环的末尾——这只是一个小技巧,这样我们就可以尽可能的在每个branch delay-slotload delay-slot处都放上有效的指令了。

       

Int

Strcmp(char* a0, char* a1)

{

char t0, t1,t2;

 

/*因为第一个load被调整到循环的末尾处,所以这里要先取一次值*/

t0 = a0[0];

 

while(1){

         t1 = a1[0];                        /*第一个字节*/

         if( t0 == 0 )

                 break;

         a0 += 2;                           /*$branch delay-slot*/

         if( t0 != t1 )

                 break;

/*2个字节,在上面我们已经把a02了,所以这里是[-1]*/

         t2 = a0[-1];                      /*$branch delay-slot*/

         t1 = a1[1];                        /*先不把a12,留到下面的delay-slot处再加*/

         if( t2 == 0 )

                 return t2-t1;              /*下面汇编代码里的标志.t21*/

         a1 += 2;                           /*$branch delay-slot*/

         if( t1 != t2 )

                 return t2-t1;              /*下面汇编代码里的标志.t21*/

         t0 = a0[0];                        /*$branch delay-slot*/

}

/*下面汇编代码里的标志.t01*/

return ( t0 - t1 );

}

 

ok,现在让我们把这段代码转成汇编来看看。

 

#include <mips/asm.h>

#include <mips/regdef.h>

 

LEAF(strcmp)

        .set           nowarn

        .set           noreorder

        lbu            t1,    0(a1);

1:    

beq           t0,    zero, .t01                  #load delay-slot

        addu         a0,   a0,2                         #branch delay-slot

        bne           t0,    t1,.t01

        lbu            t2,    -1(a0)                      #branch delay-slot

        lbu            t1,    1(a1)                        #load delay-slot

        beq          t2,    zero,.t21

        addu         a1,   a1,2                         #branch delay-slot

        beq          t2,    t1,1b

        lbu            t0,    0(a0)                        #branch delay-slot

 

.t21:

        j               ra

        subu         v0,   t2,t1                         #branch delay-slot

.t01:

        j               ra

        subu         v0,   t0,t1                         #branch delay-slot

        .set           reorder

END(strcmp)

 

Even without all the scheduling,这里已经有很多有意思的东西了,让我们来看看。

 

#include   

这是个好主意:由C语言预处理器cpp来对常量进行宏定义,并引入一些预定义的文本宏($text-subsitution macro,就是上面的LEAFEND之类的东西)。上面这个汇编文件就是这样做的。这里,在把代码提交给assembler之前,用cpp把两个头文件内嵌入汇编代码文件。Mips/asm.h定义了宏LEAF和宏END(见下面),mips/regdef.h定义了惯用的寄存器的俗称(conventional name),比如t0a1(section 2.2.1)

 

macro

        这里我们用了2个宏定义:LEAFEND。它们在mips/asm.h中定义被如下:

 #define           LEAF(name)    \

        .text; \

        .globl        name;       \

        .ent           name;       \

name:

        LEAF被用来定义一个简单子函数(simple subroutine),如果一个函数体内不调用其它函数,那幺相对于整个调用树(calling tree)而言,这个函数就是调用树上的一片“叶子”,因此得名“leaf”。相对的,一个需要调用其它函数的函数,叫“nonleaf”,nonleaf函数必须多做很多麻烦的事情例如保存寄存器和返回地址,不过很少会真的需要自己写一个nonleaf 的汇编代码($:这通常用 C语言来写)。注意下面:

        .text          表示这段用汇编写成的代码应该放在“.text”段中,“.text”是C语言程序的代码段。

        .globl        声明“name”为全局变量,在模块的符号表(symbol table)中作为全局唯一的符号而存在($:全局变量在整个程序内唯一;局部变量在其所在函数体中唯一;static变量在其所在文件内唯一)

        .ent           对程序而言没有实际意义,只是告诉assembler将这一点标志为“name”函数的起始点,为调试提供信息。

        .name       将其所在地址命名为“name”,作为assmbler的输出。名为“name”的函数调用将从该地址开始。

END定义了两个assembler需要的信息,都不是必须的。

#define     END(name)      \

        .size          name,       .-name;     \

        .end          name

        .size          表示在symbol table中,“name”函数体的大小(字节数)将与“name”符号一道列出。

        .end          指出函数尾。调试用信息。

 

.set           伪操作符(directive),用来告诉assembler如何编译。

在本例中,.noreorder表示禁止对代码重排序,让代码严格保持其书写的顺序,否则MIPS assembler会尝试将代码重新排序——填补那些delay-slot以获得较好的运行效率。Nowarn要求assembler不要费心去指出那些应该被重排序的地方,相信程序员已经处理好这些事情了。通常这不是个好主意——除非你确信你肯定正确。基本上这是个不必要的directive

Labels:“1:”是数字标志label,大多数的assembler都会把它当作**局部**label来处理。像“1:”这种label,在程序里你想用多少都可以:你可以用“1f”引用reference下一个“1:”;用“1b”来引用前一个“1:”。这会很常用。

Instructions:一些指令的顺序会有出乎预料的问题,你必须注意。.set noreorder这一directive使得delay-slot问题变得非常敏感而容易出问题,我们必须确保load的数据不会马上被下一条指令用到。比如说:

        bne           t0,    t1,.t01

lbu             t2,    -1(a0)

        ……………

.t01:

        j               ra

subu          v0,   t0,t1        

这里lbu            t2,    -1(a0)一句中,用t2不能用t0,因为要执行的下一条指令subu             v0,   t0,t1         中要用到t0

 

好,已经看过了一个例子,让我们再看一些语法方面的东西。

 

9.2      语法概要Syntax Overview

在附录B中你可以找到MIPS汇编器的语法列表,大多数的其它厂商的编译器也都遵循这个列表的规则。当然,可能少数的directive的具体含义会有少许的差别。如果你以前在类unix(unix-like)的系统上用过assembler,那这个列表你应该会很熟悉。

 

9.2.1    Layout,     Delimiters,       and Identifiers

首先你得熟悉C语言,如果你熟悉C,那幺注意,汇编代码与C代码有一些区别。

汇编代码以行为分界,换行(end-of_line)表示一个指令或伪操作符directive的结束。你也可以在一行里写多条指令或伪操作符,只要它们中间用“;”隔离开来。

以“#”开头的行是注释,assembler将忽略它。但是**不要把“#”放在行的最左面**:这将激活C预处理器cpp(C preprocessor),有时候你可能会用到它。如果你确定你的代码会经过C预处理器的预处理,那幺你可以在你的汇编代码中使用C风格的注释方式:“/*…*/”,可以跨越多行,只要你乐意。

变量和label的名字(identifiers)可以随意——只要在C语言里合法就行,甚至可以包含“$”和“。”。

在代码中你可以使用0~99之间的数字作为label,它会被视为临时性的符号,所以你可以在代码中重复使用同一个数字作为label。在一个分支指令(branch instruction)中“1f”指向下一个“1:”,而“1b”指向前一个“1:”,这样就不用费心为那些随手而写的跳转和循环起名字了,省下这些名称可以去命名那些子程序、还有那些比较关键的跳转。

MIPS/SGI assembler通过C preprocessor的宏定义来提供寄存器的俗称(conventional name)($zero,t0,~,ra),所以你必须用C preprocessor来对你的汇编代码进行预处理,为此需要在代码中包含include头文件mips/regdef.h。虽然说规范的assembler通常可以识别这些寄存器的俗称,但是为了代码的通用性起见,还是不要把宝压在这上面为好。

assembler的定位计数器指向正在编译的当前指令的地址,你可以在汇编代码中引用assembler的定位计数器的值。标识符“。”代表assembler当前的定位计数器的值。你甚至可以对它做有限的一些操作。在上下文中,label(或者其它什幺可复位位的符号relocatable symbol),将被替代为它的地址。

$:类似于armadds r0,pc,symbol address - (+8)这样的操作。)

        固定字符和字符串的定义方式与C相同。

 

9.3指令规则 General Rules for Instructions

Mips assembler允许一些指令的简略写法。有时候,你提供的操作数operand少于机器码所要求的,或者机器码要求使用寄存器而你却使用了常数,在某些情况下,assembler也会允许这种写法,并自动进行调整。你将会发现,在真正的汇编代码中这种情况非常频繁。这一节我们将讨论这个问题。

 

9.3.1    寄存器间运算指令

Mips 的运算指令有3个操作数。算术arithmetical或逻辑logical指令有2个输入和一个输出,例如:Rd        =      rs     +      rt,被写成addu     rd,    rs,rt

        这里的3个寄存器可以重复(例如addu      rd,    rd,rd)。在CISC-stylecpu(例如intel386)指令中,只有2个操作数,Mips assembler也支持这种风格的写法,目的寄存器destination register可以同时作为一个源操作数source operand:例如:addu    rd rs,这与addu         rdrdrs相同,assembler将自动将它转换成后者。

        Mips assembler提供的指令集中有一些伪指令unary operation,比如Negnot,这些伪指令实际上是一条或多条机器指令的组合。对这些指令,Assembler最大接受2个操作数。Negu   rd rs实际上被转化为subu        rd zero,rs,而not        rd将被转化为or    rdzerors

        可能最常用的寄存器间操作register-register operation要算是move  rdrs了。这条指令实际上是or rdzerors

 

9.3.2:   带立即数的运算指令

assembler和机器语言里,嵌入在指令中的常数被称为立即数immediate value。很多Mips的算术和逻辑指令都有另外一种形式,这种形式里rt寄存器被一个16bit的立即数所取代。在cpu的内部运算过程中,这个立即数将被扩展为32bit,可能是符号扩展sign-extend$:用最左面的bit(bit15)填充扩展的高16bit),也可能是零扩展zero-extend$:用0填充扩展的高16bit)——这取决于具体的指令。一般而言,算术指令进行符号扩展sign-extend,而逻辑指令进行零扩展zero-extend

在机器指令的概念上,即便执行同一种运算,操作数中是否包含立即数的区别,将导致两条不同的指令(例如addaddi)。尽管如此,对于程序员而言,还是没有太大的必要去具体的区分那些包含立即数的指令。Assembler会找出它们,并进行转换。比如:

addu          $2$464    ————————>      addiu        $2$464

如果立即数过大而超过了16bit所能表达的范围,机器码中将无法容纳,这时assembler会再次帮助我们:它会自动将立即数载入“编译用临时寄存器assembler temporary registerat/$1中,然后进行如下操作:

add   $4        0x12345  —————————>          li       at0x12345

                                                                                 add  $4$4at

注意这里的“li(load immediate)指令,在cpu提供的机器指令集中你找不到它。这是一个及其常用的宏指令,用来把32bit整数装载入寄存器,而不用程序员来操心怎幺去实现这一动作:

当这个32bit整数值介于-32k~+32k之间,assembleraddiu指令配合zero寄存器来实现“li”;

16-31bit0时,用ori指令来实现“li”;

0-15bit0时,用lui指令来实现“li”;($:运算指令(oriaddiu)要比存取指令(swlui)的处理速度快)

如果以上条件都不成立,那只好用lui/ori两条指令来实现“li”了:

li        $3-5                     ——————>              addiu        $3$0-5

li        $408000              ——————>              ori            $4$008000

li        $5120000            ——————>              lui             $50x12

li        $60x12345          ——————>              lui             $60x1

ori               $6$60x2345

9.3.3    关于32/64位指令

我们在前面(2.7.3)讲过可以对32位指令的机器码进行符号扩展到64位,以保证32位的程序(mipsII)在老的机器上能正常运行。

 

 

9.4 地址模式

        前面提到过,mips cpu硬件上只支持一种地址模式:寄存器基地址+立即数偏移量base_reg+offset,偏移量必须在-32768+32767之间(16bit带符号整型所能表示的范围)。但是assembler可以通过一些方式来支持以下几种地址模式:

        Direct:由你提供的数据标号或外部变量名。

        Direct+index:一个偏移量,加上由寄存器指出的标号(label)地址。

        Constant:一个数字,作为一个32位的绝对地址(absolute address) 处理。

        Register indirect:是寄存器加偏移量的特殊形式:偏移量为0

       

 

 

 

9.5 assembler directives

MIPS中所有指令都被塞在32bit空间里的做法导致了一个明显的问题:访问一个确定的/嵌入在指令以内的?????(compiled-in location)内存地址往往要花费至少两条指令.例如:

Lw           $2,   addr -------------à         lui     at,    %hi(addr)

                                                                Lw   $2,   %lo(addr)(at)

在大量使用全局或静态变量的程序中,这一缺陷往往导致最后编译出的代码臃肿而低效.

早期的MIPS编译器引入了一种技术以弥补以上缺陷,这项技术被以后的MIPS编译工具链toolchain一直沿用下来,它通常被称为全局量指针相对寻址”gp-relative address.这个技术要求compiler,assembler,linker以及运行时激活代码(runtime startup code)偕同配合,把程序中的变量和常数汇集到一个独立的内存区间;然后设置register $28(通常称为全局量指针global pointer或简写为gp)指向该区间的中央(linker生成一个特殊符号_gp,其地址为该区间的中央,激活代码负责将_gp的地址加载到gp寄存器,这一动作在第一个load/store指令运行之前完成).只要这些全局变量\静态编量\常量加起来不占用超过64k大小的空间,这些资料相对该区间的中点也就不超过正负32k(偏移量15bit+符号位1bit,参见mips机器码格式),那幺我们就可以在一条指令中完成对它们的load/store操作:

        lw     $2,   addr         ------à           lw     $2,   addr-_gp(at)

一个问题是在编译彼此独立的模块的时候,compilerassembler如何决定哪些变量需要通过gp来寻址,通常的做法把所有小于某个特定长度(通常是8byte)的对象放进该区间,这个长度可以通过compiler/assembler”-G n”选项来控制.特别需要指出: ”-G 0”将取消gp-relative寻址方式.

上面所说的gp-relative寻址是一种非常有用的技巧,然而在使用中会有一些陷阱值得注意.在汇编代码中声明全局量的时侯你最好小心点:

可写,且初始化过的小对象体writable,initialized small data必须显式的声明在.sdata段中.( “小对象体一词中的含意即为上面提到的长度小于8byte”)

声明全局对象时必须指出其长度.

.comm.     smallobj,   4

.comm.     bigobj,      100

声明小外部变量时同样需要指出其长度.

Extern      smallext,   4

大多数assembler不会对对象声明作辅助性的处理(如指出该对象的长度).

C代码中的全局变量,必须在所有使用它的模块中被声明.对于外部队列,你可以显式的指出它的长度,:

Int cmnarray[NARRY];

也可以不指出其长度:

extern       int     exarray[];

有时候,程序运行的方式(环境)决定了不能采用gp-relative寻址方式.比如一些实时操作系统,还有很多固化环境下的程序(PROM monitor)是用一块单独连接link的代码实现来内核kernel,应用程序直接使用子函数(而不是通常的系统调用)调用到内核中去.这种情况下无法找到一个和适的方法以使gp寄存器在内核和应用程序的两个小数据段.sdata中来回切换,所以内核与应用程序两者之一(没必要两个都这样做)必须使用”-G 0”选项来进行编译.

当使用”-G 0”选项编译模块的时候,那些需要与该模块连接的库library通常也应该使用”-G 0”选项编译.在资料是否应该放在.sdata的问题上, 模块和库的声明应该彼此一致,如果发生冲突的话,linker将无法判定资料应该放在小数据段还是普通数据段,这时linker会给出奇怪而毫无价值的错误信息.

 

9.5  Assembler  Directives

在一开始我们就已经提到过”directive”,你也可以在附禄B里找到它的清单,不过没有详细介绍.

9.5.1 段的选择

通常的数据段和代码段的名字以及对它们的支持在不同编译工具链上可能会不一样.但愿大部分至少能够支持一般的MIPS通用的段,见图9.1.

在汇编代码中,以如下方式来选择段:

.text, .rdata, and .data

简单的把适当的段名放在数据和指令之前,就象下面的样子:

.rdata

msg: asciiz        “hello world!\n”

 

        .data

table:

        .word       1

.word       2

.word       3

 

        .text

func:sub    sp,    64

………………

 

.lit4 and .lit8 : 隐式浮点常数段floating-point implict constants

你不能像directives一样写这些段.它们是由assembler隐式创建的只读数据段,用来放置li.sli.d宏指令中的浮点常数型参数.一些assemblerlinker会合并相同的常数以节省空间.

.bss, .comm., and .lcomm data

这个段名也不用作directive,它用来收集在C代码中声明的所有未初始化的资料.C的一个特点是: 在不同的模块中可以有同名的定义.只要其中被初始化的不要超过一个..bss段用来收集那些在所有模块中都没有初始化过的数据.fortran程序员可以认为这个就是fortran语言中的.common,虽然名字不一样.

你必须声明每个资料的长度(单位为byte),当程序被连结的时候,它就可以得到足够的空间(所有声明中的最大值).如果有任何模块把它声明在初始化过的数据段,这些长度将被用到,并且使用如下声明:

.comm.     dbgflag,    4              #global common variable, 4 bytes

.lcomm.    sum, 4              #local common variable, 8 bytes

.lcomm.    array,        100          #local common variable, 100 bytes

未初始化uninitialized”这一说法实际上并不准确:虽然这些段在编译出的目标文件中是不占地方的,但是在执行你的程序之前,运行时激活代码run-time startup code或操作系统会将.bss段清零---------很多C程序都依赖于这一特性.

.sdata, small data, and .sbss

这些段被编译工具链用作单独放置小资料对象的.data.bss.MIPS编译工具链进行这个处理是因为,对一个足够紧凑的小资料对象段可以进行高效率的load/store操作,其原理是在gp寄存器中保存一个资料指针,具体说明见本书9.4.1章节.

注意,.sbss不是一个合法的directive;放在.sbss段中的资料满足两个条件:1,.comm.lcomm声明;2,其长度小于”G n”编译选项所指定的长度(默认为8byte).

.section

开始一个任意名称的段,并提供旗标(可能在代码中提供,也可能是工具包提供? Which are object code specify and probably toolkit specific).查看你的工具包的说明手册,????????????????????

如图9.1所示的结构可能适合做为一个运行在裸机bare cpu上的固化程序ROM program.只读段倾向于放在远离下部可擦写区间的内存位置上.

heap和栈stack并不是真正的能被assemblerlinker所识别的段.一般的,它们在运行时由运行系统???????run-time system初始化和维持.通过把sp寄存器设置为该程序可用内存的最高地址(8byte对齐)的方式来定义栈;堆通过一个由类似于malloc函数使用的全局指针变量来定义,通常初始化为end符号symbol,symbollinker赋值为已声明变量的最高位置.

 

特殊符号

9.1显示了一些由linker自动声明的符号,以便程序找到自己各个段的起始\结束的位置.这最初只是习惯,后来在unix类的系统上得到发展,其中一些是MIPS环境中所独有的.你的工具包手册上可能定义了他们中的一部分或全部;下面打@的表示肯定会被定义的符号:

symbol                     standard                                   value

_ftext                                                代码段起始点

etext                 @                    代码段结束点

_fdata                                               数据段起始点

edata                        @                    数据段结束点

_fbss                                                未初始化段起始点

end                           @                    未初始化段结束点

(end通常也就是程序image的结束点)

9.5.3        资料的定义与对齐

选择好正确的段之后,现在你需要用下面所说的directive来定义资料对象本身.

.byte, .half, .word, and .dword

这些directive产生1,2,4,8byte长度的整数(有些工具链-----即便是64位的-------没有提供.dword directive).可以跟随着一个值的列表,彼此以逗号分离,可以在值的后面加冒号并跟随一个重复计数,以表示连续几个相同的值,如下(word=4byte):

        .byte 3                      #1 byte:                    3

        .half  1,2,3                #3 halfwords:    1      2      3

        .byte 3                      #5 words:         5      5      5      6      7

注意数据的位置(相对于段的起始处)在资料被输出之前自动对齐到合适的边界.如果你确实需要输出未对齐的资料,那幺必须自己使用西面要讲到的.align directive来说明.

.float.double

这些directive输出单精度\双精度的浮点值.如下:

.float         1.4142175                       #1个单精度浮点数

.double             1e+10,     3.1415             #1个双精度浮点数

与对整数处理相同,可以使用冒号表示重复.

.ascii.asciiz

这些directive输出ASCII字符串, 附带\不附带结束标记.如下两行代码输出相同的字符串:

.ascii         “Hello\0”

.asciiz               “Hello”

.align

这个directive允许你为下一个资料指定一个大于正常要求的对齐边界??????alignment.alignment表示为2n次方.

.align        4              #对齐到16byte边界 (2^4)

var:

.word       0

如果标志(上例中的var)后面紧跟着.align,那幺这个标志仍然可以被正确的对齐,例如下面的例子与上面的例子作用相同:

var:

        .align        4      # 对齐到16byte边界(2^4)

        .word       0

对要求紧凑结构????packed的数据结构,这个directive允许你取消.half,.word的自动对齐功能,你可以指定它为0对齐,它将持续作用直到下个段开始.??????

.half  3              #正确对齐的半字

.align        0              #关掉自动对齐功能

.word       100          #按照半字对齐的字.

.comm.lcomm

通过指定对象名和长度来声明一个common或者说未初始化的资料对象.

.comm声明的对象对所有声明过它的模块有效,它的空间由linker分配,采用所有声明中的最大值.但是,如果其中有任何一个模块将其声明在.data,.sdata,.rdata,那幺所有的长度声明都将失效,而采用初始化定义取而代之.

??????.comm的用途是为了避免以下情况:一个资料对象要在许多文件中用到,但它与其中的每一个文件都没有更特殊的联系,但是我们不得不在某个文件中声明它,这样就造成了不对称.但是它确实存在,因为fortran就是用这样的语意来定义它的,而我们想要经过汇编语言来编译fortran程序(比如查看fortran程序编译出来的汇编代码).

.lcomm声明的对象是局部对象,assembler.bss段或.sbss段为其分配空间,但是在所属模块之外它不可见.

.space directive增加当前段的空间计数,例如:

struc:        .word       3

        .space      120          #空出120byte大小的空间

.word       -1

对通常的资料\代码段而言,这个空出的空间用0填充,如果assembler允许你声明内容不在对象文件中定义的段(.bss),这个空间只影响连续的符号\变量之间的偏移.

9.5.4        符号绑定属性        symbol-binding attributes

符号symbol(在数据段或代码段中的标志)可以被调节为可见?????,并可以供linker使用以便将几个分离的模块编译成一个完整的程序.

符号有三个级别的可见度:

局部:

除了声明它的那个模块,它对外部而言是不可见的,并且不会被linker使用.你不用担心在其它模块中是否适用了相同的符号.

全局:

这些是公开的符号以供linker使用.使用.extern 关键词,你可以在其它模块中引用全局符号,而不必为它定义本地空间.

弱全局weak global:

这个晦涩的概念在一些工具链中以关键词.weakext实现.它允许你定义一个symbol,如果有同名的全局对象存在,那幺就把它连结到这个同名的全局对象;如果不存在同名的全局对象,那幺它就作为一个局部对象存在.如果.comm段存在,你就不应该用弱全局这个概念.????

.globl

C语言环境下,除非用static关键词进行声明,否则模块级的数据和函数入口都默认为全局属性.C语言不同,一般在汇编语言环境下,除非使用.globl directive显式的进行声明,否则标志label默认为局部属性.对于用.comm声明的对象不需要再使用.globl,因为它们已经自动具备全局属性.

        .data

        .globl        status                #全局变量

status:       .word       0

 

        .text

        .globl        set_status         #全局函数入口

set_status:

        subu sp,    24

………………..

.extern

如果引用当前模块中未定义的标志,那幺(assembler)将假定它是在其它模块中定义的全局对象”(外部变量).在一些情况下,如果assembler能知道所引用的对象的长度,它就可以生成更优化的代码(9.4.1).外部变量的长度用.extern directive来指明:

.extern      index,       4

.extern      array,        100

lw             $3,index           #提取一个4-byte(1-word)长度的外部变量

lw             $2,array($3)             #提取100-byte长度外部变量的一部分

sw            $2,value                   #装载一个未知长度的外部变量

.weakext

一些assembler和工具链支持弱全局的概念,这允许你为一个符号symbol指定一个暂时性的绑定(binding,是一个连接用的概念,指符号与其内存地址间的对应关系?????),如果存在一个正常全局(强全局)对象定义,那幺它将把先前的这个弱全局绑定覆盖.例如:

.data

.weakext      errno

errno:       .word       0

 

.text

        lw     $2,   errno                #可能使用局部定义,也可能使用外部定义.

如果没有其它模块使用.globl来定义errno,那幺这个模块------还有其它模块------将会使用上面代码中errno的局部定义.

另外一个可能的用法是:用一个名字声明一个局部变量,而用另一个名字声明它另外的弱全局身分.

        .data

myerrno:   .word       0

        .weakext  errno,       myerrno

 

        .text

        lw     $2,   myerrno                    #总是使用上面的局部定义

        lw     $2,   errno                        #可能使用局部定义,也可能使用其它的

#(外部定义)

9.5.5        函数directive

 

 

     Assembler language programming

这一章将告诉你如何阅读并编写MIPS体系下的汇编代码。MIPS汇编代码看上去与实际的代码差异很大,这主要是因为以下原因:

1,      MIPS汇编编译器(assembler)提供了大量的已经预定义的宏指令(extra macro-instruction)。所以编译器的指令集(instruction set)要比CPU实际提供的指令集大的多。

2,在MIPS汇编代码中有许多伪操作符,放在代码开始和结束的地方,用来预定义常用数据,控制指令排列顺序,以及控制对代码的优化。通常它们被称为“directives”或“pseudops”。

3,实际应用中,汇编代码往往要经过C语言预处理器(C preprocessor)的处理后,才被提交给assembler进行编译。C语言预处理器将汇编代码中的宏,用它自己的头文件中的定义进行替换。这可以使汇编代码书写起来稍微方便一点。

在你继续看下去之前,最好先回去温习一下Chapter-2的内容,包括低层机器码的构造,数据类型,寻址方式。($:流水线pipeline的知识很值得温习一下,主要看一下那些该死的延迟点delay-slot)

 

 

9.1      A Simple Example

我们仍然采用在Chapter-8见过的那个例子: C库函数strcmp(1)。这一次我们的演示的重点是: 汇编语法所必需的符号,以及一些人工优化(hand-optimized)并重排序(hand-scheduled)的代码。

Int

Strcmp(char* a0, char* a1)

{

char t0, t1;

while(1){

         t0 = a0[0];

         a0 += 1;

         t1 = a1[0];

         a1 += 1;

         if( t0 == 0 )

                 break;

         if( t0 != t1 )

                 break;

}

return ( t0 - t1 );

}

 

这段代码的运行速度因为以下原因而比较低:

1,           每个循环都会经过两个条件分支(conditional branch)和两个提取指令(load),而我们没有在分支延迟点(branch delay-slot)和提取延迟点(load delay-slot)上放置足够的指令($:相当于cpudelay-slot处做nop动作,从而影响了效率,参见1.5.5 programmer-visible pipeline effects)

2,           每次循环只比较一个字节,使得循环过于频繁而效率低下($:因为分支(b*)及跳转(j*)指令会造成流水线的刷新,后续指令被失效)。

我们来修改这段代码:首先把循环展开,每次循环比较2个字节;把一个load指令调整到循环的末尾——这只是一个小技巧,这样我们就可以尽可能的在每个branch delay-slotload delay-slot处都放上有效的指令了。

       

Int

Strcmp(char* a0, char* a1)

{

char t0, t1,t2;

 

/*因为第一个load被调整到循环的末尾处,所以这里要先取一次值*/

t0 = a0[0];

 

while(1){

         t1 = a1[0];                        /*第一个字节*/

         if( t0 == 0 )

                 break;

         a0 += 2;                           /*$branch delay-slot*/

         if( t0 != t1 )

                 break;

/*2个字节,在上面我们已经把a02了,所以这里是[-1]*/

         t2 = a0[-1];                      /*$branch delay-slot*/

         t1 = a1[1];                        /*先不把a12,留到下面的delay-slot处再加*/

         if( t2 == 0 )

                 return t2-t1;              /*下面汇编代码里的标志.t21*/

         a1 += 2;                           /*$branch delay-slot*/

         if( t1 != t2 )

                 return t2-t1;              /*下面汇编代码里的标志.t21*/

         t0 = a0[0];                        /*$branch delay-slot*/

}

/*下面汇编代码里的标志.t01*/

return ( t0 - t1 );

}

 

ok,现在让我们把这段代码转成汇编来看看。

 

#include <mips/asm.h>

#include <mips/regdef.h>

 

LEAF(strcmp)

        .set           nowarn

        .set           noreorder

        lbu            t1,    0(a1);

1:    

beq           t0,    zero, .t01                  #load delay-slot

        addu         a0,   a0,2                         #branch delay-slot

        bne           t0,    t1,.t01

        lbu            t2,    -1(a0)                      #branch delay-slot

        lbu            t1,    1(a1)                        #load delay-slot

        beq          t2,    zero,.t21

        addu         a1,   a1,2                         #branch delay-slot

        beq          t2,    t1,1b

        lbu            t0,    0(a0)                        #branch delay-slot

 

.t21:

        j               ra

        subu         v0,   t2,t1                         #branch delay-slot

.t01:

        j               ra

        subu         v0,   t0,t1                         #branch delay-slot

        .set           reorder

END(strcmp)

 

Even without all the scheduling,这里已经有很多有意思的东西了,让我们来看看。

 

#include   

这是个好主意:由C语言预处理器cpp来对常量进行宏定义,并引入一些预定义的文本宏($text-subsitution macro,就是上面的LEAFEND之类的东西)。上面这个汇编文件就是这样做的。这里,在把代码提交给assembler之前,用cpp把两个头文件内嵌入汇编代码文件。Mips/asm.h定义了宏LEAF和宏END(见下面),mips/regdef.h定义了惯用的寄存器的俗称(conventional name),比如t0a1(section 2.2.1)

 

macro

        这里我们用了2个宏定义:LEAFEND。它们在mips/asm.h中定义被如下:

 #define           LEAF(name)    \

        .text; \

        .globl        name;       \

        .ent           name;       \

name:

        LEAF被用来定义一个简单子函数(simple subroutine),如果一个函数体内不调用其它函数,那幺相对于整个调用树(calling tree)而言,这个函数就是调用树上的一片“叶子”,因此得名“leaf”。相对的,一个需要调用其它函数的函数,叫“nonleaf”,nonleaf函数必须多做很多麻烦的事情例如保存寄存器和返回地址,不过很少会真的需要自己写一个nonleaf 的汇编代码($:这通常用 C语言来写)。注意下面:

        .text          表示这段用汇编写成的代码应该放在“.text”段中,“.text”是C语言程序的代码段。

        .globl        声明“name”为全局变量,在模块的符号表(symbol table)中作为全局唯一的符号而存在($:全局变量在整个程序内唯一;局部变量在其所在函数体中唯一;static变量在其所在文件内唯一)

        .ent           对程序而言没有实际意义,只是告诉assembler将这一点标志为“name”函数的起始点,为调试提供信息。

        .name       将其所在地址命名为“name”,作为assmbler的输出。名为“name”的函数调用将从该地址开始。

END定义了两个assembler需要的信息,都不是必须的。

#define     END(name)      \

        .size          name,       .-name;     \

        .end          name

        .size          表示在symbol table中,“name”函数体的大小(字节数)将与“name”符号一道列出。

        .end          指出函数尾。调试用信息。

 

.set           伪操作符(directive),用来告诉assembler如何编译。

在本例中,.noreorder表示禁止对代码重排序,让代码严格保持其书写的顺序,否则MIPS assembler会尝试将代码重新排序——填补那些delay-slot以获得较好的运行效率。Nowarn要求assembler不要费心去指出那些应该被重排序的地方,相信程序员已经处理好这些事情了。通常这不是个好主意——除非你确信你肯定正确。基本上这是个不必要的directive

Labels:“1:”是数字标志label,大多数的assembler都会把它当作**局部**label来处理。像“1:”这种label,在程序里你想用多少都可以:你可以用“1f”引用reference下一个“1:”;用“1b”来引用前一个“1:”。这会很常用。

Instructions:一些指令的顺序会有出乎预料的问题,你必须注意。.set noreorder这一directive使得delay-slot问题变得非常敏感而容易出问题,我们必须确保load的数据不会马上被下一条指令用到。比如说:

        bne           t0,    t1,.t01

lbu             t2,    -1(a0)

        ……………

.t01:

        j               ra

subu          v0,   t0,t1        

这里lbu            t2,    -1(a0)一句中,用t2不能用t0,因为要执行的下一条指令subu             v0,   t0,t1         中要用到t0

 

好,已经看过了一个例子,让我们再看一些语法方面的东西。

 

9.2      语法概要Syntax Overview

在附录B中你可以找到MIPS汇编器的语法列表,大多数的其它厂商的编译器也都遵循这个列表的规则。当然,可能少数的directive的具体含义会有少许的差别。如果你以前在类unix(unix-like)的系统上用过assembler,那这个列表你应该会很熟悉。

 

9.2.1    Layout,     Delimiters,       and Identifiers

首先你得熟悉C语言,如果你熟悉C,那幺注意,汇编代码与C代码有一些区别。

汇编代码以行为分界,换行(end-of_line)表示一个指令或伪操作符directive的结束。你也可以在一行里写多条指令或伪操作符,只要它们中间用“;”隔离开来。

以“#”开头的行是注释,assembler将忽略它。但是**不要把“#”放在行的最左面**:这将激活C预处理器cpp(C preprocessor),有时候你可能会用到它。如果你确定你的代码会经过C预处理器的预处理,那幺你可以在你的汇编代码中使用C风格的注释方式:“/*…*/”,可以跨越多行,只要你乐意。

变量和label的名字(identifiers)可以随意——只要在C语言里合法就行,甚至可以包含“$”和“。”。

在代码中你可以使用0~99之间的数字作为label,它会被视为临时性的符号,所以你可以在代码中重复使用同一个数字作为label。在一个分支指令(branch instruction)中“1f”指向下一个“1:”,而“1b”指向前一个“1:”,这样就不用费心为那些随手而写的跳转和循环起名字了,省下这些名称可以去命名那些子程序、还有那些比较关键的跳转。

MIPS/SGI assembler通过C preprocessor的宏定义来提供寄存器的俗称(conventional name)($zero,t0,~,ra),所以你必须用C preprocessor来对你的汇编代码进行预处理,为此需要在代码中包含include头文件mips/regdef.h。虽然说规范的assembler通常可以识别这些寄存器的俗称,但是为了代码的通用性起见,还是不要把宝压在这上面为好。

assembler的定位计数器指向正在编译的当前指令的地址,你可以在汇编代码中引用assembler的定位计数器的值。标识符“。”代表assembler当前的定位计数器的值。你甚至可以对它做有限的一些操作。在上下文中,label(或者其它什幺可复位位的符号relocatable symbol),将被替代为它的地址。

$:类似于armadds r0,pc,symbol address - (+8)这样的操作。)

        固定字符和字符串的定义方式与C相同。

 

9.3指令规则 General Rules for Instructions

Mips assembler允许一些指令的简略写法。有时候,你提供的操作数operand少于机器码所要求的,或者机器码要求使用寄存器而你却使用了常数,在某些情况下,assembler也会允许这种写法,并自动进行调整。你将会发现,在真正的汇编代码中这种情况非常频繁。这一节我们将讨论这个问题。

 

9.3.1    寄存器间运算指令

Mips 的运算指令有3个操作数。算术arithmetical或逻辑logical指令有2个输入和一个输出,例如:Rd        =      rs     +      rt,被写成addu     rd,    rs,rt

        这里的3个寄存器可以重复(例如addu      rd,    rd,rd)。在CISC-stylecpu(例如intel386)指令中,只有2个操作数,Mips assembler也支持这种风格的写法,目的寄存器destination register可以同时作为一个源操作数source operand:例如:addu    rd rs,这与addu         rdrdrs相同,assembler将自动将它转换成后者。

        Mips assembler提供的指令集中有一些伪指令unary operation,比如Negnot,这些伪指令实际上是一条或多条机器指令的组合。对这些指令,Assembler最大接受2个操作数。Negu   rd rs实际上被转化为subu        rd zero,rs,而not        rd将被转化为or    rdzerors

        可能最常用的寄存器间操作register-register operation要算是move  rdrs了。这条指令实际上是or rdzerors

 

9.3.2:   带立即数的运算指令

assembler和机器语言里,嵌入在指令中的常数被称为立即数immediate value。很多Mips的算术和逻辑指令都有另外一种形式,这种形式里rt寄存器被一个16bit的立即数所取代。在cpu的内部运算过程中,这个立即数将被扩展为32bit,可能是符号扩展sign-extend$:用最左面的bit(bit15)填充扩展的高16bit),也可能是零扩展zero-extend$:用0填充扩展的高16bit)——这取决于具体的指令。一般而言,算术指令进行符号扩展sign-extend,而逻辑指令进行零扩展zero-extend

在机器指令的概念上,即便执行同一种运算,操作数中是否包含立即数的区别,将导致两条不同的指令(例如addaddi)。尽管如此,对于程序员而言,还是没有太大的必要去具体的区分那些包含立即数的指令。Assembler会找出它们,并进行转换。比如:

addu          $2$464    ————————>      addiu        $2$464

如果立即数过大而超过了16bit所能表达的范围,机器码中将无法容纳,这时assembler会再次帮助我们:它会自动将立即数载入“编译用临时寄存器assembler temporary registerat/$1中,然后进行如下操作:

add   $4        0x12345  —————————>          li       at0x12345

                                                                                 add  $4$4at

注意这里的“li(load immediate)指令,在cpu提供的机器指令集中你找不到它。这是一个及其常用的宏指令,用来把32bit整数装载入寄存器,而不用程序员来操心怎幺去实现这一动作:

当这个32bit整数值介于-32k~+32k之间,assembleraddiu指令配合zero寄存器来实现“li”;

16-31bit0时,用ori指令来实现“li”;

0-15bit0时,用lui指令来实现“li”;($:运算指令(oriaddiu)要比存取指令(swlui)的处理速度快)

如果以上条件都不成立,那只好用lui/ori两条指令来实现“li”了:

li        $3-5                     ——————>              addiu        $3$0-5

li        $408000              ——————>              ori            $4$008000

li        $5120000            ——————>              lui             $50x12

li        $60x12345          ——————>              lui             $60x1

ori               $6$60x2345

9.3.3    关于32/64位指令

我们在前面(2.7.3)讲过可以对32位指令的机器码进行符号扩展到64位,以保证32位的程序(mipsII)在老的机器上能正常运行。

 

 

9.4 地址模式

        前面提到过,mips cpu硬件上只支持一种地址模式:寄存器基地址+立即数偏移量base_reg+offset,偏移量必须在-32768+32767之间(16bit带符号整型所能表示的范围)。但是assembler可以通过一些方式来支持以下几种地址模式:

        Direct:由你提供的数据标号或外部变量名。

        Direct+index:一个偏移量,加上由寄存器指出的标号(label)地址。

        Constant:一个数字,作为一个32位的绝对地址(absolute address) 处理。

        Register indirect:是寄存器加偏移量的特殊形式:偏移量为0

       

 

 

 

9.5 assembler directives

MIPS中所有指令都被塞在32bit空间里的做法导致了一个明显的问题:访问一个确定的/嵌入在指令以内的?????(compiled-in location)内存地址往往要花费至少两条指令.例如:

Lw           $2,   addr -------------à         lui     at,    %hi(addr)

                                                                Lw   $2,   %lo(addr)(at)

在大量使用全局或静态变量的程序中,这一缺陷往往导致最后编译出的代码臃肿而低效.

早期的MIPS编译器引入了一种技术以弥补以上缺陷,这项技术被以后的MIPS编译工具链toolchain一直沿用下来,它通常被称为全局量指针相对寻址”gp-relative address.这个技术要求compiler,assembler,linker以及运行时激活代码(runtime startup code)偕同配合,把程序中的变量和常数汇集到一个独立的内存区间;然后设置register $28(通常称为全局量指针global pointer或简写为gp)指向该区间的中央(linker生成一个特殊符号_gp,其地址为该区间的中央,激活代码负责将_gp的地址加载到gp寄存器,这一动作在第一个load/store指令运行之前完成).只要这些全局变量\静态编量\常量加起来不占用超过64k大小的空间,这些资料相对该区间的中点也就不超过正负32k(偏移量15bit+符号位1bit,参见mips机器码格式),那幺我们就可以在一条指令中完成对它们的load/store操作:

        lw     $2,   addr         ------à           lw     $2,   addr-_gp(at)

一个问题是在编译彼此独立的模块的时候,compilerassembler如何决定哪些变量需要通过gp来寻址,通常的做法把所有小于某个特定长度(通常是8byte)的对象放进该区间,这个长度可以通过compiler/assembler”-G n”选项来控制.特别需要指出: ”-G 0”将取消gp-relative寻址方式.

上面所说的gp-relative寻址是一种非常有用的技巧,然而在使用中会有一些陷阱值得注意.在汇编代码中声明全局量的时侯你最好小心点:

可写,且初始化过的小对象体writable,initialized small data必须显式的声明在.sdata段中.( “小对象体一词中的含意即为上面提到的长度小于8byte”)

声明全局对象时必须指出其长度.

.comm.     smallobj,   4

.comm.     bigobj,      100

声明小外部变量时同样需要指出其长度.

Extern      smallext,   4

大多数assembler不会对对象声明作辅助性的处理(如指出该对象的长度).

C代码中的全局变量,必须在所有使用它的模块中被声明.对于外部队列,你可以显式的指出它的长度,:

Int cmnarray[NARRY];

也可以不指出其长度:

extern       int     exarray[];

有时候,程序运行的方式(环境)决定了不能采用gp-relative寻址方式.比如一些实时操作系统,还有很多固化环境下的程序(PROM monitor)是用一块单独连接link的代码实现来内核kernel,应用程序直接使用子函数(而不是通常的系统调用)调用到内核中去.这种情况下无法找到一个和适的方法以使gp寄存器在内核和应用程序的两个小数据段.sdata中来回切换,所以内核与应用程序两者之一(没必要两个都这样做)必须使用”-G 0”选项来进行编译.

当使用”-G 0”选项编译模块的时候,那些需要与该模块连接的库library通常也应该使用”-G 0”选项编译.在资料是否应该放在.sdata的问题上, 模块和库的声明应该彼此一致,如果发生冲突的话,linker将无法判定资料应该放在小数据段还是普通数据段,这时linker会给出奇怪而毫无价值的错误信息.

 

9.5  Assembler  Directives

在一开始我们就已经提到过”directive”,你也可以在附禄B里找到它的清单,不过没有详细介绍.

9.5.1 段的选择

通常的数据段和代码段的名字以及对它们的支持在不同编译工具链上可能会不一样.但愿大部分至少能够支持一般的MIPS通用的段,见图9.1.

在汇编代码中,以如下方式来选择段:

.text, .rdata, and .data

简单的把适当的段名放在数据和指令之前,就象下面的样子:

.rdata

msg: asciiz        “hello world!\n”

 

        .data

table:

        .word       1

.word       2

.word       3

 

        .text

func:sub    sp,    64

………………

 

.lit4 and .lit8 : 隐式浮点常数段floating-point implict constants

你不能像directives一样写这些段.它们是由assembler隐式创建的只读数据段,用来放置li.sli.d宏指令中的浮点常数型参数.一些assemblerlinker会合并相同的常数以节省空间.

.bss, .comm., and .lcomm data

这个段名也不用作directive,它用来收集在C代码中声明的所有未初始化的资料.C的一个特点是: 在不同的模块中可以有同名的定义.只要其中被初始化的不要超过一个..bss段用来收集那些在所有模块中都没有初始化过的数据.fortran程序员可以认为这个就是fortran语言中的.common,虽然名字不一样.

你必须声明每个资料的长度(单位为byte),当程序被连结的时候,它就可以得到足够的空间(所有声明中的最大值).如果有任何模块把它声明在初始化过的数据段,这些长度将被用到,并且使用如下声明:

.comm.     dbgflag,    4              #global common variable, 4 bytes

.lcomm.    sum, 4              #local common variable, 8 bytes

.lcomm.    array,        100          #local common variable, 100 bytes

未初始化uninitialized”这一说法实际上并不准确:虽然这些段在编译出的目标文件中是不占地方的,但是在执行你的程序之前,运行时激活代码run-time startup code或操作系统会将.bss段清零---------很多C程序都依赖于这一特性.

.sdata, small data, and .sbss

这些段被编译工具链用作单独放置小资料对象的.data.bss.MIPS编译工具链进行这个处理是因为,对一个足够紧凑的小资料对象段可以进行高效率的load/store操作,其原理是在gp寄存器中保存一个资料指针,具体说明见本书9.4.1章节.

注意,.sbss不是一个合法的directive;放在.sbss段中的资料满足两个条件:1,.comm.lcomm声明;2,其长度小于”G n”编译选项所指定的长度(默认为8byte).

.section

开始一个任意名称的段,并提供旗标(可能在代码中提供,也可能是工具包提供? Which are object code specify and probably toolkit specific).查看你的工具包的说明手册,????????????????????

如图9.1所示的结构可能适合做为一个运行在裸机bare cpu上的固化程序ROM program.只读段倾向于放在远离下部可擦写区间的内存位置上.

heap和栈stack并不是真正的能被assemblerlinker所识别的段.一般的,它们在运行时由运行系统???????run-time system初始化和维持.通过把sp寄存器设置为该程序可用内存的最高地址(8byte对齐)的方式来定义栈;堆通过一个由类似于malloc函数使用的全局指针变量来定义,通常初始化为end符号symbol,symbollinker赋值为已声明变量的最高位置.

 

特殊符号

9.1显示了一些由linker自动声明的符号,以便程序找到自己各个段的起始\结束的位置.这最初只是习惯,后来在unix类的系统上得到发展,其中一些是MIPS环境中所独有的.你的工具包手册上可能定义了他们中的一部分或全部;下面打@的表示肯定会被定义的符号:

symbol                     standard                                   value

_ftext                                                代码段起始点

etext                 @                    代码段结束点

_fdata                                               数据段起始点

edata                        @                    数据段结束点

_fbss                                                未初始化段起始点

end                           @                    未初始化段结束点

(end通常也就是程序image的结束点)

9.5.3        资料的定义与对齐

选择好正确的段之后,现在你需要用下面所说的directive来定义资料对象本身.

.byte, .half, .word, and .dword

这些directive产生1,2,4,8byte长度的整数(有些工具链-----即便是64位的-------没有提供.dword directive).可以跟随着一个值的列表,彼此以逗号分离,可以在值的后面加冒号并跟随一个重复计数,以表示连续几个相同的值,如下(word=4byte):

        .byte 3                      #1 byte:                    3

        .half  1,2,3                #3 halfwords:    1      2      3

        .byte 3                      #5 words:         5      5      5      6      7

注意数据的位置(相对于段的起始处)在资料被输出之前自动对齐到合适的边界.如果你确实需要输出未对齐的资料,那幺必须自己使用西面要讲到的.align directive来说明.

.float.double

这些directive输出单精度\双精度的浮点值.如下:

.float         1.4142175                       #1个单精度浮点数

.double             1e+10,     3.1415             #1个双精度浮点数

与对整数处理相同,可以使用冒号表示重复.

.ascii.asciiz

这些directive输出ASCII字符串, 附带\不附带结束标记.如下两行代码输出相同的字符串:

.ascii         “Hello\0”

.asciiz               “Hello”

.align

这个directive允许你为下一个资料指定一个大于正常要求的对齐边界??????alignment.alignment表示为2n次方.

.align        4              #对齐到16byte边界 (2^4)

var:

.word       0

如果标志(上例中的var)后面紧跟着.align,那幺这个标志仍然可以被正确的对齐,例如下面的例子与上面的例子作用相同:

var:

        .align        4      # 对齐到16byte边界(2^4)

        .word       0

对要求紧凑结构????packed的数据结构,这个directive允许你取消.half,.word的自动对齐功能,你可以指定它为0对齐,它将持续作用直到下个段开始.??????

.half  3              #正确对齐的半字

.align        0              #关掉自动对齐功能

.word       100          #按照半字对齐的字.

.comm.lcomm

通过指定对象名和长度来声明一个common或者说未初始化的资料对象.

.comm声明的对象对所有声明过它的模块有效,它的空间由linker分配,采用所有声明中的最大值.但是,如果其中有任何一个模块将其声明在.data,.sdata,.rdata,那幺所有的长度声明都将失效,而采用初始化定义取而代之.

??????.comm的用途是为了避免以下情况:一个资料对象要在许多文件中用到,但它与其中的每一个文件都没有更特殊的联系,但是我们不得不在某个文件中声明它,这样就造成了不对称.但是它确实存在,因为fortran就是用这样的语意来定义它的,而我们想要经过汇编语言来编译fortran程序(比如查看fortran程序编译出来的汇编代码).

.lcomm声明的对象是局部对象,assembler.bss段或.sbss段为其分配空间,但是在所属模块之外它不可见.

.space directive增加当前段的空间计数,例如:

struc:        .word       3

        .space      120          #空出120byte大小的空间

.word       -1

对通常的资料\代码段而言,这个空出的空间用0填充,如果assembler允许你声明内容不在对象文件中定义的段(.bss),这个空间只影响连续的符号\变量之间的偏移.

9.5.4        符号绑定属性        symbol-binding attributes

符号symbol(在数据段或代码段中的标志)可以被调节为可见?????,并可以供linker使用以便将几个分离的模块编译成一个完整的程序.

符号有三个级别的可见度:

局部:

除了声明它的那个模块,它对外部而言是不可见的,并且不会被linker使用.你不用担心在其它模块中是否适用了相同的符号.

全局:

这些是公开的符号以供linker使用.使用.extern 关键词,你可以在其它模块中引用全局符号,而不必为它定义本地空间.

弱全局weak global:

这个晦涩的概念在一些工具链中以关键词.weakext实现.它允许你定义一个symbol,如果有同名的全局对象存在,那幺就把它连结到这个同名的全局对象;如果不存在同名的全局对象,那幺它就作为一个局部对象存在.如果.comm段存在,你就不应该用弱全局这个概念.????

.globl

C语言环境下,除非用static关键词进行声明,否则模块级的数据和函数入口都默认为全局属性.C语言不同,一般在汇编语言环境下,除非使用.globl directive显式的进行声明,否则标志label默认为局部属性.对于用.comm声明的对象不需要再使用.globl,因为它们已经自动具备全局属性.

        .data

        .globl        status                #全局变量

status:       .word       0

 

        .text

        .globl        set_status         #全局函数入口

set_status:

        subu sp,    24

………………..

.extern

如果引用当前模块中未定义的标志,那幺(assembler)将假定它是在其它模块中定义的全局对象”(外部变量).在一些情况下,如果assembler能知道所引用的对象的长度,它就可以生成更优化的代码(9.4.1).外部变量的长度用.extern directive来指明:

.extern      index,       4

.extern      array,        100

lw             $3,index           #提取一个4-byte(1-word)长度的外部变量

lw             $2,array($3)             #提取100-byte长度外部变量的一部分

sw            $2,value                   #装载一个未知长度的外部变量

.weakext

一些assembler和工具链支持弱全局的概念,这允许你为一个符号symbol指定一个暂时性的绑定(binding,是一个连接用的概念,指符号与其内存地址间的对应关系?????),如果存在一个正常全局(强全局)对象定义,那幺它将把先前的这个弱全局绑定覆盖.例如:

.data

.weakext      errno

errno:       .word       0

 

.text

        lw     $2,   errno                #可能使用局部定义,也可能使用外部定义.

如果没有其它模块使用.globl来定义errno,那幺这个模块------还有其它模块------将会使用上面代码中errno的局部定义.

另外一个可能的用法是:用一个名字声明一个局部变量,而用另一个名字声明它另外的弱全局身分.

        .data

myerrno:   .word       0

        .weakext  errno,       myerrno

 

        .text

        lw     $2,   myerrno                    #总是使用上面的局部定义

        lw     $2,   errno                        #可能使用局部定义,也可能使用其它的

#(外部定义)

9.5.5        函数directive

 

 

See MIPS Run
翻译Alan Yao
10 MIPS 上的C 语言编程................................................................................................. 1
10.1 堆栈子程序链接参数传递............................................................................ 2
10.2 堆栈参数结构.................................................................................................... 2
10.3 使用寄存器传递参数......................................................................................... 3
10.4 C 库范例............................................................................................................ 3
10.5 一个特殊的例子传递数据结构........................................................................ 4
10.6 传递不定数量的参数......................................................................................... 5
10.7 函数的返回值.................................................................................................... 6
10.8 扩展的寄存器使用标准SGI n32 和n64............................................................ 6
10.9 堆栈布局堆栈帧辅助调试器........................................................................ 9
10.9.1 leaf 函数................................................................................................. 10
10.9.2 nonleaf 函数.............................................................................................11
10.9.3 复杂堆栈请求的堆栈帧指针................................................................... 13
10.10 可变长度参数列表......................................................................................... 16
10.11 不同线程间共享函数和共享库的问题............................................................. 17
10.11.1 单一地址空间的代码共享..................................................................... 17
10 MIPS 上的C 语言编程
本章主要讨论用C 语言建立完整的MIPS 系统可能需要具备的一些知识因此更多时
候本章讲述C 编译器产生的汇编语言代码而不是C 语言代码为避免讨论过于繁琐而
使本章膨胀到一本新书的规模现假定读者您是第一次向MIPS 平台移植代码
一个高效的C 运行环境依赖于C 语言程序的寄存器使用约定这一般由C 编译器强制
规定因此对于汇编工程师来说也是需要强制遵守的参照2.2.1 部分对寄存器使用的全
部约定本章内容涉及
l 堆栈子程序链接参数传递关于MIPS 进程是如何实现的以及如何为避免不
必要工作而支持的各种特性
l 共享库和非共享库关于在复杂机器上支持共享库OS 的一点注解
l 介绍编译器的优化可能对MIPS 上C 语言编程造成的影响
l C 语言访问设备的提示关于如何写绝大多数设备驱动
即使你使用其他的高级语言而非C 语言只要你想为MIPS 编译代码并与标准库链接
那么本章的大多数内容还是对你有所帮助的在这儿我并没有针对特定编程语言是因
为我对他们了解不够一直不知道如何恰当的点到为止
10.1 堆栈子程序链接参数传递
许多MIPS 程序使用混合语言编写的对于嵌入式系统的程序员这最可能是在C(或
C++)中加入汇编语言
一开始MIPS 社团建立了一套约定用来规范如何传递参数给函数在C 语言中称
为子例程或子程序和从函数返回值这个约定看起来很复杂其实只是为了逻辑
上遵循文档规则而使文档太过庞大而已
基本原则是所有参数在堆栈中的一个数据结构中分配空间只有少数堆栈开始部分的内
容可以装入CPU 寄存器相应的内存空间将变得是没有定义的实际上这意味着对于
大多数调用参数全部传递到寄存器中然而堆栈数据结构是理解进程的最好切入口
自从1995 年Silicon Graphics 开始已经为了提高性能而对调用约定作了修改并对这
些修改作如下命名
l o32 传统的MIPS 约定o 是old 简写详细说明如下这个约定不包括SGI
为支持共享库而添加的一些特性目前还是嵌入式工具相当常用的不过过不了多
久两个最新的约定将会被其他工具作为可选项加以支持
l n64 针对64 程序的约定SGI 的64 模式意味着指针和C 语言的long 整数类型都
是64 位的对于嵌入式应用程序更长的指针意味着越界这样会产生疑惑这
个约定现在是否用在了工作站环境上不过n64 改变了使用寄存器的约定和参数
传递规则因为n64 将更多的参数放进了寄存器从而提高了性能
l n32 在参数传递上采用了n64 的规则不过指针和C 语言的long 整数类型都是
32 位的然而SGI 和其他编译器支持扩展的long long 整数类型从而实现硬件
支持的64 位整数这个编译模式在嵌入式系统变得很流行
这里先描述o32 标准然后指出n32 和n64 的差异部分10.8 节
这本书出版时还有其他有争议的标准不过大多数和MIPS EABI 相类似所有
的MIPS EABI 项目的目的是要产生一个范围更广的标准以便嵌入式工具能相互工作
的更好这是个绝好的主意不过这个新的调用约定是从类似SGI n32 的私人项目继承
来的虽然简单但没有很好的兼容性我们虽然很希望这能产生很好的结果但是
目前在嵌入式应用中合理使用o32 编译模式也不会失去什么
10.2 堆栈参数结构
从这节开始将陆续介绍SGI 称为o32 的原始MIPS 约定并在10.8 节才开始明确介
绍新约定变化
MIPS 硬件不直接支持堆栈但是调用约定需要堆栈是向下延伸的当前堆栈的底保
存在寄存器sp($29)中任何提供保护和安全的OS 都不支持用户堆栈而且除非在函数调
用的地方sp 的值没有价值但是约定还是在函数使用的堆栈的最下方保留了sp
在函数调用的地方sp 必须是8 字节对齐的对于32 位MIPS 硬件不是必须的但
是对于64 位CPU 是必须的子程序总是将堆栈指针调整为8 的倍数然后填到sp 中
(注SGI 的n32 和n64 标准调用的堆栈都是以16 字节对齐的方式维护的)
在MIPS 标准中为了调用子程序调用者在堆栈中建立一个数据结构来保存参数并
将sp 指向这个数据结构第一个参数在C 程序中位于最左的在内存中是处在最下方
每个参数至少占据一个word 32 位64 位的值比如浮点double 类型和(对于某些CPU)64
位整数必须是8 字节对齐的就好像是个包含64 位纯量场scalar field 的数据结构
参数结构就像一个C 的struct 但是会有更多的规则首先为任何调用分配一个至少
16 字节的参数空间即使没有参数其次char 和short 等比word 类型短的数据类型以
一个int 类型32 位传递不过这种处理方式不能用于struct 内部
10.3 使用寄存器传递参数
任何位于参数结构开头16 字节内的参数都被传递到寄存器中调用者可以不明确定义
参数结构中的这16 字节存在于堆栈中的参数结构必须保存下来如果必要被调用的函
数有权把寄存器中参数的值存回到内存中可能是在C 中参数是一个指向变量的指针
除非调用者确认数据存在浮点寄存器(FP)中更适合否则四个寄存器中参数分别存在
a0-a3($4-$7)中
决定何时以及如何使用FP 寄存器的标准是很特别的就风格的C 没有内建机制检查调
用者和被调用者协调函数每个参数的类型为了帮助程序员避免混乱调用者将参数转换成
固定类型整数用int 浮点数用double 对于分不清整数和浮点数的程序员实在是没有
办法了不过这样至少减少了一些混乱
现代的C 编译器使用函数原型定义所有参数类型并能让所有调用者看到即使这
样还会有程序的参数类型在编译的时候是不确定的比如著名的printf() printf()是在运行
时才确定自己参数的数量和类型
MIPS 制定了如下的规则
除非第一个参数是浮点类型否则不能将后续参数传递到FP 寄存器中这样可以保证
printf()等传统函数能正常工作printf()第一个参数是指针所以所有后续参数都分配到整数
寄存器中printf()因此能够在参数数据中找到所有参数不考虑参数类型这个规定不会
使普通的数学函数效率下降因为这些函数大多数使用浮点参数
如果第一个参数是浮点类型那将会传递到FP 寄存器中这样参数结构前16 字节的
其他后续浮点参数也会被传递到FP 寄存器中两个double 占据16 字节因此只有两个FP
寄存器fa0-fa1 或$f12-$14 用于参数定义显然不会有人认为函数明确定义大量的单精度
参数是常见的事而非要定义一个新的规则来处理
另外一个比较特别的是定义一个函数它的返回结构大于正常使用的两个寄存器那
么返回值约定要求产生一个指针作为参数指向这个结构并放在其他普通参数前传入详
见10.7 节
如果需要写一个调用约定不是很简单和显而易见的汇编程序可以先用C 编写程序
并用编译器带上-S 选项产生汇编文件以此作为模板
10.4 C 库范例
这里举个例子
thesame = strcmp ( “bear” , “bearer” , 4 ) ;
在figure 10.1 中画出了分别参数结构和寄存器内容这里没有参数数据是放在内存中
的在后续部分会举出那方面的实例
堆栈位置 内容 寄存器 内容
------------------------ ------------------------------------------------
sp+0 | undefined | a0 | address of “bear” |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+4 | undefined | a1 | address of “bearer” |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+8 | undefined | a2 | 4 |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+12 | undefined | a3 | undefined |
------------------------ ------------------------------------------------
FIGURE 10.1 参数结构三个非浮点操作符
参数数据不足16 字节所以参数都存在寄存器中
看起来决定将三个参数放在普通寄存器中式荒谬的复杂方法但是在看看数学库中一
些巧妙方法
double ldexp ( double , int ) ;
y = ldexp ( x , 23 ) ; /* y = x * ( 2 ** 23 ) */
Figure 10.2 显示了相应的参数结构和寄存器值
堆栈位置 内容 寄存器 内容
------------------------ ------------------------------------------------
sp+0 | undefined | $f12 | (double) x |
sp+4 | | $f13 | |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+8 | undefined | a2 | 23 |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+12 | undefined | a3 | undefined |
------------------------ ------------------------------------------------
FIGURE 10.2 参数传递一个浮点参数
10.5 一个特殊的例子传递数据结构
C 允许使用数据结构类型作为参数实际上传递的是数据结构的指针不过C 语言同
时支持这两种方式为了适应MIPS 的规则传递的数据结构参数只能是参数结构的一部
分在一个C 的数据结构中byte 和halfword 域会共用一个word 的位置存放在内存中因
此当通过寄存器传递堆栈中参数结构的参数时也必须这样处理
因此如果是这样
struct thing {
char letter ;
short count ;
int value ;
} = { “z” , 46 , 100000 } ;
( void ) processthing ( thing ) ;
那么 将会产生Figure 10.3 显示的参数结构
堆栈位置 内容 寄存器 内容
------------------------ ------------------------------------------------
sp+0 | undefined | a0 | ”z” | x | 46 |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+4 | undefined | a1 | 100000 |
------------------------ ------------------------------------------------
FIGURE 10.3 传递数据结构作为参数
注意因为MIPS 的C 数据结构以域分布的内存中的顺序和定义数据结构时的顺序是
一致的不过填充时要尽量满足对齐原则这些域存放在寄存器中的位置是遵循load/store
指令的字节顺序因CPU 的字节序而异Figure 10.3 是针对大字节序CPU 的因此数据结
构中的char 值是在放置参数的寄存器最高8 位并和short 对齐
如果真想传递数据结构作为参数而且一定包含短于short 的数据类型那就应该测试
一下这种情况看看编译器是否处理正确
10.6 传递不定数量的参数
对于传递的参数数量和类型在运行时才能确定的函数约定对他们的限制是很严格的
考虑这样的例子
printf ( “ length = %f , width = %f , num = %d \ n”, 1.414 , 1.0 , 12 ) ;
根据前面的规定参数结构和寄存器的内容如Figure 10.4 所示
堆栈位置 内容 寄存器 内容
------------------------ ------------------------------------------------
sp+0 | undefined | a0 | format pointer |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+4 | undefined | a1 | undefined |
------------------------ ------------------------------------------------
------------------------ ------------------------------------------------
sp+8 | undefined | a2 | (double) 1.414 |
sp+12 | | a3 | |
------------------------ ------------------------------------------------
------------------------
sp+16 | (double) 1.0 |
sp+20 | |
------------------------
------------------------
sp+24 | undefined |
------------------------
FIGURE 10.4 printf()参数传递
这里有两件事需要注意首先sp+4 中存放的内容需要和double 类型值对齐在C 规
则中浮点参数需要以double 类型传递除非通过类型说明和函数原型做出明确的定义
注意8 字节内容会导致浪费掉一个标准参数寄存器译者注比如Figure 10.4 中的a1
第二第一个参数不是浮点参数根据前述规则不能给参数分配FP 寄存器因此
第二参数的数据和内存中的一致只能存放在a2-a3
这看似简单但却是非常实用的
printf()子程序定义用到的宏是在stdarg.h 中定义stdarg.h 提供可移植的接口用于寄存
器和堆栈关于对不定数量和不定类型的操作符进行的操作printf()解析所有参数是通过获
取第一参数的地址和第二个参数并在内存中向上寻找参数结构来实现的
未达到这样的目的需要C 编译器将printf()的a0-a3 寄存器放到参数结构的范围内一
些编译器会检查是否获得参数的地址并满足这样的隐性要求ANSI C 编译器函数定义中
使用来做出这样的提示其他编译器可能使用讨厌的pragma 来提示幸运的是
不久stdarg.h 的宏中就不会有它了
现在可以看出将double 类型值放入整数寄存器的必要性了这样stdarg 和编译器只要
将a0-a3 存放到参数结构的前16 字节而不用考虑参数的数量和类型
10.7 函数的返回值
函数返回的整数或指针类型通常是放在寄存器v0($2) 虽然大多数编译器都不会用到
寄存器v1($3) 不过再MIPS/SGI 定义的预定中寄存器v1($3)是保留不用的不过在32
位模式当中返回64 为非浮点类型的数据时会用到这个寄存器有些编译器定义64 位数据
类型(通常是long long) 这时也会使用v1 寄存器以返回一个64 位的数据结构值而避免32
位的限制
所有浮点类型的值都放在寄存器$f0 中返回在32 位的CPU 中双精度的值也默认使
用寄存器$f0
如果C 中的函数返回的数据结构太大不能通过v0-v1 返回就需要作额外处理这时
调用者要在堆栈中为这个数据结构预留一些空间并用一个指针指向这个空间被调用的函
数将返回值拷贝到这个地方一般的参数规则要求在函数调用时将这个指针放在寄存器
a0 传入中放在v0 中返回
10.8 扩展的寄存器使用标准SGI n32 和n64
在调用约定和寄存器使用上n32 和n64 的ABI 是一致的注不同的是在n64 中
long 和指针类型都是64 位的而在n32 中只有long long 类型是64 位的
尽管保持寄存器使用约定的一致性时非常有益的事但是o32 和n32/n64 有很大的区别
用不同的方法编译函数并且不能成功链接到一起归纳一下n32/n64 的新规则主要有以
下几点
l 提供至多8 个参数通过寄存器来传递
l 参数和用于参数传递的寄存器都是64 位的长度短于整数的类型都将以64 位的
形式操作
l 不需要调用者非寄存器中的参数分配堆栈空间
l 尽可能的通过寄存器传递数据结构和数组传递方法和旧标准相似
l 前8 个参数中的浮点值都通过FP 寄存器传送实际上除了在union 类型和类似
printf()这样参数不确定的函数中数据结构和数组中和double 等长的数据类型都
将使用FP 寄存器
允许传递数据结构和数组后情况变得复杂了不过即使现在没有堆栈空间可以保留
寄存器的使用相对于复杂参数结构还是清晰可见的
n32/n64 废除了o32 中一个约定那就是在类似printf()函数的参数不确定时要求第一
参数必须是非浮点的约定以便区别普通浮点参数的使用情况新约定要求调用者和被调用
函数在编译时有明确的函数数量和类型这是通过函数原型来实现的
n32/n64 有一套不同的寄存器使用约定Table 10.1 罗列出了于o32 的区别唯一的区别
在于单纯的用于临时存储的四个寄存器现在可以传递第5-8 个参数对临时寄存器改变译
者注这里是指o32 中的t0-t3 临时寄存器改成n32/n64 中的a4-a7 参数寄存器显然是没
有必要的我对他们的这个做法有点迷惑不解
TABLE 10.1 新SGI 工具中整型寄存器使用解决方法
寄存器号 名称 用途
$0 zero 始终为0
$1 at 汇编编译器使用的临时寄存器
$2,$3 v0,v1 函数返回值
$4-$7 a0-a3 函数参数
032 n32/n64
名称 用途 名称 用途
$8-$11 t0-t3 临时寄存器 a4-a7 参数
$12-$15 t4-t7 t0-t3 临时寄存器
$24,$25 t8,t9 t8,t9
$16-$23 s0-s7 保全寄存器saved register
$26,$27 k0,k1 留给interrupt/trap 的处理程序使用
$28 gp 全局指针Global pointer
$29 sp 堆栈指针
$30 s8/fp 需要时作为帧指针(Frame pointer) 否则作为附加的保全寄存器
$31 ra 子程序返回的地址
表面上这样可以避免编译器产生的代码丢失曾经存储在四个临时寄存器中的值但实
际上大多数时候编译器可以使用所有参数寄存器和v0-v1 寄存器作为临时存储器达到同
样的效果而且n32/n64 这一修改没有影响保全(saved) 的寄存器(可以假定能从子程
序带回值得寄存器) 注事实并非如此在SGI 计算机上函数使用gp 寄存器帮助实现
代码的位置独立性详见10.11.2 在o32 中每个函数以类似的方法使用gp 这意味着每
个函数调用后不得不恢复gp 寄存器在n32/n64 中gp 被定义成保留未使用 寄存器
在大多数嵌入式系统中gp 是常量因此上述区别只是理论上的
浮点寄存器的约定见Table 10.2 是修改比较大的这没什么可以大惊小怪的因为
MIPS III CPU 有16 个额外的FP 寄存器在旧的系统体系结构中偶数寄存器的使用通常
会附带使用奇数寄存器译者注这样寄存器的长度就被翻倍使用了即2n+0 处的8 字
节内容就放在了2n+0 和2n+1 处的两个4 字节空间里注MIPS III CPU 有个模式切换
使FP 的使用和早期的32 位CPU 兼容n32/n64 假定CPU 不支持这种兼容本来SGI 可
以添加一些新的寄存器保持兼容性但是他们却偏偏要修改大多数现有规则从新开始
TABLE 10.2 o32 和n32/n64 约定中FP 寄存器使用
寄存器号 在032 中的用途
$f0,$f2 返回值fv1 只用于复杂的数据类型不能在C 中使用
$f4,$f6,$f8,$f10 临时寄存器函数中用于不需要保留的值
$f12,$f14 参数寄存器
$f16,$f18 临时寄存器
$f20,$f22, $f24 保全寄存器为了保证函数调用过程中这些寄存器中的值长期有效
$f26,$f28, $f30 在对这些寄存器写操作时必须保存和恢复这些寄存器的值
寄存器号 在n32 中的用途 在n64 中的用途
$f0,$f2 返回值$f2 只用于处理Fortran 复数返回值正好是两个浮点值
$f1,$f3,$f4-$f10 临时寄存器
$f12-$f19 参数寄存器
$f20-$f23 偶数($f20-$f30)是临时寄存器 临时寄存器
$f24-$f31 奇数$f21-$f31 是保全寄存器 保全寄存器
除了可以通过寄存器传递更多参数n32/n64 没有依赖是否第一参数采取浮点类型而制
定新的规则实际上参数根据在参数列表中的位置决定如何分配给寄存器再看看前面的
一个例子
double ldexp ( double , int );
y = ldexp ( x , 23 ); /* y = x * (2**23) */
Figure 10.5 显示了n32/n64 中寄存器和堆栈结构的值
堆栈位置 内容 寄存器 内容
------------------------ ------------------------------------------
sp+0 | undefined | $f12 | (double) x |
sp+4 | | $f13 | |
------------------------ ------------------------------------------
------------------------ ------------------------------------------
sp+8 | undefined | a1 | 23 |
------------------------ ------------------------------------------
FIGURE 10.5 n32/n64 参数传递一个浮点参数
尽管n32/n64 能处理各种浮点和其他类型数据的混合情况但还是把前8 个参数中任何
double 类型放在FP 寄存器中不过这不包含union 类型和参数函数不定的情况这些情
况下也许不是正真的double 类型注意这是基于函数原型机制如果没有函数原型机制
那就会有问题SGI 链接器通常会作检测并给警告信息
10.9 堆栈布局堆栈帧辅助调试器
Figure 10.6 给出了MIPS 函数堆栈帧stack frame 概况图现在起堆栈恢复以前那
种增长模式高内存空间在上面传统MIPS 调用约定要求函数参数前4 个word 要给于保
留而在新的调用约定中只是在需要时分配空间
. . ^
. . |
. . |
|
| 16 字节以外其他参数 | H
i
| 参数4 | g
h
| 参数3 | e
r
| 参数2 |
A
| 参数1 | d
函数入口的sp d
| | | regoffs | 自变量本地变量临时值 | r
| | > e
| | fregoffs | 整型寄存器保存区域 | s
| s
| | 浮点寄存器保存区域 |
| L
| frame size |用于内部被调用函数建立参数的空间>=16 字节 | o
函数运行时sp w
. . e
. . r
. .
FIGURE 10.6 一个nonleaf 函数的堆栈帧
译者注nonleaf 函数是指内部会调用其他函数的函数而其他函数就是深层调用nest call
显灰的部分是被调用函数自己需要的堆栈空间上面的白底区域属于调用者堆栈帧中
所有显灰的部分都是可选的有些函数一个也不需要非常简单的函数是不需要对堆栈作什
么处理的不过在后续部分会举例说明其中的一部分
除了参数布局需要和调用者一致堆栈结构对于函数来说是私有的需要这种标
准布局的唯一理由是用于调试和诊断工具它们常常需要能够操纵堆栈如果调试过程中
中断一个正在运行的函数通常希望能够在堆栈中回溯显示运行到当前断点处所调用的函
数列表和这些函数的参数列表而且希望能够将调试器的上下文回复到堆栈中某个新位置
察看那时某个变量的值即使维护寄存器中变量数值的代码很少经过优化的编译器还是能
够通过这种机制实现这样的目的
为了做出正确的分析调试器必须知道标准的堆栈分布从而得到必要信息能够知道
每个堆栈帧内容的尺寸和内部布局如果一个函数在前面的堆栈中用s0 保存值以便今后
使用那么调试器需要知道到哪里去找这个保存的值
在CISC 体系结构中经常会有一个复杂的函数调用指令来维系类似于Figure 10.6 中的
堆栈帧不过还需要一个附加的帧指针寄存器来存储函数入口的sp 在这样的CPU 中调
用者的帧指针被保存在大家共知的堆栈位置以便允许调试器可以忽略堆栈的内容而只是
分析一个简单的链接列表就能达到目的但是在MIPS CPU 中不需要在运行时间做这些
额外工作大多数时候编译器知道在函数入口降低堆栈指针和在函数返回时增加堆栈指针
那么在这个最小限度的MIPS 堆栈帧中调试器如何知道在何处找到曾经保存的值
呢一些调试器做的非常漂亮甚至通过解析函数前几个指令就能发现堆栈帧的尺寸和返
回地址存放的位置不过大多数工具根据汇编编译器的指示在目标代码中适当的放些堆栈
帧的信息
和汇编编译器相关的这种指示很多因此需要定义一些宏对这些指示的支持做开关切
换这样免除记忆具体细节差异的痛苦并在必要时用于和其他工具间的切换现在大多数
工具都已经使用这些宏了下面的例子使用LEAF 和NESTED 宏来实现这样的目的
对SGI 约定做全面的描述是没有什么必要的下面的例子使用前面推荐的启始宏
和结束宏是和旧版本SGI 工具兼容因此能够被大多数嵌入式工具兼容
关键的指示.frame 和.mask 可以在9.5 节找到详细的说明
为了说明各种问题我们将函数分成三种类型并分别加以讲解
10.9.1 leaf 函数
内部不调用其他函数的函数成为leaf 函数这些函数不用担心建立参数结构和安全维
护没有保存的寄存器t0-t7 a0-a3 v0 v1 数据可以用堆栈存储数据并在寄存器ra 中存
放返回地址在函数运行结束后通过这个地址直接返回注将返回地址存储在别的地
方可能表现得更好但是调试器可能找不到
有时为了优化或实现C 无法实现的功能通常需要用汇编写一些函数这些函数一般
会是leaf 函数许多这些函数根本就不使用堆栈空间声明这样的函数比较简单比如
#include <mips/asm.h>
#include <mips/regdef.h>
LEAF ( myleaf )
< your code gose here >
j ra
END ( myleaf )
大多数工具可以在汇编前通过C 宏预处理器将汇编源码传入unix 风格的工具将
根据文件的扩展名做出判断mips/asm.h 和mips/regdef.h 在定义全局函数和数据的时候非常
有用的宏比如前面的LEAF 和END 同时可以允许直接使用寄存器名字比如用a0 来
代表$4 如果使用旧的MIPS 或SGI 工具上面的那段代码将会扩展成
.globl myleaf
.ent myleaf,0
< your code gose here >
j $31
.end myleaf
其他工具使用的宏可能有所不同不过实现的功能是大同小异
10.9.2 nonleaf 函数
内部调用其他函数的函数称为nonleaf 函数通常这些函数需要在开始处重新设定
sp 寄存器指向所有被其调用函数的参数结构之后的位置同时保存这些被调用函数所用到
的s0-s8 寄存器最新值必须保存ra 寄存器自变量也就是堆栈本地变量和函数在运行
后需要保存值的其他寄存器的堆栈位置如果参数寄存器a0-a3 的只需要保存可以存放
到参数结构的标准位置
注意只设定一次sp 在函数的入口处所有数据在堆栈中的位置都通过sp 加上固
定的偏移来获取
为了解释这点将通过下面这个nonleaf 函数说明联系Figure 10.6 的堆栈帧的分布
图理解
#include <mips/asm.h>
#include <mips/regdef.h>
#
#myfunc (arg1, arg2, arg3, arg4 ,arg5)
#
#framesize = locals + regsave(ra,s0) + pad + fregsave (f20/21) + args +pad
myfunc_frmsz = 4 + 8 + 4 +8 + (5*4) + 4
NESTED( myfunc , myfunc_frmsz , ra)
subu sp,myfunc_frmsz
.mask 0x80010000 , -4
sw ra , myfunc_frmsz-8 (sp)
sw s0 , myfunc_frmsz-12 (sp)
.fmask 0x00300000 , -16
s.d $f20 , myfunc_frmsz – 24 (sp)
< your code goes here , e.g.>
# local = otherfunc(arg5,arg2,arg3,arg4,arg1)
sw a0 , 16 (sp) # arg5(out) = arg1(in)
lw a0 , myfunc_frmsz + 16 (sp) # arg1(out) = arg5(in)
jal otherfunc
sw v0 , myfunc_frmsz – 4 (sp)
l.d $f20 , myfunc_frmsz – 24 (sp)
lw s0 , myfunc_frmsz – 12 (sp)
lw ra , myfunc_frmsz – 8 (sp)
addu sp , myfunc_frmsz
jr ra
END (myfunc)
上面代码开始处显示函数myfunc 有五个参数在函数入口处前四个参数放在a0-a3
中第五个参数放在sp+16 处接下来的这些代码
#framesize = locals + regsave(ra,s0) + pad + fregsave (f20/21) + args +pad
myfunc_frmsz = 4 + 8 + 4 +8 + (5*4) + 4
堆栈帧的尺寸计算方式如下
l local 4 字节在堆栈中而不是寄存器保留一个本地变量可能是需要将这个变
量地址传递给其他函数
l regsave 8 字节用来在寄存器ra 中存放返回地址因为这个函数会在内部调用
其他函数这可能会用到被调用函数的s0 保全寄存器
l pad 4 字节因为后续的fregsave 是双精度浮点寄存器根据规则需要8 字节对
齐因此这里填补一个word
l fregsave 8 字节用$f20 作为被调用函数的保全浮点寄存器
l args 20 字节内部被调用函数需要五个参数不过即使这个nested 函数没有参
数但如果其内部被调用函数是nested 函数则这部分的长度不得低于16 字节
l pad 4 字节堆栈指针也不许是8 字节对齐的因此这里也需要一个word 填补
接下来的代码
NESTED( myfunc , myfunc_frmsz , ra)
subu sp,myfunc_frmsz
在MIPS 公司的工具中这段代码扩展成
.globl myfunc
.ent myfunc , 0
.frame $29 , myfunc_frmsz , $0
subu $29 , myfunc_frmsz
这里声明了myfunc 函数的入口并使它变成全局函数而被访问.frame 告诉调试器这
个函数建立的堆栈的尺寸subu 指令建立堆栈
接下来的代码
.mask 0x80010000 , -4
sw ra , myfunc_frmsz-8 (sp)
sw s0 , myfunc_frmsz-12 (sp)
函数必须保存返回地址和这个堆栈帧中所有被调用函数的保全寄存器.mask 告诉调试
器需要保存的寄存器$31 和$16 也就是ra 和s0 和这些寄存器保存区域顶部相对于堆栈
帧顶部的偏移也就是Figure 10.6 中的regoffs sw 指令保存这些寄存器的值寄存器号大
的存放在堆栈中的位置偏上也就是寄存器位置排列沿内存地址增加方向的接下来的代

.fmask 0x00300000 , -16
s.d $f20 , myfunc_frmsz – 24 (sp)
对被调函数的保全浮点寄存器$f20 和隐含的$f21 做同样的处理.fmask 的偏移对
应于Figure 10.6 中的fregoffs 也就是 自变量区域+整形寄存器保存区域+为了对齐而填补
的word 接下来的代码
# local = otherfunc(arg5,arg2,arg3,arg4,arg1)
sw a0 , 16 (sp) # arg5(out) = arg1(in)
lw a0 , myfunc_frmsz + 16 (sp) # arg1(out) = arg5(in)
jal otherfunc
开始调用otherfunc 函数这个函数的参数2-4 是和myfunc 函数一样的因此不需要移
动就可以直接传给过来而参数1 和参数5 需要调换一下将arg1 在a0 寄存器中拷
贝到输出参数区域sp+16 作为被调用函数的arg5 将arg5 在sp+16 处拷贝到输出参
数1(放在寄存器a0 中)
接下来的代码
sw v0 , myfunc_frmsz – 4 (sp)
otherfunc 函数的返回值存放在自变量本地变量中位于堆栈帧的开始4 字节
最后
l.d $f20 , myfunc_frmsz – 24 (sp)
lw s0 , myfunc_frmsz – 12 (sp)
lw ra , myfunc_frmsz – 8 (sp)
addu sp , myfunc_frmsz
jr ra
END (myfunc)
是做一些函数结束时的处理工作恢复浮点寄存器整形寄存器和存放返回地址的寄存
器弹出堆栈帧并返回
10.9.3 复杂堆栈请求的堆栈帧指针
在前面的堆栈帧描述中编译器能够管理只需保存sp 寄存器的堆栈对于熟悉其他体
系结构的程序员来说经常会使用两个寄存器来管理堆栈sp 指向堆栈底端堆栈帧指针
指向函数在入口处建立的数据结构然而只要编译器能够在函数入口处分配函数所需的堆
栈空间那就能在入口处增加sp 在函数运行期间使它指向一个固定的堆栈偏移地址
如果这样本地堆栈帧中的所有内容在编译时就确定了相对于sp 的偏移量因此不需要
额外的堆栈帧指针但是有时候在运行过程中堆栈指针会出现混乱Figure 10.7 显示了
MIPS 是如何针对这种情况分配堆栈帧指针的
. . ^
. . |
. . |
|
| 16 字节以外其他参数 | H
i
| 参数4 | g
h
| 参数3 | e
r
| 参数2 |
A
| 参数1 | d
fp d
| 自变量(本地变量) 临时值 | r
e
old value s8/fp save here | 整型寄存器保存区域 | s
s
| 浮点寄存器保存区域 |
| 函数所需要的堆栈空间 |
L
|用于内部被调用函数建立参数的空间>=16 字节 | o
函数运行时sp w
. . e
. . r
. .
FIGURE 10.7 堆栈帧使用单独的堆栈帧指针寄存器
什么情况下会用到这种机制呢在一些编程语言中甚至一些C 的扩展语言中会创
建在运行时才确定尺寸的动态变量而且很多C 编译器使用非常实用的alloca()内建函数
在运行时按要求分配堆栈空间这时函数的入口处需要使用额外的s8 寄存器也就是
fp 来保存此时sp 值
既然fp 寄存器也就是s8 是个保全寄存器函数入口处也必须保存它的原有值方
法和在子程序中作为一个变量保存s8 一样在编译时附带了堆栈帧指针的函数里所有对
堆栈帧内部的访问都通过fp 来获取因此编译器可以需要降低sp 为运行时确定尺寸的变
量分配空间
注意对于内部调用其他函数的函数并且这个被调用函数使用太多的参数以至必须
使用堆栈传递参数那么对堆栈帧内部的访问就需要sp 的帮助了
这样设计的一个很大好处在于不管有堆栈帧指针的调用者函数还是在内部被其调用
的函数都会对这部分进行特殊处理对于被调用函数来说因为fp 寄存器是调用者函数
的保全寄存器因此必须保存sp 寄存器的值并在返回是恢复这样对于调用者函数
它所看到的堆栈帧中的这部分内容不会出问题
汇编工程师很高兴看到编译器给函数的巨大参数结构保存空间后还能使通过alloca()
分配空间而返回的地址处于sp 之上
有些工具还使用基于fp 的堆栈帧以便在本地变量很大而导致一些堆栈帧内容离sp 太
远时能通过简单的MIPS load/store 指令只能访问32KB 之间的内容来访问这些内容
现在再来看看前面一节例子的改进主要添加了alloca()的调用
#include <mips/asm.h>
#include <mips/regdef.h>
#
#myfunc (arg1, arg2, arg3, arg4 ,arg5)
#
#framesize = locals + regsave(ra,s8,s0) + fregsave (f20/21) + args +pad
myfunc_frmsz = 4 + 12 +8 + (5*4) + 4
.globl myfunc
.ent myfunc , 0
.frame fp, myfunc_frmsz, $0
subu sp, myfunc_frmsz
.mask 0xc0010000 , -4
sw ra , myfunc_frmsz-8 (sp)
sw fp , myfunc_frmsz-12 (sp) #译者注fp 就是s8
sw s0 , myfunc_frmsz-16 (sp)
.fmask 0x00300000 , -16
s.d $f20 , myfunc_frmsz – 24 (sp)
move fp , sp #save bottom of fixed frame
# t6 = alloca ( t5 )
addu t5 , 7 #make sure that size
#and t5 , ~7 # is a multiple of 8
#subu sp , t5 # allocate stack
#addu t6 , sp , 20 #leave room for args
< your code goes here , e.g.>
# local = otherfunc(arg5,arg2,arg3,arg4,arg1)
sw a0 , 16 (sp) # arg5(out) = arg1(in)
lw a0 , myfunc_frmsz + 16 ( fp ) # arg1(out) = arg5(in)
jal otherfunc
sw v0 , myfunc_frmsz – 4 ( fp ) # local = result
move sp , fp #restore stack pointer
l.d $f20 , myfunc_frmsz – 24 (sp)
lw s0 , myfunc_frmsz – 16 (sp)
lw fp , myfunc_frmsz – 12 (sp)
lw ra , myfunc_frmsz – 8 (sp)
addu sp , myfunc_frmsz
jr ra
END (myfunc)
看看修改过的地方
.globl myfunc
.ent myfunc , 0
.frame fp, myfunc_frmsz, $0
这里不再使用NESTED 宏是因为使用了独立的堆栈帧指针而这指针需要通过.frame
直接明确地进行说明后面需要修改fp 也就是s8 或$30 寄存器因此必须在堆栈帧中保

.mask 0xc0010000 , -4
sw ra , myfunc_frmsz-8 (sp)
sw fp , myfunc_frmsz-12 (sp)
sw s0 , myfunc_frmsz-16 (sp)
接着通过allocal()在堆栈中分配可变大小(t5B)的空间并用寄存器t6 指向这个空间
# t6 = alloca ( t5 )
addu t5 , 7 #make sure that size
#and t5 , ~7 # is a multiple of 8
#subu sp , t5 # allocate stack
#addu t6 , sp , 20 #leave room for args
注意这里通过处理将分配空间的尺寸调整到8 的倍数以便在满足分配空间要求的
同时使堆栈中内容的地址正确对齐另外需要注意的是在堆栈中留出了20B 空间用于
将来调用时存放参数
在为其他函数建立参数时使用sp 寄存器但是在访问自己的参数和本地变量时需
要使用fp 寄存器
sw a0 , 16 (sp) # arg5(out) = arg1(in)
lw a0 , myfunc_frmsz + 16 ( fp ) # arg1(out) = arg5(in)
jal otherfunc
sw v0 , myfunc_frmsz – 4 ( fp ) # local = result
最后在函数返回前恢复堆栈帧指针到在函数入口处所对应的寄存器并恢复这个寄
存器的内容不要忘记恢复fp 寄存器的旧值
move sp , fp #restore stack pointer
l.d $f20 , myfunc_frmsz – 24 (sp)
lw s0 , myfunc_frmsz – 16 (sp)
lw fp , myfunc_frmsz – 12 (sp)
10.10 可变长度参数列表
如果要建立一个参数数目不定的函数需要用到相关工具的stdarg.h 中定义的一套宏
ANSI 兼容工具必须具备的一套宏这套宏定义了va_start(), va_end()和va_arg() 具体
使用可以从Algorithmics 的SDE-MIPS 中实现的printf()
int printf ( const char *format , )
{
va_start ( arg , format );
n = vfprintf ( stdout , format , arg );
va_end (arg );
return n ;
}
一旦调用了va_start() 就能解析出所有的参数在给printf()作参数格式转换的代码中
可以通过下面的方式获得下一个参数这里假设这个参数是双精度浮点类型
d = va_arg( ap , double );
千万不要在汇编程序中建立参数数目不定的函数这会引起麻烦
10.11 不同线程间共享函数和共享库的问题
C 库是包含了一些预编译的模块在编译时将被程序所用到的模块中函数和变量动
态链接到程序的二进制代码中象printf()一类标准的C 函数一般都会包含在C 库中
尽管C 库提供了简单而强有力的方式扩展了C 语言但在多任务操作系统中经常会
引起麻烦通常希望C 库中的函数能和自己写的代码表现一样就象是每个任务自己都
拥有这部分代码拷贝但是为了避免对同一内容多次拷贝而大量浪费内存空间我们总是希
望能够最低限度的共享C 库中的函数代码库函数很庞大广泛使用的X windows 系统的
图形接口函数库就会给MIPS 系统增加300KB 的内容
大多数MIPS 操作系统提供不同任务间共享库代码的机制为了解释共享函数的问题
下面介绍一下函数会用到的不同数据类型
l 只读的数据和代码所有能找到这些数据和代码的线程都可以共享
l 动态数据特定线程能够安全地保持的参数函数变量保全寄存器和函数在堆栈
中保持的其他信息每个任务都要有自己的堆栈空间即使是与其它线程共享一个
地址空间的线程也必须有自己的堆栈在函数返回时这些值都会消失
l 静态暂时数据在函数调用期间不需要保留值的静态数据原则上如果不同的
几个子函数共享一个静态暂时数据可以用动态数据来取代它只不过有点繁琐
这样意味着要重写代码并重新进行编译
l 线程范围数据在库函数调用期间一直存在的静态数据每个不同线程都要有这个
数据的拷贝全局errno 变量就是这样的数据errno 用来处理因调用UNIX 风格文
件IO 操作函数而引起的出错代码
l 全局范围数据库函数管理的用来跟踪系统状态变化的静态数据一旦某个库函数
为了保持多任务状态的信息而启用了这个数据它就会成为操作系统的一部分正
常工作这个内容已经超出了本书的范围
这些数据类型的说明在所有线程共用同一地址空间和每个线程使用单独地址空间的情
况下是有很大区别的
10.11.1 单一地址空间的代码共享
在使用单一地址空间的操作系统比如大多数实时系统共享的库函数代码和数据
是放在固定的地方程序寻找库函数和库函数寻找自己的数据都不会出现问题然而
库必须可以重入可供不同的任务使用而且每个任务都可以在库的某个函数中悬挂
同时这个函数还可以被其他任务使用动态数据是足够安全的因此不需要拥有状态信
息的简单任务不需要修改也能工作正常
静态暂时数据的访问可以通过信号量机制确保数据第一次访问到最后一次访问的
连续性详见5.8.4







附录A 指令的时序和优化
MIPS CPU高度流水化,所以它们执行代码的速度依赖于流水线的工作情况。有某些情况下,代码的正确性依赖于流水线的工作方式---特别是使用CPU控制协处理器0的指令和寄存器。
通过显式使用寄存器而传递的依赖性相当明显,只不过比较凌乱。除此以外,在隐式使用的寄存器中也有一些偶然的依赖关系。例如状态寄存器中的CPU控制标志会影响到所有指令的指令的执行,改变这些标志必须非常小心。
大部分MIPS指令需要在流水线RD阶段的结束时得到它们的操作数,并且要在随后的ALU阶段的结束时产生这些指令的运行结果,这如图A.1所示的那样。如果所有的指令总能够遵守这些规则,任何指令序列都能够以最大速度正确的运行。在MIPS架构中最大的奥妙就是绝大多数指令都能遵守这些规则。
在由于某些原因而不能做到的情况下,使用从前面的紧邻指令处得到操作数的指令不能及时正确的运行。这种情况能被硬件检测到,然后通过延迟第二条指令直到数据准备好以使之得到修正,或者它可以留给程序员来避免产生试图使用未准备好的数据(pipeline hazard流水线冒险)的指令序列。

A.1 避免冒险:使代码正确

可能的冒险包括下面这些情况:
1.load延迟:在早期的MIPS CPU中的这是一种流水线冒险;紧跟着load指令后面的指令不能引用由load产生的数据。有时候当没有有用的东西能被安全的移到被延迟的指令槽时,需要编译器/汇编器使用一条nop指令。但从R4000开始,MIPS CPU已经是互锁的了,这样冒险就不会影响到通常的用户级指令。
2.乘法单元冒险:从 MIPS CPU的整数乘法器得到的运算结果是互锁的,所以取得这个运算结果的mflo指令没有延迟指令槽。但是整数乘法硬件的独立性产生了自己的问题,参看A.3节。

3.协处理器0冒险:协处理器0控制指令通常用不同于平常的时序来读/写寄存器,这样就产生了流水线问题。其中多数没有互锁。详细信息必须从你的CPU用户手册中查,但是我们将看一下你在R4000 CPU(可能是MIPS CPU中最难处理的)上必须要做的事情。

注意分枝延迟指令槽,尽管它是为了降低流水化而被引入的,但它作为MIPS架构的一部分,因此不再是冒险了;它只是一个特例而已。

A.2 避免互锁来提高性能

只要CPU发生互锁,我们将会损失性能。但是如果用一些巧妙的法子,CPU本来是可以做些有用的事情的。我们想让编译器(or for heavily used function perhaps a dedicated human programmer)重新组织代码来得到最佳运行效果。

编译器—以及人—都发现这是一个挑战。一个高度优化已避免互锁的程序经常将运算的几个阶段分解,然后交错的执行,这样就很难看出将会发生什么。如胫皇谴釉吹奈恢帽磺昂笠贫怂模逄踔噶睿ǔ;购么怼8蟮囊贫突岢鱿衷嚼丛酱蟮奈侍狻?
在单流水线机器(目前的绝大部分MIPS CPU)中,绝大部分指令使用一个时钟周期,所以对那些用四到五个时钟周期才能完成的指令以及那些成功的和其他指令交迭的指令,我们都有希望重新组织它们。在MIPS CPU中,这些标准只有对那些浮点指令才能很好的符合,所以高级调度机制会提高浮点指令的性能但对整数指令却作用无几(1)。深入讨论这个问题超出了本书的范围;如果你想很好的回顾一下所使用的编译器技术,请看一下Hennessy and Patterson, <<计算机体系结构:一种量化的方法>>。如果想知道各种CPU的详细时序,请查一下相应的用户手册。
在第十二章第388页有一个小规模的关于load互锁的代码优化的例子。

A.3 乘法单元冒险:早期修改lo and hi。

当一个MIPS CPU发生了中断或异常时,大部分流水线中的指令被中止,并且禁止将运算的结果写回。但是整数乘法单元很少与CPU的其余部分有关联,因此继续运行, 这并不会对异常产生影响。这意味着一旦乘法和除法指令开始以后,不能防止改变乘法单元的运算结果寄存器的lo and hi。
异常可能及时发生以防止mfhi或者mflo完成写回操作,但可能允许后续的乘法或者除法指令开始运行—并且一旦第二个运算开始以后原来的数据将会丢失。
为了避免这个问题,确保至少用两个时钟周期从后面的乘法或除法指令分离mfhi或者mflo指令,那么在所有的MIPS CPU上都是足够的了。好的编译器和汇编器将会为你处理这些的。而且只有你反汇编这些代码,你才会知道它的存在,这样你会发现一些没有料想到的nop指令。

----------------------------------------------------------------------------------------------------
1. 这是为何SGI编译器在高度浮点化程序上快得多的一个原因—可能快30%,但在整数代码上比GNU C差一点点。



A.4 避免协处理器0冒险:有多少nop呢?
程序员的问题是,我需要在一对特定指令之间放多少条指令(很可能是nops)才能让它们安全的工作呢?
原则上是可以得到指令对和在它们之间需要多少时钟周期的一份完整清单。但是那太费时间了。但我们能够降低这个工作的规模,我们注意到只有在以下情况时问题只会出现:

1.使用比标准时间(标准时间就是ALU阶段的末端)长的时间来产生数据的指令和/或
2.指令需要使用在标准时间(在这种情况下,标准时间是ALU流水阶段的开始)前准备好的数据


我们不需要列出在标准时刻产生和使用数据的指令,只要列出那些偏离正常途径的指令就可以了。对于其中的每一条指令,当运算结果产生了以及/或者当需要操作数时(1)我们都需要当心。有了这些,我们将能够在最复杂的情况下得到正确的或者高效的指令序列。
表A.1展示了R4000/4400 CPU的时序,这张图原来出现在Heinrich,<<R4000/R4400用户手册>>(在参考书目可以找到Web地址)中。这个表列出了操作数被使用和运算结果对后续的指令变为可用的流水阶段。

1. 列出运算结果迟了多少时钟周期的或者操作数早了多少时钟周期将是足够的---也是最简单的。但是MIPS系列的图表采用了流水线阶段。


表 A.1 有可能冒险的协处理器0指令和R4000/R4400 CPU的事件时序
























在一对有依赖关系的指令之间需要的时钟周期数(通常就是nop指令的数目)是:
ResultPipestage – OperandPipestage – 1

为什么要减1呢?在第n+1流水时期产生的运算结果和一个在第n流水时期必需的操作数产生了理想的流水线,所以不需要nop。实际上减1是流水线运行阶段的人工模拟。
对于其他大部分MIPS CPU你会在相应的用户手册中发现一张相似的表。这里我们用R4000/4400作为例子是因为它长长的流水线(你会在表中看到总共有8级流水);以及作为MIPS III CPU家族的最早产品的地位就意味着,在R4000上很好运行的任何代码序列在任何后续的CPU上都会是安全的(尽管可能不是最优的)。
注意尽管mfc0指令是在后期把数据传送到它的目标通用寄存器,这个滞后的的运算结果在表中并没有标注出来;这是因为它是互锁的。这张表仅仅列出了可能引起冒险的时序。


A.5 协处理器0指令/指令调度

我们在第6章看到了下面的在64位CPU(32位地址空间)上处理TLB扑空的一段代码:

.set normorder
.set noat

TLBmissR4K:
dmfc0 k1, C0_CONTEXT
nop #1
lw k0, 0(k1)
lw k1, 8(k1)
mtc0 k0, C0_ENTRYLO0
mtc0 k1, C0_ENTRYLO1
nop #(2)
tlbwr #
nop #(3)
eret #(4)
nop #(5)

.set at
.set reorder

现在我们能够说明这里边的nops指令的数目了。

(1) R4000 CPU和它的大多数后代不能向下一条指令传递协处理器0寄存器值;dmfc0指令时序和load指令很象。Heinrich的<<R4000/R4400用户手册>>暗示这个操作在R4000上可能会被完全互锁,并且可以肯定的是任何超过一个时钟周期的延迟都会互锁。但是它也没有变得简单一些,并且这里的nop对性能也不会产生任何不利的影响,所以我们把它保留在里边。

(2) 从表A.1,mtc0在流水线的第7步写EntryLo1寄存器,而tlbwr指令需要数据在流水线的第5步准备好。所以只需要一个nop(是这样计算的,7 – 5 - 1)。对于一些其它的CPU可能并不需要这个nop,但是为了可移植性的原因,还是值得保留下它的。

(3) tlbwr没有明显的相关性,但事实上非常重要的一点是在我们返回到用户态代码前,它所有的边际作用都会被完成。tlbwr只有到流水线的第8步才能完成写TLB,而正常指令的预取需要TLB在流水线的第二步准备好;我们必须在tlbwr和异常返回之间保留5个指令槽。eret后面跟着它的分枝延迟指令槽—在这种情况下在表中(5)处有一个nop—而且(由于R4000的长长的流水线的缘故)流水线会在分枝指令后面回填充一个”两时钟周期”的延迟。尽管如此,还是只有四条指令;所以我们需要在表中(3)处eret之前添加一个nop。

(4) 另外一个依赖性存在于eret之间,eret会将状态寄存器SR(EXL)域复位到它正常的用户态,并且是用户程序的第一个指令预取状态。但是,这个时序超出程序员的能力之外,所以机器已经内置了,分枝延迟时间槽加上”两时钟周期”如此之长的分枝延迟足够了。

有了这张表,你应该能够做任何事!

A.6 协处理器0标志和指令

正如前面我们看到的,一些CPU控制寄存器(协处理器0)包含了位字段值或者标志,这些位字段值或者标志有其他指令运行而产生的副作用。一个常用的经验方法是假设在执行了一条mtc0指令后,三个指令周期内任何这样的副作用将是不可预知的,
但是下面的情况需要特别注意:

1.启用/禁用一组协处理指令:如果你通过改变SR(CU)中的一位而启用了一个协处理器(使它特定的指令可用), mtc0指令在流水线第7步生效,因此新的运算值必须在协处理器指令的流水线第2步稳定下来。所以在这种情况下需要发射四个中间指令。

2.启用/禁用中断:如果你写SR(IE), SR(IM), 或者SR(EXL)而改变了CPU的中断状态,表A.1告诉我们在流水线的第7步开始生效。中断信号在指令流水线的第3步被采样,以决定是继续处理指令还是被中断所抢占。这意味着在新的中断状态被安全的安装之前,三条指令(是这样算出的:7 – 3 - 1)必须被执行。

在三条指令运行期间,中断能够被检测出来,并且能够引发异常。但是在被中断的指令前发射的指令改变了状态寄存器,所以规则告诉我们状态寄存器的改变还将会发生。

设想一下你已经通过设置异常等级位SR(EXL)禁用了中断。你通常只会在一个地方这样做,那就是在异常处理例程结束的地方。任何复杂情况的异常处理例程都保存异常开始处的SR值,当控制准备返回到用户态程序时再恢复这些值,这样异常开始处的SR(EXL) 的部分值就被设置了。


跟在设置SR(EXL)的指令后面有三个指令槽, 如果中断发生这三个中的一个时,那么中断异常发生了可SR(EXL)已经被设置过。那将发生非常古怪的事情,包括异常返回地址EPC没有被保存等(1)。这种情况是不可恢复的,所以当你设置SR(EXL)时确保中断已经被禁用是至关重要的;如果能确保你至少在三条指令时间之前清除SR(IE)和/或SR(IM),这样你就能做到这一点。

3.TLB改变和指令预取:在改变TLB和指令地址转换生效之间有五条指令的延迟。除此之外,有一个单条目缓冲器(single entry cache)被用于指令地址转换(称为微TLB),这种指令地址转换通过加载EntryHi而被隐式的覆盖掉;这也可能延迟生效的时间。

你只有在一个未映射的地址空间运行代码时必须显式的更新TLB。kseg0是通常的选择。


-------------------------------------------------------------------------------------------------------
1.看看第6.7.2节关于这个古怪行为的原因的讨论是个好主意。
 


 


posted @ 2005-02-28 16:57 nasiry 阅读(...) 评论(...) 编辑 收藏