среда, декабря 02, 2009

Искусство отладки: как починить баги

Мы подобрались к Злу вплотную. Вот они, баги - порождения человеческого разума и несчастного стечения обстоятельств. Они бывают разные. Забавные ( Алён, у нас по уровню ходят глаза, это нормально? ) и не очень ( CRASH BUG on the first level! FIX ASAP...).


Что меня всегда огорчало в вопросах починки багов - это многочисленные советы. Причем большинство из них, они как из страны эльфов. "Запустите дебаггер и в пошаговой отладке вы увидите что происходит". ОК, запустили. Под дебаггером у меня все хорошо. У меня вообще многопоточное приложение, его бессмысленно дебажить пошагово. Или я работаю со звуком, иногда проскакивает треск. Как такое пошагово отлаживать?
Вот еще совет. "Если у вас не работает релизная версия, запустите дебагную и посмотрите там". Запустили. В дебаге все нормально. И чего делать-то?
И это не такая уж и редкая ситуация, когда дебаг с релизом вот так кардинально различаются. Обычно здесь смотрять чем отличаются дебагная и релизная версия и что из этих различий оказалось причиной бага. Рассказ про такой баг.
Еще в различных форумах можно полчить мудрые советы, начинающиеся со слов "надо было". Крайне полезно, ага.

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

Первое и главное. Отладка багов требует активного сотрудничества между разработчиками и отделом тестирования, искренней заинтересованности в результате. Конфликты с отделом тестирования и попытки себя уговорить, что "им показалось" бессмысленны. Им не показалось.
И нет такого понятия "чужая бага". Любая бага в проекте - моя. Ведущий программист (иногда задачи в проекте распределяет менеджер, тогда это будет менеджер) назначает чинить багу как правило автора кода, в котором это бага возникла, не потому что этот человек виноват и теперь должен за это поплатиться, а потому что он лучше знает код и быстрее ее починит.

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

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

Если "раньше все работало", а потом вдруг перестало, имеет смысл пошерстить репозиторий и найти методом бинарного поиска версию, с которой начались проблемы.

Трудновоспроизводимые баги заслуживают самого пристального внимания. Здесь спасает анализ ситуации и придирчивое чтение кода. Когда, при каких условиях бага проявлялась? Почему именно тогда? Как повысить воспроизводимость? Если повезет, то через какое-то время у вас будет не один, а несколько случаев проявления баги, их можно сравнивать, искать закономерности.

Выводы могут оказаться неожиданными. Пример: Open Office не печатал по вторникам. Бага оказалась не в Open Office, а в утилите file, которая определяла тип файла. По вторникам она определяла файл, отправленный на печать, не как PostScript файл и получалось, что напечатать его нельзя. Дело в том, что в начало файла записывалась дата. Начиная с четвертого байта по вторникам там лежало Tue, от Tuesday. Для утилиты это было знаком того, что это файл Erlang JAM.

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

Одним из видов трудновоспроизводимых багов являются ошибки по памяти. Работа с "мусором", произвольная порча памяти и подобные. Тут можно смотреть чем именно была затерта память, на что это похоже. Отладка ошибок по памяти подробно проанализирована тут: Debugging Memory Corruption in Game Development.

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

Ошибки в многопоточных приложениях крайне неприятны и плохо воспроизводятся. Тут кроме анализа ситуации можно использовать волшебные тулзы типа Intel Threads Checker. Вот кроме него ничего и не знаю, прям стыдно. Под другие платформы наверяка есть свои тулзы.

Процесс починки багов любят объяснять, разбивая его на шаги. Давайте я тоже шаги напишу что ли...
Итак, хорошая последовательность действий при починке багов:
1. Понять в чем именно состоит проблема. Воспроизвести.
2. Прикинуть как ее можно решить, выбрать наилучший способ.
3. Имплементировать.
4. Проверить что проблема решена и что новых не возникло. (!не надо пропускать этот пункт)
5. Подумать над тем, что к проблеме привело, чтобы исключить подобные проявления в дальнейшем.

Классическая плохая последовательность действий:
Вот, кажется тут! Да, починил. А, не, не тут и не починил, отдел тестирования опять поймал это... ОК, смотрю опять, опять чиню.
И так по кругу. Процесс и не думает сходиться. Старый код починки остается, он бессмысленно замусоривает проект и служит источником новых багов. Багов остается столько же или их количество растет.

Как замаскировать
Если есть проблемы с починкой баги, ее можно временно замаскировать, чтобы потом вернуться к ней позже.
Итак, поздний вечер, завтра надо проект поставлять, пять минут назад была обнаружена злая бага (почему мы не замечали это раньше???), как чинить непонятно вообще. Очень хочется спать и есть. В таком случае, преисполнившись ненавистью к себе, обрамляем код комментариями //FIXME или тем, что принято в вашем стандарте кодирования. И пишем что-то вроде

if( value == 6 ) value = 5;
Ну или что-то такое же мерзкое, что решит проблему, хотя бы частично.
Хинт: иногда надо делать поиск в коде по FIXME. Сокрушаться над количеством найденного не надо. Надо чинить.

Байки:
Байка про стену
Байка про return в пустоту
Описание одного бага, другого наведённого бага, и одной отладки

Ссылки:
Дядя Дима - Рецепты отладки. 3 типа нестабильностей.
Дядя Дима - Теория ошибок. Нестабильности первого рода.
Дядя Дима - Теория ошибок. Нестабильности второго рода.
Дядя Дима - Теория ошибок. Нестабильности третьего и четвертого рода.

19 коммент.:

embarger комментирует...

Чтение документации — великая вещь.

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

Очень скоро я заметил, что программа реагирует на нажатие клавиши строго через одну итерацию. Т. е., например, когда персонаж игры (аскиишный символ) входил в зону, где должен был терять жизнь. И терял. Но он также терял ее когда ВЫХОДИЛ из этой зоны.

Короче говоря, баг мной так и не был отловлен, в цикл было успешно засунуто паразитное ReadKey;, которое решало проблему.

Только учившись в новой школе Паскалю у грамотной женщины (работала программисткой на советскую оборонку), я узнал, что функциональные клавиши (а стрелочки, которым я в игре «ходил» — функциональные клавишы) генерят два символа в буфер, а не один. Сначала 0 (что-то он значил, забыл что), а потом только ASCII-код.

Решение было элементарным:

c := ReadKey;
if (c = 0)
c := ReadKey;

StaticAn комментирует...

Насчет отладки потоков - вот тут http://blogs.microsoft.co.il/blogs/sasha/archive/2009/11/17/pdc-2009-day-1-concurrency-fuzzing-amp-data-races.aspx рассказывается о тулзе из MS Research, которая должна отлавливать ошибки с потоками.
Вряд ли, конечно, это рабочий инструмент, но в целом интересно...
И еще несколько инструментов отладки из того же источника:
http://blogs.microsoft.co.il/blogs/sasha/archive/2009/11/20/pdc-2009-day-3-power-tools-for-debugging.aspx

Alex Ott комментирует...

про поиск ошибок работы с памятью под юниксами я писал статью. а google performance tools работают и под виндой вроде
P.S. а так, полностью согласен с написанным, особенно про "долго сидеть в отладчике" - как такую ошибку отловишь, если она проявляется при 5000 реквестов/секунду и только на определенной модели железа...

AlfeG комментирует...

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

Очень часто решения вида
if( value == 6 ) value = 5;
Не помогают, потому что ломается только на одной из версий и толька на аппарате клиента.

Мне и менеджеру, это стоило недели жизни по ночами, пока не нашли что на аппарате клиента было установлено время на 2000 год...

А если учитывать что iPod с идиотической частотой сбрасывает время в 1 января 2000 года, то бага оказывается не столь тривиальной.

AlfeG комментирует...

А ещё очень нравится путь поиска багов от Tess: http://blogs.msdn.com/tess/default.aspx

alco комментирует...

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

Алёна комментирует...

2alco:
К пятому пункту неплохо бы добавить рекомендацию о в процессе разработки.

я не понимаю о какой рекомендации идет речь...

.sid комментирует...

Для повышения воспроизводимости ошибок в многопоточном environment'e можно в предполагаемых местах race condition'ов повставлять инструкции передачи управления другому потоку (sleep(0)/Thread.yield()).

somebody комментирует...

прочитав "спорим" с коллегой по поводу пункта 0 - "спросить соседа а не знаешь ли ты что тут за ерунда может быть" %-)

alco комментирует...

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

Vadym Stetsiak комментирует...

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

Принцип такой: приложение застопорилось или дэдлок, или 100% CPU - снимаем дамп приложения (в Windows можно использовать UserDump). Далее отрываем полученый дамп с помощью WinDbg и видим, что к чему.

pullover комментирует...

Для MS разработчиков может быть интересно. Про поиск хайзенбагов:

http://research.microsoft.com/en-us/projects/chess/

digital padonok комментирует...

Алена,
по поводу тулзов от Интела для многопоточного кода - помимо Thread Checker у них теперь есть целый набор из компилятора, мемори чекера, тред чекера и анализатора узких мест... Называется это все Parallel Studio, подробнее здесь: http://www.intel.com/go/parallel/

zg комментирует...

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

Alex Ott комментирует...

2zg: особенно когда ошибка воспроизводится только при 6 тыс. одновременных клиентов на 4-х процессорной машине :-)
где взять такой эмулятор?

zg комментирует...

где взять такой эмулятор?
напишите.

Alex Ott комментирует...

2zg: легко сказать, "напишите"...

P.S. я отладчиком не пользуюсь кроме как core файл посмотреть после крэша...

zg комментирует...

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

hax комментирует...

Сидит программист глубоко в отладке.
Подходит сынишка:
- Папа, почему солнышко каждый день встает на востоке, а садится на западе?
- Ты это проверял?
- Проверял.
- Хорошо проверял?
- Хорошо.
- Работает?
- Работает.
- Каждый день работает?
- Да, каждый день.
- Тогда ради бога, сынок, ничего не трогай, ничего не меняй...


Это типа ещё одна рекомендация по уменьшению количества багов :)