为什么调用和跳转指令使用相对于下一条指令而不是当前指令的位移?
Why do call and jump instruction use a displacement relative to the next instruction, not current?
在下面从英特尔文档中提取的 table 中,我们有操作码 E8 cw 和 E8 cd 位移是相对于下一条指令的。
为什么是下一条指令?为什么位移不是相对于 call
指令本身?
TL:DR: 无论如何你在decode的时候找到指令的结尾,并设置下一条指令的decode。 CPUs 相对于当前指令的 end 进行相对寻址是很正常的,尽管有些 CPUs 会做出不同的选择,例如 relative to end of next指令(用于 ARM PC 相对内存寻址)。
见Does Program Counter hold current address or the address of the next instruction?
x86 机器代码设计在 70 年代后期与 8086 一起固定下来,除了在扩展 ISA 时重新设计的东西(如 32/64 位 ModRM+SIB 寻址模式)。
原始 8086 个指令字节按顺序解码(不一定是一次完整的指令),并且对前缀字节数或总指令长度没有上限。
我认为 8086避免了曾经需要保存一条指令的起始地址,即使是例外。例如,在现代 x86 上 #DE
(除法异常)压入错误指令的地址。但是 .
8086 甚至有一个 "bug"(或记录的设计缺陷),其中在执行 cs rep movsb
(例如)期间到达的中断将最终前缀的地址推送为异常 -return 地址,使 rep
-string 指令上的段覆盖在启用中断的情况下基本上不可用。 (因为执行将在没有 rep
或没有段覆盖的情况下恢复,无论你先放哪个)。查看 和评论。
当 8086 完成对 call
指令的解码时,它不知道它从哪里开始。它唯一的参考点是 call
指令的结尾。 因此,如果他们想在硬件中进行优化(不在任何地方保留解码起始地址),他们甚至都没有有一个选择。虽然理论上他们可以使用 E8 call
操作码的地址(在任何前缀之后)作为锚点,但这可能需要额外的加法器或额外的硬件来单独记录。
Fetch/decode 已经在解码期间找到指令的结尾(同时弄清楚它是 call
或 jmp
),所以指令结束/地址-of-next-instruction 已经在内部可用。 call
甚至必须将该值作为 return 地址压入堆栈。
流水线 RISC 或完全非流水线 CPU 也将使用该下一条指令地址从内存或 I-cache 中获取下一条指令。但实际上 8086 预取是异步进入一个小的预取缓冲区。机器代码格式主要是在实现设计之前在纸上设计的,因此将事物与指令末尾相关联的这种常见原因可能是架构师的想法。
相对于指令末尾进行分支是许多 ISA 的常见设计选择。
重申一下,我只谈论 8086 的原因(它在内部 非常 不同于现代 x86)是因为它是 第一个 gen,理解它有助于解释一些机器代码设计决策。 (例如,为什么 x86 在单字节 xchg [e/r]ax, reg
上花费 8 个操作码:因为 8086 没有 movsx
或 2 操作数 imul
,并且很多东西需要或想要 AX。也代码大小是性能的主要瓶颈。)
现代 x86 只跟踪每条指令的地址,并可在解码时使用它 call rel32
。没什么大不了的。
在下面从英特尔文档中提取的 table 中,我们有操作码 E8 cw 和 E8 cd 位移是相对于下一条指令的。
为什么是下一条指令?为什么位移不是相对于 call
指令本身?
TL:DR: 无论如何你在decode的时候找到指令的结尾,并设置下一条指令的decode。 CPUs 相对于当前指令的 end 进行相对寻址是很正常的,尽管有些 CPUs 会做出不同的选择,例如 relative to end of next指令(用于 ARM PC 相对内存寻址)。
见Does Program Counter hold current address or the address of the next instruction?
x86 机器代码设计在 70 年代后期与 8086 一起固定下来,除了在扩展 ISA 时重新设计的东西(如 32/64 位 ModRM+SIB 寻址模式)。
原始 8086 个指令字节按顺序解码(不一定是一次完整的指令),并且对前缀字节数或总指令长度没有上限。
我认为 8086避免了曾经需要保存一条指令的起始地址,即使是例外。例如,在现代 x86 上 #DE
(除法异常)压入错误指令的地址。但是
8086 甚至有一个 "bug"(或记录的设计缺陷),其中在执行 cs rep movsb
(例如)期间到达的中断将最终前缀的地址推送为异常 -return 地址,使 rep
-string 指令上的段覆盖在启用中断的情况下基本上不可用。 (因为执行将在没有 rep
或没有段覆盖的情况下恢复,无论你先放哪个)。查看
当 8086 完成对 call
指令的解码时,它不知道它从哪里开始。它唯一的参考点是 call
指令的结尾。 因此,如果他们想在硬件中进行优化(不在任何地方保留解码起始地址),他们甚至都没有有一个选择。虽然理论上他们可以使用 E8 call
操作码的地址(在任何前缀之后)作为锚点,但这可能需要额外的加法器或额外的硬件来单独记录。
Fetch/decode 已经在解码期间找到指令的结尾(同时弄清楚它是 call
或 jmp
),所以指令结束/地址-of-next-instruction 已经在内部可用。 call
甚至必须将该值作为 return 地址压入堆栈。
流水线 RISC 或完全非流水线 CPU 也将使用该下一条指令地址从内存或 I-cache 中获取下一条指令。但实际上 8086 预取是异步进入一个小的预取缓冲区。机器代码格式主要是在实现设计之前在纸上设计的,因此将事物与指令末尾相关联的这种常见原因可能是架构师的想法。
相对于指令末尾进行分支是许多 ISA 的常见设计选择。
重申一下,我只谈论 8086 的原因(它在内部 非常 不同于现代 x86)是因为它是 第一个 gen,理解它有助于解释一些机器代码设计决策。 (例如,为什么 x86 在单字节 xchg [e/r]ax, reg
上花费 8 个操作码:因为 8086 没有 movsx
或 2 操作数 imul
,并且很多东西需要或想要 AX。也代码大小是性能的主要瓶颈。)
现代 x86 只跟踪每条指令的地址,并可在解码时使用它 call rel32
。没什么大不了的。