Windows用户态调试器原理.docx
《Windows用户态调试器原理.docx》由会员分享,可在线阅读,更多相关《Windows用户态调试器原理.docx(16页珍藏版)》请在冰豆网上搜索。
Windows用户态调试器原理
Windows用户态调试器原理
Windows操作系统提供了一组API来支持调试器。
这些API可以分为三类:
创建调试目标的API;
在调试循环中处理调试事件的API。
查看和修改调试目标的API。
接下来将会分别对这三种API进行介绍。
创建调试目标
在调试器工作之前,需要创建调试目标。
用户态调试器有两种创建调试目标的方法:
一是创建新进程,二是附加到一个运行的进程。
采用这两种方法中的任一种后,该进程就成为了调试目标。
操作系统将调试器与调试目标关联起来。
调试器创建调试目标是通过调用CreateProcess并传入DEBUG_PROCESS标志。
如:
[cpp]viewplaincopy
1STARTUPINFOsi={0};
2
3si.cb=sizeof(si);
4
5PROCESS_INFORMATIONpi={0};
6
7boolret=CreateProcesss(NULL,argv[1],NULL,NULL,false,
8
9DEBUG_PROCESS,NULL,NULL,&si,&pi);
调试器附加到一个运行的进程是通过调用DebugActiveProcess来实现的。
DebugActiveProcess
此函数允许将调试器捆绑到一个正在运行的进程上。
[cpp]viewplaincopy
10BOOLDebugActiveProcess(DWORDdwProcessId)
dwProcessId:
欲捆绑进程的进程标识符
如果函数成功,则返回非零值;如果失败,则返回零
无论采用哪一种方法,调试器与操作系统的交互都是相同的。
这种调试器被称为活动调试器(livingdebuger)。
每个调试器只能有一个调试目标。
调试循环
在初学Windows时我们一定接触过消息循环。
调试循环与此类似。
while(当调试不结束时)
{
//等待操作系统发送调试事件。
//处理调试事件。
//通知调试目标执行相应操作。
}
在调试目标被调试时,进程执行的一些操作会以事件的方式通知调试器。
例如动态库的加载与卸载、新线程的创建和销毁以及代码或处理器抛出的异常都会通知调试器。
当有事件需要通知调试器时,操作系统会首先挂起调试目标的所有线程,然后把事件通知调试器。
并且等待调试器通知其继续执行。
调试器会调用WaitForDebugEvent来等待事件通知的到来。
当有事件通知到来时此函数返回,返回的事件信息被封装在DEBUG_EVENT结构中。
这个结构包含事件的类型等其他信息。
事件类型有以下几种:
WaitForDebugEvent
此函数用来等待被调试进程发生调试事件。
[cpp]viewplaincopy
11BOOLWaitForDebugEvent(LPDEBUG_ENENTlpDebugEvent,DWORDdwMilliseconds)
lpDebugEvent:
指向接收调试事件信息的DEBUG_ENENT结构的指针
dwMilliseconds:
指定用来等待调试事件发生的毫秒数,如果这段时间内没有调试事件发生,函数将返回调用者;如果将该参数指定为INFINITE,函数将一直等待直到调试事件发生
如果函数成功,则返回非零值;如果失败,则返回零
在调试器调用WaitForDebugEvent返回后,得到事件通知,然后解析DEBUG_EVENT结构,并对事件进行响应,处理完成后调试器将会调用ContinueDebugEvent,并根据参数来通知调试目标执行相应操作。
ContinueDebugEvent函数
此函数允许调试器恢复先前由于调试事件而挂起的线程。
[cpp]viewplaincopy
12BOOLContinueDebugEvent(DWORDdwProcessId,DWORDdwThreadId,DWORDdwContinueStatus)
dwProcessId为被调试进程的进程标识符
dwThreadId为欲恢复线程的线程标识符
dwContinueStatus指定了该线程将以何种方式继续,包含两个定义值DBG_CONTINUE和DBG_EXCEPTION_NOT_HANDLED
如果函数成功,则返回非零值;如果失败,则返回零。
具体实现为:
[cpp]viewplaincopy
13DWORDCondition=DBG_CONTINUE;
14
15while(Condition)
16
17{
18
19DEBUG_EVENTDebugEvent={0};
20
21WaitForDebugEvent(&DebugEvent,INFINITE);//等待调试事件
22
23ProcessEvenet(DebugEvent)//处理调试事件。
24
25ContinueDebugEvent(DebugEvent.dwProcessId,DebugEvent.dwThreadId,Condition);//通知调试目标继续执行。
26
27}
ProcessEvent用于对调试事件进行处理。
它是用户自定义函数。
在该函数内会对DEBUG_EVENT结构进行解析。
DEBUG_EVENT结构为:
[cpp]viewplaincopy
28typedefstruct_DEBUG_EVENT{
29
30DWORDdwDebugEventCode;
31
32DWORDdwProcessId;
33
34DWORDdwThreadId;
35
36union{
37
38EXCEPTION_DEBUG_INFOException;
39
40CREATE_THREAD_DEBUG_INFOCreateThread;
41
42CREATE_PROCESS_DEBUG_INFOCreateProcessInfo;
43
44EXIT_THREAD_DEBUG_INFOExitThread;
45
46EXIT_PROCESS_DEBUG_INFOExitProcess;
47
48LOAD_DLL_DEBUG_INFOLoadDll;
49
50UNLOAD_DLL_DEBUG_INFOUnloadDll;
51
52OUTPUT_DEBUG_STRING_INFODebugString;
53
54RIP_INFORipInfo;
55
56}u;
57
58}DEBUG_EVENT,*LPDEBUG_EVENT;
处理通知代码如下:
[cpp]viewplaincopy
59DWORDProcessEvent(DEBUG_EVENTde)
60
61{
62
63switch(de.dwDebugEvent.Code)
64
65{
66
67caseEXCEPTION_DEBUG_EVENT:
68
69{
70
71}
72
73break;
74
75caseCREATE_THREAD_DEBUG_EVENT:
76
77{
78
79}
80
81break;
82
83caseCREATE_PROCESS_DEBUG_EVENT:
84
85{
86
87}
88
89break;
90
91caseEXIT_THREAD_DEBUG_EVENT:
92
93{
94
95}
96
97break;
98
99caseEXIT_PROCESS_DEBUG_EVENT:
100
101{
102
103}
104
105break;
106
107caseLOAD_DLL_DEBUG_EVENT:
108
109{
110
111}
112
113break;
114
115caseOUTPUT_DEBUG_STRING_EVENT:
116
117{
118
119}
120
121break;
122
123......
124
125}
126
127returnDBG_CONTINUE;
128
129}
调试事件介绍
OUTPUT_DEBUG_STRING_EVENT事件
很多程序员在调试程序时喜欢将执行的结果或中间步骤输出,用以检查程序执行的正确与否。
在很多系统中这是很不方便的。
但我们可以使用调试输出命令,将某些需要显示的结果输出到输出窗口中。
如vc的TRACE宏。
其实在TRACE宏内部是调用OutputDebugString来实现的。
调试器会把调试目标输出的字符串通过事件处理代码显示出来。
在DEBUG_EVENT结构中有一个DebugString成员。
该结构定义为:
[cpp]viewplaincopy
130typedefstruct_OUTPUT_DEBUG_STRING_INFO{
131
132LPSTRlpDebugStringData;
133
134WORDfUnicode;
135
136WORDnDebugStringLength;
137
138}OUTPUT_DEBUG_STRING_INFO,*LPOUTPUT_DEBUG_STRING_INFO;
在此结构中有一个lpDebugStringData成员,它保存被输出字符串的地址。
nDebugStringLength为字符串长度。
fUnicode表示是ANSI还是UNICODE字符。
下面为处理OUTPUT_DEBUG_STRING_EVENT事件的代码:
[cpp]viewplaincopy
139caseOUTPUT_DEBUG_STRING_EVENT:
140
141{
142
143OUTPUT_DEBUG_STRING_INFOoi=de.u.DebugString;
144
145WCHAR*msg=ReadRemoteString(调试目标句柄,
146
147oi.lpDebugStringData,oi.nDebugStringLength,oi.fUnicode);
148
149std:
:
wcout<150
151break;
152
153}
ReadRemoteString是用户自定义函数。
在此函数内部是调用ReadProcessMemory从调试目标进程内读取字符串。
具体不再介绍。
ReadProcessMemory
读取指定进程的某区域内的数据。
[cpp]viewplaincopy
154BOOLReadProcessMemory(HANDLEhProcess,LPCVOIDlpBassAddress,LPVOIDlpBuffer,SIZE_TnSize,SIZE_T*lpNumberOfBytesRead)
hProcess:
进程的句柄
lpBassAddress:
欲读取区域的基地址
lpBuffer:
保存读取数据的缓冲的指针
nSize:
欲读取的字节数
lpNumberOfBytesRead:
存储已读取字节数的地址指针
如果函数成功,则返回非零值;如果失败,则返回零
处理EXCEPTION_DEBUG_EVENT事件
当调试目标在调试时发生异常时,操作系统将会向调试器发送EXCEPTION_DEBUG_EVENT事件通知
当发生此事件时,DEBUG_EVENT结构包含的是一个EXCEPTION_DEBUG_INFO结构。
[cpp]viewplaincopy
155typedefstruct_EXCEPTION_DEBUG_INFO{
156
157EXCEPTION_RECORDExceptionRecord;
158
159DWORDdwFirstChance;
160
161}EXCEPTION_DEBUG_INFO,*LPEXCEPTION_DEBUG_INFO;
ExceptionRecord成员包含了异常信息的一个副本。
如异常码,异常引发地址以及异常参数等。
定义如下:
[cpp]viewplaincopy
162typedefstruct_EXCEPTION_RECORD{
163
164DWORDExceptionCode;
165
166DWORDExceptionFlags;
167
168struct_EXCEPTION_RECORD*ExceptionRecord;
169
170PVOIDExceptionAddress;
171
172DWORDNumberParameters;
173
174DWORDExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
175
176}EXCEPTION_RECORD;
dwFirstChance告诉调试器是否是第一轮通知这个异常。
从操作系统的角度来看,调试器必须对异常进行解析,并且将DBG_CONTINUE或者是DBG_EXECPTION_NOT_HANDLED作为参数传递给ContinueDebugEvent。
如果执行DBG_CONTINUE,则操作系统认为该异常已经被妥善处理了。
因此从产生异常的地址开始回复程序的执行。
如果传入DBG_EXCEPTION_NOT_HANDLED,则告诉操作系统该异常并未被处理,操作系统将继续分发异常。
[cpp]viewplaincopy
177caseEXCEPTION_DBUG_EVENT:
178
179{
180
181std:
:
cout<<”异常码为”<:
hex<:
endl;
182
183//在switch判断异常类型,并执行相应操作。
184
185switch(debugEvent.u.Exception.ExceptionRecord.ExceptionCode)
186
187{
188
189caseEXCEPTION_BREAKPOINT:
190
191break;
192
193caseEXCEPTION_SINGLE_STEP:
194
195beak;
196
197returnDBG_CONTINUE;
198
199}
200
201break;
202
203}
在调试循环中,从WaitForDebugEvent中返回以及调用ContinueDebugEvent之间的这段时间内,调试目标不会执行,因此它的状态也将保持不变。
当调试目标被挂起时,调试器就进入了交互模式,接收用户的各种指令,并按照不同指令执行不同操作。
调试事件到来的顺序
当我们启动调试目标时,调试器接收到的第一个事件是CREATE_THREAD_DEBUG_EVENT。
接下来是加载dll的事件。
每加载一个,都会产生一个这样的事件。
当所有模块都被加载到进程地址空间后,调试目标就准备好运行了,调试器此时也做好了接收通知的准备。
此时是设置断点的最佳时机。
在调试目标退出之前调试器会收到EXIT_DEBUG_PROCESS_EVENT通知。
此后调试器不能收到加载到进程地址空间的dll从进程卸载的UNLOAD_DLL_DEBUG_EVENT通知。
前面介绍的调试事件都是由Windows操作系统发出的,来通知调试器。
但是调试目标也会发出自己的异常。
调试器在处理这些异常时可以选择与其他调试事件一样的处理方式。
Windows操作系统使用结构化异常处理(SEH)机制将处理器引发的异常传递给内核及用户态程序。
每个SEH异常都有一个无符号整形的异常码来唯一标识。
这个异常码是由系统在异常发生时指定的。
这些异常码使用了操作系统开发人员定义的公开异常码。
例如访问违规异常异常码为0xC0000005,断点异常为0xC80000003。
为了方便记忆,这些异常码被定义为常量。
其名字形如STATUS_XXX。
如
#defineSTATUS_BREAKPOINT((NTSTATUS)0x80000003L)
由于异常码很难记忆,因此Windows调试器中包含了一些更容易记住的别名来控制调试器的行为。
例如断点异常0x80000003的别名是bpe。
C++异常码0xE06D7363别名为eh。