Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004

.pdf
Скачиваний:
322
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

181

 

 

новке точки прерывания. После восстановления кода операции можно продол жить исполнение.

Есть только одна маленькая проблема: как сбросить точку прерывания, чтобы остановиться в этом месте в следующий раз? Если процессор, на котором вы ра ботаете, поддерживает пошаговое исполнение, это сделать легко. При пошаговом исполнении процессор выполняет одну команду и вырабатывает другой тип ис ключения — EXCEPTION_SINGLE_STEP (0x80000004). К счастью, все процессоры, на ко торых работает Win32, поддерживают пошаговое исполнение. Для семейства Intel Pentium установка пошагового исполнения требует установки бита 8 регистра флагов. Руководство по процессорам Intel называет этот бит TF или флагом ло вушки (Trap Flag). Следующий код демонстрирует функцию SetSingleStep и что нужно сделать для установки флага TF. После восстановления исходного кода операции на месте точки прерывания отладчик помечает свое внутреннее состо яние ожидающим появления исключения пошагового исполнения, переводит процессор в режим пошагового исполнения и продолжает процесс.

// SetSingleStep из i386CPUHelp.C BOOL CPUHELP_DLLINTERFACE __stdcall

SetSingleStep ( PDEBUGPACKET dp )

{

BOOL bSetContext ;

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

{

TRACE0 ( "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 ) ;

}

После освобождения процесса отладчиком путем вызова функции Continue DebugEvent, процесс сразу вырабатывает исключение пошагового исполнения после исполнения единственной команды. Отладчик проверяет свое внутреннее состо яние, чтобы убедиться, что это было ожидаемое исключение. Так как отладчик ожидал появления исключения пошагового исполнения, то знает, какую точку прерывания восстанавливать. Выполнение одного шага смещает указатель команд на следующую команду после исходной точки прерывания. Следовательно, отладчик может восстановить код команды прерывания в исходной точке прерывания. ОС автоматически очищает флаг TF при каждом возникновении исключения EXCEP TION_SINGLE_STEP, поэтому нет нужды очищать его в отладчике. После установки точки прерывания отладчик освобождает отлаживаемую программу и продолжает ее исполнение.

Если вы хотите увидеть всю обработку точки прерывания в действии, взгля ните на метод CWDBGProjDoc::HandleBreakpoint в файле WDBGPROJDOC.CPP из чис

182 ЧАСТЬ II Производительная отладка

ла файлов примеров к этой книге. Я определил собственно точки прерывания в BREAKPOINT.H и BREAKPOINT.CPP, и эти же файлы содержат парочку классов, управляющих различными типами точек прерывания. Я сделал окно Breakpoints WDBG так, чтобы вы имели возможность установки точек прерывания в то вре мя, когда исполняется отлаживаемая программа, точно так же, как это делается в отладчике Visual Studio .NET. Возможность устанавливать точки прерывания на лету означает, что вам нужно четко отслеживать состояния отлаживаемой программы

иточек прерывания. Посмотрите метод CBreakpointsDlg::OnOK в файле BREAK POINTSDLG.CPP из числа примеров, прилагаемых к этой книге, чтобы понять осо бенности того, как я обрабатываю разрешение и запрещение точек прерывания в зависимости от состояния отлаживаемой программы.

Опция меню Debug Break, одна из самых проработанных функций, реализо ванных мной в WDBG, позволяет прервать исполнение и выйти в отладчик в любое время, когда исполняется отлаживаемая программа. В первом издании этой кни ги я привел весьма пространное объяснение технологии, реализованной для при остановки потоков отлаживаемой программы, установки одноразовых точек пре рывания в каждом потоке и как обеспечить исполнение точек прерывания, посы лая сообщения WM_NULL потокам (об одноразовых точках прерывания см. ниже раздел «Шаг внутрь, шаг через и шаг наружу»). Для этого потребовалось заставить рабо тать весьма большой объем кода, и он в целом работал вполне прилично. Однако в одном случае, когда он не работал, все потоки отлаживаемой программы вза имно блокировались на объектах режима ядра. Так как потоки приостанавлива лись в режиме ядра, не было способа вытолкнуть их обратно в пользовательский режим. Я вынужден был оставить все как есть и жить с ограничениями своей ре ализации, поскольку WDBG должен был работать в Windows 98/Me, а также в ОС на базе Windows NT.

Так как я прекратил поддержку Windows 98/Me, реализация меню Debug Break стала совершенно тривиальной и теперь работает всегда. Фокус заключается в замечательной функции CreateRemoteThread — ее нет в Windows 98/Me, но есть в Windows 2000 и более поздних ОС. Другая функция, дающая такой же эффект, что

иCreateRemoteThread, но доступна только в Windows XP и более поздних, — Debug BreakProcess. Как можно увидеть из следующего кода, собственно реализация весьма проста. Когда удаленный поток вызывает функцию DebugBreak, при обработке ис ключения я имею дело только с результирующим исключением по точке преры вания, будто была выполнена обычная, определенная пользователем, точка пре рывания.

HANDLE LOCALASSIST_DLLINTERFACE __stdcall

 

 

DBG_CreateRemoteBreakpointThread ( HANDLE

hProcess

,

LPDWORD

lpThreadId

)

{

 

 

HANDLE hRet = CreateRemoteThread ( hProcess

 

,

NULL

 

,

0

 

,

(LPTHREAD_START_ROUTINE)DebugBreak

,

0

 

,

0

 

,

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

183

 

 

 

lpThreadId

) ;

 

return ( hRet ) ;

}

Хотя запуск потока в отлаживаемой программе может показаться рискованным, я почувствовал себя достаточно безопасно, особенно потому, что именно эта тех нология применяется в WinDBG для реализации меню Debug Break. Заметьте, од нако, что вызов CreateRemoteThread имеет побочные эффекты. Когда в процессе запускается поток, по соглашению с ОС он вызовет DllMain каждой из загружен ных DLL, не вызывавших DisableThreadLibraryCalls. Соответственно, когда поток завершается, все функции DllMain, вызванные с уведомлением DLL_THREAD_ATTACH, будут вызваны также с уведомлением DLL_THREAD_DETACH. Все это значит, что, если в ва ших функциях DllMain имеется ошибка, способность функции CreateRemoteThread останавливать отлаживаемую программу может только усложнить проблему. Шансы невелики, но это надо учитывать.

Таблицы символов, серверы символов и анализ стека

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

Работать с современными таблицами символов трудно. Самый распространен ный формат таблицы символов, Program Database (PDB), наконец то получил до кументированный интерфейс, но с ним очень трудно работать, и он пока не под держивает такую полезную функциональность, как анализ стека (stack walking). К счастью, DBGHELP.DLL предлагает достаточное количество вспомогательных уп рощающих жизнь классов оболочек, которые мы сейчас и обсудим.

Различные форматы символов

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

Общий формат объектных файлов (Common Object File Format, COFF) был одним из первоначальных форматов таблиц символов и появился в Windows NT 3.1, первой версии Windows NT. Создатели Windows NT имели большой опыт раз работки ОС и хотели добавить в Windows NT некоторые уже существующие сред ства. Формат COFF является частью большей спецификации, которой следовали поставщики UNIX, пытаясь создать общие форматы двоичных файлов. Visual C++ 6 был последней версией компиляторов Microsoft, поддерживавших COFF.

Формат C7, или CodeView, появился как часть Microsoft C/C++ версии 7 во вре мена MS DOS. Если вы ветеран программирования, то, возможно, слышали рань

184 ЧАСТЬ II Производительная отладка

ше название CodeView — это название старого отладчика Microsoft. Формат C7 обновлен для поддержки ОС Win32, а Visual C++ 7 является последним компиля тором, поддерживающим этот формат символов. Формат C7 был частью испол няемого модуля, так как компоновщик добавлял информацию о символах к ис полняемому файлу после его компоновки. Добавление символов к двоичному файлу означает, что ваши отлаживаемые модули могут быть весьма большими; инфор мация о символах может легко оказаться больше, чем исполняемый код.

Формат PDB (Program Database, база данных программы) наиболее распрост раненный сегодня, поддерживается и Visual C++, и Visual C#, и Visual Basic .NET. Каждый, кто имеет более чем пятиминутный опыт работы, видел, что файлы PDB хранят символьную информацию отдельно от исполняемых модулей. Чтобы уви деть, содержит ли исполняемый файл сведения о символах PDB, запустите для ис полняемого файла программу DUMPBIN из комплекта поставки Visual Studio .NET. Ключ /HEADERS, указанный в командной строке DUMPBIN, распечатает информа цию заголовка переносимого исполняемого файла (Portable Executable, PE). Часть информации заголовка содержит Debug Directories (каталоги отладки). Если для них указан тип cv с форматом RSDS, значит, это исполняемый файл, созданный Visual Studio .NET с PDB файлами.

DBG файлы уникальны, так как в отличие от других форматов символов они создаются не компоновщиком. DBG файл в основном является файлом, содержа щим другие типы отладочных символов, таких как COFF и C7. DBG файлы исполь зуют некоторые из таких же структур, определяемых форматом файла PE — фор матом, используемым для исполняемых файлов Win32. REBASE.EXE строит DBG файлы путем выделения отладочной информации COFF или C7 из модуля. Нет нужды запускать REBASE.EXE для модуля, построенного с использованием файлов PDB, так как при этом символы уже отделены от модуля. Microsoft распространя ет DBG файлы с отладочными символами ОС в формате более ранних отладчи ков, чем Visual Studio .NET и последней версии WinDBG, для них будет необходи мо найти соответствующий PDB файл для исполняемых файлов ОС.

Доступ к символьной информации

Традиционный способ обработки символов заключался в применении DBGHELP.DLL, поставляемой Microsoft. Раньше DBGHELP.DLL поддерживала только общую инфор мацию, которая включала имена функций и элементарных глобальных перемен ных. Однако этой информации более чем достаточно для написания некоторых замечательных утилит. В первом издании этой книги я посвятил несколько стра ниц тому, как загрузить символы с помощью DBGHELP.DLL, но в последних реа лизациях DBGHELP.DLL загрузка символов заметно улучшена и действительно работает. Мне нет нужды описывать, как применять функции символов DBG HELP.DLL, — я только отошлю вас к документации MSDN. Обновленная версия DBGHELP.DLL входит в комплект Debugging Tools for Windows, поэтому вы може те как нибудь посетить www.microsoft.com/ddk/debugging для получения новей шей и наилучшей версии.

Когда вышли первые бета версии Visual Studio .NET, я был восхищен, так как Microsoft предполагала поставлять интерфейс к PDB файлам. Сначала я подумал, что SDK интерфейса доступа к отладочным данным (Debug Interface Access, DIA)

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

185

 

 

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

Когда я только взялся за второе издание этой книги, я намеревался написать сервер символов, используя DIA, поставляемый с Visual Studio .NET 2002. Первая возникшая проблема заключалась в том, что сервер символов DIA являлся не бо лее, чем программой чтения PDB. Это не очень важно, но это означало, что я должен буду справиться с большим количеством функций обработки высокого уровня са мостоятельно. DIA делает то, что и ему положено делать; я просто считал, что он может больше. Приступив к работе, я заметил, что в DIA, кажется, нет поддержки анализа стека. Функция API StackWalk64, о которой я еще расскажу, должна иметь некоторые возможности, помогающие осуществлять доступ к символьной инфор мации, необходимой для анализа стека. К сожалению, DIA в то время не показы вал всей нужной информации. Например, функции StackWalk64 необходим про пуск указателя фрейма (frame pointer omission, FPO).

Похоже, что реализация DIA в Visual Studio .NET 2003 поддерживает интерфейсы, которые должны работать при анализе стека, но это лишь отдельные IDL интер фейсы, а документации оказалось недостаточно для использования этого нового кода. Я думал, что, располагая этими новыми средствами анализа стека, я должен продолжить работу с DIA, так как казалось, что это перспективная вещь, хотя DIA и выглядел весьма неуклюже. А потом я нарвался на самую большую проблему: ин терфейс DIA — это псевдо COM интерфейс: он выглядит, как COM, но очень гро моздкий, из за чего получаешь все проблемы, связанные с COM, взамен не полу чая ничего.

Начав работу над базовой частью кода, я наткнулся на интерфейс, наилучшим образом демонстрирующий, почему так важно правильно проектировать COM. Символьный интерфейс IDiaSymbol имеет 95 документированных методов. Увы, почти все в DIA является символами. В действительности в перечислении SymTagEnum имеется 31 уникальный символьный тип. Все, что в DIA называется символом, на самом деле больше похоже на массив меток, а не на реальные значения. Огром нейшая проблема интерфейса IDiaSymbol в том, что все типы поддерживают толь ко несколько из этих 95 интерфейсов. Так, базовый тип, поддерживающий самые элементарные типы символов, такие как целые, поддерживает только два интер фейса: один для получения собственно базового типа перечисления и еще один для получения длины типа. Остальные интерфейсы возвращают просто E_NOTIMP LEMENTED (не реализовано). Иметь плоскую иерархию, в которой почти все делает единственный интерфейс, прекрасно, когда совместно используемые элементы относительно малы, но наличие различных типов в DIA ведет к огромному объе му кодирования и хождению по кругу, что, по моему, вовсе не нужно. Типы, ис пользуемые DIA, иерархичны, и интерфейс должен быть спроектирован так же. Вместо использования единственного интерфейса буквально для всего типы дол жны иметь собственные интерфейсы, так как с ними гораздо проще работать. Начав проектировать свою оболочку над DIA, я быстро понял, что я собираюсь напи сать море кода для построения иерархии над DIA, которая уже должна быть зало жена в интерфейсе.

186ЧАСТЬ II Производительная отладка

Яначал понимать, что в процессе работы над сервером символов, в основе которой лежит DIA, я изобретаю колесо, и это мне не понравилось. Колесом в данном случае являлся сервер символов DBGHELP.DLL. Я уже познакомился с за головочным файлом DBGHELP.H и заметил, что в позднейших версиях WinDBG библиотека DBGHELP.DLL поддерживала некоторые формы локальных и парамет ризованных перечислений, а также развертывания структур и массивов. Единствен ная проблема была в том, что большие куски локальных и параметризованных перечислений оказались недокументированными. К счастью, так как я продолжал «перемалывание» заголовочных файлов DIA, я начал понимать, что некоторые образчики возвращаемых значений в коде DBGHELP.DLL и то, что я видел в CVCONST.H (одном из заголовочных файлах DIA SDK), несомненно, соответству ют друг другу. Это было большим достижением, так как это позволяло приступить к использованию DBGHELP.DLL для получения сведений о локальных символах. DBGHELP.DLL напоминает оболочку поверх DIA, которой гораздо проще пользо ваться, чем непосредственно DIA. Поэтому я решил построить свое решение над DBGHELP.DLL, вместо того чтобы самому реализовать эту библиотеку.

Все, чего я достиг, находится в проекте SymbolEngine на диске с примерами. Этот код обеспечивает WDBG локальными символами, а также реализует диало говое окно SUPERASSERT. В общем, проект SymbolEngine является оболочкой для функций сервера символов DBGHELP.DLL, упрощающей ее использование и рас ширяющей управление символами. Чтобы избежать проблем экспорта классов из DLL, я сделал SymbolEngine статической библиотекой. В реализацим SymbolEngine вы заметите несколько вариантов компоновки, заканчивающихся на _BSU. Это специальные варианты SymbolEngine для BUGSLAYERUTIL.DLL, так как BUGSLAYER UTIL.DLL является частью SUPERASSERT. Я не хотел, чтобы SUPERASSERT исполь зовалась для утверждений, что вызвало бы проблемы с реентерабельностью кода, и поэтому не компоновал ее с этими версиями. Вы также можете заметить, что при использовании сервера символов DBGHELP.DLL я всегда вызывал функции, чьи имена заканчиваются на 64. Хотя в документации говорится, что применение не 64 битных функций в качестве оболочки для вызова 64 разрядных функций воз можно, я несколько раз замечал, что непосредственный вызов 64 разрядных фун кций работает лучше, чем вызов их оболочек. По этой причине я их использую всегда. Вы, возможно, захотите рассмотреть подробнее проект SymbolEngine, но он слишком велик, чтобы привести его в книге и следить с его помощью за об суждением. Я хочу объяснить, как пользоваться моим перечислением символов, а также коснуться некоторых главных моментов реализации. Последнее замечание о моем проекте SymbolEngine: символьный механизм DBGHELP.DLL поддержива ет только символы ANSI. Мой проект SymbolEngine является частично Unicode совместимым, поэтому мне нет нужды постоянно преобразовывать строки DBG HELP.DLL в Unicode при использовании SymbolEngine. В процессе работы над проектом я при необходимости расширял параметры формата ANSI в Unicode. Не все было конвертировано, но в большинстве случаев этого достаточно.

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

 

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

187

 

 

 

 

STACKFRAME64

stFrame

;

 

CONTEXT

stCtx

;

 

// Заполнить

stFrame.

 

 

GetThreadContext ( hThread , &stCtx ) ;

while ( TRUE == StackWalk ( . . . &stFrame . . . ) )

{

//Настроить контекстную информацию, указав

//перечисляемые локальные переменные. SymSetContext ( hProcess , &stFrame , &stCtx ) ;

//Перечисляем локальные переменные.

SymEnumSymbols ( hProcess , // Значение, передаваемое SysInitialize.

0, // Базовый адрес DLL, установлен в 0

 

// для просмотра

всех DLL.

NULL

, //

Маска RegExp для поиска,

 

//

NULL означает

"все".

EnumFunc , // Функция обратного вызова.

NULL

); //

Контекст пользователя,

 

//

передаваемый функции обратного вызова.

}

Функция обратного вызова, адрес которой передается методу SymEnumSymbols, получает структуру SYMBOL_INFO, показанную в следующем фрагменте. Если нужна только основная информация о символе, такая как адрес и имя, достаточно струк туры SYMBOL_INFO. Кроме того, поле Flags сообщит вам, чем является символ: ло кальной переменной или параметром.

typedef struct _SYMBOL_INFO {

ULONG

SizeOfStruct;

ULONG

TypeIndex;

ULONG64

Reserved[2];

ULONG

Reserved2;

ULONG

Size;

ULONG64

ModBase;

ULONG

Flags;

ULONG64

Value;

ULONG64

Address;

ULONG

Register;

ULONG

Scope;

ULONG

Tag;

ULONG

NameLen;

ULONG

MaxNameLen;

CHAR

Name[1];

} SYMBOL_INFO, *PSYMBOL_INFO;

Как почти все в компьютерах, разница между минимальным и требуемым весьма велика, что и является причиной столь большого объема кода в проекте Symbol Engine. Я добивался функциональности, позволяющей перечислять локальные переменные, показывая типы и значения точно так же, как это делает Visual Studio

188 ЧАСТЬ II Производительная отладка

.NET. Как можно увидеть из рис. 4 3 и снимков экрана с окном SUPERASSERT в главе 3, мне это удалось.

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

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

1.Вот так и получаются три обращения к функции обратного вызова.

typedef BOOL (CALLBACK *PENUM_LOCAL_VARS_CALLBACK)

 

( DWORD64

dwAddr

,

LPCTSTR

szType

,

LPCTSTR

szName

,

LPCTSTR

szValue

,

int

iIndentLevel

,

PVOID

pContext

) ;

Чтобы перечислить все локальные переменные в середине просмотра стека, просто вызывайте метод EnumLocalVariables — он выполнит все для настройки соответствующего контекста и выполнит перечисление символов. Прототип фун кции EnumLocalVariables показан в следующем фрагменте. Первый параметр — функция обратного вызова, второй и третий сообщают функции перечисления, сколько уровней надлежит раскрыть и требуется ли раскрывать массивы. Как можете представить, чем больше вы раскрываете, тем медленнее работает эта функция. Кроме того, раскрывать массивы может оказаться весьма накладно, так как нет способа узнать, сколько элементов массива используется. Хорошие новости в том, что мой код развертывания корректно обращается с массивами char * и wchar_t *, развертывая не каждый символ, а всю строку целиком. Четвертый параметр — функция чтения памяти, передаваемая методу StackWalk. Если вы укажете NULL, моя функция перечисления использует ReadProcessMemory. Остальные параметры объяс нений не требуют.

BOOL EnumLocalVariables

 

 

( PENUM_LOCAL_VARS_CALLBACK

pCallback

,

int

iExpandLevel

,

BOOL

bExpandArrays

,

PREAD_PROCESS_MEMORY_ROUTINE64

pReadMem

,

LPSTACKFRAME64

pFrame

,

CONTEXT *

pContext

,

PVOID

pUserContext

) ;

Чтобы увидеть код перечисления локальных переменных в действии, лучше всего запустить его с тестовой программой SymLookup из каталога SymbolEngi ne\Tests\SymLookup на прилагаемом к книге СD. SymLookup достаточно мала, чтобы вы смогли увидеть, что в ней происходит. Она также демонстрирует все возмож

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

189

 

 

ные типы переменных, генерируемых компилятором C++, и вы можете видеть, как развертываются различные переменные.

Код реализации развертывания всех локальных переменных и структур нахо дится в трех исходных файлах. SYMBOLENGINE.CPP содержит функции верхнего уровня для декодирования переменных и раскрытия массивов. Все декодирова ние типов сосредоточено в файле TYPEDECODING.CPP, а все декодирование пе ременных — в файле VALUEDECODING.CPP. Если вы будете читать код, вспомни те Капитана Рекурсию! Когда я учился в колледже, профессор, читавший Computer Science 101, пришел в аудиторию в костюме Капитана Рекурсии — в трико и на кидке, чтобы обучить нас рекурсии. Это было жутковатое зрелище, но я опреде ленно научился всему в рекурсии всего за один урок. Метод декодирования сим волов похож на то, как они сохраняются в памяти. Пример на рис. 4 4 показыва ет, что происходит при развертывании символа, являющегося указателем на струк туру. Значения SymTag* имеют типы, определенные в CVCONST.H как тэговые. Зна чение SymTagData имеет тип, указывающий, что для его разбора будет применена последовательность рекурсий. Основное правило таково: рекурсия продолжается, пока не встретится некоторый конкретный тип. Различные типы, такие как типы, определенные пользователем (user defined types, UDT), и классы, имеющие дочер ние классы, всегда нуждаются в проверке на наличие наследников.

SymTagData

(pMyStruct)

Рекурсия

SymTagPointerType

(*)

Рекурсия

typedef struct tag_MYSTRUCT

 

Показать указатель на

{

 

 

 

развернутую MYSTRUCT;

int

iValue

;

 

 

int *

pdata

;

 

MYSTRUCT * pMyStruct

char * szString ;

 

 

} MYSTRUCT , * LPMYSTRUCT;

 

 

 

 

 

 

 

SymTagUDT (tag_MYSTRUCT)

Потомок

SymTagData (child : iValue)

 

Рекурсия

Потомок

SymTagBasicType

(child : szString)

 

SymTagData

 

(child : pData)

Рекурсия

 

Потомок

SymTagPointerType

(*)

 

 

Рекурсия

SymTagData

SymTagBasicType

(int)

(child : szString)

Рекурсия

 

 

SymTagPointerType

 

(*)

 

Рекурсия

 

SymTagBasicType

 

(char)

Рис. 4 4. Пример развертывания символа

190 ЧАСТЬ II Производительная отладка

Хоть я и пришел к программированию сервера символов DBGHELP.DLL, думая, что это будет относительно просто, оказалось, что Роберт Бернс был прав: «Все лучшие планы мышей и людей часто приводят к печальным итогам»1 . Из всего кода этой книги реализация SymbolEngine и приведение его в рабочее состояние за няло гораздо больше времени, чем что либо еще. Так как у меня не было ясной картины, какие типы могут появляться при развертывании, потребовалось совер шить множество проб и ошибок, чтобы в конце концов все заработало. Самый большой урок, полученный мной: даже если вы думаете, что вы все понимаете, доказать это можно только в процессе реализации.

Анализ стека

Я уже говорил, что DBGHELP.DLL имеет функцию API StackWalk64 и поэтому нет нужды писать собственную процедуру анализа стека. Функция StackWalk64 проста и обеспечивает все потребности в анализе стека. WDBG использует функцию API StackWalk64 так же, как это делает отладчик WinDBG. Может быть лишь одна заг воздка: в документации нигде явно не говорится, что должно быть указано в струк туре STACKFRAME64. Этот код демонстрирует поля, которые нужно в ней заполнить:

// InitializeStackFrameWithContext из

i368CPUHelp.C.

 

 

BOOL CPUHELP_DLLINTERFACE __stdcall

 

 

 

InitializeStackFrameWithContext (

STACKFRAME64 *

pStack ,

 

 

CONTEXT *

pCtx

)

{

 

 

 

ASSERT ( FALSE == IsBadReadPtr ( pCtx , sizeof ( CONTEXT ) ) ) ; ASSERT ( FALSE == IsBadWritePtr ( pStack , sizeof ( STACKFRAME64) )); if ( ( TRUE == IsBadReadPtr ( pCtx , sizeof ( CONTEXT ) ) ) ||

( TRUE == IsBadWritePtr ( pStack , sizeof ( STACKFRAME ) ) ) )

{

return ( FALSE ) ;

}

pStack >AddrPC.Offset pStack >AddrPC.Mode pStack >AddrStack.Offset pStack >AddrStack.Mode pStack >AddrFrame.Offset pStack >AddrFrame.Mode

return ( TRUE ) ;

}

= pCtx >Eip

;

 

= AddrModeFlat

;

=

pCtx >Esp

;

 

=

AddrModeFlat

;

=pCtx >Ebp ;

=AddrModeFlat ;

Функция API StackWalk64 отлично справляется со своей работой, поэтому вы можете даже не знать, что анализировать стек оптимизированного кода может быть трудно. Причина трудности в том, что компилятор мог оптимизировать фреймы стека. Компилятор Visual C++ агрессивен при оптимизации кода, и если он может

1Та же фраза в переводе С. Я. Маршака: «И рушится сквозь потолок на нас нужда». —

Прим. перев.

Соседние файлы в предмете Программирование на C++