软件调试基础(四【断点和单步执行】4.4【实模式调试器例析】)

4.4.1 Debug.exe - DOS经典调试器

基本概况

特性 说明
起源 DOS操作系统自带调试工具
大小 DOS 6.22版本:15718字节
运行环境 实模式DOS或Windows兼容模式
位置 Windows系统中位于system32目录
启动方式 命令行输入debug

主要功能特点

  1. 体积小巧但功能全面

  2. 支持实模式下的完整调试功能

  3. 在Windows系统中仍保留兼容性

  4. 适合学习和实验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] 单步执行 单步执行一条或多条指令

历史意义

  1. 开发背景:在没有现成调试工具的时代,使用示波器观察CPU信号

  2. 技术积累:为Tim Paterson开发DOS打下坚实基础

  3. 影响: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. 关键实现技术

  1. 中断向量劫持:通过修改中断向量表接管异常处理

  2. 指令替换:用INT 3替换原指令实现软件断点

  3. 上下文保存:通过变量保存和恢复寄存器状态

  4. 标志位操作:通过TF标志实现单步执行

3. 历史意义

  • 8086 Monitor为DOS开发奠定了基础

  • Debug.exe影响了一代程序员的调试习惯

  • 实模式调试思想在现代调试器中仍有体现

4. 现代应用价值

  • 学习计算机系统原理的优秀工具

  • 嵌入式系统调试的简化模型

  • 理解现代调试器工作原理的基础

相关推荐
猫猫的小茶馆5 小时前
【Linux 驱动开发】七. 中断下半部
linux·arm开发·驱动开发·stm32·嵌入式硬件·mcu
沃尔特。13 小时前
直流无刷电机FOC控制算法
c语言·stm32·嵌入式硬件·算法
CW32生态社区13 小时前
CW32L012的PID温度控制——算法基础
单片机·嵌入式硬件·算法·pid·cw32
麒qiqi13 小时前
嵌入式 I2C 通信全解析:从硬件原理到驱动实现
stm32·单片机·嵌入式硬件
蓬荜生灰15 小时前
STM32(6)-- GPIO外设
单片机·嵌入式硬件
我爱我家diyer16 小时前
使用STM32的HAL库开发GD32F303CGT6
stm32·单片机·嵌入式硬件
新能源BMS佬大17 小时前
【仿真到实战】STM32落地EKF算法实现锂电池SOC高精度估算(含硬件驱动与源码)
stm32·嵌入式硬件·算法·电池soc估计·bms电池管理系统·扩展卡尔曼滤波估计soc·野火开发板
点灯小铭17 小时前
基于单片机的井盖安全监测与报警上位机监测系统设计
单片机·嵌入式硬件·毕业设计·课程设计
Hello_Embed18 小时前
USB 虚拟串口源码改造与 FreeRTOS 适配
笔记·单片机·嵌入式·freertos·usb