- 前一篇文章分析了指令系统(ISA)的设计方法,这里以MIPS指令系统为例进行分析
- 前文链接:计算机组成原理(4.1)—— 指令系统设计
一、MIPS架构基础
- 1981年出现,由MIPS科技公司开发并授权,广泛被使用在许多电子产品、网络设备、个人娱乐装置与商业装置上。最早的MIPS架构是32位,最新的版本已经变成64位。
- 并行化程度:流水线
- 指令集类型:RISC
1. 寄存器数据指定
(1)MIPS架构中的寄存器安排
- 32位通用寄存器GPRs:
- 31+1个(r0是机器零)
- 寄存器编号5位
- 32位浮点寄存器:
- 32个:
$f0~$f31
- 可以两个一起拼成64位的
- 32个:
- 专用特殊寄存器
- 无需编号
HI, LO, PC
等
(2)寄存器名称、编号和功能
- 通用寄存器汇总表
- 寄存器的汇编表示用
$
符号,可以接名称或编号(如$a0
和$4
都表示寄存器a0) - 在MIPS指令字中,用5位二进制编码指示通用寄存器
- 被调用函数把值保存在
s0~s7
,被掉函数结束后调用函数还可以用这些值;如果存在t0~t7
就不能用了(C翻译汇编时要小心)
2. 存储器数据指定
- 32位机器:可访问主存空间: 2^32bytes(4GB)
- MIPS使用装入-存储型指令风格:运算的操作数只能是寄存器,只能通过
Load
/Store
指令访问存储器数据 - 数据地址通过一个32位寄存器内容(基地址)加16位偏移量得到,16位偏移量是带符号整数,故应符号扩展
- 数据要求按边界对齐(地址是4的倍数)
- Big Endian(大端方式)或小端
二、操作数类型和表示方式
1. 操作数类型
2. 操作数表示方式
三、指令格式
- 指令字长度:定长指令字,32位宽。
- 须按字地址对齐(字地址为4的倍数,即指令地址的最后两位为0)
- 操作码长度:定长操作码编码(
op
段),6位宽。- 一般通过对操作码进行不同的编码来定义;
- 操作码相同时,再由功能码(
func段
)进行区分(例如MIPS的R型指令)
1. R型指令
- 两个操作数和结果都在寄存器
- R型指令功能:
- 运算指令:包括各种算数、逻辑、移位运算。R型指令基本都是运算指令
- 控制转移指令:有
jr
和jalr
2. I型指令
- 一个操作数是立即数,另一个操作数和结果在寄存器
- 16位的立即数需要扩展到32位参与运算,依据具体指令不同可能要进行符号扩展或零扩展
- I型指令功能:
- 运算指令:类似R型指令,只是源操作数之一通过立即数给出
- 访存指令:
LOAD
系列和STORE
系列指令。寄存器RS给出基地址,16位立即数(符号扩展)给出偏移量 - 条件分支指令:如
beq
/bne
等。比较RS和RT寄存器的值,16位立即数(符号扩展)给出目标指令和当前指令偏差的条数
3. J型指令
- 操作数只有一个直接地址(用来控制跳转)
- J型指令功能:
- 控制转移:转移到
target address
所指示的指令执行
- 控制转移:转移到
- 目标地址的构成
- 32位MIPS机器中,指令储存时按字地址(4字节)对齐,所有指令地址均为4的倍数,故其最后两位总为0
- 下图是MIPS架构的
memory map
,其中Text
段用于存储指令,可见指令地址范围是0x0040_0000
到0x0FFF_FFFC
,高四位恒为0。为了避免Text段浮动导致问题,我们直接用pc寄存器(当前指令)的高四位作为目标指令的高四位
- 综上,目标指令地址为:
pc高四位 + 26位target address + 0000
,共32位
4. 汇编指令和机器码指令字示例
- 从以下两个示例表中,可以看出汇编语言到机器指令的一一对应关系。汇编语言本质上就是和机器指令一一对应的,类似助记符的一种语言
(1)MIPS汇编示例
(2)机器码指令字示例
(3)汇编和反汇编
-
汇编:把汇编指令翻译为机器码
-
反汇编:把机器码翻译为汇编指令
四、寻址方式
- MIPS不同于IA-32,没有专门的寻址方式字段,各操作数的具体寻址方式由指令格式确定,而指令格式由 op来确定
1. R型指令的寻址
2. I型指令的寻址
3. J型指令的寻址
五、程序的机器级表示
1. 算术和逻辑运算指令
- 没有全部列出,还有其他指令,如addu(不带溢出处理), addiu 等
- x86 / IA-32没有分add还是addu,因为它只产生各种标志(PSW),由软件根据标志信息来判断是否溢出。而MIPS是由硬件直接判溢出与否,要告诉CPU处不处理理溢出
- 示例
//示例1:假定给 f, g, h, i, j分别分配 $s1, $s2, $s3, $s4, $s5
f = (g+h)-(i+j);
add $t0, $s2, $s3
add $t1, $s4, $s5
sub $s1, $t0, $t1
//示例2:16位有符号立即数[-32768,32767]
f = (g+100) - (i+50);
addi $7, $2, 100
addi $8, $4, 50
sub $1, $7, $8
//示例3:出现的常数超过16位有符号数范围
f = (g+65000) - (i+50)
addi $7, $2, 65000 //错了,因为超过16位立即数表示范围
addi $8, $4, 50
sub $1, $7, $8
lui $3, 0x0 //正确写法
ori $3, 0Xfde8
addi $8, $4, 50
sub $1, $7, $8
2. 访存指令
-
为什么指令必须支持不同长度的操作数:因为高级语言中的数据类型有char,short,int,long,……等,故需要存取不同长度的操作数;
-
指令中操作数长度由什么决定:由不同的操作码指定
-
示例
//A是100个字的数组(32位),g在$1, h在$2, A基址在$3 g = h + A[8]; lw $4, 32($3) //注意是4*8 = 32 add $1, $2, $4
-
如果在一个循环体内执行:
g = h + A[i]
,则能否用基址寻址方式:不行,因为循环体内指令不能变,故首地址A不变,只能把下标 i 放在变址寄存器中,每循环一次下标加1,所以,不能用基址方式而应该用变址方式- 基址寻址是:基址是寄存器给出的,偏移是立即数定值;
- 变址寻址是:基址是立即数定值,偏移是寄存器给出的
//A是100个字的数组(32位),g在$1, i在$5, A基址在$3
g = g+A[i]
3. 分支转移指令
4. 伪指令
5. 过程调用
-
过程调用的执行步骤(假定过程P调用过程Q)
-
MIPS中用于过程调用的指令:
beq
、j
、jr
、jal
、一些伪指令
… -
少量过程调用信息用寄存器传递
-
如果过程中用到的参数超过4个,返回值超过2个,怎么办
- 更多的参数和返回值要保存到存储器的特殊区域中
- 这个特殊区域为:栈(Stack)
- 一般用“栈”来传递
参数
、保存返回地址
,并用来临时存放过程中的局部变量
等。这样可以实现嵌套和递归调用
(1)MIPS中的栈
- 栈的基本概念
- MIPS中栈的实现
- 栈帧
(2)调用过程(假定P调用Q)
-
程序可访问的寄存器组是所有过程共享的资源,给定时刻只能被一个过程使用 ,因此过程中使用的寄存器的值不能被另一个过程覆盖!(主调过程使用的寄存器,被调过程要么不用,要么用完之后返回前把值还回去)
-
MIPS的寄存器使用约定
- 保存寄存器
$s0 ~$s7
的值在从被调用过程返回后还要被用,被调用者需要保留 - 临时寄存器
$t0 ~$t9
的值在从被调用过程返回后不需要被用(需要的话,由调用者保存) ,被调用者可以随意使用 - 参数寄存器
$a0~$a3
在从被调用过程返回后不需要被用(需要的话,由调用者保存在栈帧或其他寄存器中),被调用者可以随意使用 - 全局指针寄存器
$gp
的值不变 - 帧指针寄存器
$fp
用栈指针寄存器$sp-4
来初始化
- 保存寄存器
-
需在被调用过程Q中入栈保存的寄存器(称为被调用者保存)
- 返回地址
$ra
(如果Q又调用R,则$ra
内容会被破坏,故需保存) - 保存寄存器
$s0 ~$s7
(Q返后P可能还会用到,Q中用的话就被破坏,故需保存) - 除了上述寄存器以外,所有局部数组和结构体等复杂类型变量也要入栈保存
- 如果局部变量和临时变量发生寄存器溢出(寄存器不够分配),则也要入栈
- 返回地址
-
各处理器对栈帧规定的 ”调用者保存” 和 ”被调用者保存” 的寄存器可能不同
-
过程调用时MIPS中栈和栈帧的变化
-
过程调用协议
(3)调用的示例
-
swap函数示例
-
现有
swap
函数如下,主函数caller
要调用它swap(int v[ ], int k) { int temp; temp = v[k]; v[k] = v[k+1]; v[k+1] = temp; }
-
temp对应
$t0
(局部变量),变量v 和 k分别对应$a0
和$a1
(传入参数) -
根据C语言的逻辑,可以写出以下核心逻辑代码
sll $s2, $a1, 2 ; $a1=k, mulitply k by 4 addu $s2 $s2, $a0 ; address of v[k] lw $t0, 0($s2) ; load v[k] lw $s3, 4($s2) ; load v[k+1] sw $s3, 0($s2) ; store v[k+1] into v[k] sw $t0, 4($s2) ; store old v[k] into v[k+1]
分析这段程序,
swap
用到了$t0
,$s2
和$s3
,所以caller
中这三个寄存器的值被破坏。根据约定,$t0
由caller
自己保护,$s2
和$s3
需要在swap
中保护 -
使用
jal swap
指令调用swap
函数。等价于执行以下两条指令//jal swap $31 = PC+4 ; $31=$ra goto swap
-
程序执行顺序如下
-
加上保护寄存器和返回指令,完整程序如下
-
如果swap是叶子过程,无需保存返回地址到栈中。因为
$ra
的内容不会被破坏;如果将所有内部寄存器都用临时寄存器 (如$t1
等),则叶子过程swap的栈帧为空,且上述黑色指令都可去掉
-
-
嵌套调用示例
-
原始C程序
int i; // 全局变量 void set_array(int num) { int array[10]; // 局部变量 for (i = 0; i < 10; i ++) arrar[i] = compare (num, i); } int compare (int a, int b) { if (sub (a, b) >= 0) return 1; else return 0; } int sub (int a, int b) { return a-b; }
-
过程调用时的变量分配
-
全局变量一般分配到寄存器或R/W存储区。
-
该例中只有一个简单变量
i
,假定分配给$s0
。无需保存和恢复! -
为减少指令条数,并减少访问内存次数,在每个过程的过程体中总是先使用临时寄存器
$t0~$t9
;临时寄存器不够或者某个值在调用过程返回后还需要用,就使用保存寄存器$s0~$s7
。
-
-
set_array
过程的栈帧分析- 入口参数为
num
,没有返回参数,有一个局部数组,被调用过程为compare
,因此,其栈帧中除了保留所用的保存寄存器外,必须保留返回地址(因为set_array
不是叶过程) - 是否保存
$fp
要看具体情况,如果确保后面都不用到$fp
,则可以不保存,但为了保证$fp
的值不被后面的过程覆盖,通常情况下,应该保存$fp
的值,并给局部数组(int array[10]
) 预留4×10=40个字节的空间。 - 从过程体来看,从
compare
返回后还需要用到数组基地址,故将其分配给$s1
。因此要用到的保存寄存器有两个:$s0
和$s1
,但只需将$s1
保存在栈帧中($s0
保存全局变量i
不用保护),另外加上返回地址$ra
(因为已经保存了前一个函数的返回地址),帧指针$fp
(因为已经保存了前一个栈帧的尾地址)、局部数组,其栈帧空间最少为3×4+40=52B
。
- 入口参数为
-
compare
过程的栈帧分析- 入口参数为
a
和b
,仅一个返回参数,没有局部变量,被调用过程为sub
。 过程体中没用到保存寄存器,所以,其栈帧中只需保留返回地址$ra
和$fp
的值
- 入口参数为
-
sub
过程的栈帧分析:叶子过程,其栈帧为空 -
栈的变化示意图
-
6. 翻译C语言示例
-
判断等于
//i, j, f, g, h, 分别存在 $s1, $s2, $s3, $s4, $s5 if (i == j) f = g+h ; else f = g-h ; //翻译为汇编 bne $s1, $s2, else //与C语句相反, i!=j, jump to else add $s3, $s4, $s5 j exit //jump to exit else: sub $s3, $s4, $s5 exit:
-
Loop循环
//g, h, i, j ~ $1, $2, $3, $4 and base address of array is in $5 //数组元素为int类型,sizeof(int)=4 Loop: g = g +A[i]; i = i+ j; if (i != h) go to Loop: //翻译为汇编 Loop: add $7, $3, $3 //i*2,加法快 add $7, $7, $7 //i*4, 得到偏移量,也可用移位 add $7, $7, $5 //加上数组基地址 lw $6, 0($7) // $6=A[i] add $1, $1, $6 //g= g+A[i] add $3, $3, $4 //i = i+j; bne $3, $2, Loop //程序员不必计算分支指令的地址,而只要用标号即可!汇编 器完成地址计算
- 注意最后一句
bne $3,$2,Loop
怎么翻译机器码。注意imm16
的值一定是相对下一条指令说的
- 注意最后一句