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

         

Автоматический запуск приложений в отладчике


Самыми трудными для отладки типами приложений являются те приложения, которые запускаются другим процессом. В эту категорию нападают службы Windows 2000 и внепроцессные (out-of-process) СОМ-серверы (СОМ out-of-process servers). Чтобы заставить отладчик прикрепиться к процессу, во многих случаях можно использовать API-функцию DebugBreak. В двух случаях, однако, эта функция работать не будет. Во-первых, она иногда не работает со службами Windows 2000. Если нужно отладить запуск службы, то вызов DebugBreak присоединит отладчик, но к тому времени, когда отладчик запустится, может быть исчерпан интервал тайм-аута службы, и Windows 2000 остановит ее. Во-вторых, DebugBreak не будет работать, когда нужно отлаживать внепроцессный СОМ-сервер. Если вы вызовете DebugBreak, обработчик СОМ-ошибок отловит исключение точки прерывания и завершит СОМ-сервер. К счастью, Windows 2000 позволяет указать, что приложение должно стартовать в отладчике. Это свойство позволяет начать отладку прямо с первой инструкции. Однако, прежде чем вы включите это свойство для службы Windows 2000, удостоверьтесь, что в этой службе сконфигурирована возможность взаимодействия с рабочим столом Windows 2000.

Свойство запуска с отладчиком можно включить двумя способами. Самый легкий — запустить утилиту GFLAGS.EXE и выбрать переключатель Image File Options (см. рис. 4.1). После ввода в редактируемое поле Image File Name имени двоичного файла программы установите флажок Debugger в группе Image Debugger Options) и введите в редактируемое поле рядом с этим флажком полный путь к отладчику.

Более трудный способ: нужно вручную установить необходимые параметры з подходящие разделы реестра (с помощью редактора RegEdit). В разделе

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NTXCurrent Version\Image Tile Execution Options

создайте подраздел, имя которого совпадает с именем файла вашего приложения. Например, если имя приложения — FOO.EXE, то имя подраздела реестра тоже FOO.EXE. В этом подразделе создайте новый строковый параметр с именем Debugger. В диалоговом окне Изменение строкового параметра введите с клавиатуры полное (вместе с путем к каталогу) имя файла выбранного вами отладчика. Если вы указали GFLAGS.EXE и установили некоторые глобальные опции, то сможете заметить в ключе вашего приложения строчное значение GiobaiFiag.

Теперь при запуске вашего приложения автоматически загружается и запускается и отладчик. Опции командной строки для отладчика также можно определить в строковом параметре Debugger (вслед за именем программы отладчика). Например, для того чтобы использовать отладчик WinDBG и автоматически инициировать отладку, как только стартует WinDBG, нужно в диалоговом окне изменения строкового параметра Debugger ввести значение d:\platform sdk\bin\windbg.exe -g.



Быстрые клавиши прерываний




Иногда нужно быстро войти в отладчик. Если вы отлаживаете консольные приложения, то нажатие клавиш <Ctrl>+<C> или <Ctrl>+<Break> вызовет специальное исключение (с именем DBG_CONTROL_C) . Это исключение переведет вас прямо в отладчик и позволит стартовать отладку.

Полезным свойством как Windows 2000, так и Windows NT 4 является возможность в любое время переходить в отладчик также и в GUI-приложениях. При выполнении под отладчиком нажатие клавиши <F12> приводит (по умолчанию) к вызову функции DebugBreak. Интересный аспект обработки этой клавиши заключается в том, что, даже если вы используете ее как акселератор или как-то иначе обрабатываете сообщения клавиатуры для этой клавиши, она все еще будет подключать вас к отладчику.

В Windows NT 4 быстрая клавиша прерывания <F12> назначена по умолчанию, однако в Windows 2000 вы можете сами определить, какую клавишу следует использовать для этих целей. Для чего в разделе реестра

HKEY_LOCAL_MACHINE\SOFTWARE\ Microsoft\WindowsNT\CurrentVersion\AeDebug

установите для параметра userDebuggerHotKey значение кода клавиши (VK_*). Например, чтобы использовать клавишу <Scroll Lock> для подключения к отладчику, следует установить значение UserDebuggerHotKey равным 0x91. Изменения вступают в силу после перезагрузки компьютера.



Чтение и запись памяти


Чтение из памяти подчиненного отладчика довольно простая операция. Выполняет ее функция ReadProcessMemory. Когда основной отладчик запускает подчиненный, то он имеет полный доступ к подчиненному, потому что дескриптор процесса, возвращенный событием отладки CREATE_PROCESS_DEBUG_EVENT, содержит спецификаторы доступа PROCESS_VM_READ и PROCESS_VM_WRITE. Если ваш отладчик прикрепляется к процессу с помощью функции DebugActiveProcess, то, чтобы получить дескриптор подчиненного отладчика, нужно вызвать функцию openProcess и указать при этом доступ как для чтения, так и для записи.

Прежде чем рассказывать о записи в память подчиненного отладчика, нужно кратко объяснить важную концепцию "копирование-при-записи" (сору-on-write) Когда Windows загружает выполняемый файл, она разделяет между различными использующими его процессами так много отображаемых страниц памяти этого двоичного файла, насколько это возможно. Если один из этих процессов выполняется под отладчиком, и одна из этих страниц имеет записанную в нее точку прерывания, то очевидно, что эта точка прерывания не может быть представлена во всех процессах, разделяющих эту страницу. Как только какой-нибудь процесс, работающий вне отладчика, выполнит этот код, он завершится аварийно с исключением EXCEPTION_BREAKPOINT. Чтобы не допустить такой ситуации, операционная система, видя, что страница изменена для конкретного процесса, делает копию этой страницы, которая является частной (private) для процесса, записавшего в нее точку прерывания. Таким образом, как только процесс пишет в страницу памяти, операционная система ее копирует.

Запись в память подчиненного отладчика почти столь же проста, как и чтение из нее. Однако, поскольку страницы памяти, в которые будет произведена запись, могут быть помечены атрибутом "только-для-чтения", то, чтобы получить текущие характеристики защиты страницы, нужно сначала вызвать функцию virtualQueryEx. Имея эти характеристики, можно использовать API-функцию virtualProtectEx, чтобы установить для страницы параметр защиты PAGE_EXECUTE_READWRITE, что позволит вам писать в нее, а Windows сможет подготовиться к выполнению операций "копирование-при-записи".
По завершении записи в память нужно установить защиту страницы в исходное состояние. Если этого не сделать, подчиненный отладчик может случайно записать страницу (и преуспеть в этом, тогда как должен был потерпеть неудачу). Если бы, например, первоначальным параметром защиты страницы было "только-чтение", то случайная запись подчиненного отладчика привела бы к нарушению доступа. Без восстановления защиты страницы (от записи) случайная запись не сгенерирует соответствующего исключения, и вы будете иметь случай, когда выполнение под отладчиком отличается от выполнения вне его.

Интересная деталь отладочного API Win32: когда появляется событие OUTPUT_DEBUG_STRING_EVENT, то ответственным за получение строки для вывода является основной отладчик. Информация, переданная отладчику, включает расположение и длину строки. Получив это сообщение, отладчик читает память вне подчиненного отладчика. В главе 3 упоминалось, что при выполнении под отладчиком операторы трассировки могут легко изменить поведение приложения. Поскольку все потоки в приложении останавливаются, когда цикл отладки обрабатывает событие, вызов функции OutputDebugString в подчиненном отладчике означает, что все потоки стоят. Листинг 4-3 показывает, как WDBG обрабатывает событие OUTPUT_DEBUG_STRING_EVENT. Заметьте, что функция DBG_ReadProcessMemory является функцией-оболочкой вокруг ReadProcessMemory из LOCALASSIST.DLL.

Листинг 4-3.OutputDebugStringEvent изPROCESSDEBUGEVENTS.CPP

static

DWORD OutputDebugStringEvent ( CDebugBaseUser * pUserClass ,

LPDEBUGGEEINFO pData ,

 DWORD dwProcessId, 

DWORD dwThreadld , 

OUTPUT_DEBUG_STRING_INFO & stODSI )

 {

TCHAR szBuff[ 512 ];

HANDLE hProc = pData->GetProcessHandle (); DWORD dwRead;

 // Читать память.

BOOL bRet = DBG_ReadProcessMemory( hProc ,

stODSI.lpDebugStringData , 

szBuff, min ( sizeof ( szBuff) ,

stODSI.nDebugStringLength), 

&dwRead ) ;

ASSERT ( TRUE == bRet);

if ( TRUE == bRet)

{

// Строку всегда завершает NULL . 

szBuff [ dwRead + 1 ] = _T ( '\0');

 // Преобразовать символы CR/LF .

pUserClass->ConvertCRLF ( szBuff, sizeof ( szBuff)); 

// Послать ковертированную строку в класс пользователя.

 pUserClass->OutputDebugStringEvent ( dwProcessId,

dwThreadld , szBuff ); 

}

 return ( DBG_CONTINUE);

}



Интересная проблема разработки WDBG


Вообще у меня было мало неприятностей при разработке WDBG. Однако настало время обсудить одну, на мой взгляд, довольно интересную проблему. При работе с отладчиком Visual C++ окно Output показывает полные пути к загруженным программным модулям. Поскольку требовалось снабдить WDBG максимальным набором функциональных возможностей, в нем продублирована эта функция отладчика Visual C++. Но сделать это оказалось непросто.

Приведенное ниже определение структуры LOAD_DLL_DEBUG_INFO (она передается в отладчик при получении уведомлений LOAD_DLL_DEBUG_EVENT) содержит поле ipimageName, которое, по всей вероятности, должно хранить имя загружаемого модуля. Это так и есть, но ни одна из операционных систем Win32 никогда правильно не заполняет это поле при его считывании в программу.

typedef struct _LOAD_DLL_DEBUG_INFO

{

HANDLE hFile;

LPVOID IpBaseOfDll;

DWORD dwDebuglnfoFileOffset;

DWORD nDebuglnfoSize;

LPVOID IpimageName;

WORD fUnicode;

 } LOAD_DLL_DEBUG_INFO;

Поскольку при получении уведомления LOAD_DLL_DEBUG_EVENT, образно говоря, модуль загружается в символьную машину DBGHELP.DLL, то мне казалось, что после загрузки модуля (в память) легко можно отыскать его полное имя. API-функция SymGetModuieinfo получает (через соответствующий параметр) показанную ниже структуру IMAGEHLP_MODULE, где имеется место для  полного имени модуля (см. поле ModuleName[32]).

 На самом деле все, по-видимому, наоборот: это символьная машина загружает символьную информацию модуля в соответствующий символьный файл (в данном случае — в DBG-файл). — Пер

typedef struct _IMAGEHLP_MODULE {

DWORD SizeOfStruct;

DWORD BaseOfImage;

DWORD ImageSize;

DWORD TimeDateStamp;

DWORD Checksum;

DWORD NumSyms;

SYM_TYPE SymType;

CHAR ModuleName[32];

CHAR ImageName[256] ;

CHAR LoadedlmageName[256]; 

} IMAGEHLP_MODULE, *PIMAGEHLP_MODULE;

Странная вещь: когда функция SymGetModuieinfo возвращает символьную информацию модуля, то вместо имени модуля либо возвращается имя символьного DBG-файла, либо ничего не возвращается (т.
е. имя модуля в возвращаемой информации полностью пропускается). Такое поведение может показаться удивительным, но только на первый взгляд. Когда была получена • структура LOAD_DLL_DEBUG_INFO, ее первый член (типа hFile) был правильным, и тогда была вызвана функция SymLoadModuie с дескриптором того же типа (hFile). Поскольку я никогда не загружал в символьную машину DBGHELP.DLL полное имя файла, она просто заглядывала в открытый файл, обозначенный дескриптором hFile, находила в нем отладочную информацию и считывала ее. У символьной машины никогда не было необходимости знать полное имя файла.

Получить же требовалось полное имя загруженного модуля. Сначала я думал, что мог бы использовать сам дескриптор файла, чтобы получить доступ к экспортной секции модуля и сообщить найденное там имя модуля. Кроме того, модуль мог быть переименован, и его имя в экспортной секции было бы неправильным. Это мог быть ЕХЕ- или DLL-модуль, не содержащий списка экспортируемых модулей. И даже если бы как-то удалось найти правильное имя модуля, его полное имя (путь) было бы недоступно.

Затем я предположил, что где-то должна быть API-функция, которая будет получать (через аргумент вызова) значение дескриптора файла и возвращать полное имя открытого файла. Обнаружив, что в библиотеках операционной системы такой функции нет, я проверил несколько недокументированных значений, которые работали, но не полностью. Тогда я начал поиск с помощью функций из набора Tool Help и файла PSAPI.DLL, потому что оба этих средства сообщают информацию о модулях, загруженных в процесс. Функции Tool Help в Windows 98 работали нормально, в Windows NT 4 происходил сбой функций PSAPI.DLL, а в Windows 2000 функции Tool Help тяжело подвешивали отладчик. Сами функции Tool Help не были испорчены, но они пробуют стартовать новый поток в адресном пространстве подчиненного отладчика с помощью вызова CreateRemoteThread. Поскольку подчиненный отладчик был полностью остановлен в WDBG, функции Tool Help будут висеть, пока подчиненный отладчик повторно не стартует.


После переключения на PSAPI.DLL в Windows 2000 вместо зависания происходил сбой функций Tool Help, как это было в Windows NT 4.

Используя подход к решению проблем, который был намечен еще в главе 1, я приступил к формулировке некоторой гипотезы, пытающейся объяснить проблему. Внимательное изучение функции GetModuleFilenameEx из PSAPI.DLL помогло понять, почему она не работала, когда я ее вызывал. Уведомление LOAD_DLL_DEBUG_EVENT сообщало мне, что DLL только собиралась загружаться в адресное пространство, а не то что DLL уже загружена. Поскольку память не была Отображена для хранения DLL, функция GetModuleFilenameEx из PSAPI.DLL терпела неудачу. Когда я выполнял пошаговый проход памяти этой функции на уровне языка ассемблера, то казалось, что она выглядит как список отображенной памяти, который операционная система поддерживает для каждого процесса.

Локализовав источник проблемы, нужно было только выяснить, когда операционная система полностью отображала модуль в памяти. Вероятно, можно было предпринять чрезвычайные меры, чтобы получить эту информацию, например, выполнив обратную разработку загрузчика образа в NTDLL.DLL и установив там точку прерывания. К счастью, нашлось немного более простое решение, которое не вызывало остановов на каждом релизе пакета обслуживания операционной системы. Оказалось, что загрузочную информацию модуля нужно просто поставить в очередь и время от времени проверять ее. pulseModuieNotification — это авторская функция, которая управляет деталями проверки загрузочной информации модуля; ее реализацию (исходный код) можно найти в файле MODULENOTIFICATION.CPP на сопровождающем компакт-диске. Если вы просмотрите исходный код функции DebugThread в DEBUGTHREAD.CPP на сопровождающем компакт-диске, то увидите, что функция PulseModuieNotification вызывается на каждом шаге цикла отладки, и каждый раз, когда завершается интервал ожидания (тайм-аут) функции WaitForDebugEvent.

Общий вопрос отладки

Почему я не могу входить в системные функции или устанавливать точки прерывания в системной памяти Windows 98?



Если вы когда- нибудь пробовали во время отладки входить внутрь (step into) некоторых системных функций операционной системы Windows 98, то могли убедиться, что отладчик не позволяет это делать. С другой стороны, Windows 2000 позволяет входить в любую точку процесса пользовательского режима. Дело в том, что Windows 2000 реализует "копирование-при-записи" во всей памяти, тогда как Windows 98 делает это только для адресов ниже 2 Гбайт.

Напомним, что "копирование-при-записи" позволяет процессам иметь свои собственные частные копии страниц отображенной памяти, когда они (процессы) или отладчик пишут на странице. В Windows 98 все процессы разделяют адресное пространство над 2 Гбайт. Поскольку Windows 98 не реализует "копирование-при-записи" для этих адресов, то если бы Windows 98 разрешала вам установить точку прерывания в разделяемой памяти, первый же процесс, который выполнил бы этот адрес, вызвал бы исключение точки прерывания. Поскольку этот процесс, вероятно, не выполняется под отладчиком, то он закончился бы с исключением точки прерывания. Хотя некоторые системные DLL, такие как сомсть32. DLL, загружаются ниже 2 Гбайт, главные системные DLL, такие как KERNEL32.DLL и USER32.DLL, загружаются выше 2 Гбайт. Это означает, что если у вас нет корневого отладчика, выполняющегося в Windows 98, то вы не можете входить в них с отладчиком пользовательского режима.



Итак, вы хотите написать собственный отладчик?


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

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

Необходимо детально познакомиться с форматом РЕ и конкретным CPU, на котором вы работаете. Сопровождающий компакт-диск содержит PECOFF.DOC, самую последнюю спецификацию РЕ-файлов от Microsoft. Подробнее можно изучать CPU по руководствам Intel CPU, доступным на www.intel.com.

Прежде чем заняться полным отладчиком, вы должны, вероятно, написать дизассемблер. Написание дизассемблера позволит не только подробнее изучить CPU, но и получить код, который можно будет использовать в отладчике. Дизассемблер в WDBG — это код "только-для-чтения" (read-only). Другими словами, только разработчик, который его написал, может его читать. Стремитесь делать ваш дизассемблер расширяемым и удобным для сопровождения. Я написал в прошлом довольно много программ на языке ассемблера, но действительно изучил язык ассемблера, только создав собственный дизассемблер.

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

Как уже говорилось, символьная машина DBGHELP.DLL достаточна для некоторых превосходных вспомогательных отладочных утилит, но не достаточна для реального отладчика. Вы можете всегда заняться обратной разработкой формата PDB-файлов, а мы все можем надеяться, что Microsoft когда-нибудь откроет доступ к PDB-файлам.



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



На первый взгляд, отладчик для 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 ); 

}



Операции Step Into, Step Over и Step Out


Теперь, после описания точек прерывания и символьных машин, можно объяснить, как отладчики реализуют три замечательные операции — Step Into, Step Over и Step Out. Они не реализованы в WDBG, потому что я хотел сконцентрироваться на основных частях отладчика. Эти функции работают в рамках двух специальных представлений1 отлаживаемой программы, которые позволяют отслеживать текущую выполняемую строку или команду.

Все эти операции (Step Into, Step Over и Step Out) работают с одноразовыми точками прерывания, которые, как вы помните из предыдущих разделов, являются точками прерывания, сбрасываемыми отладчиком после того, как они срабатывают. При обсуждении пункта меню Debug Break (см. выше) был рассмотрен другой случай, в котором отладчик использует одноразовые точки прерывания для остановки обработки.

Операция Step Into работает по-разному, в зависимости от того, на каком уровне выполняется отладка: на исходном или на уровне дизассемблирования. При отладке на исходном уровне отладчик должен ориентироваться на одноразовые точки прерывания, потому что одна строка языка высокого уровня переводится в одну или большее количество строк языка ассемблера. При переводе CPU в пошаговый режим будет происходить пошаговое выполнение индивидуальных машинных команд, а не строк исходного кода.

На исходном уровне отладчик знает, на какой исходной строке вы находитесь. Когда выполняется команда отладчика Step Into, то для нахождения адреса следующей выполняемой строки отладчик использует символьную машину. Отладчик выполнит частичное дизассемблирование по адресу следующей строки, чтобы видеть, является ли эта строка командой вызова. Если строка — команда вызова, то отладчик установит одноразовую точку прерывания на первом же адресе функции, которую собирается вызывать подчиненный отладчик. Если адрес следующий строки — не команда вызова, то отладчик там и устанавливает точку прерывания one-shot. Затем отладчик разблокирует подчиненный отладчик, чтобы тот выполнился до только что установленной точки прерывания.
Когда эта точка прерывания сработает, отладчик заменит код операции в точке ее размещения (в памяти) и освободит связанную с ней память. Если пользователь работает на уровне дизассемблирования, то реализовать Step Into намного легче, потому что отладчик будет просто переводить CPU в режим пошагового выполнения.

Операция Step Over похожа на Step Into в том, что отладчик должен отыскивать следующую строку в символьной машине и dsgjkyznm частичное дизассемблирование по адресу строки. Различие их в том, что для Step Over (если строка является вызовом) отладчик также будет устанавливать точку прерывания one-shot, но после инструкции вызова.

Операция Step Out, в некотором смысле, является самой простой из трех. Когда пользователь выбирает команду Step Out, отладчик проходит стек, чтобы найти адрес возврата для текущей функции и устанавливает по этому адресу точку прерывания one-shot.

 Source view (представление в виде строк исходного кода) и disassembly view (представление в виде кодов дизассемблера в окне Disassembly). — Пер.

Обработка операций Step Into, Step Over и Step Out кажется довольно простой, но имеется одна особенность, которую следует рассмотреть. Что делать, если (в отладчике, создаваемом для управления этими операциями) уже установлены точки прерывания one-shot для этих операций, а перед ними срабатывает регулярная точка прерывания? Как разработчик отладчика, вы имеете две возможности. Первая — оставить только точки прерывания one-shot (чтобы только они и срабатывали). Другая возможность — удалять точки прерывания one-shot, когда отладчик уведомляет вас о том, что сработала регулярная точка прерывания. Отладчик Visual C++ использует последнюю возможность.



Отладчики режима ядра


Отладчики режима ядра находятся между CPU и операционной системой. Это означает, что, когда вы останавливаете отладчик режима ядра, операционная система также полностью останавливается. Нетрудно сообразить, что переход операционной системы к резкому останову полезен, когда вы работаете с таймером и над проблемами синхронизации. Все-таки, за исключением одного отладчика, о котором будет рассказано ниже (в разделе "Отладчик SoftlCE" данной главы), нельзя отлаживать код пользовательского режима с помощью отладчиков режима ядра.

Отладчиков режима ядра не так много. Вот некоторые из них: Windows 80386 Debugger (WDEB386), Kernel Debugger (1386KD), WinDBG и SoftlCE. Каждый из этих отладчиков кратко описан в следующих разделах.

Отладчик WDEB386

WDEB386 — это отладчик режима ядра Windows 98, распространяемый в составе Platform SDK. Этот отладчик полезен только для разработчиков, пишущих драйверы виртуальных устройств Windows 98 (VxD). Подобно большинству отладчиков режима ядра для операционных систем Windows, отладчик WDEB386 требует для работы две машины и нуль-модемный кабель. Две машины необходимы потому, что часть отладчика, которая выполняется на целевой машине, имеет ограниченный доступ к ее аппаратным средствам, так что он посылает свой вывод и получает команды от другой машины.

Отладчик WDEB386 имеет интересную историю. Он начинался как внутренний фоновый инструмент Microsoft в эпоху Windows 3.0. Его было трудно использовать, и он не имел достаточной поддержки для отладки исходного кода и других приятных свойств, которыми нас испортили отладчики Visual C++ и Visual Basic.

"Точечные" (DOT) команды — наиболее важная особенность WDEB386. Через прерывание INT 41 можно расширять WDEB386 с целью добавления команд. Эта расширяемость позволяет авторам VxD-драйверов создавать заказные отладочные команды, которые дают им свободный доступ к информации в их виртуальных устройствах. Отладочная версия Windows 98 поддерживает множество DOT-команд, которые позволяют наблюдать точное состояние операционной системы в любой точке процесса отладки.


Отладчик I386KD

Windows 2000 отличается от Windows 98 тем, что реально действующая часть отладчика режима ядра является частью NTOSKRNL.EXE — файла главного ядра операционной системы Windows 2000. Этот отладчик доступен как в свободных (выпускных), так и в проверенных (отладочных) конфигурациях операционной системы. Чтобы включить отладку в режиме ядра, установите параметр загрузчика /DEBUG в BOOT.INI и, дополнительно, опцию загрузчика /DEBUGPORT, если необходимо установить значение коммуникационного порта отладчика режима ядра, отличающееся от умалчиваемого (СОМ1). I386KD выполняется на своей собственной машине и сообщается с машиной Windows 2000 через кабель нуль-модема.

Отладчик режима ядра NTOSKRNL.EXE делает только то, что достаточно для управления CPU, так чтобы операционная система могла быть отлажена. Большая часть отладочной работы — обработка символов, расширенные точки прерывания и дизассемблирование — выполняется на стороне 1386KD. Одно время Windows NT 4 Device Driver Kit (DDK) документировал протокол, используемый в кабеле нуль-модема. Однако Microsoft больше его не документирует.

Мощь 1386KD очевидна, если посмотреть на все команды, которые он предлагает для доступа к внутреннему состоянию Windows 2000. Знание механизма работы драйверов устройств в Windows 2000 поможет программисту следить за выводом многих команд. Не смотря на всю свою мощь, i386KD почти никогда не применяется, потому что это консольное приложение, которое очень утомительно использовать для отладок исходного уровня.

Отладчик Win DBG

WinDBG — это отладчик, который поставляется в составе Platform SDK. Можно также загрузить его с http://msdn.microsoft.com/developer/sdVdebidt.asp. Это гибридный отладчик, который может работать как в режиме ядра, так и в режиме пользователя, но WinDBG не позволяет отлаживать программы пользовательского режима и режима ядра одновременно. Для отладки в режиме ядра WinDBG предоставляет всю мощь отладчика i386KD, но с более легким в использовании внешним GUI-интерфейсом, который значительно упрощает отладку на уровне исходного кода.


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

Как отладчик пользовательского режима WinDBG великолепен, и я настоятельно рекомендую установить его, если вы этого еще не сделали. WinDBG — более мощный отладчик, по сравнению с отладчиком Visual C++, в том отношении, что он показывает намного больше информации в процессе отладки. Однако за эту мощь надо платить: WinDBG труднее использовать, чем отладчик Visual C++. Тем не менее я посоветовал бы потратить время на изучение WinDBG. Это может окупиться намного более быстрым исправлением ошибок, чем с помощью отладчика Visual C++. Лично я провожу, в среднем, приблизительно 70% времени в отладчике Visual C++, а остальное — в WinDBG.

При первом запуске WinDBG можно заметить, что у него имеется специальное окно команд (Command). Подобно отладчику Visual C++, WinDBG выполняет отладку на уровне исходного кода. Однако реальная мощь WinDBG проявляется в интерфейсе окна его команд. Почувствовав удобство различных команд, вы вскоре обнаружите, что с помощью окна Command можно выполнять отладку намного быстрее, чем с помощью только GUI-интерфейса.

Возможности окна команд WinDBG возрастают, если добавить в него свои собственные команды, называемые WinDBG-расширениями. В то время как отладчик Visual C++ не очень гибок при остановке отлаживаемого процесса, в WinDBG имеется полный набор API-функций, позволяющих использовать все функциональные возможности отладчика, включая дизассемблер, символьную машину и машину трассировки стека. Дополнительную информацию о WinDBG-расширениях можно найти в разделе "Debugger Extension" (Расширение отладчика) на MSDN.

В некоторых ситуациях лучше использовать WinDBG, а не отладчик Visual C++, потому что WinDBG поддерживает более мощный набор точек прерывания. Благодаря окну команд можно связывать команды с точкой прерывания. Это позволяет вывести отладку на совершенно новый уровень. Например, отлаживая программный модуль и выполняя многочисленные вызовы, очень удобно видеть значения каждого вызова без остановки приложения.



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

В дополнение к лучшей расширяемости, чем у отладчика Visual C++, WinDBG имеет еще одно свойство, которое обязательно нужно рассмотреть, если приложение выполняется в Windows 2000 или Windows NT 4: WinDBG может читать файлы дампов пользовательского режима, созданные программой Dr. Watson. Это означает, что можно загружать в отладчик точное состояние программы во время аварийного прерывания, подробно его просмотреть и проанализировать. Дополнительную информацию о том, что нужно для установки этого свойства, можно найти в колонках "Bugslayer" журнала "Microsoft Systems Journal" ж декабрь 1999 и январь 2000.

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

Отладчик SoftICE

SoftICE — коммерческий отладчик режима ядра фирмы Compuware NuMega. Это единственный известный мне коммерческий отладчик подобного типа, а также единственный отладчик режима ядра, который может работать на одной машине. Однако, в отличие от других отладчиков этого режима, SoftICE превосходно выполняет работу по отладке программ пользовательского режима. Как упоминалось ранее, отладчики режима ядра находятся между CPU и операционной системой. При отладке программы пользовательского режима SoftICE также находится между CPU и операционной системой и, таким образом, останавливает в случае необходимости всю операционную систему целиком.

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


Но зададим такой вопрос: " Что случится, если нужно отладить код, работающий с таймером?" Если вы используете такую API-функцию, как SendMessageTimeout, то можете легко попасть в ситуацию тайм-аута, когда выполняете пошаговый проход другого потока с помощью типового GUI-отладчика. Работая с SoftICE, можно "шагать" везде где угодно, потому что таймер, с которым имеет дело SendMessageTimeout, не будет выполняться, пока вы работаете под SoftICE. SoftICE — единственный отладчик, который позволяет эффективно отлаживать многопоточные приложения. Сам факт, что SoftICE останавливает всю операционную систему, когда он активен, означает, что решение таймерных проблем становится гораздо более легким.

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

SoftICE действительно превосходен, когда нужно остановить приложение во время его обращения к определенному участку памяти. SoftICE использует преимущества отладочных регистров процессоров 1386, которые предоставляют в ваше распоряжение 4-байтовые участки памяти для размещения прерываний. Прерывание можно делать, когда выполняется чтение, запись или выполнение определенного участка памяти. Поскольку SoftICE может использовать до четырех аппаратных точек прерывания, то во время обращений к областям памяти приложение пользователя выполняется с максимальной скоростью. Это свойство чрезвычайно полезно для прослеживания проблем, связанных с порчей памяти.

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


Хотя в отладчиках 1386KD и WinDBG довольно много таких команд, в SoftICE их намного больше. В SoftICE можно просматривать почти все — от состояния всех событий синхронизации до полной HWND-информации и расширенной информации о любом потоке в системе.

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

Общий вопрос отладки

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

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

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion \AeDebug

В Windows 98 эта информация "прописана" в секции [AeDebug] файла WIN.INI. Если под указанным ключом (или в секции) нет никаких параметров, Windows 2000 сообщает только адрес аварийного сбоя. Если сбой произошел из-за нарушения доступа, Windows 2000 сообщает также адрес той области

памяти, которую процесс не смог прочитать или записать. В Windows 98 выводится стандартное диалоговое окно аварийного сбоя, и если вы нажмете в ней кнопку Details, то будет выведен список с именем модуля, адресом сбоя и содержимым регистров на момент сбоя.

Под указанным ключом (Windows 2000) или в секции [AeDebug] (Windows 98) возможно размещение трех строчных параметров:

• Auto

• Debugger

• UserDebuggerHotKey

Если значение параметра Auto установить в 0, то операционная система будет генерировать стандартную диалоговую панель аварийного сбоя и активизирует в ней кнопку Cancel (Windows 2000) или Debug (Windows 98) на тот случай, если вы сами захотите присоединить отладчик. Если параметр Auto установить в 1, то отладчик стартует автоматически. Параметр Debugger указывает отладчик, который операционная система запустит на сбойном приложении.


Единственное требование к этому отладчику: он должен поддерживать присоединение к процессу. Параметр UserDebuggerHotKey указывает клавишу, которая будет использоваться для быстрого перехода к отладчику. Чтобы уточнить процедуру установки этого параметра, обратитесь к разделу "Быстрые клавиши прерываний" этой главы.

Раздел реестра AeDebug можно устанавливать вручную, но только утилита Dr. Watson (ее версия в Windows 2000), WinDBG и отладчик Visual C++ позволяют устанавливать в нем различные значения параметров. Dr. Watson и WinDBG используют ключ командной строки -I, который определяет их в качестве отладчика по умолчанию. Чтобы установить отладчик Visual C++ в качестве отладчика, который будет вызывать операционная система, включите флажок Just-In-Time Debugging на вкладке Debug диалогового окна Options (которое открывает команда ToolsjOptions... в IDE Microsoft Visual C++).

Если заглянуть в раздел реестра AeDebug, то можно увидеть, что значение, которое введено для параметра Debugger, выглядит точно так же, как строка, передаваемая в API-функцию wsprintf:

drwtsn32 -p %d -e %d -g

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



Отладчики режима пользователя


Отладчики режима пользователя предназначены для отладки любого приложения, выполняющегося в режиме пользователя, включая любые GUI-программы, а также такие не совсем обычные приложения, как службы (services) Windows 2000. В общем случае, отладчики этого типа поддерживают графический интерфейс пользователя (GUI1). Главный признак таких отладчиков — они используют отладочный интерфейс прикладного программирования (отладочный API) Win32. Поскольку операционная система помечает подчиненный отладчик как "выполняющийся в специальном режиме", то, чтобы выяснить, выполняется ли ваш процесс под отладчиком, можно использовать API-функцию IsDebuggerPresent.

При поставке отладочного API Win32 подразумевается следующее соглашение: раз процесс выполняется под отладочным API (и делает его, таким образом, подчиненным процессом), основной отладчик не может отделиться от данного процесса. Эти симбиозные отношения означают, что если основной отладчик завершает работу, то завершается также и подчиненный отладчик. Основной отладчик ограничивается отладкой только подчиненного отладчика и любых порожденных им процессов (если основной отладчик поддерживает процессы-потомки).

 GUI — Graphical User Interface. — Пер.

Для интерпретируемых языков и исполнительных (run-time) систем, которые используют подход виртуальной машины, полную отладочную среду обеспечивают сами виртуальные машины, и они не используют отладочный API Win32. Вот некоторые примеры таких типов сред: виртуальные Java-машины (JVM) фирм Microsoft или Sun, среда сценариев для Web-приложений фирмы Microsoft, и интерпретатор р-кода в системе Microsoft Visual Basic.

Мы доберемся до отладки в Visual Basic в главе 7, но знайте, что интерфейс р-кода Visual Basic не документирован. Не будем вникать в отладочные интерфейсы Java и сценариев, эти темы выходят за рамки данной книги. Дополнительную информацию по отладке и профилированию Microsoft JVM ищите в разделе "Debugging and Profiling Java Applications" (Отладка и профилирование приложений Java) на MSDN. Набор таких интерфейсов весьма богат и разнообразен и позволяет полностью управлять работой JVM. Информацию о написании отладчика сценария можно найти в разделе MSDN "Active Script Debugging API Objects" (Активные объекты отладочных API-сценариев). Подобно JVM, объекты отладчика сценариев обеспечивают богатый интерфейс для сценариев доступа и документирования.

Отладочный API Win32 использует удивительно много программ. К ним относятся: отладчик Visual C++, который подробно рассматривается в главах 5 и 6; отладчик Windows (WinDBG), который обсуждается в следующем разделе (посвященном отладчику режима ядра); программа BoundsChecker фирмы Compuware NuMega; программа Platform SDK HeapWalker; программа Platform SDK Depends; отладчики Borland Delphi и C++ Builder, а также символический отладчик NT Symbolic Debugger (NTSD). Я уверен, что их намного больше.



Windows 2000 содержит набор специальных



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



Когда приложение стартует под отладчиком,



Когда приложение стартует под отладчиком, Windows 2000 включает проверку специальной отладочной области динамического распределения памяти (debug heap) операционной системы. Эта область не совпадает с отладочной heap-памятью С-библиотеки времени выполнения. Код этой области создается с помощью специальной API-функции — HeapCreate. О динамической области ("куче") С-библиотеки времени выполнения речь пойдет в главе 15. Поскольку отладочные heap-области используются процессами Windows 2000 довольно широко, с их информацией приходится сталкиваться часто, вот почему так важно поподробнее ознакомиться с ними. Если вы подключаете отладчик к приложению, а не стартуете приложение под отладчиком, то проверка отладочной heap-области в операционной системе Windows 2000 активизирована не будет.

Если проверка отладочной динамической области включена, то приложение будет выполняться немного медленнее, потому что когда в приложении вызывается функция HeapFree, то приходится дополнительно проверять корректность "кучи". В листинге 4-1 показан пример программы, которая портит память. Если эта программа выполняется под отладчиком, то нетрудно заметить, что функция DebugBreak вызывается дважды (на первом же вызове функции HeapFree). Ниже показан вывод, по которому видно, что при работе с heap-областью возникли некоторые проблемы.

HEAP[Heaper.exe]: Heap block at 00441E98 modified at 00441EAA past

requested size of a

HEAP[Heaper.exe]: Invalid Address specified to

RtlFreeHeapt 440000, 441eaO)

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

Если вы используете в Windows 2000 свои собственные2 heap-области, то для того чтобы получить более полный диагностический вывод, можно включить несколько дополнительных флажков. Для этого в Platform SDK включена небольшая утилита GFLAGS.EXE. С ее помощью можно установить несколько глобальных флажков, проверяемых операционной системой при первом запуске приложения. Установки, выполненные этой утилитой для файла HEAPER.EXE, показаны на рис. 4.1. 
 Heap - "куча", область динамически распределяемой памяти. — Пер.

Что вполне возможно, т. к. не только отладчики, но и любые приложения могут вызывать функцию HeapCreate. — Пер.

 Этот набор инструментов вы можете найти на сопровождающем CD. — Пер.

Это загрузочный модуль, частью которого является функция, показанная в листинге 4-1. — Пер.

Многие опции кнопок System Registry и Kernel Mode переключателя Destination окна Global Flags являются глобальными. Нужно проявлять особую осторожность при их установке, потому что они могут оказать решающее влияние на производительность системы. Установка переключателя Destination в положение Image File Options намного безопаснее, потому что все установки влияют только на один модуль (имя которого указано в соседнем поле Image File Name).

Рис. 4.1. Вывод программы GFLAGS.EXE

Листинг 4-1. Пример разрушения heap-области Windows 2000 

void main(void)

// Создать heap-область операционной системы. 

HANDLE hHeap = HeapCreate ( 0, 128, 0) ;

// Распределить память для блока размером в 10 байтов. 

LPVOID pMem = HeapAlloc ( hHeap, 0, 10);

// Записать 12 байт в 10-байтовый блок (переполнение heap-области).

 memset ( pMem, OxAC, 12);

// Распределить новый блок размером 20 байт. 

LPVOID pMem2 = HeapAlloc ( hHeap, 0, 20);

// Записать 1 байт во второй блок.

char * pUnder = (char *)( (DWORD)pMem2 - 1);

*pUnder = 'P';

// Освободить первый блок. Это обращение к HeapFree будет

 // инициировать точку прерывания в коде отладочной heap-области

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

 HeapFree ( hHeap, 0, pMem);

// Освободить второй блок. Заметим, что этот вызов не будет

 // выдавать соообщения о неполадке

 HeapFree ( hHeap, 0, pMem2);

// Освободить фиктивный блок. Заметим, что этот вызов не будет

 // выдавать сообщения о неполадке

 HeapFree ( hHeap, О, (LPVOID)Oxl); HeapDestroy ( hHeap); 

}

Если установить те же флажки, что на рис. 4.1, и повторить выполнение HEAPER.EXE, то будет получен следующий, более многословный вывод:



PAGEHEAP: process 0x490 created debug heap 00430000

(flags 0xl, 50, 25, 0, 0) 

PAGEHEAP: process 0x490 created debug heap 00CF0000

(flags Oxl, 50, 25, 0,- 0) 

PAGEHEAP: process 0x490 created debug heap 01600000

(flags Oxl, 50, 25, 0, 0)

  PAGEHEAP: Tail fill corruption detected:

 Allocation at 0x01606FF0

 Requested size 0x0000000A 

Allocated size 0x00000010

 Corruption at Ox01606FFA 

PAGEHEAP: Attempt to reference block which is not allocated

Содержимое листинга объясняют названия флажков, установленных панелью Global Flags.

Обсуждая программу GFLAGS.EXE, я хочу указать на одну очень полезную опцию — Show Loader Snaps. Если вы установите этот флажок и выполните приложение, то увидите то, что называют снимком (snap) приложения, в котором видно, где Windows 2000 загружает DLL-файлы и как она начинает организацию импорта. Если необходимо точно видеть, что делает загрузчик Windows 2000 при загрузке приложения (особенно в том случае, когда в нем обнаружена проблема), то включение этой опции может оказаться весьма полезным мероприятием. Дополнительную информацию по снимкам загрузчика можно получить в колонке Мэта Пьетрека "Under the Hood" в сентябрьском выпуске Microsoft Systems Journal за 1999 год.



В этой главе приведен краткий



В этой главе приведен краткий обзор механизмов функционирования отладчиков. Изучая различные инструментальные средства, можно существенно улучшить их использование. Здесь был представлен Win32 Debugging API (отладочный API 32-разрядных операционных систем Windows) и некоторые поддерживающие системы, такие, например, как символьные машины. Вы также узнали о существовании некоторых других отладчиков (кроме отладчика Visual C++). Наконец, приведен пример полного отладчика — WDBG, который хорошо иллюстрирует работу отладчика.
Думаю, что, внимательно просмотрев код WDBG, вы согласитесь, что отладчики выполняют такую же кропотливую работу с данными, как и любые серьезные программы. Самые большие трудности, которые нужно преодолеть при написании отладчика для Win32, заключаются в том, что существующие символьные машины обрабатывают только общие (public) функции, глобальные переменные и строки исходных и двоичных файлов. Без локальных переменных, параметров и типов трудно сделать отладчик почти таким же всесторонним, как отладчик Visual C++ или WinDBG.

Таблицы символов, символьные машины и проход стека


Реальное мастерство в написании отладчиков опирается на символьные машины (т. е. на программный код, который манипулирует таблицами символов). Отладка на традиционном уровне языка ассемблера интересна в течение первой пары минут работы, но быстро надоедает. Таблицы символов (symbol tables), называемые также символами отладки (debugging symbols) — это то, что превращает шестнадцатеричные числа в строки, имена функций и имена переменных исходных файлов. Таблицы символов содержат, кроме того, информацию о типах, которую использует ваша программа. Эта информация позволяет отладчику отображать на экране "сырые" данные как структуры и переменные, которые вы определили в своей программе. Иметь дело с современными таблицами символов трудно, потому что наиболее часто используемый их формат — PDB (Program DataBase — База данных программы), не документирован, и владельцы прав не планируют его документировать. К счастью, можно получить по крайней мере частичный доступ к таблицам символов.

Форматы символов отладки

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

SYM — самый старый формат, который применялся во времена MS-DOS и 16-разрядных Windows. В настоящее время SYM-формат употребляется только для отладочных символов в Windows 98. SYM-формат применяется лишь потому, что большая часть основного ядра операционной системы все еще остается 16-разрядным кодом. Единственный отладчик, активно использующий символы этого формата, — это WDEB386.

COFF (Common Object File Format — общий формат объектных файлов) :дин из первых форматов таблиц символов, который был представлен в Windows NT 3.1 (первой версии Windows NT). Команда разработчиков Windows NT имела опыт в разработке операционной системы и хотела загружать Windows NT с помощью некоторых существующих инструментов. Формат COFF является частью большой спецификации, которой пользовались различные поставщики UNIX-систем, пытавшиеся создать общие форматы двоичных файлов.
Хотя в WINNT.H находится полная спецификация COFF-символов, инструменты Microsoft генерируют только некоторые еe части — public-функции и глобальные переменные. В Microsoft привыкли поддерживать исходную и строчную информацию, но постепенно отошли от формата COFF в пользу более современных форматов символьных таблиц.
Формат С7 или CodeView появился еще во времена MS-DOS как часть системы программирования Microsoft C/C++ версии 7. Возможно, вы слышали название "CodeView". Это имя старого отладчика Microsoft. Формат С7 был модифицирован, чтобы поддерживать операционные системы Win32, и тетерь можно генерировать этот формат, запуская компилятор CL.EXE из командной строки с ключом /Z7 или выбирая пункт С7 Compatible1 из раскрывающегося списка Debug Info на вкладке C/C++ диалогового окна Project Settings.
Здесь и далее речь идет об интерфейсе с IDE Microsoft Visul C++. — Пер
Чтобы включить параметр компоновщика /PDB:NONE, на вкладке  Link диалогового окна Project Settings выключите в категории Customize  флажок Use Program Database. Отладчики WinDBG и Visual C++ поддерживают как полную отладку на уровне исходного кода, так и построчную отладку в формате С7. Компоновщик добавляет символьную информацию к двоичному файлу уже после того, как он выполнит компоновку, потому отладочные символы формата С7 содержатся внутри выполняемого модуля. Добавление отладочных символов более чем вдвое увеличивает объем двоичного файла. Для того чтобы узнать, содержит ли двоичный файл отладочную информацию формата С7, нужно открыть его в шестнадцатеричном редакторе и, переместившись к его концу, просмотреть содержимое последних строк. Если будет обнаружена строка с последовательностью символов "NB11", значит файл содержит отладочную информацию формата С7.
Спецификацию С7 можно найти на MSDN в разделе "VC5.0 Symbolic Debug Information". Спецификация перечисляет только необработанную байтовую структуру и определения типов. Тем, кто захочет увидеть фактические определения типов на С, рекомендуем просмотреть исходный код программы Dr.


Watson на компакт-дисках MSDN. В каталоге \ Include этой программы имеется несколько файлов заголовков старого формата С7. Хотя эти файлы значительно устарели, они могут дать некоторое представление о том, на что похожи эти структуры.
При желании, конечно, можно использовать формат С7, но лучше этого не делать. Отказаться от использования формата С7 нужно по двум причинам. Во-первых, он автоматически выключает инкрементную компоновку, из-за чего катастрофически увеличивается время компоновки. Во-вторых, значительно возрастает размер двоичных файлов. Можно убрать символьную информацию с помощью программы REBASE.EXE, но существуют такие форматы (например, PDB), которые удаляют ее автоматически.
PDB — наиболее общий из используемых сегодня символьных форматов, поддерживает как Visual C++, так и Visual Basic. В отличие от формата С7, PDB-символы сохраняются в отдельном файле или файлах, в зависимости от того, как приложение скомпоновано. По умолчанию, загрузочный файл Visual C++ 6 компонуется с ключом /PDBTYPE:SEPT, который помещает информацию о типах в файл VC60.PDB, а сами символы — в файл <имя-двоичного-файла>.РОВ. Отделение информации о типах от символов отладки ускоряет компоновку и требует меньше места на диске. Однако в документации указано, что если вы строите двоичный файл, который могли бы j отлаживать другие, то, чтобы вся информация о типах и символы отладки были сведены в единый PDB-файл, нужно указать ключ /PDBTYPE:CON. К счастью, Visual Basic автоматически использует этот ключ.
Чтобы посмотреть, содержит ли двоичный PDB-файл символьную информацию, откройте его в шестнадцатеричном редакторе и перейдите к концу файла. Вы увидите маркер отладочной информации. Если маркер начинает>:я с символов "NB10" и заканчивается полным путем к PDB-файлу, построенному во время компоновки, то двоичный файл включает PDB-символы. Отладочный формат PDB внутренне напоминает формат С7. Однако компания Microsoft оптимизировала этот формат для инкрементной компоновки.


К сожалению, интерфейсы низкого уровня с PDB-файлами являются собственностью фирмы Microsoft и не опубликованы.
DBG — файлы этого формата уникальны в том смысле, что, в отличие от файлов других символьных форматов, их создает не компоновщик. В них хранятся отладочные символы форматов COFF или С7. В DBG-файлах применяются структуры, определенные файловым форматом РЕ (Portable Executable), который использует все выполняемые файлы в операционных системах Win32. DBG-файлы создаются с помощью утилиты REBASE.EXE, которая извлекает из программного модуля отладочную информацию форматов COFF или С7 и помещает ее в DBG-файл. Нет никакой необходимости выполнять REBASE.EXE для модуля, который был построен с использованием PDB-файлов, потому что символы уже отделены от модуля. Желающим изучить методику создания DBG-файлов следует прочитать MSDN-доку-ментацию по REBASE.EXE. Microsoft распространяет отладочные символы операционной системы в DBG-файлах, а в Windows 2000 также включены и PDB-файлы. Не надейтесь, что отладочные символы операционной системы включают все, что нужно, скажем, для ее обратного проектирования. Учтите, что DBG-файлы содержат только общую и глобальную информацию. Однако их использование может значительно облегчить вашу работу с информацией окна Disassembly.
Если вы заинтересовались символьными машинами и начинаете исследовать возможности их программирования, то рано или поздно встретите символы еще одного типа — ОМАР. Символы этого типа появляются только в некоторых приложениях Microsoft. Их можно иногда встретить при получении дампа символьной информации с помощью утилиты DUMPBIN.EXE, запускаемой с параметром /SYMBOLS. (DUMPBIN.EXE распространяется вместе с системой программирования Visual C++.) Символьный формат ОМАР совершенно не документирован. Как уже говорилось, Microsoft использует специальный внутренний инструмент, который реорганизует компилированный двоичный файл так, чтобы поместить наиболее часто вызываемый код в начало файла. ОМАР-символы имеют какое-то отношение к тем отладочным символам, которые принимают во внимание этот послекомпоновочный шаг.


Подобную оптимизацию выполняет и программа Working Set Tuner (WST), поставляемая вместе с Platform SDK. WST работает на функциональном уровне, не проникая внутрь функций, тогда как инструмент Microsoft опускается до так называемого уровня базовых блоков. В следующем кодовом фрагменте базовый блок обозначен стрелками:
if ( TRUE = blsError)
{ <- Начало базового блока.
// Обработка ошибок. 
} <- Конец базового блока.
Инструмент Microsoft перемещает обработчик ошибки в конец двоичного файла, так что только наиболее общий код размещается в его начале. ОМАР-символы оказываются некоторой разновидностью адресных записей (fixup1) для главных символов, потому что инструмент Microsoft манипулирует двоичным файлом уже после того, как тот был построен.
Доступ к символьной информации
Для доступа к символьной информации можно использовать символьную машину DBGHELP.DLL фирмы Microsoft. DBGHELP.DLL может читать символьные форматы PDB, COFF и С7. В прошлом символьная машина была в IMAGEHLP.DLL, но Microsoft умудрился вывести ее из ядра системы и поместил в DLL, которую было легче обновлять. Для работы с программами, которые использовали символьную машину, встроенную в IMAGEHLP.DLL, в него все еще включены списки экспортируемых функций символьной машины (symbol engine exports). Новая IMAGEHLP.DLL пересылает соответствующие функции в DBGHELP.DLL. Во время написания этой книги MSDN-документация символьной машины еще оставалась частью библиотеки IMAGEHLP.DLL.
Символьная машина DBGHELP.DLL преобразует адрес в имя ближайшей общей функции или глобальной переменной. Она может также работать и наоборот, отыскивая адрес по имени конкретной функции. Наконец, она может отыскивать имя исходного файла и номер строки для конкретного адреса. Символьная машина DBGHELP.DLL не поддерживает ни поиск параметров или локальных переменных, ни оценку их типов. Как будет показано позже, используя только эти ограниченные функциональные возможности, можно построить некоторые превосходные утилиты, оказывающие неоценимую помощь при поиске многочисленных проблем в приложениях.


В WDBG был использован простой класс-оболочка (язык C++), который показан (файл SYMBOLENGINE.H) в листинге 4-7. Первоначально этот класс был написан как часть библиотеки BUGSLAYERUTIL.DLL. Это значительно урезанный вариант API символьной машины DBGHELP.DLL, но он обеспечивает и некоторые дополнительные возможности для решения проблем, с которыми приходится сталкиваться в старых версиях символьной машины IMAGEHLP.DLL. Исходный код SYMBOLENGINE.H представлен на тот случай, если придется использовать этот класс со старыми символьными машинами IMAGEHLP.DLL.
 Fixup — адресная запись (запись, генерируемая ассемблером для редактора связей в отношении каждого адреса, который не может быть определен). — Пер.
 Листнг4-7.Файл SYMBOLENGINE.H
/*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
"Debugging Applications" (Microsoft Press)
Copyright (c) 1997-2000 John Robbins — All rights reserved.
- - - - - - - -- - - - - - - - - - - - - - - - - - - - - - 
Этот класс - сокращенный вариант символьной машины DBGHELP.DLL. Эн охватывает только те функции, которые имеют уникальное значение HANDLE-дескриптора. Остальные функции символьной машины DBGHELP.DLL являются глобальными, поэтому они не включены в этот класс.
 - - - - - -- - - - - - - - - - - - - - - - - - - - - - - - - - 
Макроопределения компиляции:
DO_NOT_WORK_AROUND_SRCLINE_BUG — 
При определении этой константы класс не будет работать с ошибкой SymGetLineFromAddr, когда поиски PDB fMile терпят неудачу уже после первого поиска.
USE_BUGSLAYERUTIL — 
При определении этой константы данный класс будет применять другой метод инициализации символьной машины — BSUSymlnitialize из BUGSLAYERUTIL.DLL. Кроме того, флажок захвата процесса начнет работать для всех 32-разрядных Windows-систем ОС. При использовании этого определения кроме SYMBOLENGINE.Н нужно также включать и файл BUGSLAYERUTIL.H.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
#ifndef _SYMBOLENGINE_H


#define _SYMBOLENGINE_H
// Можно включить либо IMAGEHLP.DLL, либо DBGHELP.DLL.
# include "imagehlp.h"
#include <tchar.h>
// Включайте эти директивы в случае, если пользователь забывает
// про компоновку соответствующих библиотек
#pragma comment (lib, "dbghelp. lib")
#pragma comment (lib, "version, lib")
// Грандиозная идея создания классов-оболочек на структурах, которые
// имеют размерные поля, пришла от коллеги-журналиста из MSJ Поля
// (Paul DiLascia). Спасибо, Поль!;
// Я не включаю в класс константу IMAGEHLP_SYMBOL, потому что это
// структура переменного размера.
// Класс-оболочка IMAGEHLP_MODULE
struct CImageHlp_Module : public IMAGEHLP_MODULE
{
CImageHlp_Module ()
{
memset ( this, NULL, sizeof ( IMAGEHLP_MODULE));
SizeOfStruct = sizeof ( IMAGEHLP_MODULE); 
}
};
// Класс-оболочка IMAGEHLP_LINE 
struct CImageHlp_Line : public IMAGEHLP_LINE 
{
CImageHlp_Line () 
{
memset ( this, NULL, sizeof ( IMAGEHLP_LINE)); 
SizeOfStruct = sizeof ( IMAGEHLP_LINE); 
}
};
// Класс символьной машины class CSymbolEngine
{
/*- - - - - - - - - - - - - - - - - - - - - - - - - -
Public-конструктор и деструктор 
- - - -- - - - - - - - - - - - - - - - - - - - - - - - - - */
public :
// Чтобы использовать этот класс, вызовите метод Symlnitialize для
// инициализации символьной машины и затем применяйте другие методы
// вместо соответствующих функций из DBGHELP.DLL
CSymbolEngine ( void)
{
}
virtual -CSymbolEngine ( void)
{
}
/ *- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Вспомогательные информационные public-функции
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
public :
// Возвращает версию используемого файла DBGHELP.DLL.
// Чтобы преобразовать возвращаемые значения в читаемый формат,
// применяется функция:
// wsprintf ( szVer ,
// _Т ( "%d.%02d.%d.%d"),
// HIWORD ( dwMS)
// LOWORD ( dwMS)
// HIWORD ( dwLS)
// LOWORD ( dwLS) );


// Параметр szVer будет содержать строку вида: 5.00.1878.1
BOOL GetlmageHlpVersion ( DWORD & dwMS, DWORD & dwLS)
{
return( GetlnMemoryFileVersion ( _T ( "DBGHELP.DLL"),
dwMS , 
dwLS ) ) ; 
}
BOOL GetDbgHelpVersion ( DWORD & dwMS, DWORD & dwLS)
 {
return( GetlnMemoryFileVersion ( __T ( "DBGHELP.DLL"),
dwMS , 
dwLS ) ) ; 
}
// Возвращает версию DLL-файлов, читающих PDB.
 BOOL GetPDBReaderVersion ( DWORD & dwMS, DWORD & dwLS)
 {
// Первым проверяется файл MSDBI.DLL.
if ( TRUE == GetlnMemoryFileVersion ( _T ( "MSDBI.DLL"),
dwMS ,
 dwLS ) )
 {
return ( TRUE); 
}
else if.( TRUE == GetlnMemoryFileVersion ( _T ( "MSPDB60.DLL"),
dwMS
dwLS ) ) 
{
return ( TRUE); 
}
// Теперь пришла очередь проверить MSPDB50.DLL. 
return ( GetlnMemoryFileVersion ( _T ( "MSPDB50.DLL"),
dwMS
dwLS ) ) ;
 }
// Рабочая функция, используемая двумя предшествующими функциями.
 BOOL GetlnMemoryFileVersion ( LPCTSTR szFile,
DWORD & dwMS , 
DWORD & dwLS ) 
{
HMODULE hlnstlH = GetModuleHandle ( szFile);
// Получить полное имя файла загруженной версии
TCHAR sz!mageHlp[ MAX_PATH ];
GetModuleFileName ( hlnst-IH, szImageHlp, MAX_PATH);
dwMS = 0;
dwLS = 0;
// Получить размер информации о версии.
DWORD dwVerlnfoHandle;
DWORD dwVerSize;
dwVerSize = GetFileVersionlnfoSize ( szImageHlp ,
SdwVerlnfoHandle ); 
if ( 0 == dwVerSize) 
{
return ( FALSE); 
}
// Получили размер информации о версии, теперь получим
 // саму информацию.
LPVOID IpData = (LPVOID)new TCHAR [ dwVerSize ]; 
if ( FALSE == GetFileVersionlnfo ( szImageHlp ,
dwVerlnfoHandle , dwVerSize , IpData )) 
{
delete [] IpData; return ( FALSE);
 }
VS_FIXEDFILEINFO * IpVerlnfo; 
UINT uiLen;
BOOL bRet = VerQueryValue ( IpData ,
_T ( "\\")
  (LPVOID*)SlpVerlnfo, &uiLen ) ;


 if ( TRUE == bRet)
 {
dwMS = lpVerInfo->dwFileVersionMS;
 dwLS = lpVer!nfo->dwFileVersionLS;
 }
delete [] IpData; return ( bRet);
}
 /*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public-методы инициализации и чистки
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
public :
BOOL Symlnitialize ( IN HANDLE hProcess ,
 IN LPSTR UserSearchPath, 
IN BOOL flnvadeProcess )
 {
m_hProcess = hProcess;
return ( ::Symlnitialize ( hProcess ,
UserSearchPath, fInvadeProcess ));
 }
#ifdef USE_BUGSLAYERUTIL
BOOL BSUSymlnitialize ( DWORD dwPID ,
HANDLE hProcess ,
 PSTR UserSearchPath,
 BOOL flnvadeProcess ) 
{
m_hProcess = hProcess;
return ( ::BSUSymlnitialize ( dwPID ,
hProcess , UserSearchPath, flnvadeProcess ));
 }
#endif // USE_BUGSLAYERUTIL 
BOOL SymCleanup ( void) 
{
return ( ::SymCleanup ( m_hProcess)) ;
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Public-методы манипуляций с модулями
- - - - - - - - - - - - - - - - - - - - - - - - - - * /
public :
BOOL SymEnumerateModules ( IN PSYM_ENUMMODULES_CALLBACK
EnumModulesCallback, 
IN PVOID UserContext) 
{
return ( ::SymEnumerateModules ( m_hProcess ,
EnumModulesCallback , 
UserContext )) ; 
}
BOOL SymLoadModule { IN HANDLE hFile , 
IN PSTR ImageName , 
IN PSTR ModuleName , 
IN DWORD BaseOfDll ,
 IN DWORD SizeOfDll ) 
{
return ( ::SymLoadModule ( m_hProcess ,
hFile
ImageName ,
 ModuleName , 
BaseOfDll SizeOfDll )); 

BOOL EnumerateLoadedModules ( IN PENUMLOADED_MODULES_CALLBACK
EnumLoadedModulesCallback,
 IN PVOID UserContext )
{
return ( ::EnumerateLoadedModules ( m_hProcess ,
EnumLoadedModulesCallback, 
UserContext )); 
}
BOOL SymUnloadModule ( IN DWORD BaseOfDll) 
{
return ( ::SymUnloadModule ( m_hProcess, BaseOfDll)); 

BOOL SymGetModulelnfo ( IN DWORD dwAddr


OUT PIMAGEHLP__MODULE Modulelnfo ) 
{
return ( ::SymGetModulelnfo ( m_hProcess ,
dwAddr ,
 Modulelnfo )); 
}
DWORD SymGetModuleBase ( IN DWORD dwAddr) 
{
return ( ::SymGetModuleBase ( m_hProcess, dwAddr));
 }
 /*- - - - - - - - - - - - - - - - - - - - - - - - - - 
Public-методы манипуляций с символами
- - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
public :
BOOL SymEnumerateSymbols ( IN DWORD BaseOfDll,
IN PSYM_ENUMSYMBOLS_CALLBACK
EnumSymbolsCallback,
IN PVOID UserContext) 
{
return ( ::SymEnumerateSymbols ( m_hProcess ,
BaseOfDll
EnumSymbolsCallback, 
UserContext )); 
}
BOOL SymGetSymFromAddr ( IN DWORD dwAddr ,
OUT PDWORD pdwDisplacement, 
OUT PIMAGEHLP_SYMBOL Symbol ) 
{
return ( ::SymGetSymFromAddr ( m_hProcess ,
dwAddr , 
pdwDisplacement , 
Symbol )); 
}
BOOL SymGetSymFromName ( IN LPSTR Name ,
OUT PIMAGEHLP_SYMBOL Symbol )
{
return ( ::SymGetSymFromName ( m_hProcess,
Name ,
 Symbol }}; 
}
BOOL SymGetSymNext ( IN OUT PIMAGEHLP_SYMBOL Symbol)
 {
return ( ::SymGetSymNext ( m_hProcess, Symbol)); 
}
BOOL SymGetSymPrev ( IN OUT PIMAGEHLP_SYMBOL Symbol)
 {
return ( ::SymGetSymPrev ( m_hProcess, Symbol));
}
 /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Public-метод манипуляций с исходной строкой
- - - -- - - - - - - - - - - - - - - - - - - - - - - - - */
public :
BOOL SymGetLineFromAddr ( IN DWORD dwAddr ,
OUT PDWORD pdwDisplacement, OUT PIMAGEHLP_LINE Line ) 
{
# ifde f DO_NOT_WORK_AROUND_SRCLINE_BUG
// Просто передайте значения, возвращенные main-функцией
 return ( ::SymGetLineFromAddr ( m_hProcess ,
dwAddr 
, pdwDisplacement, 
Line ) ) ;
#else
// Проблема в том, что символьная машина находит только те адреса
 // исходных строк (после первого поиска), которые попадают точно
 //на нулевые смещения. Чтобы найти строку и возвратить


 // подходящее смещение, я возвращаюсь назад на 100 байт.
 DWORD dwTempDis = 0;
while ( FALSE == ::SymGetLineFromAddr ( m_hProcess ,
dwAddr — dwTempDis ,
 pdwDisplacement , 
Line ) ) 
}
dwTempDis += 1;
if ( 100 == dwTempDis)
{
return ( FALSE); 
}
}
if (0 != dwTempDis)
 {
*pdwDisplacement = dwTempDis; 
}
return { TRUE);
#endif // DO_NOT_WORK_AROUND_SRCLINE_BUG 
}
BOOL SymGetLineFromName ( IN LPSTR ModuleName ,
IN LPSTR FileName ,
 IN DWORD dwLineNumber ,
 OUT PLONG plDisplacement , 
IN OUT PIMAGEHLP_LINE Line ) 
{
return ( ::SymGetLineFromName ( m_hProcess ,
ModuleName , 
FileName , 
dwLineNumber ,
plDisplacement , 
Line ) ) ; 
}
BOOL SymGetLineNext ( IN OUT PIMAGEHLP_LINE Line) 
{
return ( ::SymGetLineNext ( m_hProcess, Line)); 
}
BOOL SymGetLinePrev ( IN OUT PIMAGEHLP_LINE Line) 
{
return ( ::SymGetLinePrev ( m_hProcess, Line));
 }
BOOL SymMatchFileName ( IN LPSTR FileName ,
IN LPSTR Match , 
OUT LPSTR * FileNameStop ,
 OUT LPSTR * MatchStop ) 
{
return ( ::SymMatchFileName ( FileName ,
Match , 
FileNameStop , 
MatchStop ));
 } 
/*- - - - - - - - - - - - - - - - - - - -- - - - - - - - - - - - -
Разные public-члены
- - - - - - - -- - - - - - - - - - - - - - - - - - - - - - - -*/
public :
LPVOID SymFunctionTableAccess ( DWORD AddrBase)
 {
return ( ::SymFunctionTableAccess ( m_hProcess, AddrBase)); 
}
BOOL SymGetSearchPath ( OUT LPSTR SearchPath ,
IN DWORD SearchPathLength ) 
{
return ( ::SymGetSearchPath ( m_hProcess ,
SearchPath , 
SearchPathLength )); 
}
BOOL SymSetSearchPath ( IN LPSTR SearchPath) 
{
return ( ::SymSetSearchPath ( m_hProcess, SearchPath)); 

BOOL SymRegisterCallback ( IN PSYMBOL_REGISTERED_CALLBACK
CallbackFunction,
IN PVOID UserContext ) 
{
return ( ::SymRegisterCallback ( m_hProcess ,


CallbackFunction , 
UserContext ));
}
/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
Защищенные члены данных
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
protected :
// Уникальное значение, которое будет использоваться для этого 
// экземпляра символьной машины. Это значение не должно быть
 // значением актуального процесса, а просто — уникальным значением. 
HANDLE m_hProcess ;
};
ttendif // _SYMBOLENGINE_H
Перед появлением Windows 2000 получить рабочий вариант символьной машины, поддерживаемой фирмой Microsoft, было не так легко. Главная причина трудностей состояла в том, что символьная машина была включена в состав системного файла IMAGEHLP.DLL, который использовали многие программы. Заменить ее новым вариантом внутри загруженного системного файла было невозможно, а получить более новую версию — сложно. Теперь, когда DBGHELP.DLL больше не является системной библиотекой, ее гораздо легче обновлять. Для этого нужно просто следить, чтобы на вашей машине всегда была установлена последняя версия Platform SDK. Ее всегда можно загрузить с сайта www.microsoft.com или получить как часть подписки MSDN. Все исходные коды в этой книге ориентированы на DBGHELP.DLL, поэтому нужно следить за тем, чтобы DBGHELP.DLL была установлена и путь к ее каталогу указан в переменной окружения PATH.
Установка DBGHELP.DLL — только часть дела, ведь для того чтобы загружать символьные файлы, нужно гарантировать их доступность для символьной машины. В случае DBG-файлов символьная машина DBGHELP.DLL будет искать их в следующих местах:
 текущий рабочий каталог приложения, использующего DBGHELP.DLL (а не подчиненный отладчик!);  переменная среды _NT_SYMBOL_PATH;  переменная среды _NT_ALT_SYMBOL_PATH;  переменная среды SYSTEMROOT. Каталоги, на которые указывают переменные среды, должны быть организованы определенным образом. Например, если приложение состоит из ЕХЕ-файла и пары DLL, расположенных в каталоге C:\MyFiles, то под этим каталогом нужно создать следующую структуру подкаталогов:


 C:\MyFiles  C:\MyFiles\Symbols  C:\MyFiles\Symbols\Exe  C:\MyFiles\Symbols\Dll Два последних подкаталога предназначены для размещения соответствующих DBG-файлов приложения.
Единственное различие при работе с PDB-файлами состоит в том, что символьная машина DBGHELP.DLL будет отыскивать PDB-файлы в первичном каталоге приложения и пробовать загружать PDB из этого каталога. Если символьная машина DBGHELP.DLL не сможет загрузить PDB-файлы из этого каталога, то она будет пытаться искать и загружать их так же, как DBG-файлы (т. е. из тех же подкаталогов, которые должны были быть созданы для хранения файлов отладочных символов).
 То есть для каждого типа рабочих файлов приложения создается отдельный подкаталог для DBG-файлов. — Пер.
 Где хранятся как двоичные файлы приложения, так и соответствующие PDB-файлы, которые были созданы компоновщиком на этапе отладочного построения. — Пер.
Прохождение стека
К счастью для всех нас, нет необходимости писать собственный код для прохода стека. В DBGHELP.DLL определена специальная API-функция stackwalk, которая берет на себя все заботы по работе со стеком. WDBG использует ее точно так же, как это делает отладчик Visual C++. Единственная неприятность — нет подробной документации по структуре STACKFRAME. В листинге 4-8 показаны только те поля этой структуры, которые должны быть заполнены. Функция stackwalk так хорошо заботится обо всех деталях, что вы можете и не знать, что при оптимизированном коде проход по стеку может быть довольно трудной задачей. Причина этих трудностей заключается в том, что для некоторых функций компилятор может выполнять оптимизацию вдали от области стека, т. е. от того места, где выталкиваются его элементы. Компиляторы Visual C++ и Visual Basic довольно агрессивны, когда они выполняют оптимизацию, и если они могут использовать стековый регистр как рабочий, то они будут это делать. Чтобы облегчать работу со стеком в таких ситуациях, компилятор генерирует то, что называется данными FPO (Frame Pointer Omission).


FPO-данные — это таблица, которую функция stackwalk использует для вычислений, связанных с обработкой тех функций, которые пропускают нормальную область стека. Мы рассматриваем FPO-данные еще и потому, что ссылки на них иногда встречаются в MSDN и в различных отладчиках. Можно подробнее познакомиться со структурой FPO-данных в файле WINNT.H.
 Листинг 4-8. InitializeStackFrameWithGontext из i386CPUHELP.C
BOOL CPUHELP_DLLINTERFACE _stdcall
InitializeStackFrameWithContext ( STACKFRAME * pStack,
CONTEXT * pCtx)
{
ASSERT ( FALSE == IsBadReadPtr ( pCtx, sizeof ( CONTEXT))); 
ASSERT ( FALSE == IsBadWritePtr ( pStack, sizeof ( STACKFRAME)) 
} ;
if ( ( TRUE == IsBadReadPtr ( pCtx, sizeof ( CONTEXT))) ||
( TRUE == IsBadWritePtr ( pStack, sizeof ( STACKFRAME)))) 
{
return ( FALSE);
 }
pStack->AddrPC.Offset = pCtx->Eip; 
pStack->AddrPC.Mode = AddrModeFlat ;
 pStack->AddrStack.Offset = pCtx->Esp;
 pStack->AddrStack.Mode = AddrModeFlat ;
 pStack->AddrFrame.Offset = pCtx->Ebp;
 pStack->AddrFrame.Mode = AddrModeFlat ;
 return ( TRUE); 
}

Типы Windows-отладчиков


Если вы хоть немного программировали для Windows, то, вероятно, слышали о различных типах отладчиков, которые можно при этом использовать. В мире Windows доступны два типа отладчиков: отладчики режима пользователя (user-mode debuggers) и отладчики режима ядра (kernel-mode debuggers).

Большинству разработчиков больше знакомы отладчики пользовательского режима. Не удивительно, что отладчики этого режима предназначены для отладки приложений пользовательского режима (user-mode applications). Главный пример отладчика режима пользователя — отладчик Microsoft Visual C++. Отладчики режима ядра, как следует из их названия, — это такие отладчики, которые позволяют отлаживать ядро операционной системы. Они используются главным образом теми, кто пишет (и отлаживает, конечно) драйверы устройств.



Точки прерывания и пошаговый проход


Большинство программистов не понимают, что отладчики широко используют точки прерывания "за сценой", чтобы позволить основному отладчику управлять подчиненным. Хотя можно устанавливать точки прерывания не напрямую, отладчик будет их устанавливать, позволяя управлять такими задачами, как пошаговый проход через (stepping over) вызванную функцию. Отладчик также использует точки прерывания, когда необходимо выполнить программу до указанной строки исходного файла и остановиться. Наконец, отладчик устанавливает точки прерывания, чтобы перейти в подчиненный отладчик по команде (например, через выбор пункта меню Debug Break в WDBG).

>Концепция установки точки прерывания довольно проста. Все, что нужно сделать — это получить адрес памяти, где требуется установить точку прерывания, сохранить код машинной команды (его значение), расположенный в этом месте, и записать по этому адресу инструкцию точки прерывания. В семействе Intel Pentium мнемоника инструкции точки прерывания выглядит как INT з, а код операции — ОхСС, так что нужно сохранить только единственный байт по адресу, где вы устанавливаете точку прерывания. Другие CPU, такие как Intel Merced, имеют иные размеры кода операции, поэтому придется сохранять больше данных по этому адресу.

В листинге 4-4 показан код функции SetBreakpoint. Читая этот код, имейте в виду, что функции DBG_* принадлежат библиотеке LOCALASSIST.DLL и помогают изолировать различные подпрограммы манипуляции с процессом, облегчая добавление к WDBG функций удаленной отладки. Функция SetBreakpoint иллюстрирует обработку (описанную ранее в этой главе), необходимую для изменения защиты памяти при записи в нее.

Листинг 4-4. Функция SetBreakepoint из 1386CPUHELP.C

int CPUHELP_DLLINTERFACE _stdcall

SetBreakpoint ( PDEBUGPACKET dp ,

ULONG ulAddr ,

OPCODE * pOpCode ) 

{

DWORD dwReadWrite = 0;

BYTE bTempOp = BREAK_OPCODE;

BOOL bReadMem;

BOOL bWriteMem;

BOOL bFlush;

MEMORY_BASIC_INFORMATION mbi;

DWORD dwOldProtect;

ASSERT ( FALSE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET))) ;


 ASSERT ( FALSE == IsBadWritePtr ( pOpCode, sizeof ( OPCODE)));

 if ( ( TRUE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET))) ||

( TRUE == IsBadWritePtr ( pOpCode, sizeof ( OPCODE))) ) 

{

TRACE0 ( "SetBreakpoint : invalid parameters\n!");

return ( FALSE); 

}

// Если текущая операционная система Windows 98 и адрес

 // больше 2 Гбайт, то просто выполните возврат,

 if ( ( FALSE = IsNT ()) && ( ulAddr >= 0x80000000))

 {

return ( FALSE); 

}

// Читать код операции по определенному адресу.

 bReadMem = DBG_ReadProcessMemory ( dp->hProcess ,

(LPCVOID)ulAddr, SbTempOp , sizeof ( BYTE), SdwReadWrite ) ; 

ASSERT ( FALSE != bReadMem); 

ASSERT ( sizeof ( BYTE) = dwReadWrite);

 if ( ( FALSE = bReadMem ) ||

( sizeof ( BYTE) != dwReadWrite)) 

{

return ( FALSE); 

}

// Готова ли эта новая точка прерывания переписать 

// код операции существующей точки прерывания?

 if ( BREAKJDPCODE = bTempOp)

{

return ( -1); 

}

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

 DBG_VirtualQueryEx ( dp->hProcess  ,

(LPCVOID)ulAddr,

&mbi ,

 sizeof ( MEMORY_BASIC_INFORMATION) ); 

// Перевести подчиненный отладчик в режим 

// "копирование-при записи" для страниц памяти,

 if ( FALSE == DBG_VirtualProtectEx ( dp->hProcess ,

mbi.BaseAddress ,

 mbi.RegionSize , 

PAGE_EXECUTE_READWRITE, 

&mbi.Protect ) ) 

{

ASSERT ( ! "VirtualProtectEx .failed!!");

 return ( FALSE);

 }

// Сохранить код заменяемой операции. 

*pOpCode = (void*)bTempOp; 

bТеmрОр = BREAK_DPCODE; 

dwReadWrite = 0;

// Код операции был сохранен, так что теперь

 // нужно установить точку прерывания. 

bWriteMem = DBG_WriteProcessMemory ( dp->hProcess ,

(LPVOID)ulAddr , 

(LPVOID)SbTempOp,

 sizeof ( BYTE) , 

sdwReadWrite ); 

ASSERT ( FALSE != bWriteMem); 



ASSERT ( sizeof ( BYTE) == dwReadWrite);

if ( ( FALSE == bWriteMem ) ||

( sizeof ( BYTE) != dwReadWrite)) 

{

return ( FALSE); 

}

 // Вернуть защиту к состоянию, которое предшествовало

// установке точки прерывания

// Change the protection back to what it was before

// I blasted thebreakpoint in.

VERIFY ( DBG_VirtualProtectEx ( dp->hProcess ,

mbi. BaseAddress, 

mbi.RegionSize ,

 mbi.Protect , 

SdwOldProtect ));

// Сбросить кэш инструкций в случае, если эта память была в кэше CPU

 bFlush = DBG_FlushInstructionCache ( dp->hProcess ,

(LPCVOID)ulAddr, 

sizeof ( BYTE) );

 ASSERT ( TRUE = bFlush);

 return ( TRUE);

 }

После установки точки прерывания CPU выполнит ее и сообщит отладчику, что произошло исключение EXCEPTION_BREAKPOINT (0x80000003) — здесь-то и появляется проблема. Если это регулярная точка прерывания, то отладчик определит место ее размещения и покажет его пользователю. После того как пользователь решает .продолжить выполнение, отладчик должен проделать некоторую работу, чтобы восстановить состояние программы. Точка прерывания переписала часть памяти, поэтому если вы, как автор отладчика, просто позволите процессу продолжаться, то выполните неправильную кодовую последовательность, и подчиненный отладчик, вероятно, завершится аварийно. Поэтому следует передвинуть указатель текущей инструкции назад, к адресу точки прерывания и заменить ее кодом операции, который был сохранен при установке этой точки. После восстановления кода операции можно продолжать выполнение.

Вопрос: как переустанавливать точку прерывания, чтобы иметь возможность повторно останавливаться в этом месте? Если CPU поддерживает пошаговое выполнение, переустановка точки прерывания тривиальна. В пошаговом режиме CPU выполняет единственную инструкцию и генерирует другой тип исключения — EXCEPTION_SINGLE_STEP (0x80000004). К счастью, все CPU, на которых выполняются 32-разрядные Windows, поддерживают пошаговое выполнение.


Для перехода в режим пошагового выполнения процессоров Intel Pentium требуется установить (в единичное состояние) бит 8 регистра флагов. Справочное руководство Intel называет его битом ловушки — Trap Rag (TF или флагом трассировки). В листинге 4-5 приведена функция Setsingiestep и действия, необходимые для установки бита TF. После замены точки прерывания исходным кодом операции отладчик отмечает в своем внутреннем состоянии, что он ожидает пошагового выполнения, устанавливает в CPU соответствующий режим и затем продолжает процесс.

Листинг 4-5.Функция SetSingleStep из 1386CPUHELP.C

BOOL CPUHELP_DLLIMNTERFACE _stdcall 

SetSingleStep ( PDEBUGPACKET dp) 

{

BOOL bSetContext;

ASSERT ( FALSE == IsBadReadPtr ( dp, sizeof ( DEBUGPACKET)));

if ( TRUE = IsBadReadPtr ( dp, sizeof ( DEBUGPACKET)))

{

TRACED ( "SetSingleStep : invalid parameters\n!");

return ( FALSE);

}

// Для i386, просто установить TF-бит.

dp->context.EFlags |= TF_BIT;

bSetContext = DBG_SetThreadContext ( dp->hThread,

&dp->context);

ASSERT ( FALSE != bSetContext);

return ( bSetContext);

}

После того как основной отладчик разблокирует процесс, вызывая функцию ContinueDebugEvent, этот процесс после каждого выполнения отдельной инструкции немедленно генерирует пошаговое исключение. Чтобы удостовериться, что это было ожидаемое пошаговое исключение, отладчик проверяет свое внутреннее состояние. Поскольку отладчик ожидал такое исключение, т "знает", что точка прерывания должна быть переустановлена. На каждом отдельном шаге этого процесса указатель команд продвигается в позицию, предшествующую исходной точке прерывания. Поэтому отладчик может устанавливать код операции точки прерывания обратно в ее исходное положение. Каждый раз, когда происходит исключение типа EXCEPTION_  SINGLE_STEP, операционная система автоматически сбрасывает бит TF, так что нет никакой необходимости сбрасывать его с помощью отладчика. После установки точки прерывания основной отладчик разблокирует подчиненный, и тот продолжает выполняться.



Всю обработку точки прерывания реализует метод CWDBGProjDOC :: .-andieBreakpoint, который можно найти в файле WDBGPROJDOC.CPP на сопровождающем компакт-диске. Сами точки прерывания определены в файлах BREAKPOINTS и BREAKPOINT.CPP. Эти файлы содержат пару классов, которые обрабатывают точки прерывания различных стилей. Диалоговое окно WDBG Breakpoints позволяет устанавливать точки прерывания при выполнении подчиненного отладчика точно так же, как это делается в отладчике Visual C++. Способность устанавливать точки прерывания "на лету" означает, что необходимо тщательно сохранять след состояния вторичного отладчика и состояния точек прерывания. Подробности обработки включения и выключения точек прерывания в зависимости от состояния подчиненного отладчика можно найти в описании метода CBreakpointsDig::OnOk в файле BREAKPOINTSDLG.CPP на сопровождающем компакт-диске.

Одно из наиболее изящных свойств, реализованных в WDBG, связано с пунктом меню Debug Break. Речь идет о том, что пока выполняется подчиненный отладчик, можно в любое время быстро войти в основной отладчик.

Точки прерывания, устанавливаемые при реализации пункта Debug Break, несколько отличаются от тех, что использует WDBG. Такие точки называют одноразовыми (one-shot) точками прерывания, потому что они удаляются сразу же, как только срабатывают. Получение набора таких точек прерывания представляет некоторый интерес. Полное представление можно получить, проанализировав функцию  CWDBGProj Doc: : OnDebugBreak из WDBGPROJDOC.CPP, а здесь приведем лишь некоторые поучительные подробности. В листинге 4-6 показана функция CWDBGProj Doc:: OnDebugBreak из WDBGPROJDOC.CPP. Дополнительные сведения об одноразовых точках прерывания приведены далее в разделе "Операции Step Into, Step Over u Step Out" этой главы.

Листинг 4-5. Обработка Debug Breake в WDBGPROJDOC.CPP

void CWDBGProjDoc :: OnDebugBreak ()

{

ASSERT ( m_vDbgThreads.size () > 0) ;

// Идея здесь состоит в том, чтобы приостановить все потоки



// подчиненного отладчика и установить указатель текущей инструкции

// для каждого из них на точку прерывания. Таким образом, я могу

// гарантировать, что по крайней мере один из потоков будет

// отлавливать одноразовые точки прерывания. Одна из ситуаций,

// при которой установка точки прерывания на каждом потоке не будет

// работать, происходит, когда приложение "висит". Поскольку в

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

// Чтобы выполнить работу в такой тупиковой ситуации, я был вынужден

// использовать следующий алгоритм:'

// 1. Установить точки прерывания с помощью данной функции.

// 2. Установить флажок состояния, указывающий, что я ожидаю

// на точке прерывания Debug Break.

// 3. Установить фоновый таймер на ожидание точки прерывания.

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

// Все хорошо!

// 5. Если таймер сбрасывается, то приложение "висит".

// 6. После таймера установить указатель инструкции одного из

// потоков на другой адрес и поместить точку прерывания по этому

// адресу.

// 7. Рестартовать поток.

// 8. Когда эти специальные точки прерывания сработают, очистить

// точку прерывания и переустановить указатель команды

// обратно в первоначальное положение.

// Повысим приоритет этого потока так,

// чтобы пройти через установку этих точек прерывания как можно

// быстрее и предохранить любой поток подчиненного отладчика от

// планирования.

HANDLE hThisThread = GetCurrentThread () ;

 int iOldPriority = GetThreadPriority ( hThisThread); 

SetThreadPriority ( hThisThread, THREAD_BASE_PRIORITY_LOWRT);

 HANDLE hProc = GetDebuggeeProcessHandle ();

 DBGTHREADVECT::iterator i; for ( i = m_vDbgThreads.begin ();

 i != m_vDbgThreads.end () ;

 i++ ) 

{

// Приостановить этот поток. Если он уже имеет счетчик

// приостановок, меня это, на самом деле, не беспокоит. Именно

// поэтому точки прерывания и устанавливались на каждом потоке

// подчиненного отладчика.


Я нахожу активный поток

//в конечном счете случайно.

DBG_SuspendThread ( i->m_hThread);

// Поток приостановлен, можно получить контекст.

CONTEXT ctx;

ctx.ContextFlags = CONTEXT_FULL;

// Поскольку, если используется ASSERT, приоритет этого потока

// установлен в реальном масштабе времени, и компьютер может

// "висеть" на панели сообщения, поэтому в if-операторе можно

// указать ошибку только с помощью оператора трассировки.

if ( FALSE != DBG_GetThreadContext ( i->m_hThread, &ctx))

{

// Найти адрес, который указатель команд собирается

// выполнить. Это адрес, где будет устанавливаться

// точка прерывания.

DWORD dwAddr = ReturnlnstructionPointer ( &ctx);

COneShotBP cBP;

// Установить точку прерывания.

cBP.SetBreakpointLocation ( dwAddr);

// Активизировать ее.

if ( TRUE == cBP.ArmBreakpoint ( hProc))

{

// Добавить эту точку прерывания к списку Debug Break, 

// только если точка прерывания была успешно

 // активизирована. Подчиненный отладчик легко мог бы

 // иметь множественные потоки, связанные с одной и той же

 // командой, но я хочу установить на этот адрес

 // только одну точку прерывания. m_aDebugBreakBPs.Add ( cBP); 

}

 }

else 

{

TRACE ( "GetThreadContext failed! Last Error = Ox%08X\n",

 GetLastError ());

#ifdef _DEBUG

// Поскольку функция GetThreadContext потерпела неудачу,

 // вероятно, следует посмотреть, что случилось. Поэтому

 // войдем в отладчик, выполняющий отладку отладчика WDBG.

 // Даже притом, что поток WDBG выполняется на уровне 

// приоритетов реального масштаба времени, вызов DebugBreak

 // немедленно удаляет этот поток из планировщика операционной

 // системы, поэтому его приоритет снижается. DebugBreak ();

 #endif

}

 }

// Все потоки имеют установленные точки прерывания. Теперь будем

 // всех их рестартовать и отправлять каждому поточное сообщение.

 // Причина для отправки таких сообщений проста.


Если подчиненный

 // отладчик прореагирует на сообщения или другую обработку, он будет 

// немедленно прерван. Однако, если он просто простаивает в цикле

 // сообщений, необходимо вынудить его к действию.

// Поскольку имеется идентификатор (ID) потока, будем просто посылать 

// потоку сообщение WM_NULL. Предполагается, что это простенькое

 // сообщение, так что оно не должно испортить подчиненный отладчик. 

// Если поток не имеет очереди сообщений, эта функция просто потерпит

 // неудачу для такого потока, не причинив никакого вреда,

 for ( i = m_vDbgThreads.begin () ;

 i!= m_vDbgThreads.end () ;

 i++ ) 



// Пусть этот поток продолжит выполнение

 //до очередной точки прерывания

. DBG_ResumeThread ( i->ro_hThread);

 PostThreadMessage ( i->m_dwTID, WM_NULL, 0, 0); 

}

// Теперь понизить приоритет до старого значения. 

SetThreadPriority ( hThisThread, iOldPriority); 

}

Для того чтобы остановить подчиненный отладчик, нужно умудриться "втиснуть" точку прерывания в поток команд CPU так, чтобы можно было останавливаться в отладчике. Если поток выполняется, то подобраться к известной точке можно при помощи API-функции suspendThread, приостанавливающей его. Затем, вызвав API-функцию GetThreadContext, определить указатель текущей команды. Имея такой указатель, можно вернуться к установке простых точек прерывания. Установив точку прерывания, нужно вызвать API-функцию ResumeThread, чтобы разрешить потоку продолжать выполнение и сделать так, чтобы он натолкнулся на эту точку.

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


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

Чтобы гарантировать, что подчиненный отладчик достигнет точки прерывания, нужно послать ему сообщение. Если все, что вы имеете, это дескриптор потока, выданный отладочным API, то непонятно, как превратить этот дескриптор в соответствующий дескриптор окна (HWND)? К сожалению, сделать это нельзя. Однако, имея дескриптор потока, всегда можно вызвать функцию PostThreadMessage, которая отправит сообщение в очередь поточных сообщений. Поскольку обработка HWND-сообщения накладывается на вершину очереди поточных сообщений, вызов PostThreadMessage сделает точно то, что нужно.

Остается понять, какое сообщение следует отправить? Нельзя отправлять сообщение, которое могло бы заставить подчиненный отладчик делать какую-нибудь реальную обработку, разрешая, таким образом, основному отладчику изменять поведение подчиненного отладчика. Например, отправка сообщения WM_CREATE, вероятно, не была бы хорошей идеей. К счастью существует более подходящее сообщение — WM_NULL, которое вы, вероятно, используете как средство отладки при изменении сообщений. Отправка сообщения WM_NULL с помощью PostThreadMessage не приносит никакого вреда, даже если поток не имеет очереди сообщений, а приложение является консольным. Поскольку консольные приложения всегда находятся в состоянии выполнения, даже если ожидают клавишную команду, установка точки прерывания в текущей выполняющейся команде вызовет прерывание.

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


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

Такая методика хорошо работает с приложением, которое имеет только два потока. Однако если потоков много, то проблема остается открытой. Приостанавливая каждый из потоков подчиненного отладчика, вы так изменяете состояние приложения, что появляется опасность загнать его в тупик. Чтобы приостанавливать все потоки, устанавливать точки прерывания и возобновлять потоки без проблем, отладчик должен повысить приоритет своего собственного потока. Повысив приоритет до THREAD_BASE_PRIORITY_LOWRT, отладчик может так планировать свой поток, чтобы потоки подчиненного отладчика не выполнялись, когда базовый отладчик манипулирует ими.

Пока алгоритм прерывания многопоточных приложений звучит разумно. Однако, чтобы сделать пункт Debug Break полностью работающим, необходимо решить еще одну, последнюю проблему. Если установлен весь набор точек прерывания во всех потоках и эти потоки возобновляются, то все еще возможна ситуация, в которой не будет происходить прерываний. Устанавливая точки прерывания, вы полагаетесь на выполнение, по крайней мере, одного из потоков, чтобы вызвать исключение точки прерывания. А что случится, если процесс находится в ситуации тупика? Ничего не случится — никакие потоки не выполняются, и ваши тщательно размещенные точки прерывания никогда не вызовут исключения.

Для того чтобы учесть возможность тупиковой ситуации, нужно установить таймер, отметив момент добавления прерывания. По истечении определенного времени (отладчик Visual C+ использует 3 секунды), нужно предпринять некоторое решительное действие. Когда время пункта Debug Break заканчивается, нужно сначала установить один из указателей поточной команды на другой адрес, затем — точку прерывания в этом новом адресе и рестартовать поток. Когда эти специальные точки прерывания сработают, необходимо сместить указатель поточной команды назад, в его первоначальное положение.В WDBG антитупиковая обработка не реализована, эта возможность оставлена читателям в качеств упражнения в функции GWDBGProjDoc::OnDebugBreak (файл WDBGPROJDOC.CPP на сопровождающем компакт-диске). Полная инфраструктура для управления антитупиковой обработкой расположена там же и, вероятно, потребуется не больше пары часов для ее заполнения. Завершив ее реализацию, вы будете хорошо понимать, как работает WDBG.



WDBG: что делать дальше?


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

Можно заняться интерфейсом пользователя (UI) WDBG-отладчика. Первое усовершенствование, которое можно осуществить — улучшить реализацию этого интерфейса. Интерфейс уже содержит всю необходимую информацию; вы должны только спроектировать лучшие способы ее представления. Сам WDBG поддерживает только простые, позиционные точки прерывания (location breakpoints). С помощью BREAKPOINT.H и BREAKPOINT.CPP можно добавить интересные дополнительные виды точек прерывания, такие как точки прерывания со счетчиком пропусков (skip count breakpoints) или точки прерывания выражений (expression breakpoints), для которых прерывание происходит, только если помеченное ими выражение истинно. Удостоверьтесь, что вы получаете новые точки прерывания с помощью функции CLocationBp (благодаря которой вы получаете се-\риализованный код и не должны ничего изменять в WDBG).  Вы должны быть способны без больших усилий расширить WDBG, чтобы поддержать отладку множественных процессов (multiple process debug->ging). Большинство интерфейсов построены для работы по схеме идентификации процесса, так что нужно только проследить, с каким процессом вы работаете во время уведомления об отладке.  Интерфейс WDBG построен так, чтобы позволить быстро перейти к удаленной отладке и различным CPU, оставив работу главной части интерфейса примерно такой же. Напишите динамические библиотеки удаленной отладки и расширьте WDBG так, чтобы позволить пользователю выбирать, где выполнять отладку: на местной или на удаленной машине.  Наконец, чтобы сделать WDBG-отладчик действительно полезным, вы всегда можете написать лучший дизассемблер и символьную машину для отладочных символов формата С7!



WDBG: реальный отладчик


Мне кажется, лучший способ показать, как работает отладчик — написать его, что и сделано в этом разделе. Хотя WDBG не может заменять отладчик Visual C++, но он, конечно, умеет многое, что полагается уметь отладчику. На рис. 4.2 показан WDBG, отлаживающий программу Microsoft Word. На рисунке Word остановлен в точке прерывания, которую я установил на функции GetProcAddress. Окно Memory, в верхнем правом углу, показывает второй параметр, который Word пересылает данному экземпляру GetProcAddress (строка PhevCreateFilelnfo). Рис. 4.2 демонстрирует большую часть возможностей этого отладчика, включая показ регистров, просмотр стеков вызова и кода дизассемблера, показ загруженных модулей и выполняющихся потоков. Кроме того, WDBG поддерживает точки прерывания, перечисление символов и прерывание приложений для остановки отладчика (эти возможности не видны на рис. 4.2, но станут очевидными при первом же запуске WDBG).

Рис. 4.2. Отладчик WDBG в действии

В целом, WDBG — хороший образец отладчика. Однако, глядя на интерфейс пользователя (UI) в WDBG, можно заметить, что я не потратил много времени на создание интерфейса пользователя. Фактически, все окна в WDBG построены по стандарту многодокументного интерфейса (Multiple-Document Interface — MDI) и относятся к редактируемым элементам управления. Это было сделано умышленно: я сохранил простой интерфейс пользователя, потому что не хотел, чтобы его детали отвлекали вас от сущности кода отладчика. Пользовательский интерфейс WDBG написан с использованием библиотеки классов MFC, поэтому попытки улучшить интерфейс не должны вызвать затруднений.

Прежде чем приступать к изучению специфических особенностей отладки, рассмотрим WDBG подробнее. В табл. 4.1 описаны все главные подсистемы WDBG. Одним из моих намерений при создании WDBG было определение нейтрального интерфейса между пользовательским интерфейсом и циклом отладки. Для того чтобы WDBG.EXE поддерживал удаленную отладку через сеть, следовало бы с помощью нейтрального интерфейса просто заменить локальные отладочные DLL на сетевые.


Таблица 4.1. Главные подсистемы WDBG

Подсистема

Описание

WDBG.EXE

Содержит весь Ul-код. Дополнительно он заботится об обработке всех точек прерывания. Большая часть этих действий отладчика запрограммирована в файле WDBGPROJDOC.CPP

LOCALDEBUG.DLL

Реализует цикл отладки. Поскольку я хотел обеспечить повторное использование этого отладочного цикла, код пользователя (в данном случае это WDBG.EXE) передает в цикл отладки С++-класс, производный от класса CDebugBaseUser (который определен в DEBUGINTERFACE.H). Когда происходит какое-нибудь отладочное событие, цикл отладки вызыва- " ется в этот класс. За всю синхронизацию ответственны классы пользователей. Файлы WDBGUSER.H и WDBGUSER.CPP содержат координирующий класс для WDBG.EXE. WDBG.EXE использует простую синхронизацию (типа SendMessage). Другими словами, поток отладки посылает сообщение Ш-потоку и блокируется, пока не произойдет возврат из Ill-потока. Если отладочное событие требует ввода пользователя, поток отладки блокируется после посылки сообщения о событии синхронизации. Как только Ill-поток начинает обработку команды Go, он устанавливает событие синхронизации, и поток отладки снова начинает выполняться

LOCALASSIST.DLL

Этот простой модуль только оболочка API-функций, манипулирующих с памятью подчиненного отладчика и регистрами. Используя интерфейс, определенный в этом модуле, WDBG.EXE и I386CPUHELP.DLL могут немедленно перейти к управлению удаленной отладкой, просто заменив этот модуль на сетевой

I386CPUHELP.DLL

Это вспомогательный модуль для процессора IA32 (Pentium). Хотя этот модуль специфичен для процессоров Pentium, его интерфейс, определенный в CPUHELP.H, не зависит от CPU. Если бы вы захотели пренести WDBG на другой процессор, то это единственный модуль, который пришлось бы заменить. Дизассемблер в этом модуле взят из программы Dr. Watson, которая поставляется в составе Platform SDK. Хотя дизассемблер работает, но он нуждается в обновлении, чтобы поддержать последние варианты CPU Pentium