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

         

Обзор операторов утверждений для Visual C++ и Visual Basic


В этом разделе приводится краткий обзор и обсуждение различных операторов утверждений, которые используются в языках Visual C++ и Visual Basic. Хотя следует отметить, что вместо них можно создавать и собственные несложные операторы-утверждения, подобные макросу ASSERT, который использован во всех предыдущих примерах.

Макросы assert, _ASSERTw _ASSERTE

Макрос assert исполнительной (run-time) библиотеки языка С определен стандартом ANSI С. Эта версия переносима на все С-компиляторы и платформы и определена во включаемом файле ASSERT.H. При сбое консольных Windows-приложений макрос утверждения assert посылает свое сообщение в стандартный поток вывода ошибок stderr. Если речь идет о Windows-приложении с графическим интерфейсом пользователя (GUI), то при его сбое assert выводит свое сообщение на экран в форме панели сообщений ASSERTION FAILURE... (см. рис. 3.1).

Другой тип операторов утверждений исполнительной С-библиотеки специфичен для Windows. Это макросы _ASSERT и __ASSERTE, которые определены в файле CRTDBG.H. Единственное различие между ними в том, что _ASSERTE выводит в свою выходную панель также и выражение, получаемое через аргумент вызова. Отслеживать это выражение настолько важно, особенно когда программу тестируют специальные инженеры1, что следует всегда использовать именно макрос _ASSERTE, а не _ASSERT. Оба макроса являются частью чрезвычайно полезной отладочной С-библиотеки времени выполнения (подробное описание DCRT-библиотеки приведено в главе 15).

Test engineers — тестирующие инженеры. — Пер. 

История отладочной войны Исчезновение файлов и потоков

 Сражение

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

UI — User Interface, интерфейс пользователя. — Пер.

Подробнее оба отладочных процесса и их терминология поясняются в следующей главе. — Пер.

Результат

Меня буквально смешали с грязью, потому что оказалось, что проблема возникла из-за моей ошибки. В приложении BoundsChecker я отвечал за цикл отладки, и при этом использовалась отладочная библиотека Windows (Windows Debugging API), которая стартует один отладочный процесс (debugger) и управляет другим3 (debuggee), а также отвечает на события отладки, которые генерирует второй процесс. Будучи добросовестным программистом, я видел, что функция WaitForDebugEvent возвращала значения дескриптора некоторых уведомлений о событиях. Например, когда процесс стартовал под отладчиком, тот получал структуру, которая содержала дескриптор процесса и начальный поток для этого процесса.

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

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

Чтобы понять, как эта ситуация привела к проблеме, нужно знать, что когда дескриптор закрывается, операционная система помечает его как "доступный". Операционная система Windows NT 4, которую мы использовали в то время, является особенно агрессивной относительно повторного использования значений дескрипторов (Windows' 2000 демонстрирует такое же агрессивное поведение).


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

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

Урок

Можно было избежать этой проблемы, прочитав упомянутую выше заметку в документации отладочного API. Кроме того (и это— большой урок), я понял, что нужно всегда проверять значения, возвращаемые в cioseHandle. Хотя в такой ситуации мало что можно сделать, но когда вы закрываете неверный дескриптор, то операционная система хотя бы выдает сообщение о соответствующих неполадках, на которое нужно обратить внимание.

Замечу, что если вы пытаетесь повторно закрыть дескриптор или передать некорректное значение в CioseHandle, выполняясь под отладчиком, то Windows NT 4 и Windows 2000 выводят следующее сообщение: "Invalid Handle exception (0x00000008)" (исключение (0x00000008) "Недействительный дескриптор"). Получив такое сообщение, можно остановить выполнение и попытаться выяснить, почему оно появилось.

Хотя макросы assert, _ASSERT и _ASSERTE удобны в работе и бесплатны, они имеют несколько недостатков. С макросом assert связаны две проблемы.

Во-первых, имя файла в его выходном сообщении усекается до 60 символов, так что иногда при завершении программы вы понятия не имеете, какой файл вызвал макрос утверждения. Вторая проблема возникает при работе с проектом, который не использует пользовательского интерфейса, например со службой Windows 2000 или внепроцессным СОМ-сервером.


Когда макрос assert направляет свой вывод в стандартный выходной поток ошибок (stderr), то его можно легко пропустить. А если assert пытается направить свой вывод в панель сообщений, то консольное приложение повиснет, пытаясь закрыть эту панель, т. к. не использует UI-интерфейса и не может выводить на экран никаких окон.

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

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

//Послать сообщение окну. Если время такой посылки истекает (тайм-аут),

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

//Напоминаем, что единственный способ проверить, произошел ли сбой 

//функции SendMessageTimeout, состоит в том, чтобы проверить функцию

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

//то SendMessageTimeout выполнила тайм-аут.

_ASSERTE ( NULL != pDataPacket)

if ( NULL == pDataPacket)

return ( ERR_INVALID_DATA);

}

LRESULT IRes = SendMessageTimeout ( hUIWnd,

WM_USER_NEEDNEXTPACKET,

0

(LPARAM)pDataPacket ,

SMTO_BLOCK ,

10000

&pdwRes ) ;

_ASSERTE ( FALSE != IRes);

if ( 0 == IRes)

{

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

DWORD dwLastErr = GetLastError ();

 if ( 0 == dwLastErr)

{

// UI висит или нет достаточно быстрой обработки данных.

return ( ERR_UI_IS_HUNG);

}

// Если ошибка в чем-то еще, то существует проблема

//с данными, посылаемыми через параметр.



return ( ERR_INVALID_DATA);

}

return ( ERR_SUCCESS);

.

.

.

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

_ASSERTE ( FALSE != IRes)

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

Макросы ASSERT_KINDOFw 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 или на класс,



 // производньм от CFrameWnd.

.

.

.

Второй MFC-макрос-утверждение — ASSERT_VALID — сводится к вызову функции AfxAssertvaiidObject, которая подтверждает корректность указателя класса, производного от cobject. После подтверждения корректности указателя, макрос ASSERT_VALID вызывает объектный метод Assertvaiid. Для проверки внутренних структур данных в производных классах этот метод можно переопределять. Метод Assertvaiid считается сильным средством для выполнения глубоких проверок, поэтому нужно переопределять его для всех ключевых классов приложения.

Оператор Debug.Assert

С одной стороны, жизнь Visual Basic-программистов намного легче, чем у программистов C/C++, потому что Visual Basic не требует обширных проверок корректности типов параметров и указателей (до тех пор, пока не используются параметры типа variant). Однако, с другой стороны, правильно организовать профилактическое программирование средствами языка Visual Basic довольно трудно. В Visual Basic имеется всего один встроенный оператор утверждения — Debug.Assert, хотя у него и существует целых четыре версии.

Это — хорошие новости. Но есть и плохие: Debug.Assert нельзя использовать, когда вы действительно в нем нуждаетесь, т. е. при отладке компилированного кода. Думаю, что создатели Visual Basic сделали большую ошибку,

не разрешив функции Debug.Assert компилироваться в родной (native) код. Оператор Debug.Assert доступен только при выполнении внутри интегрированной среды разработки (IDE) Visual Basic. Когда функция утверждения терпит неудачу при отладке, то программист попадает в окно IDE на строку Debug.Assert. Хотя Debug.Assert активна только в IDE, желательно использовать ее в максимально возможной степени, чтобы контролировать все проблемы заранее, еще на уровне исходного кода.

Для себя я разрешил все проблемы с Debug.Assert, когда просматривал книгу Advanced Visual Basic 6.0 (2nd ed., Microsoft Press, 1998) компании The Mandelbrot Set, основанной в Англии. Для этой книги Марк Пирс (Mark Реагс) написал замечательную дополнительную программу для Visual Basic, называемую Assertion Sourcerer.


Она одна стоит целой книги (и остальная часть книги тоже превосходна). Эта программа автоматически отслеживает предложения Debug.Assert в исходной программе и помещает после них вызов реального оператора утверждения. Она также вычисляет имя исходного файла и номер строки, в которых была обнаружена проблема. Дополнительно к размещению реальных операторов утверждений в исходном коде, программа Assertion Sourcerer еще и убирает их, когда работа с ними заканчивается!

Программу Марка Пирса можно легко расширить на поиск предложений Debug.Print и вставку после них реальных операторов трассировки. В листинге 3-2 показан исходный код авторского файла VBASSERTANDTRACE.BAS, который содержит реализации всех тех реальных операторов утверждений и трассировки, о которых только что шла речь. Для обработки утверждений в нем используется макрос SUPERASSERT, обсуждению которого посвящен следующий раздел.

Листинг 3-2. Файл VBASSERTANDTRACE.BAS

Attribute VB_Name = "VBAssertAndTrace"

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

' Copyright (с) 1999-2000 John Robbins — All rights reserved.

' "Debugging Applications" (Microsoft Press) 

'

' Чтобы использовать этот файл: 

' Не обязательно (но настоятельно!) рекомендуется:

' использовать подключаемый Visual Basic-модуль Assertion

' Sourcerer Марка Пирса (Mark Pearce) из

' "Advanced Microsoft Visual Basic 6.0" (2nd ed).

' Он будет отлавливать все предложения Debug.Assert

' программы и помещать под каждым таким предложением

' обращение к BugAssert, так что в компилированный Visual Basic-код

' будут вставлены реальные операторы утверждений.

' В работе с Debug.Assert придерживайтесь следующих правил:

' 1. Компилируйте BUGSLAYERUTIL.DLL, потому что этот файл

' использует несколько экспортированных функций.

' 2. Разместите операторы Debug.Assert в исходном коде программы.

' 3. Когда вы будете готовы компилировать программу, используйте

' подключаемый модуль Марка Пирса, чтобы добавить обращения к



' BugAssert.

' 4. Добавьте данный файл в ваш проект.

' 5. Компилируйте свой проект и понаблюдайте за утверждениями.

' Можно также вызывать различные функции библиотеки

' BUGSLAYERUTIL.DLL, чтобы установить различные опции и выходные

' дескрипторы.

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

Option Explicit

' Объявить все функции BUGSLAYERUTIL.DLL, которые этот модуль

' может вызывать.

Public Declare Sub DiagOutputVB Lib "BugslayerUtil" _

(ByVal sMsg As String)

Public Declare Function DiagAssertVB Lib "BugslayerUtil" _ 

(ByVal dwOverrideOpts As Long, _ 

ByVal bAllowHalts As Long,

 _ ByVal sMsg As String) _

 As Long

Public Declare Function AddDiagAssertModule Lib "BugslayerUtil" _ 

(ByVal hMod As Long) _ 

As Long

Public Declare Function SetDiagAssertFile Lib "BugslayerUtil" _

  (ByVal hFile As Long) _ 

As Long

Public Declare Function SetDiagAssertOptions Lib "BugslayerUtil" _

  (ByVal dwOpts As Long) _ 

As Long

Public Declare Function SetDiagOutputFile Lib "BugslayerUtil" _ 

(ByVal dwOpts As Long) _ 

As Long

Private Declare Function GetModuleFileName Lib "kerne!32" _

 Alias "GetModuleFileNameA" _ 

(ByVal hModule As Long, _ 

ByVal IpFileName As String, _ 

ByVal nSize As Long) _

 As Long Public Declare Sub DebugBreak Lib "kerne!32" ()

' Авторский макрос TRACE. Его можно использовать для вызова любого

' другого макроса. Кроме того, программа Assertion Sourcerer расширена

' для добавления TRACE-операторов (после предложений Debug.Print)

Public Sub TRACE(ByVal sMsg As String)

DiagOutputVB sMsg End Sub

' Функция BugAssert, вставленная с помощью

 ' Assertion Sourcerer

 Public Sub BugAssert(ByVal vntiExpression As Variant, sMsg As String)

CallAssert vntiExpression, 0, sMsg 

End Sub

' Подпрограмма SUPERASSERT.

 Public Sub SUPERASSERT{ByVal vntiExpression As Variant, sMsg As String)



CallAssert vntiExpression, 7, sMsg 

End Sub

Private Sub CallAssert{ByVal vntiExpression As Variant, _

ByVal iOpt As Long,

 _ sMsg As String)

 If (vntiExpression) Then

Exit Sub Else

' Следующий флажок используется, чтобы определить, вызывалась ли

 ' уже функция InDesign. Вызывать эту функцию повторно нет 

' необходимости.

Static bCheckedDesign As Boolean 'False по умолчанию. 

' Флажок, разрешающий остановки, я пересылаю в DiagAssertVB.

 ' Если этот флажок установлен в 1, то DiagAssertVB разрешит 

' останавливать приложение. Если этот флажок установлен в 0, 

' приложение выполняется в VB IDE так, что DiagAssertVB не будет

 ' разрешать остановки. Если пользователь запускается 

' внутри VB IDE, то прерывание довольно опасно и может погубить 

' весь ваш дневной труд! 

Static lAllowHalts As Long 

' Вызвать InDesign только раз.

 If (False = bCheckedDesign) Then 

If (True = InDesign()) Then

lAllowHalts = 0 

Else

lAllowHalts = I 

End If

bCheckedDesign = True

 End If

Dim IRet As Long

IRet = DiagAssertVB(iOpt, lAllowHalts, sMsg)

 If (I = IRet) Then

' Пользователь .хочет прервать выполнение. Однако

 ' прерывание не разрешается, если выполнение 

' происходит внутри VB IDE.

If (1 = lAllowHalts) Then

DebugBreak

 End If

 End If 

End If 

End Sub

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

' Эта замечательная функция взята из превосходной главы Пита Морриса "On

' Error GoTo To Hell" (с.25,26 в "Advanced Microsoft Visual Basic 6.0")

' InDesign позволяет проверять, выполняетесь ли вы в VB IDE. Я благодарен

' Питу, разрешившему мне использовать эту функцию!

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

Public Function InDesign() As Boolean

' Я оставлю только один комментарий Пита — он превосходен.

' Только для этого и нужен Debug.Assert!

Static nCallCount As Integer

Static bRet As Boolean ' По умолчанию этот флажок False.

nCallCount = nCallCount + 1

Select Case nCallCount

Case 1: ' Первый вход (выполнение Debug.Assert)

Debug.Assert InDesign() Case 2: ' Второй вход, когда Debug.Assert уже выполнен

bRet = True

 End Select

' Если был вызван Debug.Assert, возвратить True, 

' чтобы предотвратить ловушку. 

InDesign = bRet

' Переустановить счетчик для будущих вызовов. 

nCallCount = 0

 End Function



Содержание раздела