news 2026/10/21 16:03:24

MIPS汇编实现无符号数阶乘:HI/LO寄存器与溢出检查全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MIPS汇编实现无符号数阶乘:HI/LO寄存器与溢出检查全解析

简介:面向计算机组成原理期末课程设计的完整资料包,内容围绕在THJZ-2模型机上设计指令系统并实现无符号数阶乘的全过程。文档按课程设计报告结构展开,包含设计目的与内容、数据通路设计、指令执行流程、微程序流程图、设计步骤、程序代码与调试结果、自定义指令系统说明、设计总结与参考文献等模块,适合需要在模型机上完成指令/微指令设计并进行程序验证的本科生参考和套用。资源共1个doc文件,压缩包大小1.23MB,主体为可直接阅读编辑的课程设计报告,覆盖从汇编指令到机器指令、微指令再至微操作的概念落地,能够帮助读者理解计算机整机工作过程。目前已有61人学习下载。对寻找同类课程设计思路、微指令编码与调试排错方法的同学,这份报告提供了具体设计路径和验证结果,参考价值较高。

1. 无符号数阶乘:课程设计里最容易“满盘皆输”的一道题

如果你以为阶乘就是“写个循环连乘”,那这道课设最多拿个及格分。真正的考点藏在“无符号数”三个字里:32 位无符号整数最大只能装到 4,294,967,295,而 13! 已经是 6,227,020,800,早就溢出了;到了 20!,甚至连很多模拟器自带的打印函数都会给你吐出一个负数。换句话说,这道题表面考乘法,实际考的是边界处理、数据通路的理解,以及你对乘法指令是“截断低位”还是“保留完整 64 位乘积”的认知。这篇文章我就按自己带课设时常用的方案,从方案选型、MIPS 汇编实现、无符号打印到边界测试,把完整路子讲清楚。适合正在做计算机组成原理课设、或者想弄明白乘法指令 HI/LO 到底怎么用的同学。

2. 先定方案:为什么循环比递归稳,为什么无符号数的“边界”才是考点

任何汇编课设,动手写代码之前都要先回答三个问题:用什么指令集、用什么控制结构、结果超过 32 位怎么办。这三个问题不解决,代码写一半必然翻车。

2.1 从数学定义到硬件指令:阶乘到底在算什么

阶乘的数学定义是 N! = 1 × 2 × … × N,这个定义映射到汇编里就是一条循环:累乘器初始化为 1,循环计数器从 1 走到 N,每一步把累乘器和计数器相乘。问题在于“相乘”这两个字。MIPS 里乘法相关的指令有一组容易混淆的:mult、multu、mul。其中mult和multu是真实指令,乘积结果是 64 位,分别存放在特殊寄存器 HI(高 32 位)和 LO(低 32 位)里;mul是伪指令,它由汇编器展开成mult加mflo,只把低 32 位取回来当结果,高位直接丢掉。

你算无符号数阶乘,就应该用multu,每轮乘法后主动检查mfhi拿出来的高 32 位是不是零——不是零,说明乘积已经超出 32 位无符号数范围。很多同学图省事用mul,结果就是 13! 开始静默截断,程序“跑通”了,结果全错。这是第一个要刻在脑子里的选型结论:无符号乘法必须走multu,溢出必须靠检查 HI 寄存器来抓,不能指望乘法指令帮你报错。

2.2 循环与递归选型:延迟槽、栈帧和“20 层深度”的代价

阶乘天然可以用递归写:F(n) = n × F(n-1)。从代码简洁度看递归很诱人,但放到计算机组成原理课设里,递归要付出三笔代价。第一笔是栈帧:每次递归调用都要保存返回地址和调用者保存寄存器,至少 12 字节栈空间,N=20 时深度 20 层,栈操作指令数量是循环版本的好几倍。第二笔是延迟槽:如果你用的平台开启 branch delay slot,jr $ra后面的延迟槽指令不能被任意填充,递归返回路径上出错的概率远高于循环。第三笔是答辩追问:导师大概率会问“递归的调用开销在哪里”“叶子函数能不能优化”,你答不上来反而扣分。

我一般直接选循环。循环版本只用三个寄存器:N、计数器 i、累乘器 result,不存在栈操作,指令数少,单步调试时数据流一目了然。更重要的是,循环版本可以把溢出检查放在每轮乘法之后,做到“哪一步溢出立刻停”,递归版本想做同样的事就得在每层递归返回值之后判断,代码结构乱得多。结论:除非题目明确要求“用递归展示栈帧原理”,否则循环是这道题的更优解。

2.3 数据通路:32 位寄存器、乘法指令和输出格式

第三个方案问题是输出的数据通路。课程设计通常要求结果以十进制打印,但很多教学模拟器的打印整数 syscall 是按有符号数处理的。无符号计算结果一旦超过 0x7FFFFFFF,打印出来会变成负数,比如 12! = 479,001,600 没问题,但如果你把边界放宽到 20!,低 32 位的结果约等于 2,432,902,008,176,640,000 截断后的值,按有符号打印必然是负的。所以方案里必须包含一个“无符号十进制打印函数”,自己用除法逐位取余,而不是依赖模拟器自带打印。这个函数我后面会给出完整实现。

这里还要顺带说一个常见误区:有人为了省事,直接把 13! 以上的输入判为非法,只做 N ≤ 12。这不算错,但如果你能在答辩时拿出“13! 用 64 位乘积输出”的扩展版本,分数完全不是一个档次。第 6 章我会讲怎么把上限推到 20!。总之,方案分三层:最低限度是 N ≤ 12 输出低 32 位;进一层是 13! 到 20! 用 HI/LO 拼接输出;再进一层是 64 位乘法的进位链处理。你先做到第一层,再往上补。

3. 写出第一个能跑的阶乘程序:MIPS 汇编实现与逐段参数说明

方案定了,下面给出一份完整的、可在常见 MIPS 教学模拟器里直接运行的程序。这份代码的定位是“校验输入 + 低 32 位乘积 + 无符号打印”,它能正确处理 0 到 12 的阶乘,并在溢出时给出提示。

# 文件名: factorial_unsigned.asm # 功能: 读入 N(0~12),计算 N! 并打印无符号十进制结果 # 若 N 越界或乘积超过 32 位,则打印提示后退出 # # 寄存器分配 # $s0 = N 输入值 # $s1 = i 循环计数器(1~N) # $s2 = result 当前累乘结果 # $t1 = 乘积高 32 位 # $t2 = 临时值 / 取余结果 .data prompt: .asciiz "input N (0~12): " result_msg: .asciiz "result = " overflow_msg: .asciiz "overflow!" invalid_msg: .asciiz "invalid input." .text main: li $v0, 4 la $a0, prompt syscall li $v0, 5 syscall move $s0, $v0 # 校验: N < 0 或 N > 12 都直接退出 bltz $s0, invalid # 负数过滤 sltiu $t0, $s0, 13 # 无符号比较: N < 13? beq $t0, $zero, overflow # N >= 13, 超出低32位范围 li $s2, 1 # result = 1 li $s1, 1 # i = 1 loop: bgt $s1, $s0, done # i > N 则结束循环 multu $s2, $s1 # result * i, 64位乘积写入 HI/LO mfhi $t1 # 取出高32位 mflo $s2 # 低32位作为新的 result bne $t1, $zero, overflow # 高32位非零, 说明溢出 addi $s1, $s1, 1 b loop done: la $a0, result_msg li $v0, 4 syscall move $a0, $s2 # 把无符号结果传给打印函数 jal print_unsigned j exit overflow: la $a0, overflow_msg li $v0, 4 syscall j exit invalid: la $a0, invalid_msg li $v0, 4 syscall j exit exit: li $v0, 10 syscall # 无符号十进制打印函数 # 输入: $a0 = 待打印的无符号整数 # 做法: 反复除10取余, 余数先压栈, 最后逆序弹出打印 print_unsigned: addi $sp, $sp, -16 sw $ra, 12($sp) sw $s0, 8($sp) move $s0, $a0 li $t0, 10 li $t1, 0 # 记录余数个数 bne $s0, $zero, div_loop li $a0, 48 # 输入为0时直接打印字符'0' li $v0, 11 syscall j pu_ret div_loop: divu $s0, $t0 # 无符号除法: $s0 / 10 mflo $s0 # 商, 作为下一次被除数 mfhi $t2 # 余数 addi $t2, $t2, 48 # 余数数字转 ASCII 字符 addi $sp, $sp, -4 sw $t2, 0($sp) # 余数压栈 addi $t1, $t1, 1 bne $s0, $zero, div_loop print_stack: beq $t1, $zero, pu_ret lw $a0, 0($sp) # 逆序弹出 addi $sp, $sp, 4 addi $t1, $t1, -1 li $v0, 11 syscall j print_stack pu_ret: lw $s0, 8($sp) lw $ra, 12($sp) addi $sp, $sp, 16 jr $ra

这份代码的逻辑分三层。主循环里,multu算出的是完整 64 位乘积,高 32 位进 HI、低 32 位进 LO;mfhi先取出高 32 位判断是否为零,mflo再把低 32 位写回累乘器。顺序上先取 HI 还是先取 LO 没有影响,但必须在下一轮multu之前取完,因为新的乘法会覆盖 HI/LO。这里的sltiu是无符号比较指令,配合前面的bltz,能做到“负数直接拦下,大于 12 的提示溢出”。

打印函数是这段代码里最容易抄错的部分。divu执行无符号除法,商留在 LO、余数留在 HI,所以mflo拿商、mfhi拿余数,顺序和乘法相反。余数加 48 转成 ASCII 后压栈,全部除完之后再逆序弹出打印,这样高位在前、低位在后,十进制顺序才对。如果你用的模拟器支持扩展 syscall 直接打印无符号数,可以省掉这个函数,但绝大多数课程设计平台都要求自己处理,所以这个函数值得背下来。

还有一个细节:bgt是伪指令,MARS 这类模拟器默认支持;如果你换成需要严格 MIPS 指令集的平台,要自己改写为slt+beq组合。代码里 0 的阶乘不需要进循环,累乘器初始值为 1,直接跳到打印,这是数学定义要求的,别把初始值写成 0。

4. 避坑指南:无符号数阶乘的 5 个高频翻车点

写这道题翻车的同学,绝大部分不是不会写乘法,而是栽在下面这 5 个地方。每一条都是真实出过的错,按“现象 → 原因 → 解决”给你拆开。

4.1 输入负数被“放行”:无符号比较把负数当大正数

现象:输入 -1,程序没有报错,反而输出了一堆乱码或者直接卡死。原因:你用了sltiu $t0, $s0, 13做范围校验,但无符号比较会把所有负数当作接近 4.29 亿的大正数,于是 -1 也被判定为“小于 13”,顺利进入循环。解决:先做有符号负数判断,再加无符号上界判断。我第一次写的时候只用了sltiu,被这个坑卡了一下午。你可以在校验部分加bltz $s0, invalid,把负数拦在无符号比较之前。

4.2 用mul做乘法:13! 开始结果悄悄变错

现象:N=12 全对,N=13 输出一个很大的数,但和真实结果 6227020800 怎么都对不上。原因:mul伪指令只保留低 32 位乘积,13! 的完整乘积是 0x00000001_732B6400,高 32 位是 0x1,这一部分被mul丢弃了。解决:全部改用multu,每轮乘法后用mfhi检查高 32 位;如果只想支持 N ≤ 12,那在输入校验阶段就把 N≥13 的输入直接判为溢出,不要留给乘法去“自然出错”。

4.3 打印出来是负数:syscall 打印按有符号处理

现象:程序算出来数学上完全正确,但打印结果是负数,比如 N=12 输出 479001600 没问题,再往大算就出现负值。原因:模拟器的打印整数系统调用是按补码解释结果的,超过 0x7FFFFFFF 后最高位是 1,就被当成负数。解决:不依赖系统调用打印,自己写print_unsigned,用divu逐位取余后逆序打印。不要贪图省事直接调系统调用,这道题“无符号数”三个字就是明确要求你把无符号打印打好。

4.4 递归实现时返回地址被覆盖

现象:用递归写法,N=5 结果正确,N=10 结果错误,而且错误没有规律。原因:递归调用子程序前,当前层的返回地址$ra没压栈,被内层调用覆盖;回到外层时jr $ra跳到错误位置。解决:如果你坚持用递归,进入函数第一步就把$ra和用到的$s寄存器压栈,返回前恢复。更省心的方案是回到第 2 章的结论:直接用循环,压根不碰$ra,栈操作为零,从根上避开这个问题。

4.5 延迟槽模式下循环多执行一步

现象:开启延迟槽的平台上,程序计算出的阶乘比预期多乘了一次。原因:b loop之后紧跟的addi $s1, $s1, 1被当作延迟槽指令执行了,也就是说循环跳回时计数器又被加了一次,等于每一步多乘了一个数。解决:先确认模拟器的延迟槽是否默认开启;如果开启,把addi放到分支之前,或者在分支后面补一条无副作用的nop。我习惯的做法是写循环时无条件跳转后面永远跟nop,避免在不同平台之间移植时踩坑。

5. 验证不是跑通就完事:构造边界测试与检查点清单

很多同学交课设时只说“我试了几个数,都能跑”。这话在答辩时等于告诉导师你没做过系统性验证。阶乘的验证重点不是中间值,而是边界:0 的阶乘、首次溢出的位置、最大值附近的进位行为。

5.1 用最小输入集覆盖 0 到 20 的关键节点

不管你的程序支持到 12 还是 20,都按下面这张表跑一遍。这组用例是我每次做汇编课设都在笔记里抄一份的“地基数据”。

输入 N期望结果(十进制)期望结果(十六进制)观察重点
010x00000001循环是否一次都不执行
110x00000001i=N 时循环恰好执行一轮
51200x00000078常规小值,乘法无进位
124790016000x1C8CFC0032 位无符号内最后一个正确值
13overflow 提示高 32 位为 0x1首次溢出,mfhi必须抓到非零
20扩展版输出 2432902008176640000需 64 位拼接接近 64 位上限,验证进位链

跑完这组,程序对不对就一目了然。特别注意第 13 行:如果你的实现是“支持到 12,超出提示溢出”,那第 13 行必须走overflow分支,而不是输出一个看似正常的数。这一行是区分“正确截断”和“静默算错”的分水岭。

5.2 单步调试时看哪些寄存器

模拟器里单步调试时要盯着四个位置:$s1(循环计数器)、$s2(累乘结果)、HI、LO。每次执行完multu后,先看 HI 是否为 0,再看 LO 是否等于你手算的中间值。N=5 的时候你可以在纸上先算一轮:1×1=1,1×2=2,2×3=6,6×4=24,24×5=120,这五步的单步结果应该和寄存器完全一致。如果某一步 LO 对但 HI 非零,说明乘积已经越界,程序应该立刻跳向溢出分支,而不是继续循环。

我还建议你在单步时把$s0也加进 watch 窗口。$s0是输入的 N,程序运行过程中它不应该被任何指令改写。如果你发现它变了,基本可以断定某个move或栈操作把寄存器顺序搞错了,这是汇编课设最常见的隐蔽 bug。

5.3 把结果导出与提交检查的两种方法

验证最后一步是留证据。模拟器一般支持把运行面板内容另存为文本文件,或者用 Log 窗口导出指令执行记录。提交时我建议同时存三样:程序源代码、一组运行截图(覆盖上表前四行)、一段简短说明(写清楚 N≥13 时走溢出分支的原因)。截图里要能看到输入值和输出值,别只截一个最终弹窗——导师要看的是输入输出对应关系,尤其是 0 和 1 这两个边界值。

如果模拟器支持命令行批处理,你还可以把多个输入写进一个数据文件,通过重定向一次性跑完,然后把输出拿去和 Python 算的math.factorial结果做 diff。这一步能省大量手工核对时间,而且能证明你的程序在不同输入下行为一致。没有自动化条件就手动录屏,总之要把“我验证过”变成“我有证据”,这是答辩时最实在的加分项。

6. 让分数上去的进阶思路:查表优化与 64 位乘积处理

如果前面的内容你已经跑通,可以再往两个方向走。第一个是查表法:既然 0! 到 12! 都是合法低 32 位结果,干脆把 13 个常量写进数据段,输入 N 后直接根据索引取结果。查表版本的指令数比循环少一个数量级,答辩时你可以说“我用空间换时间,把阶乘变成 O(1) 查表”。.word伪指令按 4 字节对齐存数据,取的时候用sll把 N 左移 2 位做偏移即可。这个方向适合展示你对数据寻址方式的理解,代码量却很小。

第二个方向是真正的 64 位乘积输出,把上限推到 20!。核心思路是每轮乘法不再拿一个 32 位累乘器,而是用两个寄存器保存当前结果:H 存高 32 位、L 存低 32 位,乘法拆成两个multu。设当前结果 A = H×2^32 + L,乘数 i,则 L×i 得到新低 32 位和进位 c,H×i 得到高位乘积,新高位 = H×i 的低 32 位 + c。这里容易被忽略的是 addu 本身的进位:addu $t7, $t5, $t4产生进位时,要用sltu把它补到更高位上去。我当年第一次写 64 位扩展版,就是漏了这个进位链,导致 N=20 时结果差了 2^32 的倍数。验证方法就用手算的 20! 十进制值 2432902008176640000,转成十六进制后和程序里 H、L 拼接结果比对。这一步做出来,整套课设的深度立刻不一样了。

我自己的教训是:写任何汇编课设,先把输入边界和溢出行为写成注释贴在代码头部,再动手写主逻辑。那次我图快直接写循环,结果负数输入、13! 截断、无符号打印三个坑连环踩,改了一整天才理顺。从边界出发反推实现,比从实现出发补边界要快得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 16:02:51

lingbot-map技术解析:为聊天机器人打造轻量级地图服务

第一次看到 lingbot-map 这个项目名的时候&#xff0c;我下意识以为它又是某个地图渲染库。后来我把代码和设计文档翻了一遍才反应过来&#xff1a;它解决的其实是一类特别常见、但很少被单独拎出来讲的问题——当一个聊天机器人或者智能助手需要“地图能力”时&#xff0c;到底…

作者头像 李华
网站建设 2026/10/21 16:01:53

TOPSIS优劣解距离法:多指标决策的Python实现与实战

先从一个我实际遇到的场景说起。前阵子帮一个团队做方案选型&#xff0c;五六个备选方向摆在桌上&#xff0c;指标倒是不多——性能、成本、周期、稳定性。但麻烦的地方在于&#xff0c;每个方案都有自己特别硬的长处&#xff0c;也都有明显的短板&#xff1a;A性能最强但贵&am…

作者头像 李华
网站建设 2026/10/21 15:57:58

知识工作流插件集合:从采集到应用的全链路自动化实践

作为长期泡在知识管理工作流里的人&#xff0c;我电脑里最多的东西恐怕就是各种插件。从早期的文本片段补全工具&#xff0c;到后来配合知识库使用的各类脚本&#xff0c;再到如今给大模型打工用的上下文注入插件&#xff0c;这条折腾路线我走了好几年。最近我在整理和重构自己…

作者头像 李华
网站建设 2026/10/21 15:57:41

从概念到实操:技能养成方法论与刻意练习指南

"skills"这个词&#xff0c;在职场上被说得太勤了&#xff0c;反而没人愿意认真想一下它到底意味着什么。很多人觉得掌握了一个技能就是会操作某个工具、背下了几个概念&#xff0c;可一遇到真实场景还是发怵。我这些年带过团队&#xff0c;自己也从零啃过好几个全新…

作者头像 李华
网站建设 2026/10/21 15:55:44

无标题项目实战:从模糊需求到轻量应用搭建

1. 项目背景与核心需求 1.1 无标题项目的真实动机 说实话&#xff0c;很多朋友拿到“无标题”这种空白项目时&#xff0c;第一反应都是懵的&#xff0c;不知道该从哪下手。我自己也经历过这个阶段&#xff0c;在空白的编辑器里对着光标发呆&#xff0c;想着“这不是为难人嘛”…

作者头像 李华
网站建设 2026/10/21 15:55:14

插件化架构实战:构建知识工作自动化流程

1. 这个插件集合到底在解决什么问题第一次看到knowledge-work-plugins这个标题&#xff0c;我脑子里蹦出来的第一个判断是&#xff1a;这大概率是一个面向知识工作者的插件集合项目。所谓知识工作者&#xff0c;说白了就是那些每天跟文档、表格、会议纪要、研究资料打交道的人—…

作者头像 李华