为什么反汇编数据会变成指令?

Why are disassembled data becoming instructions?

我需要一些帮助来理解当 此代码片段 "happens":"jmp Begin"。 我只知道 .com 文件可以是 64kb,所以您想将所有内容都放在一个段中。如果要放置变量,则需要 jmp 。但是当我搜索它时,许多指南只是在评论中说 jmp Begin 只是跳过数据而不是其他。这是我的问题: 这一刻到底发生了什么:

它似乎运行这个

        mov     al, a
        mov     bl, b
        sub     al, bl

但我不明白为什么它在 turbo 调试器中看起来像这样。 当我将 Result 的起始值从 ?大于 0 的值会变为其他值,例如当我将其更改为 90 时,它看起来完全正常。我对装配完全陌生,我似乎根本无法掌握它。这是我的全部代码:

            .MODEL TINY

Code        SEGMENT

            ORG    100h
            ASSUME CS:Code, DS:Code

Start:
                jmp     Begin
a               EQU     20
b               EQU     10
c               EQU     100
d               EQU     5
Result          DB      ?


Begin:

            mov     al, a
            mov     bl, b
            sub     al, bl
            mov     bl, c
            mul     bl
            mov     bl, d
            div     bl              
            mov     Result, al
            mov     ah, 4ch
            int     21h

Code        ENDS
            END             Start

我试着给你一个解释。

问题是在过去(今天部分仍然如此)处理器不区分内存中的代码和数据字节。这意味着您的 .com 文件中的任何字节都可以用作代码和数据。调试器不知道哪些字节将作为代码执行,哪些字节将用作数据。在棘手的情况下,一个字节实际上可以同时用作代码和数据...您的程序可以在内存中创建作为代码有效的数据,您可以跳到它上面执行它。

在许多(但不是所有)情况下,调试器实际上可以找出什么是代码,什么是数据,但是这种代码分析可能会变得非常复杂,所以大多数 debuggers/disassemblers 根本没有这样的代码流分析器.出于这个原因,他们只是在你的 file/memory 中选择一个偏移量(这通常是当前指令指针),并从这个偏移量开始,他们将一系列连续的字节解码为串行 的汇编指令,而不遵循任何 jmp 指令 直到调试器的屏幕完全充满足够数量的反汇编行。 Dumb disassemblers/debuggers 不关心反汇编的字节是否在您的程序中实际用作指令或数据,他们将它们视为指令。

如果您正在调试程序并且调试器在断点处停止,那么它会获取当前指令指针并使用原始 "fill the debugger screen" 方法从该偏移量开始再次执行哑反汇编。

这种连续字节的串行反汇编是一种在大多数情况下都有效的简单方法。如果您连续解码非 jmp 指令,那么您几乎可以确定处理器将按此顺序执行它们。但是,一旦您到达并解码 jmp 指令,您就无法确定以下字节作为代码是否有效。但是,您可以尝试将它们解码为指令,希望没有数据混入代码中间(是的,在大多数情况下,jmp(或类似的控制流指令)之后没有数据,这是为什么调试器给你一个 愚蠢的反汇编作为 "possibly useful prediction")。事实上,大部分代码通常充满了条件跳转,并且反汇编它们之后的字节,因为代码是来自调试器的非常有用的帮助。跳转指令后在代码中间有数据的情况很少见,我们可以将其视为边缘情况。

让我们假设您有一个简单的 .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 字节指令("stealing" 实际代码的第一个字节)因此调试器尝试反汇编的下一条指令将出现在偏移量 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 再次执行 "dumb disassembling",直到调试器屏幕填满所以你会看到这个:

--> 0003   cd 20      int 20h
    0005   ...... whatever...

结论:如果您有笨拙的反汇编器,并且您将数据混合到代码的中间(数据周围有 jmps),那么笨拙的反汇编器会将您的数据视为代码,这可能会导致 "minor" 您遇到的问题。

具有流分析功能的高级反汇编程序(如 Ida Pro)会按照跳转指令进行反汇编。在偏移量 0000 处反汇编 jmp 后,它会发现下一条要反汇编的指令是 jmp 在 0003 处的目标,并且下一步将反汇编 int 20h。它会将偏移量 0002 处的 db cdh 字节标记为数据。

补充说明:

正如您已经注意到的(相当过时的)8086 指令集中的一条指令可以在 1-6 字节长之间的任何位置,但是 jmpcall 可以跳转到内存中的任何位置粒度。指令的长度通常可以从指令的前 1 或 2 个字节确定。但是,仅当处理器将指令的第一个字节作为其特殊 IP(指令指针寄存器)的目标并尝试在给定偏移量处执行字节时,才将字节 "stick together" 写入指令。让我们看一个棘手的例子:你在内存中的偏移量 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,因此它在执行时解码指令和那里的字节 "stick together into an instruction"。 (处理器在 0000 处执行指令解码。)由于第一个字节是 eb,它知道指令长度是 2 个字节。调试器也知道这一点,因此它会为您解码指令,并且还会根据错误的假设生成一些额外的反汇编错误,即在某些时候处理器将在偏移量 0002 处执行指令,然后在偏移量 0006 处执行指令,等等......当你会发现这不是真的,处理器会将字节组合成完全不同偏移量的指令。

如你所见,我的棘手字节代码包含一个 jmp,它跳转到偏移量 0001,它位于已执行的 jmp 指令本身的中间!然而,这根本不是问题。处理器不关心它并愉快地跳转到偏移量 0001,因此下一步它将尝试解码那里的指令(或 "stick together bytes")。让我们看看处理器会在 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 的情况容易得多。

任何字节序列都可以解码为 x86 指令。 x86 机器代码使用大部分编码 space,因此只有少数位模式不是有效指令。您有时会看到 (bad) 或类似的指示,表明字节不代表有效的 x86 指令。

如果您有一些字节不希望 CPU 作为代码执行,只需确保执行永远不会到达它们。 jmp 在非代码块上,并在执行进入非代码之前进行 exit 系统调用。 (或者永远循环)。

一些反汇编程序试图在将哪些字节反汇编为代码时变得聪明,但由于 x86 代码可以计算寄存器中的地址并跳转到该地址,因此并不总是能够确定将跳转到哪些地址。