Рецепты отладки. Падение на ровном месте.
В развитие рассказа CEMEH про страшный олдскульный WIC хочу рассказать историю, которая смогла успешно разрешиться только благодаря точу, что традиционные методы отладки были совмещены с хорошим пониманием принципов работы целевой платформы, роль которой успешно исполнил интеловский процессор, работающий в защищенном режиме под управлением операционной системы MS Windows.
В самом начале XXI века, во времена разработки “Недетских Гонок”, было зафиксировано странное падение, которое долго не удавалось исправить. Падение проявлялось только в Release конфигурации, проявлялось крайне редко, и, как тогда казалось, было просто на ровном месте.
В процессе загрузки уровня проводилось чтение файлов текстур с последующим переводом их в HW-spec формат. Не спрашивайте меня, почему мы тогда в игру грузили стандартные bmp/tga/… файлы – мы были молодые, наглые, и были уверены в том, что преобразование форматов данных вкупе с расчетом мипмапов являлось отличным времяпровождением для процессора, пока пользователь гипнотизирует картинку “Loading”. Процесс перевода заключался в выполнении ряда операций:
- Отвести оперативную память.
- Загрузить туда файл.
- Создать DX текстуру и залочить ее.
- Последовательно перегнать графический файл в залоченную область памяти, уже в нужном формате.
К слову, уверенность в том, что данный путь является правильным, испарилась моментально после первого же эксперимента на 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() функцию в помощь.