Разоблачение величайшего мифа о проблеме языка С (исходники)

Источник: ibm
Кэмерон Лэйрд, вице-президент, Phaseit,Inc.

Ошибки памяти в программах, написанных на С и С++, плохи тем, что они повсеместны и могут иметь серьезные последствия. Множество серьезных предупреждений о проблемах безопасности от Computer Emergency Response Team и разработчиков программного обеспечения являются одним из следствий простых ошибок памяти. Программисты на С обсуждали этот тип ошибок с конца 70-х годов, но они часто встречались и в 2007 году. Ситуация усугубляется тем, что множество программистов С и С++ рассматривают ошибки памяти как неконтролируемые и роковые несчастья, которые можно только исправлять по мере их возникновения, но не предотвращать.

Это совсем не так. Эта статья покажет, что возможно понять все особенности грамотного программирования распределения памяти:

  • Важность правильного управления памятью
  • Виды ошибок памяти
  • Стратегии программирования памяти
  • Вывод

Важность правильного управления памятью

Программы с ошибками памяти, написанные на С и С++, являются причиной возникновения проблем. Если в них происходит утечка памяти, то они выполняются постепенно все медленнее и медленнее и в конечном итоге зависают; если они переписывают содержимое памяти - они не защищены и легко взламываются . Действие известного червя Морриса в 1988 году было основано на переполнении буфера, а недавно обнаруженные бреши в системе безопасности Flash Player'а и других распространенных программ также связаны с переполнением буфера. "Самая главная брешь в защите компьютера - переполнение буфера", - писал Родни Бейтс (Rodney Bates) в 2004 году.

Многие другие универсальные языки, такие как Java, Ruby, Haskell, C#, Perl, Smalltalk и тому подобные, широко применяются в ситуациях, где могли бы использоваться С или С++, и каждый из них имеет энтузиастов и значительные выгоды. В компьютерном фольклоре бытует мнение, что большинство преимуществ в удобстве и простоте использования других языков по сравнению с С и С++ - это простота управления памятью. Программирование, связанное с управлением памяти, очень важно, но его правильное применение весьма трудно на практике из-за того, что сейчас доминируют другие подходы в программировании, такие как объектно-ориентированный, функциональный, высокоуровневый, декларативный и другие.

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

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

Какие встречаются ошибки в управлении памятью

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

  • утечка памяти;
  • неправильное выделение памяти, включая множественное освобождение памяти функциейfree() и неинициализированные ссылки;
  • указатели, указывающие на несуществующий объект;
  • выход за пределы массива.

Это полный список. Даже с учетом объектно-ориентированного С++ список возможных проблем сильно не изменится: модель управления памятью и ссылки в С и С++ одинаковы, независимо от того, является ли тип данных простым, структурой языка C или классом С++. Большинство кода ниже написано на чистом С, а код для С++ в большинстве случаев оставлен для самостоятельных упражнений.

Утечки памяти

Утечки памяти происходят, когда ресурсы системы выделяются для программы, но последняя никогда не возвращает их. Ниже представлен пример такой утечки: (см. листинг 1):


В чем тут кроется проблема? До тех пор пока local_log() несет необычную для себя ответственность за освобождение памяти (free()), каждый вызов f1 приводит к утечке 100 байт. 100 байт кажется достаточно незначительной потерей памяти, однако через несколько часов, в течение которых будет вызываться этот метод, программа может зависнуть.

В программировании на С и С++ недостаточно следить за использованием malloc() или new. ВВ начале этого раздела были упомянуты именно средства, а не конкретно память из-за примеров, подобных следующему (см. листинг 2). Файловые дескрипторы (FILE handles) могут не быть похожими на блоки памяти, но работать с ними нужно так же аккуратно, как и с памятью:

Листинг 2. Возможная потеря памяти в "куче" из-за неправильного управления ресурсами

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

Неправильное выделение памяти

Проще бороться с неправильным выделением памяти (см. пример из листинга 3):

Пример 3. Неинициализированный указатель

Подобные ошибки имеют печальные последствия. Если программа выше будет запущена на операционной системе AIX®, присвоение неинициализированному указателю в большинстве случаев вызовет немедленную ошибку в сегментации ( segmentation fault ). Эти ошибки легко обнаружить и исправить их гораздо проще, чем те ошибки, которые трудно повторно воспроизвести и и на идентификацию которых может уйти несколько месяцев.

Вот еще пример к этому разделу. Память может высвобождаться чаще путем вызова free(), нежели заново выделяться с помощью malloc() (см. листинга 4):

Листинг 4. Два ошибочно выполненных высвобождения памяти

Эти ошибки также не угрожают тяжелыми последствиями. Хотя стандарт С не определяет поведение в этих ситуациях, обычно в готовых продуктах эти ошибки либо игнорируются, либо помечаются быстро и четко; как упомянутые ранее, эти ошибки безвредны.

Указатели, указывающие на несуществующие объекты

Указатели, указывающие на несуществующие объекты (далее - зависшие указатели) вызывают больше неприятностей. Зависшие указатели возникают, когда программист использует ресурсы памяти после того, как их освободили (см. листинг 5):

Листинг 5. Зависшие указатели

 

 

 

 

 

 

 

 

Традиционной отладкой зависшие указатели трудно локализовать. Их трудно повторно воспроизвести по следующим причинам:

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

Зависшие указатели являются постоянной проблемой программ, написанных на С или С++.

Нарушение размера массива

Не такой уж безопасной ошибкой является нарушение размера массива - ошибка, стоящая последней в списке главных ошибок в управлении памятью. Посмотрите снова листинг 1, что случится, если длина explanation превысит 80? Трудно однозначно сказать, что случится, но в любом случае от этого не стоит ждать ничего хорошего. Более определенно, С копирует строку, которая не умещается в 100 символов, под которые отведена память. В любых общих реализациях не влезающие в отведенные 100 байт символы перепишут другие данные в памяти. Размещение данных в памяти достаточно сложное и трудно воспроизвести, поэтому любые признаки, симптомы ошибки в реализации было бы трудно связать с определенной ошибкой в исходном коде. Нарушение размера массивов - ошибка, входящая в число ошибок, причиняющих миллионные убытки.

Стратегии программирования использования памяти

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

Стиль программирования

Самое важное и, как я заметил, единственное, чему не придают особого значения другие авторы - общепринятые стандарты программирования. Функции и методы, которые влияют на ресурсы, особенно память, необходимо подробно комментировать. Вот некоторые примеры (см. листинг 6).

Листинг 6. Пример кода, в котором подробно описано управление ресурсами памяти

Эти элементы стиля должны стать частью стиля написания программ. Вот еще несколько подходов к проблеме памяти:

  • специализированные библиотеки;
  • языки;
  • утилиты;
  • аппаратный контроль.

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

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

Проверка

В дополнение к стандартам программирования существует проверка (inspection). Любые методики самостоятельны, но особенно они действенны в комплексе. Внимательный профессиональный программист на С или С++ может проанализировать даже незнакомый исходный код и быстро увидеть проблемы в управлении памятью. После небольшой практики и с соответствующими навыками текстового поиска любой программист сможет быстро научиться оценивать правильность тела программы для сбалансированного использования *alloc() и free() или new и delete. При таком просмотре кода часто вылезают проблемы как в листинге 7.

Листинг 7. Проблемная утечка памяти

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

Статический автоматический анализ синтаксиса кода

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

Нужно отказаться от использовании для анализа кода утилиты lint. Хотя lint устарела и ограничена по своим возможностям, многие программисты, которые работают с ней (или ее более продвинутыми наследниками), напрасно не беспокоятся о возможных проблемах с памятью. Вполне возможно написать хороший код профессионального качества, который пройдет проверку lint, но в нем могут оказаться серьезные ошибки, в том числе и некорректная работа с памятью. Убытки из-за проблем с памятью могут многократно превысить стоимость самого дорогого инструмента для синтаксического анализа, поэтому надо вычищать исходный код. Даже если код успешно проверен с помощью lint, вполне вероятно, что существует более элегантный, эффективный, качественный и переносимый альтернативный инструмент.

Библиотеки для управления памятью

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

Из-за всех этих причин я призываю С и С++ программистов начинать с поиска в своем собственном коде проблем с использованием памяти. Сделав это, можно переходить к использованию библиотек.

Библиотеки предназначены для работы с различными проблемами памяти, так что очень трудно сравнить их непосредственно; общими рубриками являются собирание "мусора" ( garbage collection ), интеллектуальные указатели ( smart pointers ) и интеллектуальные контейнеры ( smart containers ). Грубо говоря, библиотеки автоматизируют большую часть управления памятью, поэтому программист делает меньше ошибок

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

Инструментальные средства для управления памятью.

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

Это введение рассматривает только программные инструментальные средства для управления памятью. Аппаратные отладчики работы памяти также существуют. Я считаю, что они нужны только в исключительных ситуациях - в основном, когда надо работать со специализированными компьютерами, которые не поддерживают другие типы инструментальных средств.

Рынок программных инструментальных средств включает в себя IBM Rational® Purify, Electric Fence и другие инструментальные средства с открытым исходным кодом. Некоторые их них хорошо работают с AIX и другими операционными системами.

Все инструментальные средства для работы с памятью работают приблизительно одинаково: сначала программист создает специальную версию исполняемого кода (хотя сгенерировать отладочную версию можно с помощью флага -g при компиляции), затем запускает приложение и изучает отчеты, автоматически сгенерированные инструментарием. Рассмотрим программу из листинга 8.

Листинг 8. Эталонная ошибка

Во многих средах разработки эта программа компилируется, работает и печатает "Hello, world" на экране. Запуск того же приложения с инструментарием по управлению памятью создаст отчет о нарушении размерности массива в четвертой строке. Узнать об ошибке в программном обеспечении подобным образом (в нашем случае ошибка заключается в том, что четырнадцать символов были скопированы в место, отведенное только для пяти) значительно дешевле, нежели получить от заказчика жалобы на сбои. В этом и заключается польза инструментальных средств для работы с памятью.

Заключение

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


Страница сайта http://www.interface.ru
Оригинал находится по адресу http://www.interface.ru/home.asp?artId=20193