Рецепты отладки. Потеряный малок.

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

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

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

Из-за этого пришлось сконцентрироваться на работе с системным менеджером памяти. К сожалению, реализации системных функций типа heapfirst/heapnext/heapwalk не было, поэтому как подступиться к задаче, было не очень то и понятно. К счастью, в той версии игры действовало отличное правило “no malloc per frame”, поэтому после начала заезда можно было поставить брекпоинт на аллокатор (только пришлось довольно глубоко залезать в недра CRT, чтобы найти ту самую нижнюю функцию, через которую проходят вся безумная дюжина внешних и внутренних вариантов функций аллокации).

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

Выручило нас то, что существовала отладочная версия менеджера памяти, которая полностью работала со своей собственной картой распределения памяти. Отладочный менеджер памяти был переориентирован на работу со своим хипом, в стандартный маллок было забито что-то типа strcpy(malloc, “Умри, сцуко”), и работа над ловлей бага возобновилась. Когда наконец удалось восстановить колстек, матюгам не было предела.

Лирическое отступление. Вообще в любой ситуации, когда оперативная память фиксированная и ее немного, жесткие аллокации – это максимально возможное зло. Разные команды предпочитают решать эти вопросы по разному. Одни устраивают гибкую хендловую систему, когда блоки moveable, fixed, еще не дай бог, discarded. И в случае нехватки памяти начинают возню с перестановкой блоков, укомпакчиванием хипа и т.п. Другие (и я в том числе) предпочитают решать вопрос методом “разделяй и властвуй”, когда разным компонентам достается определенное количество оперативной памяти, в котором они могут устраивать все, что им заблагорассудится. Где то даже слышал историю, только не помню где, что в одной из команд за распределение памяти шла реальная битва, и аниматоры ходили к художникам, чтобы те уступили им полмега своей оперативки “потомуштаващепесдетс и никак анимации не влезают”. В любом случае неизвестная жесткая аллокация прямо посередине оперативки не добавляет оптимизма ни тем, ни другим.

Колстек показал, что неизвестная аллокация шла из недр sprintf(buf, %f, …), который вызывал преобразование вещественного числа в строку, и где-то там внутри _gcvt() изредка (1 раз на 40000 вызовов) выделялось несколько байт оперативки, которые и оставались висеть в оперативной памяти до окончания работы программы. Решением проблемы стала собственная реализация sprintf(), которая не страдала внутренним выделением памяти.

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

1. Реализация функций типа heapwalk. В CRT DBG MSVC эти функции присутствут, хотя их функционал не полностью адекватен. Например, они не умеют ловить разницу аллокаций new/new[]/malloc/aligned_malloc и соответствующих деаллокаций (ну разве что ядерная смесь new[]/delete на классах с нетривиальным деструктором упадет по access violation из-за корректировки адреса на 4 байта).

2. Реализация возможности подменить стандартную системную карту аллокаций на свою собственную (менеджмент блоков в заранее выделенном хипе; может быть реализован как на своих алгоритмах, так и на чем-то стандартном). Такой функционал требуется не только для отладки – если реализовывать схему “разделяй и властвуй”, аллокации в хипу будут полезны для многих компонент. Например, можно заставить Lua крутиться только в рамках своего собственного хипа, хотя если его размер ограничен, Lua надо будет отдельно дрессировать.

3. Подмена стандартной системной карты распределения может быть полезна и в двух других ситуациях: для вскрытия тяжелых “проездов” по памяти из-за другой карты распределения памяти, и для отладки ситуаций, когда сам хип оказывается безнадежно поврежден (неоднократно выручало, когда аллокации на PS2 падали в недрах внутренних аллокаторов – переключали на свой отладчик и смотрели по исходникам, почему затирались системные структуры распределения памяти).

Да, всем желающим написать в каментах, что sprintf() является убогим наследием С и надо пользоваться “правильным” std::string << float, сначала рекомендую посмотреть по исходникам CRT, куда именно уходят вызовы преобразования вещественного числа в строку.

Приятной вам отладки.

  • Tsukrov

    Пять копеек в кассу:
    А разве sprintf не должен был освободить то, что он навыделял?
    Или хотя бы переиспользовать для следующих операций?

  • http://www.psyenfant.com/ KnoPpa

    sprintf() является убогим наследием С и надо пользоваться “правильным” std::string

  • dDIMA

    По идее – должен был бы.
    Или даже не аллокировать, а держать статический буфер на 128 или сколько там ему надо байт.
    Но он не делает ни того, ни другого.

  • http://aruslan.livejournal.com/ aruslan

    Мне вот другое интересно.
    А что там вообще печаталось-то, да еще как %f?
    Или это была отладочная печать?

  • dDIMA

    Нет, не отладочная. Плавающим по экрану текстом печаталось расстояние до ближайшего гонщика. До второго знака, вот! :)

  • http://aruslan.livejournal.com/ aruslan

    > Нет, не отладочная.
    > Плавающим по экрану текстом печаталось расстояние до ближайшего гонщика. До второго знака, вот! :)

    Жжоте!