“PC始终保存当前正在执行的指令的地址。每执行一条指令时,它会自动更新以保存下一条要执行的指令的地址。 ... ... 这里的重要概念是,PC保存的是下一条指令的地址,而不是指令本身。”
我无法理解保存“当前”地址和“下一条”指令地址之间的区别。PC是否同时保存两个连续字节中的两个地址?
call
指令返回后执行的指令的地址被推送到堆栈上。jump
指令是相对于下一条指令的地址;但是也有反例(例如PowerPC VLE)。
32位x86 CPU(用于大多数台式机/笔记本电脑)call
直接读取EIP寄存器,只有跳转指令才会写入EIP。这足以使该寄存器成为CPU中的一些内部电路,如果有物理EIP寄存器的话,您也不一定知道它的内容。
(您可以将像int3
或int 0x80
这样的int
指令视为读取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的地址(我在这里不是绝对确定)。mov eax,[rip]
载入下一条指令的4个字节。我认为你的意思是 lea rax, [rip]
,它只是 读取 RIP 而不是间接引用它。32位x86有 call
,它将当前的IP/EIP/RIP作为返回地址推送,并且文档中有这样的说明。因此,x86具有PC=next insn。直接读取程序计数器。另请参阅为什么不能直接设置指令指针?,了解更多关于32位ARM如何将PC公开为16个通用寄存器之一的信息。 - Peter Cordes考虑内存中的这些(x86)指令,使用2字节指令来匹配您的书中的ISA(x86指令长度可变,从1到15个字节,包括可选/强制前缀字节)。我不明白持有当前地址和下一条指令地址之间的区别
a: 0x66 0x90 nop
c: 0x66 0x90 nop
objdump -d
)。"指令的地址"是其在内存中第一个字节的地址,不管执行它之前/期间/之后体系结构PC将保留什么值。nop
正在执行时,下一条指令的地址是c
。"当前指令"是第一个nop
,无论它执行时逻辑上的PC值是多少。
aupc
指令,将寄存器或立即数加到程序计数器中,并将结果放入另一个寄存器。所以他们有一个基于 PC 的 add
,来产生一个可以用作寻址模式的指针。但实际上并没有区别。只要在指令执行期间 PC 的逻辑值有一致的规则,确切的规则并不重要。
PC = 当前指令的起始位置(例如,MIPS在逻辑上是这样工作的,而不管实际内部实现如何)。
MIPS相对分支是相对于PC + 4
(即相对于下一条指令,因此在这个目的上它只是文档中的一个怪癖),但MIPS跳转替换PC的低28位,而不是PC+4的低28位(其高位潜在地不同)。另请参见http://www.cim.mcgill.ca/~langer/273/13-datapath1.pdf,其中介绍了MIPS指令获取/执行的逻辑操作。
PC = 下一条指令的起始位置(常见,例如x86)
PC = 两条指令后的起始位置(例如ARM)
为什么ARM PC寄存器指向要执行的下下条指令? 简而言之:早期ARM设计中三级取指-译码-执行流水线前端的产物。(32位ARM将程序计数器公开为r15
,其中之一为16个“通用”寄存器,因此您实际上可以使用or pc,r0,#4
或类似方法进行跳转,以及在任何指令中读取它进行PC相对寻址)。
call
指令的正常操作情况必须高效。)
拥有一个保存当前指令地址的PC是一种有效的设计。
对于超标量流水线设计,特别是乱序执行,这并没有什么真正的区别。流水线需要跟踪每个指令的地址(以及长度,如果可变),因为它可以在一个时钟周期内获取/解码/执行多个指令。它会获取大块数据,并从该块数据中解码出多达n
条指令。例如,一些实现可能需要获取块对齐到16字节。(请参阅https://agner.org/optimize/了解各种x86微体系结构如何处理此问题,以及如何优化Pentium、Pentium Pro、Nehalem等处理器的前端获取/解码模式。幸运的是,现代x86 CPU具有解码-uop缓存,对于循环中的获取/解码问题的敏感度要小得多。)
(半相关:x86寄存器:MBR / MDR和指令寄存器现代)
对于一个简单的顺序非流水线CPU,只有一个物理PC寄存器,这意味着指令获取逻辑需要计算下一个PC,否则下一个指令甚至在执行当前指令时都无法获取。
在x86中,IP / EIP / RIP在当前指令执行时逻辑上保留了下一条指令的地址。这是有道理的,因为它起源于8086,该处理器只有约29k个晶体管。它从指令流中预取指令,而当前指令正在执行(预取到一个小的6字节缓冲区中,该缓冲区甚至不足以容纳一个带有额外前缀的完整指令,但能够容纳6个单字节指令)。但是,它甚至直到当前指令完成后才开始解码下一条指令。(即根本没有流水线化,或者如果将预取与执行分开计数,则可以认为是2级。我认为这一情况一直持续到486)
使用可变长度的ISA,指令长度要在解码时才能确定。将PC设置为当前指令的结尾可能更加重要,因为你不能像MIPS那样简单地计算PC+4,或者像你的玩具ISA那样计算PC+2。但是,如果不知道指令长度,也无法向后跳转,因此为了正确处理异常,8086必须跟踪指令的起始位置,或记住指令的长度。
"...要执行。在我们的演示机器中,所有指令占用正好2个字节,因此PC通常会随着每个指令增加两个。这里的重要概念是...。"
这是我也没有写的最后一行。"当执行指令时,机器首先从指定地址获取实际指令,然后执行它
"。你猜对了,正如你所说的两件事不兼容,这帮助了我很多,因为我至少朝着正确的方向思考。 - Bishnu这是一组真实的指令集,但这并不重要,我对这些指令如何工作不感兴趣——它将用于展示问题。
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位指令,即2个字节,处理器从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 = 33十进制)。 在执行过程中,该指令的程序计数器为0x2006。
下一步
before data after
0x2006 0x3b 0x2007
0x2007 0x41 0x2008
解码并执行一个pop r11指令。
从上面可以看出程序计数器实际上包含至少两个值。在获取指令之前,它包含指令的地址;在获取和解码指令后,在我们开始执行之前,它包含此指令后面一个字节的地址,如果这不是跳转,则为另一个指令。如果这是无条件跳转,那么该字节可能是指令、一些数据或未使用的内存。但我们说它“指向下一条指令”,在这种情况下意味着在执行之前,通常有另一个指令的地址在此指令之后。但是,接下来我们将看到pc可以被指令修改。但在执行结束时,它始终(对于这个类似于许多简单的8位处理器的简单虚构处理器)指向要执行的下一条指令。
最后
before data after
0x2008 0x30 0x2009
0x2009 0x41 0x200A
解码ret指令,这一条指令在该处理器规则下执行时会修改程序计数器。如果调用地址0x2000的指令为0x1000,并且是一个两字节指令,则在获取和解码过程中程序计数器将在地址0x1002处。执行期间,地址0x1002会根据指令集规则存储在某个位置,程序计数器将取值0x2000以调用此子例程。当我们到达ret指令并开始执行它时,程序计数器包含0x200A,但ret指令会将调用后的下一条指令地址(即在调用过程中存储的值)放在程序计数器中,因此在这条指令结束时,程序计数器将包含值0x1002,下一次获取将从该地址进行。
因此,在执行最后一条指令之前,程序计数器指向通常是不分支或跳转或调用的指令的下一条指令。也就是0x200A。但是在执行期间,程序计数器已更改,以使“下一个”指令是将我们带到此处的调用后的指令。
还有一些内容。
c064: 0a 24 jz $+22 ;abs 0xc07a
c066: 4e 5e rla.b r14
在获取指令之前,程序计数器为0xC064。在获取并解码指令后,程序计数器变为0xC066。该指令表示如果零标志位为零,则跳转到地址0xc07a。因此,如果零标志未设置,则程序计数器保持在0xC066处,并从那里开始执行下一条指令,但如果Z标志被设置,则程序计数器将修改为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结束)。
是的,它可以并且确实会保持多个值,但不会同时保持,每个阶段在执行时都只有一个值。
最初,PC(寄存器)保持当前值,但随着时钟信号的变化,它会改为PC(以前的地址+值),并且它将包含相同的值,直到下一个时钟周期,并在添加值后将地址存储在寄存器中。