СТАТЬЯ
05.02.02

Средства отладки C/C++: использование Rational Purify при работе c GDB (Часть 1) 

© Горан Бегик (Goran Begic), Rational Software
Переведено БНТП по заказу Interface Ltd.

 Существуют ли ошибки в программах на C/C++, разработанных для платформ UNIX? Я думаю, мы все знаем ответ на этот вопрос. Вообще говоря, разработчики под UNIX встречаются с теми же проблемами, что и их коллеги работающие в среде Windows: синтаксические и логические ошибки, ошибки распределения и освобождения памяти, узкие места производительности и так далее. Список длинный, и каждый элемент в нем может отнять у разрабочтика большое количество времени, прежде чем приложение будет готово к доставке заказчику. Единственный путь тестирования C++ - приложений под UNIX для поиска проблем, связанных с динамическим распределением памяти – отладка во время исполнения. В этой статье я представлю решение для подобной отладки программ, которое совмещает один из популярных отладчиков - GNUGDB - с автоматизированной средой для определения ошибок, связанных с распределением и освобождением памяти: RationalPurify(В соответствии с Web-сайтом GNU «Проект GNU был начат в 1984 г. для разработки полной Unix-подобной бесплатной операционной системы: системы GNU»).

 Оглавление

Ошибки памяти и определение ошибок с помощью RationalPurify

Языки программирования C и C++ позволяют разработчику динамически распределять и освобождать память, используя соответствующий API. Основной функцией распределения памяти в C во время исполнения является malloc(). Распределение памяти с помощью malloc() легко осуществимо: функция malloc() определяет тип и размер структуры, которая должна быть распределена в динамической области памяти, переданной ей в качестве параметра. В результате вызова функции malloc() система распределит запрошенный участок памяти (если доступно достаточное ее количество) и возвратит ссылку на этот участок в виде указателя на распределенную область памяти.

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

Сбои при освобождении памяти
Наиболее типичная ошибка, которую вы можете совершить, работая с динамической памятью, - это забыть освободить ресурсы после того, как они больше не нужны. Память, распределенная с помощью malloc(), может быть возвращена системе с помощью API: вызова функции free(). Если память распределена с помощью malloc(), например, в цикле, и приложение является системным сервисом, что предполагает его работу без перезапуска в течение длительного времени, тогда «утечка» памяти может легко привести к переполнению памяти или даже сбою всей операционной системы.

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

Ошибки записи и чтения памяти за границами выделенной области
Другой очень серьезной ошибкой работы с памятью является чтение и запись за границами выделенной области памяти (out-of-bounds). Эта ошибка встречается тогда, когда вы обращаетесь в режиме чтения или записи к участку памяти, который лежит за пределами распределенной области памяти. Например, если у вас есть строка stringA, которая состоит из десяти символов, и вы пытаетесь скопировать ее в участок памяти, выделенный под строку stringB из пяти символов, тогда пять дополнительных символов будут записаны за концом участка памяти, выделенного под строку stringB. То же самое случится, если вы просто неправильно индексируете массив. Положим, у вас есть массив из десяти целых, а вы пытаетесь получить доступ к пятнадцатому элементу массива. Языки C/C++ позволяют вам сделать это, так что вы не заметите этой ошибки до тех пор, пока копируемая строка не испортит какие-нибудь корректные данные за границами распределенной области памяти. Однако следует помнить, что вы можете не ощутить последствий этой ошибки за один прогон (может даже и за сотню прогонов), но рано или поздно ошибка уничтожит какие-нибудь важные данные, и приложение будет разрушено. Скорее всего, сбой произойдет через длительный период времени после того, как приложение будет поставлено заказчику, но это может случиться в любой момент.

Другие ошибки работы с памятью
Кроме «утечки» памяти и ошибок записи и чтения за границами выделенной области, существуют и другие важные ошибки, связанные с работой с памятью:

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

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

Рис. 1. Цвета, используемые RationalPurify при классификации состояний памяти.

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

В качестве примера рассмотрим массив, распределенный с помощью malloc():

char *string2 = malloc(10);

Если программа пытается читать содержимое этой строки без предварительной инициализации, Purify немедленно сообщит о чтении из неинициализированного участка памяти - UMR (UninitializedMemoryRead). Бит состояния установится в «желтый цвет» для распределенной, но не инициализированной памяти. Если вы осуществляете запись в распределенную память, помеченную желтым, бит состояния изменится на «зеленый цвет», который соответствует распределенной и инициализированной памяти. «Красная» память не была ни распределена, ни инициализирована приложением. Каждая попытка доступа к этой памяти приведет к ошибке. Если программа пытается читать из этой памяти, Purify сообщит об ошибках доступа, таких как ABR (ArrayBoundsRead – чтение за границами массива), ZPR (ZeroPageRead – чтение из нулевой страницы), NPR (NullPointerRead – чтение по пустому указателю), IPR (InvalidPointerRead – чтение по некорректному указателю) и т.п., в зависимости от типа памяти, к которой осуществляется доступ. Так же Purify будет реагировать на попытки записи в эту память.

Различные комбинации состояний (цвета) и операций с памятью будут приводить к различным сообщениям об ошибках в Purify.

Подготовка приложения к отладке

GDB представляет собой бесплатный отладчик GNU. Длинный список его возможностей и его доступность, сделали его очень популярным среди разработчиков под UNIX. Вместе с бесплатным компилятором GCC, он обеспечивает надежную основу для гарантии качества приложений, написанных на C/C++. GDB может использоваться совместно с RationalPurify для тщательного анализа приложений на наличие «утечек» памяти и ошибок работы с памятью. Как отладчик, так и Purify используют символьную отладочную информацию для управления контрольными точками и привязки машинного кода к исходным файлам тестируемого приложения. Для генерации отладочной информации вам необходимо использовать опцию (-g) при компиляции исходных кодов с помощью компилятора GCC:

gcc -g helloEdge.c

Обработка приложения с помощью RationalPurify

RationalPurify необходимо вставить дополнительные инструкции языка ассемблера в вашу программу для контроля ее исполнения и мониторинга распределения памяти после ее старта. Ниже приведен пример того, как это делается:

purify gcc -g helloEdge.c

Когда вы запустите обработанный таким образом исполнимый модуль, тестируемая программа - PUT (ProgramUnderTest) автоматически запустит графический интерфейс Purify (GUI - GraphicalUserInterface) и начнет собирать информацию о выполнении.

Запуск отладчика GBD

Вы можете запустить отладчик GDB из командной строки с помощью команды gdb. Как только GDB запущен, вы можете вводить команды отладчика. Простейший способ запуска вашего приложения в отладчике GDB – это использование его имени в качестве параметра запуска:

gdb ./a.out

Пример приложения

Наш пример - это простенькая программа "Hello, RationalEdge", которая ничего особенно не делает, кроме создания нескольких ошибок, трудно обнаруживаемых без специализированных средств типа RationalPurify.

int main(void){
int i, length;
char *string1 = "Hello, theEdge";
char *string2 = malloc(10);
 
length = strlen(string2);   // UMR
 
for (i = 0; string1[i] != '\0'; i++) {
   string2[i] = string1[i];   // ABW's
}
 
length = strlen(string2);   // ABR
printf("\nHello");
 
printf("\n");
return 0;

}

Что здесь неправильного? Опытный глаз разработчика на С/C++ возможно сразу заметит некоторые очевидные ошибки, даже не зная аббревиатур, используемых в комментариях. Однако если вы откомпилируете и запустите это приложение, ни компилятор, ни система не сообщат об ошибках. Будет казаться, что приложение работает нормально.

А что если бы это была программа из тысяч строк кода? Может ли опытный глаз разработчика помочь в этом случае? Я сомневаюсь. Даже средства статического анализа не смогут обнаружить ошибки, похожие на ошибки в нашем примере. Вам придется исполнить программу, чтобы выловить все проблемные участки.

Первая ошибка – чтение из неинициализированного участка памяти. Мы распределили память под строку и читаем из распределенного участка памяти без присвоения какой-либо строки этому участку памяти.

Вторая ошибка – запись за пределами массива (ABW - ArrayBoundsWrite), относящаяся к типу ошибок выхода за пределы памяти (Out-of-Bounds). Программа пытается скопировать строку длиной в пятнадцать символов (включая символы конца строки) - "Hello, theEdge" – в область памяти, распределенную с помощью malloc(), которая предназначена только для десяти символов. Это значит, что «лишние» символы первой строки перезапишут исходные символы конца строки, и запись продолжится за границами распределенной памяти.

Третья ошибка (Out-Of-BoundsRead) заключается в чтении из той же строки, границы которой мы уже нарушили.

Кроме того, мы имеем «утечку» памяти из-за динамического распределения массива String2, поскольку мы не вызываем free() для строки, распределенной с помощью malloc(), для освобождения распределенной памяти и возвращения ее в систему.

 Продолжение статьи

Дополнительную информацию Вы можете получить в компании Interface Ltd.

Обсудить на форуме Rational Software
Отправить ссылку на страницу по e-mail


Interface Ltd.
Тel/Fax: +7(095) 105-0049 (многоканальный)
Отправить E-Mail
http://www.interface.ru
Ваши замечания и предложения отправляйте автору
По техническим вопросам обращайтесь к вебмастеру
Документ опубликован: 05.02.02