Appearance
内存寻址与数据传送指令
概念
上一篇给了一句 lw $t0, 4($sp)。这句话里有个东西一直没说清:
那个
4($sp)到底是什么?为什么不直接写一个完整的 32 位地址?
寻址方式(addressing mode) 回答的就是这个问题:
先给一个反直觉的结论:
MIPS32 只有 5 种寻址方式,而真正能访问内存的只有 1 种——
基址 + 偏移。
为什么会这么少? 因为 RISC 的取舍是 "把复杂留给编译器,把简单留给硬件"。x86 有九种以上的寻址方式(寄存器间接、基址变址、比例变址……),每多一种,译码器和地址生成单元就要多一组多路选择。MIPS 的选择是:
于是"数组下标""结构体字段""指针解引用"全变成同一件事:算术 + 一个偏移。这也正是 L1 里那个"ALU + 寄存器堆"最擅长的事。
| MIPS32 | x86-64 | |
|---|---|---|
| 寻址方式数 | 5 种 | 9 种以上 |
| 访存指令 | 只有 lw/sw 家族 | 几乎所有指令都能带内存操作数 |
| 地址计算 | 显式写成算术指令 | 地址生成单元(AGU)一条指令内完成 |
| 好处 | 译码简单、流水线规整 | 指令短、程序员方便 |
MIPS32 的全部五种寻址方式(先看表,后面逐个展开):
| # | 寻址方式 | 出现在 | 地址来源 |
|---|---|---|---|
| 1 | 立即数寻址 | addi $t0,$t1,100 | 指令里的 16 位 |
| 2 | 寄存器寻址 | add $t0,$t1,$t2 | 寄存器里的值 |
| 3 | 基址 + 偏移 | lw $t0,4($sp) | 寄存器 + 指令偏移(唯一访存) |
| 4 | PC 相对 | beq $t0,$zero,L | PC+4 + 偏移×4 |
| 5 | 伪直接 | j L | PC+4 高 4 位 : target×4 |
注意 4 和 5 上一篇已经算过了:它们的"地址"其实是指令地址,不是数据地址。真正"访问数据内存"的只有第 3 种。 这也解释了 MIPS 的一个硬性规定:只有
lw/sw能访存,add永远不访存——"访存在流水线的哪一级"是确定的(L2 流水线会用到这条)。
本篇在主线上的位置:L0 造出的是一块块能存住的硅(存储单元),L1 造出的是能算的电路。但"算出来的东西放哪、怎么按名字找到它",是 ISA 要回答的问题。 换句话说:本篇回答"L0/L1 那些存储单元与地址线,被要求以什么规则被访问"——并且顺手解释了 C 语言的数组、结构体在机器里到底是什么形状。
原理
一、基址 + 偏移:为什么这一个就够用
形式:lw rt, offset(base)。有效地址:
注意三个细节:
base是 32 位寄存器——所以先算好基址放寄存器,就绕开了"指令里装不下 32 位地址"的问题(呼应上一篇的lui+ori);offset是 16 位、要符号扩展——所以偏移可以是负数(访问栈里的本地变量经常用负偏移);- 相加是"32 位无符号回绕",不会溢出异常。
这一个形式怎么冒充其他寻址方式? 看这张表就明白了:
| 想要的效果 | 传统叫法 | MIPS32 怎么写 |
|---|---|---|
直接访问固定地址 0x10010000 | 直接寻址 | lui $t0,0x1001 + ori $t0,$t0,0x0000,然后 lw $t1,0($t0) |
| 访问"指针指向的东西" | 寄存器间接 | lw $t1,0($t0)(offset = 0) |
p[i] | 变址寻址 | sll+addu 算出地址,再 lw $t1,0($t2) |
s.field | 基址变址 | lw $t1,12($s0)(偏移是编译器算好的常量) |
看出来了吗:"寄存器间接"就是 offset = 0 的基址寻址;"直接寻址"就是"先用两条指令把基址装好"。五种形态,一套硬件。
二、字节序:大端与小端
内存是按字节编址的。但一个 int 是 4 个字节。于是问题来了:
0x12345678存到地址0x1000,那么0x1000这个字节里装的是0x12还是0x78?
两种答案都是"对的",取决于机器的字节序:
| 大端(Big-Endian) | 小端(Little-Endian) | |
|---|---|---|
| 规则 | 高字节存在低地址 | 低字节存在低地址 |
0x1000 | 0x12 | 0x78 |
0x1001 | 0x34 | 0x56 |
0x1002 | 0x56 | 0x34 |
0x1003 | 0x78 | 0x12 |
| 谁在用 | MIPS 传统、PowerPC、网络字节序 | x86、ARM(默认)、MARS 模拟器 |
| 记忆 | 和"人读数字"的顺序一致 | "低字节排在前头" |
一个反直觉但极重要的事实:
lw 读一个完整的字,两种字节序下结果完全一样(都是 0x12345678)——因为地址生成单元按同一种规则拼回来。只有 lb/lh 这种"只读一部分"的指令,才会读到不同的值。
为什么会这样? 因为 lw 是"整取整用":从 0x1000 取 4 字节、按本机的字节序拼成一个 32 位数——拼的规则和存的规则一样,正好抵消。而 lb 只取一个字节,就必须回答"这个字节是数的高位还是低位"——这就是字节序的分野。
网络字节序约定用大端(所以有 htonl/htons 这类函数)。跨机器传二进制数据必须显式转换,否则 lw 出来的数会整个反过来。
三、load / store 指令族
MIPS32 的访存指令一共十来条,按"宽度"和"符号"两个维度切分:
| 指令 | 宽度 | 符号处理 | 说明 |
|---|---|---|---|
lb | 1 字节 | 符号扩展 | 读有符号字节(char) |
lbu | 1 字节 | 零扩展 | 读无符号字节(unsigned char) |
lh | 2 字节 | 符号扩展 | 读有符号半字(short) |
lhu | 2 字节 | 零扩展 | 读无符号半字(unsigned short) |
lw | 4 字节 | — | 读字(int/指针) |
sb | 1 字节 | — | 写字节(只写低 8 位) |
sh | 2 字节 | — | 写半字(只写低 16 位) |
sw | 4 字节 | — | 写字 |
两条必须记住的规则:
规则一:l* 有带不带 u 之分,s* 没有。
因为读的时候要"补齐到 32 位",就必须选择符号扩展还是零扩展——这一个选择直接决定读出来的数是正是负:
| 内存里是 | lb 得到 | lbu 得到 |
|---|---|---|
0x80 | 0xFFFFFF80 = −128 | 0x00000080 = +128 |
0xFF | 0xFFFFFFFF = −1 | 0x000000FF = +255 |
而写的时候不存在"补齐"问题——sb/sh 直接取寄存器的低位字节丢掉高位。所以没有 sbu、shu。
C 语言的对应关系:
char在 MIPS 默认有符号(所以char c = 0x80会变成 −128),GCC 有-funsigned-char可以改。signed char用lb,unsigned char用lbu——这是编译器的选择,不是硬件的。
规则二:宽度必须对齐。
| 指令 | 地址必须 | 否则 |
|---|---|---|
lw/sw | 4 的倍数 | Address Error 异常 |
lh/lhu/sh | 2 的倍数 | Address Error 异常 |
lb/lbu/sb | 任意 | 正常 |
为什么硬件要强制对齐? 因为内存是按"字"整块读出来的(L2 的存储器一章会讲):
对齐访问 (0x1000): 读一次 4 字节块,直接取 → 1 次访存
非对齐访问 (0x1002):跨了两个 4 字节块
┌─── 块1 ───┐┌─── 块2 ───┐
│ 0x1000..03 ││ 0x1004..07 │
│ ▓▓▓▓│▓▓▓▓ │ ← 想要的 4 字节横跨两块
└───────────┘└───────────┘
要读两次 + 移位拼接 → 2 次访存 + 额外逻辑MIPS 的选择是"直接不允许"(硬件简单、流水线可预测);x86 允许但会自动拆成两次访存(性能差,且是"未对齐惩罚"的来源之一)。
这也解释了上一节结构体的填充:编译器宁愿浪费内存,也要让每个字段的偏移满足对齐——因为不对齐访问会触发异常(MIPS 上不是"慢",而是"错")。
四、地址算术:C 的三种数据结构怎么变成 lw
这是本篇最重要的实战内容:把 C 的写法翻译成 MIPS32 的地址算术。
一维数组:a[i]
"乘 4"在汇编里用左移 2 位(L1 的桶形移位器在这里第一次真派上用场):
asm
sll $t2, $t0, 2 # t2 = i * 4 ← 左移 2 位就是乘 4
addu $t2, $s0, $t2 # t2 = base + 偏移 = 元素地址
lw $t3, 0($t2) # 读 a[i]注意最后 lw 的偏移是 0——因为完整地址已经在 $t2 里算好了。"变址寻址"在 MIPS 上就是"先算地址,再用 0 偏移读"。
二维数组:a[i][j](行优先)
C 语言按行存放(row-major):同一行的元素是连续的。所以:
asm
# a[i][j],COLS = 10
mulu $t2, $t0, 10 # i * COLS(mulu 是伪指令 = mult + mflo)
addu $t2, $t2, $t1 # + j
sll $t2, $t2, 2 # * 4
addu $t2, $s0, $t2 # + base
lw $t3, 0($t2)这也解释了为什么"按行遍历比按列快"——按行是连续地址,按列每一步都跨一整行,Cache 命中率天差地别(L2 的 arch/12-cache.md 里用 93.75% vs 0% 算过这个账)。
结构体:偏移由编译器算成常量
c
struct Point { int x; int y; };
struct Point p;
p.y = 5;asm
lw $t0, p+4 # p.y 的偏移 = 4(编译器算好的常量)
addiu $t0, $zero, 5
sw $t0, p+4关键认知:结构体字段访问在汇编里"看起来很简单"——因为偏移量早就被编译器算成了常量。编译期做的事越多,运行期就越省。
指针呢? 下一篇(
lang/11-pointer.md)会讲透,但这里可以先给一句:指针本身就是一个 32 位整数(就是地址)。所以*p展开成lw $t1,0($t0);&a[i]展开成"算出地址、存进寄存器"。"指针"在汇编层面根本不是新东西。
五、16 位偏移的容量边界
offset 只有 16 位有符号,所以:
这意味着:
- 访问
int a[10000]的最后一个元素,偏移是39996,超过了 32767 → 一条lw够不着; - 编译器/程序员的解法:先把 base "挪"到数组中部或对应区域(
addiu/lui+ori),再用小偏移访问。
从"一次能覆盖多少元素"看这张表:
| 类型 | 元素大小 | 单条指令可覆盖的元素数(偏移 ≤ 32767) |
|---|---|---|
char | 1 | 32768 |
short | 2 | 16384 |
int / 指针 | 4 | 8192 |
double | 8 | 4096 |
结论:"基址 + 16 位偏移"看起来小气,但配合"先算基址"的常规做法,实际上够用——编译器只要在循环里每若干轮更新一次基址即可。这也是为什么你几乎从不手写这个调整:它是编译器的事。
示例
例 1:int a[5] 求和 —— 地址逐次算出来
任务:把下面这段 C 翻译成 MIPS32,并把每次迭代的地址算清楚。
c
int a[5] = {1, 2, 3, 4, 5};
int sum = 0;
for (int i = 0; i < 5; i++)
sum += a[i];汇编(约定:$s0 = a 的基址 0x10010000,$t0 = i,$t1 = sum):
asm
li $t1, 0 # sum = 0
li $t0, 0 # i = 0
loop:
sll $t2, $t0, 2 # $t2 = i * 4 ← 左移 2 位
addu $t2, $s0, $t2 # $t2 = base + 偏移 = &a[i]
lw $t3, 0($t2) # $t3 = a[i]
addu $t1, $t1, $t3 # sum += a[i]
addiu $t0, $t0, 1 # i++
slti $t4, $t0, 5 # $t4 = (i < 5)
bne $t4, $zero, loop # 成立就回到 looppython
# 逐次算出地址与和,验证"左移 2 位 = 乘 4"和最终结果
BASE = 0x10010000
a = [1, 2, 3, 4, 5]
print("=== int a[5] 的地址表 ===")
print(" i a[i] 字节偏移 i<<2 实际地址")
total = 0
for i, v in enumerate(a):
off = i * 4
print(f" {i} {v} {off:>2} {i << 2:>2} 0x{BASE + off:08X}")
total += v
print(f" sum = {total}")
print("\n=== 循环体指令账 ===")
body = [
("sll $t2, $t0, 2", "i*4 → 字节偏移"),
("addu $t2, $s0, $t2", "基址 + 偏移 = 元素地址"),
("lw $t3, 0($t2)", "读 a[i](偏移恒为 0)"),
("addu $t1, $t1, $t3", "sum += a[i]"),
("addiu $t0, $t0, 1", "i++"),
("slti $t4, $t0, 5", "i < 5 ?"),
("bne $t4, $zero, loop","成立回跳"),
]
for ins, note in body:
print(f" {ins:24s} # {note}")
print(f" 每轮 {len(body)} 条 × 5 轮 + 初始化 2 条 = {len(body)*5 + 2} 条指令")预期输出:
=== int a[5] 的地址表 ===
i a[i] 字节偏移 i<<2 实际地址
0 1 0 0 0x10010000
1 2 4 4 0x10010004
2 3 8 8 0x10010008
3 4 12 12 0x1001000C
4 5 16 16 0x10010010
sum = 15
=== 循环体指令账 ===
sll $t2, $t0, 2 # i*4 → 字节偏移
addu $t2, $s0, $t2 # 基址 + 偏移 = 元素地址
lw $t3, 0($t2) # 读 a[i](偏移恒为 0)
addu $t1, $t1, $t3 # sum += a[i]
addiu $t0, $t0, 1 # i++
slti $t4, $t0, 5 # i < 5 ?
bne $t4, $zero, loop # 成立回跳
每轮 7 条 × 5 轮 + 初始化 2 条 = 37 条指令要抓住的三点:
| 结论 | 说明 |
|---|---|
i<<2 就是 i*4 | 地址算术里"乘元素大小"永远用移位(元素大小是 2 的幂时);这是 sll 最典型的用途 |
lw 的偏移恒为 0 | 完整地址在 $t2 里 → "变址寻址"在 MIPS 上是"先算地址" |
| 循环里 7 条指令只读 1 个数 | Load/Store 架构的代价很直白——但换来的是每次访存的位置固定(流水线友好) |
例 2:字节序实测 —— 同一段内存,大小端读出来不一样
任务:把 0x12345678 放在 0x1000,分别在大端和小端机器上读 lw/lh/lbu,看结果差在哪。
python
V, BASE = 0x12345678, 0x1000
# 按两种字节序把 4 个字节铺进内存
be = [(V >> (8 * (3 - k))) & 0xFF for k in range(4)] # 高字节在低地址
le = [(V >> (8 * k)) & 0xFF for k in range(4)] # 低字节在低地址
print(f"值 = 0x{V:08X},存放地址 0x{BASE:04X}")
print("\n 地址 大端 小端")
for k in range(4):
print(f" 0x{BASE+k:04X} 0x{be[k]:02X} 0x{le[k]:02X}")
def word(mem, addr, order):
return int.from_bytes(bytes(mem[addr:addr+4]), order)
def half(mem, addr, order):
return int.from_bytes(bytes(mem[addr:addr+2]), order)
# 两种字节序下的内存镜像
mem_be, mem_le = list(be), list(le)
print("\n=== 同一段内存,不同字节序下各指令的结果 ===")
print(f" {'指令':30s}{'大端机器':>12s}{'小端机器':>12s}")
print(f" {'lw $t,0($base) (整字)':30s}0x{word(mem_be,0,'big'):08X} 0x{word(mem_le,0,'little'):08X}")
print(f" {'lh $t,0($base) (半字)':30s}0x{half(mem_be,0,'big'):08X} 0x{half(mem_le,0,'little'):08X}")
print(f" {'lbu $t,0($base) (字节)':30s}0x{mem_be[0]:08X} 0x{mem_le[0]:08X}")
print(f" {'lbu $t,3($base) (字节+3)':30s}0x{mem_be[3]:08X} 0x{mem_le[3]:08X}")
print("\n=== 符号扩展:lb 与 lbu 读同一个 0x80 字节 ===")
for b in (0x80, 0xFF, 0x7F):
lb = b if b < 0x80 else b - 0x100 # 符号扩展
lbu = b # 零扩展
print(f" 内存字节 0x{b:02X} → lb 得到 {lb:>4} lbu 得到 {lbu:>3}")预期输出:
值 = 0x12345678,存放地址 0x1000
地址 大端 小端
0x1000 0x12 0x78
0x1001 0x34 0x56
0x1002 0x56 0x34
0x1003 0x78 0x12
=== 同一段内存,不同字节序下各指令的结果 ===
指令 大端机器 小端机器
lw $t,0($base) (整字) 0x12345678 0x12345678
lh $t,0($base) (半字) 0x00001234 0x00005678
lbu $t,0($base) (字节) 0x00000012 0x00000078
lbu $t,3($base) (字节+3) 0x00000078 0x00000012
=== 符号扩展:lb 与 lbu 读同一个 0x80 字节 ===
内存字节 0x80 → lb 得到 -128 lbu 得到 128
内存字节 0xFF → lb 得到 -1 lbu 得到 255
内存字节 0x7F → lb 得到 127 lbu 得到 127三条结论:
lw两种字节序结果相同(都是0x12345678)——整字读对字节序"透明",因为存的规则和取的规则正好抵消;lh/lbu读出来不一样——字节序只在"拆开字"时暴露。大端lh得到高 16 位0x1234,小端得到低 16 位0x5678;lb和lbu对0x80给出 −128 和 +128——同一个字节,符号扩展和零扩展的差别就是这么大。"读一个字节"必须同时回答"宽度"和"符号"两个问题,这正是l*有 5 条指令的原因。
例 3:结构体布局 —— sizeof 与字段偏移是怎么来的
任务:手算三个 struct 的字段偏移和 sizeof,并给出汇编里读每个字段的指令。
python
def layout(fields, packed=False):
"""模拟 C 的对齐规则:字段按自身对齐要求对齐,结构体按最大对齐补齐。"""
off, out, maxa = 0, [], 1
for name, size, align in fields:
a = 1 if packed else align
maxa = max(maxa, a)
if off % a: # 对齐填充
off += a - off % a
out.append((name, size, a, off))
off += size
if not packed and off % maxa: # 结构体整体补齐
off += maxa - off % maxa
return out, off, maxa
CASES = {
"struct A { char c; int i; char d; }": [("c", 1, 1), ("i", 4, 4), ("d", 1, 1)],
"struct B { int i; char c; short s; }": [("i", 4, 4), ("c", 1, 1), ("s", 2, 2)],
"struct C { char a, b, c, d; }": [("a", 1, 1), ("b", 1, 1), ("c", 1, 1), ("d", 1, 1)],
}
LOAD = {1: "lbu", 2: "lh ", 4: "lw "}
for title, flds in CASES.items():
lay, total, ma = layout(flds)
raw = sum(sz for _, sz, _ in flds)
print(f"\n{title}")
print(f" {'字段':6s}{'大小':>4s}{'偏移':>5s} 汇编读法")
for name, sz, a, off in lay:
print(f" {name:6s}{sz:>4}{off:>5} {LOAD[sz]} $t,{off}($s0)")
print(f" sizeof = {total} (字段原始和 = {raw},填充 {total - raw} 字节,最大对齐 = {ma})")
lay_p, total_p, _ = layout(CASES["struct A { char c; int i; char d; }"], packed=True)
print(f"\n若加 __attribute__((packed)):struct A 的 sizeof = {total_p}")
print(" 代价:i 的偏移变成 1(不满足 4 对齐)→ lw 触发 Address Error")
print("\n=== 为什么 A 比 B 大?——顺序决定填充量 ===")
print(" A: char(1) [填3] int(4) char(1) [填3] → 1+3+4+1+3 = 12")
print(" B: int(4) char(1) [填1] short(2) → 4+1+1+2 = 8")
print(" 同样的字段(1+4+1 vs 4+1+2),只换了顺序,就从 8 涨到 12")
print(" → 写结构体时'大的放前面'能省内存")预期输出:
struct A { char c; int i; char d; }
字段 大小 偏移 汇编读法
c 1 0 lbu $t,0($s0)
i 4 4 lw $t,4($s0)
d 1 8 lbu $t,8($s0)
sizeof = 12 (字段原始和 = 6,填充 6 字节,最大对齐 = 4)
struct B { int i; char c; short s; }
字段 大小 偏移 汇编读法
i 4 0 lw $t,0($s0)
c 1 4 lbu $t,4($s0)
s 2 6 lh $t,6($s0)
sizeof = 8 (字段原始和 = 7,填充 1 字节,最大对齐 = 4)
struct C { char a, b, c, d; }
字段 大小 偏移 汇编读法
a 1 0 lbu $t,0($s0)
b 1 1 lbu $t,1($s0)
c 1 2 lbu $t,2($s0)
d 1 3 lbu $t,3($s0)
sizeof = 4 (字段原始和 = 4,填充 0 字节,最大对齐 = 1)
若加 __attribute__((packed)):struct A 的 sizeof = 6
代价:i 的偏移变成 1(不满足 4 对齐)→ lw 触发 Address Error
=== 为什么 A 比 B 大?——顺序决定填充量 ===
A: char(1) [填3] int(4) char(1) [填3] → 1+3+4+1+3 = 12
B: int(4) char(1) [填1] short(2) → 4+1+1+2 = 8
同样的字段(1+4+1 vs 4+1+2),只换了顺序,就从 8 涨到 12
→ 写结构体时'大的放前面'能省内存四条结论:
| 结论 | 出处 |
|---|---|
| 字段偏移 = "前面字段总大小按自身对齐补齐后的累计" | A 里 i 的偏移是 4 不是 1(因为要 4 对齐) |
sizeof 要向上补齐到最大对齐的倍数 | A 补齐到 12(不是 9),B 补齐到 8(不是 7) |
字段顺序影响 sizeof | A(12)> B(8),同样的数据换个顺序差 4 字节 |
packed 省内存但要付"非对齐异常"的代价 | A 从 12 降到 6,但 lw 4($s0) 变成 lw 1($s0) → 异常 |
这四条是 408 计算题的常见套路:给一个 struct,算每个字段的偏移和总体 sizeof。 只要记住"每个字段按自身大小对齐,结构体按最大字段对齐补齐",就能算对。
考点
考点
1. 五种寻址方式(必考识别)
| 方式 | 例子 | 访存? |
|---|---|---|
| 立即数 | addi $t0,$t1,100 | 否 |
| 寄存器 | add $t0,$t1,$t2 | 否 |
| 基址 + 偏移 | lw $t0,4($sp) | 是(唯一) |
| PC 相对 | beq $t0,$zero,L | 否(指令地址) |
| 伪直接 | j L | 否(指令地址) |
- "寄存器间接"=
lw $t,0($base);"直接寻址"= 先用lui+ori装基址,再 0 偏移读。 - MIPS 只有
lw/sw家族访存;add永远不访存 → "访存在流水线哪一级"是确定的。
2. 字节序(高频陷阱)
- 大端:高字节在低地址;小端:低字节在低地址。
lw结果与字节序无关(整取整拼,规则抵消);lh/lb结果与字节序有关。0x12345678在0x1000:大端 存12 34 56 78;小端 存78 56 34 12。- 网络字节序 = 大端(
htonl/htons)。
3. l* 家族的"符号"维度
lb/lh符号扩展,lbu/lhu零扩展;sb/sh/sw没有u版本(写的时候只取低位,没有"扩展"问题)。- 字节
0x80:lb→ −128;lbu→ +128。这是"读一个字节必须同时定宽度和符号"的最好例子。
4. 对齐(必考)
lw/sw要 4 字节对齐;lh要 2 字节对齐;lb无要求。- 不对齐 → MIPS 直接抛
Address Error(不是"变慢",是"错");x86 自动拆两次访存。 - 结构体的填充就是为了满足对齐:每个字段按自身大小对齐,结构体大小按最大字段对齐向上取整。
5. 地址算术(计算题主战场)
- 一维数组:
,sll $t,i,2就是 ×4。 - 二维数组(行优先):
。 - 结构体:字段偏移是编译期常量,汇编里直接用
lw $t,offset($s0)。 sizeof与偏移算例:{char c; int i; char d;}→ 偏移 0/4/8,sizeof= 12;{int i; char c; short s;}→ 偏移 0/4/6,sizeof= 8。
6. 16 位偏移的容量边界
- offset ∈ [−32768, 32767] 字节。
- 单条指令可覆盖的元素数:
char32768、short16384、int8192、double4096。 - 更大的数组要先调整基址(编译器自动做)。
小结
- MIPS32 只有 5 种寻址方式,真正访存的只有"基址 + 偏移"一种。寄存器间接、直接寻址都是它的特例(offset = 0 / 先装好基址)。
- EA = base + sign_extend(offset):基址 32 位绕开了"指令装不下地址",偏移 16 位可正可负(栈上访问靠它)。
- 字节序只在"拆字"时暴露:
lw与字节序无关,lh/lb有关。大端高字节在前,小端低字节在前;网络字节序是大端。 l*有带u的版本(选符号/零扩展),s*没有(写的时候只取低位)。lb 0x80= −128,lbu 0x80= +128。lw/lh强制对齐,不对齐直接抛Address Error——这就是结构体要填充的根本原因。- 地址算术:数组 =
base + i×size(用移位代替乘法);二维数组行优先;结构体偏移是编译期常量。 - 结构体布局规则:字段按自身对齐、结构体按最大对齐补齐;
{char;int;char}= 12 字节,{int;char;short}= 8 字节,顺序不同、大小不同。 - 16 位偏移 ∈ [−32768, 32767]:
int数组一次覆盖 8192 个元素,更大要先挪基址。
回到主线:我们已经能把数据搬来搬去了。但搬来之后要"算"——而上一篇只讲了 add。
MIPS 怎么处理"溢出"?
add和addu差在哪?乘法结果为什么放HI/LO?除法里-7/2到底得几?if/while/for怎么变成slt+bne?为什么 MIPS 没有"小于就跳"?
这就是下一篇:算术逻辑运算与分支跳转。
下一篇:算术逻辑运算与分支跳转
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。