如何计算跳转目标地址和分支目标地址?

36
我对汇编语言还很陌生。我正在阅读关于MIPS架构的资料,但是我对跳转目标地址和分支目标地址以及如何计算它们感到困惑。
4个回答

80
在下面的图表和文本中,PC是分支指令本身的地址。PC+4是分支指令本身的结束,也是下一条指令的开始(在使用延迟槽的MIPS中,是分支延迟槽)。
除了在绝对跳转图中,我们实际上获取跳转后指令地址的高4位。这只在256 MiB区域的末尾有所不同。这些图表在称呼PC方面是不一致的,或者可能是在简化时创建的,即PC+4的前4位与PC相同。

1. 分支地址计算

在MIPS中,分支指令只有16位的偏移量来确定下一条指令。我们需要一个寄存器将这16位值加到下一条指令上,而这个寄存器实际上是由体系结构隐含的。这个寄存器就是PC寄存器,因为在取指周期中PC会更新为(PC+4),所以它保存了下一条指令的地址。
我们还将分支距离限制在-2^15到+2^15-1条指令之间。然而,这并不是真正的问题,因为大多数分支都是局部的。
所以一步一步来:
1. 将16位的偏移值进行符号扩展,以保留其值。 2. 将结果乘以4。这样做的原因是,如果我们要跳转到某个地址,并且PC已经按字对齐,那么立即值也必须按字对齐。然而,将立即值按字对齐是没有意义的,因为我们会浪费低两位,强制它们为00。 3. 现在我们有了一个32位的相对偏移量。将这个值加上PC + 4,就得到了你的跳转地址。

Branch address calculation


2. 跳转地址计算

对于跳转指令,MIPS只有26位来确定跳转位置。在MIPS中,跳转是相对于PC的。与分支指令类似,立即跳转值需要对齐到字边界;因此,我们需要将26位地址乘以四。

再次逐步说明:

  • 将26位值乘以4。
  • 由于我们是相对于PC+4值进行跳转,将PC+4值的前四位连接到我们的跳转地址的左侧。
  • 得到的地址即为跳转值。

换句话说,用取指令的低26位左移两位的结果替换PC+4的低28位。

enter image description here

跳转是在包含分支延迟槽的区域内进行的,而不一定是分支本身。在上图中,PC已经在跳转计算之前进入了分支延迟槽。(在经典的RISC 5级流水线中,分支延迟槽在跳转解码的同一个周期中被获取,因此PC+4的下一条指令地址对于跳转和分支来说已经可用,如果相对于跳转自身的地址进行计算,就需要额外的工作来保存该地址。)
来源:Bilkent大学CS 224课程幻灯片

为什么要“将PC值的前四位连接到跳转地址的左侧”? - NoName
请检查@user786653的答案。他更详细地解释了相对于PC值跳转的概念。 - emre nevayeshirazi
6
我找到了一份更详细的回答来回应我的评论,我将把它放在这里:跳转指令的立即数乘以4得到的伪直接寻址可以达到2^28个地址。它是伪直接寻址,因为在32位MIPS系统中有2^32个地址。我们需要将PC + 4的四个最高有效位添加到28位的伪直接地址上,才能获得32位的直接地址。这意味着只有可能跳转到4个最高有效位所在的地址范围内的指令,而这些指令需要与跳转指令在同一个2^28个地址的范围内。 - NoName
为什么我们计算“jumb”的新地址时需要替换PC的26位?我认为我们可以将bne/beq视为“相对偏移量”,通过相同的方式进行操作。为什么要用新值替换PC的整个值呢?同样地,当计算beq/bnq的地址时,我们可以使用绝对值替换指令中的值。 - Mohamed
@Mohamed:MIPS本可以像b一样设计j是相对的,但它没有。这是一个相当任意的选择,尽管从现代的角度来看,这是一个不幸的选择,因为位置无关代码不能使用j,而b的范围有限。 - Peter Cordes

21
通常情况下,您不必担心计算它们,因为您的汇编程序(或链接器)将确保正确计算。假设您有一个小函数:

func:
  slti $t0, $a0, 2
  beq $t0, $zero, cont
  ori $v0, $zero, 1
  jr $ra
cont:
  ...
  jal func
  ... 

当将上述代码转换为指令的二进制流时,汇编器(或链接器,如果您首先将其组装为对象文件)将确定函数在内存中的位置(现在先忽略位置无关代码)。它通常在ABI中指定,在使用模拟器(如SPIM,它将代码加载到0x400000 - 请注意,该链接还包含了一个很好的解释过程)时也会给出。
假设我们正在讨论SPIM情况,并且我们的函数是内存中的第一个,那么slti指令将位于0x400000beq指令将位于0x400004等等。现在我们就快完成了!对于beq指令,分支目标地址cont的地址(0x400010),查看MIPS instruction reference,我们可以看到它被编码为相对于下一条指令的16位有符号立即数(除以4,因为所有指令都必须驻留在4字节对齐的地址上)。
Current address of instruction + 4 = 0x400004 + 4 = 0x400008
Branch target = 0x400010
Difference = 0x400010 - 0x400008 = 0x8
To encode = Difference / 4 = 0x8 / 4 = 0x2 = 0b10

beq $t0, $zero, cont的编码

0001 00ss ssst tttt iiii iiii iiii iiii
---------------------------------------
0001 0001 0000 0000 0000 0000 0000 0010

正如您所见,您可以在-0x1fffc .. 0x20000字节范围内分支。如果由于某种原因,您需要跳转更远,您可以使用一个跳板(一个无条件跳转到实际目标的跳转,该跳转位于给定限制内)。

跳转目标地址与分支目标地址不同,它们使用绝对地址编码(再次除以4)。由于指令编码使用6位用于操作码,这只留下26位用于地址(实际上是28位,因为最后2位将为0),因此PC寄存器的4个最高有效位在形成地址时被使用(除非您打算跨越256 MB边界,否则不会有影响)。

回到上面的例子,jal func的编码为:

Destination address = absolute address of func = 0x400000
Divided by 4 = 0x400000 / 4 = 0x100000
Lower 26 bits = 0x100000 & 0x03ffffff = 0x100000 = 0b100000000000000000000

0000 11ii iiii iiii iiii iiii iiii iiii
---------------------------------------
0000 1100 0001 0000 0000 0000 0000 0000

你可以使用我找到的在线MIPS汇编器快速验证这一点,并尝试不同的指令(请注意,它不支持所有操作码,例如slti,因此我在这里将其更改为slt):
00400000: <func>    ; <input:0> func:
00400000: 0000002a  ; <input:1> slt $t0, $a0, 2
00400004: 11000002  ; <input:2> beq $t0, $zero, cont
00400008: 34020001  ; <input:3> ori $v0, $zero, 1
0040000c: 03e00008  ; <input:4> jr $ra
00400010: <cont>    ; <input:5> cont:
00400010: 0c100000  ; <input:7> jal func

3
嘿,你找到了我的在线 MIPS 汇编器 :) - Alan H.
4
如果有人对添加指令(如 slti)感兴趣,可以随时向 https://github.com/alanhogan/online-mips-assembler 提交拉取请求。请注意,翻译尽力保持内容原意,同时使语言更加通俗易懂,不包含解释或其他额外信息。 - Alan H.

0

对于像这样的小函数,您可以手动计算从分支指令下面的指令到目标的跳数。如果它向后分支,则将该跳数变为负数。如果该数字不需要所有16位,则对于跳数最高有效位左侧的每个数字,使它们成为1,如果跳数为正,则使它们全部为0。由于大多数分支都靠近它们的目标,因此在大多数情况下,这可以节省您很多额外的算术运算。

  • 克里斯

0

我认为计算这些会相当困难,因为分支目标地址是在运行时确定的,并且该预测是由硬件完成的。如果您能更深入地解释问题并描述您正在尝试做什么,那么帮助您会更容易一些。(:


MIPS分支指令都有一个立即目标(相对于当前PC的位移)。以寄存器源为目标地址的控制流指令是jr。除非你很傻并将跳转到下一条指令视为未被执行情况的目标地址。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接