Разоблачение величайшего мифа о проблеме языка С (исходники)Источник: ibm Кэмерон Лэйрд, вице-президент, Phaseit,Inc.
Ошибки памяти в программах, написанных на С и С++, плохи тем, что они повсеместны и могут иметь серьезные последствия. Множество серьезных предупреждений о проблемах безопасности от Computer Emergency Response Team и разработчиков программного обеспечения являются одним из следствий простых ошибок памяти. Программисты на С обсуждали этот тип ошибок с конца 70-х годов, но они часто встречались и в 2007 году. Ситуация усугубляется тем, что множество программистов С и С++ рассматривают ошибки памяти как неконтролируемые и роковые несчастья, которые можно только исправлять по мере их возникновения, но не предотвращать. Это совсем не так. Эта статья покажет, что возможно понять все особенности грамотного программирования распределения памяти:
Важность правильного управления памятью Программы с ошибками памяти, написанные на С и С++, являются причиной возникновения проблем. Если в них происходит утечка памяти, то они выполняются постепенно все медленнее и медленнее и в конечном итоге зависают; если они переписывают содержимое памяти - они не защищены и легко взламываются . Действие известного червя Морриса в 1988 году было основано на переполнении буфера, а недавно обнаруженные бреши в системе безопасности Flash Player'а и других распространенных программ также связаны с переполнением буфера. "Самая главная брешь в защите компьютера - переполнение буфера", - писал Родни Бейтс (Rodney Bates) в 2004 году. Многие другие универсальные языки, такие как Java, Ruby, Haskell, C#, Perl, Smalltalk и тому подобные, широко применяются в ситуациях, где могли бы использоваться С или С++, и каждый из них имеет энтузиастов и значительные выгоды. В компьютерном фольклоре бытует мнение, что большинство преимуществ в удобстве и простоте использования других языков по сравнению с С и С++ - это простота управления памятью. Программирование, связанное с управлением памяти, очень важно, но его правильное применение весьма трудно на практике из-за того, что сейчас доминируют другие подходы в программировании, такие как объектно-ориентированный, функциональный, высокоуровневый, декларативный и другие. Ошибки памяти также могут проявляться и как некоторые другие классы ошибок. В таком случае соответствующую ошибку в исходном коде очень трудно обнаружить и воспроизвести повторно. Утечка памяти, например, может сделать работу приложения полностью некорректной и в то же время крайне трудно понять, где и когда утечка происходит. Из-за всех этих причин аспекты программирования памяти на С и С++ заслуживают внимательного анализа. Давайте посмотрим, что можно сделать на примерах выбранных нами языков С и С++. Какие встречаются ошибки в управлении памятью Не стоит отчаиваться. Ниже приведены способы решения проблем, возникающих при программировании памяти. Начнем с типичных проблем:
Это полный список. Даже с учетом объектно-ориентированного С++ список возможных проблем сильно не изменится: модель управления памятью и ссылки в С и С++ одинаковы, независимо от того, является ли тип данных простым, структурой языка C или классом С++. Большинство кода ниже написано на чистом С, а код для С++ в большинстве случаев оставлен для самостоятельных упражнений. Утечки памяти происходят, когда ресурсы системы выделяются для программы, но последняя никогда не возвращает их. Ниже представлен пример такой утечки: (см. листинг 1):
В чем тут кроется проблема? До тех пор пока В программировании на С и С++ недостаточно следить за использованием Листинг 2. Возможная потеря памяти в "куче" из-за неправильного управления ресурсами Использование Проще бороться с неправильным выделением памяти (см. пример из листинга 3): Пример 3. Неинициализированный указатель Подобные ошибки имеют печальные последствия. Если программа выше будет запущена на операционной системе AIX®, присвоение неинициализированному указателю в большинстве случаев вызовет немедленную ошибку в сегментации ( segmentation fault ). Эти ошибки легко обнаружить и исправить их гораздо проще, чем те ошибки, которые трудно повторно воспроизвести и и на идентификацию которых может уйти несколько месяцев. Вот еще пример к этому разделу. Память может высвобождаться чаще путем вызова Листинг 4. Два ошибочно выполненных высвобождения памяти Эти ошибки также не угрожают тяжелыми последствиями. Хотя стандарт С не определяет поведение в этих ситуациях, обычно в готовых продуктах эти ошибки либо игнорируются, либо помечаются быстро и четко; как упомянутые ранее, эти ошибки безвредны. Указатели, указывающие на несуществующие объекты Указатели, указывающие на несуществующие объекты (далее - зависшие указатели) вызывают больше неприятностей. Зависшие указатели возникают, когда программист использует ресурсы памяти после того, как их освободили (см. листинг 5):
Традиционной отладкой зависшие указатели трудно локализовать. Их трудно повторно воспроизвести по следующим причинам:
Зависшие указатели являются постоянной проблемой программ, написанных на С или С++. Не такой уж безопасной ошибкой является нарушение размера массива - ошибка, стоящая последней в списке главных ошибок в управлении памятью. Посмотрите снова листинг 1, что случится, если длина Стратегии программирования использования памяти Старательность и аккуратность могут снизить процент этих ошибок почти до нуля. Давайте рассмотрим некоторые шаги, которые можно предпринять, чтобы понизить процент ошибок управления памятью; мой опыт применения этих шагов в различных организациях показывает, что они последовательно уменьшают число ошибок памяти по крайней мере на порядок. Самое важное и, как я заметил, единственное, чему не придают особого значения другие авторы - общепринятые стандарты программирования. Функции и методы, которые влияют на ресурсы, особенно память, необходимо подробно комментировать. Вот некоторые примеры (см. листинг 6). Листинг 6. Пример кода, в котором подробно описано управление ресурсами памяти Эти элементы стиля должны стать частью стиля написания программ. Вот еще несколько подходов к проблеме памяти:
Я убедился, что наибольший эффект дает вдумчивое улучшение стиля исходного кода. Это улучшение не должно быть дорогим или строго формальным; сегменты исходного кода, не использующие память, как обычно могут быть оставлены без комментариев, а код, работающий с памятью, должен быть подробно прокомментирован. Достаточно вставить несколько простых слов, чтобы точно знать последствия работы кода для памяти, и тогда программирование памяти станет более эффективным. Я не делал специальных экспериментов, чтобы доказать преимущества этого стиля. Но если программист работает столько же лет, сколько я, то он рано или поздно привыкнет вставлять комментарии к коду. В дополнение к стандартам программирования существует проверка (inspection). Любые методики самостоятельны, но особенно они действенны в комплексе. Внимательный профессиональный программист на С или С++ может проанализировать даже незнакомый исходный код и быстро увидеть проблемы в управлении памятью. После небольшой практики и с соответствующими навыками текстового поиска любой программист сможет быстро научиться оценивать правильность тела программы для сбалансированного использования Листинг 7. Проблемная утечка памяти Поверхностное использование автоматических инструментов во время выполнения программы не обнаружит утечку памяти, которая происходит в случае, если условие Статический автоматический анализ синтаксиса кода Разумеется, не только люди могут анализировать исходный код. Программисту следует постоянно в процессе разработки использовать статический автоматический анализ синтаксиса кода . Нужно отказаться от использовании для анализа кода утилиты Библиотеки для управления памятью Два последних класса инструментов исправления ошибок отличаются от первых трех. Предыдущие средства поверхностны , человек может без труда понять и реализовать их функциональность. Библиотеки для управления памятью и инструментальные средства имеют более высокую цену и требуют от разработчика больше вдумчивости и квалификации. Программисты, эффективно использующие библиотеки и инструментальные средства, понимают обычные статические подходы . Доступные библиотеки и инструментальные средства внушительны и их качество довольно высоко. Однако от них будет мало пользы для упрямого программиста, который сознательно игнорирует базовые принципы управления памятью. По своему жизненному опыту могу сказать, что посредственные программисты, работая в одиночку, только мешают сами себе, когда пытаются воспользоваться преимуществами библиотек управления памятью и инструментальными средствами. Из-за всех этих причин я призываю С и С++ программистов начинать с поиска в своем собственном коде проблем с использованием памяти. Сделав это, можно переходить к использованию библиотек. Библиотеки предназначены для работы с различными проблемами памяти, так что очень трудно сравнить их непосредственно; общими рубриками являются собирание "мусора" ( garbage collection ), интеллектуальные указатели ( smart pointers ) и интеллектуальные контейнеры ( smart containers ). Грубо говоря, библиотеки автоматизируют большую часть управления памятью, поэтому программист делает меньше ошибок У меня сложные чувства к библиотекам для управления памятью. Они должны работать, но их успешное применение в проектах я видел реже, чем ожидал, особенно когда использовался язык С. У меня еще нет хорошей теории, объясняющей эти разочаровывающие результаты. Работа библиотеки должна быть столь же хорошей, как ручное управление памятью, но это скользкая тема, особенно в ситуациях, где библиотеки для сборки мусора, кажется, замедляют работу. Мое окончательное мнение: программисты С++ воспринимают интеллектуальные указатели лучше, чем программисты С. Инструментальные средства для управления памятью. Команды разработчиков, выпускающие серьезные приложения, написанные на С, нуждаются в инструментальных средствах для управления памятью во время работы программы: это часть их стратегии. Методы, которые мы уже описали, ценны и необходимы. Может быть, трудно оценить качество и функциональность инструментальных средств для работы с памятью до тех пор, пока не попробуешь их на практике. Это введение рассматривает только программные инструментальные средства для управления памятью. Аппаратные отладчики работы памяти также существуют. Я считаю, что они нужны только в исключительных ситуациях - в основном, когда надо работать со специализированными компьютерами, которые не поддерживают другие типы инструментальных средств. Рынок программных инструментальных средств включает в себя IBM Rational® Purify, Electric Fence и другие инструментальные средства с открытым исходным кодом. Некоторые их них хорошо работают с AIX и другими операционными системами. Все инструментальные средства для работы с памятью работают приблизительно одинаково: сначала программист создает специальную версию исполняемого кода (хотя сгенерировать отладочную версию можно с помощью флага Во многих средах разработки эта программа компилируется, работает и печатает "Hello, world" на экране. Запуск того же приложения с инструментарием по управлению памятью создаст отчет о нарушении размерности массива в четвертой строке. Узнать об ошибке в программном обеспечении подобным образом (в нашем случае ошибка заключается в том, что четырнадцать символов были скопированы в место, отведенное только для пяти) значительно дешевле, нежели получить от заказчика жалобы на сбои. В этом и заключается польза инструментальных средств для работы с памятью. Программист С или С++ понимает, что проблемы в управлении памятью заслуживают особого внимания. После небольшой практики можно овладеть методиками, которые позволят исключить ошибки управления памятью. Нужно выучить проверенные приемы использования памяти, уметь находить потенциальные ошибки, и тогда техника, описанная в этой статье, станет частью повседневной работы программиста. Он сможет исправлять в своих приложениях потенциальные ошибки, на отладку которых ушли бы дни или недели. |