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

7bsow1i6  于 2023-01-13  发布在  其他
关注(0)|答案(3)|浏览(156)

所以我阅读了一篇论文,在里面,他们说静态地反汇编二进制代码是不可判定的,因为一系列字节可以用尽可能多的方式表示,如图所示(它的x86)

所以我问题是:
1.那么CPU是如何执行的呢?例如,在图中,当我们到达C3之后,它如何知道它应该为下一条指令读取多少字节?

  1. CPU如何知道在执行一条指令后它应该增加多少PC?2它是否以某种方式存储当前指令的大小,并在它想增加PC时将其相加?
    1.如果CPU可以知道下一条指令应该读取多少字节,或者基本上知道如何解释下一条指令,为什么我们不能静态地做呢?
1cosmwyk

1cosmwyk1#

简单的方法是只读取一个字节,对其进行解码,然后确定它是否是一条完整的指令。如果没有读取另一个字节,则在必要时对其进行解码,然后确定是否已经读取了一条完整的指令。如果没有,则继续阅读/解码字节,直到读取了完整的指令。
这意味着,如果指令指针指向一个给定的字节序列,那么只有一种可能的方法来解码该字节序列的第一条指令。之所以会出现歧义,是因为要执行的下一条指令可能并不位于紧跟在第一条指令之后的字节。因为字节序列中的第一个指令可能改变指令指针,所以执行除了下一个指令之外的某个其它指令。
示例中的RET(retn)指令可能是函数的结尾。函数通常以RET指令结尾,但也不一定如此。一个函数可能有多条RET指令,但没有一条位于函数的结尾。相反,最后一条指令将是某种JMP指令,它跳回到函数中的某个位置,或完全跳到另一个函数。
这意味着,在示例代码中,如果没有更多的上下文,就不可能知道RET指令后面的字节是否会被执行,如果会,哪个字节会是下面函数的第一条指令,函数之间可能有数据,或者这个RET指令可能是程序中最后一个函数的结尾。
特别是x86指令集具有相当复杂的格式,包括可选择的前置码字节、一或多个运算码字节、一或两个可能的寻址格式字节然后是可能的位移和立即数字节。前缀字节可以被加到任何指令的前面。操作码字节决定有多少操作码字节,以及指令是否可以有操作数字节和立即数字节。操作码也可以指示有位移字节。第一个操作数字节决定是否有第二个操作数字节以及是否有位移字节。
英特尔64与IA-32体系结构软件开发人员手册中的下图显示了x86指令的格式:

用于解码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将返回到何处时。因为尝试解码并暂时执行不打算执行的随机数据可能存在性能损失,编译器通常不把数据放在函数之间。2尽管出于性能原因,它们可能会用从不执行的NOP指令来填充这个空间,以便对齐函数。
(They很久以前,它就用于在函数之间放置只读数据,但这是在能够推测性地执行指令的x86 CPU变得普遍之前。)

tct7dpnv

tct7dpnv2#

静态反汇编是不可判定的,因为反汇编器不能辨别一组字节是代码还是数据。您提供的示例是一个很好的示例:在RETN指令之后,可能有另一个子程序,或者是一些数据,然后是一个子程序。2在你真正执行代码之前,没有办法决定哪个是正确的。
当在指令获取阶段读取操作码时,操作码本身对一种指令进行编码,并且序列器已经知道要从中读取多少字节。没有任何模糊性。在您的示例中,在获取C3之后但在执行它之前,CPU将调整其EIP寄存器(指令指针)读取它认为是下一条指令的内容(以0F开始的一个)、BUT在指令C3的执行期间(它是RETN指令),EIP被改变为RETN是“从子例程返回”),因此其将不会到达指令0F8852。只有当代码的其他部分跳转到该指令的位置时,才能到达该指令,如果没有代码执行这种跳转,那么它将被认为是数据,但是确定特定指令是否将被执行的问题不是一个可判定的问题。
一些聪明的反汇编器(我认为IDA Pro就是这样做的)从一个已知的存储代码的位置开始,并假设所有后面的字节也是指令,直到找到一个跳转或ret。如果找到了一个跳转,并且通过阅读二进制代码知道了跳转的目的地,那么扫描就在那里继续。如果跳转是有条件的,那么扫描就分支成两条路径:未进行跳跃和已进行跳跃。
扫描完所有分支后,剩下的都被认为是数据(这意味着中断处理程序、异常处理程序和从运行时计算的函数指针调用的函数将不会被检测到)

eqzww0vc

eqzww0vc3#

你的主要问题似乎是以下一个:
如果CPU可以知道下一条指令应该读取多少字节,或者基本上知道如何解释下一条指令,为什么我们不能静态地做呢?
论文中描述的问题与"跳转"指令有关(不仅仅指jmp,还包括intretsyscall等类似指令):
这些指令的目的是在完全不同的地址继续执行程序,而不是继续执行下一条指令(函数调用和while()循环就是程序执行不在下一条指令继续的例子)。
您的示例从指令jmp eax开始,这意味着寄存器eax中的值决定在jmp eax指令之后执行哪条指令。
如果eax包含字节0F的地址,则CPU将执行jcc指令(图中左侧的情况);如果包含88的地址,则执行mov指令(图中的中例);如果它包含52的地址,则它将执行push指令(图中的右图)。
因为您不知道执行程序时eax的值是哪个,所以您无法知道会发生三种情况中的哪一种。
(有人告诉我,在20世纪80年代,甚至有一些商业程序在运行时会发生不同的情况:在您的示例中,这意味着有时执行jcc指令,有时执行mov指令!)
当我们到达C3之后时,它如何知道下一条指令应该读取多少字节?
CPU如何知道在执行一条指令后它应该使PC递增多少?
C3不是一个很好的示例,因为retn是"跳转"指令:"C3之后的指令"将永远不会到达,因为程序执行在别处继续。
但是,您可以用另一条长度为一个字节的指令(如52)替换C3。在这种情况下,下一条指令将以字节0F开始,而不是以8852开始。

相关问题