Пишем свой отладчик под windows часть 3

Время на прочтение12 мин

Количество просмотров47K

Вступление

Все мы, время от времени, используем дебаггер для отладки программ. Отладчик может использоваться с C++, C#, Java и ещё сотней других языков. Он может быть как внешним (WinDbg), так и встроенным в среду разработки (Visual Studio). Но вы хоть раз задавались вопросом, как же работает отладчик?
И вам повезло. В этом цикле статей мы разберёмся от и до, как же работает отладка изнутри. В этой статье рассматривается только написание отладчика под Windows. Без компиляторов, линковщиков и других сложных систем. Таким образом, мы сможем отлаживать только исполняемые файлы, так как мы напишем внешний отладчик. Эта статья потребует от читателя понимание основ многопоточности.

Как отлаживать программу:

  1. Запустить процесс с флагом DEBUG_ONLY_THIS_PROCESS или DEBUG_PROCESS;
  2. запустить цикл дебага, который будет отлавливать сообщения и события;

Прежде, чем мы начнём, запомните:

  • Дебаггер — это процесс/программа, которая будет отлаживать другой процесс;
  • отлаживаемая программа (ОП) – это процесс/программа, которая отлаживается;
  • именно отладчик присоединяется к ОП. Также отлачик может подключаться к различным процессам (в разных потоках);
  • отлаживать можно лишь те процессы, которые были запущены из под отладчика. Таким образом, CreateProcess и цикл отладчика должны находится в одном потоке;
  • когда завершается процесс отладчика, то он также завершает ОП;
  • Когда отладчик занят обработкой событий, он замораживает все потоки ОП на время. Об этом позже;

Запуск процесса с флагом отладки

Запускаем процесс с помощью функции CreateProcess и в шестом её параметра (dwCreationFlags) указываем флаг DEBUG_ONLY_THIS_PROCESS. Этот флаг указывает 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, а написать отладочный цикл.

Отладочный цикл

Отладочный цикл – это сердце отладчика, и строится он вокруг функции WaitForDebugEvent. Она получает два параметра: указатель на структуру DEBUG_EVENT и таймаут (DWORD). В качестве таймаута мы укажем INFINITE. Эта функция содержится в 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, мы просим ОС продолжить выполнение ОП. dwProcessId и dwThreadId указывает нам на процесс и поток. Эти значения мы получили из WaitForDebugEvent. Последний параметр указывает, продолжить выполнение или нет. Этот параметр будет иметь значение только тогда, когда в отладку пришло исключение. Это мы рассмотрим позже. Ну а пока используем просто DBG_CONTINUE (другое возможное значение – это DBG_EXCEPTION_NOT_HANDLED).

Получение событий отладки

Есть девять основных событий отладки, и 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 указывает, какое событие отладки к нам пришло. В зависимости от этого кода, один из членов union’a u содержит информацию о событии. Например, если dwDebugEventCode==OUTPUT_DEBUG_STRING_EVENT, то верно заполнится тольк OUTPUT_DEBUG_STRING_INFO.

Обработка OUTPUT_DEBUG_STRING_EVENT

Для вывода текста в output, разработчики обычно пользуются функцией OutputDebugString. В зависимости от языка/фреймворка, который вы используете, вы должны быть знакомы с макросами TRACE/ATLTRACE. Разработчики .NET, возможно, знакомы с System.Diagnostics.Debug.Print/System.Diagnostics.Trace.WriteLine. Но все эти методы вызывают OutputDebugString, если объявлен макрос _DEBUG, и отладчик получает сообщение.
Когда сообщение отладки получено, мы обрабатываем DebugString. Структура OUTPUT_DEBUG_STRING_INFO представлена ниже:

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

Поле nDebugStringLength содержит в себе длину строки, включая завершающий null. Поле fUnicode равно нулю, если строка ANSI, и не равна нулю, если юникод. В этом случае, мы должны считывать nDebugStringLength x2 байт. Внимание! 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
}

Что если ОП завершится во время считывания памяти?

Что ж, такого не будет  Позвольте вам напомнить, что отладчик замораживает все потоки ОП во время отработки отладочного сообщения. Таким образом, сам себя процесс завершить не сможет, Ни один диспетчер задач (стандартный или нет) так же не сможет завершить процесс. Если попробовать, то в следующем сообщении наш отладчик получит событие EXIT_PROCESS_DEBUG_EVENT.

Обработка 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.
};

Обратите внимание, что hProcess и hThread могут отличаться от тех, которые мы получаем в PROCESS_INFORMATION. ID процесса и потока должны быть теми же. Каждый хэндл, который вы получаете от Windows, отличается от остальных. Для этого есть различные причины.
hFile, так же как и lpImageName, может использоваться для получения имени файла ОП. Правда мы уже знаем имя этого файла, ведь мы его и запустили. Но расположение EXE или DLL нам важно знать, потому что при получении сообщения LOAD_DLL_DEBUG_EVENT, хорошо бы знать имя библиотеки.
Как вы можете прочитать в MSDN, lpImageName никогда не содержит полное имя файла и оно будет содержаться в памяти ОП. Более того, не существует гарантий, что в памяти ОП будет также лежать полное имя файла. А ещё имя файла может быть неполный. Поэтому, мы будет получать имя файла из hFile.

Как получить имя файла из hFile

К сожалению, нам необходимо будет использовать метод, описанный в MSDN, который содержит примерно 10 вызовов функций. Ниже сокращённый вариант:

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.
}

Вы могли заметить, что я не рассмотрел несколько полей этой структуры. В следующих частях мы рассмотрим это всё досконально.

Обработка LOAD_DLL_DEBUG_EVENT

Это событие похоже на CREATE_PROCESS_DEBUG_EVENT, и как вы уже догадались, это событие вызывается, когда ОС загружает DLL. Это событие возникает каждый раз, когда загружается DLL, явно или неявно. Отладочная информация содержит только время, когда была загруженаDLL, и её виртуальный адрес. Для обработки события, мы используем поле union’a 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;
};

Для получения имени файла, мы будем использовать функцию GetFileNameFromHandle, такую же, как мы использовали в CREATE_PROCESS_DEBUG_EVENT. Я покажу этот код, когда буду рассказывать про UNLOAD_DLL_DEBUG_EVENT. Событие UNLOAD_DLL_DEBUG_EVENT не содержит полной информации об имени DLL библиотеки.

Обработка 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 – адрес начала функции потока относительно ОП, а не отладчика; Мы его просто отображаем для законченности. Обратите внимание, что это событие не генерируется, когда начинает работу основной поток ОП, только при создании новых потоков основным.

Обработка EXIT_THREAD_DEBUG_EVENT

Это событие генерируется, как только дочерний поток завершается и возвращает код возврата в систему. Поле dwThreadId в DEBUG_EVENT содержит ID завершающегося потока. Для получения хэндла потока и другой информации из 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
}

Обработка UNLOAD_DLL_DEBUG_EVENT

Конечно же событие содержит информацию и выгружаемой DLL из памяти ОП. Но не всё так просто! Оно генерируется только в случае вызова FreeLibrary, а не когда система сама выгружает библиотеку. Для получения информации, используйте UnloadDll (UNLOAD_DLL_DEBUG_INFO):

struct UNLOAD_DLL_DEBUG_INFO
{
    LPVOID lpBaseOfDll;
};

Как вы видите, для нас доступен только базовый адрес библиотеки. Именно поэтому я не рассказал вам сразу про код для LOAD_DLL_DEBUG_EVENT. Во время загрузки DLL, мы также получаем lpBaseOfDll. Можно использовать Map для хранения имени библиотеки, помимо её адреса.
Важно заметить, что не все события загрузки библиотеки получат своё событие выгрузки. Тем не менее, мы должны хранить все имена библиотек, так как LOAD_DLL_DEBUG_EVENT не даёт нам информации о том, как библиотека была загружена.
Ниже код для обработки обоих событий:

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;

Обработка EXIT_PROCESS_DEBUG_EVENT

Это одно из самых простых событий, и как вы можете догадаться, вызывается тогда, когда процесс ОП завершается. Это событие показывает нам, как завершился процесс: нормально или экстренно (например, через диспетчер задач), или отлаживаемая программа упала. Информацию мы получаем из EXIT_PROCESS_DEBUG_INFO ExitProcess;

struct EXIT_PROCESS_DEBUG_INFO
{
    DWORD dwExitCode;
};

Как только мы получим это событие, нам необходимо прервать цикл отладки и завершить поток отладки. Для этого мы можем завести флаг, который будет сигнализировать о завершении отладки.

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;

Обработка EXCEPTION_DEBUG_EVENT

Это самая удивительная и сложная вещь во всех событиях отладки. Из MSDN:

Это событие генерируется, когда возникает исключение в отлаживаемом процессе (возможно при делении на ноль, выходе за границы массива, выполнения инструкции int 3 или любого другого исключения, описанного в SEH). Структура DEBUG_EVENT содержит структуру EXCEPTION_DEBUF_INFO. Именно она описывает исключение.

Описание обработки этого события требует отдельной статьи, чтобы рассказать про это полностью (да пусть хоть даже и частично). Поэтому я расскажу пока про один тип исключения.
Поле Exception содержит информацию о только что произошедшем исключении. Ниже можно увидеть описание структуры EXCEPTION_DEBUG_INFO:

struct EXCEPTION_DEBUG_INFO
{
    EXCEPTION_RECORD ExceptionRecord;
    DWORD dwFirstChance;
};

Поле ExceptionRecord содержит детальную информацию об исключении.

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
Когда процесс находится под отладкой, отладчик всегда получает исключение до того, как ОП его получит. Вы должно быть видели запись “First-chance exception at 0x00412882 in SomeModule” пока отлаживали приложение под С++. Это ссылается на First Chance исключения. Такие же исключения могут быть, а могут не быть на second chance исключениях.
Когда ОП кидает исключение, оно трактуется как second chance. ОП может обработать это исключение, а может просто упасть. Эти исключения принадлежат не к C++ исключениям, а к механизму Windows SEH. Я раскрою немного больше в следующей части статьи.
Сначала сообщение об исключении получает отладчик (first chance exception), это помогает ему обработать исключение быстрее ОП. Некоторые библиотеки генерируют исключения first-chance чтобы помочь отладчику делать его работу.

Ещё немного о ContinueDebugEvent

Третий параметр этой функции (dwContinueStatus) важен нам только получения исключения. Для остальных событий этот параметр игнорируется.
После получения исключения, ContinueDebugEvent должен быть вызван с:

  • DBG_CONTINUE, если исключение было успешно поймано отладчиком. От отлаживаемой программы больше ничего не надо и она может выполняться нормально.
  • DBG_EXCEPTION_NOT_HANDLED, если это исключение не обработано (не может быть обработано) отладчиком. Отладчик может лишь сделать запись о том, что это исключение было.

Обратите внимание, что если вернуть DBG_CONTINUE во время того события отладки, в котором это возвращать нельзя, то точно такое же исключение бросится в отладчике, и такое же событие придёт моментально. Но так как мы только начинаем писать отладчик, давайте будет играть с безопасной рогаткой, а не пистолетом, и будем возвращать EXCEPTION_NOT_HANDLED. Исключение в этой статье составляет int 3 (точка останова), которое мы обсудим позже.

Коды исключений

  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);
         }        
         dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
         }

         break;
}

Вам должно быть известно, что такое точка останова. Вне стандартной точки зрения, точку останова можно вызвать с помошью DebugBreak API, или с помощью инструкции ассемблера { int 3 }. В .NET её можно создать с помощью System.Diagnostics.Debugger.Break. Отладчик получит код STATUS_BREAKPOINT (такой же, как EXCEPTION_BREAKPOINT). Отладчик обычно использует это событие для остановки текущего процесса, и может показать исходный код того места, где произошло событие. Но так как у нас отладчик только начинает разрабатываться, то мы будем показывать пользователю только базовую информацию без исходного кода.
Если точка останова будет вызвана в приложении, которое не находится под отладчиком, то оно просто упадёт. Можно использовать следующую конструкцию:

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

В заключении, хотелось бы привести простейшее событие отладки: EXCEPTION_DEBUG_EVENT. Это событие будет приходить постоянно. Отладчики вроде Visual Studio игнорируют его, а WinDbg нет.

Заключение

Используйте любой отладчик для DebugMe.

Вторая часть будет ещё интереснее и она на подходе!
UPD: Часть 2

Что такое отладчик, как им пользоваться и как он реализован, после прочтение первых двух частей, вы знаете. В заключительной части статьи попробуем рассмотреть некоторые методы борьбы с отладчиком, на основе знаний о принципах его работы. Я не буду давать шаблонный набор антиотладочных приемов, благо при желании все это можно найти на просторах интернета, попробую это сделать немного другим способом, на основе некоего абстрактного приложения, у которого буду расширять код защиты от самой простейшей схемы до… пока не надоест :)

Сразу-же оговорюсь, в противостоянии приложение/отладчик, всегда победит последний :)
Но, только в том случае, если им будет пользоваться грамотный специалист, а с такими спецами бороться практически бесполезно (ну, если вы конечно не обладаете как минимум такой же квалификацией).

Правда как показывает практика, грамотные спецы не занимаются неинтересными им задачами, оставляя их на откуп начинающим реверсерам, которые еще не прогрызли свой гранит науки и могут спотыкаться на некоторых неочевидных трюках.

Вот что-то такое мы и рассмотрим, только в очень упрощенной форме.

Простейшая ShareWare:

Представим, у нас есть некое ПО, которое мы решили продавать. Для простоты пусть это будет обычное VCL приложение из пустой формы (ну хорошо, пусть не пустое, а с картинкой на всю морду) и мы хотим его продать. Первый же вопрос, которым нужно озаботится — как сделать так, чтобы наша картинка был видна только тем, кто за нее заплатил? Точнее — как разграничить триальных и легальных пользователей?

Самым очевидным решением является ключ. Триальный пользователь его не знает, а легальный, заплативший за него реальными деньгами, может активировать легальную копию приложения и насладиться картинкой.

Ключ так ключ.

Создаем новое VCL приложение, кидаем на форму TImage с картинкой, Visible ему выставляем в False. После чего размещаем на форме два TEdit, первый для имени пользователя и второй для кода активации. Ну и две кнопки — закрыть приложение и активировать.

Ну вот как-то так:

После чего пишем совершенно секретный код активации:

function TForm1.GenerateSerial(const AppUserName: string): string;
const
  MagicSerialMask: int64 = $C5315E6121543992;
var
  I: Integer;
  SN: int64;
  RawSN: string;
begin
  SN := 0;
  Result := '';
  for I := 1 to Length(AppUserName) do
  begin
    Inc(SN, Word(AppUserName[I]));
    SN := SN * 123456;
  end;
  Sn := SN xor MagicSerialMask;
  RawSN := IntToHex(SN, 16);
  for I := 1 to 16 do
    if ((I - 1) mod 4 = 0) and (I > 1) then
      Result := Result + '-' + RawSN[I]
    else
      Result := Result + RawSN[I];
end;

procedure TForm1.btnCheckSerialClick(Sender: TObject);
begin
  if edSerial.Text <> GenerateSerial(edAppUserName.Text) then
    Application.MessageBox('Неверный код активации',
      PChar(Application.Title), MB_OK or MB_ICONERROR)
  else
  begin
    Image1.Visible := True;
    Label1.Visible := False;
    Label2.Visible := False;
    Label3.Visible := False;
    edAppUserName.Visible := False;
    edSerial.Visible := False;
    btnCancel.Visible := False;
    btnCheckSerial.Visible := False;
  end;
end;

Суть кода в следующем:
на основании имени пользователя приложение генерирует некий серийник и сравнивает его с введенным пользователем. Если все нормально, то убираются все элементы управления, отвечающие за активацию и отображается картинка, которую и жаждал узреть пользователь.

Оть такая:

(ну… первое что нашел :)

После произведенных манипуляций, «это» публикуется на различных шароварных сайтах и даже иногда даются ссылки на программерские форумы в топиках вида: «потестируйте плз защиту».

А как это выглядит со стороны взломщика?

Он берет отладчик (для простоты возьмем тот-же Olly Debug) и видит вот такую картинку:

Кода приложения у него нет, но есть характерная ошибка начинающих «защитников» ПО — вывод диалога о неверном ключе.

Что это дает взломщику?
Он ставит ВР на вызов MessageBoxA и запустив приложение ловит вызов данного сообщения, после чего, нажав на кнопку «ОК» он может вернуться к коду, в котором происходит вызов данной ошибки, где, посмотрев немного выше, сможет определить наличие условного перехода, на основании которого и происходит данный вызов:

На картинке точка принятия решения программой выделена восклицательными знаками.
Все что ему остается сделать, это исправить инструкцию JE на JMP, отключив, таким образом, проверку серийного кода и обеспечив валидный переход на область кода, который должен выполнятся только при активации приложения.

Как-то не понятно, да?

Ну тогда вот вам такая картинка, из отладчика Delphi:

Здесь код более понятен для отладки, и его чтение более удобно из-за размапливания адресов и приведения их в читабельный вид. Например теперь явно видно  что перед выходом на адрес 0х475729, по которому происходит принятие решения, происходит получение текста из TEdit-ов и вызов процедуры GenerateSerial.

Взломщик такой информацией не обладает, и как видно на предыдущем изображении, ему придется проанализировать все вызовы, чтобы составить более-менее понятную для анализа картинку. Ну, правда, тут я немного утрирую, в действительности карта приложения строится достаточно просто, при наличии инструментария, но… Но некоторые иногда упорно стараются отлаживать системные модуля дельфи, таки честь им и хвала за упорство :)

Ну и нюанс, по адресу 0х475729 на скринах расположены две разные инструкции — JZ и JE, это нюансы интерпретации дизассемблеров, они идентичны.

Тут есть один интересный подход, который мне несколько раз озвучивали.
Вот чуть выше я озвучил что поставлю ВР на MessageBoxA, а мне говорят что вызовут MessageBoxW и вызов отловить не получится. Это заявление на твердую четверку с плюсом, ибо да, действительно, если приложение вызовет юникодную API, с бряком будет небольшой промах, но есть нюанс. А давайте-ка развернем весь стек вызова MessageBox.

Смотрите какая интересная картинка получается:
MessageBoxA -> MessageBoxExA -> MessageBoxTimeOutA -> MessageBoxTimeOutW-> SoftModalMessageBox()

Таки да, мы можем поставить ВР на вызове любой из перечисленных функций (обычно достаточно MessageBoxTimeOutW) чтобы отловить необходимый нам вызов, ее кстати так же вызовет и функция MessageBoxW.

Есть правда небольшой нюансик, в Delphi есть и иные способы отображения окна.

Ну например ShowMessage(). Данный метод не вызывает API MessageBox.
Достаточно забавно слушать рассуждения, что данный метод целиком и полностью реализован в виде создания отдельной формы, в которой кнопки размещаются так как им надо и вообще это внутренности самого VCL из которых в отладчике вообще ничего не понятно.
Так-то оно так, кабы данный вызов не упирался в API ShowWindow, с которого по стеку мы так же выйдем на необходимый нам участок кода.

Есть еще вызовы диалогов, но с ними будет точно такая же кухня. Все это детектируется без сильных времязатрат.

Поэтому, делайте первый вывод в свой блокнотик:
Вызов сообщения о неуспешной проверке кода, сразу после данной проверки — есть признак дурного тона.

Вводим контроль целостности приложения:

Ну чтож — вот нас и взломали, причем сделав всего лишь изменение в одном единственном байте приложения. Теперь наша веселая картинка доступна всем абсолютно бесплатно.

Печально, но не критично — будем бороться…

Взлом произошел посредством прямой правки тела приложения.
Значит выросла задача: обеспечить проверку целостности исходного кода.

Звучит грозно, но в действительности практически не выполнимо :)

Вот что мы можем применить для данной проверки?
Есть много умных слов: навесить цифровую подпись, сверить с образом файла на диске, проверить участок кода с контрольной суммой. Все пустое — в итоге все равно приходим к необходимости каким либо образом получить текущее значение кода приложения в памяти…

Ну хорошо: смотрим цифровую подпись. Она, во первых, платная. Во вторых проверка ее производится путем вызова API функции WinVerifyTrust, которая уязвима к перехвату. В третьих она легко удаляется штатными средствами через ImageRemoveCertificate.

Значит не вариант, что у нас по проверке образа файла на диске?
Тут тоже все печально. Смотрите, наш исполняемый файл пропатчили, мы хотим определить это сравнив с образом на диске и что мы делаем — получаем путь к текущему файлу через тот же ParamStr(0) (допустим) после чего открываем файл по данному пути и начинаем проверку, но…
Но на этапе вызова OpenFile/CreateFile взломщик подменяет путь в соответствующем параметре на путь к оригинальному, не измененному образу и все наши проверки идут лесом.

Есть еще один интересный момент. А ведь ваше приложение может храниться на диске и в не измененном виде. Есть такое понятие как лоадеры. Суть их заключается в том, что они запускают процесс и производят модификацию тела приложения непосредственно в памяти.

Вот например возьмем наш отладчик из прошлой статьи и при помощи него запустим наше приложение с волшебной картинкой, а при достижении точки входа выполним следующий код:

procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
  var ReleaseBreakpoint: Boolean);
var
  JmpOpcode: Byte;
begin
  if ExceptionRecord.ExceptionAddress =
    Pointer(FCore.DebugProcessData.EntryPoint) then
  begin
    JmpOpcode := $EB;
    FCore.WriteData(Pointer($475729), @JmpOpcode, 1);

Приложение на диске останется неизменным, но вот вместо инструкции JE будет выполнен прямой переход из-за записанной инструкции JMP. Что уже гораздо печальней, т.к. в данном случае первые два варианта проверки целостности гарантированно не сработают.

Остается третий вариант, проверка участков кода непосредственно в теле приложения.
Это достаточно ресурсоемкий по реализации вариант и так же не всегда приводящий к успеху по следующим причинам.

Во первых константы контрольных сумм. Если они хранятся в теле приложения, взломщик их изменит на правильные. (второй вывод в ваш блокнотик — константы CRC блоков кода в приложении, есть дурной тон).
Во вторых, во второй части статьи я рассказывал о МВР — Memory Breakpoint. Это идеальный механизм детектирования проверок целостности кода (если не учитывать еще более грамотный HBP — Hardware BreakPoint).

Работает просто — если есть подозрение на то, что текущий участок кода контролируется механизмом защиты, на него навешивается МВР или НВР с целью определить, где именно расположена сама проверка целостности кода.
Если таковая проверка детектируется — она так же отключается патчем.

Ну вот мы собственно и приплыли к патовой ситуации: абонент — не абонент :)

Впрочем…

Выкрутиться, конечно можно, но…
Но для начала посмотрим, как вообще реализовать проверку целостности кода приложения.

Если конкретнее, то мы хотим защитить от изменений код который был в самом начале. Для этого нам нужно каким-то образом выяснить его расположение в памяти во время работы приложения.

Метки наше «всё».
На основе меток работает большинство навесных протекторов, стало быть зачем нам придумывать очередной велосипед. Что такое метка — в принципе это столь нелюбимый всеми label, используемый при goto(), о котором свое высококвалифицированное «ФИ» не высказал только самый ленивый.
Впрочем… что нам их мнение? Как я и сказал — метки наше все :)

Правда есть нюансик, label удобно использовать при контролировании небольшой части кода внутри процедуры (при перекрестном контроле — о нем позже), сейчас-же нас интересует несколько процедур в совокупности.

Для этого label не подойдет, но вполне подойдут пустые процедуры, адрес которых мы сможем получить из кода проверки целостности.

Ну и нужна до кучи сама процедура расчета целостности, а так же (что собственно было одним из озвученных выше нюансов) некая константа, с которой мы будем сверять CRC блока данных.

Ну впрочем хватит разглагольствовать, пишем:

const
  CheckedCodeValidCheckSum: DWORD = 248268; // << оть тут мы будем хранить контрольную сумму

procedure CheckedCodeBegin;
begin
end;

function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD;
var
  pCursor: PByte;
  I: Integer;
  Dumee: DWORD;
begin
  Result := 0;
  pCursor := Addr;
  for I := 0 to Size - 1 do
  begin
    if pCursor^ <> 0 then
      Inc(Result, pCursor^)
    else
      Dec(Result);
    Inc(pCursor);
  end;
end;

procedure TForm1.CheckCodeProtect;
var
  CheckedCodeBeginAddr, CheckedCodeEndAddr: Pointer;
  CurrentCheckSum: DWORD;
begin
  // получаем адрес начала защищенного кода
  CheckedCodeBeginAddr := @CheckedCodeBegin;

  // получаем адрес конца защищенного кода
  CheckedCodeEndAddr := @CheckedCodeEnd;

  // Считем контрольную сумму и сверяемся с оригиналом

  CurrentCheckSum := CalcCheckSum(CheckedCodeBeginAddr,
    Integer(CheckedCodeEndAddr) - Integer(CheckedCodeBeginAddr));

  if CurrentCheckSum <> CheckedCodeValidCheckSum then
  begin
    MessageBox(Handle, 'Нарушение целостности исполняемого кода.',
      PChar(Application.Title), MB_ICONERROR);
    TerminateProcess(GetCurrentProcess, 0);
  end;
end;

function TForm1.GenerateSerial(const AppUserName: string): string;
const
  MagicSerialMask: int64 = $C5315E6121543992;
var
  I: Integer;
  SN: int64;
  RawSN: string;
begin
  SN := 0;
  Result := '';
  for I := 1 to Length(AppUserName) do
  begin
    Inc(SN, Word(AppUserName[I]));
    SN := SN * 123456;
  end;
  Sn := SN xor MagicSerialMask;
  RawSN := IntToHex(SN, 16);
  for I := 1 to 16 do
    if ((I - 1) mod 4 = 0) and (I > 1) then
      Result := Result + '-' + RawSN[I]
    else
      Result := Result + RawSN[I];
end;

procedure TForm1.btnCheckSerialClick(Sender: TObject);
begin
  // Проверяем целостность кода
  CheckCodeProtect;
  if edSerial.Text <> GenerateSerial(edAppUserName.Text) then
    ShowMessage('Неверный код активации')
  else
  begin
    Image1.Visible := True;
    Label1.Visible := False;
    Label2.Visible := False;
    Label3.Visible := False;
    edAppUserName.Visible := False;
    edSerial.Visible := False;
    btnCancel.Visible := False;
    btnCheckSerial.Visible := False;
  end;
end;

procedure CheckedCodeEnd;
begin
end;

Что мы здесь имеем:
Две метки в виде пустых процедур CheckedCodeBegin и CheckedCodeEnd, расчет «контрольной суммы» данных между этими двумя метками, производимая процедурой CheckCodeProtect, ну и сама контрольная сумма, вынесенная за область проверяемого кода и представленная константой CheckedCodeValidCheckSum (на ее значение пока не обращайте внимание).

В принципе вообще ничего сложного, но давайте-ка проанализируем, а что нам это вообще дает?

В действительности много, так как:
1. Этот код детектирует патч тела приложения на диске (ибо при запуске оно будет уже с измененными байтами).
2. Этот код детектирует патч тела приложения лоадером (по вышеописанной схеме).
3. И этот код детектирует… помните картинку с прошлой статьи?

Да-да, это самый что ни на есть Breakpoint, установленный отладчиком. И его данной код тоже идеально детектирует, ведь если помните, то механизм установки ВР заключается в модификации тела приложения.

Вот и третья заметочка в ваш блокнот — детект ВР производится проверкой тела кода.

Правда, к сожалению тут не все так просто, в некоторых случаях данный код не сработает, но не будем торопиться, дойдем и до этого.

Теперь к печальному, как я и говорил, данная проверка легко детектируется. Для примера вот скриншот из под отладчика, где он прерывается сразу в начале проверки:

Синим выделен асмкод процедуры расчета контрольной суммы, отладчик прервался на адресе 0х467069, как раз при первой же попытке чтения защищенной области.
Ну точнее тут я немного сжульничал, если-бы код проверки был вне рамок проверяемой области, то остановка произошла бы как раз на данной инструкции, а так я, естественно, остановился на самой первой «PUSH EBX».

Но это лирика, вопрос в другом, и что теперь делать?

Ну, во первых, все не так страшно. Здесь реализована всего лишь одна единственная проверка целостности кода приложения. Да, она легко детектируется. Да, она так-же легко снимается патчем, но что нам мешает сделать их несколько, перекрестно контролирующих друг друга? Снимут и их? Ну не вопрос, добавим еще, а что нам стоит?

Однажды мне прислали продукт на анализ защиты приложения непосредственно разработчики самой защиты (извините — без названий). Бегло просмотрев код инициализации ВМ я примерно сразу наметил путь её разбора, мне нужно было всего лишь вытащить алгоритм крипта маленьких блоков данных при вызове конкретной API функции. Проблема заключалась в том, что как только я пропатчил единственный байт приложения, сработал механизм проверки контрольной суммы.  Естественно я его быстро занопил, но как оказалось занопленный код контролировали уже четыре различных алгоритма. Я начал патчить их и что вы думаете? На каждый патч понимались все новые и новые куски кода, контролирующие целостность кода лавинообразно. В итоге я просто утонул в объеме ручных патчей и пришлось писать автоматическую утилиту/отладчик, что заняло почти неделю работы с учетом всех нюансов. А в конце я уперся в следующий уровень ядра защиты.
Впрочем это уже не важно, важен смысл —  при желании возможно реализовать достойную головную боль взломщику, даже на банальной проверке контрольных сумм.

Ну а теперь к реальности.
Для детектирования кода проверки целостности взломщик применил MBP.
А теперь вспоминаем как они работают — правильно через назначение странице атрибута PAGE_GUARD. Значит, зная принципы работы отладчика, мы можем этому воспрепятствовать, достаточно просто снять данный атрибут и отладчик перестанет реагировать на доступ к якобы контролируемой им памяти.
Правда есть нюансик, произвести мы это сможем при помощи вызова VirtualProtect, которая уязвима, ибо отладчик может ее перехватить и запретить её вызов. Но и на это у нас есть болт с обратной резьбой, например можно поступись так, как описано в данной статье: читаем.

Правда сделаем так, вариант со снятием PAGE_GUARD в демоприложении я рассматривать не буду. Но не переживайте, я покажу еще один интересный способ, только для этого нужно рассмотреть еще несколько нюансов, поэтому чуть позже.

Ну и с данного момента считаем, что код контроля целостности приложения написан так, что его не взломать (дабы упростить)…

Детектирование отладчика

Ну вот, теперь мы пришли к тому, что нашу форму с картинкой хотят, причем при помощи отладчика. Конечно же нужно научится его детектировать. Пока что остановимся на функции IsDebuggerPresent, для начала этого достаточно.

Пишем код:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;

procedure TForm1.FormCreate(Sender: TObject);
begin
  if IsDebuggerPresent then
  begin
    MessageBox(Handle, 'Работа приложения под отладчиком запрещена.',
      PChar(Application.Title), MB_ICONERROR);
    TerminateProcess(GetCurrentProcess, 0);
  end;  
end;

Все очень просто, если мы под отладчиком, данная функция вернет True.
Будем считать, что код проверки целостности приложения у нас настолько сложен, что пропатчить его нельзя и вызов данной функции у нас помещен в защищенный участок.
Что применит в данном случае взломщик?

Вариантов собственно всего три, с учетом того, что патчить тело приложения нельзя:

1. поставить ВР на вызове данной функции, где подменить результат ее вызова.
2. пропатчить код данной функции, чтобы она всегда возвращала False
3. произвести изменение переменной Peb.BeingDebugged в адресном пространстве отлаживаемого процесса.

С третьим вариантом бороться сложно (можно, но не нужно), а вот первые два мы рассмотрим поподробнее, точнее будем рассматривать второй вариант, т.к. в первом так-же производится патч кода приложения, при установке ВР с опкодом 0хСС.

Для начала добавим вот такой код в отлаживаемом приложении в процедуру FormCreate:

procedure TForm1.FormCreate(Sender: TObject);
var
  P: PCardinal;
begin
  P := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');
  ShowMessage(IntToHex(P^, 8));

Он покажет первые 4 байта функции IsDebuggerPresent.

Вот такой код писать нельзя:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;

procedure TForm1.FormCreate(Sender: TObject);
var
  P: PCardinal;
begin
  P := @IsDebuggerPresent;
  ShowMessage(IntToHex(P^, 8));

Ибо во втором варианте мы используем статическую функцию, и адрес будет указывать не на начало тела функции, а на таблицу импорта, где стоит переходник в виде JMP.

Выполним код и запомним значение.

Под каждой системой оно будет разное, в ХР например это будет тело оригинальной функции, в семерке будет переходник на аналог из kernelbase. У меня получилось значение 9090F3EB, что соответствует следующей картинке:

А теперь возьмем наш отладчик из второй части статьи, и в методе OnBreakPoint произведем патч тела данной функции вот таким кодом:

procedure TTestDebugger.HideDebugger;
const
  PachBuff: array [0..2] of Byte =
    (
      $31, $C0, // xor eax, eax
      $C3       // ret
    );
var
  Addr: Pointer;
begin
  Addr := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');
  FCore.WriteData(Addr, @PachBuff[0], 3);
end;

procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
  var ReleaseBreakpoint: Boolean);
var
  JmpOpcode: Byte;
begin
  if ExceptionRecord.ExceptionAddress =
    Pointer(FCore.DebugProcessData.EntryPoint) then
  begin
    HideDebugger;

Здесь нюансик, адрес библиотеки kernel32.dll одинаков для всех приложений, поэтому адрес функции IsDebuggerPresent будет одинаков и в отладчике и в отлаживаемом приложении.

Смысл патча заключается в обниливании регистра EAX, через который возвращается результат функции и возврата к вызывающему данную функцию коду.

Запускаем отладчик, он запустит наше приложение и в результате вмешательства в память процесса, код в функции FormCreate отладчика не обнаружит. Правда теперь код, который считывает первые 4 байта данной функции вернет нам не число 9090F3EB, а число 90C3C031, которое соответствует опкодам патча.

Как мы можем определить что тело данной функции пропатчено? В принципе мы может считать первые 4 байта данной функции из файла kernel32.dll расположенного на диске, однако в этом случае, при открытии тела библиотеки нам могут подменить путь на такой-же патченый файл и проверка скажет что все нормально.

Но есть еще один способ, достаточно редко применяемый на практике (мне встречался, если не ошибаюсь, всего 1 раз) и заключается он в следующем.

Раз мы не можем считать правильное значение с диска, мы можем его получить, считав нужные нам 4 байта из памяти какого нибудь другого процесса. Есть конечно небольшой шанс, что данный процесс так-же находится под отладчиком и в нем таким-же образом перехвачена требуемая нам функция, но очень маленький.

В итоге пишем такой код:

function IsDebuggerPresent: BOOL; stdcall; external kernel32;

procedure TForm1.CheckIsDebugerPresent;
var
  Snapshot: THandle;
  ProcessEntry: TProcessEntry32;
  ProcessHandle: THandle;
  pIsDebuggerPresent: PDWORD;
  OriginalBytes: DWORD;
  lpNumberOfBytesRead: DWORD;
begin
  pIsDebuggerPresent :=
    GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent');
  Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  if Snapshot <> INVALID_HANDLE_VALUE then
  try
    ProcessEntry.dwSize := SizeOf(TProcessEntry32);
    if Process32First(Snapshot, ProcessEntry) then
    begin
      repeat
        if ProcessEntry.th32ProcessID = GetCurrentProcessId then Continue;
        ProcessHandle := OpenProcess(PROCESS_ALL_ACCESS, False,
          ProcessEntry.th32ProcessID);
        if ProcessHandle <> 0 then
        try
          if ReadProcessMemory(ProcessHandle, pIsDebuggerPresent,
            @OriginalBytes, 4, lpNumberOfBytesRead) then
          begin
            if OriginalBytes <> pIsDebuggerPresent^ then
            begin
              MessageBox(Handle, 'Функция IsDebuggerPresent перехвачена.',
                PChar(Application.Title), MB_ICONERROR);
              TerminateProcess(GetCurrentProcess, 0);
            end;
            if IsDebuggerPresent then
            begin
              MessageBox(Handle, 'Работа приложения под отладчиком запрещена.',
                PChar(Application.Title), MB_ICONERROR);
              TerminateProcess(GetCurrentProcess, 0);
            end;
          end;
        finally
          CloseHandle(ProcessHandle);
        end;
      until not Process32Next(Snapshot, ProcessEntry)
    end;
  finally
    CloseHandle(Snapshot);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  CheckIsDebugerPresent;
  CheckCodeProtect;
end;

Здесь я не стал мудрить и воспользовался стандартными возможностями TlHelp32 для получения списка процессов, для примера достаточно.

Соответственно очередная заметка в ваш блокнотик — по возможности всегда проверяйте целостность критических API функций, любым известным вам способом, не обязательно делать так, как я показал.

Да, ну и здесь тоже есть очередной нюансик, под семеркой вызов IsDebuggerPresent из kernel32.dll приведет к вызову этой же функции из kernelbase.dll, где ее так же могут пропатчить, но тут уж думайте сами.

В итоге после всех модификаций данная функция у нас плотно опекается системой контроля целостностии и пропатчить ее в лоб уже достаточно проблематично. Как обойти данный кусок защиты, я покажу позже, а сейчас рассмотрим кое что другое.

Детектирование подключения отладчика к процессу.

Вот смотрите, до этого мы запускали приложение под отладчиком, а что нам стоит запустить его без отладчика, дождаться когда все проверки на наличие отладки пройдут и только после этого подключиться отладчиком к приложению?

Да, в таком варианте весь наш код не сработает, точнее сработает, но частично.

Как вариант, для детектирования такого безобразия, можно например поставить таймер и периодически вызывать процедуру CheckIsDebugerPresent, но де-факто, для детектирования подключения нам это не потребуется. Дело в том что при вызове в отладчике функции DebugActiveProcess, в отлаживаемом приложении всегда вызывается функция DbgUiRemoteBreakin. Зная это мы можем провернуть следующий трюк.

Мы пропатчим сами себя, точнее тело функции DbgUiRemoteBreakin, добавив в ее начало переход на адрес функции TerminateProcess, таким образом, как только произойдет подключение отладчика к процессу, процесс сразу же завершится.

Пишем очередной блок кода:

type
  TDbgUiRemoteBreakinPath = packed record
    push0: Word;
    push: Byte;
    CurrProc: DWORD;
    moveax: byte;
    TerminateProcAddr: DWORD;
    calleax: Word;
  end;

procedure TForm1.BlockDebugActiveProcess;
var
  pDbgUiRemoteBreakin: Pointer;
  Path: TDbgUiRemoteBreakinPath;
  OldProtect: DWORD;
begin
  pDbgUiRemoteBreakin :=
    GetProcAddress(GetModuleHandle('ntdll.dll'), 'DbgUiRemoteBreakin');
  if pDbgUiRemoteBreakin = nil then Exit;
  Path.push0 := $006A;
  Path.push := $68;
  Path.CurrProc := $FFFFFFFF;
  Path.moveax := $B8;
  Path.TerminateProcAddr :=
    DWORD(GetProcAddress(GetModuleHandle(kernel32), 'TerminateProcess'));
  Path.calleax := $D0FF;
  if VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath),
    PAGE_READWRITE, OldProtect) then
  try
    Move(Path, pDbgUiRemoteBreakin^, SizeOf(TDbgUiRemoteBreakinPath));
  finally
    VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath),
      OldProtect, OldProtect);
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  BlockDebugActiveProcess;
  CheckIsDebugerPresent;
  CheckCodeProtect;
end;

В результате такого патча в начале функции DbgUiRemoteBreakin будет размещен следующий код:

То есть грубо на стеке размещаются два параметра необходимые функции TerminateProcess (идут в обратном порядке), это параметр uExitCode равный нулю и параметр hProcess, вместо которого подставляется псевдохэндл DWORD(-1) означающий текущий процесс. После чего регистр EAX инициализируется адресом функции TerminateProcess и происходит ее вызов.

Если попробовать присоединится к процессу при помощи отладчика из второй части статьи, то все что мы сможем увидеть — это приход события CREATE_PROCESS_DEBUG_EVENT, но уже даже в момент прихода данного события мы не сможем ничего сделать с отлаживаемым процессом, например попытка установки ВР будет неуспешна, и т.п.

Для большинства отладчиков этого достаточно.
К сожалению, это не бронебойный вариант, ибо ничто не помешает повторно пропатчить тело нашего приложения, вернув оригинальный код обратно, перед вызовом DebugActiveProcess. (Правда я такого не встречал, но все же…)

Обход Memory Breakpoint

Как я уже и говорил, определить наличие МВР можно через проверку атрибута защиты страницы PAGE_GUARD. Делается это при помощи вызова функции VirtualQuery, а можно просто в лоб переназначить атрибуты вызовом VirtualProtect.

Но есть еще один хитрый способ и называется он ReadProcessMemory. Это та же функция, при помощи которой в отладчике мы читали данные из отлаживаемого процесса. Нюанс ее в следующем, если она попробует считать данные с страницы защищенной флагом PAGE_GUARD блок данных соответствующей странице будет заполнен нулями, причем цимус в том, что при этом не произойдет поднятие события EXCEPTION_GUARD_PAGE в отладчике. Такая вот «тихая проверка региона». Если мы будем ее использовать при проверки целостности кода приложения, в том случае если на него будет установлен МВР данные считаются не верно и в итоге контрольная сумма не сойдется с ожидаемой. Более того, если по адресу, откуда будет читать данная функция выставлен Hardware Breakpoint контролирующий запись, чтение/запись отладчик так же не получит уведомления о его срабатывании.
Поэтому перепишем функцию CalcCheckSum следующим образом:

function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD;
var
  pRealData, pCursor: PByte;
  I: Integer;
  Dumee: DWORD;
begin
  pRealData := GetMemory(Size);
  try
    ReadProcessMemory(GetCurrentProcess, Addr, pRealData, Size, Dumee);
    Result := 0;
    pCursor := pRealData;
    for I := 0 to Size - 1 do
    begin
      if pCursor^ <> 0 then
        Inc(Result, pCursor^)
      else
        Dec(Result);
      Inc(pCursor);
    end;
  finally
    FreeMemory(pRealData);
  end;
end;

Таким образом одной единственной функцией мы защищаемся и от ВР, и от МВР, и даже от НВР.

Как это все обойти?

Ну что же, больше усложнять код защиты приложения я не буду, хватит и этой информации.

На некоторых нюансах я еще остановлюсь в конце статьи, а сейчас попробуем написать лоадер на базе отладчика, учитывая тот момент, что мы условились считать код проверки целостности непробиваемым и значит патчить тело проверки мы не будем.

Пишем каркас. Запуск и остановка у нас будет выглядеть так:

constructor TTestDebugger.Create(const Path: string);
begin
  FCore := TFWDebugerCore.Create;
  if not FCore.DebugNewProcess(Path, True) then
    RaiseLastOSError;
  FCore.OnCreateProcess := OnCreateProcess;
  FCore.OnLoadDll := OnLoadDll;
  FCore.OnDebugString := OnDebugString;
  FCore.OnBreakPoint := OnBreakPoint;
  FCore.OnHardwareBreakpoint := OnHardwareBreakpoint;
  FCore.OnUnknownBreakPoint := OnUnknownBreakPoint;
  FCore.OnUnknownException := OnUnknownException;
end;

destructor TTestDebugger.Destroy;
begin
  FCore.Free;
  inherited;
end;

Второстепенные обработчики я пропущу, их можно будет посмотреть в исходном коде примера, в принципе там ничего нового, все уже было описано в прошлой части статьи.

Первая наша задача каким то образом необходимо отключить детектирование отладчика приложением. Так как приложение проверяет целостность IsDebuggerPresent, а патчить проверку нельзя (по условию задачи) у нас остается только один вариант — изменить значение параметра Peb.BeingDebugged.

Сделаем это следующим кодом:

procedure TTestDebugger.HideDebugger(hProcess: THandle);
var
  pProcBasicInfo: PROCESS_BASIC_INFORMATION;
  pPeb: PEB;
  ReturnLength: DWORD;
begin
  if NtQueryInformationProcess(hProcess, 0,
    @pProcBasicInfo, SizeOf(PROCESS_BASIC_INFORMATION),
    @ReturnLength) <> STATUS_SUCCESS then
    RaiseLastOSError;
  if not ReadProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress,
    @pPeb, SizeOf(PEB), ReturnLength) then
    RaiseLastOSError;
  pPeb.BeingDebugged := False;
  if not WriteProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress,
    @pPeb, SizeOf(PEB), ReturnLength) then
    RaiseLastOSError;
end;

Здесь все просто, получаем адрес блока окружения процесса, изменяем параметр BeingDebugged и пишем все обратно. Таким образом функция IsDebuggerPresent перестает реагировать на отладчик. Декларацию используемых структур можно посмотреть в исходнике демопримера.

Первый этап выполнили, теперь второй — надо как-то заставить приложение не реагировать на неверно введенный код и показывать нам картинку в любом случае.

Делается это достаточно просто.

Вы наверное не раз в отладчике меняли значения переменных (это описывалось в первой части статьи). Вот здесь мы сделаем что-то похожее. Как помните отвечает за отображение картинки инструкция JE, если огрубить то представьте что у нас есть булевая переменная и условие if value then..else, если мы прервемся на таком условии то мы сможем контролировать условия выполнение кода, т.е. указать изменением переменной value что именно должно выполнится: блок then или else.

Оператор JE принимает решение о переходе как раз на основе вот такой вот булевой переменной, правда она представлена в виде флага ZF. Если флаг включен происходит прыжок по новому адресу. Стало быть наша задача заставить приложение прерваться на инструкции JE чтобы мы смогли изменить значение данного флага на необходимое нам.

Сделаем это при помощи установки НВР на адрес инструкции JE, т.к. это единственное, что не умеет контролировать наше защищенное приложение. Как узнать данный адрес я пропущу. В примере в составе архива идет исполняемый файл crackme.exe, я его специально вложил в архив из-за того что при каждой перекомпиляции, да и в зависимости от версии дельфи и прочего, этот адрес будет разным. В скомпилированном экзешнике этот адрес уже вычислен и равен значению 0х467840.

Осталось написать код:

procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
  var ReleaseBreakpoint: Boolean);
begin
  if ExceptionRecord.ExceptionAddress =
    Pointer(FCore.DebugProcessData.EntryPoint) then
  begin
    Writeln;
    Writeln(Format('!!! --> Process Entry Point found. Address: %p',
      [Pointer(FCore.DebugProcessData.EntryPoint)]));
    Writeln;

    HideDebugger(FCore.DebugProcessData.AttachedProcessHandle);

    FCore.SetHardwareBreakpoint(ThreadIndex, Pointer($467840), hsByte,
      hmExecute, 0, 'wait JE');
  end
  else
  begin
    Writeln;
    Writeln(Format('!!! --> BreakPoint at addr 0X%p - "%s"',
      [ExceptionRecord.ExceptionAddress,
      FCore.BreakpointItem(BreakPointIndex).Description]));
    Writeln;
  end;
end;

После чего нужно обработать прерывание на НВР и выставить правильное значение флага:

procedure TTestDebugger.OnHardwareBreakPoint(Sender: TObject;
  ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord;
  BreakPointIndex: THWBPIndex; var ReleaseBreakpoint: Boolean);
var
  ThreadData: TThreadData;
begin
  Writeln;
  ThreadData := FCore.GetThreadData(ThreadIndex);
  Writeln(Format('!!! --> Hardware BreakPoint at addr 0X%p - "%s"',
    [ExceptionRecord.ExceptionAddress,
    ThreadData.Breakpoint.Description[BreakPointIndex]]));
  FCore.SetFlag(ThreadIndex, EFLAGS_ZF, True);
  Writeln;
end;

Ну вот и все, можно запускать на выполнение, вводить любое левое значение и наслаждаться картинкой.

Результат будет примерно таким:

Так оно обычно и бывает, думаешь что написал бронебойную защиту, а потом раз и она обходится на коленке, ну не всегда конечно, но бывает :)

Детектируем Hardware BreakPoint:

Я намерено не остановился на детектировании НВР в рамках защищаемого приложения, по той причине, что будь там такая проверка, то пришлось бы писать достаточно сложный код обхода. А так вообще конечно желательно проверять и их наличие, закрывая таки образом отладчику возможность нормальной работы.

Детект наличия НВР достаточно прост, реализовать можно как через тот же GetThreadContext и проверкой регистра DR7 (если он не пуст — значит стоит НВР), либо, чтобы нас не перехватили на вызове API функции, мы может получить контекст нити при помощи генерации исключения.

Вот первый вариант

procedure TForm1.CheckHardwareBreakPoint;
var
  Context: TContext;
begin
  Context.ContextFlags := CONTEXT_DEBUG_REGISTERS;
  GetThreadContext(GetCurrentThread, Context);
  if Context.Dr7 <> 0 then
  begin
    MessageBox(Handle, 'Обнаружен HardwareBreaakPoint.',
      PChar(Application.Title), MB_ICONERROR);
    TerminateProcess(GetCurrentProcess, 0);
  end;
end;

И второй вариант, в котором поднимается отладочное исключение и снимается информация о контексте нити в обработчике _except_handler.

type
  // структура для восстановления
  TSeh = packed record
    Esp, Ebp, SafeEip: DWORD;
  end;

var
  seh: TSeh;

function _except_handler(ExceptionRecord: PExceptionRecord;
  EstablisherFrame: Pointer; Context: PContext;
  DispatcherContext: Pointer): DWORD; cdecl;
const
  ExceptionContinueExecution = 0;
begin
  if Context^.Dr7 <> 0 then
  begin
    MessageBox(0, 'Обнаружен HardwareBreaakPoint.',
      PChar(Application.Title), MB_ICONERROR);
    TerminateProcess(GetCurrentProcess, 0);
  end;
  // возвращаем регистры на место
  Context^.Eip := seh.SafeEip;
  Context^.Esp := seh.Esp;
  Context^.Ebp := seh.Ebp;
  // и говорим продолжить выполнение
  Result := ExceptionContinueExecution;
end;

procedure TForm1.CheckHardwareBreakPoint2;
asm
  // устанавливаем SEH фрейм
  push offset _except_handler
  xor   eax, eax
  push  fs:[eax]
  mov   fs:[eax], esp
  // заполняем данные для восстановления
  lea   eax, seh
  mov   [eax], esp
  add   eax, 4
  mov   [eax], ebp
  add   eax, 4
  lea   ecx, @done
  mov   [eax], ecx
  // генерируем исключение
  mov eax, [0]
  @done:
  // удаляем SEH фрейм
  xor   eax, eax
  pop   fs:[eax]
  add   esp, 4
end;

Кстати интересный момент. Обратите внимание на то, сколько информации приходит в обработчик исключения. Вся эта информация нам не доступна в обработчике except, именно поэтому я так часто называю try..finally..except куцей оберткой над SEH :)

Резюмируя

Теперь вы знаете несколько способов борьбы с отладчиком, правда теперь вы знаете и методы противодействия им, но на то она и статья, чтобы вы делали выводы.

Исходный код с примерами забрать можно по данной ссылке:  http://rouse.drkb.ru/blog/dbg_part3.zip

И на этом можно считать мою задачу выполненной.
Все что я хотел рассказать об отладчике, я рассказал. Изначально правда планировалась всего одна статья, но сами видите какое количество материала в итоге получилось :)

В дальнейшем я еще буду рассказывать об использовании отладчика, но это будет уже совершенно другая история.

© Александр (Rouse_) Багель
Москва, ноябрь 2012

Simple x64 Windows Debugger

Just a simple x64 Windows debugger written in C++

Documentation

Used WinAPI functions

These are the usefull functions provided by the Windows API which are used for making the core functionalities of this debugger.

Processes functions

  • CreateProcessA : Creates a new process. Setting DEBUG_PROCESS as a flag for the dwCreationFlags parameter allows the process to receive all related debug events using the WaitForDebugEvent function.

  • OpenProcess : Opens an existing process. In order to perform debugging, we have to set the dwDesiredAccess parameter to PROCESS_ALL_ACCESS.

Debugging functions

  • DebugActiveProcess : Attach the debugger to an active process.

  • WaitForDebugEvent : Waits for a debugging event to occur in a debugged process. The provided DEBUG_EVENT structure contains a dwDebugEventCode member that can informs us if the event comes from a breakpoint (EXCEPTION_DEBUG_EVENT). If the event is triggered by a breakpoint, then the u member would be an EXCEPTION_DEBUG_INFO structure which can provides us extra informations about the event via its EXCEPTION_RECORD structure member.

  • ContinueDebugEvent : Enables a debugger to continue a thread that previously reported a debugging event. The options to continue the thread that reported the debugging event have to be specified inside the dwContinueStatus parameter.

Threads functions

  • CreateToolhelp32Snapshot : Creates a snapshot of a given process. Setting TH32CS_SNAPTHREAD as a flag for dwFlags will provides all the threads in the snapshot. We will then have to compare each thread’s owner ID to the ID of the debugged process.

  • Thread32First : Retrieves the first thread of a process’ snapshot as a THREADENTRY32 structure.

  • Thread32Next : Loops through the rest of the threads of a process’ snapshot.

  • OpenThread : Opens a thread so we can get its context.

  • GetThreadContext : Retrieves the context of a given thread in which we can find all its registers’ states. Feeding the CONTEXT with CONTEXT_FULL and CONTEXT_DEBUG_REGISTERS grants us access to all of the thread’s registers we need.

  • SetThreadContext : Sets the context of a given thread which allows us to modify its registers’ states.

Memory functions

  • ReadProcessMemory : Reads the memory of a process at a given address.

  • WriteProcessMemory : Writes to the memory of a process at a given address.

Memory pages related functions

  • GetSystemInfo : Provides us a SYSTEM_INFO stucture that contains a dwPageSize member which gives us the correct page size of the system.

  • VirtualQueryEx : Retrieves informations about the memory page of a given address of a process. The MEMORY_BASIC_INFORMATION structure provides us the BaseAddress of the memory page as well as its access Protection (which are defined in the Memory Protection Constants)

  • VirtualProtectEx : Allows us to edit the access protection of a given memory page of a process. We can add a GUARD_PAGE access protection to a memory page in order to trigger memory breakpoint on access to this page.

Address resolving functions

  • GetModuleHandle : Provides a HMODULE handle of a specified loaded module.

  • GetProcAddress : Retrieves the address of an exported function or variable from a given module handle.

Seriously, it will take you longer to read this long introduction, than to code a working debugger in C#.

You may also want to check out:

  • Accessing stack traces with MDbgEngine and PADRE
  • Performance impact of running under MDbgEngine

Bugs in production

Remember all those reports coming back form clients saying “hey, your program crashed” or “hey, your is site showing these ugly yellow pages at random moments”? So do I. Unfortunately, there isn’t much that can be done to diagnose problems in running software, basically we’re at the mercy of our clients’ reports (which vary in quality, most of the time tending towards useless) or more or less verbose logging. Having those, we can reproduce issues during a debugging session to see what code causes them. If that fails,  we can go to the extremes of attaching a debugger in a production environment. Not feeling enough pressure today? Attach to a live site and try to setup breakpoints the exact way needed to only catch the error, and not stop a milion people from doing their daily work. At first try. Logging, on the other hand is very safe, it but will only tell you as much as you predicted that would be needed.

Now what if we could have something in the middle? More than logging, but less than an interactive debugging session?

Windows Error Reporting

In the ancient times, when not all software run as web applications, and especially in the even darker times when programs were being written mostly in native code, you could often see hard crashes that literally made entire applications disappear. Then Microsoft invented Windows Error Reporting, that filled that post-crash emptiness with a nice-looking window that supposedly gathered some data and sent it back to the author. This feature is present in all recent versions of Windows, and is available not only for Microsoft applications, but also for third party programs. Provided that their author gets a signing certificate (authenticode), signs all components of the software and registers on a deeply hidden Microsoft site. If all these conditions are met, Windows will automatically create a so called minidump of the process when it crashes (with important parts of memory, including stack traces of all threads) and  ask the user to allow sending it to Microsoft.

Since a minidump contains stack state of all threads at the moment of the crash, it allows you to open a post-mortem debugging session, and feel almost as if you were attached to the original process when the failure occured. WinDbg has supported this since… probably always, but Visual Studio 2010 does so too – although through a slightly hidden feature of just opening (File -> Open -> File…) minidumps, i.e. *.dmp files.

Just having access to precise call stacks used to be a killer feature at the time, but does it provide any value to the .NET developer, used to getting stack traces with every exception?

You guessed it: yes, it does.

With a minidump, you’ll be able to see parameter and variable values for all functions on the call stack.

Crashes in a .NET world

Before we go deeper, we need to clarify one thing. What exactly is a crash? What causes Windows Error Reporting to kick in? It’s unhandled exceptions. Native unhandled exceptions. Normally, the last exception handler registered in a running process is a system function that reports program failure, which in turn causes a default (defined in the registry) system just-in-time debugger to launch – which will be Windows Error Reporting a.k.a Dr. Watson, Visual Studio, or something else depending on your setup. Some programs also register their own unhandled exception handlers to report errors. Firefox does that, Winamp too. Perhaps you’ve already seen their crash windows in action. By registering their own handlers, these programs can create minidumps of their own liking (there are various types to choose from) and send them wherever they want.

What about .NET apps then? Do they crash at all? Of course they do, but unfortunately not in the sense described so far. You can have an unhandled managed exception in your code, but it will always be caught by the runtime – which in turn prints a stack trace (in a console application), shows a pop-up window (in a Windows Forms application) or displays the Yellow Screen of Death (in a web application). No minidump will ever be created automatically for a .NET application.

Creating minidumps manually

A minidump is created by calling MiniDumpWriteDump from dbghelp.dll. Mind though, that it needs to be a version from Debugging Tools for Windows, not the Windows system folder. There are various tools that call it, like SysInternals’ ProcDump. MiniDumpWriteDump and ProcDump have been described in an article in the recent edition of MSDN Magazine: Writing a Plug-in for Sysinternals ProcDump v4.0.

One of those tools is also ClrDump, which is available both as a very simple command-line tool and a convenient library. Read this blog post, and you’ll know everything about how to create minidumps from .NET applications: Creating and analyzing minidumps in .NET production applications.

Choosing the right place and time

The last article I linked to has one thing wrong though. It triggers minidump generation from AppDomain.UnhandledException and Application.ThreadException. And it would be equally wrong to call it from an error handler in Global.asax or to register one with an HttpModule, like Elmah does. The reason is that those handlers are called from a generic exception handler at the base of your application. At this point all of the context (call stack, variables) available where the exception was thrown is lost. To really benefit from minidumps, you need to trigger them as deep in your call hierarchy as you can. Also, often you’ll face exceptions that are caught by your own code, but you’ll still want minidumps generated, to better explain why those exceptions occured.

And let’s not forget generating minidumps like this is similar to debugging with printf – you need to modify code and redeploy. We need something smarter. And external to the inspected application.

Writing a managed debugger in C#

The CLR exposes debugging capabilities through COM, so it isn’t impossible for a mere mortal to implement a debugger. But thanks to the CLR Managed Debugger Sample anyone can do it, really. The sample mainly consists of a console tool, mdbg.exe, that you can use as a poor-man’s replacement for windbg.exe or cdb.exe, and – more importantly – a shared debuging engine library, written in C#. The latter is now also available on NuGet (courtesy of yours truly) as Microsoft.Samples.Debugging.MdbgEngine.

Let’s consider this trivial console test application:

namespace TestApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            A(1, new { property = 2 });
        }

        private static void A(int i, object state)
        {
            B(i + 4, state);
        }

        private static void B(int i, object state)
        {
            try
            {
                C(i);
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception.Message + ": " + i);
            }
        }

        private static void C(int i)
        {
            throw new InvalidOperationException("Expected failure");
        }
    }
}

Here is an only slighly longer console application that runs a debugger and creates minidumps on each exception (as you can see, also those caught in code):

using Microsoft.Samples.Debugging.CorDebug;
using Microsoft.Samples.Debugging.MdbgEngine;

namespace DebuggerApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            var stop = new ManualResetEvent(false);
            var engine = new MDbgEngine();
            var process = engine.CreateProcess("TestApplication.exe", "", DebugModeFlag.Default, null);
            process.Go();

            process.PostDebugEvent +=
                (sender, e) =>
                    {
                        if (e.CallbackType == ManagedCallbackType.OnBreakpoint)
                            process.Go();

                        if (e.CallbackType == ManagedCallbackType.OnException2)
                        {
                            ClrDump.CreateDump(process.CorProcess.Id, @"C:\temp.dmp", (int)MINIDUMP_TYPE.MiniDumpWithFullMemory, 0, IntPtr.Zero);
                        }

                        if (e.CallbackType == ManagedCallbackType.OnProcessExit)
                            stop.Set();
                    };

            stop.WaitOne();
        }
    }
}

Go ahead and try it. If you open test.dmp in Visual Studio, you’ll be able to see not only the call stack at the moment when the exception occurred, not only values of variables that are easy to log (like i), but you’ll be also able to inspect complex and dynamic structures (like state).

I hope you’re having a big “holy shit” moment. I definitely did.

Where can we go from here?

The code above “logs” all exceptions. To implement this idea in the real world, we’d need quite a bit more – load symbols (you know where to get those from – SymbolSource), set breakpoints at specific locations, catch only specified exceptions.

Catch me on Twitter if you like the idea. This has potential for a great and very useful open source project.

We also have some very cool ideas how to integrate this with SymbolSource. Imagine setting breakpoints on the website and choosing exceptions to monitor, then seeing automatically collected reports from all your deployments. Call stacks with variable values, failure statistics, minidumps for download and offline analysis.

Interested?

UI-Beta.jpg

Preface

This article is the sequel to previous article Writing basic Windows Debugger, and it is mandatory for the reader to read and understand the

first part first! Without grasping what is mentioned in first part of this series, you may not understand what is

presented here, nor you would be able to appreciate this stuff!

One thing I should mention, which I did not state in previous part, is that our debugger is only able to debug

Native Code. Thus, an attempt to debug a .NET/managed application would fail. May be, in next

article I would cover debugging the Managed Code also.

I am here to present more of intriguing aspects, in debugging parlance, which you might not know before. This

would include showing the source code and the call stack, setting the breakpoints, stepping into code, attaching

(our) debugger to a process, making it a default debugger and so on. Since this article presents somewhat

advanced concepts in debugging, I removed «basic» word from the title!

The debugging experience would be as per with Visual C++, that includes debugging terms (Step-Into), shortcuts

(F5) etc.

Table of contents:

  • Start Debugging from main
    • Obtain start-address of the process
    • Place breakpoint instruction at start-address
    • Handle breakpoint, revert instruction
    • Halt debugging, wait for user action
    • Continue debugging as per user’s command
  • CDebuggerCore — The debugging-interface class
  • Debugging Actions
    • Enumerating source files, and line numbers
    • Placing User-Breakpoints
    • Stepping through the code (Step-in, step-out etc)
    • Conditional Breakpoints
  • Debugging a Running Process
    • Detach, Terminate or wait?
    • Debugging a Crashing Process
    • Manually attaching a Debugger

Let’s Debug!

So, what you do when you want to debug your program? Well, mostly we hit F5 to begin debugging our

applications, and the Visual Studio Debugger would halt at the places where we have placed breakpoints (or

conditional breakpoints!). A «Debug Assertion Failed» message box, followed by Retry button also

opens the source code, and halts there. The DebugBreak call or {int 3} instruction would

also do the same. There are other approaches for debugging, you know!

Rarely or occasionally, we also start debugging from the beginning by hitting F11 (Step-Into),

and the VS starts from main/wmain or WinMain/wWinMain (or

_t prefixed variants of them). Well, that is the logical start address of

your process where debugging begins. I call it logical since it is not the actual start address —

the address also known as Entry Point of the module. For a console application, it is

mainCRTStartup which calls main function, and VS Debugger starts at main.

The entry-point is also applicable for DLLs too. Please have a look at /ENTRY switch for more information.

This turns out that we need to stop program execution at the entry-point location, and

let user (programmer) continue debugging. Yes, I said «stop» the execution at entry-point location — the

process is already started (being debugged), and unless we halt it somewhere the process would complete its

startup. The following call-stack appeared as soon as I hit F11, for an MFC application — which clarifies what I

mentioned.

CallStack1.JPG

What we need to do, to stop program execution at Entry-Point?

In a nutshell:

  1. Obtain the start address of process
  2. Modify the instruction at that address — replace it with breakpoint instruction, for example.
  3. Handle the breakpoint event, revert the instruction with original instruction
  4. Stop execution, show call stack, display registers and source code, if available.
  5. Continue debugging (as per user request)

And this 5-step task is not easy, boy!

1. Obtain the Entry Point of the Process

Start Address, Entry Point and the logical* entry point (your main/WinMain) —

welcome to the complex world. Before I present textual content about these terms, let me give you visual idea about

it. But first thing you should understand: the first instruction at the given address is the point where execution

begins, and debuggers play with that address only.

* [This term is coined by me, and is relevant to this article only!]

Here is how WinMain looks in Disassembly View in Visual Studio, with annotations for the

gibberish you see:

LogicalEP_Final.JPG

You can launch the same view for you code by right clicking in code editor, and selecting Go To

Disassembly. The Code Bytes are not shown by default (Green content above), you enable it by Show

Code Bytes context menu.

Relax! You need not to understand the machine language instructions, nor the ASM language! This is just for

illustration. For the example above, 00978F10 is the start-address, 8B FF is the first

instruction. We just need to make the given instruction (mov blah blah), a breakpoint

instruction. We know the API called DebugBreak — but that cannot be used here. {int 3}

is the assembly code for the same, and we need the x86 instruction for the same. The breakpoint x86

instruction is 0xCC(204).

Thus, we just replace 8B with CC and we are done! When the program continues further

(within our debugger-loop), it would raise an exception event (EXCEPTION_DEBUG_EVENT), having the

exception code EXCEPTION_BREAKPOINT (0x80000003). We know this is our sin, we handle is

appropriately. If you don’t understand this paragraph, I request you (last time) to read the first part of

this article.

The x86 instructions are not of fixed width — but who cares? We don’t need to see if instruction is of

1, 2, or N bytes. We always replace the first byte. The first byte (of instruction) may be

anything, and not just 8B! But, we must ensure that as soon as desired breakpoint is

hit, we reverse our sin — i.e. replace the replaced instruction with original code-byte.

For few geeks out there, who know it, and for everyone else who do not know it. The breakpointing is

not the only way to stop program at start-address. There is better alternative for these One-shot/stop-once

breakpoints, which is to be discussed. Secondly, CC instruction is not the only breakpoint instruction

— but it is sufficient enough for us.

There is more of complexity involved with Start-Address, but to keep grabbing your interest, let me

jump straightaway to C++ code that retrieves the start address. The CREATE_PROCESS_DEBUG_INFO has

member named lpStartAddress, that represents the start address. We can read this information while

processing the very first debugging event:

// This is inside Debugger-loop controlled by WaitForDebugEvent, ContinueDebugEvent
switch(debug_event.dwDebugEventCode)
{
   case CREATE_PROCESS_DEBUG_EVENT:
   {
        LPVOID pStartAddress = (LPVOID)debug_event.u.CreateProcessInfo.lpStartAddress;
        // Do something with pStartAddress to set BREAKPOINT.
   ...
...

The type of CREATE_PROCESS_DEBUG_INFO::lpStartAddress is LPTHREAD_START_ROUTINE, and I

assume you know what exactly it is (function pointer). But, as I mentioned before, there are complexities around

start address. In short, this address (lpStartAddress) is relative to where image is actually loaded

in memory. To make all this sound convincing and significant, let me show you the output of dumpbin

utility with /HEADERS switch:

dumpbin /headers DebugMe.exe
...
OPTIONAL HEADER VALUES
             10B magic # (PE32)
            8.00 linker version
            A000 size of code
            F000 size of initialized data
               0 size of uninitialized data
           11767 entry point (00411767) @ILT+1890(_wWinMainCRTStartup)
           1000 base of code

This address (00411767) is what I received with lpStartAddress member, while debugging

from our debugger. But when I tried to debug the same executable from Visual Studio, the address at

wWinMainCRTStartup was somewhat different (@ILT stuff has no relevance for this).

Thus, allow me to postpone discussing intricacies around start address. Let’s just have GetStartAddress()

user defined function, whose code would be disclosed later. This would return the exact address where we

would put the breakpoint instruction!

2. Modify instruction at start-address with a breakpoint instruction

Once we get the start-address, modifying the instruction at that location with breakpoint (CC) is

quite trivial. We need to do:

  • Read one byte from that memory location, store it.
  • Write 0xCC instruction at that location.
  • Flush the instruction cache.
  • Continue debugging.

Two important questions should hit you now:

  1. How we read, write and flush the instructions?
  2. When we do the same?

Let me uncover the second query first. We do read, write and flush instructions while processing

CREATE_PROCESS_DEBUG_EVENT debug event (or optionally at EXCEPTION_BREAKPOINT event).

When the process is being loaded, we grab the actual start address (I mean the CRT-Main’s address!), we

read the very first instruction at that location of one byte, store it, and place the breakpoint

(0xCC) at that instruction. Then we let our debugger to ContinueDebugEvent.

To depict how to do the same, here I present relevant code!

DWORD dwStartAddress = GetStartAddress(m_cProcessInfo.hProcess, m_cProcessInfo.hThread);    
BYTE cInstruction;
DWORD dwReadBytes;

// Read the first instruction    
ReadProcessMemory(m_cProcessInfo.hProcess, (void*)dwStartAddress, &cInstruction, 1, &dwReadBytes);

// Save it!
m_OriginalInstruction = cInstruction;
 
// Replace it with Breakpoint
cInstruction = 0xCC;
WriteProcessMemory(m_cProcessInfo.hProcess, (void*)dwStartAddress,&cInstruction, 1, &dwReadBytes);
FlushInstructionCache(m_cProcessInfo.hProcess,(void*)dwStartAddress,1);

About the code:

  • m_cProcessInfo is member of class (not yet presented), which is nothing but

    PROCESS_INFORMATION and is filled-in by CreateProcess function.

  • The user defined function GetStartAddress returns desired start-address of the process.

    For a GUI based unicode application, it would return the address of wWinMainCRTStartup.

  • Next we call ReadProcessMemory to grab what is at the given start-address. We store it in a member

    variable of class.

  • The we replace the instruction with 0xCC, a breakpoint instruction, using

    WriteProcessMemory.

  • Finally we call FlushInstructionCache, so that CPU would read the new instruction, and not

    any of cached (old) instruction. The CPU may or may not cache the CPU instructions, but you should always

    flush it.

Remember that ReadProcessMemory requires permission

PROCESS_VM_READ. Likewise, WriteProcessMemory requires permission

PROCESS_VM_READ|PROCESS_VM_OPERATION — all these permissions are already granted to the debugger, as

soon as Debugging-Flag was given to CreateProcess. Therefore, we need not to do anything, and

reading/writing would always succeed (at valid locations, of course!).

3. Handling breakpoint instruction, and reverting the original instruction.

As you know that breakpoint (EXCEPTION_BREAKPOINT) instruction is a kind of exception that

arrives under EXCEPTION_DEBUG_EVENT debugging event. We handle the exception events through

EXCEPTION_DEBUG_INFO structure. The following code would help you recollect and understand it:

// Inside debugger-loop
switch(debug_event.dwDebugEventCode)
{
   case EXCEPTION_DEBUG_EVENT:
   {

        EXCEPTION_DEBUG_INFO & Exception = debug_event.u.Exception; // Out of union
        // Exception.ExceptionCode would be the actual exception code.
...

The OS would always send one breakpoint instruction to the debugger, which is just an indication that process is

being loaded. That is why you can also perform «placing breakpoint instruction at start-address»

on this very first breakpoint exception. This would ensure that after this very first breakpoint, all next

breakpoints are yours!

Irrespective of where you put your breakpoint instruction for the start-address, you still need to

ignore the first breakpoint event. Though, debuggers like WinDbg, would also show you this

breakpoint also. Visual C++ debugger, for instance, on other hand, does not give any indication about this very

first breakpoint, and starts executing at Logical beginning of program

(main/WinMain and not CRT-Main).

Therefore, the breakpoint handling code would look like:

// 'Exception' is the same variable declared above
switch(Exception.ExceptionRecord.ExceptionCode)
 {

 case EXCEPTION_BREAKPOINT:
  if(m_bBreakpointOnceHit) // Would be set to false, before debugging starts
  {
     // Handle the actual breakpoint event
  }
  else
  {
     // This is first breakpoint event sent by kernel, just ignore it.
     // Optionally display to the user that first BP was ignored.
     m_bBreakpointOnceHit = true; 
  }
  break;
...

You can also use the else-part to place the breakpoint instead of placing it on process-start event.

Either way, the main breakpoint event handling code goes in if-part. We need to handle the breakpoint that

we had placed at start-address.

Now it goes tricky and intriguing — you need to concentrate, read carefully, sit relaxed (adjust your butt on

the chair, if you need to!). If you have not taken a break reading this article, take it now!

In simple terms, the breakpoint event has occurred at the location/address where we placed it. We now just halt

debugging, show call stack (and other useful elements), revert the original instruction and wait for user input to

continue debugging.

At assembly or machine-code level, when any exception occurs and the debugger notified, the faulting instruction

has already executed — though, for breakpoint, it would be just of one byte. The instruction pointer has already

moved ahead by few bytes, depending on instruction size. And the system has given us this exception-event to

handle.

Thus, in addition to writing back the original instruction at the «breakpoint-location», we need to

play with CPU registers also. The registers, that are specific to the process (more specifically, thread), can be

retrieved and modified using two Windows functions: GetThreadContext and

SetThreadContext. Both of the functions take a CONTEXT structure. Strictly speaking, the

members of this structure are dependent on processor-architecture. Since, this article is about x86 architecture,

we would follow the structure definition for the same, and that can be found in winnt.h header.

This is how we retrieve the thread-context of a given thread:

CONTEXT lcContext;
lcContext.ContextFlags = CONTEXT_ALL;
GetThreadContext(m_cProcessInfo.hThread, &lcContext);

Okay! Now you retrieved the thread-context, now what?

The EIP register tells you the Instruction Pointer, which is

the next instruction to be executed. This is represented by Eip member of CONTEXT

structure. As I mentioned already, the EIP has already moved ahead, and we need to move it back. Luckily for us, we

just need to move it by exactly one byte, since BP instruction was of one byte. The following code does the

same:

lcContext.Eip --; // Move back one byte
SetThreadContext(m_cProcessInfo.hThread, &lcContext);

The EIP is the location from where the CPU would pick the next instruction and start executing. Yes, we need to

have THREAD_GET_CONTEXT and THREAD_SET_CONTEXT access permissions to successfully execute

these function, and we do already have.

For a while, allow me to take a Context-Switch to another frame: Reverting the original instruction! To

write back the original instruction into the running process, where BP instruction was placed, we just need to use

WriteProcessMemory followed by FlushInstructionCache. This is how we can do it:

DWORD dwWriteSize;
WriteProcessMemory(m_cProcessInfo.hProcess, StartAddress, &m_cOriginalInstruction, 1,&dwWriteSize);
FlushInstructionCache(m_cProcessInfo.hProcess,StartAddress, 1);

The original-instruction is reverted this way. We then call ContinueDebugEvent. The combined code

would look like:

//// In the breakpoint event switch-case:
// 1. GetThreadContext, reduce EIP by one, SetThreadContext
// 2. Revert the original instruction
// 3. Continue debugging

Well, where is the call-stack? Registers? Source code? And when did the program halt? It all continued without

user interaction/intervention!

4. Stop execution, show call stack, display registers and source code, if available.

To display the call stack we need to load the debugging symbols, which would exist in relevant

.PDB files. The set of functions from DbgHelp.DLL would help us to load symbols,

enumerate the source files, walk through the call stack and so on. All these would be discussed soon.

To render the CPU registers, we just need to display relevant registers from the CONTEXT structure.

To display the 10 registers as shown by Visual Studio debugger (Debug->Windows-

>Registers, or Alt+5), we can use following code to generate text:

CString strRegisters;


strRegisters.Format(
  L"EAX = %08X\nEBX = %08X\nECX = %08X\n"
  L"EDX = %08X\nESI = %08X\nEDI = %08X\n"
  L"EIP = %08X\nESP = %08X\nEBP = %08X\n"
  L"EFL = %08X",
  lcContext.Eax, lcContext.Ebx, lcContext.Ecx,
  lcContext.Edx, lcContext.Esi, lcContext.Edi,
  lcContext.Eip, lcContext.Esp, lcContext.Ebp,
  lcContext.EFlags
  );

And display the generated text to relevant window.

To halt program execution, until user gives appropriate commands (Continue, StepIn, Stop Debugging etc), we do

not call ContinueDebugEvent. Since the debugger-loop would be in another thread than the GUI thread,

we just ask GUI thread to display relevant information, and let the debugger-thread wait for some

«event» to occur. On user command, we trigger that «event», which would end the wait in

debugger-thread.

Confused? The event in quotes is nothing but a windows event, which is created by

CreateEvent. To halt execution we just call WaitForSingleObject (in debugger-thread). To

let debugger-thread to continue from that wait-location, we just call SetEvent (from GUI-thread). Of

couse, depending on your taste, you can use other synchronization primitive, or some class (CEvent?)

to do the same. This paragraph only gave the rough idea how we can implement Halt-and-Continue.

With this additional desription, the logical-code to achieve all this would be:

  1. GetThreadContext, reduce EIP by one, call SetThreadContext.
  2. Revert the original instruction, using WriteProcessMemory, FlushInstructionCache.
  3. Display the relevant members of CONTEXT — Registers View.
  4. With the help of symbol-information functions, locate the source file and line (if possible) and

    display the Source Code!

  5. With stack-walking functions and symbol-info functions, enumerate the Call Stack of debuggee and

    redner on UI.

  6. Since the GUI thread is intimated by now, we wait for use action through «Event«.
  7. On wait-complete, we perform the action initiated by user (Continue, Step, Stop…)
  8. Call ContinueDebugEvent.

Astonished? Great! I hope you are enjoying the Debugging !

One important point worth mentioning here — the thread which is being debugged may not be primary

thread of debuggee, but the thread that caused this breakpoint event to occur. Till now, we are still processing

the very first BP event that we had created to stop execution at process-startup. But the eight-step process I

listed above would be applicable for all debugging events (from any debuggee’ thread), that can

cause program to halt.

There is slightly more of complexity involved with instruction modification and EIP change. Whilst I would show

the solution to it later, let me just mention the problem. The breakpoints may also be placed by

user; and for the same, we would just replace the instructions with CC instructions on those addresses

(yes, saving the original instructions). When those breakpoints would arrive, we would just revert the instructions

and perform the 8-step processing that I am currently describing. Fine enough? Well, that would prevent the user-

defined breakpoint events to arrive only once! If we don’t revert original instruction — well, that’s a total

mess!

Anyway, let me continue further! :-)

Ah! Source-Code! I know you are dying to see how this can be retrieved!

Any EXE or DLL image you build may have Debugging Information attached with it in form of a

.PDB file. Few points on this:

  • The debugging-information is only available when /DEBUG switch is given in linker settings. In VS,

    you can see this here: Linker->Debugging->Generate Debug Info.

  • The /DEBUG symbol does not mean that the EXE/DLL would be Debug build. The preprocessor macro

    _DEBUG/DEBUG controls the debug build at compile time. Other linker settings and

    appropriate library imports define the meaning of debug build at link time.

  • This means, a Release build can have debugging-information, and a Debug build may not have

    debugging-information (no /DEBUG linker switch, but _DEBUG macro defined).

  • A file with .PDB extension stores the debugging-information, which is generally

    program-name.PDB, but it can be renamed in linker options. This file contains all source code

    information stored — functions, classes, datatypes and so on.

  • The linker puts small foot print about this .PDB file in image-header of the EXE/DLL generated. Since this

    information is put into headers section, it does not impact the performance of generated binary at all.

    Only the file size increases by few bytes/KBs.

To gain this debugging information, we need to use Sym* functions hosted inside

DbgHelp.DLL. This DLL is most important component for source-code level debugging. It

also hosts functions to walk the call stack, and to retrieve information about image (EXE/DLL). Click here to see all

functions exported by this DLL. The required header is Dbghelp.h, and required library is

DbgHelp.lib

To gain debugging information, we need to initialize Symbol Handler for the given process.

Since, the target process is the debuggee, we initialize with the process-handle of debuggee.To initialize symbol

handler, we need to call SymInitialize function:

BOOL bSuccess = SymInitialize(m_cProcessInfo.hProcess, NULL, false);

The first parameter is the handle of running process for which symbol-information is required. Second parameter

is semi-colon separated paths, which specifies where to look for symbol files (for us, the .PDB

files). The third parameter says if the symbol-handler should load the symbols for all modules automatically or

not. Since we need to show if debugging-information is found or not, we pass false.

The following text would make some sense:

'Debugger.exe': Loaded 'C:\Windows\SysWOW64\msvcrt.dll', Cannot find or open the PDB file
'Debugger.exe': Loaded 'C:\Windows\SysWOW64\mfc100ud.dll', Symbols loaded.

Visual Studio (2010) could not locate symbols for msvcrt.dll, thus the error. The DLL,

mfc10ud.dll has its debugging information available, therefore VS was able to load it. This

essentially means that, for MFC DLL, VS would show symbol information, viz source code, function/class names in

call-stack and so on. To explicitly load the symbol file for respective DLL/EXE we call

SymLoadModule64 / SymLoadModuleEx function.

Where should we call these functions for the debuggee? It took me lot of time to locate, since I was trying to

initialize and load symbols before the debugging-loop (i.e. before any debug-event, but after

CreateProcess). It does not work that way. We need to call it on

CREATE_PROCESS_DEBUG_EVENT debugging event. Since, we are saying «no» to automatic

symbol loading for the dependent modules, we need to call SymLoadModule64/Ex

for the EXE file just loaded. For upcoming LOAD_DLL_DEBUG_EVENTs, we also need to call this function

for the respective DLL being loaded. Depending on outcome of symbol-loading-for-image, we let the end user know if

debugging-information is available or not.

Sample code, that loads the module when a DLL-Load event is encountered. Function named

GetFileNameFromHandle is described in previous part (code available).

case LOAD_DLL_DEBUG_EVENT:
   {
    CStringA sDLLName;
    sDLLName = GetFileNameFromHandle(debug_event.u.LoadDll.hFile);
 
    DWORD64 dwBase = SymLoadModule64 (m_cProcessInfo.hProcess, NULL, sDLLName,
     0, (DWORD64)debug_event.u.LoadDll.lpBaseOfDll, 0);

    strEventMessage.Format(L"Loaded DLL '%s' at address %x.", 
                            sDLLName, debug_event.u.LoadDll.lpBaseOfDll);  
...

Of course, similar code goes for process-load event. A caveat: initializing the symbol header and loading the

respective module successfully does not mean source-code information is available! We need to call

SymGetModuleInfo64 to determine if symbols in PDB are available. Here is how we do it:

// Code continues from above
IMAGEHLP_MODULE64 module_info;
module_info.SizeOfStruct = sizeof(module_info);
BOOL bSuccess = SymGetModuleInfo64(m_cProcessInfo.hProcess,dwBase, &module_info);

// Check and notify
if (bSuccess && module_info.SymType == SymPdb)
{
     strEventMessage += ", Symbols Loaded";
}

else
{
     strEventMessage +=", No debugging symbols found.";
}

I am very thankful to Jochen Kalmbach for his excellent article on Stack-Walking, which has helped

me in locating source code information and walking the call-stack.

When the symbol type is SymPdb, we have source-code information available. Remember, a successful

PDB load does not mean source-code is available, the PDB only contains information about source-

code, the source code (.H, .CPP and other files) should be available at given path to view! The PDB contains

symbols names, file names, line number information etc. Stack-walking (without source-code view) is perfectly

possible, with fully qualified function names.

Finally, when breakpoint event arrives we can retrieve the call stack and display it. To do this we need to use

StackWalk64 funtion. The following code is stripped version, which shows the usage of this function.

Please refer to the Stack-Walking article I mentioned above for complete details.

void RetrieveCallstack(HANDLE hThread)
{
   STACKFRAME64 stack={0};
   // Initialize 'stack' with some required stuff.
  
   StackWalk64(IMAGE_FILE_MACHINE_I386, m_cProcessInfo.hProcess, hThread, &stack,
               &context, _ProcessMemoryReader, SymFunctionTableAccess64,
               SymGetModuleBase64, 0);
...

STACKFRAME64 is the data-structure that holds addresses from where call-

stack information is retrieved. It essentially tells that current instruction location in given thread. For x86, as

described by Jochen, we need to initialize following members of this structure before calling

StackWalk64 function:

 CONTEXT context;
 context.ContextFlags = CONTEXT_FULL;
 GetThreadContext(hThread, &context);

 // Must be like this
 stack.AddrPC.Offset = context.Eip; // EIP - Instruction Pointer
 stack.AddrPC.Mode = AddrModeFlat;
 stack.AddrFrame.Offset = context.Ebp; // EBP
 stack.AddrFrame.Mode = AddrModeFlat;
 stack.AddrStack.Offset = context.Esp; // ESP - Stack Pointer
 stack.AddrStack.Mode = AddrModeFlat;

In call to StackWalk64, the first constant specifies the machine type, which is x86. Second

argument is the process (the debuggee). Third is the thread whose call stack we need to retrieve (not necessarily

primary thread). Fourth parameter is in/out parameter, which is most important argument for this function-call.

Fifth is a context structure, having required addresses initialized. Function _ProcessMemoryReader is

user defined function, which does nothing except calling ReadProcessMemory. Other two

Sym* functions are from DbgHelp.DLL. The last parameter is also function-pointer, and we don’t need

that.

Walking the call stack definitely requires a loop till stack-walk finishes. Whilst there are issues

like invalid call-stack, never ending call stack and things like that. I chose to keep it simple: run it till

Return-Address becomes zero, or till StackWalk64 fails. This is how we enumerate the call

stack (symbol-name retrieval not yet shown):

BOOL bSuccess;
do
{
    bSuccess = StackWalk64(IMAGE_FILE_MACHINE_I386, ... ,0);
    if(!bTempBool)        

       break;

    // Symbol retrieval code goes here.
    // The contents of 'stack' would help determining symbols.
    // Which would put information in a vector.

}while ( stack.AddrReturn.Offset != 0 );

The symbol has few properties attached to it:

  • The module (DLL or EXE name)
  • The name of symbol — undecorated or decorated.
  • The type of symbol — function, class, parameter, local variable etc.
  • The virtual address of symbol.

Stack-walking also includes:

  • Source-file
  • Line number
  • First instruction (machine instruction) on that line.

Though, we don’t need machine-instruction, unless dis-assembling the code, we may need the displacement from

first instruction. At source-code level, this may happen if multiple instructions exist (like multiple function

calls), on the same line. For now, I am omitting it altogether.

Hence, we need the module name, the function call and the line number as a complete Stack-entry.

To retrieve module name, respective to the address on stack, we use

SymGetModuleInfo64. If you recollect, there was similar function to load the

module info — SymLoadModuleXX, which is required to be called previously for

SymGetModuleInfo64 to work. Following code (which is just after StackWalk64 call inside

the loop), demonstrates how to retrieve module information for given address:

IMAGEHLP_MODULE64 module={0};
module.SizeOfStruct = sizeof(module);
SymGetModuleInfo64(m_cProcessInfo.hProcess, (DWORD64)stack.AddrPC.Offset, &module);

The variable module.ModuleName would contain the name of module, without extension

or path. Member module.LoadedImageName would refer to full module

name with full path and extension. The member module.LineNumbers would tell if Line-

number information is available or not (1=available). There are other useful members too.

Next, we retrieve the function name for this stack-entry. We retrieve this information using

SymGetSymFromAddr64 or SymFromAddr. Former returns

information via PIMAGEHLP_SYMBOL64 having 6 members in structure, while the latter returns via

SYMBOL_INFO which returns rich symbol information. Both take four arguments, out of which first 3 are

same, and last argument is pointer to desired structure. Here is an example of first function:

IMAGEHLP_SYMBOL64 *pSymbol;
DWORD dwDisplacement;
pSymbol = (IMAGEHLP_SYMBOL64*)new BYTE[sizeof(IMAGEHLP_SYMBOL64)+MAX_SYM_NAME];

memset(pSymbol, 0, sizeof(IMAGEHLP_SYMBOL64) + MAX_SYM_NAME);
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64); // Required
pSymbol->MaxNameLength = MAX_SYM_NAME;             // Required
 
SymGetSymFromAddr64(m_cProcessInfo.hProcess, stack.AddrPC.Offset, 
                   &dwDisplacement, pSymbol); // Retruns true on success

About this wicked code:

  • Symbol name (Name field of structure) may be of variable length. Thus, we need to put more buffer

    for this variable. MAX_SYM_NAME is predefined macro having value 2000.

  • Since DbgHelp.DLL and this structure may have different versions, SizeOfStruct must be initialized

    to represent structure size we use.

  • Assignment of MaxNameLength is pretty obvious.

For us, the important member of this structure are: Name, which is a null-terminated string.

Address, which is virtual address of the symbol (including image’ base-address).

Using SymFromAddr and initializing SYMBOL_INFO is very similar, and I prefer using this

new function. Though, as of now, for us, there is no extra information. I would point out usefulness of this

extended structure, as the need arrives.

Finally, to complete the stack-entry, we need to find out the source-file name and appropriate line

number in it. Reminding you, the PDB contains this information, and source-info retrieval — only if correct PDB is

loaded successfully. Also, the PDB contains only information about source-code, not source-code!

To retrieve line-number information, we use SymGetLineFromAddr64, passing it a

IMAGEHLP_LINE64 structure. This function also takes four arguments, and first three are similar to

functions described above. It requires only SizeOfStruct to be properly initialized with with

structure size. It goes like this:

IMAGEHLP_LINE64 line;
line.SizeOfStruct = sizeof(line);

bSuccess = SymGetLineFromAddr64(m_cProcessInfo.hProcess, 
                               (DWORD)stack.AddrPC.Offset, 
                               &dwDisplacement, &line);

if(bSuccess)
{
   // Use line.FileName, and line.LineNumber
}

Symbol functions, or any other function from DbgHelp.DLL does not support loading and displaying the source. We

need to do it by ourself. If the line number information, or the original source code is not available, we cannot

show the source-code-view.

As of this writing [19-Dec-2010], I have not decided what to be displayed when source code is not

available to display. We can display set of instructions, but x86 instructions aren’t of fixed size, the

«instruction» per se doesn’t fit. But bunch of machine code instructions (like «55 04 FF 76 78

AE…») having fixed instructions in one line can be displayed. Or we can dis-assemble the instructions

and show them instead. Though, I do have source code of disassembling the x86 machine-code, it doesn’t support

complete instruction set.

By now I have shown you the important steps to stop debugging on start-address. This includes obtaining the

start address, putting breakpoint at that address, handling that breakpoint, reverting original instruction,

obtaining registers, call-stack, source-code and registers, and basic idea on how to display all this

information to UI. I have also elaborated that we need to use Windows event for halting and resuming the suspended

debuggee, as per user request.

5. Halting the Debuggee, and continuing as per user’s command

As explicated somewhere in this article, to suspend the debuggee (i.e. keep it suspended when some stoppable

debug event occurs), we just dont call ContinueDebugEvent. As per the design of debugging-system,

it also ensures that any other running threads in debuggee, remain suspended.

Here is the abstract view of how halt and continue is supposed to execute (UT=User-interface

thread, DT=Debugger thread):

  • [UT] User initiates debugging a process through given UI. This operation would not freeze the

    UI.

  • [DT] Initializes a Windows event object using CreateEvent. It is non-signaled.
  • [DT] The debugger starts the debuggee using CreateProcess. It enters the debugger-

    loop.

  • [DT] It receive the breakpoint event, which asks UI to display information and it halts.
  • [DT] It uses WaitForSingleObject on that event to keep DT suspended.
  • [UT] User initiates some debugging actions, like Continue, Stop-debugging or

    StepIn.

  • [UT] It calls appropriate function to Resume-Debugging with the debugging-action

    flag. It need to call SetEvent to wake-up that DT.

  • [DT] Wakes up, inspects the user-action and continues the debugging loop appropriately,

    or terminates debugging if Stop action was given.

Everything will get more clear when I would depict the debugging-interface (a class), that I designed. If your

memory and/or inquisitiveness is working well while reading the article, you should know that I did not cover two

things:

  • Actual code to retreive the start address (GetStartAddress function!). Reminding you,

    CREATE_PROCESS_DEBUG_INFO::lpStartAddress is the start-address, but won’t be always correct.

  • How to handle user-defined breakpoints, which must execute more than once? The start-address breakpoint I have

    discussed so far is just hit-once breakpoint, therefore reverting original instruction does the require

    job.

May be you need to take a break, and/or read the un-cached stuff again! ;)

Anyway, since I have covered DbgHelp.DLL and Sym* functions I can present the code that retrieves start-address

of a process. The function name is SymFromName, which takes the symbol-name and

returs information in SYMBOL_INFO. The similar old function is SymGetSymFromName64 which

returns information in PIMAGEHLP_SYMBOL64 structure. Following is code that retrieves address of

wWinMainCRTStartup, using SymFromName function:

DWORD GetStartAddress( HANDLE hProcess, HANDLE hThread )
{
   SYMBOL_INFO *pSymbol;
   pSymbol = (SYMBOL_INFO *)new BYTE[sizeof(SYMBOL_INFO )+MAX_SYM_NAME];
   pSymbol->SizeOfStruct= sizeof(SYMBOL_INFO );
   pSymbol->MaxNameLen = MAX_SYM_NAME;
   SymFromName(hProcess,"wWinMainCRTStartup",pSymbol);

   // Store address, before deleting pointer  
   DWORD dwAddress = pSymbol->Address;

   delete [](BYTE*)pSymbol; // Valid syntax!

   return dwAddress;
}

Ofcourse, it retrieves address of only wWinMainCRTStartup, which may or may not be start routine.

Well, there is more to go in this article — like determining if EXE specified is valid, is 32-bit module, is

unmanaged EXE, if it is Unicode/ANSI build and things like that. Keep reading!

User-defined breakpoints? I will describe about the mentioned issue when I would elaborate on creating user-

defined breakpoints.

CDebuggerCore — The debugging-interface class

I wrote an abstract class, having few pure-virtual functions that forms the foundation for debugging. Unlike

previous article, which was tightly integrated to UI and MFC, I made this class to be independent. I have used

native Windows handles, STL and CString class in this class. Remember, CString class may

be directly used in non-MFC applications, just by including <atlstr.h>, no linking with MFC is

required at all. I have placed this include in header-file of this class. If you are still discontent about

CString usage, please use your favorite class for string handling and change code appropriately.

[Subject to Change] Here is the basic skeleton of CDebuggerCore class:

class CDebuggerCore

{
    HANDLE m_hDebuggerThread;    // Handle to debugger thread
    HANDLE m_heResumeDebugging;  // Set by ResumeDebugging

    PROCESS_INFORMATION m_cProcessInfo;
    // Other member not shown for now.

public:
    // Asynchronous call to start debugging
    // Spawns a thread, that has the debugging-loop, which
    // calls following virtual functions to notify debugging events.
    int StartDebugging(const CString& strExeFullPath);

    // How the user responded to continue debugging, 
    // it may also include stop-debugging.
    // To be called from UI-thread
    int ResumeDebugging(EResumeMode);

    // Don't want to listen anything! Terminate!
    void StopDebugging();
 
protected:
    // Abstract methods 
    virtual void OnDebugOutput(const TDebugOutput&) = 0;
    virtual void OnDllLoad(const TDllLoadEvent&) = 0;


    virtual void OnUpdateRegisters(const TRegisters&) = 0;
    virtual void OnUpdateCallStack(const TCallStack&) = 0;

    virtual void OnHaltDebugging(EHaltReason) = 0;
};

For few readers, reading about this user-defined class may not be appeasing. But this is how it is — to explain

about the debugger’ code I must explain the code I wrote!

Since this class is abstract, it must be inherited and all virtual methods (On*) must be

implemented. Needless to mention, these virtual function are called by core class, depending on different debug-

events. Not a single virtual function demands return value or argument modification — you may keep the

implementations empty.

Assume you have derived a class named CDebugger, and implemented all virtuals. You start debugging

by using following code:

// In header, or at some persist-able location
CDebugger theDebugger;

// The point in code where you ask it to start debugging:
theDebugger.StartDebugging("Path to executable");

Which would just initialize required variables to set debugging-state, create the event handle I described

above,and spawn the debugger-thread. It will then return — that means StartDebugging is asynchronous call.

The actual debugging-loop is in DebuggerThread method (not shown above). It creates the given

executable using CreateProcess, and then enters debuging-loop controlled by

WaitForDebugEvent and ContinueDebugEvent, having switch-cases for the debug events. On

different debug events it calls appropriate On* method passing appropriate arguments. For instance, if

OUTPUT_DEBUG_STRING_EVENT arrives, it calls OnDebugOutput with a string parameter. For

other debugging events, it calls appropriate virtual functions. The derived class is responsible for displaying it

the UI.

Few debugging events that halts the debugging, like breakpoint event, the debugger-loop would first

call appropriate On* function(s) and then it would call HaltDebugging with proper

reason-code. This function is private to CDebuggerCore, and has declration like this:

// Enum
enum EHaltReason
{
     // Reason codes, like Breakpoint
};

// In CDebuggerCore
private:
   void HaltDebugging(EHaltReason);

The implementation of this method is something like below, which halts the debugger-thread:

void CDebuggerCore::HaltDebugging(EHaltReason eHaltReason)
{
   // Halt the debugging
   OnHaltDebugging(eHaltReason);

   // And wait for it, until ResumeDebugging is called, which would set the event
   WaitForSingleObject(m_heResumeDebugging,INFINITE);
}

Since the debugger-loop knows the exact reason of halting, it passes it to HaltDebugging

which delegates it to OnHaltDebugging. The override, OnHaltDebugging, is supposed to

intimate the end-user about this debugging event. The UI thread doesnt freeze, but it awaits further user action.

The DT remains suspended.

With proper UI, like menus, shortcuts keys etc, the UI thread then calls ResumeDebugging function

with the resume-mode (i.e. how user responded to this halt event — Continue, StepIn, Stop-debugging?). The

ResumeDebugging method which takes an EResumeMode flag, sets this flag to a member

variable in class (of EResumeType itself) and then calls SetEvent to signal the event. It

resumes the debugger-thread (which is in HaltDebugging).

Now, after HaltDebugging returns (as a result to user-action), the debugger-loop tests

what action did user perform. For the same, it checks the member variable m_eResumeMode (of type

EResumeType), which was set by ResumeDebugging and continues debugging

appropriately; or it terminates, if flag is to Stop-debugging. Just for illustration, enum EResumeMode is

something like:

// What action was initiated by user to resume 
enum EResumeMode
{
 Continue, // F5
 Stop,     // Shift+F5
 StepOver, // F10
 // More .. 
};

There is more about CDebuggerCore class, but I have covered the basic skeleton and data/control flow

that goes around this class. Please see the attached source files and comments in it.

Debugging Actions

In this section, I would elaborate different debugging actions like placing a user breakpoint at some source

code location (i.e. at a line in source file), disabling or deleting the breakpoint, continuing from breakpoint,

stepping though source code line by line and similar debugging actions. As you very well know all these

actions would require proper source files to be displayed to the end user. Therefore is is important to learn how

to enumerate relevant source files, load the files, enumerate lines of source files.

Enumerating source files of Debuggee

Definitely, as you know well, the debuggee — the running process, i.e. the executable files does not contain the

source-file information. It is contained in referenced .PDB file (though other formats also possible, other than

PDB, but I would still talk about .PDB). To enumerate all source files related to the executable we call

SymEnumSourceFiles function. We would call the function on Process-load, and

optionally on Dll-load events. We should call this function only when SymGetModuleInfo64 succeeds and

the loaded module info says the symbol-type is PDB. To recollect, this is how we test it, followed by

SymEnumSourceFiles call:

// Applicable for CREATE_PROCESS_DEBUG_EVENT and LOAD_DLL_DEBUG_EVENT
// Initialization of 'dwBase' and 'module_info' is not shown here, as it is 
// already described above.

BOOL bSuccess = SymGetModuleInfo64(m_cProcessInfo.hProcess, dwBase, &module_info);
if ( bSucess && module_info.SymType == SymPdb)
{
  // Symbols successfully loaded for this module, 
  // and source file information is available.

  // Call SymEnumSourceFiles to retrieve source file information:

  SymEnumSourceFiles(m_cProcessInfo.hProcess, dwBase,
                     NULL, EnumSourceFilesProc, this); // arguments 3,4,5
}

The first two arguments are quite understandable: the process and the base-address of module for which source-file

information is required. The third argument specified the filter. The 4th argument is a call back function, that we

must write. The last argument is nothing but a void-pointer parameter that would be passed to our callback function

for each file retrieval.

The signature of call back has to be:

BOOL __stdcall EnumSourceFilesProc(
 PSOURCEFILE pSourceFile,
 PVOID UserContext
 );

It would be an insult to your knowledge, if I tell you that this function name is just placeholder — you can

name it whatever you like! The second parameter is the same void-pointer, also refered as user-context.

This function will be called unless enumeration finishes, or the function itself returns FALSE. Thus,

we would return TRUE so that all source files can be enumerated.

The structure SOURCEFILE, from dbhhelp.h, is declared like:

struct SOURCEFILE
{
   DWORD64 ModBase;  // Base address of the module
   CHAR*   FileName; // Full source filename, including path
};

Since filename comes from a shared address of DbgHelp.DLL, we must copy the buffer, and save it.

A simple test run has shown me that it enumerates all source files, including .res, .inl,

and manifest files and all predefined header files. To limit source file enumeration, we can use the

filter argument (3rd argument of SymEnumSourceFiles). For instance, to enumerate only

.CPP files, we can call it as:

SymEnumSourceFiles(m_cProcessInfo.hProcess,dwBase, "*.CPP", EnumSourceFilesProc, this);

Specifying multiple wild card isn’t supported. We must call this function multiple times to enumerated different

extensions. (I don’t have much time to explore this).

Anyway, using this function, in the callback function, we can store all required source files into a

vector or CString. Further to this, we can let user place breakpoints in any of these

source files. As of now, the our debugging-interface is designed such that, the user would be able to

locate these source files only after first break point is hit (i.e. main). In near future, I

would make changes to facilitate placing user breakpoints before actually starting the debugging.

Being a programmer, you are well aware that not everyline of source file is an executable line. Comments, Curly-

braces, blank lines, pre-processor directives are few elements that do not constitute a valid executable line. You

might have also observed that Visual Studio shifts ahead the misplaces breakpoints to next valid statement.

Therefore, it is necessary to enumerate lines that are valid executable lines.

To enumerate valid source-lines we can use SymEnumLines, which is

very similar to SymSnumSourceFile function. It requires process handle, base-address of module, a

filter to object file (.OBJ file), a filter to source file; and a call-back function and user-context (void-

pointer). First two arguments are same. We don’t need third argument, that means we ask this function to enumerate

through all object-files. Fourth argument we pass is explicit filename with full path — the sourcefile. Since, we

have collected all source files with previous function, we have all source filenames. Here how it goes:

SymEnumLines(m_cProcessInfo.hProcess, dwBase, NULL, SourceFile, EnumLinesProc,this);

The 4th argument, is nothing but fullpath of a source file name. Assume this code is in a loop, where all

collected file names are being enumerated for valid lines. The sixth argument is user-context, which will be passed

to EnumLinesProc user-defined function. The declaration of EnumLinesProc must be as:

BOOL __stdcall EnumLinesProc(
                PSRCCODEINFO LineInfo,
                PVOID UserContext );

Other detail remain same for this call back. The SRCCODEINFO structure is declared

like:

// From DbgHelp.H.  Not all members shown here
struct SRCCODEINFO
{
    DWORD64 ModBase; 
    DWORD64 ModBase;                // base address of module this applies to
    WCHAR   Obj[MAX_PATH + 1];      // the object file within the module
    WCHAR   FileName[MAX_PATH + 1]; // full filename
    DWORD   LineNumber;             // line number in file
    DWORD64 Address;                // first instruction of line
};

SymEnumLines calls our callback function for each valid/executable line till the current source file

enumeration is finished. Thus, Obj and FileName would be repeated. These two members will

be full-path to .OBJ file and source file respectively. The member LineNumber is important to us,

which will appear incrementing, and will only refer to valid line numbers. The last member is important for us —

the address of first instruction, which we will use to place breakpoints!

Placing Breakpoints

Once we have collected the source-code information using two of the functions enlightened above, we can now

facilitate end-user to place breakpoints directly from the source file. Till now, I have not shown any interface

(programming or UI) to show the source-file, but that’s not important, it is just a matter of opening a text file

and displaying in some multiline edit/richedit control.

Visual Studio is smart enough to find more of invalid lines than we can do — VS would disable/delete

breakpoints placed at some nonsensical location (like on a pre-processor directive). We would apply simple

mechanism. Since, we have collected all source files and valid source lines, we first check if the line on which

breakpoint is placed is actually a valid line. If it is a valid line, then no issues — we happily make that address

a breakpoint (i.e. replace that instruction with 0xCC). If the line on which breakpoint

is placed is invalid, we find the next valid line (this would be trivial since we have the source line info).

Of course, there are cases when a module would be dynamically loaded by the process (via LoadLibrary)

. For those cases, we need to allow any source file to be opened in our debugger, and also allow breakpoints

to be placed anywhere in any file. This is complex, yet important aspect; I would consider writing about it!

I know you want to see some action with «placing breakpoint», especially with user-breakpoints that

must be repeatable! What is a repeatable breakpoint, few of you might be confused. Let me refresh

your memory on breakpoints!

The breakpoint we placed at start-address is hit-once type of breakpoint — that code will not get

executed again, thus we just revert the (machine) code-byte. We don’t care about preserving the breakpoint for

future hit. But with other breakpoints, you must ensure that breakpoint remain preserved. Therefore, after

breakpoint is hit, is it first reverted with original code, EIP reduced and actual (original) code gets executed.

Let me try to explicate it:

  1. End user puts a breakpoint at line 188 in source file MyApp.CPP. Let’s say the

    address is 0x0012345.

  2. In the debugger, you grab what is at address 0x0012345, save it on some breakpoints-array. You

    also save this address in some array.

  3. You set the breakpoint instruction (0xCC) at this location. For this, we use same set of

    functions: Read/WriteProcessMemory and

    FlushInstructionCache.

  4. The break-point eventually arrives (EXCEPTION_BREAKPOINT). You first check if it is user-

    breakpoint. For this just search in breakpoints-addresses-array. If it is user-breakpoint, locate the original

    code-byte, write it, flush it. Do EIP--, and SetThreadContext.

  5. Continue debug event.

Break-points array? One more thing you don’t understand? Just like you can place multiple breakpoints, the user

of your debugger can also! Ignore this array stuff, if you don’t get it, you will understand it sooner or

later.

In abstract terms, if you understand the 5-step described above, you should know that as soon user-breakpoint is

hit, it won’t hit again. You have reverted the instruction! To handle all this, you need to use sub-exception:

EXCEPTION_SINGLE_STEP (or STATUS_SINGLE_STEP, both are absolutely same).

Till now, we have been stopping program execution when break-point is hit (via EXCEPTION_BREAKPOINT),

and showing call stack, registers etc. But the correct way is to actually handle breakpoint in

EXCEPTION_SINGLE_STEP. You will understand why when you read following:

In EXCEPTION_BREAKPOINT event,

  • Get thread context, save it.
  • Reduce EIP by one.
  • Set processor trap flag for single stepping.
  • Set thread context.
  • call WriteProcessMemory, and FlushInstructionCache, as usual to revert

    instruction.

In STATUS_BREAKPOINT event,

  • Render the registers for the context saved in previous step.
  • Enumerate call-stack from saved context.
  • Halt debugging.

Now what is «trap flag»?

Trap flag, when set, asks the CPU to raise EXCEPTION_SINGLE_STEP for the instruction it is going to

execute. We set the trap-flag in EFlags member variable of CONTEXT. It is represented

by 8th bit of EFlags. Just use 0x100 or 256 to set it:

// In EXCEPTION_BREAKPOINT

lcContext.ContextFlags = CONTEXT_ALL;

GetThreadContext(m_cProcessInfo.hThread, &lcContext);
lcContext.Eip--;
lcContext.EFlags |= 0x100; // Set trap flag, which raises "single-step" exception
SetThreadContext(m_cProcessInfo.hThread,&lcContext);

Remember, we reduce EIP by one, which essentially means we set the flag to current instruction — where the

breakpoint was placed. After above given code, we write and flush the original instruction. Therefore, as a result,

the next immediate exception arrives is EXCEPTION_SINGLE_STEP at the same instruction address. In

single-step processing, we dont reduce EIP, but we re-write breakpoint instruction (if it was actually a user-

defined breakpoint!). This essentially makes user-defined breakpoints repeatable!

As a final important note, EXCEPTION_SINGLE_STEP is not same as single-

stepping through the source code in your favorite debugger (F10/F11 in VS)! Also, the Trap-Flag is

automatically reset as soon as single-step exception arrives, which prevents this same debug-event to

arrive again. Therefore, you need not to reset this flag. Yes, of course, you can enable it everytime for

assembly/machine-code level debugging!

Stepping through the Code

A good debugger should facilitate following code-stepping actions:

  • Step-in to each and every line of at source-code level, this includes stepping-inside

    the function calls. (F11 in Visual C++)

  • Step-over, which is same as Step-in, but goes not steps-in to function calls. (Same

    as F10)

  • Step-out, which means coming out of currently executing function to the function which has

    called this function. It essentially means popping-out current function from the Call-Stack. (Similar to Shift

    +F11)

  • Run-to-Cursor. Not exactly a code-stepping action. It means placing an invisible, hit-once

    breakpoint to a given location. You can do the same with Ctrl+F10 in Visual C++ debugger, while debugging. (Look at

    context menu while debugging).

  • Set-next-Statement, which allows the debugger-user to set any line to be executed next. Terms

    and conditions apply. In VS, it can be achieved through context menu (while debugging). The shortcut is Ctrl+Shift

    +F10.

Surprisingly, all these debugging actions run around the core concepts that we have discussed so for:

  • Replacing an instruction with 0xCC.
  • Handling break-point exception. Adjusting EIP. Setting trap-flag.
  • Handling single-step exception. Halt debugging, wait for user action.

Other than the last debugging action (Set-next-statement), all other actions would require invisible-

breakpoint. The instruction is same: 0xCC, handling is also same. The only difference is that

debuggers don’t give any visual indication that it is actually a breakpoint. This is nothing but absence of Red-dot

on left of source-code in Visual Studio, or presence in ‘Breakpoints’ window (Alt+F9). Disabled, Invalid or any

other kind of user-specified breakpoints are not invisible-breakpoints — they are regular

breakpoints that user specified (or attempted to).

The start-address breakpoint, I had discussed before, is nothing but an invisible breakpoint. There is no visual

indication from debugger that it is actually a breakpoint. In VS, when you start program with F10 or F11, the

debugger would halt at main-function (like WinMain), but it is not actually a user-breakpoint

(no red-dot!).

In the debugger code, how we would differentiate between user-breakpoint and invisible-breakpoint? Quite simple.

The breakpoints-array, I discussed in last subsection, is for user-specified breakpoints. When a breakpoint is hit,

we find the exception-address in breakpoints-array. If found it is user-BP, otherwise it is invisible-BP. Though,

unless we provide visual or audio indication for «breakpoint», it doesn’t make a difference.

In VS, the visual clue is the Red-icon on left and audio-clue can be enabled form Sounds

settings on Control Panel. In Sounds applet, goto Microsoft Visual Studio, specify a sound for

Breakpoint Hit entry. Restart Visual Studio. When a breakpoint is hit, the same sound would be

played. With this, I found out that user-breakpoint has priority over invisible breakpoint. Just place a breakpoint

to some line, and before that line press F10. The Step-over executes, but breakpoint-hit sound is also emitted! The

same goes for Run-to-cursor. I bet, you did not know this sound stuff!

Now, it is time to see some action. I am elaborating these debugging actions depending on complexity level

involved, simpler first. Please read them all.

Implementing Run-To-Cursor

For sure, it is nothing but an invisible, one-shot breakpoint instruction. For this, we just place a breakpoint

at given location in source code, and do not put this address in break-points array. When it is

hit, just halt the execution, show «Debugging» / «Debugging-halted» in some UI, wait for user-

action, and continue debugging as per user command.

It should be obvious to you that this action also requires 0xCC instruction to be placed at given address, be

saved, and then later reverted. Moreover, since the user is placing this invisible-breakpoint, we need to ensure

that source-file is valid, is loaded against Symbol-information, the line on which this command was given is valid

executable line. If line isn’t valid executable line, we find next valid line and place this invisible-

breakpoint there. Consequently, it means the Run-to-cursor is adjusted appropriately.

Remember, user-breakpoints takes priority over invisible-breakpoints. Therefore, if a user-BP is hit before Run-

to-cursor (or any other invisible-BP), that IBP would now be invalid (To verify, in VS, just place a BP before the

location of Run-to-cursor). Thus, we need to disable IBPs when any BP is hit. Also, there cannot be more than one

IBP pending to be hit at any time, so it is quite trivial to disable IBP! It essentially means that we need exactly

one variable for storing all stepping actions (including start-address IBP!). Even when multiple threads are

running, only IBP can be pending. Draw some sketches on paper to understand this paragraph, and unless you

understand it, dont proceed further!

Implementing Step-Over

For simplicity, let me cover Step-Over only to current function; that means excluding the case

when current function ends and program-execution goes to caller. I would cover this implicit stepping-out from a

function when I would explicate on Step-Out implementation.

Let’s have sample code for understanding:

int Max(int n1,int n2)
{
   if(n1 > n2)
      return n1;
   else
      return n2;
}

int main()
{ //*  This is line 12
   int a, b;
   a = 10; //* 
   b = 20; //*

   Max(20,30); //*  //Bogus call

   printf("Max is %d", Max(a,b)); //*

   return 0; //*
} //*

Here, for function main, only the lines marked with //* are valid

executable lines. These are the same lines returned by SymEnumLines function. Therefore, we would the

collect line information, store the line numbers and associated addresses. Let’s say the line number information

(only for main) are enumerated as below:

Line   Address
12     0x411400
14     0x41141e
15     0x411425
17     0x41142c
19     0x411438
21     0x411460
22     0x411462

You definitely need not to understand how these addresses are formed, it all depends on source code, compiler

and linker settings (optimizations, incremental linking, FPO etc) — Ignore it. All we need is to utilize these

lines and addresses for Step-over implementation. First assume that program execution is now sitting at line 12

(via start-address IBP mechanism). That means EIP is 0x411400 !

Debugger is waiting for user input (debugger-loop is on halted on WaitForSingleObject). Now,

debugger-user asks to Step-over.

  • The UI-threads calls CDebuggerCore::ResumeDebugging with EResumeFlag set to

    StepOver.

  • This tells the debugger thread (having the debugger-loop) to put IBP on next line.
  • The debugger-thread locates next executable line and address (0x41141e), it

    places an IBP on that location.

  • It calls then ContinueDebugEvent, which tells the OS to continue running debuggee.
  • The BP is now hit, it passes through EXCEPTION_BREAKPOINT and reaches at EXCEPTION_SINGLE_STEP. Both these

    steps are same, including instruction reversal, EIP reduction etc.

  • It again calls HaltDebugging, which in turn, awaits user input.

And this process can go as long as user wishes, or program ends (or, of-course, till another exception occurs!).

One important aspect I did not cover, is to disable IBP on any normal BP occurrence, before this IBP is hit.

This means, if user has placed breakpoint somewhere in function Max, and the program execution is now

passing (in main) over this function. It would hit that UBP in Max, and then user might

hit F5 (Continue). Our buggy debugger, would halt at next line in main! I will de-bug this

issue soon.

Implementing Step-In

This isn’t easy! This is the most difficult part of writing a debugger!

Whilst it would be similar to Step-Over implementation, we need to dis-assemble the code, and

to understand what next statement means (at assembly/machine-code level). Herein, I am attaching a

disclaimer, that I am not assembly expert! Therefore, the following text should be treated as

idea/concept/hint — refer to Intel’s x86 instruction documentation for complete information about opcodes.

As I had commented before, the x86 instructions are not of fixed length. Each instruction can

be of 1 byte, or more than one bytes (I have seen max of 7-byte x86

instructions). The BP instruction, 0xCC, is a one byte instruction. The processor understands

all instructions opcodes, and their lengths, and it executes it as atomic instruction. For example, consider

following simple code:

Max(10,20);

Which is disassembled as:

6A 1E            push        1Eh  
6A 14            push        14h  
E8 C2 FD FF FF   call        00400E00 
83 C4 08         add         esp,8  

I am not putting assembly jargons here, just about the above’ code. First two x86 instructions are

pushing arguments to stack. The third line is making a call to function Max.

Therefore we need to hook the call instruction!

Hooking? Not exactly. We need to find the address where call instruction exists in instruction-set

of debuggee. We can use ReadProcessMemory for the same. And, on that address we place an IBP — and

rest of the things you already know.

When do we do it? When user says step-in (F11), we start searching for this instruction from current EIP, till

we find call instruction.

The toughest part is locating the call instruction. No, this is not as easy as searching for op-

code E8. Following are the reasons:

  1. E8 is not the only instruction for function-calling, there are few others

    too.

  2. The x86 instructions are not of fixed size. As you can see from assembly code above, first two instructions are

    of 2 bytes each (push), the call instruction is of 5 bytes, and last instruction is of 3

    bytes!

  3. We need to dis-assemble code instructions properly. Dis-assembling is quite complicated task, and with more new

    CPU instruction (SSE2 etc), it becomes more troublesome. For the same, it is mandatory that we know the instruction

    size, at least, so that that unknown instruction can be ignored safely.

  4. Each call instruction, including E8, is different, and they need appropriate

    interpretation.

I do need to do further research and development, on this subject; and only after that I can present a practical

and workable solution. Till then, for completeness, I can outline how Step-In can be implemented.

  • [When F11 is hit] From the current instruction location, ignoring the current complete-instruction,

    read all instruction bytes till next valid executable line (collected via SymEnumLines).

  • Do partial or full dis-assebmly, as applicable/possible.
  • Analyze the assembly, locate one of the call instructions.
    • If found, place IBP at that location. The IBP processing goes as usual.
    • If not found, just process as if it was Step-Over debugging-action.

The function, if call was found, would now be called, and call-stack would now be changed. The called

function may be in different source file. In either case, this function would eventually exit and program-control

would go back to caller. And this needs processing of Step-out, which is described below. Along with this, there

may be more than one call instructions in same executable source-code line. Following is an

example:

printf( "%d, %d", max(10,20), min(min(20,30),40) );

Implementing Step-Out

Stepping-out from a function is the nearly inverse of Step-In debugging action. If you are debugging in

Dis-assembly mode, Step-In (F11) will let you debug at instruction level; and from that point if you do Step-Out,

it would be stepping out from the high-level «function», as if you were performing Step-In in source-code

mode. That’s why I said nearly!

To implement Step-Out debugging action, we need help from our friend function StakWalk64.

If you remember, stack-walking is achieved with loop; and in that loop we call StackWalk64 multiple

times till Return-address becomes zero:

STACKFRAME64 stack;
// Initialize stack-frame
do 
{ 
  StackWalk64(..., &stack, ..);
  // Utilize stack-entry
} while(stack.AddrReturn.Offset != 0); 

The variable AddrReturn, of type ADDRESS64, holds the return address where the program

control would go. Let’s not go into how it is implemented at assembly level, but the return-address is

somehow stored, and program-control gets transferred when a function implicitly or explicitly

returns to its caller. The return-address is the next statement after the call statement.

Therefore, for the following pseudo-assembly code:

00401070 call 0x1234000
00401075 mov  abc, xyz

which is calling the function 0x1234000, the return-address from function 0x1234000

would be 0040175 (and not 0040170).

Therefore, you just need to call StackWalk64 only once, note the return-address, and place a IBP

over there and your Step-Out is implemented!

If multiple function calls are involved in same source-code line, they would have multiple call

statements at assembly level, and they all would have respective return-addresses. Thus, it is quite possible for

debugger-user to hit F11, Shift+F10 (step-out) or F10 for same source-code line.

Though, we would be displaying only the source-code, in the debugger, we know where exactly the instruction

(EIP) is — the IBP, single-step exceptions, Debugging-APIs are assisting us for the same. Therefore, we need not to

bother if user hits F11, then returns from that function (implicitly or explicitly), and then hits F11 again for

source-line having more than one function calls (like in printf example given above).

Let’s consider another case when user hits F10, after returning from first sub-function call (into which he/she

entered via Step-In). Here, the execution, at source-code level, is in midst — and at assembly level, few

calls are made, and few more instructions (including calls, if any) are pending for

execution. The user has hit F10! What should happen? Well, if you look back to the implementation of Step-Over, you

would find that Step-Over just looks for next executable line (at source-code level), and places an IBP on

next-instruction! Therefore, we need not to worry about this complicated case, either!

What if a user-BP is hit before function returns from F11/F10 debugging-action? That means, in following

function:

printf("Next prime: %d", NextPrime(1000));

Were NextPrime is supposed to return next prime number from given number, which, for example calls

IsPrime function. User has placed a BP somewhere in IsPrime. Now, when program control

comes over this printf line, user hits F11, which would Step-Into NextPrime; and then he/she

immediately hit Shift+F11. Where the program control should go: To next line of printf call, or

To that breakpoint in IsPrime?

Reminding you! User breakpoints have priority over any Invisible breakpoints. The program control would go to BP

in IsPrime, and IBP placed at return-address of NextPrime (which was placed by debugger

for Step-Out debugging-action), would be disabled by debugger!

What if user says Run-To-Cursor, in this complicated situation? Pardon me! Do you need same text to read again!

Analyze yourself! :)

Implementing Set-Next-Statement

Well, a single threaded implementation just needs to adjust EIP to the address of given line. That means, when

user says Set-next-statement (SNS), we look up the lines information we had collected. Verify if

given/selected line is valid executable line (if not just find the next valid line as we do for BP). Grab the

address of that line, and set EIP to that address using SetThreadContext (after

GetThreadContext).

Of course, just like VS Debugger does, we need to check if given line falls within same function, or it is from

different function. If it is from different function, we can ask the confirmation from user. My testings have

revealed that VS debugger, even in case of cross-function SNS action, just adjusts EIP appropriately and nothing

else!

Conditional Breakpoints

Debuggers also provide placing conditional breakpoints, where condition can be combination of following:

  • When a particular expression is true, or false. In this kind of CB, you can use programming

    elements like variables, operators etc. Of course, this requires debuggers to have knowledge about variables

    (local, class, global), and debugger must be able to parse the given expression (like !(nMargin+10

    > m_nMaxMargin)).

  • It can filter out few hits, like only hitting even/odd hit on that breakpoint. Or hit when

    hit-count is more than some value, Or is multiple/not-multiple of some value. All in all, hit-count is used as

    condition for breakpoint.

  • With other constraints like matching/not-matching thread or process-id, or machine type and

    things like that. When same function is being called from multiple threads, the debugger-user can notice particular

    thread-id, and prevent that thread to trigger this breakpoint. Unfortunately, due to elusive user-interface, this

    is not well used, IMO.

If two or more categories are applied to same breakpoint, it depends on the debugger to trigger them with

AND or OR applied. That means if I place expression as well as filter

it, the debugger can see if both of these conditions are met, or any of these. Visual Studio debugger

applies AND.

Now the million-dollar question is: How do we implement conditional-breakpoint feature in our debugger?

The first type of CB implementation (expression) is of distant-future, since it involves knowing the

variables, data types, constants etc; that are in debuggee. The DbgHelp functions are not rich enough to support

querying such rich information. For this we have to use DIA (Debug Interface Access),

which I am not willing to discuss (or learn by myself) at this moment, unless I finish this article. Nevertheless,

the idea described below can be used for Expression CB implementation.

In our debugger, UBPs are stored in some breakpoints-array. Now, when a UBP is applied with a condition, Or a

brand new conditional BP is placed by debugger-user, we can put that UBP into another BP-array that would be

holding conditions. The data-structure or algorithm isn’t important here, only the basic idea. We may use

maps, hash-maps to store breakpoints. Store a breakpoint information in some structure, whose condition

member would be null/empty when CB not set, or things like that.

When a breakpoint is hit, we instantly check if a condition is attached to it, and if it is true. If it is true,

we let user notify that BP is hit, otherwise we silently ContinueDebugEvent without user knowing

about. Irrespective of BP is hit or not, the BP magic must go around our debugger (0xCC, break-point

exception, EPP--, SetThreadContext, Single-step exception…). That means, if BP is

enabled it would always arrive in the debugger!

When a hit-count CB is applied, just count the hits, and break the debuggee on UI when specified hit-count

condition is hit. Again, programming elements we use inside debugger is not important here.

Similarly, constraints types of CB can be implemented quite easily. We just need to facilitate what are

the set of constraints. I may list few:

  1. Thread should match; or should not match.
  2. There should be more than; or less than N number of total threads running.
  3. Only after particular thread is loaded, not-loaded or unloaded from debuggee.
  4. Some constraints with the number of processes already running.
  5. Should hit only if called; or not called, from a specific function.
  6. Should hit when memory usage exceeds some value.
  7. Check the delay between two breakpoint hits, and notify only when some timeout condition is met. For example,

    hit if there is 5 second or more delay hitting the same BP.

Implementing all these isn’t difficult! We need to use few functions (API), employ some data-structures, code

some smart stuff and the work is done! Therefore, I am not elaborating them here.

Debugging a Running Process

While I still have not covered about multiple threads running in debuggee, I am straightaway jumping on

debugging

a running process. There is no strong reason of delaying the discussion of Multithreaded Debugging, I just

found this subject worth elaborating before MT debugging. The dialogbox Attach to

Process might be known to you, which you can launch either from Tools or

Debug menu (or by Ctrl+Alt+P):

AttachTo.JPG

Attach to Process

To debug a running process you need to call DebugActiveProcess function, with a process ID, and then it

enters the debugger-loop controlled by WaitForDebugEvent. Therefore, you can say that you are just

replacing CreateProcess with DebugActiveProcess and rest of debugger-loop part remain

same. There are, however, some differences:

  • All threads in the process (debuggee) will be suspended.
  • The process is already running, along with its threads. Thus, by design,

    lpStartAddress member of CREATE_PROCESS_DEBUG_INFO and

    CREATE_THREAD_DEBUG_INFO (not discussed yet) will be null. One event notifying process-start will be

    delivered, and CREATE_THREAD_DEBUG event will be sent for each running thread.

  • For each DLL that is currently loaded in debuggee, either implicitly or explicitly, the debugger would receive

    LOAD_DLL_DEBUG_EVENT.

  • The debugger will now receive first-breakpoint instruction, which is always sent to a debugger (as you know!).

    This would occur from the first thread of debuggee. The debugger would (should) continue from this point.

  • All threads will now be resumed by system (of course, excluding the threads not suspended by debuggee

    itself).

There are some constraints:

  • The debugger must have appropriate permission to debug a particular process. If debugger is running as

    normal/non-elevated process, it cannot debug a process having administrative/elevated token. This can be achieved

    by running the debugger as elevated process. The Run As Administrator verb while launching the

    debugger can be used. Or to run the debugger always as elevated process (for Vista or higher, on VS2008 and

    higher), UAC linker flag can be set to requireAdministrator.

  • Our 32-bit debugger cannot debug a 64-bit process, irrespective of its permission level.
  • Since our debugger is native-code debugger, it cannot debug a managed processes. It can however, debug a mixed

    mode process. More on this later.

Detach, Terminate or wait ?

For long we have discussed starting a process for debugging, and recently discussed how to debug a running

process. What about detaching this relationship? Is it always required to wait until

debuggee exits, or has to quit due to unhandled exception? Until now, the debugger design was to wait until process

exits by itself. We now cover two important aspects: Terminating the debuggee and

Detaching the debuggee.

Terminating the Debuggee

By design, as mentioned in DebugSetProcessKillOnExit function, the debuggee would terminate if the thread that created the

debuggee (i.e. the one having debugger-loop) exits. But I found this to be false. The debuggee does not end, if

debugger just says «bye-bye» asking the debuggee to do whatever it wants to do, by just

returning from the debugger-thread. It enters a Limbo state, doing nothing.

It also denies any other debugger to attach on it for debugging (yes, the OS enforces it, not debuggee). Using

Process Explorer, I found a valid call-stack, which clearly sounds-out that debuggee is waiting for

ContinueDebugEvent!

May be this is not as per current Operating Systems and MSDN is not modified to reflect this.

Therefore, to terminate the debuggee, we need to use TerminateProcess function. Doing this, when requested by

debugger-user, would ask for EXIT_PROCESS_DEBUG_EVENT to occur next. This goes true, irrespective

which thread calls TerminateProcess function. I have found that if we call this function as soon as

debuggee is launched, the system would still process all process-loading, dll-loading debugging events and then

send this event to debugger!

In Visual Studio, we achieve the same by Debug->Stop Debugging or using infamous

Shift+F5 keystroke. For any debugger, implementing stop-debugging would be same irrespective how

debuggee was launched/attached. Therefore, it is also possible to terminate a process which cannot be terminated

using TaskManager (not all, though)!

Detaching the Debuggee

I believe this is quite uncommon task the software engineers perform. The concept is simple: detach the

debugger-debuggee relationship and set debuggee free. This is not terminating the debuggee, nor putting the

debuggee into a limbo state. It is just unbinding the debuggee from debugger. After detaching from debuggee, any

next unhandled debugging event would be presented default crash handler (!!).

In Visual Studio, you can do the same by Debug->Detach All command, which will set

debuggee free! If you are in single stepping mode (i.e. actively debugging a program), and then you detach

from debuggee, it would simply release the debuggee from debugger and would continue from that point on. Try it

out!

Therefore, it is perfectly possible to do following:

  • Start any process normally.
  • Attach to it, start debugging.
  • Perform debugging, even at source code level, if possible.
  • Once you have done debugging, or analyzed how process is doing, you can detach from debuggee, and set it

    free.

Of course, to perform debugging, you need to place some breakpoints (at source-code, or at assembly level). If

you want to just break wherever process is, use Debug->Break All, which will suspend

all threads and present you call stack of primary thread. Break-All is also applicable for processes you have

started. Break-All is only applicable if you are not actively debugging — since there is

nothing to break.

After long therotical session, here is the function you need: DebugActiveProcessStop, which just takes one parameter: the

Process-ID.

Debugging a Crashing Process

Depending on your Windows OS version and settings, when an application crashes, you may see one of the few

standard «Application has stopped working» dialog box. On Windows Vista and higher you can go to

Action Center and change the settings. You may encounter dialog like:

Crash1.jpg

Or a delayed dialogbox like:

In both of the cases, you have the opportunity to Debug the crashing application. If you

carefully (or partially) reading this article, you can now understand what «Exception

Code» in the first dialog would mean. This is access-violation exception

(EXCEPTION_ACCESS_VIOLATION) that would arrive under EXCEPTION_DEBUG_EVENT.

So, what happens when user hits Debug? Well, it launches the default crash

handler, which must have been setup in registry. If it is not setup, you won’t see the Debug button!

If you have installed any program that allows debugging, like WinDbg or Visual Studio, you would see the Debug

button. The Windows system keeps information about crash-handler at following location in registry:

  • HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug

If your OS is 64-bit, there would be additional entry at following location for debugging 32-bit crashing

processes:

  • HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug

I am not aware of what AE means in the key-name (or you can say I’ve not tried hard). While

there are few Values and Keys under this key; for us, Debugger string-value is of interest. The

Debugger value would have the full path of debugger and following two arguments:

  • -p %ld — Specifies the process identifier. The same process-Id we can use in

    DebugActiveProcess to start debugging a running process. %ld is a placeholder where

    the system would put the actual process-id while launching the debugger.

  • -e %ld — Handle to Windows event object which will

    be used to intimate the system that debugger has started debugging. This is not a debugging-event,

    but an EVENT

    object. As soon as debugger start debugging (i.e. enters debugger-loop), it calls

    SetEvent to let the system know that debugger has started debugging and can release the control to

    debugger.

  • Any additional parameters the custom debugger takes.

By now you must have seen that Debugger entry is set to:

  • «C:\Windows\system32\VSJitDebugger.exe» -p %ld -e %ld

Which is self-explanatory. If you have multiple version of Visual Studio installed, «Just-in-Time

Debugger» dialogbox would be shown. Note that, by this time, the debugger is not attached — it would be

attached only when user confirms. For us, the JIT dialogbox, and how it enumerates installed VS debuggers, is of no

interest — it is relevant to VS and not to Windows OS for Debugging.

I can judge your intelligence, that you already deduced we just need to put the path of our debugger in

Debugger registry entry, along with required -e and -p parameters. And

at the debugger level, we need to check these arguments and act appropriately. We must have to call

SetEvent to the handle provided (of course by typecasting it to HANDLE), as soon as we

start debugging the crashing process.

On 64-bit OS, if a 64-bit process crashes, the default AeDebug entry will be read. If a 32-bit

process crashes the AeDebug entry under Wow6432Node will be read. A 64-bit debugger can debug

either type of process. A 32-bit debugger can debug only 32-bit process — and attempt to debug 64-bit process

(either via CreateProcess or DebugActiveProcess) would simply fail.

On 32-bit OS, there would only be default AeDebug entry which will be used to debug 32-bit

processes. I am not aware of 16-bit application crashes on 32-bit OS!

Therefore, our debugging kingdom is only limited to 32-bit OS, or to Wow6432Node on 64-bit OS.

What if we build our debugger as 64-bit process? Well… boy, that would not work because of CONTEXT

structure — there are architecture differences. The EIP register, for example, is not valid on 64-bit architecture.

Also, the 64-bit architecture is not consistent — x64 and Itanuims for example as different!

Manually Attaching a Debugger

This is not same as either of two approaches:

  1. Using Attach to Process from your favourite debugger.
  2. Selecting ‘Debug‘ on a crashing process

In the first approach, the debugger would enumerate all processes that can be debugged. Thus, when a debugger-

user selects a process, it passes the same process-id to DebugActiveProcess and debugger-loop

starts.

In second approach, the debugger starts debugging a process through -p

process-id, and notifies the system by signaling the received event (via -e).

The third approach can be used by any user from Task Manager, or Process Explorer. The user would select Debug

from context menu (or whatever user-interface the utility has provided). The process-viewer would look for Debugger

entry in AeDebug key and start the given process for debugging this process. Here, only -

p will be passed, since the selected process is running fine (i.e. no unhandled exception

occurred). The debugger will enter debugger-loop for the process-id received.

ManualAttach.png

If ‘Debugger’ entry is missing, the process-viewer utility will disable or hide the Debug menu. Task

Manager hides it, while Process Explorer does not show the Debug entry in context menu, if this

information is missing in registry.

I have seen that for both 32-bit and 64-bit processes, both TM and PE use the default AeDebug key, and not the

key under Wow6432Node, for attaching the debugger. That means, even if process is 32-bit, still the 64-bit debugger

(by registry-definition) would be invoked.

Note: There can be only one debugger attached to a process, irrespective how the debugger-debuggee relationship is made. The second attempt may not be allowed by utility — if mistakenly allowed, the request to attach a debugger would simply be rejected by system.

Article is not complete yet. This is such an intricate subject, which requires time, research,

zillion-time debugging the debugger and what not! Since I wrote (and writing) this much, I could not resist myself

from publishing and sharing this information!

In my TODO list (I need to finish fast!):
— Placing user-breakpoints

— Showing call stack

— Allowing debugging actions (Stepping etc).

— Showing source code

— Multi threaded debugging

— Attaching to existing process (also, ‘Attach Debugger’ from task manager)
— Detaching from being-debugged process

— Making it the Crash-handler debugger.

— Showing dis-assembly when no source-code is available (probably not in this part).

More!

For sure, a well-documented source code, and a sample Debugger would be attached, soon! Till then, you can

bug me with bug reports!

History

  • December 16, 2010 — Initial public release
  • December 18, 2010 — Symbol retrieval
  • December 19, 2010 — Debuggee halt-resume functionality, CCoreDebugger class.
  • December 22, 2010 — Source file enumeration, placing user breakpoint (CPU trap flag)
  • December 25, 2010 — Stepping through the source-code
  • December 29, 2010 — Step-Out, Set-next-statement and Conditional Breakpoints
  • January 9, 2010 — Debugging a running process, default crash handler

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Как удалить японскую локаль windows 10
  • Какая разница между версиями windows
  • Не удалось остановить службу журнал событий windows
  • Как сделать обычный пуск в windows 11
  • Как сделать скриншот отдельной области экрана на компьютере на windows 10