作为一个初学者和自学者,我正在学习汇编,并且目前正在阅读 该书的第3章,Allen Hollub撰写的C伴侣。我不明白 他在一个具有两个字节字的虚拟演示机中描述的程序计数器或PC的描述。这是第57页中PC的描述。
“ PC始终保存正在执行的指令当前的地址。 它会在执行每条指令以保存地址时自动更新 下一条要执行的指令。 ... ... 这里的重要概念是PC保留 next 指令的地址,而不是指令本身。 “
我不明白保存当前地址和下一个指令的地址之间的区别。 PC是否同时以两个连续字节保存两个地址?
答案 0 :(得分:4)
我无法理解他在虚构的演示机器中用两个字节字描述的程序计数器或PC的描述。
他正在描述一个简单的CPU,它解释了 CPU的总体工作方式。
实际CPU 要复杂得多:
在许多手册(适用于任何类型的CPU)中,您都会看到类似以下语句:“ PC寄存器被压入堆栈。”
这通常意味着将从call
指令返回后执行的指令的地址压入堆栈。
但是这些句子不是100%正确的:对于68k CPU(见下文),将写入下一条指令的地址,而不是当前指令的指令加2 !!
对于大多数CPU,与PC相关的jump
指令是相对于下一条指令的地址的;但是有一些反例(例如PowerPC VLE)。
32位x86 CPU (在大多数台式机/笔记本电脑中使用)
在此类CPU上,只有call
directly reads the EIP register,并且只有跳转指令才能写入EIP。这足够“绝缘”,如果根本没有物理EIP寄存器,而您不一定知道其内容,则该寄存器就是CPU中的某个内部电路。
(您也可以将int
或int3
之类的int 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”表示:前面的 2 条指令的地址!)
答案 1 :(得分:2)
这些主张可能是在讨论两个不同的时间点,即在执行指令期间 与 之后。
您忽略的[...]
中有什么?它是否在谈论将PC递增2字节/ 1个指令字后完成一条指令的执行并开始获取下一条指令?
否则,这是本书中的错误,因为这两个主张(在当前指令执行期间PC指向当前指令与下一条指令) 不兼容。
我不明白保存当前地址和下一条指令的地址之间的区别
使用2字节指令来匹配您的书中的ISA来考虑内存中的这些(x86)指令(x86指令的长度在1到15个字节之间,包括可选的/强制性的前缀字节):
a: 0x66 0x90 nop
c: 0x66 0x90 nop
每条指令都有其自己的地址。我已经用十六进制数字表示了它们的起始地址(在汇编器语法中也可以是符号标签,但这旨在作为反汇编器输出的模型,例如objdump -d
)。 “指令的地址”是其在内存中的第一个字节的地址,无论架构PC在执行之前/期间/之后将持有什么内容。
在执行第一个nop
时,下一条指令的地址为c
。不管PC执行时(逻辑上)具有什么值,“当前指令”都是第一个nop
。
大多数指令实际上并不读取PC作为数据输入。仅相对跳转和相对PC的负载/存储都需要它。 (因此,编译器/汇编器需要知道计算相对位移的规则。)
MIPS和RISC-V也具有/代替aupc
指令,这些指令向程序计数器添加了一个寄存器或立即数,并将结果放入另一个寄存器中。因此,它们具有PC相对add
而不是PC相对寻址模式,以产生可以用作寻址模式的指针。但是,实际上是一样的。
只要在执行指令期间对PC的逻辑值有一致的规则,那么确切的规则是什么都没关系。
PC =当前指令的开始(例如,MIPS在逻辑上以这种方式工作,而不管内部实际执行什么操作。)
MIPS的相对分支是relative to PC + 4
(即相对于下一条指令,因此,它仅是关于如何记录的怪癖),但是MIPS跳转代替了PC的低28位,而不是PC + 4的低位(其高位可能会有所不同)。另请参阅http://www.cim.mcgill.ca/~langer/273/13-datapath1.pdf,它介绍了在MIPS上取指令/执行指令的逻辑操作。)
PC =稍后再开始2条指令。 (例如ARM)
Why does the ARM PC register point to the instruction after the next one to be executed? TL:DR:早期ARM设计中的三阶段fetch-decode-execute管道前端的构件。 (32位ARM将程序计数器公开为r15
,这是16个“通用”寄存器之一,因此您实际上可以跳转至or pc, r0, #4
之类的东西,以及在PC的任何指令中读取它-相对寻址)。
正如@Ross所说,只有一个简单的非流水线CPU将具有单个物理程序计数器寄存器。 (How does branch prediction interact with the instruction pointer)。
但是,如果任何一条指令引发异常(错误),则通常需要将错误指令的地址或 next 指令的地址存储在某个地方 。这取决于它是哪种异常。调试/单步异常将存储下一条指令的地址,因此从异常处理程序返回将逐步执行。页面错误将存储错误指令的地址,因此默认操作是重试它。
异常处理规则将与正常的PC执行期间规则分开,因此硬件必须记住指令长度或指令起始地址才能处理异常。它不必高效,因为中断/异常很少见。 CPU甚至可以跳转到中断处理程序之前也可以花费多个周期。 (相对于PC的寻址模式以及call
指令的正常运行情况确实必须有效。)
具有保存当前指令地址的PC是有效的设计。
对于超标量流水线设计,尤其是无序执行,这没有什么实质性的区别。流水线需要跟踪每条指令通过流水线时的地址(如果有变量,还要跟踪长度),因为它每个周期可以获取/解码/执行多于1条指令。它提取大块,并从该块中解码多达n
条指令。例如,某些实现可能要求提取块必须对齐16字节。 (有关各种x86微体系结构如何实现以及如何针对Pentium,Pentium Pro,Nehalem等中的前端获取/解码模式进行优化的详细信息,请参见https://agner.org/optimize/。幸运的是,现代x86 CPU具有已解码的uop缓存和对循环中的获取/解码问题不那么敏感。)
(半相关:x86 registers: MBR/MDR and instruction registers现代)
对于具有单个物理PC寄存器的简单有序非流水线CPU ,这意味着指令提取逻辑需要计算下一台PC,否则下一条指令可以”甚至在执行当前命令时被提取。
在x86中,IP / EIP / RIP在逻辑上保存当前正在执行的 next 指令的地址。考虑到它起源于8086,当时只有约29k晶体管,这是有道理的。它在执行当前insn时从指令流中预取(放入一个6字节的小缓冲区,如果使用额外的前缀,它甚至不足以容纳一条完整的指令,但是可以容纳6条单字节指令)。但是直到当前的完成,它才开始解码下一个。 (即根本不使用流水线,或者如果算上很容易解耦的预取,则可以说是两阶段的。我认为这种情况一直持续到486年。)
对于可变长度的ISA,直到解码才发现指令长度。让PC =当前指令的末尾可能更重要,因为您不能像MIPS那样仅仅计算PC + 4,也不能仅仅使用玩具ISA计算PC + 2。但是,除非您知道指令长度,否则您也不能倒退,因此要正确处理异常,8086必须也已跟踪指令的开始或记住了指令长度。
答案 2 :(得分:2)
最初,PC(寄存器)保存当前值,但随着时钟信号的变化,它更改为 PC(前一个地址+值),它将包含相同的值,直到下一个时钟周期,并在添加值后存储寄存器中的地址。
答案 3 :(得分:1)
真正的指令集,但这无关紧要,并且对这个实际指令的工作方式不感兴趣,将有助于演示该问题。
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,我们看到这是一个推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,现在这个问题就很特殊了,因为ret将根据该处理器的规则在执行期间修改程序计数器。如果调用地址0x2000的指令为0x1000,并且它是一个2字节的指令,则在获取程序并在解码期间,程序计数器将位于地址0x1002,在执行期间,地址0x1002将按照该指令集的规则存储在某个位置,并且程序计数器将采用值0x2000来调用此子例程。当我们到达ret指令并开始执行它时,我们就使用包含0x200A的程序计数器开始执行ret,但是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之前,在执行0xc056之后在执行0xc07A之前
对于该一条指令,pc至少保留三个值(如果获取一个字节 在某个时间它保持0xc078、0xc079、0xc07a,并以0xc056结尾) 一条指令。
是的,它可以并且确实拥有一个以上的值,但是不能同时保存一个值,在指令阶段。