?

Log in

 
 
01 Октябрь 2008 @ 09:35
Рецепты отладки. Четырехмесячный дебаг.  
Еще один из случаев, произошедших достаточно давно, не может использоваться как иллюстрация того, как можно быстро и эффективно поймать ошибку, но, тем не менее, является хороший примером с далеко идущими последствиями. Речь идет о четырехмесячном и неудачном дебаггинге.
 
8 лет назад мы разрабатывали (точнее, портировали на Dreamcast) игру Millenium Racer (в России известна как Вираж-3000). До поры до времени все шло гладко, пока в какой-то момент времени не было обнаружена феноменальная загадка: на пятом круге езды по трассе в том месте, где должна была находиться довольно прямая дорога, обнаружилась длинная стена (для тех, кто не видел игру, напомню, что игра заключалась в кольцевых гонках на футуристических гравимотоциклах по закольцованному уровню). Довольно быстро было обнаружено, что стена возникает через 10-15 минут игры на уровне, причиной ее возникновения является то, что 1 вполне себе валидная вершина у одного из трианглов вдруг неожиданно оказывается в начале координат. Как следствие - стена протягивается через весь уровень, и хотя она абсолютно не мешает коллижену, визуально играть становится невозможно.

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

Ситуация усугублялась тем, что это был проект для Dreamcast с довольно ограниченными функциями отладки под Metrowerks CodeWarrior, а на PC ситуацию воспроизвести не удавалось.

Это был один из редких случаев отладки, когда на решение проблемы потребовалось 4 месяца (естественно, не full time, но целенаправленно на поиск ошибки я тратил несколько полных рабочих дней в месяц). Ошибка не поддавалась. Мы сдвинули пространство так, чтобы нулевых вертексов не было по определению, и на каждом фрейме проверяли VB. Мы мониторили все возможные записи данных. Мы делали огромное количество других попыток, но все было бесполезно. Нам требовалось ровно то место записи в VB, которое было причиной; детектирование ситуации через *** миллисекунд после произошедшего не давало нам ключа к разгадке.

Ситуация разрешилась именно благодаря тому, что бага была в очередной раз отложена в надежде, что постоянно модифицирущийся код рано или поздно сломает мерзкую запись нуля в VB и мы таки получим новую пищу для размышлений. И действительно, как то раз на длительном заезде игра упала в access violation на нулевом четырехбайтовом присвоении. Почувствовав запах дичи, я взревел как раненый гризли, немедленно огородил висящий в дебаге компьютер ленточками с надписью "don't cross" и засел за тщательное выяснение того, почему упала программа.

Найденный ответ был неожиданным. В Millenium Racer использовалась озвучка персонажей, когда мы обгоняем их, или они обгоняют нас. При этом для разнообразия звуков было несколько, выбирался один из звуков по рандому, ему ставился флаг "использования", чтобы рандом на следующем применении не дал снова этот звук, а выбрал какой-то другой. Выяснилось, что раз в несколько тысяч запусков программа рандомизации дает сбой. Несмотря на то, что в нашей спецификации было указано, что random(count) выдает числа от 0 до count-1, иногда могло получиться и значение count. Как следствие - код звуковой логики адресовался к элементу звука Sound* array[count], который чудесным образом 4 месяца подряд попадал внутрь VB. random был наш собственный, VB в дримкасте лежал в основной памяти, в итоге проверка вида "если этот звук использовался на предыдущем фрейме, занулить флаг использования" воздвигала однополигональную стенку на нашем пути.
 
Несмотря на то, что бага не была отлажена традиционными путями, какие выводы были сделаны из нее?
1. Принцип "попробуй позже". Если багу отловить не удается, но есть запас времени, можно просто попробовать вернуться к проблеме потом, когда изменения в программе и данных позволят проявиться проблеме в более легкой форме. Тем не менее, см. второе замечание.
2. Если бага проявляется с трудом, попробуй отловить ее на измененных настройках приложения. Это могут быть другие параметры компиляции или оптимизации, временное включение ассертов в релизе и т.п. Хорошие результаты может дать также защита проекта какой-либо системой защиты (Starforce, Securom и т.п.).
3. Несмотря на то, что я не отношу себя к ярым поклонникам TDD, необходимость использования юнит-тестов очевидна. В данном конкретном случае тест для рандома пишется тривиально, но он помог бы сэкономить полмесяца моего дебаггинга. Как пример: сейчас в RedKey система автоматизированной сборки прогоняет тесты на подсистемы, и за последние два месяца дважды срубались неожиданные тесты после несущественных коммитов – это верификация интерсекшена и ошибки в тестах на мультизадачность. Несмотря на то, что повторный прогон тестов никаких проблем не выявил, это – серьезный сигнал, который требует существенного внимания. Потому что если проблема произошла 1 раз, она запросто произойдет и снова.
4. Необходимость ассертирования и отладочной диагностики. К сожалению, в 2000 году мы писали на Pure C, поэтому об ассертировании оператора [] можно было и не думать. Вообще, если пользоваться моей любимой аналогией с расследованием авиакатастроф, то для возникновения этого сложноуловимого бага было две основные причины, и изменения в каждой из них привели бы к тому, что баг не состоялся. Первая – это работоспособность random() не таким образом, как предполагалось, и вторая – это отсутствие диагностики у [].
Crossposted to blog.gamedeff.com
Метки: ,
 
 
Местонахождение: office
 
 
 
simsmensimsmen on Октябрь, 1, 2008 08:48 (UTC)
Четыре месяца ломать голову над одним багом, даже если и не каждый день... сильно. У меня бы терпения не хватило и жить на пороховой
alll: шушпанчикalll on Октябрь, 1, 2008 09:12 (UTC)
Вот за это C и не любят. Не работа, а лечение насморка пургеном.
Дядя Дима: officeddima on Октябрь, 1, 2008 10:47 (UTC)
Ну скорее, выводы - они не про С конкретно, а про подходы к предотвращению и поиску таких багов. Возврат значения random() в диапазоне, не предусмотренном документацией - оно как бы не сильно С специфик - "такое может случиться с каждым!" :-D
Дмитрий Ивашкинivashkin on Октябрь, 1, 2008 08:55 (UTC)
Красивая история!
Гагиев Тимур: золxproger on Октябрь, 1, 2008 09:18 (UTC)
Как по закону жанра в совершенно неожиданном месте, и мегатупая... мир жесток :(
doc_allegator: :)doc_allegator on Октябрь, 1, 2008 12:21 (UTC)
Приятное чтиво. В живую да под водочку, такие истории идут ещё лучше. Да и вообще любые истории с хэппи эндом.
anton_yakovlevanton_yakovlev on Октябрь, 1, 2008 13:58 (UTC)
Так этот кодевориор появился уже на дримкасте? Я-то думал, что mcw madness в сислибе начался только с приходом PS2.
Дядя Димаddima on Октябрь, 1, 2008 14:39 (UTC)
Кодевориор появился еще на дримкасте.
Кроме кодеворира не было вообще больше никаких компиляторов.
SBF там тоже еще не было - была его предварительная инкарнация в виде placement сохраненного scnSCENE.
anton_yakovlevanton_yakovlev on Октябрь, 1, 2008 17:38 (UTC)
Ну про scnSCENE я знаю конечно, видел средства для стаффинга в SceneProject. :)

В своё время хотел найти его код, но так и не удалось: исходники шестого и седьмого SP не сохранились :)
(Анонимно) on Октябрь, 2, 2008 11:13 (UTC)
=)
Trimax: тяжела и неказиста жизнь трудяги программиста (С)
Бывает... =)
(Удалённый комментарий)
Дядя Дима: officeddima on Октябрь, 2, 2008 13:34 (UTC)
5. В данном случае запись делалась не за концом страницы, а по указателю, который ссылался в самую что ни на есть середину датасегмента. Ну считай почти как:
DWORD* p = get_random_address();
if (!IsBadWritePtr(p, 4)) *p = 0;
Засунь это куда-нить в код и попробуй отловить? :)
6. ИМХО повторяет мой №4. Но тогда было неприменимо - см. замечание про Pure C.
(Удалённый комментарий)
Дядя Дима: officeddima on Октябрь, 2, 2008 14:03 (UTC)
Такой случай, который я привел чуть выше, отловить практически нереально. Даже боундс чекером.
Эльф Грекоффelgreco on Ноябрь, 29, 2009 19:32 (UTC)
May be получится сделать некрасивую вещь: обернуть [] в класс с ассертами, сделать сopy-replace на все случаи создания массива из встроенных типов данных на этот класс, и перекомпилироваться под C++, включая старый СИшный код?

Можно пробовать некую специализацию класса под разные типы данные.

Этот как раз тот случай, когда накладных расходов - ноль (только станет больше объектного кода за счёт инстанцирования шаблонов).
ivansorokinivansorokin on Декабрь, 3, 2009 18:44 (UTC)
boost::array?

std::vector?