Показаны сообщения с ярлыком работа. Показать все сообщения
Показаны сообщения с ярлыком работа. Показать все сообщения

среда, 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.

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

среда, 26 ноября 2008 г.

Ловушка для меморилика

Не знаю как люди ловаят мемориликов. Пробовал google-perftools, что-то ничего не говорит даже на примитивный пример с единственным malloc. valgrind можно было бы попробовать прикрутить, но это связано с определенными трудностями, поскольку меморилик затаился в сервере c замкнутой программной средой на базе FreeBSD.

К ловле меморилика надо подходить творчески.

Заменить все вызовы malloc на debug_malloc, а потом вглядываться в исписанный экран в поисках утечки - это не наш метод. Хотя при должном уровне автоматизации он тоже должен дать плоды, но такой метод обычно требует модификации кода, и поэтому не является предпочтительным.

Что касается должного уровня автоматизации, я считаю что мало вывести на экран информацию. Гораздо лучше, если эту информацию предварительно обработать. Что же мы можем сделать в случае выделения/освобождения памяти?

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

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

Некоторая проблема есть в том, что работать придется практически на системном уровне. Для реализации мы пишем свои версии malloc, free, realloc, strdup, ::operator new и других, оперирующие с памятью функций. в начале каждого обработчика определяем точку вызова. Но возможности вызвать функции из стандартных библиотек у нас уже нету.

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

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

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

четверг, 20 ноября 2008 г.

Отгадки времени

Забавная проблема возникла на работе. Разрабатываем мы Континент, в нем есть компонент - сервер доступа. Он позволяет пользователям подключаться к VPN извне. Авторизация осуществляется на сертификатах, технология открытых ключей, как там ее.

Появился баг - Невозможно создать сертификат с датами начала/окончания срока действия меньше/больше соответственно 1950/2049гг. Причем интерфейс позволяет ввести в качестве года значение от 1753 до 4111. И откуда взялись эти странные цифры?

Приоритет у запроса естественно был ниже плинтуса, но руки до него всетаки дошли и выяснилось следующее... Время в сертификате может храниться в двух форматах: UTCTime и generalizedTime. Отличаются они собственно одним - UTCTime ограничивает значение года только двумя значащими цифрами, а generalizedTime - позволяет указывать четыре.

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

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

Но текйщий момент времени мы можем получить только в time_t, диапазон которого ограничен датами от 1970 до 2037 года.

Но и generalizedTime сконвертировать в time_t сразу не получится. Можно воспользоваться структурой tm, диапазон которой снизу ограничен 1900 годом.

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

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

Ссылки по теме: RFC2459, RFC3161.

вторник, 18 ноября 2008 г.

Заниматься делом

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

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

Но статья не про это.

Традиционный SCRUM, ежели таковой встречается в природе, подразумевает однородность комманд. То есть вся команда должна состоять из специалистов одного профиля. Например из одних программистов. Наши команды не такие. На треть они состоят из тестировщиков. Что вносит смуту в организацию работ. Наш таскборд состоит из 5 колонок: todo-coding-implemented-testing-done. Я бы конечно оставил три, но это вызывает проблемы с идентификацией работ по тестированию. Может быть выделять их цветом?

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

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

Видимо это происходит из привычной всем схемы работы над ошибками - ошибка может десятки раз ходить от тестировщика к разработчику, до тех пор, пока ее не исправят и не закроют. А это происходит из за того, что кто-то не ставит перед собой реальных задач. Гадание на кофейных гущах какое-то. Исправлять ошибку можно долго, можно заниматься этим годами. Но в условиях SCRUM надо научиться отвечать на вопрос - чем я собираюсь заниматься сегодня. Не так, что сегодня я мусолю ошибку номер xxxx, хрен знает что там такое. А стараться конкретизировать работу. Например: Чтобы исправить эту ошибку я прикручу такие-то возможности, и это позволит понять в чем дело.

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

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

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

Что касается тестировщиков, во избежание большого количества бумажек только крупные работы у нас имеют отдельный этап тестирования. Мелкие работы так и кочуют через все пять колонок таскборда, главное чтобы они не возвращались обратно. Если тестирование выявляет ошибки - ошибки появляются на новых (красных) бумажках с новым временем.

суббота, 4 октября 2008 г.

Автоматизация тестирования

За что я люблю свою работу - это за то, что не скучно. Я бы наверное умер бы от скуки лет пять назад, если бы трудился все свое время над одним единственным проектом. Может быть поэтому и бытует мнение, что раз в три года надо менять работу? Мне не скучно. Редкий проект затягивается больше чем на год. Причем разнообразие в моей работе весьм радикально. Позапозавчера я писал под Линукс, позавчера под линукс для ARM, вчера под FreeBSD на Perl, сегодня я занимаюсь автоматизацией тестирования. :) Под Windows я тоже иногда пишу. Между АРМ и Perl почти месяц потратил на поддержку АРМ ЭЦП. И еще жду не дождусь, когда меня допустят до CSP, ух, я его перекопаю. Но это если меня на сервер под FreeBSD не посадят. Что-то мне FreeBSD не нравится, сил нету, хотя мне же под нею и не жить. Жить я предпочитаю в Linux, даже Windows запускаю в виртуали, благо компьютер мощный.

Но ближе к делу. Итак Автоматизация тестирования. Встала тут задача автоматизировать тестирование нашего сервера Континент (здесь могла бы быть ваша реклама, но стоит наша). Чтобы сразу после ночных билдов сервер автоматически инсталлировался, запускался и специальным инструментом, который мы писали на прошлом этапе, тестироватся бы на соответствие.

Собственно сервер представляет из себя замкнутую среду на базе FreeBSD, ни о каких vmware-tools речи там быть не может. Но надо ведь его как-то проинсталлировать без вмешательства человека. А инсталляция происходит в интерактивном режиме, вопрос/ответ.

Три дня я ломал голову над тем как можно постать в гостевую систему в vmware нажатия клавиш. Я посмотрел VI Perl (не работает ни с VMWare Server ни с Workstation), VIX - работает, но список его функций без mware-tools весьма ограничен. Там даже нельзя подключить носитель!

Я сел за Visual Studio и долго изголялся с SendInput, но VMWare Ctrl+G получает благополучно, а после блокировки клавиатуры - ничего не ловит, видимо перехватывает ввод где-то еще глубже, хотя уж куда глубже? SendInput прокачивает на ввод сканкоды.

Нагуглил, что есть filter driver. И даже видел фрагменты кода, как с помощью этого симулировать нажатия на клавиши, но фрагмент кода был весьма невелик, что-то не осилил я эту премудрость. Да и не факт что это сработало бы с vmware.

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

И только под конец рабочего дня, когда я уже отчаялся послать что-то в vmware, мне пришла в голову замечательная мысль. А почему бы мне не автоматизировать инсталляцию с помощью QEMU, при этом вовсе не обязательно в том же QEMU производить и тестирование. Тем более, что QEMU прекрасно поддерживает vmdk.

И вот в пятницу с утреца я довел до ума программку, которая посылает строки в виртуальную машину. После чего засел за Visual Build, который первым делом вызывал QEMU для инсталляции, выжидал определенное время до первого вопроса инсталлятора (Хотите ли вы установить Континент?), и начинал посылать туда 'y', всякие необходимые цифры. После окончания инсталляции дожидается перезагрузки, убивает QEMU, запускает ее снова, уже для загрузки с винта, опять куча вопросов, и наконец последняя перезагрузка, QEMU по halt выключается сам, после чего запускается машина vmware, с уже настроенной рабочей конфигурацией, и сконфигурированный сервер стартует как ни в чем не бывало.

Только потом я уже сообразил, что наверное можно было бы обойтись и без отдельной программки для передачи символов. Visual Build поддерживает различные скрипты, которые наверное вполне могут позволить сделать FindWindow, SetForegroundWindow и SendInput. Но в скриптах я не силен, мне проще отдельную программульку написать.