介绍

MS-DOS 和 Windows 98 带有一个名为的调试器程序DEBUG.EXE,可用于处理汇编语言指令和机器代码。在 MS-DOS 版本 6.22 中,这个程序被命名DEBUG.EXE并且它通常出现在C:\DOS\DEBUG.EXE. 在 Windows 98 上,该程序通常存在于C:\Windows\Command\Debug.exe. 它是一个面向行的调试器,支持各种有用的功能来处理和调试由机器代码组成的二进制可执行程序。

在这篇文章中,我们将看到如何使用这个调试器程序来组装一些将一些字符打印到标准输出的最小程序。我们首先创建一个打印单个字符的 7 字节程序。然后我们创建一个打印“hello, world”字符串的 23 字节程序。本文中提供的所有步骤也适用于 Windows 98。

「实践」使用 DOS 调试器进行编程插图

让我们首先看看如何创建一个将字符打印A到标准输出的 7 字节小程序。下面的DEBUG.EXE会议展示了我们是如何做到的。

C:\>DEBUG
-A
1165:0100 MOV AH, 2
1165:0102 MOV DL, 41
1165:0104 INT 21
1165:0106 RET
1165:0107
-G
A
Program terminated normally
-N A.COM
-R CX
CX 0000
:7
-W
Writing 00007 bytes
-Q

C:\>

现在我们可以如下执行这个程序:

C:\>A
一个
C:\>

调试器命令A根据汇编语言指令创建机器可执行代码。创建的机器码默认写入主存地址 CS:0100。前三个指令生成软件中断 0x21(十进制 33),其中 AH 设置为 2,DL 设置为 0x41(十进制 65),恰好是字符的 ASCII 码A。中断 0x21 支持多种功能。将 AH 设置为 2 会告诉此中断调用将单个字符打印到标准输出的函数。该函数要求将 DL 设置为我们要打印的字符的 ASCII 码。

该命令G从当前位置执行内存中的程序。当前位置由 CS:IP 的当前值定义,默认为 CS:0100。我们使用此命令来确认程序是否按预期运行。

接下来我们准备将机器代码写入二进制可执行文件。该命令N用于指定文件的名称。该命令W用于将机器代码写入文件。该命令要求寄存器 BX 和 CX 包含要写入文件的字节数。当 DOS 调试器启动时,BX 已经初始化为 0,所以我们只用R CX命令将寄存器 CX 设置为 7。最后,我们使用命令Q退出调试器并返回到 MS-DOS。

你好世界

以下DEBUG.EXE会话展示了如何创建一个打印字符串的程序。

C:\>DEBUG
-A
1165:0100 MOV AH, 9
1165:0102 MOV DX, 108
1165:0105 INT 21
1165:0107 RET
1165:0108 DB 'hello, world', D, A, '$'
1165:0117
-G
hello, world

Program terminated normally
-N HELLO.COM
-R CX
CX 0000
:17
-W
Writing 00017 bytes
-Q

C:\>

现在我们可以像这样执行这个 23 字节的程序:

C:\>HELLO
你好世界

C:\>

在上面的程序中,我们使用伪指令DB 来定义我们要打印的字符串的字节。我们添加尾随字节 0xD 和 0xA 以打印回车 (CR) 和换行 (LF) 字符,以便字符串以换行符终止。最后,字符串以美元符号 ( '$') 的字节结束,因为我们接下来生成的软件中断期望字符串以该符号的字节值结束。

我们再次使用软件中断 0x21。但是,这次我们将 AH 设置为 9 以调用打印字符串的函数。该函数期望 DS:DX 指向以 . 字节值结尾的字符串的地址'$'。该寄存器DS与 的值相同CS,所以我们只设置DX字符串开始的偏移量。

「实践」使用 DOS 调试器进行编程插图1

调试器脚本

我们已经在上一节中看到了如何组装一个“hello, world”程序。我们启动了调试程序,输入了一些命令,并输入了汇编语言指令来创建我们的程序。也可以准备一个单独的输入文件,其中包含所有调试器命令和汇编语言指令。然后我们将此文件提供给调试器程序。这在编写更复杂的程序时很有用,如果我们因执行非法指令而无意中使调试器崩溃,我们将无法丢失汇编语言源代码。

要创建一个可以提供给调试器的单独输入文件,我们可以使用 DOS 命令EDIT HELLO.TXT用 MS-DOS 编辑器打开一个新文件,然后键入以下调试器命令,然后保存并退出编辑器。

A
MOV AH, 9
MOV DX, 108
INT 21
RET
DB 'hello, world', D, A, '$'

N HELLO.COM
R CX
17
W
Q

这与我们在上一节中输入调试器的输入几乎相同。与上一节的唯一区别是我们在G此处省略了该命令,因为在组装程序时我们实际上并不需要运行该程序,尽管如果我们真的想这样做也可以这样做。

然后我们可以运行DOS命令DEBUG < HELLO.TXT来汇编程序并创建二进制可执行文件。这是一个 DOS 会话示例,显示了此命令的输出:

A
MOV AH, 9
MOV DX, 108
INT 21
RET
DB 'hello, world', D, A, '$'

N HELLO.COM
R CX
17
W
Q

输出实际上与上一节中的调试器会话非常相似。

「实践」使用 DOS 调试器进行编程插图2

拆卸

现在我们已经了解了如何使用调试器将简单的程序组装成二进制可执行文件,现在我们将简要了解如何反汇编二进制可执行文件。当我们想要调试现有程序时,这可能很有用。

C:\> DEBUG A.COM
-U 100 106
117C:0100 B402 MOV AH,02
117C:0102 B241 MOV DL,41
117C:0104 CD21 INT 21
117C:0106 C3 RET

调试器命令U(unassemble)用于将二进制机器码翻译成汇编语言助记符。

C:\>DEBUG HELLO.COM
-U 100 116
117C:0100 B409          MOV     AH,09
117C:0102 BA0801        MOV     DX,0108
117C:0105 CD21          INT     21
117C:0107 C3            RET
117C:0108 68            DB      68
117C:0109 65            DB      65
117C:010A 6C            DB      6C
117C:010B 6C            DB      6C
117C:010C 6F            DB      6F
117C:010D 2C20          SUB     AL,20
117C:010F 776F          JA      0180
117C:0111 726C          JB      017F
117C:0113 64            DB      64
117C:0114 0D0A24        OR      AX,240A
-D 100 116
117C:0100  B4 09 BA 08 01 CD 21 C3-68 65 6C 6C 6F 2C 20 77   ......!.hello, w
117C:0110  6F 72 6C 64 0D 0A 24                              orld..$

INT 20 与 RET

终止 .COM 程序的另一种方法是简单地使用指令INT 20。这会消耗机器代码中的两个字节:CD 20RET虽然生成尽可能小的可执行文件并不是这篇文章的真正目标,但上面的代码示例通过使用终止程序的指令来减少一点大小。这仅消耗一个字节:C3. 这是因为当一个 .COM 文件启动时,寄存器 SP 包含 FFFE。偏移 FFFE 和 FFFF 处的堆栈内存位置分别包含 00 和 00。此外,内存地址偏移量 0000 包含指令INT 20。下面是使用调试器程序对这些事实的演示:

A
MOV AH, 9
MOV DX, 108
INT 21
RET
DB 'hello, world', D, A, '$'

N HELLO.COM
R CX
17
W
Q

结果,执行RET指令会在 FFFE 处将 0000 从堆栈中弹出并将其加载到 IP 中。这INT 20会导致执行偏移 0000 处的指令,从而导致程序终止。

虽然INT 20RET在 DOS 中以及在使用 调试时都导致程序成功终止DEBUG.EXE,但它们之间存在一些影响调试体验的差异。终止程序INT 20允许我们通过重复应用G调试器命令在调试器中重复运行程序。但是当我们用 终止程序时RET,我们不能以这种方式重复运行程序。程序在我们第一次在调试器中运行时成功运行并终止,但堆栈没有用零重新初始化以准备在调试器中再次执行程序。因此,当我们第二次尝试使用G命令,程序没有成功终止。而是挂起。E FFFE 0 0可以通过在再次运行之前使用调试器命令重新初始化堆栈来解决此问题G

结论

尽管与 NASM、MASM 等复杂的汇编程序相比,DOS 调试器的功能非常有限,但这个不起眼的程序可以执行一些涉及汇编语言和机器代码的基本操作。它可以读写二进制可执行文件,检查内存,在内存中执行机器指令,修改寄存器,编辑二进制文件等。这个调试程序在MS-DOS或Windows 98系统上总是可用的事实意味着这些系统已经准备好了对于一些基本的汇编语言编程,不需要任何额外的工具。