Интересная проблема разработки 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, то вы не можете входить в них с отладчиком пользовательского режима.