简单的方法是只读取一个字节,解码它,然后确定它是否是完整的指令。如果不是,则继续读取另一个字节,必要时解码它,然后确定是否已经读取了完整的指令。如果没有,则继续读取/解码字节,直到读取完整的指令。
这意味着,如果指令指针指向给定的字节序列,则只有一种可能的方法来解码该字节序列的第一个指令。歧义只是因为要执行的下一个指令可能不位于紧随第一个指令之后的字节中。这是因为字节序列中的第一个指令可能会更改指令指针,从而执行除以下指令以外的其他指令。
在您的示例中,RET(retn
)指令可能是函数的结尾。函数通常以RET指令结束,但不一定如此。一个函数可能有多个RET指令,其中没有一个位于函数的末尾。相反,最后一条指令将是某种JMP指令,跳回到函数中的某个位置或者跳转到另一个函数。
# 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架构中,指令编码的形式使得解码器可以从每个字节中了解接下来还有多少个字节。
例如,我来向您展示一下解码器如何解码这条指令流。
55
55
并知道这是一个单字节指令 push ebp
。因此,它解码 push ebp
并继续执行下一条指令。push ebp
89
89
,这是mov r/m32,r32
指令。紧接着该指令后面跟着一个modr/m字节,用于指定操作数。push ebp
89 e5
modr/m字节为e5
,表示ebp
是r/m操作数,esp
是r操作数,因此该指令为mov ebp, esp
。
push ebp
mov ebp, esp
8b
这个指令是mov r32,r/m32
,后面同样跟着一个modr/m字节。
push ebp
mov ebp, esp
8b 45
这个modr/m字节有一个eax操作数和一个操作数,其中的格式为[ebp + disp8]
,并带有一个8位位移,该位移在下一个字节中给出。
push ebp
mov ebp, esp
8b 45 0c
0c
,因此指令为mov eax, [ebp + 0xc]
。push ebp
mov ebp, esp
mov eax, [ebp + 0xc]
03
这个指令是add r,r/m32
,后面再跟一个modr/m字节。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
03 45
与之前相同,r 操作数为 eax
,而 r/m 操作数为 [ebp + disp8]
。位移为 08
。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
add eax, [ebp + 0x08]
01
这个指令是add r/m32, r
,后面跟着一个modr/m字节。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
add eax, [ebp + 0x08]
01 05
这个modr/m字节指示了一个r操作数为eax
和一个r/m操作数为[disp32]
。下一个四个字节是位移,分别为00 00 00 00
。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
add eax, [ebp + 0x08]
add [0x00000000], eax
5d
5d
指令是pop ebp
,是一条单字节指令。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
add eax, [ebp + 0x08]
add [0x00000000], eax
pop ebp
c3
c3
指令是一个单字节指令,其操作为 ret
。该指令会将控制权转移到其他地方,因此解码器从这里停止解码。
push ebp
mov ebp, esp
mov eax, [ebp + 0x0c]
add eax, [ebp + 0x08]
add [0x00000000], eax
pop ebp
ret
在实际的x86处理器中,采用了复杂的并行解码技术。这是可能的,因为处理器可以作弊并预先读取可能或可能不是任何指令的指令字节。
.text
中。除了它可能是只读的之外,没有什么神奇的地方。此外,.text
只是一个名称。你可以为你的部分(至少代码和数据)取任何你喜欢的名字,重要的是它们的属性。一些部分确实有定义好的名称,各种工具和加载器依赖于这些名称。 - Jester.text
部分的开头开始并假设另一个指令紧接在此指令之后是可以正常工作的,即使它是无条件跳转。 x86指令根据起始点唯一解码,并且编译器永远不会将分支跳转到先前从不同起始点解码的指令中间。这只对试图打破反汇编的混淆代码有困难。 - Peter Cordes论文中描述的问题与“跳转”指令有关(这不仅意味着如果CPU可以知道下一条指令需要读取多少字节,或者基本上如何解释下一条指令,为什么我们不能静态地做到呢?
jmp
,还包括int
,ret
,syscall
和类似指令):while()
循环是程序执行不会继续执行下一条指令的例子。)jmp eax
指令开始,这意味着寄存器eax
中的值决定了在jmp eax
指令之后执行哪个指令。eax
包含字节0F
的地址,CPU将执行jcc
指令(图片中的左侧案例);如果它包含88
的地址,它将执行mov
指令(图片中的中间案例);如果它包含52
的地址,它将执行push
指令(图片中的右侧案例)。由于您不知道程序在执行时eax
将具有哪个值,因此无法知道将发生哪种情况。jcc
指令,有时会执行mov
指令!)C3
后,它如何知道下一个指令应该读取多少字节?C3
不是一个好的示例,因为retn
是一个“跳转”指令:程序执行会在其他地方继续,所以“C3
之后的指令”永远不会被执行。
然而,你可以用另一个只有一个字节长的指令(如52
)替换C3
。在这种情况下,下一条指令将以字节0F
而不是88
或52
开头,这是很明确的。
55
是push rbp
还是push ebp
(取决于是 32 位还是 64 位模式)?CPU 必须知道如何解码和解释指令字节,并在这个过程中确定指令的长度。 - RbMmjmp eax
将去哪里。 - Jesterretn
,这是另一个动态跳转,你不知道它会带你去哪里。顺便说一下,在极端情况下,相同的字节可能会被多次执行,但指令边界不同,因此 CPU 将以不同的方式执行它,因此没有单一正确的反汇编。 - Jester