不要被术语所迷惑。 “base”具有特定的技术含义,寻址模式的“base”组件必须是数组的开头。例如[esp + 16 + esi*4] 可能正在索引从 esp+16 开始的本地数组,即使 esp 是基址寄存器。
同样,[esi+ebx*4-0x4] 最明显的解释是array[i-1],在 EBX 中 i 和 esi 保存数组的起始地址。编译器将-1 折叠到寻址模式而不是在另一个寄存器中计算ebx-1 并将其用作索引是一个明显的优化。
位移为负值是什么意思?
它并不“意味着”任何东西。硬件只是进行二进制加法并使用结果。由程序员(或编译器)使用访问所需字节的寻址模式。
我在Referencing the contents of a memory location. (x86 addressing modes) 上的回答提供了一些示例,说明您何时可以使用每个可能的寻址模式进行数组索引,使用指向数组或静态数组的指针(因此您可以将数组起始地址硬编码为绝对位移)。
在技术 x86 寻址模式术语中:
-
位移:地址的 +- 常量部分,以 2 的补码符号扩展
disp8 或 disp32 编码。 (在 64 位寻址模式中,disp32 符号扩展为 64 位)。
-
偏移量:esi+ebx*4-0x4 计算的结果:相对于段基的偏移量。 (在 base=0 的普通平面内存模型中,偏移量 = 整个地址)。
人们经常使用“offset”来描述位移,通常不会造成混淆,因为从上下文中可以清楚地看出他们在谈论恒定的偏移量(在 x86 seg:off 以外的意义上使用英文单词 offset),但是我喜欢用“位移”来形容位移。
-
base:寻址方式的非索引寄存器组件,如果有的话。 (“无基址寄存器”的编码意味着有一个disp32,您可以将其视为基址。它暗示了DS段。)
这包括只有索引而没有基址寄存器的情况:[esi*4] 只能编码为[dword 0 + esi*4]。
imul eax,DWORD PTR [esi+ebx*4-0x4]
是的,eax *= memory source operand。
是的,您的地址计算是正确的。基数 + 缩放索引 + 有符号位移,产生一个虚拟地址1。
“转到地址(结果)并获取其中的值”是一种奇怪的描述方式。 “转到”通常意味着控制传输,将字节作为代码获取。但事实并非如此,这只是从该地址加载数据,完全由硬件处理。
现代 x86 CPU(例如 Intel Skylake)将 imul eax, [esi+ebx*4 - 4] 解码为两个微指令:一个 imul ALU 操作和一个负载。 ALU 操作必须等待加载结果。 (有趣的事实:对于大多数管道,这两个微操作实际上被微融合到一个微操作中,除了在乱序调度器中。请参阅https://agner.org/optimize/ 了解更多信息。)
当加载 uop 运行时,地址生成单元 (AGU) 获取 2 个寄存器输入、索引比例因子(左移 2)和立即位移 (-4)。 AGU 中的移位加硬件计算加载地址。
加载执行单元内部的下一步是使用该地址从 L1d 缓存(它具有基本内置的第一级 L1dTLB 虚拟->物理缓存。L1d 是虚拟索引的,因此可以进行 TLB 查找与从 L1d 缓存的那种方式获取 8 个标签+数据的集合并行)。假设命中 L1dTLB 和 L1d 缓存,加载执行单元会在大约 5 个周期后收到加载结果。
该加载结果作为源操作数转发到 ALU 执行单元。 ALU 不关心它是imul eax, ebx 还是内存源操作数;一旦两个输入操作数都准备好,乘法 uop 就会立即分派到 ALU。
jmp DWORD PTR [eax*4+0x80497e8]
是的,eax *4 是缩放索引。
是的,0x80497e8 是 disp32 位移。在这种情况下,寻址方式的位移分量很可能被用作静态跳转表的地址。您可以将其视为这种寻址方式的基础。
跳转到那个地址
不,从该地址加载一个新的 EIP 值。由于方括号,这是一个内存间接跳转。
你描述的应该是
lea eax, [eax*4+0x80497e8] ; address calc
jmp eax ; jump to code at that address
没有办法在一条指令中进行计算跳转,您总是需要将新的 EIP 值保存在寄存器中或作为数据从内存中获取。
脚注 1:我们假设一个平面内存模型(段基数 = 0),因此我们可以忽略分段,就像在 Linux、Windows、OS X 或几乎任何 32 或64 位操作系统。所以地址计算给你一个线性地址。
我还假设分页已启用,就像在主流操作系统下正常一样,所以它是一个虚拟地址,必须由 TLB 缓存的页表转换为物理地址。