进阶 #assembly

寻址方式

寻址方式是指令中指定操作数位置的方法,决定了CPU如何找到要处理的数据

数据的”藏身之处”

执行 A = B + 1 这条简单运算时,CPU 需要知道三件事:

  1. 做加法(由操作码决定——ADD)
  2. 加数 1(在哪里?——是直接写在指令里?还是在寄存器中?)
  3. 结果放哪(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|数据传送指令]],它们负责在寄存器和内存之间搬运数据。