
简介MIPS汇编实验斐波那契数列是一份面向计算机专业学生和汇编语言初学者的实验参考资料。资源以斐波那契数列计算为主线完整覆盖C语言参考实现与MIPS汇编实现两条路径C语言部分设计fib函数完成数列生成、输入合法性判断和溢出检测print函数按索引、十进制、十六进制格式输出汇编部分则通过数据段与.text段规划利用通用寄存器、分支跳转与内存寻址复现同样逻辑并额外加入输入错误提示与溢出判定分支。包内仅有1个docx文档大小约16KB虽然精简但代码与解析齐全便于逐段对照学习。目前已有2688人学习浏览适合作为汇编课程实验、上机练习或考前复习材料。通过研读可理解高级语言逻辑如何映射为MIPS指令掌握数组存储、循环控制、溢出判断、系统调用等基础操作同时积累实验报告撰写和调试排错的经验为后续学习计算机系统底层知识做好铺垫也能有效提升汇编代码阅读与调试能力。1. MIPS汇编斐波那契实验真正拉开差距的是溢出检测把C语言版的斐波那契翻译成MIPS汇编大多数人卡住的地方不是循环怎么写而是溢出检测那三行。这个实验在.data段用.word 0:50预留数组FIB 过程用addu做加法再用xor、and、bltz判断结果是否超出 32 位有符号数范围。它把 MIPS 课设最常考的几件事全串起来了过程调用约定、字对齐内存寻址、syscall 输入输出、边界条件与错误消息。适合正在做计算机组成实验的学生也适合想对比 C 和汇编运行时差异的从业者。用 QtSpim 跑完一遍就会发现N 取 46 和取 47 时程序走向完全不同这个边界细节往往比能不能写出主循环更影响最终结果。2. 从C函数到MIPS过程寄存器分配、数据段与调用约定2.1 .data段怎么布局array标签背后的字对齐规则实验参考的 C 代码里fib(N, array)接收 N 和一个数组返回实际写入的斐波那契项数。MIPS 版选择在静态数据段放一个固定大小的数组避免实现堆栈上动态分配。.word 0:50的含义是“50 个字初始值 0”array标签就是这段内存的首地址50 个 word 一共 200 字节标签后的地址按 4 字节对齐。访问array[i]时不能直接拿 i 当偏移量。MIPS 按字节寻址而数组元素是 4 字节的 word所以正确偏移是i * 4汇编里统一用sll左移两位完成这个乘法。.data array: .word 0:50 nmsg: .asciiz \nPlease enter N: fibmsg: .asciiz \nFibonacci Series: \nindex decimal hexadecimal\n bgzmsg: .asciiz \nN should greater than 0!\n ovfmsg: .asciiz \nOverflow!\n.asciiz会在字符串末尾补\0前导的\n是为了让提示消息和之前的输出不粘在同一行。.word 0:50分配 50 个 word实际上第 47 个斐波那契数就已经超出 32 位有符号范围后面的数组空间不会再被写入这个容量对本实验足够。2.2 FIB过程的寄存器分工与main调用序列MIPS 汇编没有变量名寄存器怎么分直接决定代码可读性。这个实验里 FIB 内部的角色划分很固定寄存器在FIB内的作用对应的C概念$a0输入参数 Nint N$a1数组首地址int array[]$t0N 的备份循环上限循环边界$t1当前下标 ii$t2array[i-1]的值临时变量$t3array[i-2]的值后复用为地址临时变量$t5、$t6溢出检查的中间结果临时变量$t7加法结果即array[i]临时变量$v0返回长度return len$t系列是临时寄存器过程返回时不需要保存。main 的调用序列也很典型main: la $a0, nmsg li $v0, 4 syscall li $v0, 5 syscall move $a0, $v0 la $a1, array jal FIB move $a0, $v0 ble $a0, 0, exit la $a1, array jal PRINT exit: li $v0, 10 syscallsyscall 4 会把$a0指向的字符串打印到控制台syscall 5 读取控制台输入的十进制整数结果写到$v0。move $a0, $v0把 N 作为 FIB 的第一个参数la $a1, array给出第二个参数。FIB 返回后$v0是数组有效长度main 再把它传给 PRINT。ble $a0, 0, exit用来截住错误分支N 小于等于 0 时 FIB 只打印提示信息没有有效数组内容没必要再调 PRINT。2.3 为什么 jal 之后要重新加载 $a1la $a1, array在 FIB 调用和 PRINT 调用前各出现了一次。从代码看 FIB 内部没有修改$a1那么第二次la是不是多余的不是。MIPS 的软性调用约定并不保证$a1经过一次过程调用后还能保持原值$a0到$a3随时可能被被调用方改写。如果把这道题改复杂一点FIB 内部用add $a1, $a1, $t1把数组指针移到了中间位置PRINT 再拿这个改过的$a1遍历就会从错误地址开始读数据。所以惯例是调用前设置参数调用结束后不要假设参数寄存器仍然有效。这个习惯放在流水线 CPU 实验里也一样每一级流水段都要重新读取当前所需的寄存器值而不是依赖前一阶段留下的旧数据。FIB 和 PRINT 都是叶子过程没有嵌套调用所以不需要操作$spjal保存返回地址到$ra过程结束时jr $ra即可。如果需要嵌套或者临时寄存器不够用才要考虑把$s0到$s7压栈保护。3. FIB核心循环寻址计算与溢出检测的位运算原理3.1 数组下标到内存地址sll、add、lw的组合FIB 循环里最常见的四行代码是这一组subi $t2, $t1, 1 sll $t2, $t2, 2 add $t2, $t2, $a1 lw $t2, 0($t2)这四行做了一件事取array[i-1]。subi先算出下标i-1sll左移两位得到(i-1)*4的字节偏移add加上数组基地址$a1最后lw把 word 加载到$t2。后面取array[i-2]用的是完全一样的三步只是把subi的立即数从 1 换成 2。MIPS 是典型的 load-store 架构lw只能接受“寄存器 偏移量”这种地址形式不能像 C 那样直接用array[i-1]表达式。所以每访问一次数组都得先把索引换算成地址。最容易犯的错是忘记sll直接把i-1当作字节偏移加给基地址。表面上程序不会报错因为地址仍然合法但取到的是数组起始地址往后 i-1 个字节处的内容结果会变成一个 word 内偏移 1、2、3 字节的错位数据甚至触发 QtSpim 的 unaligned address 异常。3.2 addu不触发异常软件用xor、and、bltz完成溢出判断加法和溢出检查连在一起addu $t7, $t2, $t3 xor $t5, $t2, $t7 xor $t6, $t3, $t7 and $t5, $t5, $t6 bltz $t5, overflow这里刻意用了addu而不是add。MIPS 里add检出有符号溢出会触发异常实验要求自己打印Overflow!消息并继续可控退出所以只能用addu让加法结果正常落入寄存器溢出与否交给后续指令判断。addu和add算出的位模式在电路上完全一样区别只在于是否让硬件介入异常处理。xor和and的组合是在检查一个条件两个加数符号相同且结果符号与它们相反。补码加法的溢出只可能发生在同号相加时例如两个正数相加得到负数或者两个负数相加得到正数。xor $t5, $t2, $t7取第 31 位的差异两个操作数分别和结果做异或再用and判断两个差异是否同时为 1。若同时为 1$t5最高位为 1即解释为负数bltz跳转。混合符号相加永远不会溢出。因为结果一定与其中某个操作数同号那个操作数对应的异或结果最高位就是 0与运算后得到 0。这个规律和操作数多大无关只看最高位所以在 32 位寄存器里判断是快手且准确的。提示调试时不要光盯十进制。$t7里出现很大的数不一定溢出要看$t5的最高位以及两个加数的最高位是否相同。这个判断和对 MIPS 单周期硬布线 CPU 中 ALU 的溢出标志设计是等价的。用 Logisim 搭 ALU 时溢出信号常写成V (A31·B31·~S31) ∨ (~A31·~B31·S31)这里的异或指令组合本质上是在用软件模拟同一个逻辑。3.3 存储结果与retfib返回的长度语义溢出检查通过之后才把结果写回数组sll $t3, $t1, 2 add $t3, $t3, $a1 sw $t7, 0($t3) addi $t1, $t1, 1 j fibloop注意顺序先sw再addi $t1, $t1, 1最后跳回循环顶部。如果加法溢出流程会提前跳到 overflow 分支这时$t1不会自增仍然停留在当前下标。这个细节决定了返回长度retfib: bgt $t1, $t0, exitfib move $t0, $t1 exitfib: move $v0, $t0 jr $ra如果循环是因为i N正常退出此时$t1 $t0bgt不跳转$v0等于 N如果是因为溢出提前退出$t1小于 N$v0等于出错时的下标 i。PRINT 拿到这个长度后从下标 0 一直打印到长度减 1正好覆盖所有成功算出来的斐波那契项。这个设计把“正常完成”和“溢出截断”两种情况统一成了同一个返回值语义。4. 输入检测与PRINT输出syscall编号、错误分支与十六进制打印4.1 N小于等于0的处理和返回长度的不确定角落FIB 入口处的判断是FIB: move $t0, $a0 ble $t0, 0, gtzero li $t1, 1 sw $t1, 0($a1) sw $t1, 4($a1) li $t1, 2ble $t0, 0, gtzero判断 N 是否小于等于 0如果是就跳到 gtzero 分支打印N should greater than 0!。这里和参考 C 代码有个不一致C 写的是if(N0)也就是说 N0 时不会报错汇编用的却是小于等于N0 也会被当成非法输入。如果实验要求严格对齐 C 语义应该把ble改成bltz。另一个值得注意的问题是N 小于等于 0 时$t1从未被初始化但流程仍然会进入 retfib接着执行bgt $t1, $t0, exitfib读到的可能是上一次遗留的寄存器值。QtSpim 复位后寄存器大多为 0实际表现是返回长度 0main 里ble $a0, 0, exit直接退出。但这属于“碰巧能跑”不是规范行为。稳妥的写法是在 FIB 入口先清零FIB: li $t1, 0 move $t0, $a0 ble $t0, 0, gtzero这样无论输入 N 是什么retfib 读到的$t1都是确定值。错误分支只打印消息、不写数组然后让 main 依据返回的 0 退出避免继续执行 PRINT。4.2 PRINT遍历与syscall的格式化差异PRINT 的过程也很直白PRINT: move $t0, $a0 la $a0, fibmsg li $v0, 4 syscall li $t1, 0 prtloop: bge $t1, $t0, exitprt sll $t3, $t1, 2 add $t3, $t3, $a1 lw $t2, 0($t3) move $a0, $t1 li $v0, 1 syscall li $a0, 32 li $v0, 11 syscall$a0先收到数组长度进入 PRINT 后马上转存到$t0之后$a0被反复用作 syscall 的参数。循环每轮先算出array[i]的地址并加载到$t2然后按 index、空格、十进制数、空格、十六进制数、换行的顺序打印。MIPS 的 syscall 是一对一的服务调用和 C 的printf格式化机制完全不同。本题涉及的编号可以列成一张表syscall编号作用参数/返回值4打印字符串$a0 字符串地址5读整数返回值在$v01打印十进制整数$a0 整数11打印字符$a0 ASCII 码34打印十六进制整数$a0 整数10退出程序无ASCII 码 32 是空格10 是换行PRINT 里用 syscall 11 逐个输出。由于每个字段都要单独发起 syscall汇编代码看起来比 C 的printf啰嗦很多但每一步的状态转换非常清楚。关于十六进制输出有一个和题目示例不一致的细节QtSpim 和 MARS 的 syscall 34 通常输出不带0x前缀的数字例如6d73e55f。题目里那种0x0000000d带前缀、带补零的格式往往是在整理输出时手工加的。如果希望程序自己复现这种格式可以先打印字符串0x再把 32 位数按 4 bit 一组拆出来通过查表输出0到9、a到f。这种移位和掩码的思路和 MIPS 指令译码器设计里的位域拆分是同一种操作核心就是srl加andihex_nibble: andi $a0, $t2, 0xf slti $t9, $a0, 10 bnez $t9, hex_dec addi $a0, $a0, 87 # 10..15 转成 a..f j hex_out hex_dec: addi $a0, $a0, 48 # 0..9 转成 0..9 hex_out: li $v0, 11 syscall这只是打印一个 nibble 的最小代码。完整输出斐波那契数时要从最高 4 位开始每打印一组就右移 4 位循环 8 次覆盖 32 位。对实验本身来说不是必需项但理解了这一段syscall 34 和自写十六进制输出之间的差异就再也不会成为困惑。5. QtSpim实测N7、N46与N47的边界行为5.1 对账输出在 QtSpim 里加载.s文件后直接运行并输入 7得到的结果应该和题目示例一致从 0 到 6 共 7 项分别是 1、1、2、3、5、8、13。唯一明显差异在十六进制列实际 syscall 34 打出的会是1、2、3、5、8、d不带0x也不补前导零。实验报告如果要贴输出直接截图原始结果比手动补成0x00000013更可信。5.2 N46和46的边界输入N实际行为直接原因-1打印N should greater than 0!后退出ble分支生效1只打印 index 0retfib 返回长度 17打印 index 0 到 6i 到 7 时bge退出46打印 index 0 到 45没有 Overflow!i46 时循环条件先判定退出47打印 index 0 到 45随后打印 Overflow!i46 时算出 F_46溢出检查触发N46 是一个很隐蔽的坑。循环条件是bge $t1, $t0, retfib意味着 i 等于 N 时直接退出不再计算array[N]。第 45 项斐波那契数是 1836311903没超出 32 位有符号范围要算第 46 项才会得到 2971215073超出上限。所以 N46 时程序恰好一路顺利打印到 index 45根本不会进入 overflow 分支。题目示例把 46 当作“溢出检测”的输入并不准确真正能看到Overflow!消息的输入是 47。如果想用 N46 也能触发溢出可以把循环条件从bge改成bgt让 i 等于 N 时再进入一次循环体。改完之后 i46、N46 这一次会算出超范围的数再走溢出分支。这正好说明循环边界和溢出点的相对位置决定了测试现象。5.3 在单步模式里看证据验证这个边界最直接的办法是单步。在 QtSpim 的 Simulator 菜单里选择单步执行跑到 fibloop 附近时重点看两个寄存器$t1是当前下标$t7是刚算出来的结果。N 取 47 时当$t1从 45 变成 46$t7会变成0xB11924E1这就是 2971215073 的无符号位模式。继续执行到bltz $t5, overflow前看$t5的最高位是 1所以判定为负数跳转生效。Data 窗口里也可以直接观察 array 标签所在内存。从array地址开始每 4 字节一项单步时能看到0x00000001、0x00000001、0x00000002、0x00000003依次写满。溢出发生前数组最后一个有效项是0x6d73e55f即 1836311903。判断溢出时盯着$t7和$t5的第 31 位比记十进制溢出值直观得多。本文还有配套的精品资源点击获取