递归调用汇编代码翻译的一些心得
本文最后更新于:2024年4月22日 晚上
前言
教程 函数调用 - 计算机组成教程 (buaa.edu.cn) 中曾介绍过如何将带有递归函数调用的c语言代码翻译成汇编指令。P2课下也涉及了部分需要应用递归的题目,在此总结一下我完成此类汇编翻译的一些心得。
我们在此就以 P2_L0_full_1 - 系统能力课程实验平台 (buaa.edu.cn) 当作例题,简要介绍一下设计思路。
例题1:全排列生成
题干
题目编号 1121-36
题目要求
实现满足下面功能的汇编程序:
使用mips实现全排列生成算法。
以0x00000000为数据段起始地址。
输入一个小于等于6的正整数,求出n的全排列,并按照字典序输出。
每组数据最多执行500,000条指令。
请使用syscall结束指令
1
2li $v0, 10
syscall
输入格式
只输入一行,输入一个整数n **(0 < n <= 6)**。
输出格式
按照字典序输出n!行数组,每行输出n个数字,数字之间以空格隔开,每行最后一个数字后可以有空格。
输入样例
1 |
|
输出样例
1 |
|
提交要求
- 请勿使用
.globl main
- 不考虑延迟槽
- 只需要提交.asm文件。
- 程序的初始地址设置为Compact,Data at Address 0。
解答步骤
在此分享一下我完成汇编代码的一些思路步骤。
第一步:完成符合要求的C语言代码
C语言的代码与汇编指令具有很高的相似性,如果有一份正确的C语言代码,汇编编程题就相当于一个“翻译题”。
全排列问题的C语言代码如下:(如果对此有疑问的话,可以去网上搜索一下全排列的相关讲解,在此不展开说明)
第二步:一些必要的macro
使用宏可有效提高代码的复用性,提高代码的可读性。
我比较习惯用的macro有:
取array[i]的值到%d
1
2
3
4.macro getNum(%d,%i)
sll %d,%i,2
lw %d,array(%d)
.end_macro取array[i] [j]的值到%d,矩阵的列数为%n
1
2
3
4
5
6.macro getNum(%d,%i,%j,%n)
mul %d,%i,%n
add %d,%d,%j
sll %d,%d,2
lw %d,array(%d)
.end_macro将字压入栈
1
2
3
4.macro push(%src)
sw %src, 0($sp)
subi $sp, $sp, 4
.end_macro相对应的弹出栈
1
2
3
4.macro pop(%des)
addi $sp, $sp, 4
lw %des, 0($sp)
.end_macro
第三步:合理规划寄存器
s0
-s7
是留给“需要保存的变量”,如常量,全局变量等;t0
-t9
是留给“临时变量”,如循环时常用的i、j、k变量等。对于一般的题目,我使用s0
来保存n的值,t0
留给循环变量i,t9
留给计算出来的临时地址。
当然,如何分配主要还是看自己写的顺不顺手,只要自己有一套可操作的规则即可。
第四步:完成代码
我们在此先给出完整的汇编代码。如果对递归调用之外的地方存在疑问,可以再去阅读额外的讲解。
1 |
|
两个注意事项
1.注意return的位置
观察C语言代码,我们发现有两个需要return的位置。一般来说,return直接写作jr $ra
即可。
2.递归调用前后的处理工作
我们把上面代码中设计递归调用的片段截取下来:
1 |
|
这里,$ra
存储的是之前调用的位置+4,比如说在执行完jal FullArray
之后,$ra
存储的是pop($t1)的地址。$t0
存的是参数index,$t1
存的是FullArray里的遍历变量i。
进行push和pop的原因,无非是对寄存器内原有的值进行保护。对寄存器的保护(进行push和pop操作)一定是在函数调用前后进行的,而往往不是刚进入函数的时候。
难点在于,哪些变量需要被push?
首先,
$ra
是必须被存在栈里的,否则调用结束后返回调用位置可能会出现异常;然后,函数的参数、返回值等一般是需要被push的,在这里就是index;
其次,就是一些涉及到函数“状态”的量,比如说变量i,因为是对不同数字是否被占用进行遍历,存在遍历的先后顺序,所以i的不同会导致函数的“当前状态不同”;
假如不对i进行保存,i=3时递归到第二层,在第二层中假设i最后为0且未进入第三层递归,那么再回到第一层时,i的值就与之前发生了变化,会造成函数执行异常;
最后,调用者(caller)调用的函数(callee)中,callee使用到了一些caller中使用的寄存器,那么进行函数调用的时候需要保护caller中的这些寄存器。
确定了需要被push和pop的变量之后,我们还需要注意push和pop的顺序是否对应,栈是后入先出,最先push($ra)
就要最后pop($ra)
。
完成了push和pop的代码之后,我们就需要更新函数的参数
1 |
|
然后就可以递归调用了!
(一般来说函数的参数保存在a0
-a3
寄存器里,当我意识到这点时已经晚了)
至此,一个完整的含有递归的汇编代码就完成了!
借助上面的分析,看看下面这两道例题,并尝试自己解决。
例题2:哈密顿回路
题干
题目编号 1109-5 challenge!哈密顿回路 - 计算机组成教程 (buaa.edu.cn)
题目要求
输入一个具有n个顶点的无向图G,判断G是否有哈密顿回路哈密顿图 - OI Wiki (oi-wiki.org)。
输入格式
第一行是一个整数n,代表G有n个顶点,第二行是一个整数m,代表G有m条边,接下来的2 * m行,每行具有一个整数,设每个奇数行的数为a,它下一行的数b,序号为a, b的两个顶点间具有一条边,两个整数之间以回车隔开(点的标号从 1 开始)。
输出格式
输出一个整数,若为 0 则代表G不具有哈密顿回路,若为 1 则代表G具有哈密顿回路。
约定
1、0 < n < 100
2、0 < m < 100
3、请勿使用 .globl main
4、最大运行指令条数限制为 100000
5、请使用 syscall
结束程序:
1 |
|
输入样例1
1 |
|
输出样例1
1 |
|
输入样例2
1 |
|
输出样例2
1 |
|
提交要求
- 请勿使用
.globl main
- 不考虑延迟槽
- 只需要提交.asm文件。
- 程序的初始地址设置为Compact,Data at Address 0。
参考C语言代码
1 |
|
参考解答
1 |
|
例题3:01迷宫
题干
题目编号 1121-38 P2_L1_puzzle - 系统能力课程实验平台 (buaa.edu.cn)
题目要求
- 使用mips实现01迷宫路线数目计算。
- 以0x00000000为数据段起始地址。
- 输入一个n*m的01矩阵作为01迷宫,并给定他的起点与终点,求出他不同逃跑路线的数目(不同逃跑路线中可以有相同的部分,但是不能完全相同)。
- 每组数据最多执行5,000,000条指令。
示例
- 上图表示的是一个4*5的01矩阵,这个矩阵就是一个01迷宫。
- 如上图,以红色0作为起点,绿色0作为终点,每一次行进只能选择上下左右中值为0且未走过的位置,满足上述条件的路线,即为一条迷宫逃跑路线。如右图中,蓝色的路线即为一条逃跑路线。
输入格式
前两行输入两个整数n和m(n、m均为正整数并且小于等于7),分别代表01矩阵行数和列数。接下来的n*m行,每行输入1个整数(0或1),对应着01矩阵各个元素值(第i*m+j个整数为矩阵的第(i+1)行第j个元素,即一行一行输入)。接下来的四行分别代表迷宫的起点和终点,每行一个整数,分别代表起点与终点行数和列数。
输出格式
只输出一个整数,代表逃跑路线的数目。
输入样例
1 |
|
输出样例
1 |
|
提交要求
- 请勿使用
.globl main
- 不考虑延迟槽
- 只需要提交.asm文件。
- 程序的初始地址设置为Compact,Data at Address 0。
参考C语言代码
1 |
|
参考解答
解答1
1 |
|