为什么 x86 跳转/调用指令使用相对位移而不是绝对目标?

Alb*_*ert 4 x86 assembly cpu-architecture x86-16

我正在学习 8086,有一个特别的问题困扰着我,我还没有找到任何令人满意的答案。

我知道 CPU 按顺序执行代码,如果想要更改代码流,我们希望 IP 指向我们感兴趣的代码所在的新/旧地址。

现在,我的问题是为什么我们(我的意思是 CPU)在遇到跳转指令时不只是使用与标签对应的地址来更新 IP?

当我们遇到跳转指令时,需要在IP中添加位移吗?

在我看来

  1. 计算位移(即跳转标签到跳转后下一条指令的距离)和
  2. 然后取那个位移 2 的恭维,
  3. 最终被添加到 IP 中,以便 IP 指向标签指向的地址/指令

对我来说,这听起来像是更多的工作,然后只需使用与标签对应的地址更新 IP。但是,我确信事情的处理方式一定是有原因的,只是我不知道。

在 8086 中选择这种设计的原因是什么?

Pet*_*des 6

您大大高估了解码相对跳跃的 CPU 复杂性成本。

  1. 计算位移(即跳转标签到跳转后下一条指令的距离)
  2. 然后取那个位移 2 的恭维,

机器码必须包含第 2 步的结果(有符号整数相对位移),因此所有这些都在汇编时完成。在汇编程序中,减去两个整数地址已经为您提供了所需的有符号 2 的补码位移。

使用相对位移确实有好处,因此仅仅为了简化编写汇编程序而使 ISA 变得更糟是没有任何意义的。您只需要编写一次汇编程序,但在机器上运行的所有内容都受益于更紧凑的代码和位置独立性。

相对分支位移是完全正常的,也用于大多数其他架构(例如 ARM:https : //community.arm.com/processors/b/blog/posts/branch-and-call-sequences-explained,其中固定 -宽度指令无论如何都无法进行直接的绝对分支编码)。 它会使 8086 成为使用相对分支编码的奇数。

更新:也许不完全是奇怪的。MIPSrel16 << 2用于beq/ bne(MIPS 指令固定为 32 位宽且始终对齐)。但是对于无条件j(跳转)指令,有趣的是它使用了伪直接编码。保留PC的高4位,直接用PC[27:2]指令中编码的值替换这些位。(同样,程序计数器的低 2 位始终为0。)因此,在地址空间的相同 1/16 内,j指令是直接跳转,并且不会为您提供与位置无关的代码。这适用于jal(jump-and-link = call),使从 PIC 代码调用函数的效率降低:( Linux-MIPS 过去需要 PIC 二进制文件,但显然现在不是(但共享库仍然必须是 PIC)。


当 CPU 运行时eb fe,它所要做的就是将位移添加到IP而不是替换IP。由于非跳转指令已经IP通过添加指令长度进行更新,因此加法器硬件已经存在。

请注意,8 位位移符号扩展到 16 位(或 32 位或 64 位)在硬件中是微不足道的:2 的补码符号扩展只是复制符号位,它不需要任何逻辑门,只需连接到将一点连接到其余部分。(例如 0xfe变成0xfffe,而0x05变成0x0005。)


8086 非常重视代码密度,提供了许多常用指令的简短形式。这是有道理的,因为代码获取是 8086 上最重要的瓶颈之一,所以较小的代码通常是更快的代码。

例如,jmp存在两种形式的相对,一种是 rel8(短),一种是 rel16(近)。(在后来的 CPU 中引入的 32 位和 64 位模式中,E9操作码是一个jmp rel32而不是rel16,但EB仍然jmp rel8是因为函数内的跳转通常在 -128/+127 之内)。

但是 for 并没有特别的缩写call,因为它在大多数情况下用处不大。那么为什么它仍然需要相对位移而不是绝对位移呢?

x86 确实有绝对跳转,但仅适用于间接或跳转。(到不同的代码段)。例如,EA操作码是jmp ptr16:16:“跳远,绝对,操作数中给出的地址”。

要进行绝对近跳,只需mov ax, target_label/ jmp ax。(或在 MASM 语法中,mov ax, OFFSET target_label)。


相对位移与位置无关

对该问题的评论提出了这一点。

考虑一个机器代码块(已经组装),在块内有一些跳转。如果您将整个块复制到不同的起始地址(或更改CS基地址,以便可以在段的不同偏移量处访问同一块),那么只有相对跳转会继续工作。

对于标签 + 绝对地址来解决同样的问题,必须使用不同的ORG指令重新组装代码。显然,当您使用远 jmp 更改 CS 时,这不会立即发生!