拨开MIPS的神秘面纱🥵

2. MIPS指令格式详解

MIPS指令字长固定为32位(4字节)。

2.1 R型指令 (Register-type)

R型指令通常用于寄存器之间的算术和逻辑操作。

字段 op (6) rs (5) rt (5) rd (5) shamt (5) funct (6)
位数 31-26 25-21 20-16 15-11 10-6 5-0
含义 操作码 (opcode) 第一个源寄存器 第二个源寄存器 目的寄存器 位移量 功能码

2.2 I型指令 (Immediate-type)

I型指令用于包含一个立即数操作数的操作,如立即数算术运算、Load/Store指令、条件分支指令。

字段 op (6) rs (5) rt (5) immediate (16)
位数 31-26 25-21 20-16 15-0
含义 操作码 源寄存器 目的/源寄存器 16位立即数/地址偏移

2.3 J型指令 (Jump-type)

J型指令用于无条件跳转。

字段 op (6) address (26)
位数 31-26 25-0
含义 操作码 跳转目标地址的部分编码

3. I型指令的几种重点用法分析

类型一:立即数算术/逻辑运算

类型二:Load/Store 指令

类型三:条件分支指令


4. 常考点分析、历年命题方式与陷阱

  1. 指令格式识别:给定一个32位的二进制或十六进制代码,要求判断其指令类型(R/I/J),并反汇编成MIPS指令。这是最基本的考法。

    • 陷阱:首先看op字段。op=0是R型,但要结合funct判断具体操作。op不为0,则可能是I型或J型。op=23是J型,其余常见为I型。
  2. I型指令立即数解释:这是选择题和综合题的绝对热点。

    • 命题方式:给定一条lwswbeq指令,结合当时的寄存器和内存状态,求操作后的结果,或者计算有效地址、跳转目标地址。

    • 陷阱

      • 忘记对16位immediate进行符号扩展。正数补0,负数补1。

      • 混淆lw/swbeqimmediate的含义。前者是字节偏移量,后者是字偏移量(需要乘以4)。

      • 在计算beq跳转地址时,忘记基准地址是PC+4而不是PC

  3. 地址计算

    • J型指令:考查如何从26位address字段还原出完整的32位目标地址。Target = {PC+4[31:28], address, 00}

    • 寻址方式lw/sw属于基址变址寻址beq属于PC相对寻址j属于伪直接寻址。考纲要求掌握这些寻址方式的计算过程。

  4. 边界情况与反例

    • R型指令的shamt:只有在位移指令(如sll, srl)中shamt字段才有效,对于addsub等指令,该字段为0。

    • I型指令rt字段的角色:在addilw中,rt是目的寄存器;但在swbeq中,rt是源寄存器。这是一个非常细微但关键的区别。

    • 立即数范围:I型指令的16位立即数表示范围是 -32768 (−215) 到 +32767 (215−1)。对于更大的立即数,需要通过lui (load upper immediate)指令配合ori指令来加载。例如加载一个32位立即数到$t0

      MIPS Assembler

      lui $at, 0xABCD       # $at = 0xABCD0000
      ori $t0, $at, 0x1234  # $t0 = 0xABCD0000 | 0x00001234 = 0xABCD1234
      

      这虽然超出了单条指令的范畴,但体现了I型指令立即数范围的局限性,属于大纲内涵的延伸,可能会在综合题中出现。

5. MIPS过程调用(函数调用)

5.1 核心思想与考点概述

MIPS过程调用的本质并非由硬件强制规定,而是通过一套**软件约定(Software Convention)来实现的。这套约定规范了调用者(Caller)和被调用者(Callee)如何协作,以正确地传递参数、保存和恢复执行现场、以及返回结果。

包括:

  1. 控制权转移机制jaljr指令的功能与区别。

  2. 数据传递规则:通过寄存器和栈传递参数与返回值。

  3. 现场保存与恢复:理解调用者保存(Caller-saved)和被调用者保存(Callee-saved)寄存器的概念,并熟知栈帧(Stack Frame)的结构和作用。

5.2 寄存器使用约定

理解过程调用的第一步,是背熟MIPS的寄存器使用约定。这是所有后续操作的基础。

寄存器 编号 名称/用途 保存策略
$v0 - $v1 2 - 3 返回值寄存器 (Values) 被调用者放入,调用者取走
$a0 - $a3 4 - 7 参数寄存器 (Arguments) 调用者放入,被调用者读取
$t0 - $t9 8 - 15, 24-25 临时寄存器 (Temporaries) 调用者保存 (Caller-saved)
$s0 - $s7 16 - 23 保留寄存器 (Saved) 被调用者保存 (Callee-saved)
$sp 29 栈指针 (Stack Pointer) 指向栈顶,始终有效
$fp 30 帧指针 (Frame Pointer) 指向当前过程活动记录的基地址
$ra 31 返回地址 (Return Address) 存放调用指令的下一条指令地址

5.3 过程调用的生命周期

我们将一个完整的过程调用(例如,过程P调用过程Q)分解为六个阶段,并结合你提供的资料进行讲解。

图示:栈的生长方向与栈帧结构

高地址  +-------------------+
        |       ...         |
        +-------------------+
        |  P的参数 (若>4个) |  <-- 调用Q前,P压入
P的栈帧 |-------------------|
        |  P保存的$t寄存器 |
        |      ...          |
        +-------------------+ <-- P的$fp
        |  Q的参数 (若>4个) |
        |-------------------|
        |  返回地址 ($ra)   |
        |-------------------|
        |  旧的帧指针 ($fp) |
Q的栈帧 |-------------------| <-- Q的新$fp
        |  Q保存的$s寄存器 |
        |-------------------|
        |  Q的局部变量     |
        +-------------------+ <-- Q的$sp (栈顶)
低地址  |       ...         |
        (栈向低地址方向生长)

阶段一:调用者(P)的准备工作

  1. 参数传递

    • 将前4个参数(或更少)依次放入$a0, $a1, $a2, $a3寄存器。

    • 如果超过4个参数,将第5个及以后的参数从右到左依次压入调用者P的栈顶

  2. 保存临时寄存器

    • 检查$t0 - $t9中,有哪些值在调用Q结束后仍需使用。将这些寄存器的值压入P的栈中。

    • 例如:sw $t0, -4($sp) sw $t1, -8($sp) addi $sp, $sp, -8

阶段二:控制权转移 (jal指令)

调用者P执行jal Q指令。这条J型指令执行两个原子操作:

  1. 保存返回地址:将PC+4(即jal的下一条指令地址)存入$ra寄存器。

  2. 跳转:将PC的值更新为标签Q的地址,开始执行过程Q。

阶段三:被调用者(Q)的“过程头”(Prologue)

这是过程Q的入口部分,完全对应你图片image_db9dbe.png中的步骤。其核心任务是建立自己的栈帧

  1. 申请栈帧空间:将$sp减去一个立即数(该过程所需的栈帧大小framesize)。

    • addi $sp, $sp, -framesize
  2. 保存$ra和旧$fp

    • 如果Q需要调用其他过程(即Q不是叶过程),那么Q将来会执行自己的jal指令,这会覆盖$ra。因此必须先把$ra的值保存到Q自己的栈帧中。

    • 为了能正确返回到P的栈帧,需要保存P的帧指针$fp

    • sw $ra, framesize-4($sp)

    • sw $fp, framesize-8($sp)

  3. 保存$s系列寄存器

    • 检查过程Q中使用了哪些$s0 - $s7寄存器。将这些寄存器的原始值(属于P)保存到Q的栈帧中。

    • sw $s0, offset($sp)

  4. 设置新的帧指针$fp

    • addi $fp, $sp, framesize

    • 作用$fp指向当前栈帧的基地址,在Q的执行过程中保持不变。后续访问局部变量、保存的寄存器都以$fp为基准,使用固定的负偏移量。这比使用随时可能移动的$sp更稳定可靠。

阶段四:被调用者(Q)的“过程体”

执行函数的实际代码。此时:

阶段五:被调用者(Q)的“过程尾”(Epilogue)与返回

这是过程Q的出口部分,任务是撤销自己的栈帧并交还控制权。操作与Prologue严格相反。

  1. 放置返回值:将计算结果放入$v0(或$v0, $v1)。

  2. 恢复$s系列寄存器:从栈帧中将调用者P的$s寄存器的值恢复。

    • lw $s0, offset($sp)
  3. 恢复$ra和旧$fp

    • lw $ra, framesize-4($sp)

    • lw $fp, framesize-8($sp)

  4. 释放栈帧空间:将$sp加上framesize,使其指回调用Q之前的位置。

    • addi $sp, $sp, framesize
  5. 执行返回:执行jr $ra指令。这是一条R型指令,它将PC的值设为$ra寄存器中的地址,从而返回到P中jal指令的下一行。

阶段六:调用者(P)的清理工作

控制权返回到P后:

  1. 获取结果:从$v0中读取返回值,并根据需要存放到其他寄存器或内存中。

  2. 恢复临时寄存器和栈:如果阶段一中保存了$t系列寄存器或在栈上传递了参数,现在需要恢复它们,并调整$sp

5.4 常考点与陷阱分析

  1. 寄存器保存责任混淆

    • 陷阱: 题目问“过程Q需要保存哪些寄存器?”,考生误将$t0选入。正确答案是$s0-$s7中被Q用到的,以及在非叶子情况下必须保存的$ra

    • 关键: $t是调用者(Caller)操心,$s`是被调用者(Callee)操心。

  2. 叶过程与非叶过程

    • 考点: 一个叶过程(不调用任何其他过程)可以极大地简化调用流程。它不需要在栈上保存$ra,因为不会有jal覆盖它。如果它也不使用$s寄存器,甚至可以完全不用栈帧。

    • 设问方式: “一个MIPS叶过程至少需要执行什么操作?” 或对比叶过程和非叶过程的开销。

  3. 栈指针$sp移动方向

    • 陷阱: MIPS栈向低地址生长。分配栈帧是subaddi负数;释放栈帧是addaddi正数。考生容易搞反加减。
  4. jaljr的区别

    • jal(Jump and Link):J型指令,用于调用。它写入$ra,并执行伪直接寻址跳转。

    • jr(Jump Register):R型指令,用于返回(或函数指针调用)。它读取一个寄存器(通常是$ra)的内容作为目标地址,执行寄存器间接寻址。