📄 lion-tut-c28.htm
字号:
<html>
<head>
<link rel="stylesheet" href="../../asm.css">
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>Iczelion 的 Win32asm 教程</title>
</head>
<body bgcolor="#FFFFFF" background="../../images/back01.jpg">
<div align="center"><font size="+1" color="#0000FF">第二十八课: Win32调试API 第一部分</font>
<hr size=1>
</div>
<p>在本教程中,我们将学习Win32提供给开发者的用于调试的原语. 在教程的结尾,我们将学习如何调试一个进程. <br>
下载 <b><a href="files/tut28.zip" style="text-decoration:none">例子程序</a></b>.</p>
<h3>理论:</h3>
<p>Win32有一些供程序员使用的API,它们提供相当于调试器的功能. 他们被称作Win32调试API(或原语).利用这些API,我们可以:</p>
<ul>
<li>加载一个程序或捆绑到一个正在运行的程序上以供调试</li>
<li>获得被调试的程序的低层信息,例如进程ID,进入地址,映像基址等.</li>
<li>当发生与调试有关的事件时被通知,例如进程/线程的开始/结束, DLL的加载/释放等.</li>
<li>修改被调试的进程或线程</li>
</ul>
<p>简而言之,我们可以用这些API写一个简单的调试器.由于这个题目有些过大,我把它分为几部分,而本教程就是它的第一部分.在本教程中,我将讲解一些基本概念及Win32调试API的大致框架.<br>
使用Win32调试API的步骤如下:</p>
<ol>
<li><b>创建一个进程或捆绑到一个运行中的进程上</b>. 这是使用Win32调试API的第一步.由于我们的程序要扮演调试器的角色,我们要找一个供调试的程序.一个被调试的程序被称为debuggee.可以通过以下两种方式获得debuggee:
<ul>
<li>通过<b>CreateProcess</b>创建debuggee进程.为了创建被调试的进程,必须指定<b>DEBUG_PROCESS</b>标志.这一标志告诉Windows我们要调试该进程.
当debuggee中发生重要的与调试有关的事件(调试事件)时,Windows 会向我们的程序发送通知.debuggee会立即挂起以等待我们的程序准备好.如果debuggee还创建了子进程,Windows还会为每个子进程中的调试事件向我们的程序发送通知.这一特性通常是不必要的.我们可以通过指定<b>DEBUG_ONLY_THIS_PROCESS</b>与
<b>DEBUG_PROCESS</b>的组合标志来禁止它. </li>
<li>我们也可以用 <b>DebugActiveProcess</b>标志捆绑到一个运行中的进程上.</li>
</ul>
</li>
<li><b>等待调试事件</b>. 在获得了一个debuggee进程后,debuggee的主线程被挂起,这种状况将持续到我们的程序调用<b>WaitForDebugEvent</b>为止.这个函数和其他的WaitForXXX函数相似,比如说,它阻塞调用线程直到等待的事件发生.对这个函数来说,
它等待由Windows发送的调试事件.下面是它的定义:
<p><b>WaitForDebugEvent proto lpDebugEvent:DWORD, dwMilliseconds:DWORD</b></p>
<p><b>lpDebugEvent</b> is the address of a <b>DEBUG_EVENT</b>这个结构将被填入关于debuggee中发生的调试事件的信息.</p>
<p><b>dwMilliseconds</b> 该函数等待调试事件的时间,以毫秒为单位.如果这段时间没有调试事件发生,<b> WaitForDebugEvent</b>返回调用者.另一方面,如果将该参数指定为<b>
INFINITE </b>常数,函数将一直等待直到调试事件发生.</p>
<p>现在我们看一下DEBUG_EVENT 结构.</p>
<p><b>DEBUG_EVENT STRUCT <br>
dwDebugEventCode dd ? <br>
dwProcessId dd ? <br>
dwThreadId dd ? <br>
u DEBUGSTRUCT <> <br>
DEBUG_EVENT ENDS </b></p>
<p><b>dwDebugEventCode</b> 该值指定了等待发生的调试事件的类型.因为有很多种类型的事件发生,我们的程序要检查该值,知道要发生事件的类型并做出响应.
该值可能的取值如下:</p>
</li>
<table border="1" cellspacing="2" cellpadding="2" align="center">
<tr bgcolor="#009999">
<th><b>取值</b></th>
<th>含义</th>
</tr>
<tr>
<td><b>CREATE_PROCESS_DEBUG_EVENT</b></td>
<td>进程被创建.当debuggee进程刚被创建(还未运行) 或我们的程序刚以<b>DebugActiveProcess</b>被捆绑到一个运行中的进程时事件发生.
这是我们的程序应该获得的第一个事件.</td>
</tr>
<tr>
<td><b>EXIT_PROCESS_DEBUG_EVENT</b></td>
<td>进程退出.</td>
</tr>
<tr>
<td><b>CREATE_THEAD_DEBUG_EVENT</b></td>
<td>当一个新线程在deuggee进程中创建或我们的程序首次捆绑到运行中的进程时事件发生.要注意的是当debugge的主线程被创建时不会收到该通知.
</td>
</tr>
<tr>
<td height="131"><b>EXIT_THREAD_DEBUG_EVENT</b></td>
<td height="131">debuggee中的线程退出时事件发生.debugee的主线程退出时不会收到该通知.我们可以认为debuggee的主线程与debugge进程是同义词.
因此, 当我们的程序看到<b>CREATE_PROCESS_DEBUG_EVENT</b>标志时,对主线程来说,就是<b>CREATE_THREAD_DEBUG_EVENT</b>标志.</td>
</tr>
<tr>
<td><b>LOAD_DLL_DEBUG_EVENT</b></td>
<td>debuggee装入一个DLL.当PE装载器第一次分解指向DLL的链接时,我们将收到这一事件. (当调用<b>CreateProcess</b>装入
debuggee时)并且当debuggee调用LoadLibrary时也会发生.</td>
</tr>
<tr>
<td><b>UNLOAD_DLL_DEBUG_EVENT</b></td>
<td>一个DLL从debuggee中卸载时事件发生. </td>
</tr>
<tr>
<td><b>EXCEPTION_DEBUG_EVENT</b></td>
<td>在debuggee中发生异常时事件发生. <b>注意:</b> 该事件仅在debuggee开始它的第一条指令之前发生一次.异常实际上是一个调试中断(int
3h).如果想恢复debuggee事,以<b> DBG_CONTINUE </b>标志调用<b>ContinueDebugEvent </b>函数.
不要使用<b>DBG_EXCEPTION_NOT_HANDLED</b> 标志否则debuggee会在NT下拒绝运行(Win98下运行得很好).</td>
</tr>
<tr>
<td><b>OUTPUT_DEBUG_STRING_EVENT</b></td>
<td>当debuggee调用<b>DebugOutputString</b>函数向我们的程序发送消息字符串时该事件发生. </td>
</tr>
<tr>
<td><b>RIP_EVENT</b></td>
<td>系统调试发生错误</td>
</tr>
</table>
<p><b>dwProcessId</b> 和<b>dwThreadId</b>发生调试事件的进程和线程Id.我们可以用这些值作为我们感兴趣的进程或线程的标志符.记住如果我们使用<b>CreateProcess</b>来装载debuggee,我们仍可在<b>PROCESS_INFO</b>结构中获得debuggee的进程和线程.我们可以用这些值来区别调试事件是发生在debuggee中还是它的子进程中(当没有指定
<b>DEBUG_ONLY_THIS_PROCESS </b>标志时).</p>
<p> <b>u</b> 是一个联合,包含了调试事件的更多信息.根据上面<b>dwDebugEventCode</b>的不同,它可以是以下结构:</p>
<table border="1" cellspacing="2" cellpadding="2" align="center">
<tr bgcolor="#009900">
<th><b>dwDebugEventCode</b></th>
<th>u的解释</th>
</tr>
<tr>
<td><b>CREATE_PROCESS_DEBUG_EVENT</b></td>
<td>名为<b>CreateProcessInfo</b>的<b>CREATE_PROCESS_DEBUG_INFO</b>结构</td>
</tr>
<tr>
<td><b>EXIT_PROCESS_DEBUG_EVENT</b></td>
<td>名为<b>ExitProcess</b>的<b>EXIT_PROCESS_DEBUG_INFO</b>结构</td>
</tr>
<tr>
<td><b>CREATE_THREAD_DEBUG_EVENT</b></td>
<td>名为<b>CreateThread</b>的<b>CREATE_THREAD_DEBUG_INFO</b>结构</td>
</tr>
<tr>
<td><b>EXIT_THREAD_DEBUG_EVENT</b></td>
<td>名为<b>ExitThread</b>的<b>EXIT_THREAD_DEBUG_EVENT </b>结构</td>
</tr>
<tr>
<td><b>LOAD_DLL_DEBUG_EVENT</b></td>
<td>名为<b>LoadDll</b>的<b>LOAD_DLL_DEBUG_INFO</b> 结构</td>
</tr>
<tr>
<td><b>UNLOAD_DLL_DEBUG_EVENT</b></td>
<td>名为<b>UnloadDll</b>的<b>UNLOAD_DLL_DEBUG_INFO</b>结构</td>
</tr>
<tr>
<td><b>EXCEPTION_DEBUG_EVENT</b></td>
<td>名为<b>Exception</b>的<b>EXCEPTION_DEBUG_INFO</b>结构</td>
</tr>
<tr>
<td><b>OUTPUT_DEBUG_STRING_EVENT</b></td>
<td>名为<b>DebugString</b>的<b>OUTPUT_DEBUG_STRING_INFO </b>结构</td>
</tr>
<tr>
<td><b>RIP_EVENT</b></td>
<td>名为<b>RipInfo</b>的<b>RIP_INFO</b> 结构</td>
</tr>
</table>
<p>我不会在这一个教程里讲所有这些结构的细节,这里只详细讲一下<b>CREATE_PROCESS_DEBUG_INFO </b>结构. <br>
假设我们的程序调用了<b>WaitForDebugEvent</b>函数并返回,我们要做的第一件事就是检查<b>dwDebugEventCode</b>中的值来看debuggee进程中发生了那种类型的调试事件.比如说,如果<b>dwDebugEventCode</b>的值为
<b>CREATE_PROCESS_DEBUG_EVENT</b>,就可认为<b>u</b>的成员为<b>CreateProcessInfo</b>
并用<b>u.CreateProcessInfo</b>来访问. </p>
<li><b>在我们的程序中做对调试事件的响应</b>. 当<b>WaitForDebugEvent </b>返回时,这意味着在debuggee进程中发生了调试事件或者发生了超时.所以我们的程序要检查<b>dwDebugEventCode</b>
来作出适当的反应.这里有些象处理Windows消息:由用户来选择和忽略消息.</li>
<li><b>继续运行debuggee</b>. 当调试事件发生时, Windows挂起了debuggee,所以当我们处理完调试事件,还要让debuggee继续运行.调用<b>ContinueDebugEvent</b>
函数来完成这一过程.
<p><b>ContinueDebugEvent proto dwProcessId:DWORD, dwThreadId:DWORD, dwContinueStatus:DWORD</b></p>
<p>该函数恢复由于调试事件而挂起的线程.<br>
<b>dwProcessId</b>和<b>dwThreadId</b>是要恢复的线程的进程ID和线程ID,通常这两个值从 <b>DEBUG_EVENT</b>结构的<b>dwProcessId</b>
和<b>dwThreadId</b>成员获得.<br>
<b>dwContinueStatus</b>显示了如何继续报告调试事件的线程.可能的取值有两个:<b> DBG_CONTINUE</b> 和<b>DBG_EXCEPTION_NOT_HANDLED</b>.
对大多数调试事件,这两个值都一样:恢复线程.唯一的例外是<b>EXCEPTION_DEBUG_EVENT</b>,如果线程报告发生了一个异常调试事件,这意味着在debuggee的线程中发生了一个异常.如果指定了<b>DBG_CONTINUE</b>,线程将忽略它自己的异常处理部分并继续执行.在这种情况下,我们的程序必须在以<b>DBG_CONTINUE</b>恢复线程之前检查并处理异常,否则异常将生生不息地不断发生....如果我们指定了
<b>DBG_EXCEPTION_NOT_HANDLED</b>值,就是告诉Windows我们的程序并不处理异常:Windows将使用debuggee的默认异常处理函数来处理异常.
<br>
总而言之,如果我们的程序没有考虑异常,而调试事件又指向debuggee进程中的一个异常的话,就应调用含<b>DBG_CONTINUE</b>标志的<b>ContinueDebugEvent</b>函数.否则,我们的程序就必须以<b>DBG_EXCEPTION_NOT_HANDLED</b>调用
<b>ContinueDebugEvent</b>.但在下面这种情况下必须使用<b>DBG_CONTINUE</b>标志:第一个在ExceptionCode成员中有值<b>EXCEPTION_BREAKPOINT</b>的
<b>EXCEPTION_DEBUG_EVENT</b>事件.当debuggee开始执行它的第一条指令时,我们的函数将接受到异常调试事件.它事实上是一个调试中断(int
3h).如果我们以<b>DBG_EXCEPTION_NOT_HANDLED</b>调用<b>ContinueDebugEvent </b>来响应调试事件,
Windows NT会拒绝执行debuggee(因为它没有异常处理).所以在这种情况下,要用<b>DBG_CONTINUE</b>标志告诉Windows我们希望该线程继续执行.</p>
</li>
<li><b>继续上面的步骤循环直到debuggee进程退出</b>. 我们的程序必须在一个很象消息循环的无限循环中直到debuggee结束.该循环大体如下:
<p><b>.while TRUE<br>
invoke WaitForDebugEvent, addr DebugEvent, INFINITE<br>
.break .if DebugEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT<br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -