MIPS 汇编/MIPS 细节
MIPS 有 32 个通用寄存器和另外 32 个浮点寄存器。寄存器都以美元符号 ($) 开头。浮点寄存器命名为 $f0、$f1、...、$f31。通用寄存器既有名称又有编号,如下所示。在 MIPS 汇编语言编程时,最好使用寄存器名称。
编号 | 名称 | 注释 |
---|---|---|
$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 | 帧指针 |
$31 | $ra | 返回地址 |
一般来说,有很多寄存器可以在程序中使用:十个**临时寄存器**和八个**保存寄存器**,以及 arg $a 和返回值 $v 寄存器。临时寄存器是通用寄存器,可以自由地用于算术和其他指令(称为被覆盖),而保存寄存器必须在函数调用之间保持其值。(如果要使用一个保存寄存器,则必须在过程进入时保存它,并在过程退出时恢复它)。处理调用保留的 $s0..7 寄存器的最简单方法是根本不碰它们。
临时寄存器名称都以 $t 开头。例如,有 $t0、$t1 ... $t9。这意味着有 10 个临时寄存器可以使用,而不用担心保存和恢复其内容。保存寄存器名为 $s0 到 $s7。
**零寄存器**,名为 $zero ($0),是一个静态寄存器:它始终包含值为零。此寄存器不能用作存储操作的目标,因为它的值是硬连线的,不能由程序更改。
还有一些寄存器,大多数指令不能直接访问。其中包括程序计数器 (PC),它存储正在执行的指令的地址(由 JAL 读取以计算返回地址,由跳转和分支写入),以及“hi”和“lo”寄存器,它们用于乘法和除法,其结果大于 32 位(乘法可能导致 64 位乘积,除法导致商和余数)。有一些特殊指令用于将数据移入和移出 hi 和 lo 寄存器。
有 3 种指令格式:R 指令、I 指令、J 指令。
R 指令采用三个参数:两个源寄存器(**rt** 和 **rs**)和一个目标寄存器(**rd**)。R 指令使用以下格式编写
- 指令 rd, rs, rt
其中每一个代表如下
rd | 目标寄存器说明符 |
rs | 源寄存器说明符 |
rt | 源/目标寄存器说明符 |
例如:
add $t0, $t1, $t2
将 $t1 和 $t2 的值相加,并将结果存储在 $t0 中。
当汇编成机器码时,R 指令表示如下
操作码 | rs | rt | rd | shamt | func |
---|---|---|---|---|---|
6 位 | 5 位 | 5 位 | 5 位 | 5 位 | 6 位 |
对于 R 格式指令,**操作码**或“操作码”始终为零。**rs**、**rt** 和 **rd** 分别对应于两个源寄存器和一个目标寄存器。**shamt** 用于移位指令而不是 **rt**,以简化硬件。在汇编中,要将 $t4 中的值左移两位并将结果放入 $t5 中
sll $t5, $t4, 2
由于所有 R 格式指令的操作码都是零,**func** 向硬件指定要执行的确切 R 格式指令。上面的 add 示例将编码如下
opcode rs rt rd shamt funct 000000 01001 01010 01000 00000 100000
由于它是一个 R 格式指令,所以前六位(操作码)为 0。接下来的 5 位对应于 rs,在本例中为 $t1。从上面的表格中,我们发现 $t1 是 $9,其二进制表示为 01001。同样,接下来的五位编码为 $t2 = $10 = 01010。目标是 $t0 = $8 = 01000。我们没有执行移位,所以 shamt 为 00000。最后,由于 add 指令的 func 为 100000。
对于上面的移位示例,操作码字段再次为 0,因为这是一个 R 格式指令。rs 字段在移位中未使用,因此我们将接下来的五位保留为 0。rt 字段为 $t4 = $12 = 01100。rd 字段为 $t5 = $13 = 01101。移位量,shamt,为 2 = 00010。最后,sll 的 func 字段为 000000。因此,sll $t5, $t4, 2 的编码为
opcode rs rt rd shamt funct 000000 00000 01100 01101 00010 000000
I 指令采用两个寄存器参数和一个 16 位“立即数”值。立即数是存储为指令一部分而不是存储在内存中的值。这使得访问常量比将常量放在内存中然后加载它们快得多(因此得名)。I 格式指令与 R 格式指令一样,首先指定目标寄存器(**rt**)。接下来是一个源寄存器(**rs**),最后是立即数。
- 指令 rt, rs, imm
例如,假设我们想要将值 5 添加到寄存器 $t1 中,并将结果存储在 $t0 中
addi $t0, $t1, 5
或将比较和分支到附近的标签(范围为 16 位有符号位移,左移 2 位)。汇编器计算 `(目标 - 分支指令地址 + 4) >> 2` 作为立即数
beq $t0, $zero, t0_equals_zero_branch_target
I 格式指令在机器码中表示如下
操作码 | rs | rt | imm |
---|---|---|---|
6 位 | 5 位 | 5 位 | 16 位 |
**操作码**指定请求的操作。**rs** 和 **rt** 各为 5 位,与 R 格式指令相同,位置也相同。**imm** 字段保存立即数。根据指令的不同,立即数常量可以是符号扩展的或零扩展的。如果需要 32 位立即数,则存在一个特殊的指令 **lui**(“加载上部立即数”),用于将立即数加载到寄存器的上部 16 位中。然后,该寄存器可以与另一个 16 位立即数进行逻辑 或运算,以将最终值存储在该寄存器中。然后,该值可以在普通的 R 格式指令中使用。以下指令序列将位模式 0101 0101 0101 ... 存储到寄存器 $t0 中
lui $t0, 0x5555 ori $t0, $t0, 0x5555
通常,汇编器会自动以这种方式拆分 32 位常量,因此如果编写 li $t0, 0x5555555,程序员就不必担心。一些汇编器(如启用了扩展伪指令模式的 MARS)甚至支持 addi / addiu[检查拼写] / xori / 等等的较大立即数,这样。
上面的 addi 示例将编码如下。addi 指令的操作码为 001000。源寄存器 $t1 的编号为 9,即二进制的 01001。目标寄存器 $t0 的编号为 8,即二进制的 01000。5 在二进制中为 101,因此机器码中的 addi $t0, $t1, 5 为
opcode rs rt imm 001000 01001 01000 0000 0000 0000 0101
addi 和 addiu[检查拼写] 将其 16 位立即数符号扩展到 32 位(因此可以表示 -32768 .. 32767 的有符号二进制补码值,即 0..0x7fff 和 0xffff8000 .. 0xffffffff 的无符号值)
按位布尔逻辑指令,如 andi、ori 和 xori,将立即数零扩展(因此可以使用 0..65535 的值)
使用 I 格式的其他指令包括加载/存储(地址 = 寄存器 + 带符号的 16 位位移)、立即比较(例如 slti $t1, $t0, 1234,在寄存器中生成 0 或 1)、lui 用于将立即数加载到寄存器的较高 16 位,以及分支(但不是跳转)指令。
J 指令用于将程序流程转移到 PC 寄存器周围 256MB 区域内的给定硬编码绝对地址。J 指令几乎总是用标签编写:汇编器和链接器会将标签转换为数值。J 指令只接受一个参数:要跳转到的地址。
(真正针对 PC 的相对控制转移可以使用 'b label' 完成,'b label' 可用于位置无关代码。MIPS 分支指令是 I 格式指令,具有左移 2 位的 16 位相对位移,这与跳转不同。)
指令 地址
有两种 J 格式指令:j 和 jal。后者将在后面讨论。j(“跳转”)指令告诉处理器立即跳到 地址 指示的指令。例如,要跳转到 label1
j label1
J 格式指令被编码为
操作码 | 地址 |
---|---|
6 位 | 26 位 |
在 MIPS32 机器上,地址是 32 位宽的,因此 26 位可能不足以指定要跳转到的指令。幸运的是,由于所有指令都是 32 位(4 个字节)宽的,我们可以假设所有指令都从可被 4 整除的字节地址开始(实际上,加载程序保证了这一点)。在二进制中,可被 4 整除的数字以两个零结尾(就像十进制中可被 100 整除的数字总是以两个零结尾)。因此,我们可以允许汇编器省略最后两个零,并让硬件重新插入它们。这实际上使地址字段变成了 30 位。最后 4 位将从下一条指令的地址中借用,因此我们不能让程序跨越 256MB 边界,因为跨越边界的跳转将需要改变最高 4 位。(当 J 指令正在处理时,PC 已经更新为指向下一条指令,即分支延迟槽。)
在上面的示例中,如果 label1 指定了地址为 120 或二进制的 1111000 的指令,我们可以用机器码对上面的跳转示例进行编码。j 的操作码为 2 或二进制的 10,我们必须截断跳转地址的最后两位,将其变为 11110。因此,j 120 的机器码如下所示。
opcode |---------------addr-----------| 000010 0000 0000 0000 0000 0000 0111 10