想象你是图书馆的管理员。有人向你要书,给了你一个精确的地址:“A 栋 301 房间,第 3 排书架,第 5 本。”这种把地址写死的做法,就像汇编里的直接寻址。可如果图书馆重新装修,A 栋被搬到别处,这个地址就彻底失效了。这时聪明的方式是换一种说法:“从你脚下出发,向前 50 米左转的那个书架,第 5 本。”只要内部布局不变,图书馆搬到哪里你都能找到。这就像 RIP 相对寻址——以你的当前位置为基准。而当你面对一排整齐划一的书架时,管理员只需说“第 3 排书架,每格宽 4 厘米”,你直接用 3×4 就知道该往哪走,这就是比例变址寻址。这篇文章将用这个图书馆比喻,带你彻底搞懂这两种寻址方式,以及它们如何支撑现代操作系统的安全机制 PIE 与 ASLR。

岗位关联

掌握比例变址与 RIP 相对寻址,是读懂反汇编代码、理解编译器优化与系统安全机制(PIE/ASLR)的关键一步,也是从事底层开发、性能优化与安全分析的必备功底。

目录
  1. 可视化演示
  2. 寻址基础:内存的门牌号与指示牌
  3. 比例变址原理:仓库取货的定位公式
  4. 比例因子scale:四档储物格
  5. RIP相对寻址:从脚下出发的导航
  6. PIE与ASLR:让黑客猜不到的家
  7. 工程实践对比:安全与性能的权衡
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示SIB字节比例变址计算、RIP相对寻址偏移及多种寻址方式对比,支持模式切换与Scale参数调节。

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

SIB比例变址寻址 SIB字节编码Scale/Index/Base三字段计算有效地址 EA=Base+Index*Scale+disp Scale取1/2/4/8对应元素大小

寻址基础:内存的门牌号与指示牌

图书馆里的每本书都有一个唯一编号,CPU 访问内存也是如此:内存中的每个字节都有一个唯一的地址,就像每个房间都有门牌号。CPU 发起读操作时,必须告诉内存控制器它想访问哪个“房间”。最简单的取数方式,是直接在指令里把门牌号写死。比如 mov eax, [0x401000],意思就是“去 0x401000 这个地址把数据读出来”,这叫做直接寻址。

直接寻址听起来很直白,但它有一个致命缺陷:如果程序被加载到另一个位置(比如操作系统把程序放到了随机的地址),这个写死的门牌号就作废了。就像你跟管理员说“A 栋 301”,可 A 栋整栋被挪走了,自然找不到。因此聪明的方式是间接寻址——不直接写门牌号,而是把地址先放在寄存器里,指令再说“去这个寄存器指定的地址”。地址值可以在运行时动态计算,灵活性大大提升。

从更宏观的视角看,x86 的内存寻址一直遵循一个统一的模型:有效地址 = 段基址 + 偏移。这里的“段基址”是某个段的起始地址,“偏移”是离起点有多远。后来这个模型演化出了更丰富的变体,也就是现代汇编中常见的“基址 + 索引 × 比例 + 位移”四元组。理解了这个模型,再看后面的一切都会豁然开朗——它本质上就是“从哪个起点出发、往前跨几步、再往里走几米”的组合。

这条“基准 + 距离”的思维贯穿了整个 x86 寻址史。到了 64 位模式,段的概念被大幅弱化,但“从已知位置出发,加一个相对距离”的思想被 RIP 相对寻址完整继承了下来。接下来的比例变址寻址,本质上是把“距离”部分拆成“步数 × 步长 + 零头”,让定位更加灵活高效。

有效地址=段基址+偏移量\text{有效地址} = \text{段基址} + \text{偏移量}
这条公式定义了 x86 寻址的最基本骨架:任何内存地址都可以拆成“基准”加“距离”。
  1. 段基址段的起始地址,通常由段寄存器或基址寄存器提供,是地址计算的起点。
    段基址\text{段基址}
  2. 偏移量指令编码中给出的相对距离,表示目标离段起点有多远,可为正数也可为负数。
    偏移量=目标地址−段基址\text{偏移量} = \text{目标地址} - \text{段基址}
  3. 组合关系CPU 在访问内存时把段基址与偏移量相加,生成最终的有效地址。
    有效地址=段基址+偏移量\text{有效地址} = \text{段基址} + \text{偏移量}

比例变址原理:仓库取货的定位公式

在图书馆取书时,管理员不会告诉你每一本书的精确门牌号,而是给你一套“定位方法”:“从东区入口的第三个书架出发,往前数 4 个书架,每个书架的宽度为 2 米,然后在目标书架往里走 1 米,那本就是你要的书。”这套方法里,起点、步数、每步距离、最后的小零头,四个信息缺一不可。

这套定位方法在 x86 汇编里有一个标准的名字——比例变址寻址(Scaled Index Addressing)。一条典型的指令写成 mov eax, [base + index*scale + disp],其中 base(基址)来自基址寄存器,index(索引)来自索引寄存器,scale(比例因子)是 1/2/4/8 中的某一个,disp(位移)是写在指令里的立即数偏移。

它的典型场景是数组访问。假设你有一个 int 数组 arr,编译器把数组起始地址放入基址寄存器 rbx,要取 arr[3] 时,把下标 3 放入索引寄存器 rcx,把 int 的大小 4 作为 scale,于是一条 mov eax, [rbx + rcx*4] 就完成了 arr[3] 的读取。注意 Intel 语法中 rcx*4 这部分写在方括号内,CPU 会自动完成乘法。它比“先乘、再加、再访问”三条指令高效得多,因为地址计算已经内置在内存寻址的硬件电路里。

回到图书馆的例子,“基址”是起点书架,“索引”是第几个书架,“比例”是每个书架的宽度,“位移”是书架内的零头距离。你不需要背下每个书架的精确门牌号,只要记住这套定位规则,无论书架如何摆放,都能准确找到目标。这正是 C 语言中 arr[i]、p->field 等访问模式在汇编层的统一实现方式。

Address=Base+Index×Scale+Disp\text{Address} = \text{Base} + \text{Index} \times \text{Scale} + \text{Disp}
比例变址的核心就是把“数组下标”翻译成“离起点的字节距离”,一次硬件计算完成定位。
  1. Base(基址)基址寄存器中的值,如 rbx、rbp,表示数组或结构体的起始地址,相当于“起点书架”。
    Base\text{Base}
  2. Index(索引)索引寄存器中的值,如 rcx、rsi,表示“第几个元素”,相当于“从起点数第几个书架”。
    Index\text{Index}
  3. Scale(比例因子)每个元素占据的字节数,硬件仅支持 1/2/4/8 四档,相当于“每个书架的宽度”。
    Scale∈{1,2,4,8}\text{Scale} \in \{1, 2, 4, 8\}
  4. Disp(位移)指令中编码的立即数偏移,常用来定位结构体里的字段,相当于“书架内再往里走的零头距离”。
    Disp\text{Disp}
  5. 求有效地址CPU 的地址生成单元把四部分相加,得到最终访问内存的有效地址。
    Address=Base+Index×Scale+Disp\text{Address} = \text{Base} + \text{Index} \times \text{Scale} + \text{Disp}

比例因子scale:四档储物格

有些图书馆会将书架统一采购成四种规格:小格 1 米、中格 2 米、大格 4 米、超大格 8 米。管理员告诉你“第 5 个大格”,你立刻知道它离起点是 5×4=20 米,完全不用一格一格数。这种“预先把步长固定成几档”的设计,正是比例因子 scale 的灵魂。

为什么 scale 只支持 1/2/4/8 四种值?因为它在指令编码中只占 2 位:00 对应 1,01 对应 2,10 对应 4,11 对应 8。这四种值精确对应 C 语言中最常见的四种基本类型宽度:char(1 字节)、short(2 字节)、int(4 字节)、long 或 double(8 字节)。编译器在生成访问数组元素的代码时,会根据元素的 sizeof 自动填入对应的 scale,程序员很少需要手动指定。

手工汇编时,这个特性也有妙用。例如 switch-case 编译器生成的跳转表(jump table),就是通过 base + index*8 找到每个 case 分支的入口地址。如果你知道元素的宽度是 2 的幂,就可以直接用比例变址完成快速索引,不必先乘法再加法。反之,如果遇到元素大小为 12 字节的结构体数组,编译器只能退而求其次,把寻址拆成多条指令,这正是“2 的幂元素大小”在性能上占便宜的根本原因。

下次写代码时,如果能让结构体大小保持 2 的幂,编译器就有机会用一条比例变址指令完成寻址。反之,如果结构体里有奇数长度的数组,编译器就只能退化为多条指令。理解 scale 的四档设计,你就能解释为什么“对齐”和“2 的幂大小”在底层性能优化里如此重要。

Scale=sizeof(element),Scale∈{1,2,4,8}\text{Scale} = \text{sizeof}(\text{element}), \quad \text{Scale} \in \{1, 2, 4, 8\}
scale 的取值不是软件约定,而是硬件编码的上限:2 位字段决定了最多只有 4 种步长。
  1. 元素大小C 语言中 char=1、short=2、int=4、long=8,编译器用 sizeof 得到元素宽度。
    sizeof(Type)\text{sizeof}(\text{Type})
  2. 编码映射指令里 2 位字段 00/01/10/11 分别对应 scale=1/2/4/8,硬件译码后直接作为乘法因子。
    Scale∈{1,2,4,8}\text{Scale} \in \{1, 2, 4, 8\}
  3. 乘法关系数组第 i 个元素的位置 = 起点 + i × 元素大小,这正是比例变址寻址在数组访问中的应用。
    elementi=base+i×Scale\text{element}_i = \text{base} + i \times \text{Scale}

RIP相对寻址:从脚下出发的导航

在大型商场问路,工作人员不告诉你“B 区 207 号柜台”,而是说“从你现在站的位置往前走 50 米,右转就到了”。这比报门牌号更聪明:因为无论你身处商场哪一层,只要按相对方向走,总能到达。RIP 相对寻址的想法与此完全一致。

x86-64 中,RIP 寄存器(Instruction Pointer,指令指针)保存着下一条待执行指令的地址。所谓 RIP 相对寻址,就是指令中不写完整的目标地址,而是写一个 32 位的带符号偏移量 disp32。CPU 取指后,将“下一条指令的地址 + disp32”作为目标地址。这个偏移可正可负,覆盖范围约 ±2GB,足以访问当前模块内的代码和全局数据。

RIP 相对寻址最大的价值在于“不需要知道绝对地址”。只要指令和它要访问的数据之间的相对距离在链接时确定,运行时无论模块被加载到哪个地址,RIP + disp 都自动指向正确位置。这完美适配了操作系统加载程序的实际情况——程序每次运行在哪个地址,不再是程序员能决定的事。

就像商场的“从当前位置出发”,你不需要知道商场的绝对坐标,只需要知道相对距离。在 64 位程序中,全局变量、字符串常量、跳转表、异常处理表等模块内引用,几乎都会被编译器编译成 RIP 相对寻址。这也解释了为什么现代 64 位反汇编代码里,[rip+0x1234] 这类模式无处不在。

目标地址=RIP+disp32\text{目标地址} = \text{RIP} + \text{disp32}
这条公式的微妙之处在于:RIP 指向的是下一条指令,而不是当前正在执行的那一条。
  1. RIP 当前值x86-64 中 RIP 总是指向下一条指令的地址,这是 CPU 流水线预取机制决定的。
    RIPcurrent\text{RIP}_{\text{current}}
  2. disp32指令编码中附带的 32 位带符号位移,可以为正也可以为负,最大范围约 ±2GB。
    disp32\text{disp32}
  3. 加法CPU 在地址生成阶段自动将二者相加,得到目标地址,整个过程无需额外指令。
    目标地址=RIP+disp32\text{目标地址} = \text{RIP} + \text{disp32}

PIE与ASLR:让黑客猜不到的家

重要人物的住宅,每次入住前都会更换地址,但房间内部布局不变——客厅到卧室的距离永远相同。即使小偷摸清了内部布局,也不知道新家具体在哪个小区哪栋楼。这个场景与 PIE + ASLR 的组合如出一辙。

PIE(Position Independent Executable,位置无关可执行文件)是一种编译期属性。使用 gcc -fPIE -pie 编译出的程序,代码内部不依赖任何绝对地址,所有模块内引用都用 RIP 相对寻址等相对方式表达。ASLR(Address Space Layout Randomization,地址空间布局随机化)则是操作系统加载期的行为:内核在每次运行程序时,把代码段、数据段、栈等映射到随机选择的基址。

两者是如何配合的?在 PIE 程序中,一条 RIP 相对指令的 disp 值在链接时就固定了,它表示“目标离下一条指令的距离”。无论加载基址如何变化,RIP 本身也会随基址同步漂移,于是 RIP + disp 永远指向正确目标。而如果没有 PIE,程序代码段必须加载到固定地址,攻击者提前知道布局,就能精准构造攻击输入。PIE + ASLR 使攻击者每次都要猜测地址,成功率大大下降。

图书馆大楼每夜都可能被安排到城市的不同地块(ASLR),但只要馆内书架间距不变,管理员从大门出发(RIP)用相对距离依然能准确找到任何书。这就是安全机制的优雅之处:它不改变程序内部逻辑,只改变程序“住”在哪里,而 RIP 相对寻址让“住在哪里”变得不再重要。

运行地址=加载基址随机+编译时偏移\text{运行地址} = \text{加载基址}_{\text{随机}} + \text{编译时偏移}
PIE 是“代码愿不愿意搬家”,ASLR 是“系统把家安在哪个随机地址”,二者缺一不可。
  1. 加载基址ASLR 为每次加载随机选择的起始地址,可执行文件整体被映射到这个地址之后。
    LoadAddr\text{LoadAddr}
  2. 编译时偏移链接器在构建时确定的目标在模块内的偏移,RIP 相对指令的 disp 本质就是这个偏移。
    Offsetlink\text{Offset}_{\text{link}}
  3. 运行地址目标在运行时的真实地址等于加载基址与编译时偏移之和,这条加法由 RIP+disp 天然完成。
    RunAddr=LoadAddr+Offsetlink\text{RunAddr} = \text{LoadAddr} + \text{Offset}_{\text{link}}

工程实践对比:安全与性能的权衡

想象两种搬家方式。第一种把地址写在门牌上,搬家后每块门牌都要重做——这是绝对寻址;第二种画一张“从门口出发的路线图”,搬家后路线图依然有效——这是 RIP 相对寻址。两者的差距在重定位成本上体现得淋漓尽致。

绝对寻址需要指令中直接携带完整地址。在 64 位模式下,一个 64 位绝对地址需要 8 字节立即数,指令编码长、缓存占用大,而且在 PIE 程序中每个这样的地址都是一条重定位记录,加载器要在启动时逐一修正。RIP 相对寻址则完全不同:disp32 是相对距离,链接时就已确定,加载时不需要任何修正,指令也只有 7 字节左右。

从运行时开销看,RIP 相对寻址并没有引入额外代价。地址加法由 CPU 的地址生成硬件完成,与普通寻址同在一个时钟周期内结束,性能几乎零损失。对比之下,绝对寻址带来的重定位开销不仅拖慢程序加载,还给攻击者留下可预测的固定地址。这就是为什么现代 64 位系统全面转向 RIP 相对寻址:安全性和性能同时获益。

于是,聪明的图书馆决定把所有指示牌都换成“相对路线图”。搬家成本几乎为零,安全性还大幅提升——这正是现代 64 位系统全面拥抱 RIP 相对寻址的根本原因。理解了这个权衡,你在阅读反汇编、分析性能瓶颈或排查安全问题时,就能一眼看穿编译器为什么选择这种寻址方式。

重定位条目:绝对寻址=N,RIP相对=0\text{重定位条目}: \quad \text{绝对寻址} = N, \quad \text{RIP相对} = 0
重定位条目从 N 降到 0,是 RIP 相对寻址在工程上最直观的收益。
  1. 绝对寻址重定位程序中每个外部绝对地址都需要一条重定位记录,加载器启动时逐个修正。
    Relocabs=N\text{Reloc}_{\text{abs}} = N
  2. RIP相对重定位disp 偏移在链接期固定,加载器无需修正,重定位条目数为零。
    Relocrip=0\text{Reloc}_{\text{rip}} = 0
  3. 总结论RIP 相对寻址把重定位开销降到零,同时为 ASLR 提供支撑,实现安全与性能双赢。
    Reloctotal⟶0\text{Reloc}_{\text{total}} \longrightarrow 0

AI 应用场景

AI推理引擎中的张量寻址优化

阶段:模型推理与性能优化

解决的问题:深度学习中的多维张量本质上是连续内存,访问 tensor[i][j] 需要计算 base + i*row_size + j*elem_size。朴素实现要先乘后加,指令多、延迟高;通过比例变址一条指令即可完成“基址 + 索引×比例 + 位移”,把地址计算的时间降到最低。

具体案例:PyTorch CPU 后端的 TensorIterator 在遍历连续内存时大量使用类似 stride 的机制;ONNX Runtime 的 GEMM 内核用 AVX2 指令做高吞吐乘累加,地址增量完全依赖比例变址完成。理解寻址模型,才能读懂内核热点函数反汇编中反复出现的 [rax + rcx*4 + imm] 模式。

AI编译器后端的指令选择

阶段:AI编译器(TVM / MLIR)代码生成

解决的问题:自动调度在生成高性能循环时,需要把多维索引映射为线性地址。如果编译器不懂目标 ISA 的寻址能力,就会生成冗余的乘法和加法指令;利用比例变址能显著减少地址计算指令,提升生成代码质量。

具体案例:TVM 在生成 x86 CPU 代码时,对 shape=(N,C,H,W) 的张量访问会生成类似 ptr[n*stride + c*scale + offset] 的寻址,底层对应 [rax + rcx*4 + imm] 这类比例变址指令。这是 AI 编译器后端做指令选择时的高频优化点。

AI安全与二进制分析

阶段:AI系统安全与红队攻防

解决的问题:在 PIE + ASLR 保护下,安全分析师需要通过泄漏的一条 RIP 相对指令推算出程序基址。不理解 RIP 相对寻址,就无法判断泄漏值的意义,更无从计算 ASLR 偏移。

具体案例:CTF pwn 中,利用 [rip+0x2020] 访问 GOT 表项,泄漏 libc 地址后反推 ASLR 偏移,进而构造 ROP 链。这类分析在现代 AI 框架安全审计中同样常见——AI 服务的二进制部署同样依赖 PIE/ASLR 防护。

Python 演示

python47 行
# -*- coding: utf-8 -*-"""比例变址与RIP相对寻址的Python模拟"""  def scaled_index_address(base, index, scale, disp=0):    """比例变址寻址: 有效地址 = 基址 + 索引*比例 + 位移"""    return base + index * scale + disp  def read_array(arr, addr, array_base, scale):    """通过比例变址计算出的地址读取数组元素"""    if not (array_base <= addr < array_base + len(arr) * scale):        raise IndexError("有效地址越界: 0x%x" % addr)    return arr[(addr - array_base) // scale]  def rip_relative_address(rip, disp32):    """RIP相对寻址: 目标 = 下一条指令地址 + 带符号32位位移"""    return rip + disp32  def demo_scaled_indexing():    """模拟 int 数组的 arr[i] 访问: base + i*4"""    data = [11, 22, 33, 44, 55, 66, 77, 88]    base = 0x1000    scale = 4    print("== 比例变址寻址 ==")    for i in range(len(data)):        addr = scaled_index_address(base, i, scale, 0)        value = read_array(data, addr, base, scale)        print("arr[%d] addr=0x%04x value=%d" % (i, addr, value))  def demo_rip_relative():    """模拟ASLR换基址后, RIP相对寻址依然正确"""    print("== RIP相对寻址与ASLR ==")    disp = 0x200    loads = [0x400000, 0x555555554000, 0x7ffff7a00000]    for load in loads:        rip = load + 0x1128          # 指令位置随加载基址漂移        target = rip_relative_address(rip, disp)        print("load=0x%x rip=0x%x target=0x%x" % (load, rip, target))  if __name__ == "__main__":    demo_scaled_indexing()    demo_rip_relative()
目的:用纯 Python 模拟比例变址寻址的数组访问,以及 ASLR 换基址后 RIP 相对寻址依然正确的过程。
  1. scaled_index_address(base, index, scale, disp)核心寻址函数,把 base + index*scale + disp 组装成有效地址,对应 CPU 硬件地址生成单元的工作。
  2. read_array(arr, addr, array_base, scale)通过有效地址回读数组元素,模拟汇编 mov eax, [rbx+rcx*4] 的完整路径。
  3. rip_relative_address(rip, disp32)目标 = RIP + 位移,注意这里的 rip 已经是“下一条指令”的地址。
  4. loads = [0x400000, ...]三个不同的加载基址,模拟 ASLR 把程序放在不同地址,RIP 相对寻址依然指向正确目标。
  5. if __name__ == "__main__":保证作为脚本直接运行时才执行演示,作为模块导入时不触发。

常见误区

误区“我想访问第 5 个 double,它占 8 字节,那我写 base + rcx*5 不就行了?直接乘 5 多天然啊,为什么编译器总是用 *8 或者拆成好几步?”

正解x86 指令编码里 scale 只占 2 位,硬件只解码 1/2/4/8 四种值,*5 这种编码根本不存在。对于非 2 的幂元素大小(如 12 字节的结构体),编译器只能把寻址拆成多条指令(如 base + i*8 + i*4)。这也是 C 语言数组元素大小设计成 2 的幂能显著提升性能的底层原因。

为何常见:高级语言里数组下标可以是任意整数,而汇编硬件编码只有固定枚举,两者概念混淆导致。

误区“`jmp [rip+0x50]` 就是从这条指令的起始地址加 0x50 嘛,这有什么好说的?”

正解x86 流水线中,指令还在取指阶段 RIP 就已经指向下一条指令了。所以 rip+disp 的基准是下一条指令的地址。举例:一条 7 字节指令位于 0x1000,disp=0x50,目标是 0x1000+7+0x50=0x1057,而不是 0x1050。反汇编工具会帮你折算,手算时经常在这里翻车。

为何常见:人类直觉认为“当前指令”是起点,忽略了硬件预取机制下 RIP 始终超前一条指令的细节。

误区“开了 ASLR 就是 PIE 程序;编了 PIE 就是有 ASLR。”

正解PIE 是编译/链接期属性(gcc -fPIE -pie),生成内部全部用相对寻址的可执行文件;ASLR 是操作系统加载期行为,内核随机选择映射基址。PIE 是前提,ASLR 是执行者。只有 PIE 没 ASLR 会固定加载;只有 ASLR 没 PIE,虽然重定位能修正地址,但攻击者仍可预测布局。两者配合才能让攻击者每次都要猜。

为何常见:两者总是一起出现在安全特性列表里,且都围绕“地址随机化”,很容易被当成同一件事。

误区“每次访问都要拿 RIP 加偏移,多一次加法,肯定比直接写绝对地址慢。”

正解RIP 相对寻址的加法由 CPU 地址生成单元硬件完成,与普通地址计算同在一个时钟周期内结束,性能几乎零损失。反而绝对地址在 64 位下要么编码不存在,要么需要 8 字节立即数,指令更长、缓存效率更低。

为何常见:把软件层“多一次运算所以更慢”的观念,错误地套在硬件自动完成的地址计算上。

误区“64 位模式看反汇编全是 RIP 相对,绝对寻址应该被彻底移除了吧。”

正解x86-64 移除了 mov [imm64], rax 这种直接访存绝对地址的编码,但绝对地址仍可通过多种方式构造:movabs 加载 64 位立即数到寄存器再间接寻址、lea 把 RIP 相对地址装入寄存器、以及加载器与内核代码中的绝对跳转。应用层编译代码对“模块内引用”才普遍优先 RIP 相对。

为何常见:日常反汇编看到的大多是编译出来的应用层代码,存在“幸存者偏差”,让人误以为绝对寻址被彻底淘汰。

面试题

Q1. 比例变址寻址中,scale 参数允许的取值是?

单选题 · easy

  • A.1, 2, 3, 4
  • B.1, 2, 4, 8← 正确答案
  • C.2, 4, 6, 8
  • D.1, 4, 8, 16

答案:B。scale 字段在 x86 指令编码中只占 2 位,00/01/10/11 映射到 1/2/4/8。像 3、6、16 这样的值无法编码,硬件也不支持。好比图书馆的四档书架宽度,只有 1/2/4/8 米,没有 3 米或 5 米。

Q2. x86-64 中 RIP 寄存器保存的是?

单选题 · easy

  • A.当前正在执行的指令
  • B.下一条待执行的指令← 正确答案
  • C.栈顶内存的地址
  • D.数据段起始的地址

答案:B。RIP 在流水线预取阶段指向下一条指令,而不是当前执行的那条。这是 CPU 预取机制决定的。RIP 相对寻址正是利用这一点,从“下一条指令的地址”出发加偏移,所以手算跳转目标时总要加上当前指令长度。

Q3. 访问 int arr[8] 的 arr[3] 时,编译器填入的 scale 应为?

单选题 · easy

  • A.1
  • B.2
  • C.4← 正确答案
  • D.8

答案:C。int 占 4 字节,编译器把 scale 设为 4,于是 arr[i] 的地址 = base + i*4。选 1 或 2 对应 char/short 数组,8 对应 long/double。就像书架宽度由书的厚度决定,scale 由元素类型宽度决定。

Q4. 指令 mov eax, [rbx + rcx*4 + 8] 中,比例因子(scale)是哪个?

单选题 · mid

  • A.rbx 寄存器内容
  • B.rcx 寄存器内容
  • C.指令中的常数4← 正确答案
  • D.指令中的常数8

答案:C。Intel 语法 [rbx+rcx*4+8] 中,rbx 是基址寄存器,rcx 是索引寄存器,4 是比例因子,8 是位移。注意区分“比例因子”和“位移”,两者都在指令编码中,但作用完全不同:比例因子乘索引,位移单独相加。

Q5. RIP 相对寻址相比绝对寻址的优势有哪些?

多选题 · mid

  • A.指令编码更加紧凑← 正确答案
  • B.内部引用免重定位← 正确答案
  • C.天然支持PIE与ASLR← 正确答案
  • D.能访问全部地址空间

答案:ABC。RIP 相对寻址用 disp32 替代 8 字节绝对地址,编码更短;disp 在编译期确定,内部引用无需重定位;相对距离与加载基址无关,天然支持 PIE/ASLR。D 错误,因为 disp32 有符号范围只有 ±2GB,不能覆盖全部 64 位地址空间。

Q6. PIE(Position Independent Executable)指的是?

单选题 · mid

  • A.位置无关可执行文件← 正确答案
  • B.程序完整性检查机制
  • C.性能增强指令集扩展
  • D.可移植调试信息格式

答案:A。PIE 是编译期用 -fPIE -pie 生成的位置无关可执行文件,代码内部全部使用相对寻址。B 是完整性校验,C 是 CPU 指令集扩展(如 AVX),D 是调试格式,三者都与“位置无关”无关。

Q7. 关于 PIE 与 ASLR 的配合,下列说法正确的有?

多选题 · hard

  • A.PIE让代码段可随机放置← 正确答案
  • B.RIP相对保持内部相对距离← 正确答案
  • C.内部引用免加载重定位← 正确答案
  • D.每个进程加载地址固定

答案:ABC。PIE 让代码段可以整体挪动;RIP 相对寻址保持内部相对距离,程序搬到任何地址都正确;模块内部引用偏移在编译期固定,无需加载重定位。D 明显错误,ASLR 的目的恰恰是让地址不固定。

Q8. 为什么 RIP 相对寻址能减少重定位?

单选题 · hard

  • A.偏移编译期已确定← 正确答案
  • B.RIP是动态计算的
  • C.操作系统自动修复RIP
  • D.链接器直接嵌入绝对地址

答案:A。RIP 相对寻址的 disp 在链接时确定,表示“目标相对于下一条指令的距离”,这个距离只取决于文件内部布局,与加载基址无关。加载到任何地址,RIP+disp 都自动指向正确目标,因此无需重定位。B、C、D 描述的机制都不准确。

Q9. 关于 x86-64 中的绝对寻址,说法正确的有?

多选题 · hard

  • A.mov [imm64] 编码不存在← 正确答案
  • B.可用 lea rax 构造绝对地址← 正确答案
  • C.绝对寻址永不重定位
  • D.立即数寻址等价于绝对寻址

答案:AB。x86-64 的 mov 指令不支持 mov [imm64], reg 这种直接绝对地址访存编码(A 正确);要得到绝对地址可以用 lea rax, [rip+disp] 把目标地址装入寄存器(B 正确)。C 错误,绝对寻址恰恰通常需要重定位;D 错误,立即数寻址是把立即数当作常量值而非内存地址。

Q10. 某 RIP 相对指令位于加载地址 L+0x1000,指令长度 7 字节,disp=0x50,目标地址是?

单选题 · extreme

  • A.L + 0x1057← 正确答案
  • B.L + 0x1050
  • C.L + 0x1007
  • D.L + 0x1000

答案:A。RIP 指向下一条指令,即 L+0x1000+7 = L+0x1007,目标 = L+0x1007+0x50 = L+0x1057。这正是“RIP 相对偏移从下一条指令算起”的经典例题,手算时最容易出错的地方就是忘了加指令长度。