[Win32]一个调试器的实现(一)调试事件与调试循环

[Win32]一个调试器的实现(一)调试事件与调试循环

作者:Zplutor 
出处:http://www.cnblogs.com/zplutor/ 
本文版权归作者和博客园共有,欢迎转载。但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

前言

程序员离不开调试器,它可以动态显示程序的执行过程,对于解决程序问题有极大的帮助。如果你和我一样对调试器的工作原理很感兴趣,那么这一系列文章很适合你,这些文章记录了我开发一个调试器雏形的过程,希望对你有帮助。或许我写的代码很拙劣,还请大家多多见谅!

这个调试器使用Visual Studio 2010作为开发工具,是一个控制台程序。为了简化,一切输入输出都使用C++标准库的相关类,而且省略了很多错误检查和处理的过程。

启动被调试程序

要想对一个程序进行调试,首先要做的当然是启动这个程序,这要使用CreateProcess这个Windows API来完成。例如,下面的代码以记事本作为被调试程序:

1 #include <Windows.h>
 2 #include <iostream>
 3 
 4 int wmain(int argc, wchar_t** argv) {
 5 
 6     STARTUPINFO si = { 0 };
 7     si.cb = sizeof(si);
 8 
 9     PROCESS_INFORMATION pi = { 0 };
10 
11     if (CreateProcess(
12         TEXT("C:\\windows\\notepad.exe"),
13         NULL,
14         NULL,
15         NULL,
16         FALSE,
17         DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE,
18         NULL,
19         NULL,
20         &si,
21         &pi) == FALSE) {
22 
23         std::wcout << TEXT("CreateProcess failed:") << GetLastError() << std::endl;
24         return -1;
25     }
26 
27     CloseHandle(pi.hThread);
28     CloseHandle(pi.hProcess);
29 
30     return 0;
31 }

CreateProcess的第六个参数使用了DEBUG_ONLY_THIS_PROCESS,这意味着调用CreateProcess的进程成为了调试器,而它启动的子进程成了被调试的进程。除了DEBUG_ONLY_THIS_PROCESS之外,还可以使用DEBUG_PROCESS,两者的不同在于:DEBUG_PROCESS会调试被调试进程以及它的所有子进程,而DEBUG_ONLY_THIS_PROCESS只调试被调试进程,不调试它的子进程。一般情况下我们只想调试一个进程,所以应使用后者。

我建议在第六个参数中加上CREATE_NEW_CONSOLE标记。因为如果被调试程序是一个控制台程序的话,调试器和被调试程序的输出都在同一个控制台窗口内,显得很混乱,加上这个标记之后,被调试程序就会在一个新的控制台窗口中输出信息。如果被调试程序是一个窗口程序,这个标记没有影响。

上面的代码仅仅是启动了被调试进程,然后就立即退出了。要注意的是,如果调试器进程结束了,那么被它调试的所有子进程都会随着结束。这就是为什么虽然CreateProcess调用成功了,却看不到记事本窗口。

调试循环

调试器如何知道被调试进程内部发生了什么呢?是这样的,当一个进程成为被调试进程之后,在完成了某些操作或者发生异常时,它会发送通知给调试器,然后将自身挂起,直到调试器命令它继续执行。这有点像Windows窗口的消息机制。

被调试进程发送的通知称为调试事件,DEBUG_EVENT结构体描述了调试事件的内容:

1 typedef struct _DEBUG_EVENT {
 2   DWORD dwDebugEventCode;
 3   DWORD dwProcessId;
 4   DWORD dwThreadId;
 5   union {
 6     EXCEPTION_DEBUG_INFO Exception;
 7     CREATE_THREAD_DEBUG_INFO CreateThread;
 8     CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
 9     EXIT_THREAD_DEBUG_INFO ExitThread;
10     EXIT_PROCESS_DEBUG_INFO ExitProcess;
11     LOAD_DLL_DEBUG_INFO LoadDll;
12     UNLOAD_DLL_DEBUG_INFO UnloadDll;
13     OUTPUT_DEBUG_STRING_INFO DebugString;
14     RIP_INFO RipInfo;
15   } u;
16 } DEBUG_EVENT, 
17  *LPDEBUG_EVENT;

dwDebugEventCode描述了调试事件的类型,总共有9类调试事件:


CREATE_PROCESS_DEBUG_EVENT


创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件。


CREATE_THREAD_DEBUG_EVENT


创建一个线程之后发送此类调试事件。


EXCEPTION_DEBUG_EVENT


发生异常时发送此类调试事件。


EXIT_PROCESS_DEBUG_EVENT


进程结束后发送此类调试事件。


EXIT_THREAD_DEBUG_EVENT


一个线程结束后发送此类调试事件。


LOAD_DLL_DEBUG_EVENT


装载一个DLL模块之后发送此类调试事件。


OUTPUT_DEBUG_STRING_EVENT


被调试进程调用OutputDebugString之类的函数时发送此类调试事件。


RIP_EVENT


发生系统调试错误时发送此类调试事件。


UNLOAD_DLL_DEBUG_EVENT


卸载一个DLL模块之后发送此类调试事件。

每种调试事件的详细信息通过联合体u来记录,通过u的字段的名称可以很快地判断哪个字段与哪种事件关联。例如CREATE_PROCESS_DEBUG_EVENT调试事件的详细信息由CreateProcessInfo字段来记录。

dwProcessId和dwThreadId分别是触发调试事件的进程ID和线程ID。一个调试器可能同时调试多个进程,而每个进程内又可能有多个线程,通过这两个字段就可以知道调试事件是从哪个进程的哪个线程触发的了。本系列文章只考虑单进程单线程的情况,因此这两个字段不会被用到,因为在调用CreateProcess的时候已经获取到这两个值了。

调试器通过WaitForDebugEvent函数获取调试事件,通过ContinueDebugEvent继续被调试进程的执行。ContinueDebugEvent有三个参数,第一和第二个参数分别是进程ID和线程ID,表示让指定进程内的指定线程继续执行。通常这是在一个循环中完成的,如下面的代码所示:

1 void OnProcessCreated(const CREATE_PROCESS_DEBUG_INFO*);
 2 void OnThreadCreated(const CREATE_THREAD_DEBUG_INFO*);
 3 void OnException(const EXCEPTION_DEBUG_INFO*);
 4 void OnProcessExited(const EXIT_PROCESS_DEBUG_INFO*);
 5 void OnThreadExited(const EXIT_THREAD_DEBUG_INFO*);
 6 void OnOutputDebugString(const OUTPUT_DEBUG_STRING_INFO*);
 7 void OnRipEvent(const RIP_INFO*);
 8 void OnDllLoaded(const LOAD_DLL_DEBUG_INFO*);
 9 void OnDllUnloaded(const UNLOAD_DLL_DEBUG_INFO*);
10 
11 BOOL waitEvent = TRUE;
12 DEBUG_EVENT debugEvent;
13 while (waitEvent == TRUE && WaitForDebugEvent(&debugEvent, INFINITE)) {
14 
15     switch (debugEvent.dwDebugEventCode) {
16 
17         case CREATE_PROCESS_DEBUG_EVENT:
18             OnProcessCreated(&debugEvent.u.CreateProcessInfo);
19             break;
20 
21         case CREATE_THREAD_DEBUG_EVENT:
22             OnThreadCreated(&debugEvent.u.CreateThread);
23             break;
24 
25         case EXCEPTION_DEBUG_EVENT:
26             OnException(&debugEvent.u.Exception);
27             break;
28 
29         case EXIT_PROCESS_DEBUG_EVENT:
30             OnProcessExited(&debugEvent.u.ExitProcess);
31             waitEvent = FALSE;
32             break;
33 
34         case EXIT_THREAD_DEBUG_EVENT:
35             OnThreadExited(&debugEvent.u.ExitThread);
36             break;
37 
38         case LOAD_DLL_DEBUG_EVENT:
39             OnDllLoaded(&debugEvent.u.LoadDll);
40             break;
41 
42         case UNLOAD_DLL_DEBUG_EVENT:
43             OnDllUnloaded(&debugEvent.u.UnloadDll);
44             break;
45 
46         case OUTPUT_DEBUG_STRING_EVENT:
47             OnOutputDebugString(&debugEvent.u.DebugString);
48             break;
49 
50         case RIP_EVENT:
51             OnRipEvent(&debugEvent.u.RipInfo);
52             break;
53 
54         default:
55             std::wcout << TEXT("Unknown debug event.") << std::endl;
56             break;
57         }
58 
59     if (waitEvent == TRUE) {
60         ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
61     }
62     else {
63         break;
64     }
65 }

这样一个循环就是所谓的调试循环。要注意这里是如何退出循环的:引入一个BOOL类型的waitEvent变量,在处理EXIT_PROCESS_DEBUG_EVENT之后将它的值改成FALSE。之所以要这样处理,是因为在被调试进程结束之后仍然可以调用WaitForDebugEvent函数等待调试事件,这样就会陷入无限的等待之中,导致调试器进程无法结束。

示例代码

示例代码将上面两段代码结合起来,并实现了上述的OnProcessCreated等调试事件处理函数,实现过程仅仅是输出提示信息。当然,对调试事件的处理远远不只这么简单,虽然你可以选择忽略某些调试事件,但有些调试事件是必须进行处理的,这部分内容将放到下一篇文章中进行讲解。

http://files.cnblogs.com/zplutor/MiniDebugger1.rar

时间: 2024-10-27 01:25:33

[Win32]一个调试器的实现(一)调试事件与调试循环的相关文章

[Win32]一个调试器的实现(八)单步执行

[Win32]一个调试器的实现(八)单步执行 作者:Zplutor 出处:http://www.cnblogs.com/zplutor/ 本文版权归作者和博客园共有,欢迎转载.但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利. 上回讲解了如何实现断点功能,这回讲解如何实现与断点紧密相关的单步执行功能.单步执行有三种类型:StepIn,StepOver和StepOut,它们的实现方式比较多样化,单独实现它们的话并不困难,但是将它们整合到一起就比较困难了,

《python灰帽子》学习笔记:写一个windos 调试器(一)

一.开发内容介绍 为了对一个进程进行调试,你首先必须用一些方法把调试器和进程连接起来.所以, 我们的调试器要不然就是装载一个可执行程序然后运行它, 要不然就是动态的附加到一个运行的进程.Windows 的调试接口(Windows debugging API)提供了一个非常简单的方法完成这两点. 运行一个程序和附加到一个程序有细微的差别. 打开一个程序的优点在于他能在程序运行任何代码之前完全的控制程序. 这在分析病毒或者恶意代码的时候非常有用. 附加到一个进程,仅仅是强行的进入一个已经运行了的进程

Vs2013在Linux开发中的应用(16): 修改调试器参数

快乐虾 http://blog.csdn.net/lights_joy/ 欢迎转载,但请保留作者信息 仿照debugger_local_windows.xml的写法,修改名称和ID: <?xml version="1.0"encoding="utf-8"?> <!--Copyright, Microsoft Corporation, All rights reserved.--> <Rule Name="9F2571B6-55

打造简单的调试器

本文转自:http://www.freebuf.com/sectool/92279.html 0×1 概述 在Fuzzing过程中,必须要监控程序的执行状态,若程序发生异常,立即保存测试用例,以便将来对其进行重现.监控器的作用就是监控程序的执行,如果程序有异常时通知给Fuzzing主线程. 目前Fuzzing工具各种各样,监控器也有很多种,但大多都是以调试器的方式实现.一个简单的监控器是这样实现的: (一)以调试模式启动进程,为目标进程开启调试端口,可使用参数DEBUG_ONLY_THIS_PR

【转】浅谈LLDB调试器

随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器.它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能.LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板,在这里我们可以直接调用LLDB命令.如图1所示: 图1:位于Xcode调试区域的控制台 在本文中,我们主要整理一下LLDB调试器提供给我们的调试命令,更详细的内容可以查看The LLDB Debugger. LLDB命令结构 在使用LL

windows简单调试器源码2700行左右代码

简单调试器项目中on开头的函数为接收系统的调试事件并做相应的处理,简单调试器实现过程中主要的调试事件为异常事件,相应的处理函数为DispatchException. 在异常事件中访问异常.int3异常.单步异常是跟实现调试器功能密切相关的异常事件,这里用三个函数分别处理三个函数分别为OnExceptionAccess.OnExceptionBreakPoint.OnExceptionSingleStep. 异常处理函数中如果为调试器自己设置的异常程序就会停下来接收用户输入等待下一步处理,相应的用

Unity官方文档之“图形性能优化-帧调试器”的翻译

Frame Debugger 帧调试器 The Frame Debugger lets you freeze playback for a running game on a particular frame and view the individual draw calls that are used to render that frame. As well as listing the drawcalls, the debugger also lets you step through

GDB调试器的使用

http://blog.chinaunix.net/uid-23193900-id-3184605.html 1.什么是程序的调试? 程序调试的目的就是找出程序中隐藏的故障,校正那些不正常的指令,使程序能够正常工作. 2.调试的类别 程序的调试有几种不同的级别.最高级别当然是编程人员通过自己肉眼观察和推断,找出那些有毛病的代码并修改之.最低级别是对汇编代码进行调试.由于汇编语言代码的复杂.冗长与不直观.在汇编级对程序进行调试是一件比较费力的事. 使用得最多的大概还是源程序级即符号级的调试,在此种

嵌入式调试器原理和各类调试器集锦(JLINK、STLINK、CCDEBUG)

工欲善其事,必先善其器.调试器在嵌入式开发调试中的重要性不言而喻,单步.断点和监察的效率远高于串口打印.但是,调试器对于一般开发人员往往是一个黑匣子.今天我们就来谈谈调试器的原理,顺便把自己的几类调试器接线和注意事项记录下来,以便查找.我常常要面对几个方案,而各个方案的调试器都不一样,接线有时连自己都记不住.所以这个帖子应值得嵌入式开发工程师收藏. 一.嵌入式调试多样性 我们先来回想调试的场景,思考一下这几个问题: 1. ARM开发环境有Keil.IAR.ADS等等,我们发现这几个平台都能用同一

调试器如何工作(3)

调试器如何工作:第三部分--调试信息 原作者:Eli Bendersky http://eli.thegreenplace.net/2011/02/07/how-debuggers-work-part-3-debugging-information 这是关于调试器如何工作系列文章的第三部分.在这之前确保你读过第一.第二部分. 在这部分 我将解释调试器如何明白,在它跋涉机器代码里,在哪里找到C函数与变量,以及它用来在C源代码与机器语言内存字间进行映射的数据. 调试信息 现代编译器在翻译高级语言代码