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

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

В самом начале XXI века, во времена разработки “Недетских Гонок”, было зафиксировано странное падение, которое долго не удавалось исправить. Падение проявлялось только в Release конфигурации, проявлялось крайне редко, и, как тогда казалось, было просто на ровном месте.

В процессе загрузки уровня проводилось чтение файлов текстур с последующим переводом их в HW-spec формат. Не спрашивайте меня, почему мы тогда в игру грузили стандартные bmp/tga/… файлы – мы были молодые, наглые, и были уверены в том, что преобразование форматов данных вкупе с расчетом мипмапов являлось отличным времяпровождением для процессора, пока пользователь гипнотизирует картинку “Loading”. Процесс перевода заключался в выполнении ряда операций:

  1. Отвести оперативную память.
  2. Загрузить туда файл.
  3. Создать DX текстуру и залочить ее.
  4. Последовательно перегнать графический файл в залоченную область памяти, уже в нужном формате.

К слову, уверенность в том, что данный путь является правильным, испарилась моментально после первого же эксперимента на PS2, но это было только спустя год. А в тот момент времени код жил вполне себе замечательно.

В процессе выполнения пункта 4 был код следующего вида:

BYTE* p;

DWORD pixel = *(DWORD*)(p);

p += sourceImage->bpp;

В переменную pixel при этом записывалось 4 байта из области памяти исходного изображения (p), но, в зависимости от типа исходного изображения, использовалось от 1, 2, 3 или 4 байта данных, в зависимости от byte per pixel (bpp) исходного формата.

Так вот. Этот код падал. Падал крайне нерегулярно, падал только на некоторых компах, и совсем в непонятные моменты времени. Причем если на одном из запусков игра падала, при втором перезапуске все было отлично. Несколько раз нам удавалось словить падение в Release под отладчиком, но даже имея на руках все полные данные о переменных (падение всегда случалось на последнем пикселе изображения), природа падения все равно оставалась непонятной. Самое обидное было в том, что 80% всех известных мне падений у игроков (по результатам анализа логов) тоже приходилось на эту же функцию.

Лирическое отступление 1. Процессор Intel – один из немногих, который умеет читать данные по невыровненным адресам. Большинство известных мне процессоров предпочитает ронять систему в Bus Error, если происходит чтение WORD значения по нечетному адресу, DWORD значения по адресу, не кратному 4, ну и так далее. На PC эти операции проходят безболезненно, в нашем случае процессор автоматически выдает 2 запроса по двум ближним кратным 4 адресам, ну и далее компонует запрошенное значение из 8 байтового результата микропрограммно. Поэтому PC-программисты про bus error и традиционные правила не знают ничего, те, кто начинают программировать SSE или консоли, сначала дико удивляются, но потом все равно привыкают.

Лирическое отступление 2. При работе процессора в защищенном режиме в глобальных и локальных таблицах дескрипторов (LDT/GDT) формируются адреса блоков памяти, которым разрешен доступ по чтению, по чтению и записи, или по исполнению. Тем самым операционная система формирует для каждого из процессов свои виртуальные адреса и блокирует возможности программы по доступу к адресам, которые не относятся к текущему приложению.

Понимание природы этого падения пришло сильно позже, хотя я знал и про bus error, и про LDT/GDT, и даже в свое время писал на ассемблере под DOS примеры, которые переводили 386-й процессор в защищенный режим, что-то там делали и возвращали его обратно. Причиной падения этой функции была редко случающаяся ситуация, при которой область памяти располагалась в самом конце хипа. То есть на последнем пикселе p указывало на разрешенную область памяти, а вот p+1 уже адресовалось к адресу, который был недоступен для приложения.

Ситуация усугублялась тем, что мы всегда считывали DWORD значение из переменной p, даже если bpp равнялся 1. Именно эта “оптимизация” функции и являлась причиной того, что процессу не всегда удавалось считать 3 лишних и ненужных байта из памяти. Код был переписан на switch (sourceImage->bpp) и в зависимости от значения 1, 2 или 4 указатель p кастился в BYTE*, WORD* или DWORD* (тоже не спрашивайте, почему мы сделали так, я уже писал выше, что мы были молодые и наглые).

В общем, симптомы такого бага достаточно устрашающи, но тем не менее, легко определяемы:

  • падение всегда на одном и том же месте;
  • падение при обработке последнего элемента массива;
  • при перезапусках программы падение может бесследно исчезнуть;
  • имеет место определенная “вольность” с загрузкой DWORD значений, хотя вам достаточно слова или даже байта.

Приятной вам отладки. И IsBadReadPtr() функцию в помощь.

  • lenik

    Когда я вижу код подобного вида:

    BYTE* p;

    DWORD pixel = *(DWORD*)(p);

    p += sourceImage->bpp;

    я сразу хватаюсь за пистолет (с)понятнокто.

    Потому что это микрософт с их армиями индусских программаторов может позволить себе писать всё вручную (см.пост СЕМЕНА про WIC), просто завалив проблему избытком рабочей силы, а мелким конторам, если они хотят выжить в конкурентной борьбе и остаться на плаву, рекомендуется использовать современнные средства разработки, и современные подходы, когда не надо заниматься подобным побитовым рукоблудием, и можно отдать компилятору проверку тех назойливых мелочей, которые он способен проверить без участия программиста.

  • http://rageous.livejournal.com Rageous

    классическая ошибка при обработке изображений, ага

  • http://blog.not-a-kernel-guy.com alexeypa

    И IsBadReadPtr() функцию в помощь.

    Использование IsBadReadPtr тоже очень и очень плохо.

  • volodya

    Оверквотинг делать не будем. Кому надо – сам нагуглит. Товарищ Ховард говорит следующее: “Michael Howard calls these APIs “CrashMyApplication” and “CorruptMemoryAndCrashMySystem” respectively.”

    Почему так – use google, Luke :)

  • CEMEH

    В Windows все просто. Длина сканлайна всегда должна быть кратна четырем. И привет.

  • belaz

    Аналогичный баг был и у нас, в одном из проектов (название вряд ли имеет смысл приводить). 2D RPG.
    Для спрайтов использовались RLE сжатые 8 бит изображения (также было много MMX asm кода).
    И где-то раз в 20-40 минут игра падала после 2-3 смен уровней, причём “только в релизе” :-)
    При этом игра работала в 16 бит цвете, и использовались 16 бит палитра размером в 512 байт. А значение в asm коде читалось DWORD’ами… с аналогичным описанному результатом…

  • http://kss.livejournal.com/ Joes

    Когда разбирался с ARM архитектурой (Windows Mobile в частности), писал либу для работы с 2D графикой. Ну там, спрайты (включая всякие трансформации типа вращения и т.д.). Зная что процессор 32-битный, решил оптимизировать скорость alpha-blending через использование DWORD’ов. А поскольку цвет 16-битный, то либа падала на не выровненном доступе к памяти.

    Кстати, я могу еще обрадовать – в x64 доступ к памяти только по выровненным адресам. Если надо читать DWORD откуда-угодно, для VS добавляется __unaligned к указателю, что бы оно побайтово читало и потом собирало результат в отдельной временной переменной/регистре.

  • mephisto

    Вот кстати по теме недавно вопрос задали:

    typedef enum
    {
    One=1,
    Two,
    Three,
    Four,
    MaxIterations
    } EWords;

    EWords type = One;

    int main(int argc,char **argv)
    {
    int returnValue=1;

    while (type

  • mephisto

    //упс не влезло
    while (type

  • http://belightsoft.com/liveinterior/ EugeneGff

    Joes: Не в x64 а в итаниуме видимо. Ну и в SSE/SSE2 до новых мега пупер AMD64 with unaligned SSE.

  • http://kss.livejournal.com/ Joes

    EugeneGff: И в Itanium, и в x64. Так что добро пожаловать в мир выровненных данных :-)