我试着给你一个解释。
问题在于,在过去(今天仍然部分如此),处理器没有区分内存中的代码和数据字节。这意味着 .com 文件中的任何字节都可以用作代码和数据。调试器不知道哪些字节将作为代码执行以及哪些字节将用作数据。在棘手的情况下,字节实际上可以用作代码和数据......您的程序可以在内存中创建作为代码有效的数据,并且您可以跳转到它来执行它。
在许多(但不是全部)情况下,调试器实际上可以找出什么是代码,什么是数据,但是这种代码分析可能会变得非常复杂,因此大多数调试器/反汇编器根本没有这样的代码流分析器。因此,他们只是在文件/内存中选择一个偏移量(这通常是当前指令指针),并从该偏移量开始,将一系列连续字节串行解码为汇编指令不遵循任何jmp
指示直到调试器的屏幕完全充满足够数量的反汇编行。愚蠢的反汇编器/调试器不关心反汇编的字节实际上是用作程序中的指令还是数据,它们将它们视为指令。
如果您正在调试程序并且调试器在断点处停止,那么它将获取当前指令指针,并使用原语“填充调试器屏幕”方法从该偏移量开始再次执行哑反汇编。
这种连续字节的串行反汇编是一种在大多数情况下都有效的简单方法。如果您串行解码非jmp
指令彼此跟随,您几乎可以确定处理器将按此顺序执行它们。然而,一旦你到达并解码jmp
指令您无法确定以下字节作为代码是否有效。但是,您可以尝试将它们解码为指令,希望代码中间没有混合数据(是的,在大多数情况下,在jmp
(或类似的控制流指令),这就是为什么调试器给你一个愚蠢的反汇编作为“可能有用的预测”)。事实上,大多数代码通常充满了条件跳转和反汇编它们之后的字节,因为代码对调试器来说是非常有用的帮助。跳转指令后的代码中间有数据的情况非常罕见,我们可以将其视为边缘情况。
假设您有一个简单的 .com 程序,它只是跳过一些数据,然后存在一个int 20h
:
jmp start
db 90h
start:
int 20h
反汇编程序可能会通过从偏移量 0000 开始反汇编来告诉您类似以下内容:
--> 0000 eb 01 jmp short 0003
0002 90 nop
0003 cd 20 int 20h
酷,这看起来和我们的 asm 源代码一模一样……现在让我们稍微改变一下程序:让我们改变数据……
jmp start
db cdh
start:
int 20h
现在反汇编程序将向您展示:
--> 0000 eb 01 jmp short 0003
0002 cd cd int cdh
0004 20 ...... whatever...
问题是某些指令由超过 1 个字节组成,调试器并不关心字节是否代表代码或数据。在上面的示例中,如果反汇编器从偏移量 0000 到程序末尾(包括数据)连续反汇编字节,那么您的 1 字节数据将反汇编为 2 字节指令(“窃取”实际代码的第一个字节),因此调试器尝试反汇编的下一条指令将位于偏移量 0004 而不是 0003 处,您的位置jmp
正常情况下会跳。在第一个示例中,我们没有遇到这样的问题,因为数据被反汇编为 1 字节指令,并且偶然反汇编程序的数据部分后,调试器要反汇编的下一条指令位于偏移量 0003 处,这正是您的目标jmp
.
然而,幸运的是,调试器在这种情况下向您显示的内容并不是程序执行时会发生的情况。通过执行一条指令,程序实际上会跳转到偏移量 0003,调试器将再次执行愚蠢的反汇编,但这次从偏移量 0003 开始,该偏移量位于上一个错误反汇编指令的中间...
假设您调试第二个示例程序并逐一执行其中的所有指令。当您使用指令指针 == 0000 启动程序时,调试器会显示以下内容:
--> 0000 eb 01 jmp short 0003
0002 cd cd int cdh
0004 20 ...... whatever...
然而,当您触发“step”命令来执行一条指令时,指令指针(IP)将更改为 0003,并且调试器从偏移量 0003 再次执行“哑反汇编”,直到调试器屏幕被填满,因此您将看到以下内容:
--> 0003 cd 20 int 20h
0005 ...... whatever...
结论:如果您有愚蠢的反汇编程序,并且将数据混合到代码中间(使用jmp
s 围绕数据),那么愚蠢的反汇编程序会将您的数据视为代码,这可能会导致您遇到的“小”问题。
具有流分析功能的高级反汇编程序(如 Ida Pro)将按照跳转指令进行反汇编。拆解你的后jmp
在偏移量 0000 处,它会发现下一条要反汇编的指令是jmp
在 0003 处,它会拆卸int 20h
作为下一步。它将标记db cdh
偏移量 0002 处的字节作为数据。
补充说明:
正如您已经注意到的(相当过时的)8086 指令集中的指令可以是 1-6 个字节长之间的任何位置,但jmp
or call
可以以字节粒度跳转到内存中的任何位置。指令的长度通常可以根据指令的前 1 或 2 个字节来确定。然而,仅当处理器以其特殊IP(指令指针寄存器)定位指令的第一个字节并尝试在给定偏移处执行字节时,字节才会“粘在一起”到指令中。让我们看一个棘手的例子:内存中偏移量 0000 处有字节 eb ff 26 05 00 03 00,然后逐步执行它。
--> 0000 eb ff jmp short 0001
0002 26 05 00 03 es: add ax, 300h
0006 00 ...... whatever...
处理器指令指针 (IP) 指向偏移量 0000,因此它对指令进行解码,并且其中的字节在执行时“粘在一起形成一条指令”。 (处理器在 0000 处执行指令解码。)由于第一个字节是 eb,因此它知道指令长度是 2 个字节。调试器也知道这一点,因此它会为您解码指令,并根据错误的假设生成一些额外的错误反汇编,即处理器在某些时候会在偏移量 0002 处执行指令,然后在偏移量 0006 处执行指令,等等...你会发现这不是真的,处理器会将字节以完全不同的偏移量组合成指令。
正如你所看到的,我棘手的字节代码包含一个jmp
跳转到偏移量0001,即执行的中间位置jmp
指令本身!!!然而,这根本不是问题。处理器并不关心它,而是愉快地跳转到偏移量 0001,因此下一步它将尝试解码那里的指令(或“将字节粘在一起”)。让我们看看处理器会在0001处找到什么样的指令:
--> 0001 ff 26 05 00 jmp word ptr [5]
0005 03 00 add ax, word ptr [bx+si]
正如你所看到的,我们的下一条指令位于 0001,调试器向我们展示了偏移量 0005 处的一些垃圾反汇编,这是基于处理器将在某个时刻到达该偏移量的错误假设......
0001 处的指令告诉处理器从偏移量 0005 处拾取一个字,并将其解释为跳转到那里的偏移量。正如你看到的价值word ptr [5]
是 3(作为小端 16 位值),因此处理器将 3 放入其 IP 寄存器(跳转到 0003)。让我们看看它在偏移 0003 处找到了什么:
--> 0003 05 00 03 add ax, 300h
以调试器的方式显示我棘手的字节代码 eb ff 26 05 00 03 00 的反汇编是很困难的,因为处理器执行的实际指令位于重叠的内存区域中。处理器首先执行字节0000-0001,然后执行0001-0004,最后执行0003-0005。
在一些较新的 RISC 架构中,指令的长度是固定的,它们必须位于对齐的内存区域上,并且不可能跳转到任何地方,因此调试器的工作比 x86 的情况要容易得多。