所以我正在阅读一篇论文,其中他们说静态反汇编二进制代码是不可判定的,因为一系列字节可以用图片(其 x86 )所示的尽可能多的方式表示
所以我的问题是:
那么CPU如何执行呢?例如在图中,当我们到达 C3 之后,它如何知道下一条指令应该读取多少字节?
CPU如何知道在执行一条指令后PC应该增加多少?它是否以某种方式存储当前指令的大小并在它想要增加 PC 时添加它?
如果 CPU 能够以某种方式知道它应该为下一条指令读取多少字节或者基本上如何解释下一条指令,为什么我们不能静态地做到这一点?
简单的方法是只读取一个字节,对其进行解码,然后确定它是否是一条完整的指令。如果没有读取另一个字节,则根据需要对其进行解码,然后确定是否已读取完整的指令。如果不继续读取/解码字节,直到读取完整的指令。
这意味着如果指令指针指向给定的字节序列,则只有可能的方法来解码该字节序列的第一条指令。之所以产生歧义,是因为要执行的下一条指令可能不会位于紧跟第一条指令之后的字节处。那是因为字节序列中的第一条指令可能会更改指令指针,因此会执行除以下指令之外的其他指令。
retn
您示例中的 RET ( ) 指令可能是函数的结尾。函数通常以 e RET 指令结尾,但不一定如此。一个函数可能有多个 RET 指令,但没有一个位于函数的末尾。相反,最后一条指令将是某种 JMP 指令,它跳转回函数中的某个位置,或者完全跳转到另一个函数。
这意味着在您的示例代码中,如果没有更多上下文,就不可能知道 RET 指令后面的任何字节是否会被执行,如果是,那么哪个字节将是以下函数的第一条指令。函数之间可能有数据,或者此 RET 指令可能是程序中最后一个函数的结尾。
特别是 x86 指令集具有相当复杂的格式,包括可选的前缀字节、一个或多个操作码字节、一两个可能的寻址形式字节,以及可能的位移和立即数字节。前缀字节几乎可以添加到任何指令中。操作码字节决定了有多少操作码字节以及指令是否可以有操作数字节和立即数字节。操作码也可能表明有位移字节。第一个操作数字节确定是否有第二个操作数字节以及是否有位移字节。
Intel 64 and IA-32 Architectures Software Developer's Manual 有这张图显示了 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
Run Code Online (Sandbox Code Playgroud)
上面伪代码中遗漏的一个重要细节是指令长度限制为 15 个字节。可以构造 16 字节或更长的其他有效 x86 指令,但如果执行此类指令,则会生成未定义的操作码 CPU 异常。(我还忽略了其他细节,例如如何在 Mod R/M 字节内编码部分操作码,但我认为这不会影响指令的长度。)
然而,x86 CPU 实际上并不像我上面描述的那样解码指令,它们只解码指令,就好像它们一次读取一个字节一样。相反,现代 CPU 会将整个 15 个字节读入缓冲区,然后并行解码字节,通常在一个周期内。当它完全解码指令,确定其长度,并准备好读取下一条指令时,它会转移缓冲区中不属于指令的剩余字节。然后它读取更多字节以再次将缓冲区填充到 15 个字节并开始解码下一条指令。
现代 CPU 会做的另一件事不是我上面写的内容,而是推测性执行指令。这意味着 CPU 将解码指令,并在执行完之前的指令之前试探性地尝试执行它们。这反过来意味着 CPU 可能最终会解码 RET 指令之后的指令,但前提是它无法确定 RET 将返回到哪里。由于尝试解码和暂时执行不打算执行的随机数据可能会降低性能,因此编译器通常不会在函数之间放置数据。尽管他们可能会用永远不会执行的 NOP 指令填充这个空间,以便出于性能原因对齐功能。
(很久以前他们曾经在函数之间放置只读数据,但这是在可以推测性执行指令的 x86 CPU 变得司空见惯之前。)