考虑到指令的长度不同,CPU如何知道下一条指令应读取多少字节?

时间:2019-05-30 21:43:44

标签: assembly x86 cpu

所以我正在读一篇论文,在那儿,他们说静态分解二进制代码是不确定的,因为一系列字节可以用图片(其x86)所示的多种方式表示。

disassembling

所以我的问题是:

  1. 然后CPU如何执行此操作?例如在图片中,当我们到达C3之后时,它如何知道下一条指令应读取多少字节?

  2. 在执行一条指令后,CPU如何知道应该递增PC多少?它会以某种方式存储当前指令的大小并在想要增加PC时添加它吗?

  3. 如果CPU能够以某种方式知道下一条指令应该读取多少字节,或者基本上是如何解释下一条指令,为什么我们不能静态地执行呢?

3 个答案:

答案 0 :(得分:3)

简单的方法是只读取一个字节,对其进行解码,然后确定它是否为完整的指令。如果没有读取另一个字节,则在必要时对其进行解码,然后确定是否已读取完整指令。如果没有继续读取/解码字节,直到读取完整的指令。

这意味着,如果指令指针指向给定的字节序列,则只有一种方法可以解码该字节序列的第一条指令。唯一的歧义是因为下一条要执行的指令可能不位于紧随第一条指令之后的字节处。这是因为字节序列中的第一条指令可能会更改指令指针,因此会执行除后一条指令以外的其他指令。

您的示例中的RET(retn)指令可能是函数的结尾。函数通常以e RET指令结尾,但不一定如此。一个函数可能具有多个RET指令,而这些指令都不在该函数的末尾。相反,最后一条指令将是某种JMP指令,它会跳回到函数中的某个位置,或完全跳回到另一个函数。

这意味着在示例代码中,如果没有更多上下文,就不可能知道是否将执行RET指令之后的任何字节,如果是,则哪个字节将成为后续函数的第一条指令。函数之间可能有数据,或者此RET指令可能是程序中最后一个函数的结尾。


特别是x86指令集具有相当复杂的格式,包括可选的前缀字节,一个或多个操作码字节,一个或两个可能的寻址形式字节以及可能的位移和立即字节。前缀字节几乎可以加在任何指令之前。操作码字节确定有多少操作码字节,以及指令是否可以具有操作数字节和立即数字节。操作码还可以指示存在位移字节。第一个操作数字节确定是否存在第二个操作数字节以及位移字节。

《英特尔64和IA-32架构软件开发人员手册》中的该图显示了x86指令的格式:

X86 Instruction Format

用于解码x86指令的类似Python的伪代码如下所示:

# read possible prefixes

prefixes = []
while is_prefix(memory[IP]):
    prefixes.append(memory[IP))
    IP += 1

# read the opcode 

opcode = [memory[IP]]
IP += 1
while not is_opcode_complete(opcode):
    opcode.append(memory[IP])
    IP += 1

# read addressing form bytes, if any

modrm = None
addressing_form = []    
if opcode_has_modrm_byte(opcode):
    modrm = memory[IP]
    IP += 1
    if modrm_has_sib_byte(modrm):
        addressing_form = [modrm, memory[IP]]
        IP += 1
    else:
        addressing_form = [modrm]

# read displacement bytes, if any

displacement = []
if (opcode_has_displacement_bytes(opcode)
    or modrm_has_displacement_bytes(modrm)):
    length = determine_displacement_length(prefixes, opcode, modrm)
    displacement = memory[IP : IP + length]
    IP += length

# read immediate bytes, if any

immediate = []
if opcode_has_immediate_bytes(opcode):
    length = determine_immediate_length(prefixes, opcode)
    immediate = memory[IP : IP + length]
    IP += length

# the full instruction

instruction = prefixes + opcode + addressing_form + displacement + immediate

上面的伪代码遗漏的一个重要细节是指令的长度限制为15个字节。可以构造其他有效的16字节或更长的x86指令,但是如果执行,则此类指令将生成未定义的操作码CPU异常。 (我遗漏了其他一些细节,例如如何在Mod R / M字节内部编码操作码的一部分,但是我认为这不会影响指令的长度。)


但是,x86 CPU实际上并没有像我上面描述的那样对指令进行解码,它们仅对指令进行解码,就好像它们一次读取一个字节一样。取而代之的是,现代CPU会将整个15个字节读入缓冲区,然后通常在单个周期内并行解码字节。当它完全解码指令,确定其长度,并准备读取下一条指令时,它将移入缓冲区中不属于该指令的其余字节。然后,它读取更多字节以再次将缓冲区填充为15个字节,并开始解码下一条指令。

我上面写的内容并没有暗示现代CPU可以做的另一件事,就是推测性地执行指令。这意味着CPU会解码指令,并在完成之前的指令之前尝试尝试执行它们。反过来,这意味着CPU可能最终只能解码RET指令之后的指令,但前提是无法确定RET将返回到哪里。由于尝试解码和临时执行不打算执行的随机数据可能会导致性能下降,因此编译器通常不会在函数之间放入数据。尽管它们可能会用NOP指令填充此空间,但出于性能原因,它们将永远不会执行以对齐功能。

(很久以前,它们曾经在功能之间放置只读数据,但这是在可以推测性执行指令的x86 CPU普及之前。)

答案 1 :(得分:2)

静态反汇编是无法确定的,因为反汇编器无法识别一组字节是代码还是数据。您提供的示例就是一个很好的例子:在RETN指令之后,ther可能是另一个子例程,或者可能是一些数据,然后是例程。在实际执行代码之前,无法确定哪个是正确的。

在指令提取阶段读取操作码时,该操作码本身会对一种指令进行编码,而定序器已经知道要从中读取多少字节。没有歧义。在您的示例中,在获取C3之后但在执行C3之前,CPU会调整其EIP寄存器(指令指针)以读取它认为将成为下一条指令(以0F开头的那条指令)的内容,但在执行C3指令期间会BUT( (这是一个RETN指令),由于RETN为“从子例程返回”,因此EIP被更改,因此它不会到达指令0F 8852。只有在代码的其他部分跳转到该指令的位置时,该指令才会到达如果没有代码执行这种跳转,则将其视为数据,但是确定特定指令是否执行的问题不是可以决定的问题。

一些聪明的反汇编程序(我认为IDA Pro会这样做)从已知的存储代码的位置开始,并假定所有随后的字节也是指令,直到找到跳转或退出。如果找到了跳转,并且通过读取二进制代码知道了跳转的目的地,则扫描将继续进行。如果跳转是有条件的,则扫描会分为两条路径:未执行跳转和已执行跳转。

在扫描完所有多余的东西之后,剩下的所有东西都被视为数据(这意味着将不会检测到中断处理程序,异常处理程序以及从运行时计算的函数指针调用的函数)

答案 2 :(得分:1)

您的主要问题似乎是以下问题:

  

如果CPU能够以某种方式知道下一条指令应该读取多少字节,或者基本上是如何解释下一条指令,为什么我们不能静态地执行它?

本文中描述的问题与“跳转”指令有关(这不仅意味着jmp,还意味着intretsyscall和类似的指令):

此类指令的目的是在完全不同的地址处继续执行程序,而不是在下一条指令处继续执行。 (函数调用和while()循环是示例,其中下一条指令不继续执行程序。)

您的示例以指令jmp eax开始,这意味着寄存器eax中的值决定在jmp eax指令之后执行哪个指令。

如果eax包含字节0F的地址,则CPU将执行jcc指令(图中左)。如果它包含88的地址,它将执行mov指令(在图片中为中号);如果它包含52的地址,它将执行push指令(在图片中为大写)。

因为您不知道执行程序时eax的值,所以您不知道三种情况中的哪一种。

(有人告诉我,在1980年代,甚至有一些商业程序在运行时发生了不同的情况:在您的示例中,这意味着有时执行jcc指令,有时执行mov指令! )

  

当我们到达C3之后时,它如何知道下一条指令应读取多少字节?

     

CPU如何知道执行一条指令后应该增加PC多少?

C3不是一个好例子,因为retn是一条“跳转”指令:由于程序在别处继续执行,因此永远不会到达“ C3之后的指令”。

但是,您可以用另一字节长的指令(例如C3)来代替52。在这种情况下,可以很好地定义下一条指令将以字节0F开头,而不是以8852开头。