ハードウェアブレークポイントを使う

コードを実際に書いたことがなかったので書いてみた。デバッグレジスタは本来特権レベルでなければ操作できないが、Windowsの場合はSetThreadContext系APIを使って設定することができる。


ハードウェアブレークポイントはソフトウェアブレーク(int 3)ほど使われていないものの、ReadやWrite時にブレークさせることができたり、仮想メモリの書き換えを行わない(チェックサムなどの整合性検査で検知されない*1)で設定できたり、コンテキストに紐づくためスレッド単位の制御が可能であったりと、用途によっては有用だったりする。


以下のプログラムは、Sleepを実行するスレッドを2つ生成し、片方のスレッドのSleepにだけ実行時ブレークポイントを設定する。

//
// hwbp.c
// 
#include <windows.h>
#include <stdio.h>

// for see: IA-32 インテル アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル
//          下巻:システム・プログラミング・ガイド - 15.2. デバッグレジスタ

typedef union 
{
    ULONG_PTR Dr;
    struct Dr
    {
        unsigned int L0        :1; // セットすると現行タスクの関連ブレークポイントをイネーブルにする。
        unsigned int G0        :1; // セットするとすべてのタスクの関連ブレークポイントをイネーブルにする。
        unsigned int L1        :1;
        unsigned int G1        :1;
        unsigned int L2        :1;
        unsigned int G2        :1;
        unsigned int L3        :1;
        unsigned int G3        :1;
        unsigned int LE        :1; // P6ファミリ・プロセッサおよびそれ以降の
        unsigned int GE        :1; // IA-32プロセッサではサポートされていないが、下位互換性のためにセットすべき。
        unsigned int Reserved1 :3;
        unsigned int GD        :1; // セットするとデバッグレジスタ保護がイネーブルになる。
        unsigned int Reserved2 :2; //   デバッグレジスタにアクセスするどの MOV 命令の前でもデバッグ例外が発生する。
        unsigned int Type0     :2;
        unsigned int Length0   :2;
        unsigned int Type1     :2;
        unsigned int Length1   :2;
        unsigned int Type2     :2;
        unsigned int Length2   :2;
        unsigned int Type3     :2;
        unsigned int Length3   :2;
#ifdef _AMD64_
        unsigned int Reserved3 :32;
#endif
    } Fields;
} DebugControlRegister;
C_ASSERT(sizeof(DebugControlRegister) == sizeof(void*));


typedef enum 
{
    Byte,       // b00
    Word,       // b01
    Reserved,   // b10
    DWord       // b11
} HWBLength;

typedef enum 
{
    Execute,    // b00
    Write,      // b01
    IOReadWrite,// b10  CR4 の DE(デバッグ拡張)フラグをセットしている場合のみ
    ReadWrite   // b11
} HWBType;

typedef enum
{
    Dr0,
    Dr1,
    Dr2,
    Dr3
} HWBRegister;

BOOL SetHardwareBreakPoint(
    __in HANDLE Thread,
    __in ULONG_PTR Address,
    __in HWBLength Length,
    __in HWBType Type,
    __in HWBRegister DebugRegister)
{
    CONTEXT Context;
    DebugControlRegister Dr7;
 
 
    if (SuspendThread(Thread) == -1)
    {
        return FALSE;
    }
 
    Context.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_CONTROL;
    if (!GetThreadContext(Thread, &Context))
    {
        goto return_FALSE;
    }
 
    // invalid parameter check
    if (Length == Reserved)
    {
        goto return_FALSE;
    }
 
    // alignment check
    if ((Length == Word  && Address % 2)
     || (Length == DWord && Address % 4))
    {
        goto return_FALSE;
    }
 
    switch (DebugRegister)
    {
    case Dr0:
        Dr7.Dr             = Context.Dr7;
        Dr7.Fields.L0      = 1;
        Dr7.Fields.LE      = 1;
        Dr7.Fields.Length0 = Length;
        Dr7.Fields.Type0   = Type;
        Context.Dr7 = Dr7.Dr;
        Context.Dr0 = Address;
        break;

    case Dr1:
        Dr7.Dr             = Context.Dr7;
        Dr7.Fields.L1      = 1;
        Dr7.Fields.LE      = 1;
        Dr7.Fields.Length1 = Length;
        Dr7.Fields.Type1   = Type;
        Context.Dr7 = Dr7.Dr;
        Context.Dr1 = Address;
        break;

    case Dr2:
        Dr7.Dr             = Context.Dr7;
        Dr7.Fields.L2      = 1;
        Dr7.Fields.LE      = 1;
        Dr7.Fields.Length2 = Length;
        Dr7.Fields.Type2   = Type;
        Context.Dr7 = Dr7.Dr;
        Context.Dr2 = Address;
        break;

    case Dr3:
        Dr7.Dr             = Context.Dr7;
        Dr7.Fields.L3      = 1;
        Dr7.Fields.LE      = 1;
        Dr7.Fields.Length3 = Length;
        Dr7.Fields.Type3   = Type;
        Context.Dr7 = Dr7.Dr;
        Context.Dr3 = Address;
        break;

    default:
        goto return_FALSE;
    }
 
    if (!SetThreadContext(Thread, &Context))
    {
        goto return_FALSE;
    }
    return (ResumeThread(Thread) != -1);

return_FALSE:    
    ResumeThread(Thread);
    return FALSE;
}



DWORD WINAPI StartRoutine(LPVOID lpThreadParameter)
{
    for (;;)
    {
        LPEXCEPTION_POINTERS Exception;
        __try
        {
            printf("TID:0x%08X Enter Sleep\n", GetCurrentThreadId());
            Sleep(INFINITE);
        }
        __except (Exception = GetExceptionInformation(), EXCEPTION_EXECUTE_HANDLER)
        {   
#ifdef _X86_
            printf("Exception!! %p\n", Exception->ContextRecord->Eip);
#else
            printf("Exception!! %p\n", Exception->ContextRecord->Rip);
#endif
        }
        SleepEx(2000, FALSE);
    }
    return 0;
}

int main(int argc, char* argv[])
{
    HANDLE Thread;
    ULONG_PTR Address;
    
    Thread = CreateThread(NULL, 0, StartRoutine, NULL, CREATE_SUSPENDED, NULL);
    Address = (ULONG_PTR)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "Sleep");
    printf("BreakPoint: %p\n", Address);
    if (!SetHardwareBreakPoint(Thread, Address, Byte, Execute, Dr0))
    {
        return 1;
    }
    ResumeThread(Thread);
    CloseHandle(Thread);
    
    CloseHandle(CreateThread(NULL, 0, StartRoutine, NULL, 0, NULL));

    Sleep(INFINITE);
    return 0;
}
BreakPoint: 000000007784C120
TID:0x00000AB0 Enter Sleep
TID:0x000012A4 Enter Sleep
Exception!! 000000007784C120
TID:0x00000AB0 Enter Sleep
Exception!! 000000007784C120
TID:0x00000AB0 Enter Sleep
Exception!! 000000007784C120
TID:0x00000AB0 Enter Sleep
Exception!! 000000007784C120
TID:0x00000AB0 Enter Sleep
Exception!! 000000007784C120
...

Visual Studioではデータ ブレークポイントとして近い機能が、WinDbgではそのものがba (Break on Access)として提供されている。

*1:もちろんデバッグレジスタの内容を確認されたら検知される