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

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

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

ГЛАВА 2 Приступаем к отладке

71

 

 

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

На компьютере для сборки программы следует при помощи команды SUBST отобразить корень дерева проекта на диск S:. В результате этого при сборке про$ граммы диск S: будет корневым каталогом информации об исходных текстах, включаемой во все PDB$файлы, которые вы будете добавлять в хранилище сим$ волов. Если разработчику нужно будет отладить предыдущую версию исходного кода, он сможет извлечь ее из системы управления версиями и отобразить ее при помощи команды SUBST на диск S:. Благодаря этому отладчик, показывая исходный код программы, сможет загрузить правильную версию файлов символов с мини$ мумом проблем.

Хотя я вкратце описал серверы символов, вам непременно следует полностью прочитать раздел «Symbols» в документации к пакету Debugging Tools for Windows. Технология серверов символов настолько важна для успешной отладки, что в ва$ ших интересах знать о ней как можно больше. Надеюсь, я смог доказать важность серверов символов и описать способы их лучшего применения. Если вы еще не создали свой сервер символов, я приказываю вам прекратить чтение и сделать это.

Резюме

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

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

Г Л А В А

3

Отладка при кодировании

В главе 2 я заложил основу общепроектной инфраструктуры, обеспечивающей более эффективную работу. В этой главе мы определим, как облегчить отладку, когда вы погрязли в кодовых баталиях. Большинство называет этот процесс за$ щитным программированием (defensive programming), но я предпочитаю думать о нем несколько шире и глубже — как о профилактическом программировании (proactive programming) или отладке при кодировании. По моему определению, защитное программирование — это код обработки ошибок, сообщающий вам, что возникла ошибка. Профилактическое программирование позволяет узнать, почему возникла ошибка.

Создание защищенного кода — лишь часть борьбы за исправление ошибок. Обычно специалисты пытаются провести очевидные защитные маневры — ска$ жем, проверить, что указатель на строку в C++ не равен NULL, — но они часто не принимают дополнительных мер: не проверяют тот же параметр, чтобы удосто$ вериться в наличии достаточного объема памяти для хранения строки максимально допустимого размера. Профилактическое программирование подразумевает вы$ полнение всех возможных действий, чтобы избежать необходимости применения отладчика и вместо этого заставить код самостоятельно сообщать о проблемных участках. Отладчик — одна из самых больших в мире «черных дыр» для времени, и, чтобы ее избежать, нужны точные сообщения кода о любых отклонениях от идеала. При вводе любой строки кода остановитесь и подумайте, что вы предпо$ лагаете в хорошем развитии ситуации и как проверить, что именно такое состо$ яние будет при каждом исполнении этой строки кода.

Все просто: ошибки не появляются в коде по волшебству. «Секрет» в том, что вы и я вносим их при написании кода и эти досадные ошибки могут появляться из тысяч источников. Они могут стать следствием таких критических проблем, как недостатки дизайна приложения, или таких простых, как опечатки. Хотя не$

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

73

 

 

которые ошибки легко устранить, есть и такие, которых не исправить без серьез$ ных изменений в коде. Хорошо бы взвалить вину за ошибки в вашем коде на грем$ линов, но следует признать, что именно вы и ваши коллеги вносите их туда. (Если вы читаете эту книгу, значит, в основном в ошибках виноваты ваши коллеги.)

Поскольку вы и другие разработчики отвечаете за ошибки в коде, возникает проблема поиска путей создания системы проверок и отчетов, позволяющей на$ ходить ошибки в процессе работы. Я всегда называл такой подход «доверяй, но проверяй» по знаменитой фразе Рональда Рейгана о том, как Соединенные Шта$ ты собираются приводить в жизнь один из договоров об ограничении ядерных вооружений с бывшим Советским Союзом. Я верю, что мы с моими коллегами будем использовать код правильно. Однако для предотвращения ошибок я проверяю все: данные, передаваемые другими в мой код, внутренние операции в коде, любые допущения, сделанные в моем коде, данные, передаваемые моим кодом наружу, данные, возвращаемые от вызовов, сделанных в моем коде. Можно хоть что$то проверить — я проверяю. В столь навязчивой проверке нет ничего личного по отношению к коллегам, и у меня нет (серьезных) психических проблем. Я про$ сто знаю, откуда появляются ошибки, и знаю, что если вы хотите обнаруживать ошибки как можно раньше, то ничего нельзя оставлять без проверки.

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

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

По$моему, разработчик — это тестировщик и разработчик: если разработчик не тратит хотя бы 40–50% времени разработки на тестирование своего кода, он не разрабатывает. Обязанность тестировщика — сосредоточиться на таких про$ блемах, как подгонка, тестирование на устойчивость и производительность. Тес$ тировщик крайне редко должен сталкиваться с поиском причин краха. Крах кода напрямую относится к компетенции инженера$разработчика. Ключ тестирования, выполняемого разработчиком, — в блочном тестировании (unit test). Ваша зада$ ча — запустить максимально большой фрагмент кода, чтобы убедиться, что он не приводит к краху и соответствует установленным спецификациям и требовани$ ям. Вооруженные результатами блочного тестирования модулей тестировщики мо$ гут сосредоточиться на проблемах интеграции и общесистемном тестировании. Мы подробно поговорим о тестировании модулей в разделе «Доверяй, но прове$ ряй (Блочное тестирование)».

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

Assert, Assert, Assert и еще раз Assert

Надеюсь, большинство из вас уже знает, что такое утверждение (assertion), так как это самый важный инструмент профилактического программирования в арсена$ ле отладочных средств. Для тех, кто не знаком с этим термином, дам краткое определение: утверждение объявляет, что в определенной точке программы дол$ жно выполняться некое условие. Если условие не выполняется, говорят, что утвер$ ждение нарушено. Утверждения используются в дополнение к обычной проверке на ошибки. Традиционно утверждения — это функции или макросы, выполняе$ мые только в отладочных компоновках и отображающие окно с сообщением о том, что условие не выполнено. Я расширил определение утверждений, включив туда компилируемый по условию код, проверяющий условия и предположения, которые слишком сложно обработать в функции или макросе обычного утверж$ дения. Утверждения — ключевой компонент профилактического программиро$ вания, потому что они помогают разработчикам и тестировщикам не только опре$ делить наличие, но и причины возникновения ошибки.

Даже если вы слышали об утверждениях и порой вставляете их в свой код, вы можете знать их недостаточно, чтобы применять эффективно. Разработчики не могут быть слишком жирными или слишком худыми — они не могут использо$ вать слишком много утверждений. Метод, которому я всегда следовал, чтобы опре$ делить достаточное количество утверждений, прост: утверждений достаточно, если мои подчиненные жалуются на появление множества информационных окон о нарушении утверждений, как только они пытаются вызвать мой код, используя не$ верную информацию или предположения.

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

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

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

75

 

 

Как и что утверждать

Мой стандартный ответ на вопрос «что утверждать?» — утверждайте все. Я бы с удовольствием заявил, что утверждение следует создать для каждой строки кода, но это нереальная, хоть и прекрасная цель. Следует утверждать каждое условие, поскольку именно оно может в будущем оказаться решением мерзкой ошибки. Не переживайте, что внесение слишком большого числа утверждений снизит произ$ водительность программы, — как правило, утверждения активны только в отла$ дочных сборках, а созданные возможности по обнаружению ошибок с лихвой перевесят небольшую потерю производительности.

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

Вэтом разделе я хочу сосредоточиться на том, как использовать утверждения

ичто утверждать. Я покажу это на примерах кодов. Замечу, что в этих примерах Debug.Assert — это утверждение .NET из пространства имен System.Diagnostic, а ASSERT — встроенный метод C++, который я представлю ниже.

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

Удар по карьере

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

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

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

BOOL DoSomeWork ( HMODULE * pModArray , int iCount , LPCTSTR szBuff )

{

ASSERT ( if ( ( pModArray == NULL ) &&

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

76

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

 

 

 

 

 

 

 

( IsBadWritePtr ( pModArray ,

 

( sizeof ( HMODULE ) * iCount ) ) &&

 

( iCount != 0 ) &&

 

( szBuff != NULL ) ) )

 

{

 

return ( FALSE ) ;

 

}

 

) ;

 

for ( int i = 0 ; i < iCount ; i++ )

 

{

pModArray[ i ] = m_pDataMods[ i ] ;

}

}

Исход

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

К этому моменту я уже побагровел и орал во весь голос: «Того, кто это написал, нужно уволить! Не могу поверить, что у нас работает такой неве$ роятный и полный @#!&*&$ идиот!» Мой начальник притих, выхватил рас$ печатку из моих рук и тихо сказал: «Это мой код». Ударом по карьере стал мой истерический смех, понесшийся вдогонку ретирующемуся боссу.

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

Подчеркну: используйте утверждения как дополнение к обычным средствам обработки ошибок, а не вместо них. Если у вас есть утверждение, то рядом в коде должна быть какая$то процедура обработки ошибок. Что до моего босса, то когда несколько недель спустя я пришел к нему в кабинет уволь$ няться, поскольку получил работу в компании получше, он был готов танце$ вать на столе и петь о том, что это был лучший день в его жизни.

Как утверждать

Первое правило: каждый элемент нужно проверять отдельно. Если вы проверяете несколько условий в одном утверждении, то не сможете узнать, какое именно вызвало сбой. В следующем примере я демонстрирую одну и ту же функцию с разными утверждениями. Хотя утверждение в первой функции обнаружит невер$ ный параметр, оно не сможет сообщить, какое условие нарушено или даже какой из трех параметров неверен.

 

 

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

77

 

 

 

// Ошибочный

способ написания утверждений. Какой параметр неверен?

 

BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )

 

 

 

{

 

 

 

 

 

ASSERT (

(

i > 0

)

&&

 

 

(

NULL != szItem

)

&&

 

 

(

( iLen > 0 ) && ( iLen < MAX_PATH )

)

&&

 

 

(

FALSE == IsBadStringPtr ( szItem , iLen ) ) )

;

 

 

 

 

 

 

 

}

//Правильный способ. Каждый параметр проверяется отдельно,

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

BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )

{

ASSERT ( i > 0 ) ;

ASSERT ( NULL != szItem ) ;

ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;

ASSERT ( FALSE == IsBadStringPtr ( szItem , iLen ) ) ;

}

Утверждая условие, старайтесь проверять его полностью. Например, если в .NET ваш метод принимает в виде параметра строку и вы ожидаете наличия в ней не$ ких данных, то проверка на null опишет ошибочную ситуацию лишь частично.

// Пример частичной проверки ошибочной ситуации. bool LookupCustomerName ( string CustomerName )

{

Debug.Assert ( null != CustomerName , "null != CustomerName" ) ;

}

Ее можно описать полностью, добавив проверку на пустую строку.

// Пример полной проверки ошибочной ситуации. bool LookupCustomerName ( string CustomerName )

{

Debug.Assert ( null != CustomerName , "null != CustomerName" ) ;

Debug.Assert ( 0 != CustomerName.Length ,"\"\" != CustomerName.Length" ) ;

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

//Пример плохо написанного утверждения: nCount должен быть положительным,

//но утверждение не срабатывает, если nCount отрицательный.

void UpdateListEntries ( int nCount )

{

ASSERT ( nCount ) ;

}

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

// Правильное утверждение, проверяющее необходимое значение в явном виде.

void UpdateListEntries ( int nCount )

{

ASSERT ( nCount > 0 ) ;

}

Неверный пример проверяет только то, что nCount не равен 0, что составляет лишь половину нужной информации. Утверждения, в которых допустимые зна$ чения проверяются явно, сами себе служат документацией и, кроме того, гаран$ тируют обнаружение неверных данных.

Что утверждать

Теперь мы можем перейти к вопросу о том, что утверждать. Если вы еще не дога$ дались по приведенным до сих пор примерам, позвольте прояснить, что в пер$ вую очередь следует утверждать передающиеся в метод параметры. Утверждение параметров особенно важно для интерфейсов модулей и методов классов, вызы$ ваемых другими участниками вашей команды. Поскольку эти шлюзовые функции являются точками входа в ваш код, стоит убедиться в корректности всех параметров и предположений. В истории «Удар по карьере» я уже обращал ваше внимание на то, что утверждения ни в коем случае не должны вытеснять обычную обработку ошибок.

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

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

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

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

79

 

 

Напомню: я не выступаю за применение утверждений для каждого возможно$ го сбоя. Некоторые сбои являются ожидаемыми, и вам следует соответствующим образом их обрабатывать. Инициация утверждения при каждом неудачном поис$ ке в базе данных скорее всего заставит всех отключить утверждения в проекте. Учтите это и утверждайте возвращаемые значения там, где это важно. Обработка в программе корректных данных никогда не должна приводить к срабатыванию утверждения.

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

В обоих предыдущих примерах, как и в большинстве случаев утверждения предположений, нельзя проверять предположения в общем методе или макросе утверждения. В таких случаях поможет технология условной компиляции, кото$ рую я упомянул в предыдущем абзаце. Поскольку код, выполняемый в условной компиляции, работает с «живыми» данными, следует соблюдать особую осторож$ ность, чтобы не изменить состояние программы. Чтобы избежать серьезных про$ блем, которые могут появиться от введения кода с побочными эффектами, я пред$ почитаю, если возможно, реализовывать такие типы утверждений отдельными ме$ тодами. Таким образом вы избежите изменения локальных переменных внутри исходного метода. Кроме того, компилируемые по условию методы утверждений могут пригодиться в окне Watch, что вы увидите в главе 5, когда мы будем гово$ рить об отладчике Microsoft Visual Studio .NET. Листинг 3$1 демонстрирует ком$ пилируемый по условию метод, который проверяет существование таблицы до начала интенсивной работы с данными. Заметьте: этот метод предполагает, что вы уже передали строку подключения и имеете полный доступ к базе данных. AssertTableExists подтверждает существование таблицы, чтобы вы могли опираться на это предположение, не получая странных сообщений о сбоях из глубин ваше$ го кода.

Листинг 3-1. AssertTableExists проверяет существование таблицы

[Conditional("DEBUG")]

static public void AssertTableExists ( string ConnStr , string TableName )

{

SqlConnection Conn = new SqlConnection ( ConnStr ) ;

StringBuilder sBuildCmd = new StringBuilder ( ) ;

sBuildCmd.Append ( "select * from dbo.sysobjects where " ) ; sBuildCmd.Append ( "id = object_id('" ) ;

sBuildCmd.Append ( TableName ) ; sBuildCmd.Append ( "')" ) ;

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

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

// Выполняем команду.

SqlCommand Cmd = new SqlCommand ( sBuildCmd.ToString ( ) , Conn ) ;

try

{

//Открываем базу данных. Conn.Open ( ) ;

//Создаем набор данных для заполнения. DataSet TableSet = new DataSet ( ) ;

//Создаем адаптер данных.

SqlDataAdapter TableDataAdapter = new SqlDataAdapter ( ) ;

//Устанавливаем команду для выборки. TableDataAdapter.SelectCommand = Cmd ;

//Заполняем набор данных из адаптера. TableDataAdapter.Fill ( TableSet ) ;

//Если что нибудь появилось, таблица существует. if ( 0 == TableSet.Tables[0].Rows.Count )

{

String sMsg = "Table

: '" +

TableName +

"' does not

exist!\r\n" ;

Debug.Assert ( false

, sMsg

) ;

}

}

catch ( Exception e )

{

Debug.Assert ( false , e.Message ) ;

}

finally

{

Conn.Close ( ) ;

}

}

Прежде чем описать специфические проблемы различных утверждений для .NET и машинного кода, хочу показать пример того, как я обрабатываю утверждения. В листинге 3$2 показана функция StartDebugging отладчика машинного кода из главы 4. Этот код — точка перехода из одного модуля в другой, так что он демон$ стрирует все утверждения, о которых говорилось в этом разделе. Я выбрал метод C++, потому что в «родном» C++ всплывает гораздо больше проблем и поэтому надо утверждать больше условий. Я рассмотрю некоторые проблемы этого примера ниже в разделе «Утверждения в приложениях C++».

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