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

         

Что делать дальше с LIMODS?


Работать с очередной версией LIMODS (скажем, с версией 1.1) приятно, но всегда имеется возможность для ее усовершенствования. Посоветовал бы читателю добавить следующие свойства (и придумать совершенно новые):

 LIMODS.EXE одновременно просматривает только один LOM-файл. Было бы очень хорошо, если бы утилита LIMODS.EXE могла поддерживать множественные LOM-файлы, используя LOP-файл (проект LIMODS), позволяющий просматривать и манипулировать LOM-файлами всего проекта в целом. Кроме того, нет необходимости усиленно работать с интерфейсом управления отметками дерева, который я использовал — с помощью множественных LOM-файлов можно найти лучший способ работы с данными проекта;  перед выполнением приложения в LIMODS.EXE версии 1.1 нужно указывать список файлов, предложения трассировки которых требуется увидеть, причем длина этого списка остается фиксированной во время выполнения данного приложения. Было бы намного полезнее организовать некоторого рода канал связи между выполняющимися экземплярами LIMODSDLL.DLL и LIMODS.EXE, чтобы сохранение LOM-файла автоматически обновляло все экземпляры LIMODSDLL.DLL, которые были подключены к этому модулю;  спроектируйте новую, более привлекательную пиктограмму для LIMODS. Мои художественные способности не простираются дальше названий двух-трех первичных цветов. Нужно, чтобы пиктограмма утилиты LIMODS.EXE сразу обращала на себя внимание и однозначно идентифицировала программу.



Что подключает LIMODSDLL.DLL


Когда LIMODSDLL.DLL стартует, он перехватывает ключевые импортированные функции трассировки во всех модулях процессов. Для данной версии LIMODS этими функциями являются OutputDebugStringA и OutputDebugStringW ИЗ KERNEL32.DLL, DiagOutputA DiagOutputW ИЗ BUGSLAYERUTIL.DLL, _CrtDbgReport из MSVCRTD.DLL и AfxTrace из MFC42(U)D.DLL. Кроме того, я подключил семейство функций LoadLibrary, что позволяет получать своевременную информацию о загрузке в адресное пространство дополнительных модулей.

Для того чтобы LIMODS могла работать с Visual Basic, потребовалось также подключить функцию GetProcAddress, чтобы обеспечить возвращение соответствующей функции, когда MSVBVM60.DLL пытается получить функцию OutputDebugStringA. О функциях подключения говорилось в главе 12 и, казалось бы, эта тема исчерпана. Однако при подключении функций DiagOutputA, DiagOutputW и AfxTrace возникают некоторые уникальные проблемы. Во-первых, эти функции имеют спецификатор вызова _cdeci (а не _stdcaii), и в главе 12 показано, как нужно их подключать. Кроме того, AfxTrace экспортируется по порядковому значению (а не по имени).





Исключение исходных файлов из LOM-файлов


Программа GENLIMODS.EXE обладает исключительной способностью ограничивать размер генерируемых ею LOM-файлов за счет включения в них лишь тех исходных файлов, которые содержат предложения трассировки. Конечно, было бы интересно увидеть в сгенерированном коде половину библиотеки стандартных шаблонов (Standard Template Library — STL), но эти файлы не содержат предложений трассировки, они только увеличивают размер памяти, занимаемый LIMODSDLL.DLL, и замедляют процесс генерации LOM-файлов. GENLIMODS.EXE отыскивает два файла — SYSINCL.DAT и MSVCINCL.DAT. Visual C++ использует эти файлы для исключения файлов из проверки зависимостей. Файл SYSINCL.DAT — это просто список файлов, содержащихся в каталогах \Include системы программирования (<VC98>\Include и <VC98>\MFC\Include). Необязательный, пользовательский файл MSVCINCL.DAT может содержать любой список файлов заголовков, для которых не планируется использовать проверку зависимостей. В дополнение к этим файлам, GENLIMODS.EXE ищет в каталогах, указанных в переменной PATH, файл LIMODSINCL.DAT, содержащий любые дополнительные файлы, которые надо исключить из LOM-файлов. Например, для того чтобы исключить из LOM-файлов файлы исполнительной библиотеки языка С, можно поместить их в файл LIMODSINCL.DAT. На сопровождающем компакт-диске в каталоге \SourceCode\LIMODS есть версия файла LIMODSINCL.DAT, которая исключает из LOM-файлов все исходные файлы исполнительной библиотеки языка С.



Использование LIMODS


Перед дальнейшим чтением нужно установить LIMODS (исходный код LIMODS находится на сопровождающем компакт-диске) и откомпилировать полный LIMODS-проект. После компиляции скопируйте файл параметров LIMODS (LIMODS.INI) в переменную среды %SYSTEMROOT% или каталог Windows. По умолчанию LIMODS разместит свои файлы данных (*.LOM) в том же каталоге, в котором находится каждый модуль, загруженный во время активности LIMODS. Если установить параметр LOMDirectory в секции [LIMODS] файла LIMODS.INI, то программа GENLIMODS.EXE разместит все сгенерированные LOM-файлы в одном каталоге. После установки файла параметров нужно поместить двоичные файлы LIMODSDLL.DLL, BUGSLAYERUTIL.DLL, GENLIMODS.EXE и LIMODS.EXE в каталог, который должен быть предварительно указан в переменной среды PATH.



LOM-файлы


Просматривая листинг 14.2, нетрудно заметить, что формат LOM-файлов, генерируемых программой GENLIMODS.EXE, в точности соответствует формату файлов *.INI. В первой секции ([Module info]) хранится главная информация того модуля, который использовался при построении LOM-файла (включая имя модуля, его базовый адрес и метку даты/времени). Когда LIMODSDLL.DLL просматривает модуль в памяти, он проверяет его по этой части LOM-файла; если метка даты/времени модуля отличается от соответствующей метки LOM-файла в LIMODSDLL.DLL, то программа GENLIMODS.EXE генерирует новый LOM-файл для этого модуля. Я сохраняю базовый адрес модуля для того, чтобы LIMODSDLL.DLL мог повторно, "на лету" вычислять диапазоны адресов (в случае перемещения модуля загрузчиком образа). LIMODSDLL.DLL также сообщит пользователю (через вызов функции outputoebugstring), что модуль был перемещен.

Листинг 14-2. Пример LOM-файла 

[Module Info] 

DateTimeStamp=380b75e8

BaseAddress=400000 

ModuleName=LIMODS.exe

[Ranges]

RangeCount=11

Range0=0x004017D0|0x00401C8E|0 ID:\Book\SourceCode\LIMODS\About.cpp

Rangel=0x00401EF0|0x00402313|0 ID:\Book\SourceCode\LIMODS\BigIcon.CPP

Range2=0x00402430|0x00402A5E|0|D:\Book\SourceCode\LIMODS\LIMODS.cpp

Range3=0x00402D60| 0x00403727111D:\Book\SourceCode\LIMODS\LIMODSDoc.cpp

Range4=0x004044B0 0x0040480010|D:\Book\SourceCode\LIMODS\LIMODSOptions.cpp

Range5=0x00404950I 0x00405823|0 ID:\Book\SourceCode\LIMODS\LIMODSView.cpp

Range6=0x00405D70|0x00405DB0|0 ID:\Book\SourceCode\LIMODS\LIMODSDoc.h

Range7=0x00406150|0x0040752110 ID:\Book\SourceCode\LIMODS\LOMFile.cpp

Range8=0x00408D00|0x004090FF|0|D:\Book\SourceCode\LIMODS\MainFrm.cpp

Range9=0x00409270 I 0x00409516|0 ID:\Book\SourceCode\LIMODS\OptionsDialog.cpp

RangelO=0x0040A0A0|0x0040A140I 0 Iappmodul.cpp

[Sources]

Source0=0|D:\Book\SourceCode\LIMODS\About.cpp

Sourcel=0|D:\Book\SourceCode\LIMODS\BigIcon.CPP

Source2=0|D:\Book\SourceCode\LIMODS\LIMODS.cpp

Source3=lID:\Book\SourceCode\LIMODS\LIMODSDoc.cpp


Source4=0|D:\Book\SourceCode\LIMODS\LIMODSOptions.cpp

Source5=0ID:\Book\SourceCode\LIMODS\LIMODSView.cpp

Source6=0|D:\Book\SourceCode\LIMODS\LIMODSDoc.h

Source7=0|D:\Book\SourceCode\LIMODS\LOMFile.cpp

Source8=0|D:\Book\SourceCode\LIMODS\MainFrm.cpp

Source9=0|D:\Book\SourceCode\LIMODS\OptionsDialog.cpp

Sourcel0=0|appmodul.cpp

SourceCount=l1

В секции [Ranges] указаны ( в определенном формате) диапазоны адресов исходных файлов. Именно эту секцию, прежде всего, и использует LIMODSDLL.DLL, чтобы определить, какие предложения трассировки показывать и когда их показывать. Поля каждой записи этой секции расположены в следующем порядке: адрес начала диапазона, адрес конца диапазона, булевское значение (флажок) показа трассы и имя исходного файла. Секция [Sources] используется LIMODS.EXE, чтобы показать имена исходных файлов в их полной форме (с именем диска и полным путем в дереве каталогов). Первоначально формат INI-файла был выбран, чтобы облегчить исходное тестирование и скрыть его в специальном классе доступа в файлах LOMFILE.H и LOMFILE.CPP. В дальнейшем выяснилось, что производительность LIMODS.EXE вполне приемлема, поэтому я так и не перешел "к другому формату.



Обработка _сdесl-подключений


Как показано в главе 12, _stdcail-функции легко подключать, потому что такая функция сама чистит стек; для _cdeci-функций стеки чистит вызывающая функция.

_stdcaii _cdeci — спецификаторы соглашений о вызовах функций в языках C/C++ (см. табл. 6.3 главы 6). — Пер

Кроме того, функции DiagOutputA, DiagOutputW И AfxTrace, имеют параметры переменной длины, так что перехватить их намного труднее. Само подключение — такое же, как и для экспортируемых _stdcaii-функций, но обработка _cdeci-функций должна быть совсем другой. В LIMODSDLL.DLL требовалось, чтобы функция подключения захватила адрес возврата и определила, является ли он диапазоном адресов, в котором пользователь хочет видеть предложения трассировки. После проверки источника функция трассировки либо выполняется, либо игнорируется, после чего выполняется возврат в вызывающую. Для _stdcaii-функций эта обработка очень проста. Можно напрямую вызвать функцию трассировки и возвратиться прямо из функции подключения в вызывающую функцию, потому стек очищается внутри функции подключения. Для _cdeci-функций нужно вернуть стек обратно в первоначальное состояние и затем, если необходимо выполнить функцию трассировки, перейти к ней (а не вызвать ее!).

 Листинг 14-3. cdecl-функция подключения с расширенным макросом 

VOID NAKEDDEF LIMODS_DiagOutputA ( void)

{

// Содержит адрес возврата вызывающей функции

DWORD_PTR dwRet;

// Содержит сохраненный регистр ESI, поэтому отладочные построения

// Visual C++ 6 работают. (ESI использует функцию chkesp,

// вставляемую ключом компилятора /GZ.)

DWORD_PTR dwESI;

_asm PUSH EBP /* Установить стандартный кадр. */

_asm MOV EBP, ESP

_asm SUB ESP, _LOCAL_SIZE /* Сохранить место для локальных */

/* переменных. */

_asm MOV EAX, EBP /* EBP указатели на исходный стек.*/

 _asm ADD EAX, 4 /* Счетчик для PUSH EBP. */ 

_asm MOV EAX, [EAX] /* Получить адрес возврата. */ 

_asm MOV [dwRet], EAX /* Сохранить адрес возврата. */

 _asm MOV [dwESI], ESI /* Сохранить ESI, чтобы в отладочных*/


/* построениях работала chkesp. */

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

// для показа. После этого вызова возвращаемое значение находится в

 // ЕАХ и затем проверяется. Возврат TRUE означает выполнение функции

 // трассировки, a FALSE — пропуск функции трассировки.

 ChecklfAddressIsOn ( dwRet);

_asm MOV ESI, [dwESI] /* Восстановить ESI. */

 _asm ADD ESP, _LOCAL_SIZE /* Исключить область локальных

/* переменных. */

_asm MOV ESP, EBP /* Восстановить стандартный кадр. */

 _asm POP EBP

// Здесь и начинается функция! Четыре предшествующих строки

 // ассемблерного кода восстанавливают стек точно до того состояния,

 //в котором он был до входа в эту функцию, поэтому теперь можно 

// перейти к функции трассировки. Функция pReadDiagOutputA содержит 

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

 _asm TEST ЕАХ, ЕАХ /* Проверить ЕАХ на 0. */

 _asm JZ IblDiagOutputA /* Если ЕАХ содержит 0, просто

/* выполнить возврат.*/ 

_asm JMP pReadDiagOutputA /* Сделано! JUMP выполняет возврат */

/* в вызывающую, а не в эту функцию. */

 IblDiagOutputA:

/* Пропущенный TRACE! Просто выполнить */

_asm RET /* возврат в вызывающую функцию. */

 }

В листинге 14-3 показана функция подключения с расширенным макросом, которая подключает функцию DiagOutputA из BUGSLAYERUTIL.DLL. Чтобы облегчить повторное использование общих подпрограмм языка ассемблера, таких как _cdeci-код пролога, в LIMODSDLL.CPP определены несколько макросов языка ассемблера (для использования в функциях подключения). Настоятельно рекомендую читателям выполнить пошаговый проход этих макросов в окне Disassembly отладчика Visual C++, чтобы наблюдать каждую инструкцию в действии.



Общие проблемы реализации


Разобравшись с подключением экспорта по порядковым значениям, я больше не сталкивался со сколько-нибудь серьезными проблемами при реализации LIMODS в целом. Одно интересное свойство, реализованное в LIMODS.EXE, — это автоматическая отметка элементов управления видом дерева (см. рис. 14.1). Если установить или снять отметку корневого узла в представлении дерева (имя модуля), автоматически выполняется установка (или снятие) отметок всех дочерних узлов (исходных файлов). Для выполнения подобной работы нужно было организовать уведомление о переключении метки. Все это реализовано в файле LIMODSVIEW.CPP, который можно найти на сопровождающем компакт-диске.

Самая большая проблема, возникшая при реализации LIMODS, была связана с библиотекой стандартных шаблонов (STL). Я допускаю, что разработчики STL умнее меня, но, тем не менее, я не ожидал что, Visual С++-код STL окажется настолько непроницаемым. Одна расшифровка ошибок компиляции заняла очень много времени. Кроме того, было чрезвычайно сложно разобраться, в какой части кода произошел отказ, или как что-то в нем работает. В соответствии с рекомендациями главы 2, я использовал предупреждения компилятора 4-го уровня и рассматривал все предупреждения как ошибки, и при компиляции кода STL был завален предупреждениями С4786 ("'identifier': identifier was truncated to '255' characters in the debug information" — '"идентификатор1: в отладочной информации был усечен до '255' символов") для любого класса из шаблонов STL, который имеет более двух символов в своем имени.

Секрет предупреждений С4786 состоит в том, что отключать их надо через директиву #pragma warning перед включением любых файлов заголовков STL. Кроме того, методика, использующая директиву #pragma warning, работает лучше, если в главный предварительно компилированный заголовочный файл включить только STL-заголовки, а обычные предупреждения в предварительно компилированном заголовке отключите раз и навсегда. Хотя при этом и пришлось немного повозиться, но все же удалось сэкономить время, используя STL вместо реализации своих собственных возрастающих массивов и классов отображений (map classes).


Что касается последней проблемы, с которой я столкнулся, то не могу сказать, была ли это ошибка в компиляторе или недопонимание с моей стороны. В LIMODSDLL.DLL применяется статический массив HOOKFUNCDESC, содержащий указатели реальных функций для DiagOutputA и DiagOutputw из BUGSLAYERUTIL.DLL. В функциях подключения используется указатель реальной функции вне структуры как пункт назначения для оператора перехода. Проблема заключалась в том, что ссылка на второй элемент в массиве создавала недействительную ссылку. Исходная строка встроенного ассемблера

JMP g_stBugslayerUtilRealFuncs [0] .рРгос

генерировала строку кода языка ассемблера

JMP g_stBugslayerUtilRealFuncs+4h

которая была правильной. Однако исходная строка, которая сослалась на второй элемент в структуре:

JMP g_stBugslayerUtilRealFuncs[l].pProc

генерировала

JMP g_stBugslayerUtilRealFuncs+5h,

тогда как я думал, что она должна генерировать

JMP g_stBugslayerUtilRealFuncs+OCh

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

JMP g_stBugslayerUtilRealFuncs[0x8].рРгос

Это частная проблема, и она может возникнуть, только если разработчик добавит к LIMODSDLL.DLL собственные специальные функции трассировки. Добавляя такие функции, используйте в качестве примеров таблицы из BUGSLAYERUTIL.DLL.



Определение диапазонов исходного кода


Читатель, вероятно, не очень удивится тому, что здесь снова используется символьная машина DBGHELP.DLL (подробнее она описана в главе 4). Я полагал, что ее функции, работающие с именами исходных файлов и номерами строк исходного кода, позволят отыскивать адреса первой и последней строк рабочих кодов конкретного исходного файла (эту пару адресов я и называю диапазоном адресов). Вооружившись диапазоном адресов и методикой, которая обсуждалась в главе 12, можно было бы подключить функцию OutputDebugString и по адресу возврата определить, находится ли она в диапазоне адресов того исходного файла, из которого пользователь хочет получать предложения трассировки. Хотя этот подход теоретически довольно прост, но для его практической реализации мне пришлось изрядно потрудиться.

Не существует специальной API-функции, которая перечисляет диапазоны адресов исходных файлов, но я полагал, что смог бы обойтись функцией перечисления символов SymEnumerateSymbols. С помощью этой функции Я хотел извлечь первый символ, затем (находясь уже внутри собственной функции перечисления символов) переместиться обратно к началу исходного файла (с помощью функции SymGetLinePrev) и затем перейти к его концу с помощью функции symGetLineNext. В случае простых тестов функция SymEnumerateSymbols работала великолепно, но когда я провел аналогичное тестирование программы GENLIMODS.EXE, то заметил, что диапазоны исходного кода, полученные с помощью SymEnumerateSymbols, не соответствовали тому, что показывал дизассемблер при тестировании GENLIMODS.EXE. Казалось, что при моей методике пропускаются целые секции исходного файла.

Когда я вручную вычислил диапазоны, они получились похожими на перечисленные в табл. 14.1. Проблема возникала из-за того, что функции SymGetLineNext И SymGetLinePrev перечисляют только смежные (непрерывные) диапазоны. Из табл 14.1 видно, что исходные файлы со встроенными (inline) функциями находятся между первой и второй частями GENLIMODS.CPP. Я быстро понял, что это не ошибка, а скорее следствие работы компилятора.
Непонимание наблюдалось с моей стороны: я сосредотачивал свое внимание на исходном файле, когда на самом деле нужно было в первую очередь думать о диапазонах адресов.

Таблица 14.1. Примеры диапазонов адресов GENLIMODS.EXE

Начало

Конец

Исходный файл

0x00401900

0x0040 1A8A

COMMANDLINE.CPP

0x00401000

Ox00402F1F

GENLIMODS.CPP

0x00403450

0x00403774

RESSTRING.H

0x00403700

0x00403700

GENLIMODS.H

0x00403060

Ox004040F9

SYMBOLENGINE.H

0x00404690

Ox004046AC

GENLIMODS.CPP

0x00407080

Ox0040852E

LOMFILE.CPP

0x00409050

Ox0040A532

READIGNOREFILES.CPP

Ox0040C800

0x00400894

VERBOSE.CPP





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


Должен честно сказать, что почти не поддерживал функций подключения, экспортируемых по порядковому значению, потому что подобная попытка весьма чревата ошибками (из-за того, что разные версии MFC DLL используют различные порядковые значения). Однако если абстрагироваться от проблем, связанных с версиями, то процесс подключения по порядковому значению почти идентичен подключению по имени. Сравните функцию HookordinaiExport, показанную в листинге 14-4, с функцией HookimportedFunctionsByName, рассмотренной в главе 12, и вы увидите, что обе функции выполняют много одинаковых действий.

Листинг 14-4. Функция HookordinaiExport 

BOOL BUGSUTILJ3LLINTERFACE _stdcall

HookordinaiExport ( HMODULE hModule , 

LPCTSTR szImportMod, 

DWORD dwOrdinal ,

 PROC pHookFunc , 

PROC * ppOrigAddr )

{

// Проверить параметры с помощью утверждений.

 ASSERT ( NULL != hModule);

ASSERT ( FALSE == IsBadStringPtr ( szImportMod, MAX_PATH)); 

ASSERT ( 0 != dwOrdinal);

ASSERT ( FALSE = IsBadCodePtr ( pHookFunc));

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

if ( ( NULL == hModule ' ) | |

 ( TRUE == IsBadStringPtr ( szImportMod, MAX_PATH)) || 

( 0 == dwOrdinal ) I I ( TRUE == IsBadCodePtr ( pHookFunc) ) )

 {

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

 return ( FALSE); 

}

if ( NULL != ppOrigAddr)

{

ASSERT ( FALSE ==

IsBadWritePtr ( ppOrigAddr, sizeof ( PROG))); 

if ( TRUE == IsBadWritePtr ( ppOrigAddr, sizeof ( PROC)))

 {

SetLastErrorEx ( ERROR_INVALID_PARAMETER, SLE_ERROR);

 return ( FALSE);

 }

 }

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

 PIMAGE_IMPORT_DESCRIPTOR plmportDesc =

GetNamedlmportDescriptor ( hModule, szImportMod);

 if ( NULL == plmportDesc) 

{

// Запрошенный модуль не был импортирован. Не возвращать ошибку,

 return ( TRUE); 

}

// Получить информацию об исходных переходниках для этого DLL

. // Невозможно использовать информацию переходников, хранящуюся в


 // pImportDesc->FirstThunk, т. к. загрузчик уже изменил этот массив 

// при установке всех импортов. Исходный переходник обеспечивает

 // доступ к именам функций.

 PIMAGE_THUNK_DATA pOrigThunk =

MakePtr ( PIMAGE_THUNK_DATA

hModule , 

pImportDesc->OriginalFirstThunk );

// Получить массив p!mportDesc->FirstThunk, в котором будут

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

PIMAGE_THUNK_DATA pRealThunk = MakePtr ( PIMAGE_THUNK_DATA ,

hModule , 

pImportDesc->FirstThunk );

// Флажок будет устанавливаться из переходника,

// что облегчает его поиск.

DWORD dwCompareOrdinal = IMAGE JDRDINAL_FLAG | dwOrdinal;

// Цикл поиска подключаемых функций.

while ( NULL != pOrigThunk->ul.Function)

{

// Отыскивать только функции, которые импортируются по

 // порядковому значению, а не по имени, 

if ( IMAGE__ORDINAL_FLAG ==

( pOrigThunk->ul.Ordinal & IMAGE_ORDINAL_FLAG))

 {

// Найдена ли функция подключения? 

if ( dwCompareOrdinal == pOrigThunk->ul.Ordinal) . 

{

// Функция для подключения найдена. Теперь нужно

 // изменить защиту памяти на "read-write" (для записи),

 // прежде чем перезаписывать указатели функций. Заметьте,

 // что ничего не записывается в реальную область 

// переходников!

MEMORY_BASIC__IN FORMATION mbi_thunk ; 

VirtualQuery ( pRealThunk , 

 &mbi_thunk , 

sizeof ( MEMORY_BASIC_INFORMATION) ); 

if ( FALSE == VirtualProtect ( mbi_thunk.BaseAddress,

rabi_thunk.RegionSize ,

 PAGE_READWRITE ,

&mbi_thunk.Protect ))

 {

ASSERT ( !"VirtualProtect failed!");

 // Здесь приходится фиксировать неуспешное 

// выполнение функции (возвращая FALSE),

 // предварительно указав причину ошибки.

 SetLastErrorEx ( ERROR__INVALID_PARAMETER,

SLE^ERROR );

return ( FALSE); 

}

// Сохранить исходные адреса, если требуется

 if ( NULL != ppOrigAddr)



 {

*ppOrigAddr = (PROC)pRealThunk->ul.Function; 

}

// Microsoft имеет два различных определения 

// РIМАСЕ_ТШ№С_ОАТА-полей для будущей поддержки Win64

 // Будет использован самый последний набор заголовков

//из W2K RC2 Platform SDK, с которыми будут иметь дело

 // заголовки из Visual C++ 6 Service Pack 3.

// Подключить функцию (DWORD*)SpRealThunk->ul.Function; 

*pTemp = (DWORD)(pHookFunc); 

DWORD dwOldProtect;

// Изменить защиту обратно к тому состоянию, которое

 // предшествовало переписыванию указателя функции.

 VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress,

mbi_thunk.RegionSize ,

 mbi_thunk.Protect , 

sdwOldProtect )); 

// Жизнь прекрасна! 

SetLastError ( ERROR_SUCCESS);

 return ( TRUE);

 } 

}

// Инкремент обеих таблиц. pOrigThunk++; pRealThunk++;

 }

// Ничего не было подключено. Технически это не ошибка. Это просто

 // означает, что модуль импортирован, а функция — нет.

 SetLastError ( ERROR_SDCCESS);

 return ( FALSE); 

}

Для реализации обработки AfxTrace без ее подключения, пришлось бы просматривать стек при каждом вызове, чтобы вернуться к реальному вызову функции OutputDebugstring. Дополнительная работа на каждом вызове была бы медленнее, по сравнению с прямым подключением AfxTrace. Кроме того, если бы я игнорировал AfxTrace, то утилита LIMODS была бы, в основном, бесполезна для MFC-программистов. И, наконец, я предпочел создавать утилиту LIMODS настолько полной, насколько это возможно, причем я был вынужден дважды проверять версии MFC DLL.



Поиск решения


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

Другим возможным решением было включение в программу уникального макроса трассировки. Пытаясь решить проблему слишком большого количества трассировок в огромной команде разработчиков, я опробовал и такой подход. Каждая "подкоманда" получала уникальный макрос трассировки для своей специфической секции проекта. Подобный подход реализует библиотека классов MFC (Microsoft Foundation Class), внутренняя трассировка которой включается с помощью специальной программы TRACER.EXE. Предложения внутренней трассировки MFC проверяют глобальную переменную-флажок, и если бит, назначенный подсистеме, установлен, в окне Output появляются предложения трассировки. В моей большой команде подход с уникальным макросом некоторое время работал, но постепенно перестал применяться, потому что MFC-мастера (wizards) сами генерировали специальный трассировочный макрос, и разработчики забывали использовать свой уникальный макрос. Чтобы заставить разработчиков использовать правильный макрос, я пробовал бороться с этой проблемой, отменяя определение макроса TRACE в исходных файлах (с помощью директивы #undef), но обнаружил, что наличие макросов, устанавливаемых и мастером, и еще кем-то, сильно раздражает разработчика. Другая проблема состоит в том, что этот макрос не так просто расширить. В общем, нужно тратить довольно много времени на предварительный перенос макроса трассировки с проекта на проект. Становится также проблемой поддержка таких макросов при изменении архитектуры программы и перемещении кода из одной подсистемы в другую. Кроме того, подход с уникальным макросом не работает с программами на Visual Basic.

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

В отличие от других глав этой книги, проблемы, описанные в предыдущем разделе, я решал независимо друг от друга. Чтобы построить систему, которая ограничивает количество предложений трассировки, нужно только связать индивидуальные решения вместе. Кажется, мне удалось достичь заветной цели повторного использования кода (см. ниже раздел "Реализация LIMODS" данной главы).



Работа с MFC


Если планируется применять LIMODS с приложением, которое использует либо MFC42D.DLL, либо MFC42UD.DLL, нужен дополнительный шаг для ее установки. К сожалению, существует по крайней мере 1001 версия этих двух очень важных DLL. LIMODS должна знать, какое порядковое значение соответствует экспортируемой функции AfxTrace из каждого DLL-файла MFC. Хотя можно предположить, что AfxTrace будет всегда иметь одинаковое порядковое значение, независимо от того, в какой DLL она находится, на самом деле это не так. Дистрибутивный LIMODS.INI содержит информацию о файлах MFC42(U)D.DLL, которые используются с Visual C++ 5 без Service Pack, Visual C++ 6 без Service Pack, Visual C++ 6 с Service Pack 1 (SP1), Visual C++ 6 с Service Pack 2 (SP2) и Visual C++ 6 Service Pack 3 (SP3).

Если установленная версия Visual C++ отличается от перечисленных в LIMODS.INI (включая любые последующие выпуски Service Pack), то придется проделать некоторую дополнительную работу, чтобы гарантировать получение утилитой LIMODS правильного экспорта из MFC42D.DLL и MFC42UD.DLL. Информацию о версии библиотечного файла %SYSTEMROOT%\System32\MFC42(U)D.DLL можно получить, если щелкнуть правой кнопкой мыши на этом файле в Проводнике Windows и выбрать пункт Свойства в раскрывшемся контекстном меню. Затем следует перейти на вкладку Версия диалоговой панели Свойства. Первый пункт этой вкладки (Версия файла) и является искомым номером версии. Например, версия файла MFC42D.DLL, которую использует Visual C++ 6.0 SP3, имеет номер 6.00.8447.0.

Затем нужно перейти в подкаталог \MFC\SRC\Intel каталога Visual C++. Там можно найти DEF-файлы, которые использовались для компоновки MFC. Имена DEF-файлов соответствуют именам двоичных файлов. Например, MFC42D.DEF является DEF-файлом для MFC42D.DLL. Откройте соответствующий DEF-файл и отыщите текст ?AfxTrace@@YAXPBpzz для MFC42D.DLL. Для MFC42UD.DLL найдите текст ?AfxTrace@@YAXPBGZZ. Эта важная строка будет похожа на следующую:

?AfxTrace@@YAXPBDZZ @ 1179 NONAME

Число после знака @ — порядковое значение экспортируемой функции AfxTrace.
Запомните это число: его нужно будет ввести в файл LIMODS.INI.

Откройте свою копию LIMODS.INI. Для MFC42D.DLL ищите секцию [MFC42D.DLL Hack-0-Rama], а для  MFC42UD.DLL— секцию [MFC42UD.DLLHack-o-Rama]. Примерный вид секции для MFC42D.DLL показан ниже (секция для MFC42UD.DLL выглядит примерно так же):

[MFC42D.DLL Hack-0-Rama] 

VerCount=3

; VC 6.0 SP3 

VerO=6.00.8447.0,1179 ;

VC 6.0 SP1 and SP2.

Verl=6.00.8267.0,1179 ; 

VC 6.0 NO SERVICE PACKS

Ver2=6.00.8168.0,1179

Параметры verN определяют номера версий и их порядковые значения. Первое число — номер версии MFC-файла, а второе — порядковое значение функции AfxTrace. Добавьте1 версию вашего MFC-файла и порядковое значение функции AfxTrace В конец секции [MFC42D.DLL Hack-0-Rama] ИЛИ [MFC42UD.DLL Hack-0-Rama]. Например, если вы работаете с Visual C++ 5 без Service Pack, то добавьте строку ver3=4.21.7022,1253 к предшествующему примеру (для MFC42D.DLL). Нужно также увеличить на 1 значение счетчика vercount (vercount=4). Для MFC42UD.DLL в Visual C++ 5 без Service Pack строка была бы такой:

Ver3=4.21.7022,1256.

Если вы модифицируете исходный код MFC и строите собственную MFC42(U)D.DLL, то можно проверять информацию порядкового значения точно так же, как описано выше. Лучше, однако, не изменять исходный код MFC и создавать собственную версию MFC (если, конечно, вы не испытываете желания вручную обновлять каждое исправление MFC-ошибки в своих версиях исходных файлов).

 Эту операцию нужно выполнять только в том случае, когда в списке версий нет параметра ParN для вашей версии системы программирования. — Пер.

LIMODS проверяет версию MFC-файла, когда MFC42(U)D.DLL загружается в память. Если в LIMODS.INI нет соответствующей версии файла, то будет выведено (в окне Output) сообщение об ошибке, сопровождаемое несколькими звуковыми сигналами. Если компилировать и выполнить программу тестирования LIMODS (\SourceCode\LIMODS\TestLIMODS), поставляемую на сопровождающем компакт-диске, то можно быстро выяснить, правильно ли сконфигурирована утилита LIMODS.



Реализация LIMODS


Реализация LIMODS оказалась весьма интересной. Например, чтобы заставить работать символьную машину DBGHELP.DLL, пришлось прибегнуть к некоторым хитростям, но действительный интерес представляет разработка _cdeci-функций, подключающих импортируемые функции, и подключение импортируемых функций по их порядковым номерам.



В этой главе рассматриваются некоторые



В этой главе рассматриваются некоторые проблемы, связанные с ограничением количества предложений трассировки, и объясняется, почему утилита LIMODS, контролирующая эти предложения в больших командах разработчиков, так сложна. Ключевым требованием при разработке LIMODS было обеспечение простоты ее использования. На данный момент LIMODS требует изменения только одной строки исходного кода в Visual С++-программах и несколько большего количества строк в программах Visual Basic.
LIMODS разрабатывалась довольно долго. Первоначальная идея LIMODS возникла у меня пять лет назад, но до сих пор не удалось сделать утилиту максимально неинтрузивной и удобной в работе. С помощью LIMODS разработчики могут использовать любые предложения трассировки, какие захотите.
 Неинтрузивная (nonintrusive) программа — программа, не изменяющая режима работы другой, связанной с ней программы. — Пер.

Требования к LIMODS


Начнем, как обычно, с определения требований, сформулированных к программе LIMODS во время ее разработки. Ключевое требование — простота использования этой утилиты. Чем проще работать с инструментом, тем больше вероятность того, что он найдет применение. Итак:

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

Прежде чем перейти к обсуждению реализации LIMODS, поговорим о том, как нужно использовать данную программу. Это значительно облегчит понимание ее реализации.



Выбор исходных файлов для трассировки


Все файлы *.LOM для модулей, компилированных с символами отладки, будут генерироваться во время первого же выполнения приложения с загруженной библиотекой LIMODSDLL.DLL, так что при выполнении приложения возникнет небольшая пауза. LIMODS сохраняет LOM-файлы неизменными вплоть до даты изменения программных модулей, причем отслеживание этих изменений и модификация соответствующих ШМ-файлов выполняются автоматически. При желании генерацию LOM-файлов можно сделать частью процесса нормального построения приложения, используя программу GENLIMODS.EXE. Чтобы просмотреть описание параметров GENLIMODS.EXE, нужно запустить ее в командной строке без параметров. После того как LOM-файлы загружены, просто запустите LIMODS.EXE и выберите, какие исходные файлы и из каких модулей вы хотите просматривать на предмет трассировки. На рис. 14.1 показан LIMODS.EXE в действии. Установите флажки тех исходных файлов, чьи предложения трассировки необходимо просматривать.

Рис. 14.1. LIMODS.EXE в действии



Вызов LIMODS из кода


Вложив столько усилий в гарантии правильного использования MFC DLL, можно слегка разочароваться, узнав, что утилита LIMODS заработает, если добавить в исходный код С/С++-приложения всего лишь одну строку:

LoadLibrary ( "LIMODSDLL.DLL");

Основная часть работы LIMODSDLL.DLL сосредоточена в ее функции DllMain, так что никаких других функций вызывать не нужно. Полагаю, что добавление одной строки кода — не слишком большая цена за услуги LIMODS.

Если разрабатываются Visual Basic-программы, то заставить работать LIMODS в их коде немного сложнее, — но не очень. Сначала нужно копировать файлы LIMODS.CLS и INDESIGNMOD.BAS из каталога \SourceCode \LIMODS\VB сопровождающего компакт-диска и добавить их к вашему проекту. Затем нужно создать глобальный экземпляр класса CISLIMODS, показанного в листинге 14-1. Я бы рекомендовал именовать глобальную переменную экземпляра как CLIMODS. Класс CISLIMODS содержит только один метод; его имя Trace, а его входные параметры — такие же, как у метода Debug.Print. Нет никакой возможности перехватить внутренний объект Debug.Print, поэтому, чтобы утилита LIMODS работала в приложении Visual Basic, это приложение нужно компилировать. Если запускать такую программу из IDE, то метод Trace класса CISLIMODS преобразуется в вызов метода Debug. Print, вот почему предложения трассировки можно будет увидеть в окне Immediate.

Кроме того, в проекте нужно определить режим условной компиляции LIMODS=-1, что позволит при компиляции получать необходимые методы класса. Если этого не сделать, то будут получены пустые версии. Условная компиляция позволяет избежать дополнительных расходов на применение LIMODS в тех случаях, когда она не нужна, а расходы на пустые функции можно исключить, окружив все обращения к объекту ClsLIMODS директивами условной компиляции.

Листинг 14-1. LIMDOS.CLS

VERSION 1.0 CLASS

BEGIN

MultiUse = -1 'True

Persistable = 0 'NotPersistable

DataBindingBehavior = 0 'vbNone

DataSourceBehavior = 0 'vbNone

MTSTransactionMode = 0 'NotAnMTSObject


 END

Attribute VB_Name = "clsLIMODS" 

Attribute VB_GlobalNameSpace = False 

Attribute VB_Creatable = True 

Attribute VB_PredeclaredId = False

 Attribute VB_Exposed = False

''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

'Copyright (c) 1997-2000 John Robbins — All rights reserved. 

' "Debugging Applications" (Microsoft Press)

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

' Вспомогательный класс LIMODS для разработки приложений Visual Basic

' 1. Включить этот файл класса в Visual Basic-проект и создать в нем

' глобальный экземпляр этого класса. ( Я назвал экземплярную

' переменную "cLIMODS".)

' 2. Чтобы отправлять отсюда предложения трассировки, нужно просто

' вызвать метод cLIMODS.Trace.

' 3. LIMODS активен только в компилированном Visual Basic-приложении.

' Если выполнять объект этого класса под отладчиком VB-IDE, то

' предложения трассировки будут посылаться с помощью регулярного

' оператора трассировки Debug.Print.

'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

Option Explicit

Private Declare Function LoadLibrary Lib "kerne!32" _

 Alias "LoadLibraryA" _

(ByVal IpLibFileName As String) As Long

 Private Declare Sub OutputDebugString Lib "kerne!32" _

 Alias "OutputDebugStringA" _ 

(ByVal IpOutputString As String)

Private Declare Function GetModuleFileName Lib "kerne!32" _ 

Alias "GetModuleFileNameA"

 _ (ByVal hModule As Long, _ 

ByVal IpFileName As String, _ 

ByVal nSize As Long) As Long

Private Declare Function GetModuleHandle Lib "kerne!32" _ 

Alias "GetModuleHandleA" _

(ByVal IpModuleName As String) As Long

Private m_Is!nIDE As Boolean

#If LIMODS Then 

Private Sub Class_Initialize()

 Dim blsInlDE As Boolean 

blsInlDE = InDesign()

' If blsInlDE is False, the main module isn't the Visual Basic IDE, 

' so I can load LIMODSDLL.DLL. 

If (False = blsInlDE) Then

LoadLibrary "LIMODSDLL.DLL" 

m_Is!nIDE = False Else

m_IsInIDE = True 

End If 

End Sub

#End If

#If LIMODS Then

Public Sub Trace(sOut As Variant) 

If (True = m_Is!nIDE) Then

Debug.Print sOut

 Else

Dim s As String 

s = sOut

OutputDebugString s 

End If 

End Sub

#Else ' LIMODS is *not* conditionally defined.

 Public Sub Trace(sOut As Variant)

 End Sub

#End If