寻址方式
寻址方式是指令中指定操作数位置的方法,决定了CPU如何找到要处理的数据
数据的”藏身之处”
执行 A = B + 1 这条简单运算时,CPU 需要知道三件事:
- 做加法(由操作码决定——ADD)
- 加数 1(在哪里?——是直接写在指令里?还是在寄存器中?)
- 结果放哪(A 在哪里?——是内存某个地址还是一个寄存器?)
加数 1 可能直接写在指令里,也可能在寄存器中,还可能在内存里——寻址方式(Addressing Mode) 就是 CPU 找到这些数据的方法。
类比:找东西的不同方式
想象你在找一本书:
- 立即寻址:书就夹在你朋友的微信消息里(数据在指令中)
- 寄存器寻址:书就在你书桌上(数据在寄存器中)
- 直接寻址:有人告诉你”书在 3 号书架第 2 层”(指令中包含内存地址)
- 寄存器间接寻址:“你去问管理员,他知道书在哪”(寄存器存放地址)
- 变址寻址:“从 3 号书架往右数 5 格”(寄存器 + 偏移量)
常见寻址方式
下面用 MOV(数据传送)指令 来演示每种寻址方式。假设有一条指令 MOV R1, <操作数>,意思是将操作数的值存入 R1。
1. 立即寻址(Immediate Addressing)
数据直接写在指令中,不需要额外访问寄存器或内存:
MOV R1, #42 ; 把数值 42 存入 R1
ADD R2, R1, #10 ; R2 = R1 + 10
┌──────────────┬──────────┐
│ 操作码 MOV │ 立即数 42 │
└──────────────┴──────────┘
指令本身
优点:极快——数据不用去别处取,指令里就有 缺点:只能表示常数,不能表示变量
#前缀是汇编语法中表示”立即数”的约定。不同汇编器可能写法不同,但含义相同。
2. 寄存器寻址(Register Addressing)
操作数就在寄存器中——CPU 直接读取寄存器,速度极快:
MOV R1, R2 ; 把 R2 的值复制到 R1
ADD R1, R2, R3 ; R1 = R2 + R3
┌──────────────┐
指令: │ MOV R1, R2 │
└──────┬───────┘
│
CPU 内部 ──────┤
┌──────┴───────┐ ┌──────────┐
│ 寄存器组 │───→│ R2 = 42 │
│ R0, R1, R2… │ └──────────┘
└──────────────┘
优点:非常快(寄存器在 CPU 内部,访问延迟约 0.31 纳秒)
缺点:寄存器的数量有限(通常 1632 个)
3. 直接寻址(Direct / Absolute Addressing)
指令中直接给出内存地址:
LOAD R1, [1000] ; 从内存地址 1000 读取数据到 R1
STORE R1, [2000] ; 把 R1 的值存入内存地址 2000
指令: LOAD R1, [1000]
│
▼
内存: ┌──────┬──────┬──────┬──────┐
│ ... │ 42 │ ... │ ... │
└──────┴──────┴──────┴──────┘
地址1000
优点:可以访问任意固定内存位置 缺点:地址在指令中固定,无法在运行时改变——不能用来遍历数组
方括号
[ ]在汇编语法中表示”内存引用”,[1000]意思是”内存 1000 号地址里的内容”。
4. 寄存器间接寻址(Register Indirect Addressing)
寄存器中存放的是地址,CPU 先去寄存器取地址,再根据地址访问内存:
LOAD R1, [R2] ; 从 R2 所指向的内存地址读取数据到 R1
STORE R1, [R2] ; 把 R1 的值写入 R2 所指向的内存地址
┌──────────┐
R2 = │ 1000 │ ← R2 存放的是地址
└────┬─────┘
│
▼ 访问内存地址 1000
┌──────┬──────┐
│ 42 │ ... │
└──────┴──────┘
优点:地址可以在运行时计算和修改——适合遍历数组、链表等数据结构 缺点:多一步间接访问
让你想起 C 语言的指针了吗?没错,
LOAD R1, [R2]就相当于 C 语言的R1 = *R2。汇编层面的间接寻址就是 C 指针的硬件基础。
5. 变址寻址(Indexed Addressing)
寄存器 + 偏移量的组合:
LOAD R1, [R2, #8] ; 从 R2 指向的地址 + 8 字节处读取数据
LOAD R1, 8(R2) ; 另一种写法(AT&T 风格)
R2 = 1000
偏移 = 8
有效地址 = 1000 + 8 = 1008
┌──────┬──────┬──────┬──────┐
内存: │ ... │ ... │ ... │ 42 │
└──────┴──────┴──────┴──────┘
1000 1004 1008 1012
▲
└── 取这里的数据
优点:非常适合数组和结构体访问(基址 + 字段偏移) 缺点:需要加法运算计算有效地址
类比:快递柜取件
- 立即寻址:快递员直接递给你一个包裹(数据在手上)
- 寄存器寻址:包裹就在你口袋里
- 直接寻址:“去 3 号柜 7 号格取”(地址固定)
- 寄存器间接寻址:“去这个取件码对应的柜子取”(扫描取件码,地址是动态的)
- 变址寻址:“去 3 号柜,从下往上数第 5 格”(固定柜号 + 偏移量)
寻址方式对比
| 寻址方式 | 示例 | 数据来源 | 速度 | 灵活性 |
|---|---|---|---|---|
| 立即寻址 | MOV R1, #42 | 指令本身 | ⭐⭐⭐ | ❌ 只能表示常数 |
| 寄存器寻址 | MOV R1, R2 | 寄存器 | ⭐⭐⭐ | ⭐⭐ |
| 直接寻址 | LOAD R1, [1000] | 固定内存地址 | ⭐⭐ | ⭐ |
| 寄存器间接 | LOAD R1, [R2] | 变内存地址 | ⭐ | ⭐⭐⭐ |
| 变址寻址 | LOAD R1, [R2, #4] | 地址+偏移 | ⭐ | ⭐⭐⭐ |
实际运用:遍历数组
使用寄存器间接寻址来遍历数组——这就是循环访问数组的底层实现:
// C 代码:计算数组元素之和
int arr[] = {5, 10, 15, 20};
int sum = 0;
for (int i = 0; i < 4; i++) {
sum += arr[i];
}
等价的汇编实现——关键看 arr[i] 怎么变成间接寻址:
; 假设数组从地址 0x1000 开始,每个元素 4 字节
MOV R1, #0x1000 ; R1 = 数组首地址(相当于 C 中的 &arr[0])
MOV R2, #0 ; R2 = 累加和 sum
MOV R4, #4 ; R4 = 数组长度 4
LOOP:
LOAD R3, [R1] ; 用间接寻址读取当前元素 arr[i] ← 关键指令!
ADD R2, R2, R3 ; sum += arr[i]
ADD R1, R1, #4 ; 地址后移一个元素(相当于 i++)
SUB R4, R4, #1 ; 计数器减一
CMP R4, #0 ; 循环结束了吗?
JNE LOOP ; 没结束继续
LOOP: LOAD R3, [R1] ; 用间接寻址读取当前元素 ADD R2, R2, R3 ; 累加到总和 ADD R1, R1, #4 ; 地址后移一个元素(4 字节) CMP R1, #0x1010 ; 是否到了数组末尾? JNE LOOP ; 没到就继续
每一轮循环中 `LOAD R3, [R1]` 都在读不同的地址——第一次读 0x1000,第二次读 0x1004……这就是间接寻址的威力。
## 小结
寻址方式决定了 CPU 从哪里获取操作数,是速度和灵活性之间的艺术平衡:
- **立即寻址**最快但只能表示常数
- **寄存器寻址**极快但受限于寄存器数量
- **间接寻址**最灵活但速度最慢
理解寻址方式是掌握汇编编程的关键一步。接下来,你将学习最常用的一类指令——[[data-transfer-instructions|数据传送指令]],它们负责在寄存器和内存之间搬运数据。