深夜的快递站,老站长把一个小包裹递给新来的快递员小明:“送到 3 栋 404。”小明愣住了——这个小区有 20 栋楼,每栋之间有固定间距,每层有 6 间房,光靠“3 栋 404”根本找不到。老站长笑了:“地址从来不是一句话,而是一条计算链:小区大门(基址)+栋号×楼间距(变址×比例)+门牌号(位移)。”那一晚,小明学会了用三个数字拼出世界上任何一个精确位置。而 x86 处理器找内存地址时,用的正是同一套“寻址算术”:把基址寄存器里的起点值,加上变址寄存器乘上比例因子,最后再加上一个位移常量,就得到了一条完整地址。更有意思的是,x86 还专门提供了一条“只算地址、不出门送件”的指令——LEA,它像快递员先查好路线但不实际跑腿。后来,程序员们把这条“查路线的本事”玩成了快速算术的魔法。

岗位关联

掌握有效地址计算是看透汇编寻址、读懂编译器生成代码的第一步,也是做性能优化时顺手用 LEA 省周期的看家本领。

目录
  1. 可视化演示
  2. 四元结构
  3. 寻址形式
  4. 计算过程
  5. LEA指令
  6. 优化应用
  7. 访存流程
  8. 寻址限制
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示EA=Base+Index×Scale+Displacement四分量公式、五种寻址方式分类与LEA快速乘法优化,交互理解x86寻址机制。

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

'+modeData.formula.caption+'

四元结构

快递站新挂出一张“全城门牌速查表”,上面规定地址必须拆成四个零件来填:小区大门是基址,第几栋是变址,栋与栋之间的间距是比例,具体门牌号是位移。快递员小明恍然大悟:原来“3 栋 404”只是把“大门 + 3×间距 + 404”压缩后的口语。x86 的括号 [] 里装着的,正是这样一张四零件速查表。

在 x86 里,方括号表示“访问这个地址”,括号里的算式告诉 CPU 怎么拼出这条地址。基址寄存器(如 EBX、EBP)保存一段内存区域的起始位置,相当于“小区大门”;变址寄存器(如 ESI、EDI)保存一个可变的序号,相当于“第几栋”;比例因子是变址的缩放倍数,相当于“每栋的间距”;最后一个位移常量是直接写在指令里的固定数字,相当于“门牌号”。四个零件齐了,CPU 就能算出唯一的地址。

x86 的地址拼装公式如下:

小明把这张公式抄在工牌背面,从此看到括号就能心算出地址。其实 CPU 也在做同样的事——只不过它用的是晶体管,快得多。

EA=Base+Index×Scale+Disp\displaystyle \text{EA} = \text{Base} + \text{Index} \times \text{Scale} + \text{Disp}
有效地址 = 基址寄存器的值 + 变址寄存器的值乘以比例因子 + 位移常量。
  1. 取出基址从基址寄存器里读出起点值,它决定地址从哪开始。
    Base=[EBX]\text{Base} = [\text{EBX}]
  2. 变址缩放把变址寄存器的值乘以比例因子,换算成真实距离。
    Index×Scale=[ESI]×4\text{Index} \times \text{Scale} = [\text{ESI}] \times 4
  3. 三值求和基址加上缩放后的变址,再加上位移常量,拼出完整地址。
    EA=Base+Index×Scale+Disp\text{EA} = \text{Base} + \text{Index} \times \text{Scale} + \text{Disp}

寻址形式

有时候快递单不用填满:只写小区名、不写栋号和门牌,快递员也能把小件放到门卫处;只写小区和栋号,能送到楼下。x86 的寻址形式也允许“填空”,缺哪个零件就省掉哪段计算。这就是为什么同一个括号,形态可以千变万化。

最常见的三种形态:第一种是寄存器间接寻址,括号里只有一个寄存器,比如 [EBX],相当于“送到 EBX 指向的小区门卫室”;第二种是基址加变址,比如 [EBX+ESI],相当于“EBX 是小区大门,ESI 是栋号,两段拼起来”;第三种是完整形态,四个零件一个不少,比如 [EBX+ESI*4+8],地址被精确到门牌号。此外还有相对寻址,把指令指针 EIP 当作隐藏基址,常用于跳转表。

三种形态的地址表达式如下:

老站长总结:地址填得越细,快递送得越准;寻址形式选得越全,程序能表达的数据结构就越丰富。小明点点头,决定先从“只填小区”练起。

[reg][base+index][base+index×scale+disp]\displaystyle [\text{reg}] \quad\quad [\text{base}+\text{index}] \quad\quad [\text{base}+\text{index}\times\text{scale}+\text{disp}]
从左到右,括号里的零件越来越多,能表达的内存位置也越来越精确。
  1. 寄存器间接只靠一个寄存器就能得到地址,适合遍历数组首地址。
    EA=[EBX]\text{EA} = [\text{EBX}]
  2. 基址加变址两个寄存器相加,无缩放,适合二维结构的行列定位。
    EA=[EBX]+[ESI]\text{EA} = [\text{EBX}] + [\text{ESI}]
  3. 完整形态加入比例和位移,适合结构体数组按元素大小跳步。
    EA=[EBX]+[ESI]×4+8\text{EA} = [\text{EBX}] + [\text{ESI}] \times 4 + 8

计算过程

小明发现,快递站的老快递员从不打草稿:瞥一眼单子,两步三步就在心里走完路程。CPU 也一样,它压根不会为“算地址”单独占用一长串时间,而是交给一个专门的“心算器”——AGU(地址生成单元)。这家伙平时不显山露水,可一旦指令里有括号,它就在同一个时钟周期里悄悄把事情办完。

这个“心算”分三步:先把变址寄存器左移 0、1、2 或 3 位,实现乘 1、2、4、8;再把结果和基址寄存器一起交给加法器;最后加上位移常量。因为移位器和加法器是并行连接的,三个数可以一口气加完,所以 AGU 在一个时钟周期内就能输出有效地址,不会给流水线增加额外周期。这也是为什么数组遍历、结构体访问能这么“顺滑”。

地址生成单元的计算可表示为:

老站长说:心算快是因为诀窍刻在肌肉记忆里。AGU 的“肌肉记忆”就是那几条专用的加法器连线,只要指令需要,一拍就把地址端上来。

EA=Base⏟加法器输入A+(Index≪log⁡2Scale)⏟移位器输出+Disp⏟加法器输入B\displaystyle \text{EA} = \underbrace{\text{Base}}_{\text{加法器输入A}} + \underbrace{(\text{Index} \ll \log_2 \text{Scale})}_{\text{移位器输出}} + \underbrace{\text{Disp}}_{\text{加法器输入B}}
变址部分先用移位器完成乘比例,再与基址、位移一起并行相加。
  1. 变址左移把变址寄存器按比例对应的位数左移,相当于乘 1/2/4/8。
    Index≪log⁡2Scale=Index×Scale\text{Index} \ll \log_2 \text{Scale} = \text{Index} \times \text{Scale}
  2. 并行相加移位结果、基址、位移三路进入加法器,一次完成求和。
    EA=Base+(Index×Scale)+Disp\text{EA} = \text{Base} + (\text{Index} \times \text{Scale}) + \text{Disp}
  3. 输出地址结果在周期结束前送到地址总线,等下一拍访存。
    Address Bus←EA\text{Address Bus} \gets \text{EA}

LEA指令

有一天,小明对着单子念:“这单地址是 2021 号。”老站长却摆摆手:“你只查地址,不用真跑。”小明一愣——查出来又不送,图什么?老站长说:“地址本身也是值!把它记在脑子里,以后有用。”x86 里这条“只查地址不出门”的指令,就是 LEA(Load Effective Address,加载有效地址)。

LEA 的源操作数是个方括号地址表达式,但它不会真的去内存取数,而是把计算出来的地址数值本身写入目标寄存器。举个例子,LEA EAX, [EBX+ESI*4+8] 做的事是:算出右边括号的有效地址,然后把这个数字存进 EAX,全程不碰内存。而 MOV EAX, [EBX+ESI*4+8] 会拿着这个地址去内存里把真正的数据搬出来。一个“只算不取”,一个“先算再取”,这就是两者最本质的区别。

LEA 指令的语法可抽象为:

小明恍然大悟:原来 LEA 是“把地址当数据”的搬运工。它既不敲门,也不进屋,只是把门牌号抄下来递给你。这份“抄门牌”的本事,很快就会被程序员们玩出花来。

LEA r32,[base+index×scale+disp]\displaystyle \text{LEA} \ \text{r32}, \left[ \text{base} + \text{index} \times \text{scale} + \text{disp} \right]
把右侧方括号计算出的有效地址值写入左侧寄存器,不触发内存访问。
  1. 读源地址式解析右侧括号里的四元零件,交给 AGU 计算。
    src=[base+index×scale+disp]\text{src} = [\text{base} + \text{index} \times \text{scale} + \text{disp}]
  2. 计算有效地址AGU 在单周期内算出地址数值。
    EA=Base+Index×Scale+Disp\text{EA} = \text{Base} + \text{Index} \times \text{Scale} + \text{Disp}
  3. 写入目标寄存器把 EA 当普通整数存进左侧寄存器,不读内存。
    dest←EA\text{dest} \gets \text{EA}

优化应用

老站长发现小明有个毛病:算运费时非要用计数器一个个加。同样是算“三倍还要加八”的账,老快递员只扫一眼单子,顺路就报出数。LEA 正是这样的“顺路算术”。因为地址计算天生支持“基址 + 变址×比例 + 位移”,程序员可以把乘法拆成加法,塞进方括号里白嫖 AGU。

最经典的优化是乘 3 加常量:要算 3×EAX + 8,常规做法是 MUL 再 ADD,要花好几个周期;而一条 LEA EAX, [EAX+EAX*2+8] 就齐活——EAX 当基址,EAX 又当变址,比例取 2,位移放 8,三个数一拍加完。同理,LEA REG, [REG*8] 可以快速乘 8,LEA EAX, [EBX+ESI*4] 可以一次完成指针加索引,这在数组和结构体遍历里遍地都是。

优化原理可以写成:

小明终于明白“查地址”为什么值钱:它把算术题藏进了寻址公式,让本来要排队的运算搭上 AGU 的快车。老站长拍拍他的肩:“记住,能用 LEA 算的账,绝不让 MUL 动手。”

3x+c=x⏟基址+x×2⏟变址×比例+c⏟位移 ⟹ LEA reg,[reg+reg×2+disp]\displaystyle 3x + c = \underbrace{x}_{\text{基址}} + \underbrace{x \times 2}_{\text{变址} \times \text{比例}} + \underbrace{c}_{\text{位移}}\ \Longrightarrow\ \text{LEA} \ \text{reg}, [\text{reg}+\text{reg}\times2+\text{disp}]
把乘 3 拆成“自己加自己的两倍”,把加常量塞进位移,一条 LEA 完成两件事。
  1. 拆分乘法把 3x 写成 x + 2x,让“变址×比例”承接 2x。
    3x=x+2x=Base+Index×23x = x + 2x = \text{Base} + \text{Index} \times 2
  2. 复用同一寄存器基址和变址都写 EAX,硬件不限制两者相同。
    LEA EAX,[EAX+EAX×2]\text{LEA} \ \text{EAX}, [\text{EAX}+\text{EAX}\times2]
  3. 并入位移常量 c 直接放进括号的位移位,省掉独立 ADD。
    LEA EAX,[EAX+EAX×2+disp]\text{LEA} \ \text{EAX}, [\text{EAX}+\text{EAX}\times2+\text{disp}]

访存流程

地址算出来了,可快递真能直接送到吗?小明发现小区门口还有两道闸:第一道是段选择,楼栋管家要核对“你属于哪个片区”;第二道是页转换,楼层电梯要按“页表”把你映射到真实的房间号。有效地址只是“逻辑上的门牌”,不等于物理内存的最终地址。

在实模式下,物理地址 = 段寄存器值 × 16 + 有效地址,相当于“片区号”左移 4 位再接上偏移。在保护模式的分页机制里,线性地址会被拆成页目录、页表、页内偏移三截,查表得到页框号后,再把页框号左移 12 位(即乘以 4096)拼上页内偏移,才算碰到真正的物理内存。也就是说,AGU 算出的有效地址只是起点,后面还有两关要过。

两种模式下的转换可概括为:

老站长说:门牌号写得很精确,不代表你一定找得到;还得过了保安和电梯这两道关卡。小明这才明白,有效地址只是“寻址三部曲”的第一句,后面还有段和页两段故事。

物理地址={段基址×16+有效地址,实模式页框号×212+页内偏移,分页模式\displaystyle \text{物理地址} = \begin{cases}\text{段基址} \times 16 + \text{有效地址}, & \text{实模式}\\[4pt]\text{页框号} \times 2^{12} + \text{页内偏移}, & \text{分页模式}\end{cases}
实模式靠“段×16+偏移”拼地址,分页模式靠“页框号×4096+页内偏移”落地。
  1. 实模式段拼接段寄存器值左移 4 位,加上有效地址得到 20 位物理地址。
    物理地址=(段基址≪4)+EA\text{物理地址} = (\text{段基址} \ll 4) + \text{EA}
  2. 分页查表用线性地址高位查页表,得到物理页框号。
    页框号=PageTable[高20位]\text{页框号} = \text{PageTable}[\text{高}20\text{位}]
  3. 页内偏移合并页框号乘以页大小后,加上低 12 位偏移。
    物理地址=(页框号×212)+页内偏移\text{物理地址} = (\text{页框号} \times 2^{12}) + \text{页内偏移}

寻址限制

小区物业贴出一张“特殊规定”:不是所有门都能当大门,也不是所有楼号都能用来乘间距。x86 的寻址也有类似的“白名单”。基址寄存器可以是 EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP 中的任意一个;但变址寄存器少一个 ESP——因为栈指针要专职守栈顶,不能拿来当“栋号”。比例因子更是死规矩:只有 1、2、4、8 四个值,对应移位器左移 0 到 3 位。

此外还有默认段规则:用 EBP 或 ESP 做基址时,默认段是堆栈段 SS,其他情况默认是数据段 DS。这就像“从 1 号楼出发的单子默认归 A 区保安管”,地址语义因此更清晰。16 位模式下限制更多,只能从 BX、BP、SI、DI 里选,比例因子只有 1 和 2。这些限制不是为了刁难人,而是让硬件加法器的连接线尽量少、尽量快。

合法组合可用集合表示:

小明把这些规矩编成口诀:“基址八选一,变址少栈顶,比例四位表,默认 DS 把门守。”老站长点头:规矩虽多,却让每一次寻址都快如闪电。

B∈{EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP}I∈{EAX,EBX,ECX,EDX,ESI,EDI}S∈{1,2,4,8}\displaystyle \text{B} \in \{\text{EAX},\text{EBX},\text{ECX},\text{EDX},\text{ESI},\text{EDI},\text{EBP},\text{ESP}\}\quad\text{I} \in \{\text{EAX},\text{EBX},\text{ECX},\text{EDX},\text{ESI},\text{EDI}\}\quad\text{S} \in \{1,2,4,8\}
基址可从全部八个通用寄存器中选,变址少 ESP,比例只能用 1/2/4/8。
  1. 基址集合八个通用寄存器全部可用,这是“小区大门”的候选池。
    B={EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP}\mathcal{B} = \{\text{EAX},\text{EBX},\text{ECX},\text{EDX},\text{ESI},\text{EDI},\text{EBP},\text{ESP}\}
  2. 变址集合排除 ESP,防止破坏栈顶专用指针。
    I={EAX,EBX,ECX,EDX,ESI,EDI}\mathcal{I} = \{\text{EAX},\text{EBX},\text{ECX},\text{EDX},\text{ESI},\text{EDI}\}
  3. 比例集合仅 1、2、4、8,等于移位的 0、1、2、3 位。
    S={1,2,4,8}={20,21,22,23}\mathcal{S} = \{1,2,4,8\} = \{2^{0},2^{1},2^{2},2^{3}\}

AI 应用场景

编译器自动生成 LEA 做算术优化

阶段:代码生成与性能优化

解决的问题:编译器要把 x*3+8 翻译成机器码。如果老实生成 MUL 再 ADD,会占用多个时钟周期;AI 时代的编译器后端会做“指令选择”,识别出“乘小常数加常量”的模式,把它替换成一条 LEA,省掉乘法器和额外指令,让热循环提速。

具体案例:LLVM/GCC 在开启 -O2 时,对 a[i] = b[i] * 3 + 8 这类代码,常生成 lea reg, [reg+reg*2+8],把地址计算单元当加法器用。

低延迟交易系统中的热路径优化

阶段:延迟敏感型系统优化

解决的问题:高频交易里,微秒级延迟就是金钱。计算一个连续数据结构的偏移(比如订单簿第 N 档的价格 = 基址 + N×档位大小 + 偏移),如果每步都用乘法和加法,流水线会多等几个周期;用一条 LEA 把“定位 + 算术”合并,延迟立刻降下来。

具体案例:某撮合引擎在纳秒级路径上用 LEA rdx, [rax+rcx*8+offset] 取代“IMUL + ADD + MOV”三条指令,单次查询节省约 0.3ns。

Python 演示

python62 行
class SimX86:    """模拟 x86 有效地址计算与 LEA/MOV 行为的小工具库。"""    SCALES = (1, 2, 4, 8)    BASE_REGS = ('eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'ebp', 'esp')    INDEX_REGS = ('eax', 'ebx', 'ecx', 'edx', 'esi', 'edi', 'ebp')     def __init__(self):        self.reg = dict.fromkeys(self.BASE_REGS, 0)        self.memory = {}     def set_reg(self, name, value):        self.reg[name] = value     def effective_address(self, base=None, index=None, scale=1, disp=0):        """模拟硬件 AGU:把四个零件拼成有效地址。"""        if scale not in self.SCALES:            raise ValueError('scale 只能是 1/2/4/8')        if base is not None and base not in self.BASE_REGS:            raise ValueError('非法基址寄存器: ' + str(base))        if index is not None and index not in self.INDEX_REGS:            raise ValueError('非法变址寄存器: ' + str(index))        addr = disp        if base is not None:            addr += self.reg[base]        if index is not None:            addr += self.reg[index] * scale        return addr     def lea(self, dest, base=None, index=None, scale=1, disp=0):        """LEA 指令:只计算地址,不访问内存。"""        addr = self.effective_address(base, index, scale, disp)        self.reg[dest] = addr        return addr     def mov_load(self, dest, base=None, index=None, scale=1, disp=0):        """MOV 指令:先算地址,再去内存取数。"""        addr = self.effective_address(base, index, scale, disp)        self.reg[dest] = self.memory.get(addr, 0)        return addr     def compile_lea_3x_plus_c(self, x, c):        """编译器式优化:用一条 LEA 完成 3*x+c。"""        self.set_reg('eax', x)        self.lea('eax', base='eax', index='eax', scale=2, disp=c)        return self.reg['eax']  def demo():    cpu = SimX86()    cpu.memory[2021] = 100    cpu.set_reg('esi', 7)    ea = cpu.lea('eax', base='esi', index='esi', scale=2, disp=2000)    print('LEA eax,[esi+esi*2+2000] -> eax =', ea)    print('内存未被读取,eax =', cpu.reg['eax'])    v = cpu.mov_load('ebx', base='esi', index='esi', scale=2, disp=2000)    print('MOV ebx,[esi+esi*2+2000] -> ebx =', v)    r = cpu.compile_lea_3x_plus_c(7, 8)    print('LEA 实现 3*x+8: x=7 时结果 =', r)  if __name__ == '__main__':    demo()
目的:用 Python 模拟 x86 的 AGU 地址计算、LEA 与 MOV 的行为差异,以及编译器用 LEA 做乘 3 加常量优化的思路。
  1. class SimX86模拟一台迷你 x86 机器,内置寄存器文件和内存字典。
  2. SCALES = (1, 2, 4, 8)比例因子白名单,直接照搬硬件限制。
  3. def effective_address对应硬件 AGU,把基址、变址×比例、位移三段相加。
  4. if scale not in self.SCALES模拟硬件“比例只能是 1/2/4/8”的硬性约束。
  5. addr = disp位移常量作为求和起点,后续逐步累加基址和缩放后的变址。
  6. def leaLEA 指令核心:算出地址后写入寄存器,完全不动 memory。
  7. def mov_loadMOV 指令核心:算出地址后去 memory 里取真实数据。
  8. self.reg[dest] = addrLEA 把“地址值”当普通整数保存,这是它能做算术的根源。
  9. cpu.lea('eax', base='esi', index='esi', scale=2, disp=2000)演示同一寄存器同时充当基址和变址。
  10. cpu.mov_load('ebx', ...)同样的地址,MOV 却读到了内存中的 100,与 LEA 形成鲜明对比。
  11. compile_lea_3x_plus_c展示编译器把 3*x+c 压进一条 LEA 的经典优化。

常见误区

误区我一直以为方括号就是“去内存里取数”,所以 `LEA eax, [ebx+esi*4]` 也一定访问了内存,只是没把值用起来而已。

正解LEA 的方括号从头到尾只是“地址计算式”,它只让 AGU 算出数字,然后把这个数字写进目标寄存器,完全不触发访存。就像快递员看着门牌号记在纸上,但绝不会敲门。真正访存的是 MOV。

为何常见:因为几乎所有带 [] 的指令都会访存,大脑把“括号”和“访存”绑定了,忽略了 LEA 的特殊语义。

误区地址要“基址 + 变址×比例 + 位移”这么多加法,肯定比普通指令多花几个周期吧?流水线岂不是要卡顿?

正解x86 有独立的 AGU,移位器和加法器并行连接,单周期就能吐出有效地址,且不占用 ALU 流水线。只要地址计算不是地址依赖链的瓶颈,它基本是“零成本”的。

为何常见:人脑计算三步加法需要时间,想当然地把人的计算速度投射到硬件身上。

误区既然公式里写着 `Index × Scale`,Scale 填 3、5、7 应该都行吧?无非是乘法器算一下而已。

正解x86 的 Scale 只有 1、2、4、8 四种取值,对应移位器左移 0~3 位。写 [eax+ebx*3] 是非法指令,汇编器直接报错。

为何常见:日常数学里乘法是连续运算,硬件里却是用固定的移位器硬连线实现的,自由度被物理设计限制了。

误区算出 `EA = 0x1000`,我就以为 CPU 一定会去物理内存的 0x1000 号单元拿数据。

正解有效地址只是“逻辑地址”,后面还有段选择和页转换两道关。实模式下要“段×16+EA”,分页模式下还要查页表得到物理页框号,最终地址可能完全不同。

为何常见:早期编程用实模式,段基址通常固定为 0,EA 恰好等于物理地址,这种“巧合”让很多人误以为两者恒等。

误区LEA 的名字叫“加载有效地址”,那它唯一的用处就是在取数组元素地址时用一下,跟算术优化没关系。

正解LEA 实际上是一台“隐藏加法器”,能同时完成“乘小常数 + 加多值”。编译器大量用它生成 x*3+8 这类表达式的代码,比 MUL+ADD 快得多。

为何常见:指令名叫“Load Effective Address”太有迷惑性,让人忽略了它“把地址算式当算术”的实质能力。

面试题

Q1. x86 有效地址的一般计算结构是什么?

单选题 · easy

  • A.基址 + 变址 × 比例 + 位移← 正确答案
  • B.段基址 + 偏移 + 寄存器值
  • C.栈顶 + 帧指针 + 立即数
  • D.操作码 + 操作数 + 寻址位

答案:A。x86 有效地址的标准结构就是“基址 + 变址×比例 + 位移”四元拼装。B 是实模式物理地址的雏形,C 只是栈帧特例,D 是指令编码层面,都不是通用公式。

Q2. 执行 LEA EAX, [EBX+ESI*4] 时,CPU 会做什么?

单选题 · easy

  • A.只计算地址不访问内存← 正确答案
  • B.先访存再算地址
  • C.把内存中的数装入 EAX
  • D.将 EBX 和 ESI 交换

答案:A。LEA 的语义是“加载有效地址”,只让 AGU 算出括号里的地址值并写入 EAX,全程不碰内存。C 是 MOV 的行为,B 和 D 完全不相关。

Q3. x86 寻址中,比例因子 Scale 的合法取值是?

单选题 · easy

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

答案:A。硬件用移位器实现变址缩放,只能左移 0~3 位,即乘 1、2、4、8。其他数值无法用一条移位完成,因此机器码格式中比例字段只有两位,只能编码这四种取值。

Q4. 在 32 位模式下,下列哪些寄存器可以用作变址寄存器?

多选题 · mid

  • A.eax← 正确答案
  • B.esp
  • C.esi← 正确答案
  • D.edi← 正确答案

答案:ACD。变址寄存器候选是 EAX、EBX、ECX、EDX、ESI、EDI、EBP,唯独 ESP 被排除。因为 ESP 专职管理栈顶,硬件约定它不能参与变址缩放,避免因误用破坏栈指针语义。

Q5. 已知 EAX = 5,执行 LEA EAX, [EAX+EAX*2] 后,EAX 的值是多少?

单选题 · mid

  • A.10
  • B.15← 正确答案
  • C.20
  • D.25

答案:B。该指令等价于 EAX ← EAX + EAX×2 = 3×EAX。5×3 = 15。这个例子展示了 LEA 把地址计算器当加法器用的经典技巧,一条指令完成乘 3。

Q6. 关于 x86 有效地址的计算过程,下列说法正确的是?

多选题 · mid

  • A.AGU 在单周期内完成计算← 正确答案
  • B.通常不占用 ALU 流水线← 正确答案
  • C.不额外增加指令周期← 正确答案
  • D.必须多次访问内存完成

答案:ABC。有效地址由专用地址生成单元 AGU 处理,移位器和加法器并行工作,单周期即可输出,因此不占用 ALU 也不额外增加周期。D 完全是错误理解,计算地址本身不访存。

Q7. 以下哪些是合法的 32 位寻址表达式?

多选题 · hard

  • A.[ebx+esi*4+8]← 正确答案
  • B.[eax+ecx*8+4]← 正确答案
  • C.[esp+ebp*2+8]
  • D.[ebx+eax*3+4]

答案:AB。C 非法,因为 ESP 不能充当变址寄存器;D 非法,因为比例因子只能是 1/2/4/8,ax*3 不在硬件支持集内。A、B 的基址、变址、比例都满足白名单要求。

Q8. 要计算 EAX×3 再加常量 8,且要求尽量少占周期,最佳选择是?

单选题 · mid

  • A.lea eax, [eax*3+8]
  • B.lea eax, [eax+eax*2+8]← 正确答案
  • C.lea eax, [eax*2+4]
  • D.lea eax, [esi+eax*4]

答案:B。B 把 3x+8 拆成 x + 2x + 8,基址和变址都用 EAX,比例取 2,位移放 8,一条 LEA 完成。A 用了非法比例 3,C 只算 2x+4,D 结果与 3x+8 无关。

Q9. 实模式下段寄存器 DS = 0x1000,有效地址 = 0x2345,则物理地址是?

单选题 · hard

  • A.0x12345← 正确答案
  • B.0x22345
  • C.0x10000
  • D.0x23450

答案:A。实模式物理地址 = 段值×16 + 有效地址。0x1000×16 = 0x10000,加上 0x2345 得 0x12345。B 是忘了 ×16 之后段值应该左移 4 位,C 漏加偏移,D 是偏移被错误放到高位。

Q10. 分页模式下,线性地址 0x10A8 经查页表得到页框号 0x200,则物理地址计算过程包含哪些步骤?

多选题 · hard

  • A.有效地址先由 AGU 算出← 正确答案
  • B.页框号乘以页大小 4096← 正确答案
  • C.拼接页内偏移 0x0A8← 正确答案
  • D.乘以 16 再与段值相加

答案:ABC。物理地址 = 页框号×2¹² + 页内偏移 = 0x200×4096 + 0x0A8 = 0x200A8。A 说明 AGU 先算线性地址,B 和 C 是分页合并的关键步骤。D 是实模式的段拼接,分页模式下不适用。