4.4.1 Debug.exe - DOS经典调试器
基本概况
| 特性 | 说明 |
|---|---|
| 起源 | DOS操作系统自带调试工具 |
| 大小 | DOS 6.22版本:15718字节 |
| 运行环境 | 实模式DOS或Windows兼容模式 |
| 位置 | Windows系统中位于system32目录 |
| 启动方式 | 命令行输入debug |
主要功能特点
-
体积小巧但功能全面
-
支持实模式下的完整调试功能
-
在Windows系统中仍保留兼容性
-
适合学习和实验8086/8088汇编
Debug.exe命令列表(清单4-9)
| 命令 | 功能描述 | 语法示例 | 说明 |
|---|---|---|---|
| A | 汇编命令 | A [地址] |
将汇编语句翻译为机器码写入内存 |
| C | 比较内存块 | C 范围 地址 |
比较两个内存块的内容 |
| D | 显示内存 | D [范围] |
显示内存内容(十六进制和ASCII) |
| E | 编辑内存 | E 地址 [列表] |
编辑内存内容 |
| F | 填充内存 | F 范围 列表 |
用指定值填充内存区域 |
| G | 执行程序 | G [=地址] [地址...] |
执行程序,可设多个断点 |
| H | 十六进制运算 | H 值1 值2 |
计算两个十六进制数的和与差 |
| I | 输入端口 | I 端口号 |
从I/O端口读取数据 |
| L | 加载程序/扇区 | L [地址] |
加载文件或磁盘扇区到内存 |
| M | 移动内存块 | M 范围 地址 |
复制内存块到指定地址 |
| N | 指定文件名 | N 文件名 |
为L/W命令指定文件名 |
| O | 输出端口 | O 端口号 字节 |
向I/O端口输出数据 |
| P | 步过程序 | P [=地址] [值] |
执行程序、子程序或循环 |
| Q | 退出调试器 | Q |
退出Debug程序 |
| R | 显示/修改寄存器 | R [寄存器名] |
显示或修改寄存器内容 |
| S | 搜索内存 | S 范围 列表 |
在内存中搜索指定字节序列 |
| T | 单步执行 | T [=地址] [值] |
单步执行一条或多条指令 |
| U | 反汇编 | U [范围] |
将内存内容反汇编为指令 |
| W | 写文件/扇区 | W [地址] |
将内存内容写入文件或扇区 |
特殊说明:
-
X系列命令:用于扩展内存操作 -
命令不区分大小写
-
输入
?可显示命令帮助
Debug.exe典型使用场景
场景1:调试可执行程序
bash
bash
# 启动Debug并加载程序
debug program.com
# 或先启动Debug再加载
debug
N program.com
L
场景2:调试磁盘扇区
bash
bash
# 调试主引导扇区(MBR)
debug
L 100 0 0 1 # 从驱动器0读取0扇区到地址100
U 100 # 反汇编查看
场景3:学习和实验汇编
bash
bash
# 直接编写和运行汇编代码
debug
A 100 # 从地址100开始汇编
MOV AX, 1234 # 输入汇编指令
INT 20 # DOS退出调用
# 空行结束汇编
G =100 # 从地址100执行
4.4.2 8086 Monitor - Debug的前身
基本概况
| 特性 | 说明 |
|---|---|
| 开发者 | Tim Paterson(DOS之父) |
| 开发时间 | 1979-1980年 |
| 运行平台 | SCP公司的8086 CPU板 |
| 历史意义 | DOS Debug程序的先驱 |
8086 Monitor命令表(1.4A版本)
| 命令 | 功能 | 说明 |
|---|---|---|
| B <地址> | 启动 | 读取磁盘0道0扇区到内存并执行 |
| D <地址>/<范围> | 显示内存 | 显示指定内存内容 |
| E <地址> <列表> | 编辑内存 | 编辑内存内容 |
| F <地址> <列表> | 填充内存 | 用指定值填充内存区域 |
| G <地址>...<地址> | 执行并设断点 | 设置断点并执行程序 |
| I <HEX4> | 输入端口 | 从I/O端口读取数据 |
| M <范围> <地址> | 移动内存 | 复制内存块 |
| O <HEX4> <字节> | 输出端口 | 向I/O端口输出数据 |
| R [寄存器名] | 寄存器操作 | 读取或修改寄存器 |
| S <范围> <列表> | 搜索内存 | 在内存中搜索指定值 |
| T [HEX4] | 单步执行 | 单步执行一条或多条指令 |
历史意义
-
开发背景:在没有现成调试工具的时代,使用示波器观察CPU信号
-
技术积累:为Tim Paterson开发DOS打下坚实基础
-
影响:Windows NT开发团队也采用了类似策略(先开发KD调试器)
4.4.3 关键实现原理
实模式调试环境特点
| 特点 | 说明 | 对调试的影响 |
|---|---|---|
| 单任务环境 | 只有调试器和被调试程序运行 | 可直接访问所有内存 |
| 共享地址空间 | 调试器和被调试程序在同一物理地址空间 | 无需地址转换 |
| 直接内存访问 | 调试器可直接读写被调试程序内存 | 简化内存操作命令 |
| 中断向量表直接操作 | 可修改中断向量表 | 直接设置异常处理程序 |
内存布局示例(8086 Monitor)
text
高地址
+----------------+
| 8086 Monitor | ← 0xFF80开始
| (调试器代码) |
+----------------+
| |
| 可用内存空间 |
| |
+----------------+
| 被调试程序 | ← 低端内存
| |
+----------------+
低地址
中断向量设置
8086 Monitor设置的中断处理程序:
| 中断号 | 地址范围 | 用途 | 处理程序入口 |
|---|---|---|---|
| INT 1 | 0x0004-0x0007 | 单步异常处理 | REENTER |
| INT 3 | 0x000C-0x000F | 断点异常处理 | BREAKFIX→REENTER |
| INT 19H | 0x0064-0x0067 | 串口通信接收 | REENTER |
断点处理详细流程
1. 初始化阶段
cpp
assembly
; 8086 Monitor初始化代码
初始化:
; 设置中断向量表
MOV WORD PTR [0x000C], BREAKFIX ; INT 3处理程序偏移
MOV WORD PTR [0x000E], CS ; INT 3处理程序段
MOV WORD PTR [0x0004], REENTER ; INT 1处理程序
MOV WORD PTR [0x0006], CS
MOV WORD PTR [0x0064], REENTER ; INT 19H处理程序
MOV WORD PTR [0x0066], CS
2. 断点命中处理
cpp
assembly
; BREAKFIX: INT 3处理程序入口
BREAKFIX:
XCHG SP, BP ; 交换SP和BP,BP指向栈顶
DEC WORD PTR [BP] ; 将栈顶的IP值减1
XCHG SP, BP ; 恢复SP和BP
; 此时IP已调整为指向INT 3指令本身
; 继续执行到共享的异常处理代码
; REENTER: 共享的异常处理入口
REENTER:
; 保存寄存器到变量区
MOV [SPSAVE], SP
MOV [DSSAVE], DS
; ... 保存其他寄存器
; 显示寄存器值
CALL CRLF ; 换行
CALL DISPREG ; 显示寄存器
; 检查单步计数
DEC TCOUNT ; 单步计数器减1
JNZ STEP1 ; 如果不为0,继续单步
; 检查断点计数
CMP BRKCNT, 0 ; 比较断点计数器
JG CLEANBP ; 如果>0,跳转到清除断点
JMP COMMAND ; 否则跳转到命令等待
3. 断点设置流程(G命令)
cpp
assembly
; G命令处理流程
处理_G命令:
; 解析地址参数(最多10个)
; 对每个断点地址:
1. 保存到断点表
2. 读取原字节保存到备份表
3. 写入0xCC(INT 3指令)
; 设置单步计数器
MOV TCOUNT, 1
; 跳转到异常返回代码
JMP EXIT
4. 异常返回与恢复执行
cpp
assembly
; EXIT: 异常返回代码
EXIT:
; 恢复栈指针
MOV SP, [SPSAVE]
; 将保存的寄存器值压栈(用于IRET)
PUSH [FSAVE] ; 标志寄存器
PUSH [CSSAVE] ; 代码段寄存器
PUSH [IPSAVE] ; 指令指针
; 恢复数据段寄存器
MOV DS, [DSSAVE]
; 中断返回,恢复执行
IRET
关键变量说明
| 变量名 | 作用 | 对应寄存器 |
|---|---|---|
| SPSAVE | 保存栈指针 | SP |
| DSSAVE | 保存数据段 | DS |
| FSAVE | 保存标志寄存器 | FLAGS |
| CSSAVE | 保存代码段 | CS |
| IPSAVE | 保存指令指针 | IP |
| TCOUNT | 单步执行计数器 | - |
| BRKCNT | 断点个数计数器 | - |
调试器核心机制
1. 断点实现原理
cpp
// 断点设置伪代码
void SetBreakpoint(address) {
// 1. 保存原指令字节
original_byte = ReadMemory(address);
backup_table[address] = original_byte;
// 2. 写入INT 3指令
WriteMemory(address, 0xCC);
// 3. 更新断点计数
breakpoint_count++;
}
// 断点清除伪代码
void ClearBreakpoints() {
for (each breakpoint in breakpoint_table) {
// 恢复原指令字节
WriteMemory(breakpoint.address,
backup_table[breakpoint.address]);
}
breakpoint_count = 0;
}
2. 单步执行实现
cpp
// 单步执行实现
void SingleStep() {
// 设置TF标志(单步陷阱)
saved_flags = ReadFlags();
saved_flags |= 0x0100; // 设置TF位(第8位)
WriteFlags(saved_flags);
// 恢复执行(会立即触发单步异常)
ResumeExecution();
}
3. 修改执行流程
cpp
// 修改IP实现"跳转"
void JumpToAddress(address) {
// 修改保存的IP值
IPSAVE = address;
// 下次恢复执行时会从新地址开始
// 因为IRET会从栈中加载CS:IP
}
实模式调试器实操指南
Debug.exe基本操作流程
步骤1:启动和加载程序
bash
# 方法1:直接加载程序
debug program.com
# 方法2:先启动后加载
debug
N program.com # 指定文件名
L # 加载程序
步骤2:查看和修改内存
bash
# 查看内存内容(从地址100开始)
D 100
# 编辑内存(从地址200开始)
E 200
# 依次输入要修改的字节值
# 填充内存(用FF填充100-1FF区域)
F 100 200 FF
步骤3:设置和执行断点
bash
# 从地址100开始执行,在150、200设断点
G =100 150 200
# 单步执行(从当前地址执行一条指令)
T
# 执行多步(从地址100执行10条指令)
T =100 10
步骤4:查看和修改寄存器
bash
# 查看所有寄存器
R
# 修改AX寄存器
R AX
# 显示当前值并提示输入新值
步骤5:汇编和反汇编
bash
# 从地址100开始反汇编
U 100
# 从地址200开始编写汇编代码
A 200
MOV AX, 1234
INT 20
# 空行结束汇编
8086 Monitor调试会话示例
text
监控器启动... 显示提示符 *
* D 0:7C00 ; 显示主引导扇区内容
* A 100 ; 从地址100开始汇编
MOV AX, 1234
INT 3 ; 设置断点
RET
* G =100 ; 从100执行,遇到INT 3中断
显示寄存器状态...
* R IP ; 查看IP寄存器
IP=0103
* R IP=100 ; 修改IP返回100
* T ; 单步执行
显示寄存器变化...
* Q ; 退出
与现代调试环境的对比
| 特性 | 实模式调试器 | 现代保护模式调试器 |
|---|---|---|
| 内存访问 | 直接物理内存访问 | 通过操作系统API访问 |
| 地址空间 | 共享地址空间 | 独立地址空间 |
| 断点设置 | 直接写入INT 3指令 | 通过调试API或硬件断点 |
| 权限要求 | 无特别权限要求 | 需要调试权限 |
| 多任务支持 | 不支持 | 支持多进程/线程调试 |
| 符号支持 | 无符号调试 | 支持符号文件(PDB) |
学习要点总结
1. 实模式调试的核心特点
-
直接性:调试器可直接操作所有系统资源
-
简单性:无需操作系统复杂的调试支持
-
教育价值:适合学习CPU和调试的基本原理
2. 关键实现技术
-
中断向量劫持:通过修改中断向量表接管异常处理
-
指令替换:用INT 3替换原指令实现软件断点
-
上下文保存:通过变量保存和恢复寄存器状态
-
标志位操作:通过TF标志实现单步执行
3. 历史意义
-
8086 Monitor为DOS开发奠定了基础
-
Debug.exe影响了一代程序员的调试习惯
-
实模式调试思想在现代调试器中仍有体现
4. 现代应用价值
-
学习计算机系统原理的优秀工具
-
嵌入式系统调试的简化模型
-
理解现代调试器工作原理的基础