前言
程序员离不开调试器,它可以动态显示程序的执行过程,对于解决程序问题有极大的帮助。如果你和我一样对调试器的工作原理很感兴趣,那么这一系列文章很适合你,这些文章记录了我开发一个调试器雏形的过程,希望对你有帮助。或许我写的代码很拙劣,还请大家多多见谅!
这个调试器使用Visual Studio 2010作为开发工具,是一个控制台程序。为了简化,一切输入输出都使用C++标准库的相关类,而且省略了很多错误检查和处理的过程。
启动被调试程序
要想对一个程序进行调试,首先要做的当然是启动这个程序,这要使用CreateProcess这个Windows API来完成。例如,下面的代码以记事本作为被调试程序:
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结构体描述了调试事件的内容:
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,表示让指定进程内的指定线程继续执行。通常这是在一个循环中完成的,如下面的代码所示:
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等调试事件处理函数,实现过程仅仅是输出提示信息。当然,对调试事件的处理远远不只这么简单,虽然你可以选择忽略某些调试事件,但有些调试事件是必须进行处理的,这部分内容将放到下一篇文章中进行讲解。