天天看点

调试器相关笔记

调试器的工作流程

1 调试器进程和目标进程

1.1 调试器进程和目标进程是两个独立的进程.两个进程拥有独立的内存地址.

1.2 一般情况下,调试进程和目标进程的关系有两种,主要区别在于目标进程和调试进程谁先运行:

1.2.1 目标进程由调试器进程创建(调试进程已经运行,目标进程尚未运行,调试器进程通过CreateProcess()创建目标进程.)

1.2.2 调试器进程附加到目标进程上(目标进程运行之后调试器进程才运行),调试器使用DebugActiveProcess()附加到目标进程.

2 调试事件

2.1 调试事件由目标进程产生,调试器进程接收.

2.2 调试事件的种类一共有以下类型:

2.2.1 进程创建和进程退出事件(在目标进程的主进程或子进程创建和退出时产生)

2.2.2 线程创建和线程退出事件(在目标进程创建或退出线程时产生)

2.2.3 DLL加载和DLL卸载事件(在目标进程加载和卸载DLL时产生)

2.2.4 调试字符串输出事件(在目标进程调用OutputDebugString()时产生)

2.2.5 内部错误事件(在目标进程中发生内部错时产生)

2.2.6 异常事件(在目标进程中执行代码中发生异常时)

2.3 目标进程只要产生任意一种调试事件,系统都会将目标进程挂起,让其处于停止运行的状态,随后将调试事件和描述调试事件的信息结构体(DEBUG_EVENT)发送给调试器进程,调试器进程使用WaitForDebugEvent()来接收这些调试事件,并使用ContinueDebugEvent()来让目标进程重新运行.

3 异常事件

3.1 异常事件是调试事件中的一个种类.

3.2 在异常事件中,异常被细分为多种类型的异常:

3.2.1 断点异常(目标进程执行int 3指令就会产生这种异常)

3.2.2 单步异常(目标进程的标志寄存器中的TF标志位被设为1时,就会产生这种异常)

3.2.3 单步异常(目标进程的DR0~DR3寄存器中被赋值为一个地址,并DR7寄存器被配置时,也会产生这种异常.这种异常和TF异常使用的是同一个类型,但是两者是不相同的).

3.2.4 内存访问异常(当目标进程操作了一个没有相应权限的内存地址时会产生这种异常).

3.2.5 一些其他类型的异常(对于调试器来说,这些异常都不是很重要,可以忽略)

3.3 异常事件的描述信息结构体:

3.3.1 异常事件的描述信息保存在DEBUG_EVENT结构体中,当这个结构体的dwDebugEventCode等于EXCEPTION_DEBUG_EVENT时, DEBUG_EVENT中的EXCEPTION_DEBUG_INFO结构体就记录着异常事件的描述信息,描述信息里面包含有异常的类型,发生异常的地址.

4 设置断点的原理

4.1 在目标进程中设置断点实际上就是在目标进程中人为地制造出调试事件,使目标进程被系统暂停,并通知调试器.所以只有目标进程产生了调试事件,目标进程才会处于停止的挂起状态,系统通知调试器之后,调试器才能够知道目标进程已经运行到了何处.进程创建和进程退出事件,线程创建和线程退出事件,DLL加载和DLL卸载事件在特殊情况下才会产生,因此,一般不会有人使用这几种调试事件来作为断点.所以,在所有的调试事件中,只有异常事件是适合用来设置断点的.

调试器相关笔记

4.2 修复异常的重要性

如果异常是调试器自己人为制造的, 那么如果想让目标进程重新跑起来就必须先把异常修复.如果不修复异常就让目标进程跑起来,那么,目标进程会继续执行会触发异常的指令,使得目标进程再次产生异常,异常又被再次发送到调试器中,如此反复,就陷入到了死循环中.因此,调试器在接收到异常之后,如果异常是自己设置的,则必须要将异常修复.

4.3 修复异常的方法

4.3.1 软件断点异常也被称为int 3异常,该异常产生的原因是目标进程执行了 int 3指令.该异常产生之后,调试器会捕获到该异常并发送给调试器. 而所有异常的描述信息都是保存在ECEPTION_DEBUG_INFO结构体中的,这个结构体的第一个字段是一个结构体,叫做EXCEPTION_RECOREAD,EXCEPTION_RECOREAD结构体中有一个字段就是用来保存产生异常的地址的。目标进程产生异常之后,目标进程的线程环境块中的EIP会指向int 3指令的地址之后,因此,调试器要修复int 3异常,需要将目标进程的线程环境块的EIP减一,随后将异常地址的第一字节的0xcc还原成原来的内容(int 3指令的机器码就是0xcc)。

4.3.2 单步断点异常也称为TF断点异常,是由目标进程的线程环境块中的EFlag寄存器里的TF标志位来控制的,当TF标志位==1的时候,目标进程每执行一条指令就会产生一次异常,产生异常之后,TF标志位会被CPU重新置零,因此,TF断点异常不用手动修复。

4.3.3 硬件断点异常,这类异常的地址分别保存在DR0 ~ DR3 寄存器上。产生这类异常后,DR6寄存器中的B0~B3标志位分别记录着DR0~DR3这四个寄存器中哪个寄存器保存的地址触发了异常,当BO==1的时候,B1~B3都等于0,则说明DR0寄存器保存的地址触发异常,当B1==1的时候,其他标志位等于0,则说明DR1寄存器保存的地址触发了异常。由于硬件断点异常和TF断点异常使用的都是EXCEPTION_SINGLE_STEP宏来标记,所以当EXCEPTION_RECOREAD中的字段dwExceptioinCode的值==EXCEPTION_SINGLE_STEP之时,需要判断DR6寄存器中的B0~B3标志位其中一个标志位是否被置为1来区分是TF断点异常还是硬件断点异常。

4.3.4 内存访问断点,内存访问断点利用的是内存分页属性来工作,当目标进程在一个没有任何权限的内存分页上进行读/写/执行操作时,就会产生这种异常。修复的时候,只需要把相应的内存分页属性通过VirtualProtectEx设置回去即可。

5 调试循环的建立

5.1 建立调试循环的目的有以下3点

5.1.1 为了能够持续的接收到目标进程的调试事件.

5.1.2 为了能够在恰当的时候输出反汇编信息,线程环境块等信息

5.1.3 为了能够接受用户的控制.

5.2 搭建调试循环的框架.

5.2.1 框架第一层(完成目的1):

接收调试事件,并将调试事件交给。。

,O一个函数处理,用这个函数的返回值来作为ContinueDebugEvent的第三个参数.

5.2.2 框架的第二层

框架的第二层将调试事件分为两部分,进程创建和退出,线程创建和退出,DLL加载和卸载,调试字符串输出,内部错误作为一部分, 异常事件独立作为一部分.

5.2.3 框架的第三层(完成目的2和3)

框架的第三次处理的是异常事件,由于异常可以细分为多种类型的,不同类型的异常的恢复手段不一样,因此需要进行分类处理.

此外,将信息输出给用户,接收用户的输入也是在第三层中.

// 框架的第一层

void StartDebug(const TCHAR* pszFile /目标进程的路径/)

{

if(pszFile == nullptr)

return ;

STARTUPINFOA stcStartupInfo = { sizeof(STARTUPINFOA) };

PROCESS_INFORMATION stcProcInfo = { 0 }; // 进程信息

/* 创建调试进程程 */
BOOL bRet = FALSE;
bRet = CreateProcessA(pszFile,                // 可执行模块路径
                   NULL ,                   // 命令行
                   NULL ,                   // 安全描述符
                   NULL ,                   // 线程属性是否可继承
                   FALSE ,                  // 否从调用进程处继承了句柄
                   DEBUG_ONLY_THIS_PROCESS ,// 以调试的方式启动
                   NULL ,                   // 新进程的环境块
                    NULL ,                   // 新进程的当前工作路径(当前目录)
                   &stcStartupInfo ,        // 指定进程的主窗口特性
                   &stcProcInfo             // 接收新进程的识别信息
                   );

/*建立调试循环*/
DEBUG_EVENT dbgEvent = {0};
DWORD       dwRet = DBG_CONTINUE;
while(1)
{
    /*框架的第一层*/
    WaitForDebugEvent(&dbgEvent,-1);// 等待调试事件
    dwRet = DispatchEvent(&dbgEvent); // 分发调试事件,进入框架的第二层
    ContinueDebugEvent(dbgEvent.dwProcessid,
                       dbgEvent.dwThreadid,
                       dwRet);// 回复调试事件的处理结果,如果不回复,目标进程将会一直处于暂停状态.
}
           

}

// 框架的第二层

DWORD DispatchEvent(DEBUG_EVENT* pDbgEvent)

{

// 框架的第二层

// 第二层框架将调试事件分为两部分来处理

DWORD 单位Ret= 0;

switch(pDbgEvent->dwDebugEventCode)

{

// 第一部分是异常调试事件

case EXCEPTION_DEBUG_EVENT:

dwRet = DispatchException(&pDbgEvent->u.Exception); //进入到第三层分发异常

return dwRet; // 返回到框架的第一层

// 第二部分是其他调试事件

default:

return DBG_CONTINUE;

}

}

// 框架的第三层

DWORD DispatchException(EXCEPTION_DEBUG_INFO* pExcDbgInfo)

{

// 框架的第三层

// 第三层是专门负责修复异常的.

// 如果是调试器自身设置的异常,那么可以修复,返回DBG_CONTINUE

// 如果不是调试器自身设置的异常,那么不能修复,返回DBG_EXCEPTION_NOT_HANDLED

switch(pExcDbgInfo->ExceptionRecord.ExceptionCode)

{

case EXCEPTION_BREAKPOINT: // 软件断点

{

// 修复断点

}

break;

case EXCEPTION_SINGLE_STEP: // 硬件断点和TF断点

{

// 修复断点

}

break;

case EXCEPTION_ACCESS_VIOLATION:// 内存访问断点

{

// 修复断点

}

break;

default:

return DBG_EXCEPTION_NOT_HANDLED;

}

// 输出信息,完成目的2

printf(“断点在地址%08X上触发\n”,pDbgEvent->u.Exception.ExceptionAddress);

// 输出反汇编代码

// 输出寄存器信息

// 接收用户输入,完成目的3

UserInput();

// 返回到框架的第二层中

return DBG_CONTINUE;

}

// 处理用户输入的函数,完成目的3

void UserInput()

{

char buff[100];

while(1)

{

printf(“请输入命令: “);

gets_s(buff , 100);

if(buff[0] == ‘t’) // 单步步入

{

}

else if(strcmp(buff,”bp”))// 设置断点

{

}

else if(buff[0] == ‘g’)

{

break; // 跳出循环,返回到框架的第三层中

}

}

}

继续阅读