关于用户层调试器编写。

Preamble
前言

在我们使用某些语言时都用过某些调试器。你使用过的调试器可能用C++、C#、Java或者其它语言编写的。它可能是独立的,像WinDbg,或者内嵌在一个像Visual Studio 的IDE中。然而你是否会对“调试器如何工作”的感到好奇?

好,这篇文档展示了调试器如何工作的隐藏亮点。这篇文章仅包含编写Windows上的调试器。请注意,我在这仅关注“调试器”,而不是编译器、链接器或调试器的扩展。因此,我们仅调试可执行的(像WinDbg)。这篇文章假设读者对多线程有基本的理解(阅读我的关于多线程的文章)

1. How to Debug a Program?
1. 怎样调试一个程序?

两步:

1. 使用DEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS标志启动进程。

2. 设置调试器的循环,那将处理调试事件。

在进一步阅读之前,请记住:

1. 调试器是调试其它进程(目标进程)的进程/程序。

2. 被调试者是被调试器调试的进程。

3. 一个被调试者仅可以与一个调试器关联。然而,一个调试器可以调试多个进程(在不同线程中)。

4. 仅仅创建/产生被调试者的线程可以调试目标进程。因此,CreateProcess和调试循环必须在同一个线程中。

5. 当调试线程终止时,被调试者也终止。然而调试进程可能保持运行。

6. 当调试器的调试线程正在忙于处理一个调试事件时,在被调用者(目标进程)所有的线程保持挂起状态。后面会有更多讨论。

A. Starting the process with the debugging flag
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. The debugger loop
B. 调试循环

调试循环式调试器的中心区域!这个循环围绕着WaitForDebugEvent API运行。这个API需要两个参数:一个指向DEBUG_EVENT结构体的指针和一个DWORD类型的超时参数。对于超时,我们简单的指定无限(INFINITE这个APIkernel32.dll中,因此我们不需要连接任何库了。

DEBUG_EVENT结构体包括调试事件信息。它有4个成员:调试事件代码,进程ID,线程ID事件信息。只要WaitForDebugEvent一返回,我们就处理接收到的调试事件,最后调用ContinueDebugEvent。这有一个最小的调试循环:

DEBUG_EVENT debug_event = {0};
for(;;)
{
    if (!WaitForDebugEvent(&debug_event, INFINITE))
        return;
    ProcessDebugEvent(&debug_event);  //用户定义的函数,不是API
    ContinueDebugEvent(debug_event.dwProcessId,
                      debug_event.dwThreadId,
                      DBG_CONTINUE);
}

使用ContinueDebugEvent API,我们要求操作系统继续运行被调试者。dwProcessIddwThreadId分别指定进程和线程。这些值和我们从WaitForDebugEvent得到的相同。最后一个参数指定是否需要继续运行。这个参数仅用于判断是否有异常事件。我们将在后面讨论。在那之前,我们仅会利用DBG_CONTINUE(另一个可能值是DBG_EXCEPTION_NOT_HANDLED)。

2. Handling debugging events
2. 处理调试事件

这有9种不同的主要调试事件,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_EVENTOUTPUT_DEBUG_STRING_INFO成员就是正确的。

A. Processing OUTPUT_DEBUG_STRING_EVENT
A. 处理OUTPUT_DEBUG_STRING_EVENT

程序员一般使用OutputDebugString产生输出到调试器’输出’窗口的调试文本。根据你使用的语言/框架,你可能对TRACE,ATLTRACE宏很熟悉。一个.NET程序员可能使用System.Diagnostics.Debug.Print/System.Trace.WriteLine方法(或其它方法)。但是对于所有这些方法,OutputDebugString API都会被叫用,调试器也会接收到这个事件(除非它被DEBUG符号取消定义!)。

当这个事件到达时,我们工作在(work on)DebugString成员变量。OUTPUT_DEBUG_STRING_INFO结构体是这样定义的:

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

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

要从另一个进程内存中读取数据,我们使用ReadProcessMemory函数。那要求调用进程需要有适当的权限。因为调试器仅创建了那个进程,我们当然有这个权利。这是处理这个调试事件的代码:

case OUTPUT_DEBUG_STRING_EVENT:
{
   CStringW strEventMessage;  // 强制 Unicode
   OUTPUT_DEBUG_STRING_INFO & DebugString = debug_event.u.DebugString;
 
   WCHAR *msg=new WCHAR[DebugString.nDebugStringLength];
   // 不用关心字符串是不是ANSI编码,我们分配双倍空间...
 
   ReadProcessMemory(pi.hProcess,       // 被调试者的HANDLE
         DebugString.lpDebugStringData, // 目标进程的正确指针
         msg,                           // 复制这个地址空间
         DebugString.nDebugStringLength, NULL);
 
   if ( DebugString.fUnicode )
      strEventMessage = msg;
   else
      strEventMessage = (char*)msg; // char*到CStringW(Unicode)的转换.
 
   delete []msg;
   // 使用 strEventMessage
}

What if the debuggee terminates before the debugger copies the memory contents?

如果被调试者在调试器复制内存内容时终止了?

好……在这种情况下,我想提醒你:当调试器处理一个调试事件时,挂起被调试者的所有线程。这个时候那个进程就无法杀死自己。并且,没有其它的方法可以终止那个进程(任务管理器,进程资源管理器,杀死效用(Kill utility)……)。然而,这些工具的杀死那个进程的企图将会按照调度终止进程。因此,调试器将会接收到的下一个事件是EXIT_PROCESS_DEBUG_EVENT

B. Processing CREATE_PROCESS_DEBUG_EVENT
B. 处理CREATE_PROCESS_DEBUG_EVENT

当那个进程(被调试者)产生时会引发这个事件。这是调试器(sedebugger)接收到的第一个事件。对于这个事件,CreateProcessInfoDEBUG_EVENT的相关成员。这是CREATE_PROCESS_DEBUG_INFO结构体的定义:

struct CREATE_PROCESS_DEBUG_INFO
{
    HANDLE hFile;   // 物理文件的句柄(.EXE)
    HANDLE hProcess; //进程的句柄
    HANDLE hThread;  // 进程的main/初始化线程的句柄
    LPVOID lpBaseOfImage; // 可执行映像的基址
    DWORD dwDebugInfoFileOffset;
    DWORD nDebugInfoSize;
    LPVOID lpThreadLocalBase;
    LPTHREAD_START_ROUTINE lpStartAddress;
    LPVOID lpImageName;  // 指向映像名称第一个字节的指针(在被调试者中)
    WORD fUnicode; // 映像名称是否Unicode编码.
};

请注意我们接收到的pi (PROCESS_INFORMATION)中的hProcess 和 hThread可能不是一个句柄值。然而进程ID和线程ID可能是相同的。你获取的每个窗口(对于同一个资源)句柄和其它的句柄都是不同的,也有不同的目的。因此,调试器可能选择显示句柄或者ID。

通过hFilelpImageName都能获取正在被调试进程的文件名称。尽管我们已经知道了进程的名称是什么,因为我们仅仅创建了被调试者。但是定位的EXE或DLL模块名称是重要的,因为当处理LOAD_DLL_DEBUG_EVENT消息时我们常常需要找到DLL的名称。

和你在MSDN上读到的一样,lpImageName从不会直接返回文件名称,而且这个名称会在目标进程中。更进步的说,在目标进程中可能没有文件名称(例如,通过ReadProcessMemory)。还有,文件名称可能不是完全符合要求(就像我曾经测试的)。因此,我们不会使用这个方法。我们将会从hFile成员获取文件名称。

How to get the name of the file by HANDLE
怎样通过HANDLE获取文件名称

不幸的是,我们需要使用MSDN描述的方法,使用将近10个API调用来从句柄获取文件名称。我稍微修改了GetFileNameFromHandle函数。为了简洁,这里没有显示代码,可以从和这篇文章关联的源代码文件中获取。还有,这时处理这个事件的基本代码:

{
   CString strEventMessage =
     GetFileNameFromHandle(debug_event.u.CreateProcessInfo.hFile);
   // 使用 strEventMessage和CreateProcessInfo的其它成员
   // 来暗示这个事件的用户.
}

你可能已经注意到了我没有讨论这个结构体的一些成员。我可能在这篇文章的下一个部分讨论它们的全部。

C. Processing LOAD_DLL_DEBUG_EVENT
C.处理LOAD_DLL_DEBUG_EVENT

这个事件和CREATE_PROCESS_DEBUG_EVENT很相似,就像你能猜到的,这个事件会在OS载入了一个DLL时引发。无论什么时候载入了一个DLL这个都会引发这个事件,不论是隐式的还是明确的(当被调试者调用LoadLibrary时)。这个调试事件仅在系统第一次关联一个DLL到一个进程的虚拟地址空间发生。对于这个事件的处理,我们使用联合体的'LoadDll'成员。它的类型是LOAD_DLL_DEBUG_INFO

struct LOAD_DLL_DEBUG_INFO
{
   HANDLE hFile;         //DLL物理文件的句柄.
   LPVOID lpBaseOfDll;   // 进程中DLL实际导入地址.
   DWORD dwDebugInfoFileOffset;
   DWORD nDebugInfoSize;
   LPVOID lpImageName;   // 这两个成员和CREATE_PROCESS_DEBUG_INFO一样
   WORD fUnicode;
};

想要获取这个文件名称,我们使用和CREATE_PROCESS_DEBUG_EVENT事件中使用的相同的函数GetFileNameFromHandle。当我描述UNLOAD_DLL_DEBUG_EVENT时我将会列出处理这个事件的代码,因为UNLOAD_DLL_DEBUG_EVENT没有任何直接的可用信息来查找DLL文件的名称。

D. Processing CREATE_THREAD_DEBUG_EVENT
D.处理CREATE_THREAD_DEBUG_EVENT

无论何时被调试者创建了一个新的线程是这个调试事件就会产生。像CREATE_PROCESS_DEBUG_EVENT,这个事件是在一个线程实际开始运行前触发的。我们使用联合体成员来获取关于这个事件的信息。这个变量的类型是CREATE_THREAD_DEBUG_INFO:

struct CREATE_THREAD_DEBUG_INFO
{
  //被调试者创建的新线程的句柄
  HANDLE hThread;
  LPVOID lpThreadLocalBase;
  // 指向线程的起始地址的指针
  LPTHREAD_START_ROUTINE lpStartAddress;
};

DEBUG_EVENT::dwThreadId中可以得到新到来的线程的线程ID。使用这个成员暗示用户直截了当:

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);
            // 线程0xc(Id:7920)创建在: 0x77b15e58
}

'lpStartAddress'和被调试者相关而不是调试器;我们只是为了完整的显示它。记住这个事件不是在进程的主/初始化线程中接收到的。仅在被调试者创建子线程时会接收到。

E. Processing EXIT_THREAD_DEBUG_EVENT
E. 处理EXIT_THREAD_DEBUG_EVENT

线程一返回就会引发这个事件,系统可以得到返回代码。DEBUG_EVENT的成员'dwThreadId'指定了哪个线程退出了。要获得CREATE_THREAD_DEBUG_EVENT事件中我们接收到的线程句柄和其它信息,我们需要在一些map中存储信息。这个事件和名称为'ExitThread'的成员关联,类型是EXIT_THREAD_DEBUG_INFO:

struct EXIT_THREAD_DEBUG_INFO

{
   DWORD dwExitCode; // DEBUG_EVENT::dwThreadId的线程退出代码
};

这是事件处理代码:

case EXIT_THREAD_DEBUG_EVENT:
{
   CString strEventMessage;
   strEventMessage.Format( _T("%d 线程退出,代码: %d"),
      debug_event.dwThreadId,
      debug_event.u.ExitThread.dwExitCode);    //2760线程退出,代码:0
}
F. Processing UNLOAD_DLL_DEBUG_EVENT
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'。我们可以使用map(或你喜欢的其它的数据结构)来存储对应DLL基址的DLL名称。在处理UNLOAD_DLL_DEBUG_EVENT时会接收到相同的基址。

应该注意到并不是所有的DLL装载事件都会获取DLL卸载事件;还有,我们还得把所有DLL名称保存到map中,因为LOAD_DLL_DEBUG_EVENT不给我们提供DLL是怎么装载的信息。

这是处理这两个事件的代码:

std::map < LPVOID, CString > DllNameMap;
...
case LOAD_DLL_DEBUG_EVENT:
{
   strEventMessage = GetFileNameFromHandle(debug_event.u.LoadDll.hFile);
 
 
   // 将DLL名称存储到map中。Map的键值是基址。
   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] ); // 从map中获取DLL名称
}
break;
G. Processing EXIT_PROCESS_DEBUG_EVENT
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"进程退出,代码:0x%x",
                          debug_event.u.ExitProcess.dwExitCode);
   bContinueDebugging=false;
}
break;
H. Processing EXCEPTION_DEBUG_EVENT
H. 处理EXCEPTION_DEBUG_EVENT

在所有调试事件中这是一个很大的事件!摘自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是很重要的。

Are exceptions giving chances?

异常提供了机会?

不太准确!当一个进程被调试时,调试器总在被调试者获取它之前接收到。当调试你的Visual C++模块时你一定见过“在一些模块中首次异常发生在0x00412882:...”。这被称为首次机会异常。第二次相同的异常可能会也可能不会随之发生。

当被调试者获取异常时,这称为第二次机会异常。被调试者可能处理这个异常,或可能简单的崩溃了。这些类型的异常不是C++异常,而是Windows的SEH(结构异常处理)机制。我会在这篇文章的下一部分讨论关于这些内容的更多内容。

调试器首先获取异常(首次机会异常),以便可以在投递给被调试者前处理它。断点异常时一种与调试器相关的异常,而不是被调试者。一些库也会产生首次机会异常来辅助调试器和调试过程。

A word for ContinueDebugEvent
一个关于ContinueDebugEvent的字

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

在异常事件处理后,ContinueDebugEvent需要这样调用:

· DBG_CONTINUE如果调试器成功处理异常事件。不要求被调试者有任何动作,被调试者也可以正常运行。

· DBG_EXCEPTION_NOT_HANDLED如果被调试者没有处理/解决这个事件。调试器可能仅仅记录下这个事件,通知调试器用户,或做其它的。

请注意在调试器中对于不合适的调试事件返回DBG_CONTINUE将会引起相同的事件,同样的事件将会无限的发生。因为我们处在编写调试器的早期,因此我们应该安全进行,并且返回EXCEPTION_NOT_HANDLED(放弃标识!)。这篇文章将断点事件排除,我将会在下一篇讨论它。

Exceptions codes
异常代码

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:  // 和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);
         }
         // 否则
         // {让操作系统处理 }
 
         // 有些情况下操作系统忽略dwContinueStatus,
         // 并以它自己的方式执行进程.
         // 对于首发机会异常,这个参数并不重要
         // 但是我们仍然要说我们还没有处理这个事件.
 
         // 将这个改为DBG_CONTINUE (对于首发机会异常也是),
         // 可能引起同样的调试事件连续出现.
         // 总之,这个调试器没有处理调试异常事件
         // 高效,让我们保持一段时间的简单性!
 
         dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
         }
 
         break;
}

你可能意识到了什么是断点。在标准调试器场景外,断点可以发生于DebugBreak API,或{int 3}汇编指令,或.NET框架的System.Diagnostics.Debugger.Break。在正在运行的进程中,当这些情况中的任何一个发生时,调试器会收到调试异常代码STATUS_BREAKPOINT(和EXCEPTION_BREAKPOINT相同)。但是在我们的基本的调试器中,我们仅将这个事件展示给用户。不展示任何源代码或指令。我们将会在这篇文章的下一部分介绍显示源代码。

从一个没有被调试过的进程中设定一个断点可能会使程序崩溃,或者可能显示JIT对话框。该是我用过的:

if ( !IsDebuggerPresent() )
   AfxMessageBox(L"当前没有关联调试器.");
else
   DebugBreak();

作为最后一个最简单的调试异常事件:EXCEPTION_DEBUG_EVENT会被内核本身发起,并且同时还会到达。像Visual Studio这样的调试器会忽略第一个断点异常,但是像WinDbg这样的调试器则会向你显示这个事件。

Winding up...
清盘……

使用任何调试的程序或者名称为DebugMe的被关联的被调试者:

clip_image002

附加的二进制文件(EXE)使用Visual Studio 2005 Service Pack 1编译。你可能没有相同版本的VC++运行时链接库。你可以从Microsoft.com下载或从你的IDE中重建工程。

Follow up:
追加:
License
许可

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

这篇文章和相关的源代码和文件,由The Code Project Open License (CPOL)授权。

About the Author
关于作者

Ajay Vijayvargiya

clip_image003[4]

Software Developer (Senior) 
clip_image004India 
Member

Started programming with GwBasic back in 1996 (Those lovely days!). Found the hidden talent! 
早在1996年开始使用GwBasic编程(那些可爱的日子!)。发现了隐藏的天赋!

Touched COBOL and Quick Basic for a while. 
接触过一段时间的COBOL和Quick Basic。

Finally learned C and C++ entirely on my own, and fell in love with C++, still in love! Began with Turbo C 2.0/3.0, then to VC6 for 4 years! Finally on VC2008/2010. 
最后自学了C和C++,然后爱上了C++,一直爱着!开始使用Turbo C 2.0/3.0,接着4年VC6 !最后VC2008/2010。

I enjoy programming, mostly the system programming, but the UI is always on top of MFC! Quite experienced on other environments and platforms, but I prefer Visual C++. Zeal to learn, and to share!

我喜欢编程,大部分是系统编程,而UI总是在MFC基础上!其它环境与平台下也有些经验,但我更喜欢Visual C++。热爱学习和分享!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值