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

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

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

ГЛАВА 3 Отладка при кодировании

101

 

 

 

 

 

 

String LocalStr =

 

Req.ServerVariables.Get ( "LOCAL_ADDR" ) ;

 

// Сравниваем локальный IP адрес с IP адресом запроса.

 

bRet = Req.UserHostAddress.Equals ( LocalStr ) ;

 

}

 

return ( bRet ) ;

 

}

///<summary>

///Ищет на странице элементы управления утверждений.

///</summary>

///<remarks>

///Все элементы управления утверждений носят имя "AssertControl",

///так что этот метод просто просматривает набор элементов

///управления на странице и ищет это имя. Кроме того,

///он рекурсивно просматривает вложенные элементы.

///</remarks>

///<param name="CtlCol">

///Набор элементов для просмотра.

///</param>

///<param name="AssertCtrl">

///Исходящий параметр, который содержит найденный элемент управления.

///</param>

private void FindAssertControl ( ControlCollection

CtlCol

,

out AssertControl

AssertCtrl )

{

// Просматриваем все элементы управления из массива. foreach ( Control Ctl in CtlCol )

{

// Это тот элемент?

if ( "AssertControl" == Ctl.GetType().Name )

{

// Да! Выходим.

AssertCtrl = (AssertControl)Ctl ;

return ;

}

else

{

// Если этот элемент имеет вложенные, просматриваем их тоже. if ( true == Ctl.HasControls ( ) )

{

FindAssertControl ( Ctl.Controls ,

out AssertCtrl ) ;

//Если один из вложенных элементов

//содержал искомый, то можно выходить. if ( null != AssertCtrl )

{

return ;

см. след. стр.

102 ЧАСТЬ I Сущность отладки

}

}

}

}

// В этом наборе его не нашли. AssertCtrl = null ;

return ;

}

}

Утверждения в приложениях C++

Многие годы в старой компьютерной шутке, сравнивающей языки программиро вания с машинами, C++ всегда сравнивают с болидом Формулы 1: быстрый, но опасный для вождения. В другой шутке говорится, что C++ дает вам пистолет, чтобы прострелить себе ногу, и, когда вы проходите «Hello World!», курок уже почти спу щен. Я думаю, можно сказать, что C++ — это болид Формулы 1 с двумя ружьями, чтобы вы могли прострелить себе ногу во время аварии. Тогда как даже малейшая ошибка способна обрушить ваше приложение, интенсивное использование утвер ждений в C++ — единственный способ получить шанс на отладку таких приложе ний.

C и C++ также включают все виды функций, которые помогут максимально подробно описать условия утверждений (табл. 3 2).

Табл. 3-2. Вспомогательные функции для описательных утверждений C и C++

Функция Описание

GetObjectType Функция подсистемы интерфейса графических устройств (GDI), возвращающая тип описателя GDI.

IsBadCodePtr Проверяет, что указатель на область памяти может быть запущен.

IsBadReadPtr Проверяет, что по указателю на область памяти можно считать указанное количество байт.

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

IsBadWritePtr Проверяет, что по указателю на область памяти можно записать указанное количество байт.

IsWindow

Проверяет, является ли параметр HWND допустимым окном.

 

 

Функции IsBad* небезопасны в многопоточной среде. В то время как один поток вызывает IsBadWritePtr, чтобы проверить права доступа к участку памяти, другой поток может менять содержимое памяти на которую указывает указатель. Эти функции дают вам лишь описание ситуации на отдельный момент времени. Не которые читатели первого издания этой книги утверждали, что, поскольку функ ции IsBad* небезопасны в многопоточной среде, их вообще лучше не трогать, раз они могут вызвать ложное ощущение безопасности. Категорически не согласен. Гарантировать полностью безопасную проверку памяти в многопоточной среде практически нельзя, если только вы не выполняете доступ к каждому байту в рамках

ГЛАВА 3 Отладка при кодировании

103

 

 

структурной обработки исключений. Такое возможно, но код станет настолько мед ленным, что вы не сможете работать на компьютере. Еще одна проблема, кото рую порой сильно преувеличивают, в том, что функции IsBad* в очень редких слу чаях могут проглатывать исключения EXCEPTION_GUARD_PAGE. За все годы, которые я занимаюсь разработкой под Windows, я никогда не сталкивался с этой пробле мой. Я, безусловно, согласен мириться с такими недостатками функций IsBad* за те преимущества, которые получаю от информированности о плохом указателе.

Следующий код демонстрирует одну из ошибок, которые я совершал в утвер ждениях C++:

// Неверное использование утверждения.

 

BOOL CheckDriveFreeSpace ( LPCTSTR szDrive )

 

{

 

ULARGE_INTEGER ulgAvail ;

 

ULARGE_INTEGER ulgNumBytes ;

 

ULARGE_INTEGER ulgFree ;

 

if ( FALSE == GetDiskFreeSpaceEx ( szDrive

,

&ulgAvail

,

&ulgNumBytes ,

&ulgFree

) )

{

 

ASSERT ( FALSE ) ;

 

return ( FALSE ) ;

}

}

Хотя я использовал правильное утверждение, я не отображал невыполненное условие. Информационное окно утверждения показывало лишь выражение «FALSE», что не очень то помогало. Используя утверждения, старайтесь сообщать в инфор мационном окне максимально подробную информацию о сбое утверждения.

Мой друг Дейв Энжел (Dave Angel) обратил мое внимание на то, что в C и C++ можно просто применить логический оператор NOT (!), используя строку в каче стве операнда. Такая комбинация дает гораздо лучшее выражение в информаци онном окне утверждения, так что вы хотя бы имеете представление о том, что случилось, не заглядывая в исходный код. Вот правильный способ утверждения условия сбоя:

// Правильное использование утверждения

 

BOOL CheckDriveFreeSpace ( LPCTSTR szDrive )

 

{

 

ULARGE_INTEGER ulgAvail ;

 

ULARGE_INTEGER ulgNumBytes ;

 

ULARGE_INTEGER ulgFree ;

 

if ( FALSE == GetDiskFreeSpaceEx ( szDrive

,

&ulgAvail

,

&ulgNumBytes ,

&ulgFree

) )

{

 

ASSERT ( !"GetDiskFreeSpaceEx failed!" ) ;

 

104 ЧАСТЬ I Сущность отладки

return ( FALSE ) ;

}

}

Фокус Дейва можно усовершенствовать, применив логический условный опе ратор AND (&&) так, чтобы выполнять нормальное утверждение и выводить текст сообщения. Вот как это сделать (заметьте: при использовании логического AND в начале строки не ставится «!»):

BOOL AddToDataTree ( PTREENODE pNode )

{

ASSERT ( ( FALSE == IsBadReadPtr ( pNode , sizeof ( TREENODE) ) ) &&

"Invalid parameter!"

) ;

 

 

}

 

Макрос VERIFY

Прежде чем перейти к макросам и функциям утверждений, с которыми вы стол кнетесь при разработке под Windows, а также к связанным с ними проблемам, я хочу поговорить о макросе VERIFY, широко используемом в разработках на осно ве библиотеки классов Microsoft Foundation Class (MFC). В отладочной сборке макрос VERIFY ведет себя, как обычное утверждение, поскольку он определен как ASSERT. Если условие равно 0, макрос VERIFY инициирует обычное информацион ное окно утверждения, предупреждая о проблемах. В финальной сборке макрос VERIFY не выводит информационного окна, однако его параметр остается в исход ном коде и вычисляется в ходе нормальной работы.

По сути макрос VERIFY позволяет создавать обычные утверждения с побочны ми эффектами, и эти побочные эффекты остаются в финальных сборках. В иде але ни в каких типах утверждений не следует использовать условия, вызывающие побочные эффекты. Однако макрос VERIFY может пригодиться: когда функция воз вращает код ошибки, который вы все равно не стали бы проверять иначе. Напри мер, если при вызове ResetEvent для очистки освободившегося описателя собы тия происходит сбой, то не остается ничего другого, кроме как завершить работу приложения, поэтому большинство разработчиков вызывает ResetEvent, не про веряя возвращаемое значение ни в отладочных, ни в финальных сборках. Если выполнять вызов через макрос VERIFY, то по крайней мере в отладочных сборках вы будете получать уведомления о том, что нечто пошло не так. Конечно, тот же результат можно получить и благодаря ASSERT, но VERIFY позволяет избежать со здания новой переменной только для хранения и проверки возвращаемого зна чения из вызова ResetEvent — переменной, которая скорее всего будет использо вана только в отладочных сборках.

Думаю, большинство программистов MFC использует макрос VERIFY потому, что им так удобнее, но попробуйте отказаться от этой привычки. В большинстве слу чаев вместо применения VERIFY следовало бы проверять возвращаемые значения. Хороший пример частого использования VERIFY — функция член CString::LoadString, загружающая строки ресурсов. Здесь макрос VERIFY сгодится для отладочных сбо

ГЛАВА 3 Отладка при кодировании

105

 

 

рок, так как при сбое LoadString он предупреждает вас об этом. Однако в финаль ных сборках сбой LoadString приведет к вызову неинициализированной переменной. Если повезет, вы получите пустую строку, но чаще всего это ведет к краху финальной сборки. Мораль: проверяйте возвращаемые значения. Если хотите задействовать макрос VERIFY, подумайте, не послужит ли игнорирование возвращаемого значе ния причиной проблем в финальных сборках.

Отладка: фронтовые очерки

Исчезающие файлы и потоки

Боевые действия

В работе над версией BoundsChecker в NuMega мы испытывали невероят ные трудности со случайными сбоями, которые было почти невозможно повторить. Единственной зацепкой было то, что описатели файлов и пото ков внезапно становились недействительными. Это означало, что файлы случайно закрывались и иногда нарушалась синхронизация потоков. Раз работчики пользовательского интерфейса также сталкивались с периоди ческими сбоями, но только при работе в отладчике. Наконец эти пробле мы привели к тому, что все члены команды прекратили свою работу и ста ли пытаться исправить эти ошибки.

Исход

Команда чуть было не облила меня смолой и не вываляла в перьях, потому что, как выяснилось, виноват в этой проблеме был я. Я отвечал за отладоч ные циклы в BoundsChecker. В отладочном цикле используется отладочный API Windows для запуска и управления другими процессами и отлаживае мой программой, а также для реакции на события отладки, генерируемые отладчиком. Как добросовестный программист, я видел, что функция WaitFor DebugEvent возвращала описатели для некоторых уведомлений о событиях отладки. Например, при запуске процесса в отладчике последний мог по лучать структуру, содержащую описатель процесса и начальный поток для него.

Я был очень осторожен и знал, что, если API предоставил описатель ка кого то объекта, который вам больше не нужен, следует вызвать CloseHandle, чтобы освободить память, занимаемую этим объектом. Поэтому, когда от ладочный API предоставлял описатель, я закрывал его, как только он мне становился не нужен. Это выглядело вполне оправданно.

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

Чтобы понять, как это привело к нашей проблеме, надо знать, что, ког да вы закрываете описатель, ОС помечает его значение как свободное. Micro

см. след. стр.

106 ЧАСТЬ I Сущность отладки

soft Windows NT 4, которую мы тогда использовали, весьма агрессивна в отношении повторного применения значений описателей. (Microsoft Win dows 2000/XP демонстрируют такую же агрессивность по отношению к значениям описателей.) Элементы нашего UI, интенсивно применявшие многопоточность и открывавшие много файлов, постоянно создавали и использовали новые описатели. Поскольку отладочный API закрывал мои описатели, и ОС обращалась к ним повторно, иногда элементы UI получа ли один из описателей, с которыми работал я. Закрывая позже свои копии описателей, я на самом деле закрывал потоки и описатели файлов UI!

Я едва избежал смолы и перьев, показав что эта же ошибка присутство вала в отладочных циклах предыдущих версий BoundsChecker. До сих пор нам просто везло. Разница в том, что та версия, над которой мы работали, имела новый улучшенный UI, гораздо интенсивнее работавший с файлами и потоками, что создало условия для выявления моей ошибки.

Полученный опыт

Если б я читал написанное мелким шрифтом в документации отладочного API, то избежал бы этих проблем. Кроме того — и это главный урок! — я понял, что нужно всегда проверять возвращаемые значения CloseHandle. Хотя, закрывая неверный поток, вы не сможете ничего предпринять, ОС сообщает вам, что что то не так, и к этому надо относиться со вниманием.

Замечу также: если, работая в отладчике, вы пытаетесь дважды закрыть описатель или передать неверное значение в CloseHandle, ОС Windows ини циируют исключение «Invalid Handle» (0xC0000008). Увидев такое значение исключения, можете прерваться и выяснить, почему это произошло.

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

Различные типы утверждений в Visual C++

Хотя в C++ я описываю все свои макросы и функции утверждений с помощью простого ASSERT, о котором расскажу через секунду, сначала все же хочу коротко остановиться на других типах утверждений, доступных в Visual C++, и немного рассказать об их использовании. Тогда, встретив какое нибудь из них в чужом коде, вы сможете его узнать. Кроме того, хочу предупредить вас о проблемах, возни кающих с некоторыми реализациями.

assert, _ASSERT и _ASSERTE

Первый тип утверждения из библиотеки исполняющей системы C — макрос assert из стандарта ANSI C. Эта версия переносима на все компиляторы и платформы C и определяется включением ASSERT.H. В мире Windows, если в работе с консоль ным приложением инициируется утверждение, assert направит вывод в stderr. Если ваше Windows приложение содержит графический пользовательский интерфейс, assert отобразит сведения о сбое в информационном окне.

ГЛАВА 3 Отладка при кодировании

107

 

 

Второй тип утверждения в библиотеке исполняющей системы C ориентиро ван только на Windows. В него входят утверждения _ASSERT и _ASSERTE, определен ные в CRTDBG.H. Единственная разница между ними в том, что вариант _ASSERTE также выводит выражение, переданное в виде параметра. Поскольку это выраже ние так важно, особенно при тестировании инженерами отладки, всегда выбирайте _ASSERTE, применяя библиотеку исполняющей среды C. Оба макроса являются ча стью исключительно полезного отладочного кода библиотеки исполняющей среды, и утверждения — лишь одна из многих его функций.

Хотя assert, _ASSERT и _ASSERTE удобны и бесплатны, у них есть недостатки. Макрос assert содержит две проблемы, способные несколько вас огорчить. Первая заклю чается в том, что отображаемое имя файла усекается до 60 символов, так что иногда вы не сможете понять, какой файл инициировал исключение. Вторая проблема assert проявляется в работе с проектами, не содержащими UI, такими как службы Windows или внепроцессные COM серверы. Поскольку assert направляет свой вывод в stderr или в информационное окно, вы можете его пропустить. В случае инфор мационного окна ваше приложение зависнет, так как вы не можете закрыть ин формационное окно, если не отображаете UI.

С другой стороны, макросы исполняющей среды C решают проблему с ото бражением по умолчанию информационного окна, позволяя через вызов функ ции _CrtSetReportMode перенаправить утверждение в файл или в функцию API OutputDebugString. Однако все поставляемые Microsoft утверждения страдают од ним пороком: они изменяют состояние системы, а это нарушение главного зако на для утверждений. Влияние побочных эффектов на вызовы утверждений едва ли не хуже, чем полный отказ от их использования. Следующий пример демонст рирует, как поставляемые утверждения могут вносить различия между отладоч ными и финальными сборками. Сможете ли вы обнаружить проблему?

//Направляем сообщение в окно. Если время ожидания истекло, значит, другой

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

//проверить сбой SendMessageTimeout — вызвать GetLastError.

//Если функция возвращает 0 и код последней ошибки 0,

//время ожидания SendMessageTimeout истекло.

_ASSERTE ( NULL != pDataPacket ) ;

 

if ( NULL == pDataPacket )

 

{

 

return ( ERR_INVALID_DATA ) ;

 

}

 

LRESULT lRes = SendMessageTimeout ( hUIWnd

,

WM_USER_NEEDNEXTPACKET

,

0

,

(LPARAM)pDataPacket

,

SMTO_BLOCK

,

10000

,

&pdwRes

) ;

_ASSERTE ( FALSE != lRes ) ;

 

if ( FALSE == lRes )

 

{

 

// Получаем код последней ошибки.

 

DWORD dwLastErr = GetLastError ( ) ;

 

108 ЧАСТЬ I Сущность отладки

if ( 0 == dwLastErr )

{

// UI завис или обрабатывает данные слишком медленно. return ( ERR_UI_IS_HUNG ) ;

}

//Если ошибка другая, значит, проблема в данных,

//передаваемых через параметры.

return ( ERR_INVALID_DATA ) ;

}

return ( ERR_SUCCESS ) ;

Проблема здесь в том, что поставляемые утверждения уничтожают код после дней ошибки. До проверки исполняется «_ASSERTE ( FALSE != lRes )», отобража ется информационное окно, и код последней ошибки меняется на 0. Так что в от ладочных сборках всегда будет казаться, что завис UI, а в финальных сборках про явятся случаи, когда переданные SendMessageTimeout параметры были неверны.

То, что предоставляемые системой утверждения уничтожают код последней ошибки, может никак не отразиться на вашем коде, но я видел и другое: две ошибки, на обнаружение которых ушло немало времени, были вызваны именно этой про блемой. Но, к счастью, если вы будете использовать утверждение, представленное ниже в этой главе в разделе «SUPERASSERT», я позабочусь об этой проблеме за вас и расскажу кое что, о чем не сообщают системные версии утверждений.

ASSERT_KINDOF и ASSERT_VALID

Если вы программируете, применяя MFC, в вашем распоряжении есть два допол нительных макроса утверждений, специфичных для MFC и являющих собой фан тастические примеры профилактической отладки. Если вы объявляли классы с помощью DECLARE_DYNAMIC или DECLARE_SERIAL, то, используя макрос ASSERT_KINDOF, можете проверить, является ли указатель на потомок CObject определенным классом или потомком определенного класса. Утверждение ASSERT_KINDOF — всего лишь оболочка метода CObject::IsKindOf. Следующий фрагмент сначала проверяет параметр в ут верждении ASSERT_KINDOF, а затем выполняет действительную проверку ошибок в параметрах.

BOOL DoSomeMFCStuffToAFrame (

CWnd *

pWnd )

{

 

 

 

 

ASSERT

( NULL != pWnd ) ;

 

 

ASSERT_KINDOF (

CFrameWnd

, pWnd

) ;

if ( (

NULL ==

pWnd ) ||

 

 

(

FALSE ==

pWnd >IsKindOf (

RUNTIME_CLASS ( CFrameWnd ) ) ) )

{

 

 

 

 

return ( FALSE ) ;

}

//Выполняем прочие действия MFC; pWnd гарантированно

//является CFrameWnd или его потомком.

}

ГЛАВА 3 Отладка при кодировании

109

 

 

Второй специфичный для MFC макрос утверждений — ASSERT_VALID. Это утвер ждение интерпретирует AfxAssertValidObject, который полностью проверяет кор ректность указателя на класс потомок CObject. После проверки правильности ука зателя ASSERT_VALID вызывает метод AssertValid объекта. AssertValid — это метод, который может быть переопределен в потомках для проверки всех внутренних структур данных в классе. Этот метод предоставляет прекрасный способ глубо кой проверки ваших классов. Переопределяйте AssertValid во всех ключевых классах.

SUPERASSERT

Рассказав вам о проблемах с поставляемыми утверждениями, я хочу продемонст рировать, как я исправил и расширил утверждения так, чтобы они действительно сообщали, как и почему возникли проблемы, и делали еще больше. На рис. 3 3 и 3 4 показаны примеры диалоговых окон SUPERASSERT, сообщающих об ошибках. В первом издании этой книги вывод SUPERASSERT представлял собой информаци онное окно, в котором показывалось расположение невыполненного утвержде ния, код последней ошибки, преобразованный в текст, и стек вызова. Как видно из рисунков, SUPERASSERT определенно подрос! (Однако я не стал называть его

SUPERPUPERASSERT!)

Рис. 3 3. Пример свернутого диалогового окна SUPERASSERT

Самое изумительное в труде писателя — потрясающие дискуссии с читателя ми, в которых я участвовал по электронной почте и лично. Мне повезло учиться у таких поразительно умных ребят! Вскоре после выхода первого издания между Скоттом Байласом (Scott Bilas) и мной состоялся интересный обмен письмами по электронной почте, в которых мы обсудили его мысли о том, что должны делать сообщения утверждений и как их использовать. Изначально я применял инфор мационное окно, так как хотел оставить утверждение максимально легковесным. Однако, обменявшись массой интересных соображений со Скоттом, я убедился, что сообщения утверждений должны предлагать больше функций, таких как по давление утверждений (assertion suppression). Скотт даже предложил код для свер тывания диалоговых окон (dialog box folding), свой макрос ASSERT для отслежива ния числа пропусков (ignore) и т. п. Вдохновленный идеями Скотта, я создал но вую версию SUPERASSERT. Я сделал это сразу после выхода первой версии и с тех пор использовал новый код во всех своих разработках, так что он прошел серь езную обкатку.

На рис. 3 3 показаны части диалогового окна, которые видны постоянно. Поле ввода Failure содержит причину сбоя (Assertion или Verify), невыполненное выра жение, место сбоя, расшифрованный код последней ошибки и число сбоев дан

110 ЧАСТЬ I Сущность отладки

ного конкретного утверждения. Если утверждение работает под Windows XP, Server 2003 и выше, оно также отображает общее число описателей ядра (kernel handle) в процессе. В SUPERASSERT я преобразую коды последней ошибки в их текстовое описание. Получение сообщений об ошибках в текстовом виде исключительно полезно при сбоях функций API: вы видите, почему произошел сбой, и можете быстрее запустить отладчик. Так, если в GetModuleFileName происходит сбой по причине малого объема буфера ввода, SUPERASSERT установит код последней ошибки равным 122, что соответствует ERROR_INSUFFICIENT_BUFFER из WINERROR.H. Сразу увидев текст «The data area passed to a system call is too small» («Область данных, передан ная системному вызову, слишком мала»), вы поймете, в чем именно проблема и как ее устранить. На рис. 3 3 — стандартное сообщение Windows об ошибке, но вы вправе добавить свои ресурсы сообщений к преобразованию сообщений о последней ошибке в SUPERASSERT. Подробнее о собственных ресурсах сообщений см. раздел MSDN «Message Compiler». Дополнительный стимул к использованию ресурсов сообщений в том, что они здорово облегчают локализацию ваших при ложений.

Рис. 3 4. Пример развернутого диалогового окна SUPERASSERT

Кнопка Ignore Once, расположенная под полем ввода Failure, просто продол жает выполнение. Она выделена по умолчанию, так что, нажав Enter или пробел, вы можете сразу продолжить работу, изучив причину сбоя. Abort Program вызы вает ExitProcess, чтобы попытаться корректно завершить приложение. Кнопка Break Into Debugger инициирует вызов DebugBreak, так что вы можете начать отладку сбоя, перейдя в отладчик или запустив отладчик по требованию. Кнопка Copy To Clipboard

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