Отладка приложений

       

На первый взгляд, отладчик для



На первый взгляд, отладчик для 32-разрядных ОС Windows (Win32) — это простая программа, к которой предъявляются всего два требования. Во-первых, отладчик должен передать функции createProcess (через параметр dwCreationFlags) специальный флажок  DEBUG_ONLY_THIS_PROCESS. Этот флажок сообщает операционной системе, что вызывающий поток вошел в цикл отладки, чтобы управлять процессом, который он запускает. Если отладчик может управлять множеством процессов, порождаемых первоначальным подчиненным процессом, то вместо флажка DEBUG_ONLY_THIS_PROCESS в :reateProcess будет пересылаться флажок DEBUG_PROCESS.

Таким образом, отладочный API Win32 организует базовый и подчиненный отладчики в отдельных процессах, что делает операционные системы Win32 намного более устойчивыми при отладке. Даже если подчиненный отладчик выполняет неконтролируемые записи (wild memory writes) в память, это не приведет к аварии базового отладчика. (Отладчики в 16-разрядных операционных системах Windows и Macintosh восприимчивы к повреждениям подчиненного отладчика, потому что как базовый, так и подчиненный отладчики выполняются в одном и том же контексте процесса.)

Второе требование заключается в том, что после запуска подчиненного отладчика базовый должен войти в цикл, вызывающий API-функцию v/aitForDebugEvent, чтобы ждать получения отладочного уведомления. Закончив обрабатывать конкретное событие отладки, он вызывает функцию :ontinueDebugEvent. Знайте, что только поток, который вызывает функцию :reateProcess со специальными флажками создания отладки, может вызывать отладочные API-функции. Следующий псевдокод показывает, как немного нужно, чтобы создать простейший Win32-oxnafl4HK:

void main ( void)

CreateProcess ( ..., DEBUG_ONLY_THIS_PROCESS, ...);

while ( 1 == WaitForDebugEvent ( ...))

 {

if ( EXIT_PROCESS) 

{

break;

 }

ContinueDebugEvent ( ...); 

}

}

Заметим, что для создания минимального варианта 32-разрядного Win-отладчика не требуется ни многопоточности, ни интерфейса пользователя, ни чего-либо еще.
Реальный отладочный API Win32 ориентирован на то, чтобы цикл отладки находился в отдельном потоке.

Пока базовый отладчик находится в цикле отладки, он получает различные уведомления о том, что в подчиненном отладчике имели место некоторые события. Следующая структура (с именем DEBUG_EVENT), которая заполнена функцией WaitForDebugEvent, содержит всю интересную информацию о событии отладки.

typedef struct _DEBUG_EVENT { 

DWORD dwDebugEventCode; 

DWORD dwProcessId;

 DWORD dwThreadld;

 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 Riplnfo;

 } u;

 } DEBUG_EVENT

Ниже приведено полное описание индивидуальных событий отладки.

  •  CREATE_PROCESS_DEBUG_EVENT
Это отладочное событие генерируется всякий раз, когда в отлаживаемом процессе создается новый процесс или когда отладчик начинает отладку уже активного процесса. Ядро генерирует это событие прежде, чем процесс начинает выполняться в режиме пользователя и прежде, чем ядро генерирует любые другие отладочные события для нового процесса.

Структура DEBUG_EVENT  содержит  структуру CREATE_PROCESS_DEBUG_INFO.

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

Дескриптор процесса имеет доступ PROCESS_VM_READ и PROCESS_VM_WRITE. Если отладчику открыты эти типы доступа к потоку, он может читать и писать в память процесса, используя функции ReadProcessMemory и WriteProcessMemory.

Дескриптор файла образа процесса имеет доступ GENERIC_READ и открывается для разделенного чтения (read-sharing).

Дескриптор потока инициализации процесса имеет доступ к потокам THREAD_GET_CONTEXT, THREAD_SET_CONTEXT и THREAD_SUSPEND_RESUME.


ЕСЛИ отладчик имеет эти типы доступа к потоку, он может читать (из) и писать В регистры потока, используя  функции GetThreadContext  и SetThreadContext, а также может приостанавливать и возобновлять поток, используя функции ResumeThread и SuspendThread. 

  •  CREATE_THREAD_DEBUG_EVENT
Это отладочное событие генерируется всякий раз, когда в отлаживаемом процессе создается новый поток или когда отладчик начинает отладку уже активного процесса. Это событие отладки генерируется прежде, чем новый поток начинает выполняться в режиме пользователя.

Структура DEBUG_EVENT содержит структуру CREATE_THREAD_DEBUG_INFO. Эта структура включает дескриптор нового потока и стартовый адрес потока. Дескриптор имеет доступ к потоку THREAD_GET_CONTEXT, THKEAD_SET_ CONTEXT и THREAD_SUSPEND_RESUME. Если отладчик имеет эти типы доступа к потоку, он может читать (из) и писать в регистры потока, используя функции GetThreadContext и SetThreadContext, и может приостанавливать и возобновлять поток, используя функции ResumeThread и SuspendThread.

  •  EXCEPTION_DEBUG_EVENT
Этот событие генерируется всякий раз, когда в отлаживаемом процессе происходит исключение. Возможные исключения включают попытку доступа к недоступной памяти, выполнение инструкций точки прерывания, попытку деления на 0 или любое другое исключение, упомянутое в теме "Structured Exception Handling" (Обработка структурированных исключений) в Platform SDK.

Структура DEBUG_EVENT содержит структуру EXCEPTION_DEBUG_INFO. Эта структура описывает исключение, которое послужило причиной события отладки.

Кроме условий стандартных исключений, во время отладки консольного процесса может возникнуть дополнительный код исключения. Ядро генерирует код исключения DBG_CONTROL_C, когда вводом в консольный процесс является результат нажатия клавиш <Ctrl>+<C>. Этот процесс обрабатывает сигналы от <Ctrl>+<C> и отлаживается. Данный код исключения не предполагается обрабатывать приложениями. Приложение никогда не должно использовать для него обработчик исключения.


Это событие возникает только для удобства отладчика и используется, только когда отладчик прикрепляется к консольному процессу.

Если процесс не отлажен или если отладчику пересылается необработанное исключение DBG_CONTROL_C, то отыскивается список функций обработчика консольного приложения. (Дополнительную информацию о функциях обработчика консольного процесса можно найти в документации MSDN для функции SetConsoleCtrlHandler.)

  • EXIT_PROCESS_DEBUG_EVENT
Это событие генерируется всякий раз, когда происходит выход из последнего потока в отлаживаемом процессе. Оно происходит сразу же после того, как ядро разгружает DLL процесса и обновляет код выхода из него.

Структура DEBUG_EVENT содержит структуру EXIT_PROCESS_DEBUG_INFO, которая специфицирует код выхода.

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

  • EXIT_THREAD_DEBUG_EVENT
Это событие генерируется всякий раз, когда происходит выход из потока, являющегося частью отлаживаемого процесса. Ядро генерирует это событие отладки сразу после того, как оно обновит код выхода потока.

Структура DEBUG_EVENT содержит структуру  EXIT_THREAD_DEBUG_INFO, которая специфицирует код выхода.

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

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

  •  LOAD_DLL_DEBUG_EVENT
Это событие генерируется всякий раз, когда отлаживаемый процесс загружает DLL. Оно происходит, когда системный загрузчик разрешает связи с DLL или когда отлаженный процесс использует функцию LoadLibrary. Это отладочное событие вызывается каждый раз, когда в адресное пространство загружается DLL. Если счетчик ссылок на DLL уменьшается до 0, DLL выгружается.


При следующей загрузке DLL это событие будет сгенерировано снова.

Структура DEBUG_EVENTсодержит  структуру LOAD_DLL_DEBUG_INFO. Эта структура включает дескриптор недавно загруженной DLL, базовый адрес DLL, и другую информацию, которая описывает DLL.

Как правило, при получении этого события отладчик загружает таблицу символов, связанную с DLL.

  •  OUTPUT_DEBUG_STRING_EVENT
Данное событие генерируется, когда отлаживаемый процесс использует функцию OutputDebugString.

Структура DEBUG_EVENT содержит структуру OUTPUT_DEBUG_STRING_INFO. Эта структура указывает адрес, длину и формат строки отладки.

  •  UNLOAD_DLL_DEBUG_EVENT
Это событие генерируется всякий раз, когда отлаживаемый процесс разгружает DLL, используя функцию FreeLibrary. Это отладочное событие происходит только тогда, когда DLL последний раз разгружается из адресного пространства процесса (т. е. когда счетчик использования DLL становится равным 0).

Структура DEBUG_EVENT содержит структуру UNLOAD_DLL_DEBUG_INFO. Эта структура указывает базовый адрес DLL в адресном пространстве процесса, который разгружает DLL.

Как правило, при получении этого отладочного события отладчик разгружает таблицу символов, связанную с DLL.

Когда происходит выход из процесса, ядро автоматически разгружает все DLL процесса, но не генерирует отладочное событие UNLOAD_DLL_DEBUG _EVENT. 

  •  RIP__INFO
Это событие генерируется только контролируемой сборкой Windows 98 и используется, чтобы сообщить об условиях ошибок, таких, например, как закрытие неправильных дескрипторов.

Когда отладчик обрабатывает события отладки, возвращаемые функцией :.TaitForDebugEvent, он имеет полный контроль над подчиненным отладчиком, потому что операционная система останавливает все потоки в этом отладчике, и не будет планировать их до тех пор, пока не будет вызвана функция continueoebugEvent. Если отладчик должен читать или писать в адресном пространстве подчиненного отладчика, он может использовать функции ReadProcessMemory и WriteProcessMemory.


Если память имеет атрибут "только-для-чтения", то можно вызвать функцию virtuaiProtect, чтобы повторно установить уровни защиты. Если базовый отладчик модифицирует код подчиненного отладчика через обращение к функции WriteProcessMemory, он должен вызвать функцию FlushinstructionCache, чтобы очистить кэш команд памяти. Если не вызвать FlushinstructionCache, то ваши изменения, в принципе, могут работать, но если память, которую вы изменили в настоящий момент, находится в кэше CPU, этого может и не быть. Вызов FlushinstructionCache особенно важен для мультипроцессорных машин. Если базовому отладчику нужно получить или установить текущий контекст подчиненного отладчика или регистры CPU, то он может вызвать функцию GetThreadContext или SetThreadContext.

Единственным отладочным событием Win32, которое нуждается в специальной обработке, является точка прерывания загрузчика. После того как операционная система посылает начальные уведомления CREATE_PROCESS_DEBUG_VENT и LOAD_DLL_DEBUG_EVENT для неявно загружаемых модулей, бвзовый отладчик получает уведомление EXCEPTION_DEBUG_EVENT. Это отладочное событие является точкой прерывания загрузчика (loader breakpoint). Подчиненный отладчик выполняет ее, потому что уведомление CREATE_PROCESS_DEBUG_EVENT указывает только на то, что процесс был загружен, а не на то, что он был выполнен. Базовый же отладчик в этот момент впервые узнает о том, что подчиненный отладчик действительно выполняется. В реальных (real-world) отладчиках инициализация главных структур данных (например, таблицы символов) выполняется во время процесса создания, и отладчик стартует,показывая код дизассемблера или делая необходимые модификации подчиненного отладчика в точке прерывания загрузчика.

1 Речь, видимо, идет об отладчиках систем программирования Visual Basic, Visual C++ и пр. — Пер

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


Дополнительная обработка, необходимая для первой точки прерывания (и для всех точек прерывания вообще), зависит от CPU. Для семейства Intel Pentium отладчик должен продолжать обработку, вызывая функцию ContinueDebugEvent и передавая ей флажок DBG_CONTINUE, чтобы подчиненный отладчик возобновил выполнение.

В листинге 4-2 показан "минимальный отладчик" MinDBG. Он обрабатывает все события отладки и должным образом выполняет дочерний отладочный процесс. При выполнении MinDBG обратите внимание, что обработчики событий отладки реально не показывают никакой интересной информации, такой, например, как имена исполняемых файлов и DLL. Нужно совсем немного поработать, чтобы превратить этот "минимальный" отладчик в "реальный".

Листинг 4-2. MINDBG.CPP

/*- - - - - - - - - - - - - - - - - - - - - - - -

Программы самого простого в мире отладчика для Win32 

- - - - - - - - - - - - - - - - - - - - - - - - - */

/*//////////////////////////////////////////////////////////////

Обычные директивы #include

//////////////////////////////////////////////////////////////*/ 

#include "stdafx.h"

/*///////////////////////////////////////////////////

Прототипы

////////////////////////////////////////////////////////*/

// Shows the minimal help.

void ShowHelp ( void);

// Display-функции

void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI);

void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI);

void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI);

void DisplayExitProcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI);

void DisplayDllLoadEvent ( LOAD_DLL_DEBUG_INFO & stLDDI);

void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI);

void DisplayODSEvent ( HANDLE hProcess,

OUTPUT_DEBUG_STRING_INFO & stODSI );

void DisplayExceptionEvent ( EXCEPTION_DEBUG_INFO & stEDI);

 /*////////////////////////////////////////////////////////////

Точка входа! 



/////////////////////////////////////////////////////////////*/

void main ( int argc, char * argv[ ])

// Проверка наличия аргумента командной строки.

if ( 1 == argc)

{

ShowHelp (); 

return;

}

// Конкатенация параметров командной строки.

TCHAR szCmdLine[ МАХ_РАТН ];

szCmdLine[ 0 ] = '\0';

for ( int i = 1; i < argc; i++)

{ strcat ( szCmdLine, argv[ i ]);

 if ( i < argc) 

{

strcat ( szCmdLine, " "); 

}

}

// Попытка стартовать процесс подчиненного отладчика.

// Вызов функции выглядит как нормальный вызов CreateProcess,

//за исключением флажка специального режима

// запуска DEBUG_ONLY_THIS_PROCESS.

STARTUPINFO stStartlnfo ;

PROCESS_INFORMATION stProcessInfo ;

memset ( sstStartlnfo , NULL, sizeof ( STARTUPINFO ));

memset ( SstProcessInfo, NULL, sizeof ( PROCESS_INFORMATION));

stStartlnfo.cb = sizeof ( STARTUPINFO);

BOOL bRet = CreateProcess ( NULL ,

szCmdLine , 

NULL

NULL , 

FALSE , 

CREATE_NEW_CONSOLE |

DEBUG__ONLY_THIS_PROCESS,

 NULL , 

NULL , 

&stStartlnfo ,

 &stProcessInfo ) ;

// Посмотреть, стартовал ли процесс подчиненного отладчика.

if ( FALSE == bRet)

{

printf ( "Unable to start %s\n", szCmdLine); 

return;

}

// Подчиненный отладчик стартовал, войдем в цикл отладки.

DEBUG_EVENT stDE

BOOL bSeenlnitialBP = FALSE ;

BOOL bContinue = TRUE ;

HANDLE hProcess = INVALID_HANDLE_VALUE;

DWORD dwContinueStatus

// Входим в цикл while.

while ( TRUE == bContinue)

{

// Пауза, пока не придет уведомление о событии отладки.

bContinue = WaitForDebugEvent ( &stDE, INFINITE);

// Обработать конкретные отладочные события. Из-за того что

// MinDBG является минимальным отладчиком, он обрабатывает

// только некоторые события.

switch ( stDE.dwDebugEventCode)

{

case CREATE_PROCESS_DEBUG_EVENT : 

{

DisplayCreateProcessEvent ( stDE.u.CreateProcessInfo);

 // Сохранить информацию дескриптора, необходимую

 // для дальнейшего использования



. hProcess = stDE.u.CreateProcessInfo.hProcess;

 dwContinueStatus = DBG_CONTINUE;

 }

break;

case 'EXIT_PROCESS_DEBUG_EVENT : 

{

DisplayExitProcessEvent ( stDE.u.ExitProcess);

 bContinue = FALSE; 

dwContinueStatus = DBG_CONTINUE;

 }

break;

case LOAD_DLL_DEBUG_EVENT : 

{

DisplayDllLoadEvent ( stDE.u.LoadDll); 

dwContinueStatus = DBG_CONTINUE; 



 break;

case UNLOAD_DLL_DEBUG_EVENT :

 {

DisplayDllUnLoadEvent ( stDE.u.UnloadDll); 

dwContinueStatus = DBG_CONTINUE; 

}

break;

case CREATE_THREAD_DEBUG_EVENT : 

{

DisplayCreateThreadEvent ( stDE.u.CreateThread);

 dwContinueStatus = DBG_CONTINUE; 

}

break;

case EXIT_THREAD_DEBUG_EVENT :

{

DisplayExitThreadEvent ( stDE.u.ExitThread);

dwContinueStatus = DBG_CONTINUE;

 }

break;

case OUTPUT_DEBUG_STRING_EVENT : 

{

DisplayODSEvent ( hProcess, stDE.u.DebugString);

dwContinueStatus = DBG_CONTINUE;

 }

break;

case RIPR_VENT : 

dwContinueStatus = DBG_CONTINUE;

 }

break;

case EXCEPTION_DEBUG_EVENT : 

{

DisplayExceptionEvent ( stDE.u.Exception);

// Единственным исключением, с которым следует

// обращаться по-особому, является начальная

// точка прерывания, которую обеспечивает загрузчик.

switch ( stDE.u.Exception.ExceptionRecord.ExceptionCode)

{

case EXCEPTION_BREAKPOINT :

{

// Если возникает исключение точки прерывания

// и оно замечается впервые, то продолжаем;

// иначе, передаем исключение подчиненному

// отладчику

if ( FALSE == bSeenlnitialBP)

{

bSeenlnitialBP = TRUE;

 dwContinueStatus = DBG_CONTINUE; 

}

else {

// Хьюстон, у нас проблема! 

dwContinueStatus =

DBG_EXCEPTION_NOT_HANDLED; 





break;

// Просто передать любые другие исключения

 // подчиненному отладчику, 

default :

 {

dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;

 }

break; 

}

 }

break;

// Для любых других событий просто продолжить,



 default :

 {

dwContinueStatus = DBG_CONTINUE;

 }

break; 

}

// Перейти к операционной системе. 

ContinueDebugEvent ( stDE.dwProcessId, 

stDE.dwThreadld , 

dwContinueStatus );

 } 

}

/*/////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////*/ 

void ShowHelp ( void)

{

printf ( "MinDBG <program to debug> "

"<program's command-line options>\n");

 }

void DisplayCreateProcessEvent ( CREATE_PROCESS_DEBUG_INFO & stCPDI)

 {

printf ( " Create Process Event :\n"); 

printf (." hFile : Ox%08X\n",

stCPDI.hFile ); 

printf ( " hProcess : 0x%08X\n",

stCPDI.hProcess ); 

printf ( " hThread : 0x%08X\n",

stCPDI.hThread);

printf (" lpBaseOfImage :0x%08X\n",

stCPDI.lpBaseOfImage);

printf("dwDebugInfoFileOffset: 0x%08X\n",

stCPDI.dwDebugInfoFileOffset);

printf("nDebugInfoSize: 0x%08X\n",

stCPDI.nDebugInfoSize);

printf ( " IpThreadLocalBase : Ox%08X\n",

stCPDI.IpThreadLocalBase ); 


printf ( " IpStartAddress : Ox%08X\n",

stCPDI.IpStartAddress ) ;  

printf ( " IpImageName : Ox%08X\n",

stCPDI.IpImageName );

printf ( " fUnicode : Ox%08X\n",

stCPDI.fUnicode );

}

void DisplayCreateThreadEvent ( CREATE_THREAD_DEBUG_INFO & stCTDI)

{

printf ( "Create Thread Event :\n");

printf ( " hThread : Ox%08X\n",

stCTDI.hThread );

printf ( " IpThreadLocalBase : Ox%08X\n",

stCTDI.IpThreadLocalBase );

printf ( " IpStartAddress : Ox%08X\n",

stCTDI.IpStartAddress );

}

void DisplayExitThreadEvent ( EXIT_THREAD_DEBUG_INFO & stETDI)

{

printf ( "Exit Thread Event :\n");

printf ( " dwExitCode : Ox%08X\n",

stETDI.dwExitCode );

}

void DisplayExitPrpcessEvent ( EXIT_PROCESS_DEBUG_INFO & stEPDI)



{

printf ( " Exit Process Event :\n");

printf ( " dwExitCode ' : Ox%08X\n",

stEPDI.dwExitCode );

}

void DisplayDllLoadEvent ( LOAD_DLL_DEBUG_INFO & stLDDI)

{

printf ( "DLL Load Event :\n");

printf ( " hFile : Ox%08X\n",

stLDDI.hFile );

printf ( " IpBaseOfDll : Ox%08X\n",

stLDDI.IpBaseOfDll );

printf ( " dwDebuglnfoFileOffset : Ox%08X\n",

stLDDI.dwDebuglnfoFileOffset );

printf ( " nDebuglnfoSize : Ox%08X\n",

stLDDI.nDebuglnfoSize );

printf ( " IpImageName : Ox%08X\n",

stLDDI.IpImageName );

printf ( " fUnicode : Ox%08X\n",

stLDDI.fUnicode ); 

}

void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI)

{

printf ( "DLL Unload Event :\n"); 

printf ( " IpBaseOfDll : Ox%08X\n",

stULDDI.IpBaseOfDll ); 

}

 void DisplayODSEvent { HANDLE hProcess,

OUTPUT_DEBUG STRING INFO & stODSI ) 

{

printf ( "OutputDebugString Event :\n");

 printf ( " IpDebugStringData : Ox%08X\n",

stODSI.IpDebugStringData ); 

printf ( " fUnicode : Ox%08X\n",

stODSI.fUnicode );

  printf ( " nDebugStringLength : Ox%08X\n",

stODSI.nDebugStringLength ); 

printf ( " String :\n"); char szBuff[ 512 ];

if ( stODSI.nDebugStringLength > 512)

 {

return; 

}

DWORD dwRead; 

BOOL bRet; 

bRet = ReadProcessMemory ( hProcess

stODSI.IpDebugStringData ,

 szBuff ,

 stODSI.nDebugStringLength , 

SdwRead ); 

printf ( "%s", szBuff); 

}

void DisplayExceptionEvent ( EXCEPTION_DEBUG INFO & stEDI)

 {

printf ( "Exception Event :\n");

 printf ( " dwFirstChance : Ox%08X\n",

stEDI.dwFirstChance );

printf ( " ExceptionCode : Ox%08X\n",

stEDI.ExceptionRecord.ExceptionCode );

 printf ( " ExceptionFlags : Ox%08X\n",

stEDI.ExceptionRecord.ExceptionFlags );

 printf ( " ExceptionRecord : Ox%08X\n",

stEDI.ExceptionRecord.ExceptionRecord );

printf ( " ExceptionAddress : Ox%08X\n",

stEDI.ExceptionRecord.ExceptionAddress );

printf ( " NumberParameters : Ox%08X\n",

stEDI.ExceptionRecord.NumberParameters ); 

}



Содержание раздела