Appearance
算术逻辑运算与分支跳转
概念
前两篇把"指令长什么样""地址怎么算"讲完了。现在到了指令的主体内容:
add之外,机器还会算什么?
MIPS32 的运算指令可以分成四组:
| 组 | 指令 | 一句话 |
|---|---|---|
| 算术 | add addu sub subu addi addiu | 加减(乘除单列) |
| 逻辑 | and or xor nor + andi ori xori | 按位操作 |
| 移位 | sll srl sra + sllv srlv srav | 移位(乘除 2 的幂) |
| 比较 | slt sltu slti sltiu | 产生 0/1,不跳转 |
而这一篇真正的主题,是一个贯穿全部四组的分野:
这个分野在 MIPS32 里表现为"同一个动作有两套指令":
| 动作 | 有符号版 | 无符号版 | 差别在哪 |
|---|---|---|---|
| 加 | add | addu | 溢出时是否产生异常 |
| 减 | sub | subu | 同上 |
| 乘 | mult | multu | 乘法的扩展方式(结果不同) |
| 除 | div | divu | 商与余数的取整方向(结果不同) |
| 比较 | slt | sltu | 比较的是"大小"还是"位型大小" |
| 右移 | sra | srl | 补 0 还是补符号位 |
注意这张表的前两行和后面四行,性质完全不同:
- 加法和减法:
add和addu算出来的位型完全一样——差别只在**"检测到溢出时要不要抛异常"**; - 乘、除、比较、右移:
mult和multu算出来的结果真的不一样——因为它们对操作数的"解读"不同(有没有符号位)。
所以"u 后缀"在 MIPS 里有两个不同的含义:
| 后缀位置 | 含义 |
|---|---|
add/sub/addi 的 u | "不检测溢出"(no Overflow exception) |
mult/div/slt 的 u | "按无符号解读操作数" |
这是最容易混淆的一处,先记死:
addiu里的u不是"无符号"——它照样做符号扩展,只是不抛溢出异常。
还有一个 MIPS 的著名设计选择要在这里说清:
对比 x86:x86 的 add 会顺手把 ZF/CF/OF/SF 全更新一遍,然后 je/jb/jl 去读这些标志。
MIPS 不做这件事,原因和上一篇的"只有一种寻址方式"是同一条哲学:
标志位是"全局隐式状态"——它让两条本来无关的指令产生了依赖。 流水线里,后一条指令必须等前一条把标志位写完才能读;这条依赖在指令里根本看不出来,硬件却必须为它安排转发和前递(L2 流水线会讲"数据冒险")。
MIPS 的选择是:比较结果写进普通寄存器(
slt产生 0/1),依赖就变成显式的、编译器可见的、硬件可调度的。
代价是:一条"小于就跳"要写两条指令(slt + bne)。这是 RISC"把复杂度交给编译器"的又一笔账。
本篇在主线上的位置:L1 造出了 ALU(能加、能逻辑运算、能移位),L2 会把它接到数据通路上。但"ALU 到底要做哪些运算、有符号/无符号怎么区分、除法的余数归谁"——这些是 ISA 层面的约定,是 L3 要定下来的。 换句话说:本篇回答"L1 那些电路,被要求提供哪些运算能力"。
原理
一、算术运算:add 与 addu 到底差在哪
先看指令清单:
| 指令 | 格式 | 语义 |
|---|---|---|
add rd,rs,rt | R | rd = rs + rt,溢出抛异常 |
addu rd,rs,rt | R | rd = rs + rt,溢出回绕,不抛异常 |
addi rt,rs,imm | I | rt = rs + sign_extend(imm),溢出抛异常 |
addiu rt,rs,imm | I | 同上,溢出回绕 |
sub rd,rs,rt | R | rd = rs − rt,溢出抛异常 |
subu rd,rs,rt | R | 同上,溢出回绕 |
关键事实:add 和 addu 算出的 32 位结果一模一样。差别只在**"结果超出 32 位补码范围时怎么办"**:
| 场景 | add | addu |
|---|---|---|
0x7FFFFFFF + 1 | 抛 Overflow 异常,不写回 | 写回 0x80000000(读作 −2147483648) |
0xFFFFFFFF + 1 | 正常,写回 0x00000000 | 同左 |
1 + 2 | 正常 | 正常 |
注意第二行:0xFFFFFFFF + 1 不是"有符号溢出"!
- 若把
0xFFFFFFFF当无符号读,它是4294967295,加 1 确实越界 → 无符号溢出(进位); - 若把它当有符号读,它是
−1,加 1 得0→ 完全正常。
而 add/addu 的溢出检测只看"有符号",不看"无符号进位"——因为MIPS 没有进位标志位(也就没有地方放"无符号溢出"这个信息)。
对照 L1 的
arch/04-alu.md:ALU 里算出了C_n(最高位进位)和OF = C_n ⊕ C_{n-1}。add的溢出异常就是拿OF当触发信号;而C_n(无符号进位)在 MIPS 里被丢掉了——因为 ISA 不给它留寄存器。
那"无符号加法越界"要怎么检测? 只能手工判断:
asm
addu $t0, $s0, $s1 # 先加
sltu $t1, $t0, $s0 # 结果 < 加数 → 说明进位了sltu 在这里的关键作用:用无符号比较来看"和是否比加数小"——这才是"无符号溢出"的正统检测法(也是 C 里做"安全加法"的常见写法)。
为什么需要两种? 因为两种语义都有用户:
| 场景 | 想要 | 用什么 |
|---|---|---|
C 的 int 溢出(C 里是 UB,编译器可以假设不溢出) | 检测到就报错/走别的路 | add |
| 地址计算、哈希、CRC、校验和 | 一定要回绕(数学上是模 | addu |
| 编译器生成的普通代码 | 大多不在乎 | addu(因为不会突然抛异常,更安全) |
这也是为什么你在编译器生成的汇编里看到的多半是 addu——它不会因为"意外溢出"而让程序崩掉。
二、逻辑运算与 nor 的妙用
逻辑指令一共 7 条:
| 指令 | 格式 | 立即数扩展 | 说明 |
|---|---|---|---|
and / andi | R / I | 零扩展 | 按位与,常用于取位(掩码) |
or / ori | R / I | 零扩展 | 按位或,常用于置位 |
xor / xori | R / I | 零扩展 | 按位异或,常用于翻转/比较 |
nor | R(只有寄存器版) | — | 按位或非,MIPS 特意提供 |
三件事值得说:
① 逻辑运算的立即数一律零扩展(呼应上一篇"算术带符号,逻辑不带")。因为**"取低 8 位"这种操作里根本没有"负数"的概念**。
② nor 为什么单独存在? 两个理由,一个硬件、一个软件:
- 硬件理由:
nor是 CMOS 里"最自然"的门——L1 的circuit/04-cmos.md讲过,CMOS 的或非(NOR)和与非(NAND)比"与/或"更省晶体管。所以让nor当"一等公民",正好对上硬件的实现。 - 软件理由:
nor x, $zero就是按位取反(因为 ,再取反 = )。一条指令顶一个 NOT。
asm
nor $t0, $t1, $zero # $t0 = ~$t1 ← 没有专门的 not 指令,伪指令 not 就展开成这个③ xor 的两个经典用法(408 里偶尔考):
| 用法 | 原理 |
|---|---|
翻转指定位(x ^= mask) | 与 1 异或翻转,与 0 异或保持 |
判断两数相等(x ^ y == 0) | 异或为 0 ⟺ 完全相同 |
不用临时变量交换(a^=b; b^=a; a^=b) | 位级技巧,现代编译器不一定采用(反而更慢) |
三、移位:sll / srl / sra 的语义差别
MIPS 有两条"移位路线":
| 形式 | 指令 | 移位量来源 |
|---|---|---|
| 立即数移位 | sll srl sra | 指令里的 shamt(5 位,0~31) |
| 寄存器移位 | sllv srlv srav | 一个寄存器里的值(可 0~31,低位有效) |
为什么移位量只有 5 位? 因为32 位数最多移 31 位——5 位正好(
三条指令的语义:
| 指令 | 名称 | 空出的高位/低位填什么 | 数学含义 |
|---|---|---|---|
sll | 逻辑左移 | 低位填 0 | × |
srl | 逻辑右移 | 高位填 0 | 无符号 ÷ |
sra | 算术右移 | 高位填符号位 | 有符号 ÷ |
注意:没有"算术左移"。 因为左移不区分符号——补 0 就对了,所以 sll 一条就够(sla 不存在)。
srl 与 sra 的差别只在"负数"上——而这个差别很要命:
| 输入(32 位位型) | srl 2(逻辑) | sra 2(算术) |
|---|---|---|
0x00000006 (=6) | 0x00000001 | 0x00000001 |
0xFFFFFFFA (=−6) | 0x3FFFFFFE | 0xFFFFFFFE (=−2) |
0xFFFFFFFF (=−1) | 0x3FFFFFFF | 0xFFFFFFFF (=−1) |
看出来了吗:对 −6,srl 把它当成 4294967290 去除以 4;sra 把它当成 −6 去除以 4。 差的不是一点。
最反直觉的一条:sra 是"向下取整"(向负无穷),不是"向零取整":
| 表达式 | sra 1 的结果 | C 的 /2 |
|---|---|---|
−1 >> 1 | −1 | 0 |
−7 >> 1 | −4 | −3 |
−8 >> 1 | −4 | −4 |
为什么 −7 >> 1 = −4? 因为 −7 的补码是 0xFFFFFFF9,算术右移 1 位填符号位 1,得到 0xFFFFFFFC = −4。而 C 的 −7 / 2 = −3(向零截断)。
这是编译器优化里的一个经典结论: 编译器不能把
x / 2直接优化成x >> 1——除非它先证明x ≥ 0。 对负数,x/2要写成(x + (x>>31 & 1)) >> 1(先加一个"符号修正量")才等价。 这一条在 L2 的arch/03-float.md(舍入方向)和soft的编译优化里都会再出现。
四、乘除法:HI / LO 与结果的归属
MIPS 把乘除结果放在一对专用寄存器 HI(高 32 位)和 LO(低 32 位)里:
| 指令 | 功能 | 结果放哪 |
|---|---|---|
mult rs,rt | 有符号乘法 | HI:LO = rs × rt(64 位) |
multu rs,rt | 无符号乘法 | 同上(值不同!) |
div rs,rt | 有符号除法 | LO = 商,HI = 余数 |
divu rs,rt | 无符号除法 | 同上 |
mfhi rd | 读 HI | rd = HI |
mflo rd | 读 LO | rd = LO |
mthi rs / mtlo rs | 写 HI / LO | 用于恢复现场 |
为什么乘法的两个操作数在 rs/rt,而 rd 填 0? 因为结果不在 rd 里,而在 HI/LO。这也解释了一条常见困惑:"为什么 mult 的机器码里 rd 字段是 0?"——因为它根本不用那个字段。
为什么乘法结果要 64 位? 因为 32 位 × 32 位 = 最多 64 位。"高 32 位和低 32 位分开放",比"占用两个通用寄存器"更省编号,也保住了"寄存器编号 0~31 的规整性"。
mult 与 multu 的结果真的不同,举例:
| 运算 | 有符号解读 | 无符号解读 |
|---|---|---|
0xFFFFFFFF × 2 | (−1) × 2 = −2 → HI:LO = 0xFFFFFFFFFFFFFFFE | 4294967295 × 2 = 8589934590 → HI:LO = 0x00000001FFFFFFFE |
这就是"两个结果真的不一样"的具体样子——不是"异常与否"的差别,是两个完全不同的数。
除法:商与余数的符号规则——MIPS 与 C 一致:
a | b | a div b(商) | a mod b(余) | 校验 |
|---|---|---|---|---|
| 7 | 2 | 3 | 1 | 7 = 3×2 + 1 |
| −7 | 2 | −3 | −1 | −7 = (−3)×2 + (−1) |
| 7 | −2 | −3 | 1 | 7 = (−3)×(−2) + 1 |
| −7 | −2 | 3 | −1 | −7 = 3×(−2) + (−1) |
注意 −7/2 = −3 而不是 −4(向零截断);余数 −1 而不是 +1(符号跟被除数)。这正是 C99 之后的 % 语义。(C89 里 % 的符号是"实现定义"的,C99 才定死为"跟被除数"。)
几个"不产生异常"的事实(MIPS 的取舍,与 x86 不同):
| 情况 | MIPS | x86 |
|---|---|---|
div 除以 0 | 不抛异常,结果未定义(HI/LO 内容不定) | 抛 #DE 异常 |
0x80000000 / −1(商 +2³¹ 装不下) | 不抛异常,结果未定义 | 抛 #DE 异常 |
| 乘法结果超出 32 位 | 不会(结果本就是 64 位) | 看情况 |
MIPS32 手册的原话是"DIV 在任何情况下都不产生算术异常"——要不要检查这两个边界,是软件的事(编译器会在需要时插入显式检查)。
伪指令的展开(呼应上一篇的"汇编器帮你写的简写"):
| 伪指令 | 展开 | 条数 |
|---|---|---|
mul $t0,$t1,$t2 | mult $t1,$t2 + mflo $t0 | 2 条 |
div $t0,$t1,$t2 | div $t1,$t2 + mflo $t0 | 2 条 |
rem $t0,$t1,$t2 | div $t1,$t2 + mfhi $t0 | 2 条 |
abs $t0,$t1 | bgez + sub(分支!) | 3 条左右 |
注意 mul 只取低 32 位——这意味着"两个大数相乘"在伪指令 mul 下会静默丢高位。要做"检测溢出"的乘法,必须显式用 mult + mfhi 检查高 32 位。
五、比较与分支:slt 家族与"没有 blt"
比较指令只有 4 条,全部产生 0/1:
| 指令 | 语义 | 立即数扩展 |
|---|---|---|
slt rd,rs,rt | rd = (rs < rt) ? 1 : 0(有符号) | — |
sltu rd,rs,rt | 同上(无符号比较) | — |
slti rt,rs,imm | rt = (rs < sign_extend(imm)) ? 1 : 0 | 符号扩展 |
sltiu rt,rs,imm | 无符号版 | 符号扩展 |
slt 与 sltu 的差别(用具体数看最清楚):
| 比较 | slt(有符号) | sltu(无符号) |
|---|---|---|
−1 < 1? | 1(真) | 0(假),因为 0xFFFFFFFF > 1 |
−1 < −2? | 0(假) | 1(真),因为 0xFFFFFFFF > 0xFFFFFFFE |
"−1 比 1 小"和"0xFFFFFFFF 比 1 大"——同一个位型,两个相反的答案。 这就是有符号与无符号分野最赤裸的样子。
然后是那个著名的"缺失指令":
只有 6 条分支:
| 指令 | 含义 | 备注 |
|---|---|---|
beq rs,rt,L | rs == rt 就跳 | 唯一"两寄存器比较"的分支之一 |
bne rs,rt,L | rs != rt 就跳 | 同上 |
blez rs,L | rs ≤ 0 就跳 | 与 $zero 比较 |
bgtz rs,L | rs > 0 就跳 | 与 $zero 比较 |
bltz rs,L | rs < 0 就跳 | 与 $zero 比较 |
bgez rs,L | rs ≥ 0 就跳 | 与 $zero 比较 |
要做"小于就跳",用 slt + bne 展开:
asm
blt $s0,$s1,L # 伪指令
# 展开为:
slt $at,$s0,$s1 # $at = (s0 < s1)
bne $at,$zero,L # $at != 0 就跳注意 $at 被占用——这正是 $at 叫"汇编器保留"的原因(上一篇已提过)。
为什么 MIPS 不直接做 blt? 三个理由叠起来:
- 比较-跳转合并成一条指令 → 控通路变复杂(要在分支单元里塞进一个比较器 + 选择器);
- 流水线里"算条件"和"算目标地址"会打架(比较要等 ALU 出结果,而目标地址要提前算好)——分开写,硬件可以并行算目标地址,不必等比较结果;
slt的结果是普通寄存器值,可以被转发、被多次复用、被组合(比如a < b && c < d)。合成分支指令做不到这种"结果可复用"。
这又是一笔"把复杂度交给编译器"的账:MIPS 用"少指令 + 程序员/编译器组合"换"硬件简单 + 流水线友好"。
一个实践推论:bgt $a,$b,L 展开时要交换操作数:
asm
bgt $s0,$s1,L → slt $at,$s1,$s0 ; bne $at,$zero,L
↑ 注意交换了!因为 (a > b) == (b < a)这是手写汇编里最容易写错的一处。
六、if / while / for 的汇编模板
控制流的三种基本结构,翻成汇编有固定套路:
if / if-else
c
if (a < b) x = 1;
else x = 2;asm
slt $t0, $s0, $s1 # t0 = (a < b)
beq $t0, $zero, else # 条件不成立 → 跳 else
addiu $t1, $zero, 1 # x = 1
j end # 跳过 else
else: addiu $t1, $zero, 2 # x = 2
end:要点:"条件取反 + 跳到 else" 是最常见的写法(因为 beq 判断的是"相等",用 beq $t0,$zero 就是"条件为假")。
while
c
while (i < n) i++;asm
loop: slt $t0, $s2, $s3 # i < n ?
beq $t0, $zero, end # 不成立 → 退出
addiu $s2, $s2, 1 # i++
j loop # 回跳
end:注意"循环条件放在开头"——这是先判断后执行,与 C 的 while 语义一致。
for
c
for (i = 0; i < n; i++) sum += a[i];asm
addiu $t0, $zero, 0 # i = 0
loop: slt $t1, $t0, $s3 # i < n ?
beq $t1, $zero, end
sll $t2, $t0, 2 # i * 4
addu $t2, $s0, $t2 # &a[i]
lw $t3, 0($t2) # a[i]
addu $t4, $t4, $t3 # sum += a[i]
addiu $t0, $t0, 1 # i++
j loop
end:这就是"高级语言的控制流在汇编里长什么样"的完整答案。没有魔法:只有"比较 → 条件跳转 → 跳回"三件套。
示例
例 1:add 与 addu —— 溢出到底怎么算
任务:对若干加法,同时给出 addu 的结果和"是否发生有符号溢出"的判断。
溢出判据(L1 的 ALU 里就是靠 OF 这位):
python
M32 = 0xFFFFFFFF
def s32(u):
"""把 32 位无符号位型读成有符号补码"""
return u - 0x100000000 if u & 0x80000000 else u
def addu(a, b):
"""addu:按 2^32 回绕,不检测溢出"""
return (a + b) & M32
print(f" {'表达式':24s}{'addu 结果':>12s}{'有符号读法':>14s}{' 溢出?':>8s}")
cases = [
("0x7FFFFFFF + 1", 0x7FFFFFFF, 1),
("0xFFFFFFFF + 1", 0xFFFFFFFF, 1),
("0x7FFFFFFF + 0x7FFFFFFF", 0x7FFFFFFF, 0x7FFFFFFF),
("0xFFFFFFFF + 0xFFFFFFFF", 0xFFFFFFFF, 0xFFFFFFFF),
("0x40000000 + 0x40000000", 0x40000000, 0x40000000),
("1 + 2", 1, 2),
]
for name, a, b in cases:
r = addu(a, b)
# 有符号溢出:同号相加、结果异号
ov = (not ((a ^ b) & 0x80000000)) and bool((a ^ r) & 0x80000000)
print(f" {name:24s}0x{r:08X}{s32(r):>14}{' 是' if ov else ' 否'}")
print("\n=== 无符号进位的检测(MIPS 没有 CF,只能手工判断)===")
for name, a, b in [("0xFFFFFFFF + 1", 0xFFFFFFFF, 1), ("0x7FFFFFFF + 1", 0x7FFFFFFF, 1)]:
r = addu(a, b)
carry = 1 if r < a else 0 # sltu 的判据:和 < 任一加数 → 进位
print(f" {name:22s}addu=0x{r:08X} sltu 判定进位 = {carry}"
f" (有符号溢出 = {'是' if (not ((a^b)&0x80000000)) and ((a^r)&0x80000000) else '否'})")
print("\n 0xFFFFFFFF+1:无符号进位=1,但有符号不溢出(-1+1=0)——两个概念无关!")预期输出:
表达式 addu 结果 有符号读法 溢出?
0x7FFFFFFF + 1 0x80000000 -2147483648 是
0xFFFFFFFF + 1 0x00000000 0 否
0x7FFFFFFF + 0x7FFFFFFF 0xFFFFFFFE -2 是
0xFFFFFFFF + 0xFFFFFFFF 0xFFFFFFFE -2 否
0x40000000 + 0x40000000 0x80000000 -2147483648 是
1 + 2 0x00000003 3 否
=== 无符号进位的检测(MIPS 没有 CF,只能手工判断)===
0xFFFFFFFF + 1 addu=0x00000000 sltu 判定进位 = 1 (有符号溢出 = 否)
0x7FFFFFFF + 1 addu=0x80000000 sltu 判定进位 = 0 (有符号溢出 = 是)
0xFFFFFFFF+1:无符号进位=1,但有符号不溢出(-1+1=0)——两个概念无关!三条结论:
| 结论 | 说明 |
|---|---|
| "无符号进位"与"有符号溢出"是两个完全无关的概念 | 0xFFFFFFFF + 1 进位了但不溢出;0x7FFFFFFF + 1 溢出但没进位 |
add 只管"有符号溢出" | 因为 MIPS 没有 CF,C_n 这个信号在 ISA 层面被丢弃了 |
sltu 才是"无符号进位"的检测工具 | "和 < 任一加数" ⟺ 发生进位——这是"安全整数加法"的标准写法 |
例 2:三种移位对同一个数的结果
任务:把 sll / srl / sra 的结果排成表,验证"算术右移向下取整"这条反直觉的结论。
python
M32 = 0xFFFFFFFF
def show(x):
u = x & M32
sll = (u << 2) & M32 # 逻辑左移:低位补 0
srl = (u >> 2) & M32 # 逻辑右移:高位补 0
sra = (x >> 2) & M32 # 算术右移:高位补符号位
return u, sll, srl, sra
print(f"{'x':>12s}{'bits':>12s}{'sll2':>12s}{'srl2':>12s}{'sra2':>12s}")
for x in (6, -6, 0x7FFFFFFF, -1, -2147483648):
u, sll, srl, sra = show(x)
print(f"{x:>12d}" + f"0x{u:08X}".rjust(12) + f"0x{sll:08X}".rjust(12)
+ f"0x{srl:08X}".rjust(12) + f"0x{sra:08X}".rjust(12))
print("\n=== 反直觉点:sra 是'向下取整',C 的 / 是'向零取整' ===")
print(f" {'x':>6}{'x >> 1 (算术)':>16}{'C 的 x/2':>12}{'一致?':>10}")
for x in (-1, -7, -8, 6):
print(f" {x:>6}{x >> 1:>16}{int(x / 2):>12}{'是' if (x >> 1) == int(x / 2) else '否':>10}")
print("\n → 负数上 '>>' 与 '/' 不等价;编译器要先把 x/2 改成 (x + ((x>>31)&1)) >> 1")预期输出:
x bits sll2 srl2 sra2
6 0x00000006 0x00000018 0x00000001 0x00000001
-6 0xFFFFFFFA 0xFFFFFFE8 0x3FFFFFFE 0xFFFFFFFE
2147483647 0x7FFFFFFF 0xFFFFFFFC 0x1FFFFFFF 0x1FFFFFFF
-1 0xFFFFFFFF 0xFFFFFFFC 0x3FFFFFFF 0xFFFFFFFF
-2147483648 0x80000000 0x00000000 0x20000000 0xE0000000
=== 反直觉点:sra 是'向下取整',C 的 / 是'向零取整' ===
x x >> 1 (算术) C 的 x/2 一致?
-1 -1 0 否
-7 -4 -3 否
-8 -4 -4 是
6 3 3 是
→ 负数上 '>>' 与 '/' 不等价;编译器要先把 x/2 改成 (x + ((x>>31)&1)) >> 1三条结论:
| 结论 | 说明 |
|---|---|
srl 对负数会"变成大正数" | −6 的 srl 2 是 0x3FFFFFFE(约 10 亿)——因为逻辑右移把它当无符号数了 |
sra 对负数保持符号 | −6 的 sra 2 = 0xFFFFFFFE = −2,"看起来对" |
sra 与 C 的 / 在负数上不等价 | −1>>1 = −1 而 −1/2 = 0;−7>>1 = −4 而 −7/2 = −3 ← 编译器不能用移位代替除法去优化负数 |
例 3:除法的商与余数 —— 与 C 逐项对齐
任务:验证"MIPS 的 div 与 C 的 /、% 在所有符号组合下都一致"。
python
print(f" {'a':>5}{'b':>5}{'商':>6}{'余':>6} 校验 C 的 a/b C 的 a%b")
def mips_div(a, b):
"""MIPS 的 div:商向零截断,余数符号与被除数相同(与 C99 一致)"""
q = abs(a) // abs(b)
if (a < 0) != (b < 0):
q = -q
return q, a - q * b
for a, b in [(7, 2), (-7, 2), (7, -2), (-7, -2), (7, 3), (-7, 3)]:
q, r = mips_div(a, b)
cq = int(a / b) # C99 的 / :向零取整
cr = a - cq * b # C99 的 % :符号跟被除数
ok = (q == cq) and (r == cr)
print(f" {a:>5}{b:>5}{q:>6}{r:>6} {a} = {q}*{b} + {r} {cq:>6}{cr:>8}"
f"{'' if ok else ' !!不一致'}")
print("\n=== 与 Python 的差异(Python 用'向下取整')===")
for a, b in [(-7, 2), (7, -2)]:
q, r = mips_div(a, b)
print(f" {a} div {b}: MIPS/C 商={q} 余={r} | Python //={a//b} %={a%b}")
print("\n → 把 C 的整除算法搬到 Python 上,负数会错一个——这是移植 bug 的高发区")
print("\n=== 边界:不产生异常(与 x86 不同)===")
print(" 除以 0 : MIPS 结果未定义、不抛异常 | x86 idiv 抛 #DE")
print(" 0x80000000 / -1: MIPS 结果未定义、不抛异常 | x86 idiv 抛 #DE")
print(" → 这两个边界的检查是软件(编译器)的责任,不是硬件的")预期输出:
a b 商 余 校验 C 的 a/b C 的 a%b
7 2 3 1 7 = 3*2 + 1 3 1
-7 2 -3 -1 -7 = -3*2 + -1 -3 -1
7 -2 -3 1 7 = -3*-2 + 1 -3 1
-7 -2 3 -1 -7 = 3*-2 + -1 3 -1
7 3 2 1 7 = 2*3 + 1 2 1
-7 3 -2 -1 -7 = -2*3 + -1 -2 -1
=== 与 Python 的差异(Python 用'向下取整')===
-7 div 2: MIPS/C 商=-3 余=-1 | Python //=-4 %=1
7 div -2: MIPS/C 商=-3 余=1 | Python //=-4 %=-1
→ 把 C 的整除算法搬到 Python 上,负数会错一个——这是移植 bug 的高发区
=== 边界:不产生异常(与 x86 不同)===
除以 0 : MIPS 结果未定义、不抛异常 | x86 idiv 抛 #DE
0x80000000 / -1: MIPS 结果未定义、不抛异常 | x86 idiv 抛 #DE
→ 这两个边界的检查是软件(编译器)的责任,不是硬件的三条结论:
| 结论 | 说明 |
|---|---|
MIPS 的 div 与 C99 的 /、% 完全一致 | 商向零取整,余数符号跟被除数 |
Python 的 // 和 % 是"向下取整" | −7 // 2 = −4(不是 −3)——不要用 Python 模拟 C 的整除 |
除零和 0x80000000 / −1 在 MIPS 上"不抛异常" | 结果未定义——这是 RISC 的取舍,检查责任在软件 |
例 4:for 循环的汇编模板与指令账
任务:把三种控制结构翻成汇编,数清"真指令"条数(标号不占指令)。
python
TEMPLATES = {
"if (a < b) x = 1;": [
"slt $t0,$s0,$s1", # a < b ?
"beq $t0,$zero,skip", # 假 → 跳过
"addiu $t1,$zero,1", # x = 1
"skip:",
],
"while (i < n) i++;": [
"loop:",
"slt $t0,$s2,$s3", # i < n ?
"beq $t0,$zero,end",
"addiu $s2,$s2,1", # i++
"j loop",
"end:",
],
"for (i=0;i<n;i++) sum+=a[i];": [
"addiu $t0,$zero,0", # i = 0
"loop:",
"slt $t1,$t0,$s3", # i < n ?
"beq $t1,$zero,end",
"sll $t2,$t0,2", # i*4
"addu $t2,$s0,$t2", # &a[i]
"lw $t3,0($t2)", # a[i]
"addu $t4,$t4,$t3", # sum += a[i]
"addiu $t0,$t0,1", # i++
"j loop",
"end:",
],
}
for src, lines in TEMPLATES.items():
real = [l for l in lines if not l.endswith(":")]
print(f"\nC: {src}")
for l in lines:
print(f" {l}")
print(f" → 真指令 {len(real)} 条({len(lines)-len(real)} 个标号不占指令)")
print("\n=== '小于就跳'的展开成本 ===")
print(" blt $s0,$s1,L == slt $at,$s0,$s1 ; bne $at,$zero,L")
print(" → 一条伪指令 = 2 条真指令,还要占用汇编器保留的 $at")
print(" 而 MIPS 原生提供与 $zero 比较的四条分支,无需展开:")
for ins, mean in [("blez", "≤ 0"), ("bgtz", "> 0"), ("bltz", "< 0"), ("bgez", "≥ 0")]:
print(f" {ins} $s0, L # $s0 {mean} 就跳")
print("\n → 所以 'i < 0' 写 bltz 只要 1 条;'a < b' 要 2 条 + $at")
print(" 编译器会尽量把比较写成'与 0 比'的形式来省指令")预期输出:
C: if (a < b) x = 1;
slt $t0,$s0,$s1
beq $t0,$zero,skip
addiu $t1,$zero,1
skip:
→ 真指令 3 条(1 个标号不占指令)
C: while (i < n) i++;
loop:
slt $t0,$s2,$s3
beq $t0,$zero,end
addiu $s2,$s2,1
j loop
end:
→ 真指令 4 条(2 个标号不占指令)
C: for (i=0;i<n;i++) sum+=a[i];
addiu $t0,$zero,0
loop:
slt $t1,$t0,$s3
beq $t1,$zero,end
sll $t2,$t0,2
addu $t2,$s0,$t2
lw $t3,0($t2)
addu $t4,$t4,$t3
addiu $t0,$t0,1
j loop
end:
→ 真指令 9 条(2 个标号不占指令)
=== '小于就跳'的展开成本 ===
blt $s0,$s1,L == slt $at,$s0,$s1 ; bne $at,$zero,L
→ 一条伪指令 = 2 条真指令,还要占用汇编器保留的 $at
而 MIPS 原生提供与 $zero 比较的四条分支,无需展开:
blez $s0, L # $s0 ≤ 0 就跳
bgtz $s0, L # $s0 > 0 就跳
bltz $s0, L # $s0 < 0 就跳
bgez $s0, L # $s0 ≥ 0 就跳
→ 所以 'i < 0' 写 bltz 只要 1 条;'a < b' 要 2 条 + $at
编译器会尽量把比较写成'与 0 比'的形式来省指令三条结论:
| 结论 | 说明 |
|---|---|
| 控制流 = "比较 + 条件跳转 + 回跳"三件套 | 没有别的魔法;if 3 条、while 4 条、for 9 条 |
| 标号不占指令、不占内存 | 汇编器把它们变成"偏移量"(上一篇的 PC+4+imm×4) |
| "与 0 比较"的分支是免费的 | bltz/bgtz/blez/bgez 原生 1 条;"两数比较"要 2 条 + $at → 编译器刻意制造"与 0 比"的机会 |
考点
考点
1. add 与 addu(高频陷阱)
- 结果位型完全相同,差别只在溢出时是否抛
Overflow异常; addiu的u是"不检测溢出",不是"无符号"——它照样符号扩展;add只检测"有符号溢出"(同号相加得异号);MIPS 没有 CF,不管无符号进位;- 无符号进位要手工判:
sltu $t, 结果, 加数; 0xFFFFFFFF + 1:无符号进位 = 1,但有符号不溢出(−1+1=0)——两个概念无关。
2. 逻辑指令
and/or/xor/nor+andi/ori/xori;立即数一律零扩展;nor x,$zero= 按位取反(没有not指令,伪指令not就展开成这个);nor之所以是"一等公民",因为 CMOS 里 NOR/NAND 最省晶体管(L1 对应)。
3. 移位(必考)
| 指令 | 补什么 | 数学含义 |
|---|---|---|
sll | 低位填 0 | ×2ᵏ(高位丢弃) |
srl | 高位填 0 | 无符号 ÷2ᵏ |
sra | 高位填符号位 | 有符号 ÷2ᵏ,向下取整 |
- 没有算术左移(左移不分符号);移位量只有 5 位 → 0~31;
−7 >> 1 = −4而−7 / 2 = −3→ 编译器不能用移位代替负数除法。
4. 乘除与 HI/LO
- 结果在
HI:LO,不在rd:mult放 64 位积;div把商放LO、余放HI; - 读用
mfhi/mflo;mult与multu结果真的不同(符号扩展方式不同); div的商向零取整、余数符号跟被除数——与 C99 的/、%完全一致;div不产生任何异常:除 0 和0x80000000 / −1结果都是未定义(x86 会抛#DE);- 伪指令
mul/div/rem都展开成 2 条;mul只取低 32 位,会静默丢高位。
5. 比较与分支
| 指令 | 语义 |
|---|---|
slt / sltu | 有符号 / 无符号 比较,产生 0/1 |
slti / sltiu | 与符号扩展的立即数比较 |
- 同一组位
0xFFFFFFFF与1:slt说"−1 < 1"(真),sltu说"4294967295 < 1"(假); - MIPS 没有
blt/bgt:用slt+bne展开(2 条 + 占用$at); bgt $a,$b,L展开时要交换操作数(因为a>b⟺b<a);- 原生只有 6 条分支:
beq/bne(两寄存器)+blez/bgtz/bltz/bgez(与$zero比); - 不设
blt的理由:控通路简单、目标地址可与比较并行算、slt的结果可复用。
6. 控制流模板(写代码/读反汇编都用得上)
if-else:slt+beq ... else+ ... +j end+else:+end:(3 条真指令/分支);while:loop:+ 条件 +beq ... end+ 循环体 +j loop+end:(4 条);for:初始化 +loop:+ 条件 + 循环体 + 步进 +j loop+end:(9 条);- 一律"条件取反、失败就跳"——标号不占指令,最终变成
PC相对偏移。
小结
- "有符号/无符号"在这篇里分成两类差别:加减只是在"要不要抛溢出异常"上不同(结果位型一样);乘、除、比较、右移是结果真的不同(对操作数的解读不同)。
add只认"有符号溢出",MIPS 没有 CF;无符号进位要手工用sltu判("和 < 加数")。nor x,$zero就是取反;nor是"一等公民"是因为 CMOS 里 NOR 最省管。- 三种移位:
sll填 0 左移(×2ᵏ)、srl填 0 右移(无符号 ÷2ᵏ)、sra填符号位右移(有符号 ÷2ᵏ,且向下取整)。−7>>1 = −4≠−7/2 = −3。 - 乘除结果在
HI:LO;div的商向零取整、余数跟被除数,与 C99 一致;div任何情况下都不抛异常(除 0、INT_MIN/−1都不抛)。 slt/sltu产出 0/1;MIPS 故意不做blt/bgt——用slt+bne(2 条,占$at),换来控通路简单 + 目标地址可并行算 + 比较结果可复用。- 控制流只有"比较 → 条件跳转 → 回跳"三件套:
if3 条、while4 条、for9 条。
回到主线:到这里,我们已经能写"顺序 + 分支 + 循环"的程序了——但还写不出"函数"。
jal跳过去以后,怎么回来?$ra被覆盖了怎么办?参数怎么传?返回值放哪?局部变量放哪?为什么递归一定要用栈?一个函数调用在内存里长什么样(栈帧)?$t和$s的"调用者保存/被调用者保存"到底是谁的责任?
这就是下一篇:函数调用约定与栈帧。
下一篇:函数调用约定与栈帧
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。