вторник, 23 декабря 2008 г.

uleak

Создал новый проект на googlecode, который назвал uleak.

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

Почему такое странное название? С названиями у меня всегда проблема. С одной стороны длинное название труднее запоминать. А с другой стороны длинные названия обычно больше говорят о себе. Но всетаки предпочитаю краткость. uleak - это типа как umount... :)

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

К тому же, я там всякой фигни наворотил насчет фильтрации функций. Вероятно их надо будет потереть все. Да и с блокировками тоже малость намудрил.

Да и не все мысли еще воплотились. Например, под linux наверняка можно использовать libUnwind, которая почему-то не работает под FreeBSD. Но под linux uleak пока не заработает. В линуксе его наверное можно будет использовать к уже скомпиленным проектам через LD_PRELOAD, но я никогда с этим не работал.

Если у кого-то есть в сопровождении проблемные проекты, можно попробовать погонять их с uleak. Это поможет сделать его более универсальным и полезным.

PS: Кроме того на сайте uleak я доработал классификацию проблем, с удовольствием выслушаю критику.

PPS: самый короткий пост?

среда, 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: Еще немного поковыряюсь с ликтрейсером и наверное выложу его куда нибудь. Может быть будет полезен.

понедельник, 8 декабря 2008 г.

Ловушка для меморилика (проблемы)

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

Первая проблема возникает тогда, когда мы пытаемся воспользоваться функцией printf. И заключается она примерно в следующем: первый раз malloc вызывается задолго до вызова функции main, и видимо задолго до инициализации подсиситемы ввода-вывода. В результате попытка дернуть printf из malloc приводит к тому, что printf пытается выделить буфер, и опять таки вызывает наш malloc. Замена оператора printf чем нибудь (sprintf, puts, и тд) ничего путного не принесла. Но наша библиотека в любом случае требует инициализации. Мы просто проинициализируем буфер stdout, можно в _IONBF, и эта проблема себя исчерпывает.

setvbuf(stdout, NULL, _IONBF, 0);

Другая проблема проявляет себя во всей красе, когда мы пытаемся использовать библиотеку в многопоточном приложении. Как ни крути, а медленный менеджер хипа надо защищать блокировками. Но pthread_mutex_lock, опять таки, вызывает malloc для инициализации tls, локов и тд. Надо сказать что libpthread во FreeBSD весьма закрытая вещ. Все структуры у них сугубо приватны, всмысле в инклюды не включены, и выделить и проинициализоровать лок самостоятельно не выходит.

Долго бился над этой проблемой. Пытался прикрутить свои блокировки на ассемблере, не помогает. Еще что-то пытался - сейчас уже не вспомню. Но приложение упорно уходило в кору по исчерпанию стека. В результате поступил следующим образом: если гора не идет к Магомету... в смысле, если pthread_mutex_lock вызывает malloc, тогда мы не будем из malloc вызывать pthread_mutex_lock. Но и совсем от него мы тоже не может отказаться. Мы не будем вызывать блокировку только в том случае, если malloc вызывается из недр libpthread... хотя наверное можно было бы просто сделать проверку на рекурсию, флажек какой нибудь, это наверное тоже сработало бы. Хотя я вроде даже что-то такое пробовал, не помню. Сделал по хитрому. я проверяю адрес возврата на попадание в области критичных к рекурсии функций, и в этом случае не произвожу блокировку. Извращенный метод конечно, но показало себя вполне надежным.

Но это была еще и не последняя проблема. Последняя проблема с трудом поддается моему пониманию. Заключается она в том, что при переключении потоков приложение падало в функции _thr_setcontext, на операции FXRSTOR. Поковырявшись немного в ядре вычитал, что блок данных для хранения контекста FPU должен быть выровнен на 16 байт. Исправилось выравниванием всех блоков в менеджере хипа на 16 байт. Но так и не могу понять каким образом сохранение контекста FPU производится в буфер, выделенный пользовательским malloc, в пользовательском же пространстве? Возможно, там используется какой-то хитрый коллбек из ядра... странно это. Я полагал, что этим должно заниматься ядро.

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

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

Ловушка для меморилика (слайды)

Струя воды толщиной в спичку
дает утечку двести литров в сутки...
(c) Афоня



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

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

Но обо всем попорядку. Как же это работает?

Сразу отмечу, что данный код работает только под FreeBSD 32bit, возможно он сможет работать и под Linux - не проверял, но точно не заработает на 64-х битной платформе. Кроме того весь код приводить тоже не буду, может быть выложу куда нибудь.

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

extern "C" void *malloc (size_t size);
extern "C" void *calloc (size_t number, size_t size);
extern "C" void *realloc (void *ptr, size_t size);
extern "C" void *reallocf (void *ptr, size_t size);
extern "C" void free (void *ptr);
extern "C" char *strdup(const char *str);
void *operator new(size_t size);
void operator delete (void *ptr) throw();
void *operator new[] (unsigned int size);
void operator delete[] (void *ptr) throw();

Каждая из этих функций представляет из себя обертку к медленному и простому хип менеджеру.
Например:

extern "C"
void *malloc (size_t size)
{
return salloc (size);
}

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

static
uint32_t getCallPoint(const void *stack)
{
const uint32_t *sptr = reinterpret_cast(stack);
return sptr[-1];

}

extern "C"
void *malloc (size_t size)
{
const uint32_t cp = getCallPoint(&size);
return salloc (size, cp);
}

Вот так будет достаточно. Что же касается менеджера памяти, то переопределив malloc во FreeBSD мы потеряли всякую возможность воспользоваться стандартным, и вынуждены писать свой. Но так даже лучше, в четвертой статье расскажу почему.

Релизация у него будет предельно проста. Заводится статический буффер большого размера. Определяется структура заголовка блока, которая состоит из двух полей: cp - точка выделения блока, и size - размер блока. Если cp имеет нулевое значение - это обозначает, что блок пуст.

И для учета блоков по точкам вызова мы заводим массив структур:

struct callpoint {
uint32_t cp;
uint32_t current_blocks;
uint32_t max_blocks;
uint32_t size;
};

static struct callpoint scps[cnum];

Размеры хипа и таблицы точек вызова я выбрал вроде бы с запасом, 16 мегабайт и 4096 точек. При выделении каждого блока я нахожу соответствующую точке запись и увеличиваю счетчик блоков. Если current_blocks превышает max_blocks, это может быть утечка. Для понижения уровня шума max_blocks можно проинициализировать некоторым значением - у меня 100. То есть до 100 блоков на точку - ругани не будет, дальше будет. И при каждом повышении количества блоков ругань будет продолжаться.

При освобождении блока я извлекаю точку вызова из информации блока, ищу эту точку в таблице и понижаю счетчик current_blocks.

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