想象你生活在一座叫“内存市”的城市里,每家每户都有唯一的门牌号,每个门牌号对应一个装信件的小柜子。CPU 就是这座城市的邮递员阿寻,他每天的工作就是按派送单取件、送件。有的派送单上印着写死的门牌号——“桂花路12号”,阿寻看都不用看,直接骑车过去取,这叫直接寻址;有的派送单上却写着“去 3 号口袋拿纸条,纸条上写哪就去哪”,阿寻得先翻口袋再看地址,这叫间接寻址。还有一张神奇的派送单写着“从你脚下往前数三条街”,无论阿寻被调去哪个片区,这句话都能找准目标,这便是RIP 相对寻址。这篇文章就带你走进阿寻的送件日常,把 x86 的三种寻址方式一次讲透。

岗位关联

对 AI 从业者而言,理解寻址方式能帮你读懂编译器生成的汇编、优化推理引擎里的张量遍历,甚至看懂 JIT 和共享库的加载原理——这是通往高性能系统编程的必修课。

目录
  1. 可视化演示
  2. 直接寻址原理
  3. 间接寻址原理
  4. 寄存器寻址角色
  5. 数组遍历应用
  6. RIP相对寻址
  7. 位置无关代码
  8. 性能与优化
  9. 调试与实战
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示直接寻址通过绝对地址访问内存、间接寻址通过寄存器指针解引用、RIP相对寻址实现位置无关代码,揭示x86寻址核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

直接寻址:绝对地址访问内存 绝对地址:指令中直接编码32/64位内存地址 固定地址:适用于全局变量和静态数据访问 限制:地址在编译时确定不支持动态计算 MOV EAX, [0x404000] 直接编码绝对地址访问内存数据

直接寻址原理

邮递员阿寻每天收到的大多数派送单,都印着一个写死的门牌号,比如“桂花路12号”。他不用思考、不用查地图,骑着三轮车径直抵达。在计算机里,这样的派送单就是一条访问内存的指令,门牌号就是编码在指令里的绝对地址。地址固定,目标固定,每一次执行都去同一扇门取同一个柜子。

汇编指令 mov eax, [0x1000] 中,方括号表示“访问内存单元”,0x1000 就是编译器或链接器直接塞进指令的绝对地址。C 语言里的全局变量 int g_count; 经编译链接后,符号 g_count 会被解析成具体地址,最终生成 mov eax, [g_count] 也即 mov eax, [0x...] 的机器码。这就是直接寻址:地址作为指令的一部分,写死在那里。

直接寻址的好处是心理模型简单——地址先验可知,汇编器、链接器、调试器都能直接解释;坏处是缺乏动态性,想访问哪个单元在编译期就定死了,无法根据运行状态改变目标。对于存放配置、查找表、全局状态这类“位置永远不变”的数据,直接寻址是天然之选。

对阿寻来说,这种单子是最省心的:门牌号印得清清楚楚,永远不变。但聪明的邮递员很快发现,如果派送单上的地址是“从某个纸条上看来的”,他就能服务更多变的用户——这就要说到间接寻址了。

EffectiveAddr=disp\displaystyle \text{EffectiveAddr} = disp
公式里的 disp 就是派送单上印死的门牌号,CPU 的“有效地址”等于它本身,不需要任何额外计算。
  1. 识别位移量指令中直接给出一个常量,即位移量 disp,它代表内存目标的门牌号。
    disp=0x1000\displaystyle disp = 0x1000
  2. 计算有效地址有效地址就等于这个常量,中间没有任何换算步骤。
    EffectiveAddr=disp\displaystyle \text{EffectiveAddr} = disp
  3. 完成内存访问CPU 拿着有效地址去内存市取回数据,写入目标寄存器。
    [EffectiveAddr]→Rdest\displaystyle [\text{EffectiveAddr}] \rightarrow R_{dest}

间接寻址原理

有一天,阿寻收到一张奇怪的派送单:“先打开 3 号口袋,把里面的纸条掏出来,纸条上写哪儿就去哪儿。”他低头一看,3 号口袋里真有一张纸条,上面写着“桂花路12号”。于是阿寻骑车去了桂花路12号。第二天,纸条变成了“梧桐路3号”,他又去了梧桐路3号。纸条内容每天在变,但他自己不用换工作流程。

这就是间接寻址:指令并不直接给出地址,而是指定一个“口袋”——寄存器,口袋里面装的东西才是真正的地址。mov eax, [ebx] 里,[ebx] 的意思是“把 EBX 寄存器里的值当作内存地址,去那个地址上取数据”。EBX 就是口袋,里面的数是纸条。

间接寻址让“去哪个门牌”变成了可以在运行时修改的动态值。同一个指令,上次去 5 号,这次去 9 号,只要改寄存器即可。链表节点的逐个访问、函数指针的跳转、多态对象的调用,底层都依赖这种“地址可以转交、可以流动”的能力。地址像一张便条,从数据流里来,再被 CPU 按图索骥。

阿寻从此爱上了口袋里那张会变的纸条:它让一枚快递员可以服务整座城市任意一个门牌。而做到这一点,全靠他口袋里那几个被称作“寄存器”的便签位——接下来,我们仔细看看这些口袋的本事。

EffectiveAddr=(RB)\displaystyle \text{EffectiveAddr} = (R_B)
括号表示“打开寄存器这个口袋,把里面的纸条取出来当门牌号”。比如 EBX=5,有效地址就是 5。
  1. 读取寄存器值CPU 先读出寄存器 RB 中保存的数,这个数本身是一个内存地址。
    RB=5\displaystyle R_B = 5
  2. 解引用方括号方括号 [RB] 的含义是“以上一步的数作为目标地址”,完成地址转换。
    EffectiveAddr=(RB)=5\displaystyle \text{EffectiveAddr} = (R_B) = 5
  3. 按地址访存CPU 去内存市 5 号柜取数据,返回值给指令使用。
    val=M[(RB)]\displaystyle \text{val} = M[(R_B)]

寄存器寻址角色

阿寻的工装上有好几只口袋,每只都有自己的编号:RAX、RBX、RCX、RDX、RSI、RDI……有的口袋随手放些小零碎,有的口袋专门用来放“地址纸条”。CPU 里的通用寄存器就是这样的口袋阵列。x86 在设计寻址时,给这些口袋分配了不同角色:有的当“基址”,负责告诉你从哪条街开始数;有的当“变址”,负责告诉你隔几栋楼取一次件。

x86 的完整寻址算式把口袋们组合起来,一条指令就能算出任意目标地址:

这里 Base 是基址寄存器的值,Index 是变址寄存器的值,Scale 是变址的缩放倍数(1、2、4、8),Disp 是写在指令里的位移量。比如 lea eax, [ebx + ecx*4 + 8],意思是“把 EBX 的值加上 ECX×4 再加 8,结果放进 EAX”。注意 lea 只是算地址,不访问内存,就像阿寻拿纸笔口算出门牌号,但先不去取件。

阿寻把口袋按角色分配好:一个定起点,一个数跨度,再加上单子上的微调数字,一条街不管多长都能精确送到。接下来,这套“口袋组合术”最经典的用武之地,就是挨家挨户送一条街的数组。

EA=Base+Index×Scale+Disp\displaystyle EA = Base + Index \times Scale + Disp
  1. 基址寄存器拿出来作为计算起点的口袋值,如同确定“从商业街入口开始数”。
    Base=RB\displaystyle Base = R_{B}
  2. 变址与缩放变址寄存器值乘以 Scale,得到跨度总长,如同“每栋楼之间隔 4 个门牌”。
    Index×Scale=RI×4\displaystyle Index \times Scale = R_{I} \times 4
  3. 位移修正加上指令中携带的常量 Disp,用来微调起点,比如“过了入口再走 8 米”。
    +Disp\displaystyle +Disp
  4. 合成有效地址把三步相加,得到最终要访问的内存地址。
    EA=RB+RI×4+Disp\displaystyle EA = R_{B} + R_{I} \times 4 + Disp

数组遍历应用

阿寻被派去送商业街,他发现这条街的门牌号非常规整:第一家店是 100 号,每家店占 4 个门牌,那么第 i 家店的门牌就是 100 + i×4。只要知道起点和间隔,他闭着眼都能一家一家送下去——这正是数组遍历的本质。

数组 arr 在内存里是连续排放的,首元素地址称为基地址 base,每个元素占 size 字节。第 i 个元素 arr[i] 的地址就是:

C 语言里写 arr[i],编译器最终会生成类似 mov eax, [ebx + ecx*4] 的指令。EBX 保存数组首地址(基址),ECX 保存索引 i,*4 表示每个 int 占 4 字节。循环每走一轮,要么给 ECX 加 1,要么直接把 EBX 加 4——“指针往后挪一格”。无论哪种方式,都不需要重新拼接完整地址,汇编器用一条指令就完成了“基址+变址×比例”的计算。

挨家挨户送完商业街,阿寻对这种“从起点按间隔推算门牌”的套路已经烂熟。但真正让他大呼神奇的,是总部发来的一批新派送单:上面不写全城门牌号,只写一句“从你脚下往前数三条街”——RIP 相对寻址登场了。

addri=base+i×size\displaystyle \text{addr}_i = \text{base} + i \times \text{size}
  1. 确定基地址base 是数组首元素的门牌号,由编译器或链接器给定。
    base=&arr[0]\displaystyle \text{base} = \&arr[0]
  2. 定位索引i 是当前遍历到的元素下标,来自循环计数器。
    i=0,1,2,…\displaystyle i = 0,1,2,\dots
  3. 乘以元素大小每个元素占 size 字节,偏移总量为 i×size。
    offset=i×size\displaystyle \text{offset} = i \times \text{size}
  4. 合成元素地址基地址加偏移量,得到第 i 个元素的真实地址。
    addri=base+i×size\displaystyle \text{addr}_i = \text{base} + i \times \text{size}

RIP相对寻址

阿寻调到了新城区,手里那张“向前三条街”的单子依然有效——因为这句话根本不需要知道全城地图,只需要知道他此刻站在哪。x86 里的 RIP 寄存器就是“阿寻此刻站的位置”,它永远指向下一条即将执行的指令。

RIP 相对寻址的规则是:目标地址 = 当前 RIP 的值 + 指令里编码的 32 位带符号偏移。例如 mov rax, [rip + 0x1234],CPU 先读出 RIP(下一条指令的地址),加上 0x1234,得到的数才是真正访问的内存地址。偏移可正可负,就像“往前三条街”或“往回两栋楼”。

这种寻址方式让代码只关心“目标离我多远”,而不关心“目标在城市哪个角落”。无论整个程序被装载到内存的哪个地址,指令与数据之间的相对位置不变,偏移量也就不变。这正是现代 Linux 可执行文件默认使用 PIE(位置无关可执行文件)的底层基础。

“向前三条街”这句话让阿寻意识到:只要描述的是相对位置,搬多少次家都不怕。这套哲学放大到整个共享库的世界,就诞生了位置无关代码——一条让所有城市通用的送件法则。

target=RIP+disp32\displaystyle \text{target} = RIP + disp_{32}
RIP 是“阿寻现在站的位置”,disp32 是“往前走几步”——两条信息一加,目标立刻定位。
  1. 获取当前 RIPCPU 记录下一条指令的地址,这是相对计算的基准点。
    RIP=addrnext\displaystyle RIP = \text{addr}_{next}
  2. 读取偏移量从指令中取出 32 位带符号偏移 disp32。
    disp32=+0x1234\displaystyle disp_{32} = +0x1234
  3. 相加得目标基准点加上偏移量,得到实际要访问或跳转的地址。
    target=RIP+disp32\displaystyle \text{target} = RIP + disp_{32}

位置无关代码

如果一座城市的所有派送单都写着“桂花路12号”,阿寻一调走,这些单子全部作废。但如果所有单子都写“从你脚下往前三条街”,那么无论把他派到 A 城还是 B 城,这些单子都依旧准确。位置无关代码(PIC)就是这样一套“相对门牌”体系。

编译器使用 -fPIC 或 -fPIE 选项时,会尽量把对全局数据和函数的访问编译成 RIP 相对形式。这样生成的共享库 .so 文件被加载到任意内存基址时,代码段里的指令不需要修改——因为所有偏移都是相对量,加载器把整个模块平移后,相对关系保持不变。只有少量绝对地址(如 GOT 表项)需要重定位,大大减少了加载开销,也让代码段可以安全共享。

用数学语言说:设某数据 x 与引用它的指令相距 δ,模块被平移 ΔL 后,x 和 RIP 同时增加 ΔL,两者之差不变。这正是“整体搬家,相对位置不变”的严格表述。

阿寻靠着一句“向前三条街”走遍所有城市,共享库也靠 RIP 相对寻址在任意地址安家。不过,这套灵活的送件体系也不是没有代价——多翻一次口袋、多背一截门牌,会不会影响送件速度?我们来看看性能这笔账。

δ=x−RIP,δ′=(x+ΔL)−(RIP+ΔL)=δ\displaystyle \delta = x - RIP,\qquad \delta' = (x+\Delta L) - (RIP+\Delta L) = \delta
整座城市平移后,新位置减去新起点,平移量 ΔL 自动抵消,「向前几条街」的答案不变。
  1. 定义相对偏移数据 x 与指令 RIP 之间的距离是 δ。
    δ=x−RIP\displaystyle \delta = x - RIP
  2. 整体平移模块被加载器放到新基址,x 与 RIP 都加上同一个 ΔL。
    x′=x+ΔL,RIP′=RIP+ΔL\displaystyle x' = x+\Delta L,\quad RIP' = RIP+\Delta L
  3. 抵消平移量相减时 ΔL 消去,相对偏移 δ 保持不变。
    δ′=(x+ΔL)−(RIP+ΔL)=x−RIP=δ\displaystyle \delta' = (x+\Delta L) - (RIP+\Delta L) = x - RIP = \delta

性能与优化

写死门牌的单子,拿起来就能跑;翻口袋看纸条的,多一道工序。但阿寻后来发现,真正让他迟到的从来不是翻口袋那一下,而是路上堵车——对应计算机里就是缓存未命中(cache miss)。寻址方式对性能的影响,远没有想象中那么大。

从编码长度看:直接寻址要在指令里携带完整的 32 位甚至 64 位绝对地址,指令变得很长;而用寄存器间接寻址,往往一个字节的 ModRM 字段就能指定“哪个口袋”,指令更紧凑。粗略对比可以写成:

K 是操作码等公共编码部分,直接寻址要多背 4 字节“门牌号”。指令短的好处是取指快、缓存占用小、代码密度高。但从访存次数看,二者真正访问内存的次数通常都是 1 次——寄存器在 CPU 内部,读取它不产生访存开销。真正的性能大杀器是缓存局部性:数组连续访问会命中最快的一级缓存,而跳来跳去的指针访问则可能反复 miss。

所以阿寻的经验是:单子长短别太在意,路上的红绿灯(缓存)才是关键。编译器在 -O2 下会替他做选择:固定全局变量用 RIP 相对寻址,大数组用基址加变址,结构体字段用位移修正。调优时把热数据放进连续数组,比纠结直接还是间接有效得多。可一旦送错件,阿寻还是得老老实实回到最基础的三样东西上排查。

Ldirect=K+4,Lindirect=K\displaystyle L_{direct} = K + 4,\qquad L_{indirect} = K
  1. 公共编码部分操作码、ModRM 等字节是两种寻址共有的基础开销。
    K=opcode+modrm\displaystyle K = \text{opcode} + \text{modrm}
  2. 直接寻址的额外位移绝对地址需要追加 4 字节立即数。
    Ldirect=K+4\displaystyle L_{direct} = K + 4
  3. 对比结论间接寻址编码更短,但延迟高低还要看缓存命中率等宏观因素。
    Lindirect=K<Ldirect\displaystyle L_{indirect} = K < L_{direct}

调试与实战

阿寻送错件了,站长教他从三处查起:第一看派送单(指令本身),第二看口袋里的纸条(寄存器值),第三看门牌号(内存地址)。在计算机里,这就是反汇编、看寄存器、查看内存三板斧。

用 objdump -d 反汇编程序,能清楚看到每条指令的寻址方式:mov eax, [0x404010] 说明访问的是一个固定地址的全局变量;mov eax, [rbx] 说明访问的是寄存器指向的动态对象。GDB 调试时,p/x $rax 查看口袋里的纸条内容,x/gx 0x6010 查看某个门牌号下存放的 8 字节数据。这些操作的本质,都是对“地址到值”这一映射的查询:

调试器里输入一个地址,机器按“内存这本大字典”返回一个值,就这么简单。寻址方式还与安全紧密相关:绝对寻址的代码段容易被定位,不方便做地址随机化;而代码里有 call [rax] 这类间接跳转时,往往意味着在调用函数指针,逆向工程师看到它会立刻联想到虚函数表。PIC + ASLR 的组合,正是为了让攻击者无法猜中“门牌号”而设计的。

阿寻终于明白:无论派送单写得多花哨,最终都是“按某种方式拿到一个门牌号,再去取件”。写死的门牌、口袋里的纸条、脚下的三条街,三种哲学支撑起了 x86 寻址的整片天空。下次当你盯着一条汇编指令发呆时,不妨想想阿寻的工装口袋。

val=M[addr]\displaystyle \text{val} = M[\text{addr}]
  1. 输入地址调试者提供要查看的内存地址,如同告诉阿寻门牌号。
    addr=0x6010\displaystyle \text{addr} = 0x6010
  2. 内存映射查表内存作为从地址到字节的映射函数 M,给出该地址存储的内容。
    M:addr↦val\displaystyle M: \text{addr} \mapsto \text{val}
  3. 输出数据得到目标单元里的实际值,用于判断程序状态。
    val=M[0x6010]\displaystyle \text{val} = M[0x6010]

AI 应用场景

AI推理引擎中的张量遍历优化

阶段:模型推理与部署优化

解决的问题:AI 框架处理张量(Tensor)时,需要反复遍历多维数组。如果每次都写 arr[i][j][k] 这种高维索引,编译器生成的地址计算会非常臃肿,缓存命中率也不理想。理解 x86 的“基址+变址×比例”寻址后,就能看懂编译器为什么把多维索引展开成连续的指针步进。

具体案例:在 ONNX Runtime 或 PyTorch 的 kernel 里,张量数据在内存中连续存放。底层代码常用 float* ptr = tensor->data; ptr += offset; 模式遍历,最终编译成 movss xmm0, [rax + rcx*4] 这样的指令——RAX 是基址(数组首地址),RCX 是变址(元素偏移量)。AI 工程师通过手写 SIMD kernel 并对齐内存,把访存模式从“随机跳变”改成“连续步进”,推理延迟能下降 20% 以上。

JIT 编译器生成位置无关代码

阶段:模型动态编译与执行(JIT)

解决的问题:AI 框架的 JIT(如 PyTorch 的 TorchScript、LLVM JIT)在运行时把中间表示编译成机器码。机器码要写入内存缓冲区才能执行,但缓冲区的地址每次运行都可能不同。如果生成绝对地址指令,每次都要修补;使用 RIP 相对寻址,代码可以原样搬去任何位置。

具体案例:LLVM 的 JIT 默认生成位置无关代码,编译出的函数用 call [rip + .LJTI0_0] 这类相对调用。AI 平台团队在实现“图编译缓存”时,把 JIT 产出的二进制块直接落盘复用,下次加载到不同基址也能直接运行,省去重定位步骤,冷启动时间缩短 30%。

AI 安全分析中的逆向调试

阶段:AI 安全攻防与模型保护

解决的问题:分析恶意 AI 模型文件、对抗样本生成器或窃取模型权重的行为,需要逆向汇编代码。从寻址方式上能快速区分“访问固定全局表”还是“遍历动态指针”,从而定位关键数据结构。

具体案例:安全工程师用 GDB 附加到可疑的推理进程,先 x/gx 查看权重张量所在地址,再用 p/x $rbx 观察寄存器指针的移动轨迹,判断模型正在被正常推理还是被恶意转储。配合反汇编识别出对函数指针表的间接调用,还能揪出模型后门触发的隐藏分支。

Python 演示

python42 行
class Memory:    def __init__(self, size=16):        self.cells = [0] * size     def read(self, addr):        return self.cells[addr]     def write(self, addr, val):        self.cells[addr] = val  def direct_read(mem, addr):    # 直接寻址: 地址由调用方写死, 编译期已知    return mem.read(addr)  def indirect_read(mem, reg):    # 间接寻址: reg 寄存器中存放的是目标地址    addr = reg    return mem.read(addr)  def main():    mem = Memory()    for i in range(16):        mem.write(i, i * 10)     # 直接寻址: 每次都访问固定的 7 号单元    print('direct mem[7] =', direct_read(mem, 7))     # 间接寻址: 先把地址放进"寄存器"变量    reg = 7    print('indirect mem[reg] =', indirect_read(mem, reg))     # 用间接寻址遍历一段连续的数组    base = 4    for i in range(4):        print('array element', i, '=', indirect_read(mem, base + i))  if __name__ == '__main__':    main()
目的:用 Python 列表模拟内存单元,分别演示直接寻址(地址写死)与间接寻址(寄存器变量保存地址)的语义差异,并展示“基址+索引”遍历数组的底层思想。
  1. class Memory:定义一个模拟内存的类,cells 列表相当于内存市的门牌柜子。
  2. self.cells = [0] * size用列表预设一批存储单元,下标就是门牌号。
  3. def direct_read(mem, addr):模拟直接寻址:调用方传入固定地址,直接访问。
  4. addr = reg模拟间接寻址的核心:寄存器变量里存的是地址纸条。
  5. mem.write(i, i * 10)给每个内存单元写入初始数据,相当于架子上摆好货物。
  6. indirect_read(mem, base + i)用“基址 + 索引”计算目标,对应 x86 的数组遍历寻址。
  7. if __name__ == '__main__':保证模块被导入时不会意外执行演示逻辑。

常见误区

误区我以为 `mov eax, [0x1000]` 里带了“直接”两个字,就是数据直接装进寄存器,不经过内存这层。

正解直接寻址的“直接”说的是地址给出方式直接(写死在指令里),但数据仍然要从内存取出来。真正不访问内存的是寄存器寻址,比如 mov eax, ebx 只是把寄存器里的值复制一份。你可以把直接寻址理解为派送单上印着门牌号——还是要跑一趟去取件,只是不用查地址。

为何常见:“直接”这个词太有迷惑性,初学者容易把“地址直接给出”误解为“数据不需要中转”。

误区我反汇编看到的都是 `mov eax, [rip+...]`,没见到 `mov eax, [0x12345678]`,所以认为 64 位时代直接寻址被淘汰了。

正解x86-64 并未彻底移除绝对寻址,只是把它限制在少数场景(如 mov al/ac, [moffs64] 或带 SIB 的绝对地址形式),并且默认编译器倾向于用 RIP 相对寻址,因为 64 位绝对地址会把指令撑得很长。直接寻址的“写死门牌号”思想,在符号定位和调试信息里依然随处可见。

为何常见:现代编译器默认开 PIE,反汇编里满是 RIP 相对寻址,让人误以为旧的寻址方式消失了。

误区我以为多一层“从寄存器取地址”的工序,每次内存访问都会多花不少时间,所以尽量不用间接寻址。

正解对于现代 x86 CPU,寄存器间接寻址和直接寻址的真实访存延迟基本一致,因为寄存器在 CPU 内部,读取地址几乎是免费的。真正的性能差别来自指令编码长度和缓存命中率:指令更短则取指更快,数据连续则缓存更友好。间接寻址的灵活性常常让编译器主动选择它。

为何常见:把高级语言里“指针多一层间接”的开销直觉,错误地套到了硬件寄存器寻址上。

误区我看到 `mov eax, ebx` 时也把它叫间接寻址,觉得只要指令里有寄存器在参与,就算间接。

正解mov eax, ebx 是寄存器寻址——操作数本身就是寄存器的内容,不涉及内存。而 mov eax, [ebx] 才是寄存器间接寻址——EBX 里的数值被当作地址,CPU 必须去那个地址对应的内存柜取数据。差别在于有没有方括号,也就是要不要“去门牌号跑一趟”。

为何常见:初学者没意识到方括号 [] 才是“解引用、访问内存”的标记,把寄存器的两种用法混为一谈。

面试题

Q1. 直接寻址中,操作数的内存地址来自哪里?

单选题 · easy

  • A.指令中编码的立即数常量← 正确答案
  • B.某个通用寄存器的值
  • C.栈顶指针指向的位置
  • D.程序计数器当前的值

答案:A。直接寻址把绝对地址作为立即数编码在指令里,好比派送单上直接印着门牌号,CPU 照单取件。B 是寄存器寻址,C 是栈寻址,D 是 PC 相关寻址,都不符合“地址写死在指令中”的定义。

Q2. 间接寻址最核心的特点是什么?

单选题 · easy

  • A.地址由选定的寄存器给出← 正确答案
  • B.地址由指令直接给出
  • C.地址由当前栈帧给出
  • D.地址由一个常量给出

答案:A。间接寻址的关键是“先读寄存器里的地址纸条,再去对应门牌取件”,地址存放在寄存器中,可以动态变化。而指令直接给地址是直接寻址,栈帧给地址是栈寻址,常量给地址本质仍是直接寻址。

Q3. 指令 mov eax, [ebx] 中,[ebx] 表示什么?

单选题 · easy

  • A.EBX 的寄存器数值
  • B.EBX 指向的内存值← 正确答案
  • C.EBX 寄存器编号值
  • D.EBX 存储的数据值

答案:B。方括号是“解引用”标记,[ebx] 的意思是“把 EBX 里的数当作地址,去那个地址的内存柜取数据”。A、D 都是 EBX 本身的值,那是没有方括号的寄存器寻址;C 更是把寄存器编号和内容搞混了。

Q4. 遍历数组 arr[i] 时,元素地址通常如何计算?

单选题 · mid

  • A.基址加上索引与步长的乘积← 正确答案
  • B.基址减去当前元素的偏移量
  • C.基址乘以索引再加一个偏移
  • D.直接用索引数值作为地址

答案:A。数组连续存放,arr[i] 的地址 = 首地址 base + i×size,对应 “第一家店门牌 + 店序号×每店间距”。减偏移虽然能访问前面的元素,但遍历方向通常是向后;乘索引没有意义;直接用索引当地址则完全丢掉了数组基址。

Q5. RIP 相对寻址的目标地址如何计算?

单选题 · mid

  • A.当前 RIP 的值加上偏移量← 正确答案
  • B.当前 RIP 的值减去指令长度
  • C.当前 RIP 的值乘上比例因子
  • D.当前 RIP 的值与寄存器异或

答案:A。RIP 相对寻址的公式是 target = RIP + disp32,等于“从我现在站的位置往前数 N 条街”。减指令长度、乘比例、异或都不是 x86 定义的计算方式,偏移量直接与 RIP 相加即可。

Q6. 关于直接寻址的说法,正确的是哪些?

多选题 · mid

  • A.地址在编译链接期就能确定← 正确答案
  • B.适合访问位置固定的全局变量← 正确答案
  • C.地址在运行时才动态计算出来
  • D.访问数据不需要经过内存总线

答案:AB。直接寻址的地址编码在指令里,链接完成后即为常量,所以编译期可确定,也正因如此适合全局变量。C 是间接寻址的特征;D 错误——直接寻址仍然要按地址访问内存,数据要经过内存总线,只有寄存器操作数才不走内存。

Q7. 关于 RIP 相对寻址的说法,哪些是正确的?

多选题 · mid

  • A.目标地址依赖当前指令的位置← 正确答案
  • B.有利于生成可重定位的机器码← 正确答案
  • C.偏移量在链接阶段即可确定← 正确答案
  • D.该寻址方式只能用于跳转指令

答案:ABC。RIP 相对寻址以当前 RIP 为基准加偏移,因此目标依赖指令位置;模块在内存中整体平移时相对偏移不变,所以有利于生成 PIC;偏移由链接器计算得出。D 错误——它同样广泛用于数据访问,例如 mov rax, [rip+0x1234]。

Q8. 关于直接寻址与间接寻址的性能对比,正确的是哪些?

多选题 · hard

  • A.间接寻址的指令编码往往更短← 正确答案
  • B.缓存命中率对实际开销影响很大← 正确答案
  • C.直接寻址需携带完整绝对地址← 正确答案
  • D.直接寻址的访问延迟必然最小

答案:ABC。间接寻址只用 ModRM 指定寄存器,指令往往更紧凑;直接寻址要多带 4 字节甚至 8 字节地址;现代 CPU 的性能瓶颈常是缓存而非寻址方式。D 错误——若目标地址数据不在缓存里,直接寻址同样会遭遇漫长的访存延迟,谈不上“必然最小”。

Q9. 位置无关代码 PIC 为何偏爱 RIP 相对寻址?

单选题 · hard

  • A.装载基址改变时相对偏移仍保持不变← 正确答案
  • B.装载基址改变时偏移会同步变化
  • C.绝对地址指令的编码长度通常更短
  • D.该寻址方式不需要任何寄存器参与

答案:A。PIC 的核心诉求是“搬到任何地址都能原样运行”。RIP 相对寻址的偏移是相对量,模块整体平移 ΔL 后,RIP 与目标同时变化,差值不变,所以不需要重写代码段。B 说反了;C 对绝对地址的评价恰好相反;D 错误,RIP 本身就是指令指针寄存器。

Q10. x86-64 中默认用 RIP 相对寻址替代绝对直接寻址,主要原因是什么?

单选题 · extreme

  • A.64 位绝对地址会让指令长度明显膨胀← 正确答案
  • B.RIP 相对寻址的访问速度一定更快
  • C.间接寻址在现代处理器中已无价值
  • D.全局变量不再适合使用直接寻址

答案:A。64 位模式下,若每条访存指令都携带 8 字节绝对地址,指令长度会显著增加,拖累取指效率和代码密度。RIP 相对寻址用 4 字节偏移达到“自定位”效果,成为默认选择。B 说“一定更快”太绝对;C、D 明显错误——间接寻址和直接寻址思想都依然存在。