3.4.寻址方式

世界杯梅西点球

3.4. 寻址方式 寻址方式决定了指令如何指定操作数的位置。x86_64 提供了多种寻址方式,灵活组合寄存器和内存引用。 如果说寄存器是 CPU 的"……

3.4. 寻址方式

寻址方式决定了指令如何指定操作数的位置。x86_64 提供了多种寻址方式,灵活组合寄存器和内存引用。

如果说寄存器是 CPU 的"随身工具盒",内存就是"大仓库"。不同的寻址方式好比不同的取货方式:有时直接把工具拿在手里(寄存器寻址),有时要去仓库的固定位置拿(直接内存寻址),有时得先查一个地址再去拿(寄存器间接寻址),还有时要根据索引计算位置再去拿(基址+变址寻址)。

面对这么多种寻址方式,新手最常问的问题是:我该用哪一种? 答案是:取决于你的数据在哪里、访问模式是什么。以下逐一讲解每种方式,并在末尾给出场景选择指南。

; 通用格式:mov 目标, 源

; 汇编器会解析操作数中的各种寻址模式

3.4.1. 立即数寻址

操作数直接编码在指令中,以常量形式给出:

mov rax, 42 ; rax = 42

mov rbx, 0x1000 ; rbx = 4096

mov rcx, 'A' ; rcx = 65(ASCII 'A')

add rax, 1 ; rax = rax + 1

3.4.2. 寄存器寻址

操作数位于寄存器中,指令直接操作寄存器:

mov rax, rbx ; rax = rbx

add rcx, rdx ; rcx = rcx + rdx

xor rax, rax ; rax = 0(常见清零惯用法)

inc r8 ; r8 = r8 + 1

3.4.3. 直接内存寻址

操作数位于内存中,地址通过符号标号指定:

section .data

var dq 42

section .text

mov rax, [var] ; rax = 内存中 var 处的值

mov [var], rbx ; 将 rbx 写入 var

add rax, [var] ; rax += *var

3.4.4. 寄存器间接寻址

地址存放在寄存器中(类似指针解引用):

mov rax, [rbx] ; rax = 内存中 rbx 指向的值

mov [rsi], rax ; 将 rax 写入 rsi 指向的地址

add rcx, [rdx] ; rcx += *rdx

3.4.5. 基址+变址寻址

地址由 [基址 + 变址 * 比例 + 偏移] 计算,常用于遍历数组或结构体:

; 格式: [base + index * scale + displacement]

; 其中 scale 只能是 1, 2, 4, 8

; 数组遍历

mov rax, [rbx + rcx*8] ; rax = arr[rcx](64 位数组)

mov eax, [rbx + rcx*4] ; eax = arr[rcx](32 位数组)

mov ax, [rbx + rcx*2] ; ax = arr[rcx](16 位数组)

; 结构体成员访问

mov rax, [rbx + rcx*8 + 16] ; rax = arr[rcx].field_at_offset_16

; 多个变体

mov rax, [rbx + 8] ; 基址 + 偏移

mov rax, [rbx + rcx] ; 基址 + 变址

mov rax, [rbx + rcx*4 + 32] ; 基址 + 变址*比例 + 偏移

3.4.6. RIP 相对寻址

x86_64 特有的寻址方式,地址相对于当前指令指针(rip)计算。这是 64 位模式下访问全局数据最常用的方式:

; 汇编器通常自动使用 RIP 相对寻址

section .data

msg db 'Hello', 0

section .text

mov rax, [msg] ; 汇编器自动转为 [rip + msg]

lea rsi, [rip + msg] ; 获取 msg 的地址(RIP 相对)

; 手动使用 RIP 相对

lea rsi, [rip + msg]

备注

在 x86_64 中,64 位立即数地址无法直接编码到指令中。RIP 相对寻址通过 32 位偏移量就能访问整个地址空间中的任何符号,

使指令编码更紧凑,同时也支持位置无关代码(PIC,Position-Independent Code)。

3.4.7. 各种寻址方式对比

类型

格式

示例

说明

立即数

imm

mov rax, 42

常量操作数

寄存器

reg

mov rax, rbx

寄存器操作数

直接内存

[addr]

mov rax, [var]

静态地址

寄存器间接

[reg]

mov rax, [rbx]

指针解引用

基址+偏移

[reg + disp]

mov rax, [rbx+8]

结构体字段

基址+变址

[reg + reg]

mov rax, [rbx+rcx]

数组/指针

比例变址

[reg + reg*scale]

mov rax, [rbx+rcx*8]

数组元素

比例变址+偏移

[reg + reg*scale + disp]

mov rax, [rbx+rcx*8+4]

一般情况

RIP 相对

[rip + disp]

mov rax, [rip + msg]

PIC 代码(默认)

警告

在 x86_64 中,不能 直接将两个内存操作数用于一条指令。例如:

mov [dst], [src] ; ❌ 非法——不能 mem→mem

mov rax, [src] ; ✅ 先加载到寄存器

mov [dst], rax ; ✅ 再写回内存

3.4.8. 寻址方式选择指南

选择合适的寻址方式可以让代码更简洁、更高效:

场景

推荐寻址方式

示例

理由

访问全局变量

RIP 相对

mov rax, [global_var]

自动生成 PIC 代码,编码紧凑

指针解引用

寄存器间接

mov rax, [rbx]

直接通过指针访问

遍历数组

基址+变址*比例

mov rax, [arr + rcx*8]

不修改基址指针,仅变索引

访问结构体字段

基址+偏移

mov eax, [rdi + 8]

结构体指针+字段偏移

遍历结构体数组

基址+变址*比例+偏移

mov rax, [rbx + rcx*32 + 8]

同时处理索引和字段偏移

大量常数操作

立即数

mov rax, 100

不在内存/寄存器中占用空间

指针递增遍历

寄存器间接(自增)

add rsi, 8; mov rax, [rsi]

计算地址的代价分摊到每次迭代

查表(switch/case)

基址+变址*比例

jmp [.jumptable + rax*8]

一次计算即可跳转到目标

3.4.9. LEA(加载有效地址)的特殊用途

lea``(Load Effective Address)计算内存操作数的地址并将结果存入寄存器。

虽然名字叫"加载地址",但 ``lea 并不访问内存——它只做**地址计算**:

; lea 的常见用途

; 1. 代替 add/mul 做算术(比 imul 更快)

lea rax, [rbx + rcx] ; rax = rbx + rcx(2 周期延迟)

; vs: mov rax, rbx; add rax, rcx(需 2 条指令)

lea rax, [rbx + rbx*4] ; rax = rbx * 5(1 周期延迟)

; vs: imul rax, rbx, 5(3 周期延迟)

lea rax, [rbx + rcx*8] ; rax = rbx + rcx*8

; 2. 获取标号地址(RIP 相对)

lea rsi, [rip + msg] ; rsi = msg 的地址(位置无关)

; 3. 双指针算术

lea rax, [rsi + rdi] ; rax = rsi + rdi(同时使用两个基址寄存器)

; 4. 计算数组元素的地址

lea rsi, [arr + rcx*4] ; rsi = &arr[rcx](32 位元素数组)

mov eax, [rsi] ; 读取元素

备注

lea 不设置任何标志位(不像 add 会影响 ZF/SF/CF/OF)。

它也不访问内存——尽管使用内存操作数语法。这是 lea 独特的"零副作用"算术能力:

可以在不干扰标志位的情况下做加法/乘法,这在需要同时维护循环计数和累加结果时非常有用。