X86的主要调试设施

INT 3 指令 软件断点基础
追踪标志TF 单步追踪的基础
调试寄存器 硬件寄存器的基础
分支监视和记录 按分支单步的基础

软件断点

0xCC int3指令
机器码为1字节,无数量限制,只支持代码段(可执行的段),且不支持在只读存储器中使用

将一个int 3 写入,触发异常break之后会被恢复,再执行后会再次写入

硬件断点

通过调试寄存器实现,DR0到DR3 的四个寄存器中存放目标地址
DR7有8组设置标志位,每组分别有2个2位标志,其中一个标志表示R/W,另一个标志表示长度
CPU每执行1步就进行一次匹配,如果是这个地址,模式正确,就会写入DR6的标志位,然后触发断点(产生1号异常),操作系统通过检查标志位知道哪一个命中了断点

由于每个线程的寄存器信息独立保存,所以每个线程都可以设4个地址

陷阱标志

标志寄存器efl(ag)的TF位
单步异常在efl的最低位。每当efl最后一位是1,系统会自动置0,然后触发单步异常

异常

前面几节已经差不多说明了,这里就不赘述了。不过在程序中,尤其是VS生成的代码,一般来说低级的异常往往会封装成Cxx异常。

中断向量表

1
2
3
4
5
6
7
8
9
10
0 除零异常
1 调试异常
3 断点异常
4 溢出异常
5 数组越界异常
6 非法指令异常
13 保护异常
14 缺页异常
18 机器检查异常
32-255 用户自定义,很多是中断,而不是异常

来源

CPU产生 执行指令检测到的错误,机器异常,调试异常等
程序产生,RaiseException win32api
C++ throw E

JTAG

硬件调试标准

用户态调试模型XP

断点命中时,CPU立即切换到内核执行一系列异常处理函数。其中KISystemService调用KiRaiseException()产生异常KiDebugService和KiTrap04~00做分发准备。KiDispatchException()是分发中枢,会通知用户态调试子系统Dbgk,然后触发DbgkForwardExcption(),如果有调试器执行DbgkpSendApiMessages()继续传递,到DbgkQueueMessage()生成调试事件并将产生的调试对象挂到内核调试对象队列中。调试器一直等待这个队列,有调试对象时就会取出并处理。

调试器的载入程序

一般情况下,会在目标进程创建一个线程执行int 3,但是系统在分发异常时会冻结所有线程

KiDisPatchException

用户态异常内含2轮异常分发:
第一轮:

如果没有用户态调试器,尝试分发给内核调试器
DbgkForwardException尝试交给用户态调试器
复制栈帧然后改程序指针到KeUserExceptionDispatcher到用户态

第二轮:

DbgkForwardException尝试交给用户态调试器
然后尝试交给服务进程,让服务进程做最后处理,一般直接杀死
如果还是不处理,直接调用ZwTerminateProcess在内核态杀死

产生硬件异常通过 IDT调用异常处理例程, 产生软件异常通过 API的层层调用产地异常信息。而异常又由于发生位置不同,分为内核异常和用户态异常,二者最后都会靠 kiDispathException函数来进行异常分发;
当内核产生异常时,程序处理流程进入到 KiDispatchException函数,在该函数内备份当前线程 R3 的 TrapFrame(即栈帧的基址)。异常处理首先判断这是否是第一次异常,判断是否存在内核调试器,如果有内核调试器,则把当前的异常信息发送给内核调试器;如果没有内核调试器或者内核调试器没有处该异常 , 则进入步骤3,调用 RtlDispatchException
内核异常进入 RtlDispatchException 函 数, 如果RtlDispatchException 函数没有处理该异常,那么将再次尝试将异常发送到内核调试器,如果此时内核调试器仍然不存在或者没有处理该异常,那么此时系统会直接蓝屏;
如果是用户态异常则经过 KiDispatchException进行用户态异常分发和处理。如果是第一次分发异常,则调用 DbgKForwardException将异常分发到内核调试器;如果内核调试器不存在或没有处理异常,则尝试将异常分发给用户态调试器;如果异常被处理,则进入步骤10;如果用户态调试器不存在或未处理异常,则检测是否是第一次处理异常,如果是第一次处理异常则进入第5步中的异常数据准备;
准备一个返回ntdll!KiUserExceptionDispatcher 函数的应用层调用栈,结束本次KiDispatchException 函数的运行,调用KiServiceExit 返回用户层。此时函数栈帧是ntdll!KiUserExceptionDispatcher的执行环境,用户态线程从执行 ntdll!KiUserExceptionDispatcher 开始执行。该函数调用 ntdll!RtlDispatchException进行异常的分发,进入第 6 步;
通过 RtlCallVectoredExceptionHandlers遍历 VEH链表尝试查找异常处理函数;如果 VEH未处理异常。则从 fs[0]读取 ExceptionList并开始执行 SEH 函数处理,进入步骤7;
如果SEH没有处理函数处理该异常,则检查用户是否通过SetUnhandledExceptionFilter函数注册过进程的异常处理函数,如果用户注册过异常处理函数,调用该异常处理函数,如果异常没有被成功处理或没有自定义的异常处理函数,则进入步骤3;
如果最后仍没有处理该异常,便会主动调用 NtRaiseException将该异常重新跑出来,但是此时不是第一次分发,此时 NtRaiseException流程重新调用了 ntdll!KiDispatchException,并再次进入用户态异常的处理分支,进入步骤9;
第二次进入用户态异常处理时,不会再尝试发送到内核调试器,也不会再进行异常分发,而是直接尝试发送到用户态体异常调试器,如果最后异常仍未被处理则进入步骤11;
异常被处理,调用 NtContine,将之前保存的 TrapFrame还原,程序继续从异常处正常运行;
异常不能被处理,系统调用 ntdll!KiDispatchException 调用 ZeTerminateProcess结束进程。


WinDbg常见指令

1
2
3
4
5
6
7
8
9
10
bp addr 下软件断点
ba addr 下硬件断点
g 执行
u addr 看对应地址指令
k 看栈回溯
~* 看当前线程
~num k 看某个(序号为num)线程的栈回溯
x moduleName!*funcName 在对应位置设置断点
bp moduleName!funcName "指令" 断点命中后执行指令,比如".echo *********;k;gc"自动k然后go
sxe ld 模块加载时停止

以Noninvasive模式可以只读的形式附加

参考资料

2.概览和软件断点_哔哩哔哩_bilibili

windows SEH分析 - 知乎

Windows异常世界历险记(二)——Win32用户层下SEH机制之对RtlUnwind的逆向分析-CSDN博客