作为一个初学者和自学者,我正在学习汇编,目前正在阅读艾伦Hollub的书《C Companion》的第三章。我无法理解他在一个虚构的演示机器中用两个字节字描述的程序计数器或PC的描述。这里是第57页中对PC的描述。PC机总是保存着当前正在执行的指令的地址。当每条指令执行时,它会自动更新,以保存下一条要执行的指令的地址。......这里的重要概念是,PC机保存的是下一条指令的地址,而不是指令本身。我不明白保存当前地址和保存下一个指令地址的区别。PC是否同时在两个连续字节中保存这两个地址?
63lcw9qa1#
我不明白他在一个想象中的演示机中用两个字节的字描述的程序计数器或PC。他描述了一个简单的CPU,解释了CPU一般是如何工作的。
真实的的CPU要复杂得多:
在许多手册中(针对任何类型的CPU),您会发现如下语句:“PC寄存器被压入堆栈.”这通常意味着从call指令返回后执行的指令的地址被压入堆栈。然而,这样的句子不是100%正确:在68 k CPU(见下文)的情况下,写入下一条指令的地址,而不是当前指令的地址加2!对于大多数CPU,PC相关jump指令相对于下一条指令的地址;然而,存在相反示例(例如PowerPC VLE)。
call
jump
32位x86 CPU(用于大多数台式机/笔记本电脑)
在这样的CPU上,只有calldirectly reads the EIP register和跳转指令写入EIP,这是足够的“隔离”,如果有一个物理EIP寄存器的话,这个寄存器是CPU中的一些内部电路,你不必知道它的内容。(You也可以将int指令(如int3或int 0x80)视为阅读CS:EIP,因为它们必须压入异常帧,但将它们视为触发异常处理机制更有意义。很可能不同的x86 CPU内部工作方式不同,因此EIP“寄存器”的实际内容在不同的CPU中也不同(现代高性能实现不会只有一个EIP寄存器,而是会做任何必要的事情来保持假象,并在需要时推送正确的返回地址)。(PC相对跳转相对于下一条指令的地址。)
int
int3
int 0x80
64位x86 CPU
这些CPU具有直接使用RIP寄存器的指令,例如mov eax,[rip+symbol_offset],以执行静态数据的PC相关加载;使共享库和ASLR的位置无关代码比32位x86更有效。在这种情况下,“RIP”是下一条指令的地址。
mov eax,[rip+symbol_offset]
六万八千
这些CPU也有可能直接使用PC寄存器的内容。在这种情况下,PC反映的是当前指令的地址加上2(这里我不是绝对确定)。因为这样的指令至少有4个字节长,所以PC寄存器的值将反映指令的“中间”字节的地址。
手臂
在ARM CPU上阅读PC时(可以直接读取!),该值通常反映当前指令的地址加上8,在某些情况下甚至加上12!(指令为4字节长,因此“当前指令加8”表示:前面两条指令的地址!)
tmb3ates2#
这些声明可能是在谈论两个不同的时间点,* 在执行指令期间 * 与 * 在执行指令之后 。你忽略的那些[...]中有什么?它是不是说在PC递增2字节/ 1指令字之后,完成一条指令的执行并开始获取下一条指令?否则,这是书中的一个错误,因为这两个声明(PC在执行当前指令期间指向当前指令与下一个指令) 是 * 不兼容的。我不明白保持当前地址和下一条指令的地址之间的区别考虑内存中的这些(x86)指令,使用2字节指令来匹配书中的伊萨(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的逻辑值有一致的规则,具体的规则是什么并不重要。
objdump -d
nop
c
aupc
add
MIPS相对分支是relative to PC + 4(即相对于下一条指令,因此这只是文档中的一个怪癖),但MIPS跳转替换PC的低28位,而不是PC+4的低28位(PC+4的高位可能不同)。
PC + 4
Why does the ARM PC register point to the instruction after the next one to be executed?目标日期:目标日期:早期ARM设计中的3阶段获取-解码-执行流水线前端的产物。(32位ARM将程序计数器公开为r15,这是16个“通用”寄存器之一,因此您实际上可以使用or pc, r0, #4或其他寄存器跳转,以及在任何指令中阅读它以进行PC相对寻址)。正如@Ross所说,只有一个简单的非流水线CPU才有一个物理程序计数器寄存器(分支预测如何与指令指针交互)。
r15
or pc, r0, #4
**但如果任何指令引发异常(faults),它通常需要存储出错指令的地址或 next 指令的地址。这取决于异常的类型。调试/单步异常将存储下一条指令的地址,因此从异常处理程序返回时将步进。缺页将存储出错指令的地址,因此默认操作是重试。
异常处理规则将与正常的PC执行中规则分开,因此硬件必须记住指令长度或指令起始地址才能处理异常,它不一定要“高效”,因为中断/异常很少发生;CPU在跳转到中断处理程序之前需要多个周期是可以接受的(PC相对寻址模式和call指令的正常操作情况必须是高效的)。
拥有一个保存当前指令地址的PC是一种有效的设计。
对于超标量流水线设计,特别是无序执行,这没有什么区别。(和长度,如果可变的话),因为它可以在每个周期获取/解码/执行多于1个指令。它获取大块,并解码来自该块的多达n指令。某些实现可能要求fetch-blocks是16字节对齐的,例如。(有关各种x86微体系结构如何执行此操作以及如何优化Pentium、Pentium Pro、Nehalem、幸运的是,现代的x86 CPU具有decoded-uop缓存,对循环中的获取/解码问题不太敏感。)(半相关:x86 registers: MBR/MDR and instruction registers现代版)
n
在x86中,IP/EIP/RIP在当前指令被执行时逻辑上持有 * next * 指令的地址。考虑到它起源于8086,只有大约29k个晶体管,这是有意义的。它在当前insn被执行时从指令流中预取(进入小的6字节缓冲器,如果使用额外的前缀,该缓冲器甚至不够长以容纳整个指令,但是它保存6个单字节指令)。但是它甚至直到当前指令完成才开始解码下一个指令。(Iidoe根本不是流水线的,或者可以说是2级的,如果你算上预取的话,这是非常容易解耦的。我想直到486之前都是这样。)对于可变长度的ISA,指令长度直到解码时才被发现。PC =当前指令的结束可能更重要,因为你不能像MIPS那样计算PC +4,或者用你的玩具ISA计算PC +2。但是你也不能后退,除非你知道指令长度,所以为了正确处理异常,8086必须也跟踪指令的开始。或者记住了指令长度。
qojgxg4l3#
这是一个真实的指令集,但没关系,我对这个真实的指令如何工作不感兴趣-它将用于演示问题。
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
解码并执行弹出r11所以你可以看到程序计数器实际上至少包含两个值,在指令的开始,在取数之前,它包含指令的地址,在取数和解码之后,就在我们开始执行之前,它包含这个指令之后的字节的地址,如果这不是一个跳转,那就是另一个指令,如果这是一个无条件跳转,那个字节可能是一个指令或者一些数据,或未使用的内存。但我们说它"指向下一条指令",在这种情况下是指在执行之前,在这条指令之后的地址,这条指令通常有另一条指令。但正如我们接下来将看到的,pc可以被指令修改。但总是在执行结束时,它指向(对于这个简单构成的处理器,其类似于多个简单的8位处理器)到要执行的下一个指令。最后
before data after 0x2008 0x30 0x2009 0x2009 0x41 0x200A
对ret进行解码,现在这个问题很特殊,因为ret将在执行期间根据该处理器的规则修改程序计数器。如果调用地址0x2000的指令是0x1000,并且它是一个两字节指令,则在获取之后和解码期间,程序计数器将位于地址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结束)。是的,它可以并且确实保存多个值,但不是同时保存,在指令的各个阶段中一次保存一个值。
6psbrbz94#
最初,PC(寄存器)保持当前值,但是随着时钟信号改变,它改变为PC(先前地址+值),并且它将包含相同的值直到下一个时钟周期,并且在值相加之后,它将在寄存器中存储地址。
4条答案
按热度按时间63lcw9qa1#
我不明白他在一个想象中的演示机中用两个字节的字描述的程序计数器或PC。
他描述了一个简单的CPU,解释了CPU一般是如何工作的。
真实的的CPU要复杂得多:
在许多手册中(针对任何类型的CPU),您会发现如下语句:“PC寄存器被压入堆栈.”
这通常意味着从
call
指令返回后执行的指令的地址被压入堆栈。然而,这样的句子不是100%正确:在68 k CPU(见下文)的情况下,写入下一条指令的地址,而不是当前指令的地址加2!
对于大多数CPU,PC相关
jump
指令相对于下一条指令的地址;然而,存在相反示例(例如PowerPC VLE)。32位x86 CPU(用于大多数台式机/笔记本电脑)
在这样的CPU上,只有
call
directly reads the EIP register和跳转指令写入EIP,这是足够的“隔离”,如果有一个物理EIP寄存器的话,这个寄存器是CPU中的一些内部电路,你不必知道它的内容。(You也可以将
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”是下一条指令的地址。六万八千
这些CPU也有可能直接使用PC寄存器的内容。在这种情况下,PC反映的是当前指令的地址加上2(这里我不是绝对确定)。
因为这样的指令至少有4个字节长,所以PC寄存器的值将反映指令的“中间”字节的地址。
手臂
在ARM CPU上阅读PC时(可以直接读取!),该值通常反映当前指令的地址加上8,在某些情况下甚至加上12!
(指令为4字节长,因此“当前指令加8”表示:前面两条指令的地址!)
tmb3ates2#
这些声明可能是在谈论两个不同的时间点,* 在执行指令期间 * 与 * 在执行指令之后 。
你忽略的那些
[...]
中有什么?它是不是说在PC递增2字节/ 1指令字之后,完成一条指令的执行并开始获取下一条指令?否则,这是书中的一个错误,因为这两个声明(PC在执行当前指令期间指向当前指令与下一个指令) 是 * 不兼容的。
我不明白保持当前地址和下一条指令的地址之间的区别
考虑内存中的这些(x86)指令,使用2字节指令来匹配书中的伊萨(x86指令的长度可变,从1到15字节,包括可选/强制前缀字节):
每条指令都有自己的地址。我用十六进制数字表示了它们的起始地址(在汇编语法中也可以是符号标签,但这是反汇编输出的模拟,就像
objdump -d
一样)。“指令地址”是它在内存中的第一个字节的地址,而不管架构PC在执行它之前/期间/之后会保存什么。当第一条
nop
正在执行时,下一条指令的地址是c
。“当前指令”是第一条nop
,无论PC在执行时具有什么值(逻辑上)。大多数指令实际上并不把PC作为数据输入读取,只有相对跳转和PC相对加载/存储需要它(因此编译器/汇编器需要知道计算相对位移的规则)。
MIPS和RISC-V也有
aupc
指令,这些指令会向程序计数器添加一个寄存器或立即数,并将结果放入另一个寄存器。因此,它们没有使用相对于PC的寻址模式,而是使用相对于PC的add
,以产生一个指针,您可以将其用作寻址模式。只要在指令执行过程中PC的逻辑值有一致的规则,具体的规则是什么并不重要。
MIPS相对分支是relative to
PC + 4
(即相对于下一条指令,因此这只是文档中的一个怪癖),但MIPS跳转替换PC的低28位,而不是PC+4的低28位(PC+4的高位可能不同)。Why does the ARM PC register point to the instruction after the next one to be executed?目标日期:目标日期:早期ARM设计中的3阶段获取-解码-执行流水线前端的产物。(32位ARM将程序计数器公开为
r15
,这是16个“通用”寄存器之一,因此您实际上可以使用or pc, r0, #4
或其他寄存器跳转,以及在任何指令中阅读它以进行PC相对寻址)。正如@Ross所说,只有一个简单的非流水线CPU才有一个物理程序计数器寄存器(分支预测如何与指令指针交互)。
**但如果任何指令引发异常(faults),它通常需要存储出错指令的地址或 next 指令的地址。这取决于异常的类型。调试/单步异常将存储下一条指令的地址,因此从异常处理程序返回时将步进。缺页将存储出错指令的地址,因此默认操作是重试。
异常处理规则将与正常的PC执行中规则分开,因此硬件必须记住指令长度或指令起始地址才能处理异常,它不一定要“高效”,因为中断/异常很少发生;CPU在跳转到中断处理程序之前需要多个周期是可以接受的(PC相对寻址模式和
call
指令的正常操作情况必须是高效的)。PC=当前指令的简单物理实现的含义
拥有一个保存当前指令地址的PC是一种有效的设计。
对于超标量流水线设计,特别是无序执行,这没有什么区别。(和长度,如果可变的话),因为它可以在每个周期获取/解码/执行多于1个指令。它获取大块,并解码来自该块的多达
n
指令。某些实现可能要求fetch-blocks是16字节对齐的,例如。(有关各种x86微体系结构如何执行此操作以及如何优化Pentium、Pentium Pro、Nehalem、幸运的是,现代的x86 CPU具有decoded-uop缓存,对循环中的获取/解码问题不太敏感。)(半相关:x86 registers: MBR/MDR and instruction registers现代版)
在x86中,IP/EIP/RIP在当前指令被执行时逻辑上持有 * next * 指令的地址。考虑到它起源于8086,只有大约29k个晶体管,这是有意义的。它在当前insn被执行时从指令流中预取(进入小的6字节缓冲器,如果使用额外的前缀,该缓冲器甚至不够长以容纳整个指令,但是它保存6个单字节指令)。但是它甚至直到当前指令完成才开始解码下一个指令。(Iidoe根本不是流水线的,或者可以说是2级的,如果你算上预取的话,这是非常容易解耦的。我想直到486之前都是这样。)
对于可变长度的ISA,指令长度直到解码时才被发现。PC =当前指令的结束可能更重要,因为你不能像MIPS那样计算PC +4,或者用你的玩具ISA计算PC +2。但是你也不能后退,除非你知道指令长度,所以为了正确处理异常,8086必须也跟踪指令的开始。或者记住了指令长度。
qojgxg4l3#
这是一个真实的指令集,但没关系,我对这个真实的指令如何工作不感兴趣-它将用于演示问题。
如前所述,在讨论程序计数器时,存在时间概念。
一个超级简单的处理器,老的8位,其他的都可以这样想,新的就不一样了。
当我们输入这段代码时,不管我们怎么得到的,都无关紧要,程序计数器是0x2000,它告诉我们从哪里取指令,我们必须取指令,解码,然后执行,重复。
这些是16位指令,两个字节,处理器开始读取,pc指向指令,指令的地址,处理器读取两个字节,一个在地址0x2000(0x0b),处理器将程序计数器递增到0x2001并使用其来获取地址0x2001处的指令的后半部分(0x12),并将程序计数器递增到0x2002。因此,对于此组成处理器中的每次获取,我将描述使用程序计数器作为地址获取的每次获取,然后递增程序计数器。
现在我们解码指令,程序计数器当前显示0x2002,我们看到这是一个压入r11,因此我们继续执行。
执行此指令期间,程序计数器保持0x2002。寄存器r11的值压入堆栈。
现在我们开始取下一条指令。
当我们解码此指令(pc == 0x2004)mov #immediate,r11时,处理器意识到此指令需要立即数,因此需要再取两个字节
它确定现在可以通过将值0x0021写入寄存器r11来执行指令(小端字节序0x0021 = 33十进制)。执行期间,此指令的程序计数器为0x2006。
下一个
解码并执行弹出r11
所以你可以看到程序计数器实际上至少包含两个值,在指令的开始,在取数之前,它包含指令的地址,在取数和解码之后,就在我们开始执行之前,它包含这个指令之后的字节的地址,如果这不是一个跳转,那就是另一个指令,如果这是一个无条件跳转,那个字节可能是一个指令或者一些数据,或未使用的内存。但我们说它"指向下一条指令",在这种情况下是指在执行之前,在这条指令之后的地址,这条指令通常有另一条指令。但正如我们接下来将看到的,pc可以被指令修改。但总是在执行结束时,它指向(对于这个简单构成的处理器,其类似于多个简单的8位处理器)到要执行的下一个指令。
最后
对ret进行解码,现在这个问题很特殊,因为ret将在执行期间根据该处理器的规则修改程序计数器。如果调用地址0x2000的指令是0x1000,并且它是一个两字节指令,则在获取之后和解码期间,程序计数器将位于地址0x1002,在执行期间,地址0x1002将按照此指令集的规则存储在某处,且程序计数器将取值0x2000以调用此子例程。当我们到达ret指令并开始执行它时,我们使用包含0x200A的程序计数器开始执行ret,但ret将调用后指令的地址,调用执行期间存储的值,因此在该指令结束时,程序计数器将包含值0x1002,并且下一次获取将从该地址开始。
因此,在执行前的最后一条指令中,pc指向通常是下一条指令的指令,这些指令不分支、跳转或调用。0x200A。但在执行期间,程序计数器发生了变化,因此"下一条"指令是调用后的指令,我们在这里。
再来点
在获取之前,pc是0xC064。在获取和解码之后,pc是0xC066。指令说如果zerp到0xC07a则跳转。因此,如果没有设置零标志,则pc停留在0xC066,并且从那里开始下一条指令,但是如果z被设置,那么pc被修改为0xc07a,并且这是下一个要执行的指令将在的地方。因此,在0xc064之前,在0xc066之后,或者0xc07a取决于。
一条指令的"后"是下一条指令的"前"。
无条件转移
读取0xc07a之前,执行0xc056之后执行0xc07A之前
对于这一条指令,pc在一条指令中至少保存了三个值(如果一次取一个字节,则保存0xc078、0xc079、0xc07a,并以0xc056结束)。
是的,它可以并且确实保存多个值,但不是同时保存,在指令的各个阶段中一次保存一个值。
6psbrbz94#
最初,PC(寄存器)保持当前值,但是随着时钟信号改变,它改变为PC(先前地址+值),并且它将包含相同的值直到下一个时钟周期,并且在值相加之后,它将在寄存器中存储地址。