Skip to content

07_调试

一、 源码级调试环境准备

要实现“以源代码的方式”来调试汇编程序,必须在编译和链接阶段将调试符号信息(Debug Information)写入最终的可执行文件中。

以 Borland 的 TASM/TLINK 工具链为例,编译步骤如下:

  1. 编译阶段 (TASM):

    tasm /zi hello.asm
    
    • /zi 参数:指示 TASM 在生成的 .obj 目标文件中包含完整的行号和局部符号等调试信息。
  2. 链接阶段 (TLINK):

    tlink /v hello.obj
    
    • /v 参数:指示 TLINK 将目标文件中的调试信息整合,并附加到最终生成的 .exe 可执行文件中。
  3. 启动调试:

    td hello.exe
    
    • 使用 Turbo Debugger (TD) 加载带有调试信息的程序。

二、 断点机制的底层原理剖析

断点是调试器的核心功能,它迫使 CPU 在特定的指令处暂停执行,并将控制权交还给调试器。在 x86 架构下,断点主要分为软件断点和硬件断点两种截然不同的实现机制。

1. 软件断点 (Software Breakpoints)

软件断点是最常用的断点类型,数量不受限制。

  • 底层原理:在调试器中某行代码下断点时,调试器会在内存中将该指令的首字节动态替换为机器码 0CCh。

  • 触发机制:0CCh 是单字节中断指令 INT 3 的机器码。当 CPU 执行到被修改的这个字节时,会触发 3 号中断,CPU 自动保存当前上下文,并跳转到调试器注册的异常处理例程。

  • 恢复执行:当用户选择继续运行时,调试器会将 0CCh 恢复成原来的指令字节,将指令指针 (IP) 回退一步,然后继续执行。

  • 局限性:必须修改内存代码,无法应用于只读内存(如 ROM),且容易被程序的反调试逻辑检测。

2. 硬件断点 (Hardware Breakpoints)

硬件断点完全依赖于 CPU 内部专门的调试寄存器 (Debug Registers) 实现。

  • 调试寄存器体系:CPU 提供了 DR0、DR1、DR2、DR3 共 4 个地址寄存器,用于存储断点的线性地址。同时配合 DR7(控制寄存器,定义断点条件和长度)与 DR6(状态寄存器,记录触发原因)。

  • 数量限制:由于物理寄存器只有 DR0~DR3,因此硬件断点最多只能同时设置 4 个。

  • 触发机制:CPU 在执行指令或总线周期中,硬件电路会实时比对当前访问的内存地址与 DR0~DR3 中的值。一旦命中且符合 DR7 设置的条件,CPU 硬件将直接抛出 单步异常 INT 1,通知调试器接管。

  • 断点类型分类:

    • 指令处 (执行断点):当 CS:IP 指向特定地址准备执行该指令时触发。

    • 变量地址处 (数据访问断点):当特定内存地址的数据被读取或写入时触发。常用于追踪全局变量被哪个未知函数篡改。

  • 优势:不需要修改目标代码的任何字节。可在 ROM 区域下断,极为隐蔽,且支持数据访问拦截。

三、 反调试技术初步:检测软件断点

由于软件断点通过插入 0CCh 篡改了代码,程序可以通过读取自身的代码段 (CS) 来检查是否被下了断点。

代码实例解析:

    mov cx, 10
next:
    mov ah, 2       ; 假设调试器在此处下了一个软件断点
    mov dl, 'A'
    int 21h

    ; ---- 反调试检测逻辑 ----
    mov al, byte ptr cs:[next]  ; 读取 next 标签处的第一个字节代码
    cmp al, 0CCh                ; 检查是否被篡改为 INT 3 (0CCh)
    je done                     ; 若发现 0CCh,直接跳过核心逻辑 (反调试生效)

    sub cx, 1
    jnz next
done:
    mov ah, 4Ch
    int 21h

四、 Turbo Debugger (TD) 与 Quickview 工具

1. Turbo Debugger (TD) 核心操作

  • 多视图切换 (Ctrl+M):在源代码视图 (Source)、反汇编视图 (Assembly)、混合视图 (Mixed) 之间无缝切换。

  • 基础步进:F7 单步步入 (Trace Into),F8 单步步过 (Step Over),F2 切换断点, F4 运行到光标处。

2. Quickview 二进制修改工具

在逆向工程或破解中,经常需要直接修改可执行文件的二进制机器码(如修改跳转指令 74 JZ 为 75 JNZ),此时需要使用如 QuickReview 这类十六进制/二进制直接编辑工具。

  • 核心操作流程与快捷键:

    • 启动与载入:在 DOS/命令行下输入 qv target.exe 载入目标文件。

    • F1 (Help / 帮助):呼出工具的内置热键与命令帮助手册,这是熟悉操作界面的第一步。

    • 查看模式与编辑模式:通常工具默认处于只读的浏览模式。需要按下 Enter 切换到十六进制编辑状态。

    • Alt+F5 (Save / 保存修改):完成机器码的修改后,按下 Alt+F5 会将内存中的改动直接回写并覆盖到磁盘上的二进制文件中。此操作不可逆,修改前通常需要备份源文件。

五、 SoftICE 系统级调试

SoftICE 是系统内核级常驻内存 (TSR) 调试器,拥有最高的系统权限。

1. 核心运行与界面操作

  • Ctrl + D:呼出热键。强行中断操作系统的运行,冻结当前所有线程,弹出 SoftICE 全屏调试界面。

  • rs (Restore Screen) 或 F5:切换用户界面。调试过程中,若程序进行了屏幕输出,按 F5 或输入 rs 可以隐藏调试器界面,切换回被调试程序的实际画面,查看输出结果,任意键切回。

  • ec (Enter Code Window):进入代码窗口。将输入焦点强制转移到代码窗口区,方便通过键盘上下翻页浏览当前 CS:IP 附近的汇编指令。

2. 增强型功能与修改指令

  • a (Assemble / 汇编):在线汇编功能。输入 a [地址] 后,可以直接用人类可读的汇编助记符(如 mov ax, 1)重写该地址的指令。SoftICE 会实时将其编译为机器码并写入内存。

  • exit:强制退出。在某些环境下,用于强制关闭当前的 DOS 虚拟机或会话窗口。

  • map (Memory Map / 显示内存布局):

    • 作用:用于列出当前系统的内存段分配、物理内存映射以及各内存块的基地址。

    • 长度单位注意:map 指令返回结果中的 Length(长度)字段,其单位是“节”(Paragraph),即 10h(16)字节,而非单纯的字节数。

    • 中断向量表:若在内存顶端看到 0000:0000 Length 0040,将其转换为常规字节数:\(0040h \times 10h = 0400h\) 字节(即十进制的 1024 字节)。由于 x86 实模式下共有 256 个中断(00h~0FFh),每个中断向量(段地址+偏移量)占用 4 字节,\(256 \times 4 = 1024\) 字节,因此该区域恰好完整容纳了所有的中断向量,此块内存即为系统的“中断向量表(IVT)”。

  • ? (Evaluate / 表达式计算):

    • 作用:SoftICE 内置的快速计算器。直接输入 ? [表达式](例如 ? 40 * 10),可在命令区实时输出十六进制、十进制、八进制、二进制的计算结果与字符 ASCII 码,常用于调试过程中的进制转换、地址偏移量计算。
  • u (Unassemble / 反汇编):

    • 作用:将指定内存地址的机器码翻译为人类可读的汇编指令并显示在代码窗口中。

    • 语法:u [地址](例如 u cs:0100 或 u ds:1000)。

    • 核心快捷键 .:在 SoftICE 中,输入一个小数点 . 并回车,是 u cs:ip 的极简缩写形式。它会立刻将代码窗口的视图强制拉回到 CPU 当前正在执行的指令位置。

  • d (Display / 显示内存数据):

    • 作用:在数据窗口(Data Window)中以十六进制和 ASCII 码的形式查看指定内存地址的原始数据(类似 Dump 功能)。

    • 语法扩展:直接输入 d [地址] 默认按字节显示。为了更精准地查看特定数据结构,可附加数据宽度后缀:

      • db [地址]:按字节 (Byte, 8位) 显示。

      • dw [地址]:按字 (Word, 16位) 显示。

      • dd [地址]:按双字 (Double Word, 32位) 显示。

  • e (Edit / 编辑内存数据):

    • 作用:直接在底层修改特定内存地址的数据内容。与 a 指令(写入汇编代码)不同,e 指令用于直接写入十六进制机器码或原始数据。

    • 语法:e [地址] [数据列表]。如果不带数据列表直接输入 e [地址],SoftICE 会进入交互编辑模式,按空格键跳到下一个字节,按回车键结束修改。

    • 语法扩展:与 d 指令同理,严格区分修改的数据宽度:

      • eb [地址] [数据]:修改单字节(例如 eb ds:0040 00 将该处字节清零)。

      • ew [地址] [数据]:修改字。

      • ed [地址] [数据]:修改双字。

    • 联合应用场景:在破解或绕过反调试时,常先用 u 定位到条件跳转指令(如 74 JZ),然后用 eb 直接将该地址的机器码修改为 75 (JNZ) 或 EB (JMP)。

3. 断点指令体系

A. 内存/硬件断点 (BPM / BPMB)

利用 CPU 的 DR0~DR3 寄存器实现,最多 4 个。

  • 语法:bpm [地址] [条件 r/w/x]

  • bpmb (Breakpoint on Memory Byte):针对变量地址处的精准断点。专门针对单字节内存地址设置硬件断点。例如:bpmb ds:0040 rw,当数据段 0040 处的变量(1个字节)被读写时,触发硬件中断。

B. 执行/软件断点 (BPX)

利用插入 0CCh (INT 3) 实现,针对指令处。

  • 用法:bpx cs:0100 或 bpx API函数名。

C. 断点与流控制

  • 管理:bl (列表), bc (清除), bd (禁用), be (启用)。

  • 步进:t (步入), p (步过), g (继续执行)。

调试 INT 1(单步中断)程序的注意事项:

  • 当在 SoftICE 中深入调试自定义的 INT 1 中断服务例程时,在程序最后的 iret(中断返回)指令处,不能使用 F8(Step Over / p)或 F7(Trace Into / t)等单步执行指令。

  • 原理:SoftICE 自身的“单步调试”机制,高度依赖于对 CPU 标志寄存器中 TF(Trap Flag,陷阱标志位) 的控制。当执行 iret 时,CPU 会从堆栈中强行弹出并恢复原程序的 FLAGS 寄存器。如果在 iret 处强行使用单步,会直接冲突并破坏调试器本身对 TF 位的内部设置,导致调试器失去对 CPU 的控制流接管,引发程序跑飞、死机或调试器自身崩溃。

  • 正确流控手段:在 iret 即将返回的目标下一条指令地址处,手动使用 bpx 或 bpm 设置一个明确的断点。断点落实后,在命令行中输入 x(Exit 隐藏调试器界面并向下执行)或者 g,让 CPU 全速自由执行完 iret 的弹栈过程,直至触发刚刚设下的用户断点,从而安全交回控制权。

六、 Bochs 内置调试器

Bochs 是一款全量模拟 x86 硬件的虚拟机,它的内置调试器(Bochs Debugger)运行在“宿主机”层面,目标操作系统甚至无法察觉到它的存在,对付任何反调试手段均处于绝对降维打击。

1. 断点设置 (Breakpoints)

Bochs 将断点严格区分了物理地址、线性地址和虚拟地址。

  • pb (Physical Breakpoint / 物理断点):

    • 语法:pb [物理地址]

    • 原理:直接在硬件的内存物理地址上下断。即使操作系统发生了分页切换或段式地址重映射,只要 CPU 执行到该物理内存单元,立刻断下。

  • vb (Virtual Breakpoint / 虚拟断点):

    • 语法:vb [段寄存器:偏移量] (例如 vb cs:0100)

    • 原理:基于实模式或保护模式下的逻辑地址下断。

  • lb (Linear Breakpoint / 线性断点):

    • 语法:lb [线性地址]

    • 原理:基于经过段机制转换后、分页机制转换前的线性地址下断。

2. 内存监视点 (Watchpoints)

对应了硬件数据断点的功能,用于监视变量地址处的数据访问。

  • watch read [物理地址]:当目标物理地址的数据被 CPU 读取时断下。

  • watch write [物理地址]:当目标物理地址的数据被 CPU 修改/写入时断下。

  • 注意:Bochs 的 watch 命令监控的是物理内存地址,必须将程序中的逻辑地址转化为物理地址才能精准拦截。

3. 核心调试流控制与查看指令

  • 步进控制:

    • c (Continue):全速运行虚拟机,直到遇到下一个 pb/vb 或 watch 命中。

    • s (Step):单步执行(步入)。

    • n (Next):单步步过,不进入函数内部。

  • 状态查看:

    • info cpu:查看当前 CPU 的所有核心状态和寄存器。

    • r / info r:显示通用寄存器、段寄存器及 EFLAGS 标志位。

    • x [地址]:查看线性地址的内存数据。

    • xp [地址]:查看物理地址的内存数据(Examine Physical)。

    • u [地址]:对目标地址进行反汇编查看。