第五讲 · MIPS 汇编编程与函数调用
对应材料:2025 计组复习提纲“MIPS 汇编程序”,并用实验目录中的 MARS 汇编练习核对程序类型。
学汇编不是为了拿助记符替换 C 关键字,而是为了把程序拆成 CPU 真正能执行的微小动作:寄存器之间算一步、主存与寄存器之间搬一步、根据比较结果改一次 PC。
1. 程序与数据在内存中的样子
MIPS 汇编常用 .text 表示代码段,用 .data 表示静态数据段:
.data
msg: .asciiz "hello"
arr: .word 3, 5, 8
.text
main:
# 指令写在这里
汇编里的数据类型首先说明“占多少字节”:
| 声明 | 大小 | 常见用途 |
|---|---|---|
.byte | 1 B | 字符、字节数组 |
.half | 2 B | 半字 |
.word | 4 B | 32 位整数、地址 |
.asciiz | 字符串长度 + 1 | 末尾自动补 \0 |
2. 寄存器要按调用约定分工
| 名称 | 编号 | 约定用途 |
|---|---|---|
$zero | 0 | 常量 0 |
$at | 1 | 汇编器临时使用 |
$v0-$v1 | 2–3 | 返回值 |
$a0-$a3 | 4–7 | 前四个参数 |
$t0-$t9 | 8–15, 24–25 | 临时值,调用者若需要应自行保护 |
$s0-$s7 | 16–23 | 长期变量,被调用者使用后要恢复 |
$sp | 29 | 栈顶指针 |
$fp | 30 | 可选的栈帧基准 |
$ra | 31 | 返回地址 |
这些不是硬件强制的“类型”,而是软件约定。大家遵守同一套约定,分别编译的函数才可以互相调用。
3. 主存访问:地址先算出来,再读或写
统一格式是
op rt, offset(base)
有效地址为
3.1 字、半字和字节
| 指令 | 传输量 | 读出后怎样扩展 |
|---|---|---|
lw/sw | 4 B | 整个字,不需要扩展 |
lh/sh | 2 B | lh 符号扩展 |
lhu | 2 B | 零扩展 |
lb/sb | 1 B | lb 符号扩展 |
lbu | 1 B | 零扩展 |
sb 和 sh 只把寄存器的低 8 位或低 16 位写入主存。lb 读到 0x80 时得到 0xFFFFFF80,lbu 则得到 0x00000080。
3.2 对齐
- 字地址通常要求是 4 的倍数;
- 半字地址通常要求是 2 的倍数;
- 字节可以位于任意地址。
数组 int A[100] 的 A[i] 地址为
这就是“基址 + 偏移”与数组、结构体天然契合的原因。
3.3 大端与小端
字节序只决定一个多字节对象的各字节如何摆在连续地址中,不改变寄存器里的数值:
- 大端:最高有效字节放最低地址;
- 小端:最低有效字节放最低地址。
做题时先按题目给出的字节序画出地址从低到高的字节,再执行 lb/lbu/lw,不要靠肉眼猜。
4. 算术、逻辑和移位
4.1 加减法
add $t0, $s0, $s1
addu $t0, $s0, $s1
addi $t0, $s0, -12
sub $t0, $s0, $s1
带 u 的名字并不意味着寄存器突然有了无符号类型;主要差别是是否对有符号溢出触发异常。加法器做的仍然是相同的 32 位补码加法。
乘除法结果可能超过 32 位,因此课件使用 HI/LO:
mult $s0, $s1 # 64 位乘积写入 HI:LO
mflo $t0 # 取乘积低 32 位
div $s0, $s1 # LO=商,HI=余数
mfhi $t1 # 取余数
4.2 位运算
and $t0, $s0, $s1
or $t0, $s0, $s1
xor $t0, $s0, $s1
nor $t0, $s0, $s1
andi $t0, $s0, 0x00FF
ori $t0, $s0, 0x8000
逻辑立即数采用零扩展,适合掩码操作。
4.3 移位
| 指令 | 含义 |
|---|---|
sll | 逻辑左移,低位补 0 |
srl | 逻辑右移,高位补 0 |
sra | 算术右移,高位复制符号位 |
sllv/srlv/srav | 移位量来自寄存器低 5 位 |
例如 $t0=0xFFFFFF00:
srl $s0, $t0, 8 # 0x00FFFFFF
sra $s1, $t0, 8 # 0xFFFFFFFF
左移 位在不溢出时相当于乘 ;算术右移负数时与 C 语言整数除法的舍入规则未必完全等价,不能无条件拿它代替除法。
5. 16 位立即数怎样构造 32 位常量
lui 把 16 位立即数放到寄存器高 16 位,低 16 位清零:
lui $t0, 0xABCD # $t0 = 0xABCD0000
ori $t0, $t0, 0x1234 # $t0 = 0xABCD1234
所以 li $t0,0xABCD1234 通常是伪指令,要由汇编器展开成一条或多条真实指令。
6. 分支、比较和循环
6.1 相等分支
beq $s0, $s1, Equal
bne $s0, $s1, NotEqual
beq/bne 只有“相等/不等”比较。其他关系通常先用 slt 产生 0/1,再与零寄存器分支:
slt $t0, $s0, $s1 # $t0 = 1 当且仅当 $s0 < $s1(有符号)
bne $t0, $zero, Less
sltu 按无符号数比较。寄存器比特没变,变化的是比较器如何解释最高位。
6.2 把 if 翻成控制流
if (a == b) {
x = x + 1;
} else {
x = x - 1;
}
假设 a,b,x 分别在 $s0,$s1,$s2:
bne $s0, $s1, Else
addi $s2, $s2, 1
j EndIf
Else:
addi $s2, $s2, -1
EndIf:
关键不是死记“先跳哪边”,而是先画基本块,再用分支把不该执行的块绕开。
6.3 循环例:字符串复制
课件用 while((*q++=*p++)!='\0') 说明 C 到 MIPS 的翻译:
Loop:
lbu $t0, 0($s0) # 取 *p
sb $t0, 0($s1) # 写 *q
addiu $s0, $s0, 1 # p++
addiu $s1, $s1, 1 # q++
bne $t0, $zero, Loop
指针每次加 1,因为字符串元素是 1 字节;若是 int 数组,则通常每次加 4。
7. 伪指令:给人写,不给 CPU 执行
常见伪指令:
move $t0, $s0 # 可展开为 addu $t0,$s0,$zero
li $t0, 100 # 可能是一条 addiu,也可能是 lui+ori
la $t0, label # 加载标号地址,展开方式取决于地址与重定位
blt $s0, $s1, L # 通常展开为 slt + bne
课件把含伪指令、面向程序员的语言称为 MAL,把硬件真实执行的指令称为 TAL。做机器码题时必须先展开伪指令,否则“一行对应一条机器码”的前提不成立。
8. 函数调用的六件事
一次函数调用要完成:
- 调用者放好参数;
jal保存返回位置并跳到函数;- 被调用者为局部变量、需保护寄存器开栈帧;
- 执行函数主体;
- 把返回值放入
$v0-$v1,恢复现场; jr $ra返回。
8.1 jal 与 jr
jal func # 跳转,并把返回位置写入 $ra
...
func:
...
jr $ra
课程单周期模型没有实现分支延迟槽,因此把 jal 的链接值按 处理;经典带一条延迟槽的 MIPS 流水线会涉及 。做题必须服从题目给定的处理器模型,不能把两套约定混在一起。
8.2 栈帧为什么向低地址增长
课件采用 MIPS 常见约定:$sp 指向栈顶,开栈帧时减小 $sp,返回前再加回去。
func:
addiu $sp, $sp, -8
sw $ra, 4($sp)
sw $s0, 0($sp)
# 函数主体
lw $s0, 0($sp)
lw $ra, 4($sp)
addiu $sp, $sp, 8
jr $ra
“先减再存、先取再加”保证整个调用期间栈帧都属于当前函数。
9. 谁来保护寄存器
$s0-$s7:被调用者保存。函数若改过,返回前必须恢复;$t0-$t9:调用者保存。调用者若想跨函数调用保留,就在调用前自行保存;$a0-$a3、$v0-$v1:本来就用于传参和返回,调用后不能假定保持;$ra:叶函数不调用别的函数时可不压栈;非叶函数再次执行jal前必须保护旧$ra。
9.1 非叶函数示例
int sumSquare(int x, int y) {
return mult(x, x) + y;
}
sumSquare 调用 mult,所以至少要保护返回地址;y 若调用后还要用,也不能留在可能被覆盖的 $a1 或临时寄存器里。可以存入栈,或放进受保护的 $s 寄存器并按约定恢复。
10. 写汇编时按这条路线检查
- 先给变量、指针和数组基址分配寄存器;
- 把 C 结构拆成基本块和控制流,不逐字符翻译;
- 所有主存数据先
load,计算后再store; - 检查元素大小与地址步长;
- 区分符号扩展、零扩展和有符号/无符号比较;
- 函数调用前后检查
$ra、$sp与需要跨调用保存的值; - 最后才考虑减少指令数。
实验目录中的闰年、最大公约数、汉诺塔、回文串与冒泡排序,表面算法不同,底层都只是在反复组合本讲的几类动作。