【问题标题】:Does Program Counter hold current address or the address of the next instruction?程序计数器保存当前地址还是下一条指令的地址?
【发布时间】:2019-01-31 19:38:57
【问题描述】:

作为初学者和自学者,我正在学习汇编,目前正在阅读 本书的第 3 章,Allen Hollub 的 The C Companion。我无法理解 程序计数器或 PC 的描述,他在一个带有两个字节字的假想演示机器中描述。这是第57页对PC的描述。

“PC 始终保存当前正在执行的指令的地址。 它会随着每条指令的执行而自动更新以保存地址 下一条要执行的指令。 ... ... 这里的重要概念是 PC 持有 下一个 指令的地址,而不是指令本身。 "

我不明白持有当前地址和下一个指令地址之间的区别。 PC 是否同时将两个地址保存在两个连续字节中?

【问题讨论】:

  • 这在很大程度上取决于 CPU 的实现,有的在指令执行开始时递增对应于程序计数器的内部寄存器,有的在指令执行结束时递增。然而,对于大多数现代 CPU,两者都不是真的。它们没有一个您可以指向并说它是程序计数器的内部寄存器,而它只是架构状态的概念部分。看到这个答案:stackoverflow.com/questions/51942523/…
  • 在执行一条指令之前,它必须首先从内存中读取。读取它将增加指令计数器。这通常只对计算调用或跳转位置的偏移量很重要,汇编器会处理这个细节。
  • @HansPassant:这个问题与 x86 无关。在 x86 中,IP / EIP / RIP 在执行当前指令时逻辑上保存 next 指令的地址。但这不是本书作者描述他们论文架构的方式。拥有一台保存当前指令地址的 PC 是一种有效的设计。对于 OoO / 流水线设计,它没有真正的区别。对于具有单个物理 PC 寄存器的简单顺序,这意味着取指逻辑需要计算下一个 PC,否则在执行当前指令时甚至无法提取下一条指令。
  • @Peter Cordes,这台演示机器基于松散的 68000 和 PDP11。谢谢。
  • 当我发布该评论时,我的答案写了一半,马丁发布了他的答案。我终于开始完成我的答案,包括扩展该评论​​的部分。

标签: assembly cpu-architecture


【解决方案1】:

我无法理解他在一个假想的带有两个字节字的演示机中描述的程序计数器或 PC 的描述。

他正在描述一个简单的 CPU,它解释了 CPU 的一般工作原理

真正的 CPU 要复杂得多:

在许多手册(适用于任何类型的 CPU)中,您会发现类似这样的句子:“PC 寄存器被压入堆栈。”

这通常意味着从call 指令返回后执行的指令的地址被压入堆栈。

然而这样的句子并不是100%正确:在68k CPU的情况下(见下文)写的是下一条指令的地址,而不是当前指令的指令加2!

对于大多数 CPU,PC 相关的jump 指令是相对于下一条指令的地址;但是也有反例(例如 PowerPC VLE)。

32 位 x86 CPU(用于大多数台式机/笔记本电脑)

在这样的CPU上,只有calldirectly reads the EIP register,而且只有跳转指令写EIP。这是足够的“绝缘”,这个寄存器是 CPU 中的一些内部电路,如果有物理 EIP 寄存器的话,你不一定知道它的内容。

(您也可以将int 指令如int3int 0x80 视为读取CS:EIP,因为它们必须推送异常帧。但将它们视为触发异常更有意义-搬运机械。

很可能不同的 x86 CPU 在内部工作方式不同,因此 EIP“寄存器”的实际内容在不同的 CPU 中是不同的。 (而且现代高性能实现不会只有一个 EIP 寄存器,但它们会做任何必要的事情来保持错觉并在需要时推送正确的返回地址。)

(PC相对跳转是相对于下一条指令的地址。)

64 位 x86 CPU

这些 CPU 具有直接使用 RIP 寄存器的指令,例如 mov eax,[rip+symbol_offset] 执行相对于 PC 的静态数据加载;使共享库和 ASLR 的位置无关代码比 32 位 x86 更高效。在这种情况下,“RIP”是下一条指令的地址。

68k

这些 CPU 也有可能直接使用 PC 寄存器的内容。在这种情况下,PC 反映了当前指令的地址加上 2(我在这里不确定)。

因为这样的指令至少有 4 个字节长,所以 PC 寄存器的值将反映指令“中间”字节的地址。

ARM

在 ARM CPU 上读取 PC 时(可以直接读取!),该值通常反映当前指令的地址加 8,在某些情况下甚至加 12!

(指令长4个字节,所以“当前指令加8”的意思是:前面两条指令的地址!)

【讨论】:

  • mov eax,[rip] 加载下一条指令的 4 个字节。我认为您的意思是lea rax, [rip] 只是读取 RIP 而不是取消引用它。 32位x86有call,它推送当前IP/EIP/RIP作为返回地址,为documented that way。所以 x86 确实有 PC=next insn。 Reading program counter directly。有关 32 位 ARM 如何将 PC 公开为 16 个通用寄存器之一的更多信息,另请参阅 Why can't you set the instruction pointer directly?
  • @PeterCordes 感谢您的评论。我想写“使用 RIP 寄存器”。我更正了。
【解决方案2】:

这些声明可能涉及两个不同的时间点,指令执行之后。

您忽略的[...] 中有什么内容?它是否谈到了在将 PC 增加 2 个字节/1 个指令字之后完成一条指令的执行并开始获取下一条指令?

否则这就是书中的错误,因为这两个声明(PC 指向当前指令与当前指令执行期间的下一条指令)不兼容。

我没看懂持有当前地址和下一条指令地址的区别

考虑内存中的这些 (x86) 指令,使用 2 字节指令来匹配您书中的 ISA(x86 指令是 1 到 15 个字节的可变长度,包括可选/强制前缀字节):

 a:  0x66 0x90     nop
 c:  0x66 0x90     nop

每条指令都有自己的地址。我已经用十六进制数字表示了它们的起始地址(这也可以是汇编语法中的符号标签,但这旨在成为反汇编输出的模型,例如objdump -d)。 “一条指令的地址”是它在内存中的第一个字节的地址,不管架构 PC 在执行它之前/期间/之后会保存什么。

在执行第一条nop 时,下一条指令的地址是c。 “当前指令”是第一条nop,无论 PC(逻辑上)在执行时具有什么值。


大多数指令实际上并不将 PC 读取为数据输入。只有相对跳转和 PC 相对加载/存储需要它。 (因此编译器/汇编器需要知道计算相对位移的规则。)

MIPS 和 RISC-V 也/相反有 aupc 指令,将寄存器或立即数添加到程序计数器,并将结果放入另一个寄存器。因此,它们不是 PC 相对寻址模式,而是 PC 相对 add,以生成可用作寻址模式的指针。但确实是一样的区别。

只要在指令执行过程中PC的逻辑值有一个一致的规则,确切的规则是什么并不重要。

  • PC = 当前指令的开始(例如 MIPS 在逻辑上以这种方式工作,无论内部实现实际做什么)。

    MIPS 相关分支是relative to PC + 4(即相对于下一条指令,因此为此目的,它只是记录方式的一个怪癖),但 MIPS 跳转取代了 PC 的低 28 位,而不是 PC+4 的低 28 位(这可能不同之处在于其高位)。另请参阅http://www.cim.mcgill.ca/~langer/273/13-datapath1.pdf,它介绍了在 MIPS 上取指/执行的逻辑操作。)

  • PC = 下一条指令的开始(常见,例如 x86)

  • PC = 稍后开始 2 条指令。 (例如 ARM)

    Why does the ARM PC register point to the instruction after the next one to be executed? TL:DR:早期 ARM 设计中的 3 阶段提取-解码-执行流水线前端的产物。 (32 位 ARM 将程序计数器公开为 r15,这是 16 个“通用”寄存器之一,因此您实际上可以使用 or pc, r0, #4 或其他东西跳转,也可以在任何指令中读取它以进行 PC 相对寻址) .

正如@Ross 所说,只有一个简单的非流水线 CPU 才会有一个物理程序计数器寄存器。 (How does branch prediction interact with the instruction pointer)。

但如果任何指令引发异常(错误),它通常需要在某处存储错误指令的地址或下一条指令的地址 .这取决于它是哪种异常。调试/单步异常将存储下一条指令的地址,因此从异常处理程序返回将单步执行。页面错误将存储错误指令的地址,因此默认操作是重试它。

异常处理规则将与正常的 PC-during-execution 规则分开,因此硬件必须记住指令长度或指令起始地址才能处理异常。它不必高效,因为中断/异常很少见; CPU 在跳转到中断处理程序之前需要多个周期是可以的。 (PC 相对寻址模式和call 指令的正常操作情况确实必须高效。)


使用 PC=当前指令的简单物理实现的含义

拥有保存当前指令地址的 PC 是一种有效的设计。

对于超标量流水线设计,尤其是乱序执行,它没有真正的区别。流水线需要在每条指令通过流水线时跟踪其地址(以及长度,如果可变),因为它每个周期可以获取/解码/执行超过 1 条指令。它提取大块,并从该块中解码多达n 指令。例如,某些实现可能要求获取块是 16 字节对齐的。 (请参阅https://agner.org/optimize/,了解有关各种 x86 微架构如何做到这一点的详细信息,以及如何针对 Pentium、Pentium Pro、Nehalem 等中的前端获取/解码模式进行优化。幸运的是,现代 x86 CPU 具有解码的 uop 缓存并且很多对循环中的获取/解码问题不太敏感。)

(半相关:x86 registers: MBR/MDR and instruction registersmodern)

对于具有单个物理 PC 寄存器的简单有序非流水线 CPU,这意味着取指逻辑需要计算下一个 PC,否则下一条指令可以t 甚至在执行当前时被获取。

在 x86 中,IP / EIP / RIP 在执行当前指令时,逻辑上保存了 next 指令的地址。这是有道理的,因为它起源于 8086,它只有大约 29k 个晶体管。它在执行当前 insn 时从指令流中预取(到一个小的 6 字节缓冲区,如果使用额外的前缀,它甚至不足以容纳整条指令,但它容纳 6 个单字节指令)。但它甚至没有开始解码下一个,直到当前一个完成。 (即根本没有流水线,或者可以说是 2 阶段,如果您计算预取,这很容易解耦。我认为这种情况一直持续到 486 年。)

对于可变长度的 ISA,指令长度在解码之前不会被发现。让 PC = 当前指令的结束可能更重要,因为您不能像 MIPS 那样只计算 PC+4,或者用您的玩具 ISA 计算 PC+2。但是你也不能倒退,除非你知道指令长度,所以要正确处理异常 8086 必须也跟踪指令开始,或者记住指令长度。

【讨论】:

  • 8086 和 80186 都是 2 阶段流水线:一个 fetch 阶段和一个 execute 阶段,两者都可以在同一个周期中运行。架构寄存器与物理寄存器相同,包括 IP。我读过多本声称第一个流水线英特尔处理器是 80486 的书。我不知道他们从哪里得到的。有很多资源可以讨论两级 8086 流水线和后续设计。我不知道任何没有流水线的微处理器。
  • @HadiBrais:我认为人们认为预取是如此明显和容易,以至于它甚至不能算作流水线,即使它确实如此。我的理解是,真正的根本区别在于当您流水线甚至解码时,因为那时微编码的内部实现不能以相同的方式工作,其中解码和操作数获取过程可能使用一些与执行过程相同的加法器。
  • 是的,我想是的。虽然它在技术上是一个管道阶段。即使是现在,在每本教科书和论文中,他们都在谈论代码获取阶段。 IIRC,80286 有 4 个管道级,这是有道理的,因为它引入了保护模式的东西并具有更高的频率。
  • @PeterCordes 这是我没有提到的缺失行。 "... to be executed. In our demo machine, all instructions occupy exactly 2 bytes, so the PC is normally incremented by two with every instruction. The important concept here... ." 这是我也没有写的最后一行。 “When an instruction is executed, the machine first fetches the actual instruction from the indicated address, and then execute it”。你猜对了,正如你所说的这两件事是不相容的,这对我帮助很大,因为我至少在思考正确的方向。
【解决方案3】:

真正的指令集,但没关系,并且对这个真正的指令如何工作不感兴趣,它将用于演示问题。

2000: 0b 12        push r11     
2002: 3b 40 21 00  mov #33, r11
2006: 3b 41        pop r11      
2008: 30 41        ret  

正如前面提到的,在谈论程序计数器时有一个时间概念。

超级简单的处理器,老的8位,其他的都可以这样想,新的就不一样了。

当我们输入此代码时,无论我们到达这里,都没有关系。程序计数器 是 0x2000。这告诉我们从哪里获取指令,我们必须获取它,解码它,然后执行它,重复。

这些是 16 位指令,两个字节,处理器从 pc 指向指令开始获取指令的地址。处理器读取地址 0x2000 (0x0b) 的两个字节,处理器将程序计数器递增到 0x2001,并使用它来获取地址 0x2001 (0x12) 处的指令的后半部分,并将程序计数器递增到 0x2002。因此,对于这里的每个提取,我们将其称为组成处理器,我正在描述您使用程序计数器作为地址提取的每个提取,然后递增程序计数器。

before data after
0x2000 0x0b 0x2001
0x2001 0x12 0x2002

所以现在我们解码指令,程序计数器当前显示0x2002,我们看到这是一个push r11,所以我们继续执行。

在执行该指令期间,程序计数器保持为 0x2002。寄存器 r11 的值被压入堆栈。

现在我们开始获取下一条指令。

before data after
0x2002 0x3b 0x2003
0x2003 0x40 0x2004

当我们解码这条指令 (pc == 0x2004) mov #immediate,r11 时,处理器意识到这条指令需要一个立即数,因此它需要再获取两个字节

before data after
0x2004 0x21 0x2005
0x2005 0x00 0x2006

它已经确定它现在可以通过将值 0x0021 写入寄存器 r11 来执行指令(小端序 0x0021 = 33decimal)。在执行期间,该指令的程序计数器为 0x2006。

下一个

before data after
0x2006 0x3b 0x2007
0x2007 0x41 0x2008

解码并执行 pop r11

因此您可以开始看到程序计数器实际上包含至少两个值。在获取之前的指令开始时,它包含指令的地址,在我们开始执行之前获取和解码之后,它包含该指令之后的字节的地址,如果这不是跳转,则是另一条指令。如果这是一个无条件跳转 字节可能是一条指令或一些数据,或未使用的内存。但是我们说 它“指向下一条指令”在这种情况下意味着在执行之前 该指令后面的地址通常有另一条指令。但正如我们接下来将看到的,PC 可以通过指令进行修改。但总是在END 它指向的执行(对于这个简单的处理器,它类似于 一些简单的 8 位处理器)到要执行的下一条指令。

最后

before data after
0x2008 0x30 0x2009
0x2009 0x41 0x200A

解码一个 ret,现在这个问题就问题而言是特殊的,因为 ret 将在执行期间根据该处理器的规则修改程序计数器。如果调用地址 0x2000 的指令是 0x1000 并且它是一个两字节指令,那么在获取和解码期间程序计数器将位于地址 0x1002,在执行期间,地址 0x1002 将根据该指令集的规则存储在某处,并且程序计数器将采用值 0x2000 来调用此子例程。当我们到达 ret 指令并开始执行它时,我们开始执行 ret,程序计数器包含 0x200A 但 ret 将指令的地址放在调用之后,即在调用执行期间存储的值,所以在在这条指令的结尾,程序计数器将包含值 0x1002,下一次取指将来自该地址。

因此,在执行前的最后一条指令中,pc 指向的内容 通常是不分支或跳转的指令的下一条指令或 称呼。 0x200A。但是在执行期间程序计数器已​​更改,因此 “下一条”指令是我们到达这里的呼叫之后的指令。

更多

c064:   0a 24           jz  $+22        ;abs 0xc07a
c066:   4e 5e           rla.b   r14     

在获取 pc 之前是 0xC064。获取和解码后,PC 为 0xC066。指令说如果 zerp 跳转到 0xc07a。因此,如果未设置零标志,则 pc 将停留在 0xC066,这是它开始下一条指令的地方,但如果 z 是 设置然后 pc 被修改为 0xc07a ,这是下一条指令 执行将。所以在 0xc064 之前 0xc066 或 0xc07a 之后取决于。

一条指令的后面是下一条指令的前面。

无条件跳转

c074:   c2 4d 21 00     mov.b   r13,    &0x0021 
c078:   ee 3f           jmp $-34        ;abs 0xc056

在获取0xc07a之前,执行之前0xc07A之后执行0xc056

对于那条指令,pc 至少保存了三个值(如果获取一个字节 一次然后它保持 0xc078、0xc079、0xc07a 并以 0xc056 结束)期间 一条指令。

是的,它可以并且确实保存多个值,但不能同时保存一个值,在指令的各个阶段一次一个值。

【讨论】:

    【解决方案4】:

    最初,PC(寄存器)保存当前值,但随着时钟信号的变化,它变为 PC(上一个地址 + 值),并且它将包含相同的值直到下一个时钟周期,并且在添加值之后它将存储寄存器中的地址。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-10-22
      • 1970-01-01
      • 1970-01-01
      • 2021-08-22
      • 2014-03-05
      • 2018-05-07
      相关资源
      最近更新 更多