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

alt text

完整题目#

将以下 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 位),所以第 ii 个元素的物理门牌号公式永远是:

目标地址=基地址+(i×4)\text{目标地址} = \text{基地址} + (i \times 4)

汇编实现:使用左移 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 语言的正向思维: “如果 i<sizei < \text{size},就进循环继续干活。”
  • 汇编的反向思维: “如果 isizei \ge \text{size}(或者判断 i<sizei < \text{size} 为假),我就跳出去(exit),否则我就老老实实掉头回到上面(j loop)。” 所以你会看到 slt (Set on Less Than) 经常和 beq (Branch on Equal) 结对出现,用来判断终止条件。

computer architecture (22) Nested Procedure Call
https://blog.yirong.site/posts/0071/
Author
Kuchina
Published at
2026-06-09
License
CC BY-NC-SA 4.0
ページ閲覧数: 読み込み中…
サイト閲覧数: 読み込み中…