среда, 17 декабря 2008 г.

Классификация проблем с памятью

Что-то я очень глубоко углубился в эту тему. Но, стабильность сервера пока оставляет желать лучшего, значит я наверное еще не одну статью про отладку напишу.

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

Кто-то скажет - переписать... Нет, это путь тупиковый. Всмысле переписать - это непроизводительная деятельность, Надо учиться наводить порядок. :) Но, ближе к теме.

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

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

Нарушение границы блока мне попалось не далее как сегодня. Выделялась память для локального буфера (фу), 26 байт (два раза фу), потом она заполнялась данными и прогонялась через RC2_cbc_encode, который выравнивает данные в большую сторону на 8 байт. Итого 6 байт за границей блока. Но в данном случае это скорее всего не вызывало проблем. malloc вероятно до 16 байт выравнивает и сам, и реально в блоке 6 дополнительных байт точно было. Так никто бы и не узнал о проблеме.

Эта ошибка является типичной при работе с си-строками.

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

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

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

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

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

Использование неинициализированной памяти. Не знаю как можно извратиться так, чтобы работать с неинициализированным блоком. Но если есть подозрения - можно заполнять блоки при выделении специальным знчением, чтобы сломать приложение.

Выделение блока нулевой длины. В стандарте си по этому поводу написано, что поведение может определяться реализацией. If the size of the space requested is zero, the behavior is implementation-defined: either a null pointer is returned, or the behavior is as if the size were some nonzero value, except that the returned pointer shall not be used to access an object.

Однако далеко не всем приложения может понравиться NULL в ответ на malloc. Хотя помоему это наиболее правильный вариант. Допустимым также можно считать указатель куда нибудь на ридонли память, чтобы приложение не пыталось туда что-нибудь писать. Кстати вот не знаю как должен вести себя в этом случае operator new?

Выделение блоков для этого дела только зря нагружает менеджер, но мне для статистики нужно. :)

Освобождение нулевого указателя. Это даже и не проблема а вполне допустимая операция. Но если приложение дергает ее слишком часто - повод задуматься.

В ликтрейсере даже ругань по этому поводу выключил, очень ее много...

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

PS: Еще немного поковыряюсь с ликтрейсером и наверное выложу его куда нибудь. Может быть будет полезен.