1454 words
7 minutes
computer architecture (22) Nested Procedure Call

完整题目
将以下 C 语言代码(数组求平方和)翻译为 MIPS 汇编代码。
在这个程序中,主函数 sum_of_squares 会遍历一个数组,提取每个元素,调用子函数 square 计算其平方,并将结果累加。
// 主函数:计算数组元素的平方和int sum_of_squares(int a[], int size) { int i = 0; int sum = 0; for (i = 0; i < size; i++) { sum = sum + square(a[i]); } return sum;}
// 子函数:计算单个整数的平方int square(int a) { int square; square = a * a; return square;}完整解答 (MIPS 汇编)
寄存器分配约定:
$a0:数组首地址a[]$a1:数组长度size$a2:传递给子函数square的参数(讲师此处未使用标准的$a0传参,是为了避免覆盖原有的数组基址)$t0:循环变量i$t1:累加变量sum$v0:函数的返回值
# ==========================================# 主函数:sum_of_squares# ==========================================sum_of_squares: # --- 1. 函数序言(保护现场) --- addi $sp, $sp, -4 # 栈指针向下移动 4 字节,开辟栈空间 sw $ra, 0($sp) # 【关键】将当前的返回地址 $ra 压入栈中保存
# --- 2. 变量初始化 --- add $t0, $zero, $zero # i = 0 add $t1, $zero, $zero # sum = 0
# --- 3. 循环体 ---loop: # 计算数组元素 a[i] 的内存地址 sll $t3, $t0, 2 # $t3 = i * 4 (计算字节偏移量,一个 int 占 4 字节) add $t3, $t3, $a0 # $t3 = 偏移量 + 数组基址 (得到 a[i] 的绝对地址)
# 读取 a[i] 的值并调用子函数 lw $a2, 0($t3) # 将 a[i] 的值从内存读入 $a2 (准备作为参数传给 square) jal square # 调用 square 子函数 (此时会自动更新 $ra)
# 累加结果与更新计数器 add $t1, $v0, $t1 # sum = sum + square 的返回值 ($v0) addi $t0, $t0, 1 # i++
# --- 4. 循环条件判断 --- slt $t3, $t0, $a1 # 判断 i < size。如果成立,$t3 = 1;否则 $t3 = 0 beq $t3, $zero, exit # 如果 $t3 == 0 (即 i >= size),跳出循环进入 exit j loop # 否则,跳回 loop 标签继续下一次循环
# --- 5. 函数收尾(恢复现场) ---exit: add $v0, $t1, $zero # 将最终的累加结果 sum 放入返回值寄存器 $v0 lw $ra, 0($sp) # 【关键】从栈中恢复外层函数的返回地址 $ra addi $sp, $sp, 4 # 栈指针加 4,释放栈空间 jr $ra # 跳转回上一层调用者
# ==========================================# 子函数:square# ==========================================square: mul $v0, $a2, $a2 # 将传入参数自己相乘,结果放入 $v0 jr $ra # 子函数执行完毕,返回到主函数没问题,MIPS 汇编的嵌套调用确实是个容易绕进去的难点。为了帮你长久记忆,我们抛开繁琐的代码行,提炼出这道题背后最核心的 3 个知识点,并为你提供记忆的“心智模型”。
核心痛点:为什么一定要用栈?($ra 覆盖危机)
你可以把寄存器 $ra (Return Address) 想象成一张只有一行的便签纸,上面写着“干完活回哪里”。
- 当
main调用sum_of_squares时,硬件自动在便签纸上写下了main的返回地址。 - 但是,当
sum_of_squares内部再次调用jal square时,硬件会无情地擦掉刚才的内容,写上square的返回地址。 - 灾难发生:等
sum_of_squares全部的循环跑完,想要回家时,发现便签纸上的地址早就变了,程序就会迷路(死循环或崩溃)。
记忆口诀: 只要你的函数里出现了 jal 指令,进门第一件事必须是“复印便签纸存进保险箱(压栈)”,出门最后一件事必须是“从保险箱拿出复印件(弹栈)”。
必须刻在脑子里的 3 个知识点
1. 嵌套调用的“汉堡包”模板 (The Stack Sandwich)
不要死记硬背每一行代码,把你写的所有嵌套函数都想象成一个汉堡包,上下两块面包是固定死板的操作,中间的肉饼才是你要写的核心逻辑。
-
顶层面包(保护现场 / 压栈):
addi $sp, $sp, -4 # 栈顶指针下移(分配空间)sw $ra, 0($sp) # 将便签纸上的地址存入栈 -
中间肉饼(核心业务逻辑):
- 这里你可以随意使用寄存器,肆无忌惮地使用
jal调用其他函数。
- 这里你可以随意使用寄存器,肆无忌惮地使用
-
底层面包(恢复现场 / 弹栈):
lw $ra, 0($sp) # 把存好的地址重新抄回便签纸addi $sp, $sp, 4 # 栈顶指针上移(释放空间)jr $ra # 循着便签纸上的地址回家
2. 数组地址计算的“乘 4 法则”
在高级语言里,a[i] 看起来像是一步操作。但在底层硬件看来,内存是一条连续的字节街区。因为一个 int 整数占据 4 个字节(32 位),所以第 个元素的物理门牌号公式永远是:
汇编实现:使用左移 2 位 (sll) 来实现乘以 4 的操作,这比使用 mul 乘法指令快得多。
sll $t3, $t0, 2 # $t3 = i * 4 (位移大法好)add $t3, $t3, $a0 # $t3 = 偏移量 + 数组首地址3. 循环结构的底层倒装句
我们在 C 语言中习惯的 for (i = 0; i < size; i++),在汇编里往往被翻译成了“倒装句”。
- C 语言的正向思维: “如果 ,就进循环继续干活。”
- 汇编的反向思维: “如果 (或者判断 为假),我就跳出去(
exit),否则我就老老实实掉头回到上面(j loop)。” 所以你会看到slt(Set on Less Than) 经常和beq(Branch on Equal) 结对出现,用来判断终止条件。
computer architecture (22) Nested Procedure Call
https://blog.yirong.site/posts/0071/
ページ閲覧数:
読み込み中…
サイト閲覧数:
読み込み中…