MIPS寄存器文件详解:32个寄存器如何为CPU提供数据
处理器每秒可以执行数百万甚至数十亿次操作。它可以完成数字相加、数值比较、地址计算、循环控制、函数调用,以及程序中的数据移动。
但是,在执行这些操作之前,处理器必须先取得当前指令所需要的数值。
这些数值最初可能来自内存,但处理器不可能在每一次简单计算时都依赖主内存。主内存容量很大,但它比CPU内部的执行电路慢得多。
因此,处理器会把当前最需要使用的工作数据保存在寄存器中。
在经典的32位MIPS架构中,程序员可见的整数寄存器集合包含32个通用寄存器。指令使用5位寄存器字段来指定这些寄存器。
在常见的单发射教学数据通路中,这些寄存器通常被表示成一个具有两个读取端口和一个写入端口的寄存器文件。这样,一条指令可以取得两个源操作数,并在稍后写入一个结果。
本文将从两个层面介绍MIPS寄存器文件:
- MIPS架构向软件提供了哪些可见行为
- 经典教材中的寄存器文件如何实现这些行为
区分这两个层面非常重要,因为指令集架构并不要求每一颗物理MIPS处理器都采用完全相同的内部寄存器文件电路。
1. 什么是寄存器?
寄存器是处理器内部的一个小型存储位置。
在MIPS32中,每个通用寄存器可以保存一个32位数值。这个数值可能表示:
- 有符号或无符号整数
- 内存地址
- 字符或位字段
- 逻辑运算使用的位掩码
- 函数参数
- 函数返回值
- 中间计算结果
32位寄存器中的位通常从0编号到31。
- bit 0是最低有效位
- bit 31是最高有效位
每一位保存0或者1。32个位组合在一起,构成一个完整的二进制数值。
单个寄存器的容量非常小,但寄存器直接参与指令执行。算术指令、逻辑指令、分支指令、加载指令和存储指令,都会使用寄存器中保存的数值。
2. 架构寄存器与物理硬件
这里需要区分两个相互关联的概念。
架构寄存器
架构寄存器是机器指令和软件能够看到的寄存器。
MIPS32架构定义了32个通用寄存器,编号从0到31。一条指令可以把这些寄存器指定为源寄存器或者目标寄存器。
物理寄存器文件硬件
物理寄存器文件,是某一种具体处理器实现用来提供架构寄存器行为的内部电路。
一个简单的MIPS处理器,可以把这些寄存器实现为一个具有两个读取端口和一个写入端口的紧凑阵列。
更加先进的处理器还可能使用:
- 更多读取或写入端口
- 多银行寄存器结构
- 复制的存储阵列
- 流水线阶段
- 影子寄存器组
- 不同硬件线程使用的独立寄存器上下文
- 用于寄存器重命名的内部物理寄存器
即使物理实现更加复杂,软件看到的仍然是架构规定的寄存器集合。
除非特别说明,本文所说的寄存器文件,主要指经典教学模型中的实现。
3. 32个通用寄存器
经典MIPS32中,程序员可见的整数寄存器集合包含32个通用寄存器。
它们的编号是:
|
1 2 3 4 5 6 |
Register 0 Register 1 Register 2 ... Register 31 |
因为一共有32个寄存器,所以一个寄存器编号需要5位:
|
1 2 |
2^5 = 32 |
一个5位二进制数可以表示0到31。
这就是为什么经典MIPS指令格式中的rs、rt和rd寄存器字段通常都是5位宽。
指令提供一个寄存器编号,处理器再使用这个编号访问对应的架构寄存器。
这里的寄存器编号不是内存地址,而是处理器内部使用的一个小型标识符。
4. “32 × 32寄存器文件”是什么意思?
经典MIPS32寄存器文件通常被描述为:
32 × 32 Register File
第一个32表示一共有32个寄存器。
第二个32表示每个寄存器宽32位。
从概念上看,可以把它画成一张表:
|
1 2 3 4 5 6 |
Register 0 — 32 bits Register 1 — 32 bits Register 2 — 32 bits ... Register 31 — 32 bits |
当读取地址指定其中一行时,对应的32位数值就会出现在读取输出端。
当写入地址指定其中一行时,如果写入已经启用,这个寄存器就可以保存一个新的32位数值。
这张表只是一个便于理解的逻辑模型。真实集成电路不一定真的由32个独立的长方形寄存器,再连接到巨大的可见多路选择器组成。
5. 寄存器文件真的只有128字节吗?
32个寄存器,每个寄存器32位,因此:
|
1 2 |
32 × 32 bits = 1,024 bits |
因为8位等于1字节,所以:
|
1 2 |
1,024 bits ÷ 8 = 128 bytes |
因此,程序员可见的32个MIPS32通用寄存器,代表128字节的架构整数状态。
但是,这并不一定意味着一颗真实处理器中,与寄存器有关的物理存储总共只有128字节。
某一种具体实现还可能包含:
- 通用寄存器集合的影子副本
- 多个硬件线程使用的独立寄存器组
- 流水线存储
- 测试与故障恢复结构
- 其他实现相关寄存器
128字节这个数字,描述的是一组程序员可见的32个32位通用寄存器所能表示的信息容量。
6. 经典MIPS为什么选择32个寄存器?
寄存器数量与指令编码密切相关。
经典MIPS指令长度为32位。许多指令必须同时编码:
- 一个源寄存器
- 第二个源寄存器
- 一个目标寄存器
- 操作码
- 额外的功能信息或者立即数信息
32个寄存器要求每个寄存器字段占用5位。
这是一个实用的平衡。
更多寄存器可以减少把临时数值移动到内存的次数,但更大的寄存器编号也会占用更多指令位。
更少的寄存器可以缩小寄存器字段,但软件能够直接使用的高速工作空间也会减少。
MIPS使用32个通用寄存器,同时保持规则的32位指令格式。
7. 寄存器编号与汇编名称
MIPS汇编程序员通常使用符号名称,而不是只写寄存器编号。
例如:
|
1 2 |
add $t0, $t1, $t2 |
这些符号名称可以让代码的用途更容易理解。
但是,$t0、$s0、$a0和$v0等名称,主要属于软件约定和应用程序二进制接口,也就是ABI。
它们并不表示底层寄存器使用了完全不同的存储电路。
例如:
|
1 2 3 4 5 6 |
$t0 = Register 8 $t1 = Register 9 $s0 = Register 16 $sp = Register 29 $ra = Register 31 |
汇编器在生成机器码之前,会把这些符号名称转换成寄存器编号。
硬件最终处理的是编码后的数字。
软件使用名称提高可读性,硬件使用编号完成选择。
8. 经典o32 ABI中的常用寄存器名称
下面这些名称和用途,通常与经典MIPS o32调用约定有关。
| 编号 | 名称 | 常见软件用途 |
|---|---|---|
| 0 | $zero |
固定为零 |
| 1 | $at |
保留给汇编器生成的代码 |
| 2–3 | $v0–$v1 |
函数返回值 |
| 4–7 | $a0–$a3 |
函数参数 |
| 8–15 | $t0–$t7 |
调用者保存的临时值 |
| 16–23 | $s0–$s7 |
被调用者保存的值 |
| 24–25 | $t8–$t9 |
额外的临时值 |
| 26–27 | $k0–$k1 |
保留给内核使用 |
| 28 | $gp |
全局指针 |
| 29 | $sp |
栈指针 |
| 30 | $fp或$s8 |
帧指针或保存寄存器 |
| 31 | $ra |
返回地址 |
这些用途属于软件约定。
寄存器0是一个架构层面的特殊情况:读取它时得到零,尝试修改它也不会产生一个持久的非零结果。
其他寄存器通常既可以保存地址,也可以保存普通数据,只是软件约定为它们安排了不同用途。
不同的MIPS ABI可能会以不同方式使用某些寄存器,因此不能把这张表理解为通用于所有MIPS环境的硬件规则。
9. 两个源寄存器和一个目标寄存器
许多经典MIPS指令会使用两个源寄存器,并产生一个目标寄存器结果。
例如:
|
1 2 |
add $t0, $t1, $t2 |
它的含义是:
|
1 2 |
$t0 = $t1 + $t2 |
这条指令需要取得两个数值:
$t1中保存的数值$t2中保存的数值
然后,它会产生一个结果,并把结果写入$t0。
这种指令级行为,很自然地对应到一种寄存器文件实现:
- 两个读取端口
- 一个写入端口
但是,端口数量属于实现选择,而不是ISA的强制要求。
经典的单发射数据通路通常使用两个读取端口和一个写入端口,因为这种结构可以满足许多普通MIPS指令的需要。
如果一颗超标量处理器每个周期可以发射多条指令,它可能需要额外端口、寄存器分银行、阵列复制或者其他技术。
10. 经典的双读单写寄存器文件
在教材模型中,寄存器文件通常具有下面这些接口:
|
1 2 3 4 5 6 7 8 9 10 |
Read Address 1 — 5 bits Read Address 2 — 5 bits Read Data 1 — 32 bits Read Data 2 — 32 bits Write Address — 5 bits Write Data — 32 bits Register Write — 1 control signal Clock |
第一个5位读取地址选择一个架构寄存器。
第二个5位读取地址独立选择另一个寄存器。
两个被选中的32位数值,分别出现在两个独立输出端。
写地址指定目标寄存器。
写数据输入提供新的数值。
寄存器写控制信号决定是否更新架构寄存器状态。
这个接口足够简单,便于清楚说明,同时也足以支持一个基本的MIPS数据通路。
11. 读取端口可以怎样建模?
在简化电路图中,一个读取端口可以表示为一个32选1多路选择器。
32个寄存器的数值都连接到这个多路选择器。
5位读取地址控制哪一个数值到达输出端。
例如:
|
1 2 3 |
Read Address = 8 Read Data = Contents of Register 8 |
如果地址发生变化:
|
1 2 3 |
Read Address = 16 Read Data = Contents of Register 16 |
第二条独立的选择路径会产生第二个读取数值。
两个读取端口可以选择不同的寄存器:
|
1 2 3 |
Read Address 1 = 9 Read Address 2 = 10 |
它们也可以同时选择同一个寄存器:
|
1 2 3 |
Read Address 1 = 16 Read Address 2 = 16 |
读取寄存器不会把数据从寄存器中移走,而只是把选中的数值提供给数据通路的其他部分。
在真实制造的处理器中,内部电路可能使用紧凑的多端口阵列,而不是两组字面意义上的巨大多路选择器。
12. 写入端口可以怎样建模?
经典写入端口通常接收:
- 一个5位目标寄存器编号
- 一个32位数据值
- 一个寄存器写使能信号
- 时钟
写地址选择目标寄存器。
写数据提供需要保存的数值。
写使能信号决定当前指令是否应该修改通用寄存器状态。
例如:
|
1 2 3 4 |
Write Address = 8 Write Data = Calculation Result Register Write = 1 |
这表示寄存器8将接收运算结果。
如果寄存器写控制信号为0,那么这个写入端口就不应该更新普通通用寄存器。
13. 5到32译码器
一个简单的写入电路可以使用5到32译码器。
译码器把一个5位寄存器编号,转换成32条独立的选择信号。
例如:
|
1 2 3 |
Input = 01000 Selected Output = Register 8 |
只有被选中的寄存器,才会收到有效的写选择信号。
这个信号还要与全局寄存器写控制信号结合。
只有在以下三个条件同时满足时,寄存器才会更新:
- 对应的译码器输出已经激活
- 寄存器写入已经启用
- 所需的时钟事件已经到来
所有寄存器都可以连接到同一个写数据值,但只有被选中的目标寄存器会保存它。
需要再次说明,译码器模型是一种教学表示。真实处理器可能会采用不同方式优化写选择电路。
14. 组合读取与时钟写入
许多经典单周期MIPS教材,会把寄存器读取建模为组合逻辑。
在这个模型中,读取地址发生变化后,经过电路传播延迟,对应寄存器的数值就会出现在输出端。
读取不需要等待新的时钟边沿。
寄存器写入则通常被建模为时钟控制操作。
在一个时钟周期中,写地址、写数据和写使能信号会逐渐稳定。在有效时钟边沿到来时,被选中的寄存器保存新的数值。
这种安排形成了一个容易理解的执行顺序:
- 读取源操作数
- 完成所需操作
- 准备结果
- 在受控时钟边沿写入结果
这种时序行为属于经典数据通路模型。
MIPS指令集架构规定的是一条指令最终对软件可见的结果,而不是每一种处理器实现所使用的精确晶体管级时序。
流水线处理器和更高性能的处理器,可能以不同方式组织寄存器访问,同时保持相同的架构行为。
15. 跟随一条add指令
现在来看下面这条指令:
|
1 2 |
add $t0, $t1, $t2 |
在经典MIPS R型编码中:
rs指定第一个源寄存器rt指定第二个源寄存器rd指定目标寄存器
在这个例子中:
|
1 2 3 4 |
rs = $t1 = Register 9 rt = $t2 = Register 10 rd = $t0 = Register 8 |
简单数据通路按照下面的步骤工作:
- 通过第一个读取端口选择寄存器9
- 通过第二个读取端口选择寄存器10
- 把两个32位数值送往执行硬件
- 算术逻辑单元完成加法
- 结果返回寄存器文件的写输入端
- 选择寄存器8作为目标
- 启用寄存器写入
- 结果成为寄存器8新的架构值
在这一阶段,可以把ALU看成一个黑盒:
|
1 2 3 4 |
Operand A ─┐ ├── ALU ── Result Operand B ─┘ |
ALU的内部结构是另一个独立主题。
16. 寄存器0的特殊行为
寄存器0在MIPS架构中具有特殊行为。
读取寄存器0时,输出始终为零。
向它写入非零数值,也不会产生一个持久的非零结果。
这为指令提供了一个永久的零值来源。
例如:
|
1 2 |
add $t0, $t1, $zero |
在不考虑溢出等其他问题时,这条指令的结果相当于把$t1中的数值复制到$t0:
|
1 2 |
$t0 = $t1 + 0 |
零寄存器可以用于:
- 把数值与零进行比较
- 构造类似
move的汇编操作 - 产生一个已知的零输入
- 丢弃不需要的目标结果
- 简化指令编码
处理器可以通过阻止向寄存器0写入、强制读取输出为零,或者使用其他等效电路,实现这种行为。
在ISA层面,软件看到的结果始终相同。
17. 立即数指令
并不是所有指令都会从寄存器文件取得两个操作数。
例如:
|
1 2 |
addi $t0, $t1, 10 |
这条指令使用:
$t1中的数值- 指令内部编码的立即数10
寄存器文件负责提供$t1中的数值。
第二个操作数来自经过符号扩展的立即数字段,而不是来自第二个寄存器输出。
数据通路中的另一个多路选择器,可以在下面两个输入之间进行选择:
- Read Data 2
- 扩展后的立即数
寄存器文件本身并不决定这些操作数代表什么。
它只负责返回被请求寄存器中的内容。
处理器中的控制逻辑和选择逻辑,决定哪些数值进入执行单元。
18. 加载指令
现在来看下面这条指令:
|
1 2 |
lw $t0, 4($t1) |
这条指令从内存中加载一个32位字。
有效地址根据下面的方式计算:
|
1 2 |
Contents of $t1 + Signed Offset 4 |
一个简单的执行流程如下:
- 从寄存器文件读取
$t1 - 把
$t1作为基地址 - 加上指令中的有符号偏移量
- 向内存系统请求一个字
- 把读取到的数值送回寄存器文件写输入端
- 选择
$t0作为目标寄存器 - 启用架构寄存器写入
从寄存器文件的角度看,加载指令会:
- 读取一个基地址寄存器
- 稍后从内存路径接收一个数值
- 把这个数值写入目标寄存器
高速缓存、总线和内存的内部工作方式,可以分别在后续文章中介绍。
19. 存储指令
现在来看下面这条指令:
|
1 2 |
sw $t0, 4($t1) |
这条指令把$t0中的数值写入内存。
它需要两个寄存器值:
$t1提供基地址$t0提供需要存储的数据
具有两个读取端口的寄存器文件,可以同时提供这两个数值:
|
1 2 3 |
Read Port 1 → $t1 Read Port 2 → $t0 |
处理器把基地址与有符号偏移量相加,形成有效内存地址。
第二个寄存器值则被送往内存系统,作为存储数据。
存储指令不会向普通通用寄存器写入新结果。
因此,执行存储指令时,通用寄存器的写控制信号会被关闭。
这个例子说明,即使一条指令不写寄存器,它仍然可能需要同时读取两个寄存器。
20. 寄存器文件不是处理器的全部状态
32个通用寄存器,只是处理器架构状态和微架构状态的一部分。
根据MIPS架构版本和具体实现,处理器还可能包含:
- 程序计数器
- HI和LO寄存器
- 浮点寄存器
- Coprocessor 0控制寄存器
- 异常与状态寄存器
- 内存管理寄存器
- 影子寄存器组
- 硬件线程上下文
- 流水线寄存器
- 内部实现寄存器
程序计数器通常不被视为32个通用寄存器之一。
传统MIPS乘法和除法操作可能使用HI和LO寄存器,它们也位于普通GPR集合之外。
如果处理器实现了浮点硬件,还会使用一组独立的浮点寄存器。
因此,现代或者专用MIPS核心中实际包含的寄存器存储,可能远远多于基本的32寄存器架构模型。
21. 寄存器不是Cache
寄存器和高速缓存都能让数据靠近执行硬件,但它们的控制方式不同。
寄存器
寄存器由指令明确指定。
例如:
|
1 2 |
add $t0, $t1, $t2 |
这条指令直接指定了源寄存器和目标寄存器。
Cache
Cache通常保存内存地址空间中某些数据块的副本。
程序通常访问内存地址,而Cache硬件会自动判断所需数据块是否已经存在。
软件通常不会在一条算术指令中,直接指定某一条Cache Line。
寄存器保存的是由指令直接选择的单个工作数值。
Cache保存的是由硬件选择和管理的内存层次结构数据块。
两种结构都与性能密切相关,但它们在计算机体系结构中承担着不同角色。
总结
经典MIPS32架构向软件提供32个通用寄存器,每个寄存器包含32位。
5位指令字段,可以指定寄存器0到寄存器31中的任意一个。
寄存器0始终提供数值零。
$a0、$t0、$s0、$sp和$ra等常见名称,主要来自软件调用约定,而不是物理上不同类型的寄存器。
在经典的单发射教学数据通路中,寄存器集合通常被表示为一个具有以下接口的寄存器文件:
- 两个读取端口
- 一个写入端口
- 两个5位读取地址
- 一个5位写地址
- 两个32位读取输出
- 一个32位写数据输入
- 一个寄存器写控制信号
这种结构符合需要读取两个源寄存器,并产生一个目标结果的指令需求。
但是,两个读取端口、一个写入端口、组合读取和边沿触发写入,都属于教学实现的特点。
它们不是每一颗物理MIPS处理器都必须遵守的统一要求。
更加先进的实现可以使用不同的内部结构,同时向软件提供相同的架构寄存器行为。
寄存器文件不会执行算术或者逻辑运算。
它的任务是:
- 保存程序员可见的工作数值
- 向执行硬件提供操作数
- 接收指令产生的结果
操作数离开寄存器文件之后,通常会进入算术逻辑单元,也就是ALU。
因此,ALU是学习完MIPS寄存器文件之后,最自然的下一个主题。
