調試篇——斷點與單步

寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章後面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


🔒 華麗的分割線 🔒


概述

  在軟件調試的過程中,最常用的就是斷點和單步了。有過調試經驗的人知道,斷點有軟件斷點、硬件斷點、內存斷點;單步有單步步入和單步步過。下面我們來介紹它們的實現原理。

軟件斷點

  軟件斷點是實現的本質就是在下斷點的地方插入int3,也被稱之為CC斷點。下面我們來做個實驗:
  我們打開一個調試器調試一個進程,然後在某條彙編下斷點,於此同時通過CE來查看當前指令的位元組變化情況,如下是操作示例:

  也就是說,調試器幫我們屏蔽掉了0xCC的顯示,本質上還是int3,就憑這些我們就可以寫一個接管軟件斷點的簡單的調試器了:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;

#define DBGBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
    DWORD buffer;
        
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }

            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    isContinue = BreakPointHandler(&info);
                    break;
                }
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT"); 

#ifdef DBGBREAKPOINT

                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,DBG_CONTINUE);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("創建進程失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        printf("COMMAND>> ");
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  我定義DBGBREAKPOINT這個宏是為了方便之後實驗只需定義宏來控制單個功能的測試。WaitUserInput函數裏面清空緩衝區的代碼不清楚的話可以閱讀 羽夏閑談—— C 的 scanf 的高級用法 ,如下是我的實驗結果:

  當然我們只是簡單的實現,一般的調試器都會保留這個斷點,具體實現我就不實現了。

內存斷點

  內存斷點是通過修改頁屬性來製造異常出現接管的,既然是修改其他程序的,就需要調用下面的函數:

BOOL VirtualProtectEx(
  HANDLE hProcess,        // handle to process
  LPVOID lpAddress,       // region of committed pages
  SIZE_T dwSize,          // size of region
  DWORD flNewProtect,     // desired access protection
  PDWORD lpflOldProtect   // old protection
);

  內存斷點有被分為內存訪問斷點和內存寫入斷點,它們分別對應的第四個參數為PAGE_NOACCESSPAGE_EXECUTE_READPAGE_NOACCESS屬性會把對應的頁屬性的P位改為0,而PAGE_EXECUTE_READ對應的頁屬性的P位雖然是1,但R/W0。下面我們來實現一個簡單的內存訪問斷點:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

//#define DBGBREAKPOINT
#define MemBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;

    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }

            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");  

#ifdef DBGBREAKPOINT

                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

#endif
    
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;   
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("內存執行斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("內存訪問斷點的位置:0x%X\n",buffer);
#endif
                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");    
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("創建進程失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
        
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("內存執行斷點:%x %x\n",ExceptionADDR,   ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,  orignExcuteAccess,&orignExcuteAccess))
        {
            printf("內存斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  可以看出,該代碼是基於軟件斷點實驗基礎上寫的。在WaitUserInput有一個小Bug,就是沒有輸入的話至少需要隨意輸入字符來繼續清空緩衝區操作,不過對於示例來說無傷大雅,如下是測試效果圖:

  注意,我是簡單是實現內存斷點,為什麼這麼說呢?是因為這個函數一旦影響就是一個物理頁,所以你得做判斷。如果對數據下訪問斷點,如果多於1個位元組,你還得考慮在多個物理頁的情況,還得實現內存斷點的記錄功能。這些都是一個基本能用的調試器所具備的功能。

硬件斷點

概述

  硬件斷點和上面的不同,它是基於硬件的,不依賴調試程序,有自己的優勢,如果通過CRC校驗是不會被檢測到的。如下是與硬件斷點相關的寄存器結構:

  Dr0 ~ Dr3用於設置硬件斷點,Dr4Dr5被保留了。由於只有4個斷點寄存器,所以最多只能設置4個硬件調試斷點。Dr7是最重要的寄存器,它比較複雜,我們來看看它的結構:

L0/G0 ~ L3/G3

  控制Dr0 ~ Dr3是否有效,局部還是全局。每次異常後,Lx都被清零,Gx不清零。

LEN0 ~ LEN3

  表示硬件斷點的長度。如果是0表示1個位元組;是1表示2個位元組;是3表示4個位元組。

R/W0 ~ R/W3

  指示斷點類型。如果是0表示執行斷點;是1表示寫入斷點;是3表示訪問斷點。

處理

  硬件調試斷點產生的異常是STATUS_SINGLE_STEP,即單步異常。觸發異常後,B0 ~ B3對應的位會被置1,以此可以區分單步步入產生的單步異常,後續會詳細講解。

實驗

  如下是實驗代碼:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

//#define DBGBREAKPOINT
//#define MemBREAKPOINT
#define HardwareBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
        
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }
   
            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                case EXCEPTION_SINGLE_STEP:
                    ret = SingleStepHandler(&info);
                    break;
                default:
                    ret = FALSE;
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");  
    
#ifdef DBGBREAKPOINT
    
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                    
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
    
#endif
    
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;   
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("內存執行斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("內存訪問斷點的位置:0x%X\n",buffer);
#endif
    
#ifdef HardwareBREAKPOINT
                SuspendThread(pi.hThread);
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);
                CONTEXT context;
                context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
                GetThreadContext(pi.hThread,&context);
                context.Dr0 = buffer;
                context.Dr7 |= 1;
                if (!SetThreadContext(pi.hThread,&context))
                {
                    puts("硬件斷點設置失敗!");
                }
                ResumeThread(pi.hThread);
#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");    
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("創建進程失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
        
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("內存執行斷點:%x %x\n",ExceptionADDR,ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,orignExcuteAccess,&orignExcuteAccess))
        {
            printf("內存斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info)
{
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
        
    if (context.Dr6&0xF)
    {
        puts("硬件斷點被觸發!");
        context.Dr7&=~1;
    }
    else
    {
        printf("單步中……:0x%X\n",context.Eip);
    }
        
    WaitUserInput();
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  硬件斷點的實現十分簡單,就不贅述了,如下是實驗效果圖:

單步步入

  在調試中我們經常一條指令一條指令的進行調試,這大大方便了我們查閱結果,CPU提供了這樣的基址,就是在Eflag中的TF位實現的,如下圖所示:

  好我們繼續在之前的代碼基礎上擴展單步功能:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

#define DBGBREAKPOINT
//#define MemBREAKPOINT
//#define HardwareBREAKPOINT


int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
    
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);
    
    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }
            
            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                case EXCEPTION_SINGLE_STEP:
                    ret = SingleStepHandler(&info);
                    break;
                default:
                    ret = FALSE;
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");        
                
#ifdef DBGBREAKPOINT
                
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;
                
                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                
                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟件斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                
#endif
                
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;            
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("內存執行斷點設置失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("內存訪問斷點的位置:0x%X\n",buffer);
#endif
                
#ifdef HardwareBREAKPOINT
                SuspendThread(pi.hThread);
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);
                CONTEXT context;
                context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
                GetThreadContext(pi.hThread,&context);
                context.Dr0 = buffer;
                context.Dr7 |= 1;
                if (!SetThreadContext(pi.hThread,&context))
                {
                    puts("硬件斷點設置失敗!");
                }
                ResumeThread(pi.hThread);
#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");                
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }
            
            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);            
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("創建進程失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
    
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
    
    WaitUserInput();
    
    SuspendThread(pi.hThread);
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    ResumeThread(pi.hThread);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
    
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("內存執行斷點:%x %x\n",ExceptionADDR,ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,orignExcuteAccess,&orignExcuteAccess))
        {
            printf("內存斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info)
{
    
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    SuspendThread(pi.hThread);
    GetThreadContext(pi.hThread,&context);
    
    if (context.Dr6&0xF)
    {
        puts("硬件斷點被觸發!");
        context.Dr7&=~1;
    }
    else
    {
        printf("單步中:0x%X\n",context.Eip);
        context.EFlags &= ~0x100;
    }    
    SetThreadContext(pi.hThread,&context);
    ResumeThread(pi.hThread);
    WaitUserInput();
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        case 't':
            p=false;
            SuspendThread(pi.hThread);
            CONTEXT context;
            context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
            GetThreadContext(pi.hThread,&context);
            context.EFlags |= 0x100;
            if (!SetThreadContext(pi.hThread,&context))
            {
                puts("單步設置失敗!");
            }
            ResumeThread(pi.hThread);
            break;
        default:
            break;
        }
    }
}

  單步是實現比較簡單,如下是效果圖:

單步步過

  單步步過和單步步入不同,單步步入會逐個走指令,到達call執行會進入,而單步步過不會進入。單步步過是可以基於硬件斷點或者軟件斷點實現。至於實現過程我就不贅述了。

小結

  本篇文章主要介紹了調試器斷點和單步的實現相關基本知識,如果要真正的實現一個調試器,還需要大量的實現。在總結與提升篇,我還會詳細介紹所有斷點和單步異常的內核處理流程。

下一篇

  調試篇——總結與提升