65.9K
CodeProject 正在变化。 阅读更多。
Home

编写一个基本的 Windows 调试器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (79投票s)

2009 年 11 月 11 日

CPOL

14分钟阅读

viewsIcon

223195

downloadIcon

12788

了解如何编写自己的 Windows 调试器。

DebuggerUI.jpg

前导码

我们在用某种语言编程时,都使用过某种形式的调试器。你使用的调试器可能是 C++、C#、Java 或其他语言的。它可能是 WinDbg 这样的独立程序,也可能是 Visual Studio 这样的 IDE 中的一部分。但你是否曾好奇过调试器是如何工作的?

好吧,本文将揭示调试器工作背后的精彩之处。本文仅涵盖在 Windows 上编写调试器。请注意,我这里只关注**调试器**本身,而不涉及编译器、链接器或调试扩展。因此,我们将只调试可执行文件(如 WinDbg)。本文假设读者对多线程有基本了解(请阅读我关于多线程的文章)。

1. 如何调试程序?

两个步骤

  1. 使用 DEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS 标志启动进程。
  2. 设置调试器的循环,以处理调试事件。

在我们继续之前,请记住

  1. 调试器 是用于调试其他进程(目标进程)的进程/程序。
  2. 被调试程序 是被调试器调试的进程。
  3. 一个被调试程序只能附加一个调试器。但是,一个调试器可以同时调试多个进程(在单独的线程中)。
  4. 只有创建/生成被调试程序的线程才能调试目标进程。因此,CreateProcess 和调试器循环必须在同一个线程中。
  5. 当调试器线程终止时,被调试程序也将终止。然而,调试器进程可能仍然在运行。
  6. 当调试器的调试线程忙于处理调试事件时,被调试程序(目标进程)中的**所有**线程都会被挂起。稍后将对此进行更详细的讨论。

A. 使用调试标志启动进程

使用 CreateProcess 启动进程,将 DEBUG_ONLY_THIS_PROCESS 指定为第六个参数(dwCreationFlags)。使用此标志,我们要求 Windows 操作系统就所有调试事件(包括进程创建/终止、线程创建/终止、运行时异常等)与*本线程*进行通信。下面将进行详细说明。请注意,本文将使用 DEBUG_ONLY_THIS_PROCESS。它基本上意味着我们只想调试我们创建的进程,而不调试我们创建的进程可能创建的任何子进程。

STARTUPINFO si; 
PROCESS_INFORMATION pi; 
ZeroMemory( &si, sizeof(si) ); 
si.cb = sizeof(si); 
ZeroMemory( &pi, sizeof(pi) );

CreateProcess ( ProcessNameToDebug, NULL, NULL, NULL, FALSE, 
                DEBUG_ONLY_THIS_PROCESS, NULL,NULL, &si, &pi );

在此语句之后,你会在任务管理器中看到该进程,但该进程尚未启动。新创建的进程处于挂起状态。不,我们不需要调用 ResumeThread,而是需要编写调试器循环。

B. 调试器循环

调试器循环是调试器的核心区域!循环围绕着 WaitForDebugEvent API 运行。该 API 接受两个参数:指向 DEBUG_EVENT 结构的指针和 DWORD 超时参数。对于超时,我们将简单地指定 INFINITE。该 API 存在于*kernel32.dll* 中,因此我们无需链接任何库。

BOOL WaitForDebugEvent(DEBUG_EVENT* lpDebugEvent, DWORD dwMilliseconds);

DEBUG_EVENT 结构包含调试事件信息。它有四个成员:调试**事件代码**、**进程 ID**、**线程 ID** 和**事件信息**。一旦 WaitForDebugEvent 返回,我们就处理收到的调试事件,然后最终调用 ContinueDebugEvent。这是一个最小化的调试器循环

DEBUG_EVENT debug_event = {0};
for(;;)
{
    if (!WaitForDebugEvent(&debug_event, INFINITE))
        return;
    ProcessDebugEvent(&debug_event);  // User-defined function, not API
    ContinueDebugEvent(debug_event.dwProcessId,
                      debug_event.dwThreadId,
                      DBG_CONTINUE);
}

使用 ContinueDebugEvent API,我们要求操作系统继续执行被调试程序。dwProcessIddwThreadId 指定进程和线程。这些值与我们从 WaitForDebugEvent 收到的值相同。最后一个参数指定是否继续执行。此参数仅在收到异常事件时相关。稍后将对此进行讨论。在此之前,我们将仅使用 DBG_CONTINUE(另一个可能的值是 DBG_EXCEPTION_NOT_HANDLED)。

2. 处理调试事件

有九个主要的调试事件,以及异常事件类别下的 20 个不同的子事件。我将从最简单的开始讨论它们。这是 DEBUG_EVENT 结构

struct DEBUG_EVENT
{
    DWORD dwDebugEventCode;
    DWORD dwProcessId;
    DWORD dwThreadId;
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
        RIP_INFO RipInfo;
    } u;
};

WaitForDebugEvent 在成功返回时,会填充此结构中的值。dwDebugEventCode 指定发生了哪种调试事件。根据收到的事件代码,联合体**u** 的一个成员包含事件信息,我们应该只使用相应的联合体成员。例如,如果调试事件代码是 OUTPUT_DEBUG_STRING_EVENT,则 OUTPUT_DEBUG_STRING_INFO 成员将有效。

A. 处理 OUTPUT_DEBUG_STRING_EVENT

程序员通常使用 OutputDebugString 来生成调试文本,这些文本将显示在调试器的“输出”窗口中。根据你使用的语言/框架,你可能熟悉 TRACEATLTRACE 宏。.NET 程序员可能使用 System.Diagnostics.Debug.Print/System.Diagnostics.Trace.WriteLine 方法(或其他方法)。但所有这些方法都会调用 OutputDebugString API,调试器将收到此事件(除非它被 DEBUG 符号未定义所掩盖!)。

当收到此事件时,我们将处理 DebugString 成员变量。OUTPUT_DEBUG_STRING_INFO 结构定义为

struct OUTPUT_DEBUG_STRING_INFO
{
   LPSTR lpDebugStringData;  // char*
   WORD fUnicode;
   WORD nDebugStringLength;
};

'nDebugStringLength' 成员变量指定字符串的长度(包括终止的 null),以字符为单位(而不是字节)。'fUnicode' 变量指定字符串是否为 Unicode(非零)或 ANSI(零)。这意味着,如果字符串是 ANSI,我们读取 'lpDebugStringData' 中的 'nDebugStringLength' 字节;否则,我们读取 (nDebugStringLength x 2) 字节。但请记住,'lpDebugStringData' 指向的地址*不是*来自调试器内存地址空间。该地址与被调试程序内存相关。因此,我们需要*从被调试程序的进程内存中读取内容*。

要从另一个进程的内存中读取数据,我们使用 ReadProcessMemory 函数。它要求调用进程拥有适当的权限。由于调试器刚刚创建了进程,我们拥有该权限。以下是处理此调试事件的代码

case OUTPUT_DEBUG_STRING_EVENT:
{
   CStringW strEventMessage;  // Force Unicode
   OUTPUT_DEBUG_STRING_INFO & DebugString = debug_event.u.DebugString;

   WCHAR *msg=new WCHAR[DebugString.nDebugStringLength];
   // Don't care if string is ANSI, and we allocate double...

   ReadProcessMemory(pi.hProcess,       // HANDLE to Debuggee
         DebugString.lpDebugStringData, // Target process' valid pointer
         msg,                           // Copy to this address space
         DebugString.nDebugStringLength, NULL);

   if ( DebugString.fUnicode )
      strEventMessage = msg;
   else
      strEventMessage = (char*)msg; // char* to CStringW (Unicode) conversion.

   delete []msg;
   // Utilize strEventMessage
}

如果调试程序在调试器复制内存内容之前终止了怎么办?

嗯……在这种情况下,我想提醒你:当调试器正在处理调试事件时,被调试程序中的**所有**线程都会被挂起。此时进程无法以任何方式杀死自己。此外,其他任何方法也无法终止该进程(任务管理器、进程浏览器、Kill 工具...)。尝试从这些工具中杀死该进程确实会安排终止进程。因此,调试器将收到 EXIT_PROCESS_DEBUG_EVENT 作为下一个事件!

B. 处理 CREATE_PROCESS_DEBUG_EVENT

当进程(被调试程序)被生成时,会引发此事件。这将是调试器收到的第一个事件。对于此事件,DEBUG_EVENT 的相关成员将是 CreateProcessInfo。这是 CREATE_PROCESS_DEBUG_INFO 的结构定义

struct CREATE_PROCESS_DEBUG_INFO
{
    HANDLE hFile;   // The handle to the physical file (.EXE)
    HANDLE hProcess; // Handle to the process
    HANDLE hThread;  // Handle to the main/initial thread of process
    LPVOID lpBaseOfImage; // base address of the executable image
    DWORD dwDebugInfoFileOffset;
    DWORD nDebugInfoSize;
    LPVOID lpThreadLocalBase;
    LPTHREAD_START_ROUTINE lpStartAddress;
    LPVOID lpImageName;  // Pointer to first byte of image name (in Debuggee)
    WORD fUnicode; // If image name is Unicode.
};

请注意,hProcesshThread 可能与我们在 piPROCESS_INFORMATION)中收到的句柄值不同。然而,进程 ID 和线程 ID 将是相同的。Windows 为(同一资源)获得的每个句柄都与其他句柄不同,并且具有不同的目的。因此,调试器可以选择显示句柄或 ID。

hFilelpImageName 都可以用来获取被调试进程的文件名。虽然我们已经知道进程的名称,因为我们只创建了被调试程序。但定位 EXE 或 DLL 的模块名称很重要,因为在处理 LOAD_DLL_DEBUG_EVENT 消息时,我们总是需要找到 DLL 的名称。

正如你在 MSDN 上所读到的,lpImageName 永远不会直接返回文件名,并且该名称将在目标进程中。此外,它在目标进程中可能也没有文件名(即,通过 ReadProcessMemory)。此外,文件名可能不是完全限定的(正如我测试的那样)。因此,我们不会使用此方法。我们将从 hFile 成员中检索文件名。

如何通过句柄获取文件名

不幸的是,我们需要使用 MSDN 中描述的方法,该方法使用大约 10 个 API 调用来从句柄中获取文件名。我稍微修改了 GetFileNameFromHandle 函数。为简洁起见,此处未显示代码,它在本文附带的源代码文件中可用。无论如何,这是处理此事件的基本代码

case CREATE_PROCESS_DEBUG_EVENT:
{
   CString strEventMessage = 
     GetFileNameFromHandle(debug_event.u.CreateProcessInfo.hFile);
   // Use strEventMessage, and other members
   // of CreateProcessInfo to intimate the user of this event.
}

你可能会注意到我没有涵盖该结构中的几个成员。我可能会在本篇文章的下一部分中涵盖所有这些内容。

C. 处理 LOAD_DLL_DEBUG_EVENT

此事件与 CREATE_PROCESS_DEBUG_EVENT 类似,正如你可能猜到的,当操作系统加载 DLL 时会引发此事件。每当加载 DLL 时,都会引发此事件,无论是隐式加载还是显式加载(当被调试程序调用 LoadLibrary 时)。此调试事件仅在系统首次将 DLL 附加到进程的虚拟地址空间时发生。对于此事件处理,我们使用联合体的 'LoadDll' 成员。它属于 LOAD_DLL_DEBUG_INFO 类型

struct LOAD_DLL_DEBUG_INFO
{
   HANDLE hFile;         // Handle to the DLL physical file.
   LPVOID lpBaseOfDll;   // The DLL Actual load address in process.
   DWORD dwDebugInfoFileOffset;
   DWORD nDebugInfoSize;
   LPVOID lpImageName;   // These two member are same as CREATE_PROCESS_DEBUG_INFO
   WORD fUnicode;
};

为了检索文件名,我们将使用与我们在 CREATE_PROCESS_DEBUG_EVENT 中使用的相同的函数 GetFileNameFromHandle。我将在描述 UNLOAD_DLL_DEBUG_EVENT 时列出处理此事件的代码,因为 UNLOAD_DLL_DEBUG_EVENT 没有直接信息可用于查找 DLL 文件名。

D. 处理 CREATE_THREAD_DEBUG_EVENT

每当在被调试程序中创建新线程时,都会生成此调试事件。与 CREATE_PROCESS_DEBUG_EVENT 一样,此事件在线程实际开始执行之前引发。为了获取有关此事件的信息,我们使用 'CreateThread' 联合体成员。此变量属于 CREATE_THREAD_DEBUG_INFO 类型

struct CREATE_THREAD_DEBUG_INFO
{
  // Handle to the newly created thread in debuggee
  HANDLE hThread;
  LPVOID lpThreadLocalBase;
  // pointer to the starting address of the thread
  LPTHREAD_START_ROUTINE lpStartAddress;
};

新到达线程的线程 ID 可在 DEBUG_EVENT::dwThreadId 中获得。使用此成员通知用户非常简单

case CREATE_THREAD_DEBUG_EVENT:
{
   CString strEventMessage;
   strEventMessage.Format(L"Thread 0x%x (Id: %d) created at: 0x%x",
            debug_event.u.CreateThread.hThread,
            debug_event.dwThreadId,
            debug_event.u.CreateThread.lpStartAddress);
            // Thread 0xc (Id: 7920) created at: 0x77b15e58
}

'lpStartAddress' 与被调试程序相关,而不是与调试器相关;我们仅为完整性而显示它。请记住,此事件*不会*为进程的主/初始线程接收。它仅为被调试程序中的后续线程创建而接收。

E. 处理 EXIT_THREAD_DEBUG_EVENT

一旦线程返回,系统就会提供返回值,从而引发此事件。DEBUG_EVENT 的 'dwThreadId' 成员指定了哪个线程已退出。为了获取我们在 CREATE_THREAD_DEBUG_EVENT 中收到的线程句柄和其他信息,我们需要将信息存储在某个映射中。此事件有一个名为 'ExitThread' 的相关成员,它属于 EXIT_THREAD_DEBUG_INFO 类型

struct EXIT_THREAD_DEBUG_INFO
{
   DWORD dwExitCode; // The thread exit code of DEBUG_EVENT::dwThreadId
};

这是事件处理程序代码

case EXIT_THREAD_DEBUG_EVENT:
{
   CString strEventMessage;
   strEventMessage.Format( _T("The thread %d exited with code: %d"),
      debug_event.dwThreadId,
      debug_event.u.ExitThread.dwExitCode);    // The thread 2760 exited with code: 0
}

F. 处理 UNLOAD_DLL_DEBUG_EVENT

当然,当 DLL 从被调试程序的内存中卸载时,会发生此事件。但等等!它*仅*针对 FreeLibrary 调用生成,而不是当系统卸载 DLL 时。被调试程序可能多次调用 LoadLibrary,因此只有最后一次调用 FreeLibrary 才会引发此事件。这意味着,隐式加载的 DLL 在进程退出时卸载时不会收到此事件。(你可以在你喜欢的调试器中验证此断言!)

对于此事件,你使用联合体的 'UnloadDll' 成员,它属于 UNLOAD_DLL_DEBUG_INFO 类型

struct UNLOAD_DLL_DEBUG_INFO
{
    LPVOID lpBaseOfDll;
};

正如你所见,只有 DLL 的基地址(一个简单的指针)可供我们处理此事件。这就是为什么我推迟给出 LOAD_DLL_DEBUG_EVENT 代码的原因。在 DLL 加载事件中,我们也获取了 'lpBaseOfDll'。我们可以使用映射(或你喜欢的其他数据结构)来存储*DLL 名称*与*DLL 基地址*的对应关系。在处理 UNLOAD_DLL_DEBUG_EVENT 时,将收到相同的基地址。

应注意,并非所有 DLL 加载事件都会收到 DLL 卸载事件;但我们仍必须将所有 DLL 名称存储到映射中,因为 LOAD_DLL_DEBUG_EVENT 没有提供关于*如何*加载 DLL 的信息。

以下是处理这两个事件的代码

std::map < LPVOID, CString > DllNameMap;
...
case LOAD_DLL_DEBUG_EVENT:
{
   strEventMessage = GetFileNameFromHandle(debug_event.u.LoadDll.hFile);


   // Storing the DLL name into map. Map's key is the Base-address
   DllNameMap.insert(
      std::make_pair( debug_event.u.LoadDll.lpBaseOfDll, strEventMessage) );

   strEventMessage.AppendFormat(L" - Loaded at %x", debug_event.u.LoadDll.lpBaseOfDll);
}
break;
...
case UNLOAD_DLL_DEBUG_EVENT:
{
   strEventMessage.Format(L"DLL '%s' unloaded.",
      DllNameMap[debug_event.u.UnloadDll.lpBaseOfDll] ); // Get DLL name from map.
}
break;

G. 处理 EXIT_PROCESS_DEBUG_EVENT

这是最简单的调试事件之一,正如你可以评估的那样,当进程退出时会发生。无论进程如何退出,此事件都会发生——正常退出、外部终止(任务管理器等),或被调试程序自身故障导致崩溃。

我们使用 'ExitProcess' 成员,它属于 EXIT_PROCESS_DEBUG_INFO 类型

struct EXIT_PROCESS_DEBUG_INFO
{
    DWORD dwExitCode;
};

一旦发生此事件,我们还将结束调试器循环并终止调试线程。为此,我们可以使用一个变量来控制循环(第一页中显示的“for”循环),并设置其值以指示循环终止。请下载附件文件以查看完整代码。

bool bContinueDebugging=true;
...
case EXIT_PROCESS_DEBUG_EVENT:
{
   strEventMessage.Format(L"Process exited with code:  0x%x", 
                          debug_event.u.ExitProcess.dwExitCode);
   bContinueDebugging=false;
}
break;

H. 处理 EXCEPTION_DEBUG_EVENT

这是所有调试事件中最重要(prodigious)的事件!来自 MSDN

当调试进程中发生异常时,会生成此事件。可能的异常包括尝试访问无权访问的内存、执行断点指令、尝试除以零,或结构化异常处理中注意到的任何其他异常。DEBUG_EVENT 结构包含一个 EXCEPTION_DEBUG_INFO 结构。该结构描述了导致调试事件的异常。

此调试事件需要单独的文章来完整(或部分)讨论!因此,我将只讨论一种类型的异常事件,并对此事件本身进行介绍。

成员变量 'Exception' 包含有关刚刚发生的异常的信息。它属于 EXCEPTION_DEBUG_INFO 类型

struct EXCEPTION_DEBUG_INFO
{
    EXCEPTION_RECORD ExceptionRecord;
    DWORD dwFirstChance;
};

'ExceptionRecord' 成员的此结构包含有关异常的详细信息。它属于 EXCEPTION_RECORD 类型

struct EXCEPTION_RECORD
{
    DWORD     ExceptionCode;
    DWORD     ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID     ExceptionAddress;
    DWORD     NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];  // 15
};

详细信息被放入此子结构中,因为异常可能出现嵌套,并且会以链表的方式相互链接。目前讨论嵌套异常已超出范围。

在我们深入研究 EXCEPTION_RECORD 之前,讨论 EXCEPTION_DEBUG_INFO::dwFirstChance 很重要。

异常是否提供机会?

不完全是!当进程被调试时,调试器总是在被调试程序收到异常之前收到它。你在调试 Visual C++ 模块时可能看到过“在 SomeModule 中发生首次异常,地址为 0x00412882:...”。这被称为*首次异常*。相同的异常可能会也可能不会伴随第二次异常。

当被调试程序收到异常时,它被称为*二次异常*。被调试程序可以处理该异常,或者可能直接崩溃。这些类型的异常不是 C++ 异常,而是 Windows 的 SEH(结构化异常处理)机制。我将在本文的下一部分进一步讨论。

调试器首先收到异常(首次异常),以便在将其交给被调试程序之前对其进行处理。断点异常是一种与调试器相关的异常,而不是与被调试程序相关的。某些库还会生成首次异常以辅助调试器和调试过程。

关于 ContinueDebugEvent 的说明

此函数(dwContinueStatus)的第三个参数仅在收到异常事件后才相关。对于我们讨论的非异常事件,系统会忽略传递给此函数的值。

异常事件处理后,应使用以下值调用 **ContinueDebugEvent**

  • 如果调试器已成功处理异常事件,则为 DBG_CONTINUE。被调试程序不需要采取任何操作,并且可以正常运行。
  • 如果被调试程序未处理/解决此事件,则为 DBG_EXCEPTION_NOT_HANDLED。调试器可以仅记录此事件、通知调试器用户或执行其他操作。

请注意,对于不正确的调试事件返回 DBG_CONTINUE 会在调试器中引发相同的事件,并且相同的事件会无限期地到来。由于我们正处于编写调试器的早期阶段,我们应该谨慎行事,并返回 EXCEPTION_NOT_HANDLED(放弃标志!)。本文的例外是断点事件,我将在下面讨论。

异常代码

EXCEPTION_RECORD::ExceptionCode 变量保存收到的异常代码,并且可以具有以下代码之一(忽略嵌套异常!):

  1. EXCEPTION_ACCESS_VIOLATION
  2. EXCEPTION_ARRAY_BOUNDS_EXCEEDED
  3. EXCEPTION_BREAKPOINT
  4. EXCEPTION_DATATYPE_MISALIGNMENT
  5. EXCEPTION_FLT_DENORMAL_OPERAND
  6. EXCEPTION_FLT_DIVIDE_BY_ZERO
  7. EXCEPTION_FLT_INEXACT_RESULT
  8. EXCEPTION_FLT_INVALID_OPERATION
  9. EXCEPTION_FLT_OVERFLOW
  10. EXCEPTION_FLT_STACK_CHECK
  11. EXCEPTION_FLT_UNDERFLOW
  12. EXCEPTION_ILLEGAL_INSTRUCTION
  13. EXCEPTION_IN_PAGE_ERROR
  14. EXCEPTION_INT_DIVIDE_BY_ZERO
  15. EXCEPTION_INT_OVERFLOW
  16. EXCEPTION_INVALID_DISPOSITION
  17. EXCEPTION_NONCONTINUABLE_EXCEPTION
  18. EXCEPTION_PRIV_INSTRUCTION
  19. EXCEPTION_SINGLE_STEP
  20. EXCEPTION_STACK_OVERFLOW

放松!我不会讨论所有这些,但只讨论一个:**EXCEPTION_BREAKPOINT**。好的,这是代码

case EXCEPTION_DEBUG_EVENT:
{
   EXCEPTION_DEBUG_INFO& exception = debug_event.u.Exception;
   switch( exception.ExceptionRecord.ExceptionCode)
   {
      case STATUS_BREAKPOINT:  // Same value as EXCEPTION_BREAKPOINT

         strEventMessage= "Break point";
         break;

      default:
         if(exception.dwFirstChance == 1)
         {
            strEventMessage.Format(L"First chance exception at %x, exception-code: 0x%08x",
                        exception.ExceptionRecord.ExceptionAddress,
                        exception.ExceptionRecord.ExceptionCode);
         }
         // else
         // { Let the OS handle }

         // There are cases where OS ignores the dwContinueStatus,
         // and executes the process in its own way.
         // For first chance exceptions, this parameter is not-important
         // but still we are saying that we have NOT handled this event.

         // Changing this to DBG_CONTINUE (for 1st chance exception also),
         // may cause same debugging event to occur continously.
         // In short, this Debugger does not handle debug exception events
         // efficiently, and let's keep it simple for a while!

         dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
         }

         break;
}

你可能知道什么是断点。从标准的调试器角度来看,断点可以通过 DebugBreak API、{int 3} 汇编指令,或者 .NET Framework 中的 System.Diagnostics.Debugger.Break 来实现。当这些操作在正在运行的进程中发生时,调试器将收到 Debug-exception 代码 STATUS_BREAKPOINT(与 EXCEPTION_BREAKPOINT 相同)。调试器通常使用此事件来中断正在运行的进程,并可能显示事件发生源文件的位置。但在我们基本的调试器中,我们只会向用户显示此事件。不显示源代码或指令位置。我们将在本文的下一部分讨论显示源代码。

从未被调试的进程中引发断点会导致应用程序崩溃,或者可能显示 JIT 对话框。这就是我使用

if ( !IsDebuggerPresent() )
   AfxMessageBox(L"No debugger is attached currently.");
else
   DebugBreak();

作为对这个最简单的调试异常事件的最后说明:EXCEPTION_DEBUG_EVENT 将由内核本身首次引发,并且总是会到达。像 Visual Studio 这样的调试器会忽略这个第一次断点异常,但像 WinDbg 这样的调试器总会向你显示这个事件。

收尾...

使用任何进程进行调试,或使用附加的被调试程序 DebugMe

DebuggeeUI.jpg

此处附加的二进制文件(EXE)使用 **Visual Studio 2005 Service Pack 1** 编译。你可能没有相同版本的 VC++ 运行时库。你可以从 Microsoft.com 下载它们,或从你的 IDE 中重新构建项目。

后续

© . All rights reserved.