Предыстория
Когда-то давно,
Что имеем
IDA — для дизассемблирования
Cheat Engine — для редактирования памяти и инструкций
Visual Studio — для отладки (Trace Points, оказались весьма удобной вещью)
У нас есть куча дампов. Приличная куча, гигабайт на 10. С них мы и начнем — проанализируем, на каких инструкциях падает игра. А падает она довольно рандомно, хотя некоторые закономерности прослеживаются. За время решения проблем мы нашли несколько потенциально опасных мест, которые иногда крашат игру. Например:
в функции вычисления хэша строки. Видимо, разработчики не ожидали получить null-pointer в этом месте, поэтому не добавили проверку на него. Из-за этого в редких случаях игра падала. Фикс довольно банальный — прыгнуть в первый пустой кусок экзешника, да сделать test edi, edi. Потом jz retun и jmp откуда прыгали изначально.
Другой похожий случай нашёлся в процедуре по адресу
0х0057D105 mov edx, [ecx] ; я так и не смог понять, что конкретно она делает
Разработчики снова не ожидали получить там null pointer, поэтому игра падала. Фикс абсолютно идентичен предыдущему.
Наиболее распространённая причина падения оказалась в функции AllocateMemory. Попытки ее дизассемблирования повергли в ужас всех, кто работал над проблемой падений игры. Внимания удостоен уже тот факт, что в игре как минимум 5 разных подсистем управления памятью. Во что я ввязался…
Ладно, нет времени ныть, надо реверсить. Несколько вечеров за разборами этого мусора принесли свои плоды: код, хоть все еще и не читаемый, стал более понятен. Судя по всему, эта подсистема работает по стандартной схеме: грабастаем некоторое количество памяти сразу, разбивая на блоки, храним их в двусвязном списке; по требованию выдаём свободные участки, а если таковых нету — пытаемся взять у системы еще. Ах, 2005-ый, когда операции с памятью были достаточно дорогими, чтобы ей разбрасываться как попало…
Некоторые места в этой функции вызывают у меня приступы головной боли, потому что мой мозг напрочь отказывается даже пытаться их обработать. Но одно мне ясно точно — где-то среди всех этих связных списков, состоящих из связных списков, кроется неправильный указатель, из-за которого все и падает. Единственное решение, пришедшее в голову — отключить проверку «use_best_fit» чтобы подсистема выдавала первый попавшийся свободный блок, а не искала тот, который она считает наиболее подходящим.
Конечно, проблемы целиком это не решило, но как минимум игра стала действительно стабильнее — за неделю тестирования в этом конкретном месте она упала всего несколько раз (при учете, что KuruHS проводит в игре по 10 часов в день), что я считаю довольно неплохим результатом.
Pure virtual function call.
Та самая ошибка, что проиллюстрирована в шапке. Люди, знакомые с С++ сразу поймут, в чем проблема. Однако без исходного кода все становится значительно сложнее. Ситуацию усложняет CRT, который, аки партизан, упорно не хочет генерировать дампы, если вылавливает этот тип ошибки.
Purecall означает, что код попытался вызвать «чисто виртуальную функцию» (виртуальную функцию класса, не имеющую реализации). Без сомнений, этого сделать у него не получается, поэтому единственное, на что он решается — сообщить об этом пользователю и завершиться с кодом 0. В итоге вроде бы все и хорошо с кодом, а на деле все плохо.
Спасибо Microsoft за замечательную функцию — _set_purecall_handler, которая позволяет заменить обработчик purecall'ов. Ищем в экзешнике упоминания/ссылки, находим саму функцию. Теперь осталось написать свой обработчик и не забыть установить его как хэндлер. Для этого нам нужно найти достаточно большой кусок неиспользуемого кода в самом экзешнике, который мы сможем перезаписать на наш код. Недолгий поиск показал, что это будет функция _CxxThrowException (ссылок на нее не было найдено). Беспощадно записываем все ее тело nop'ами и начинаем творить поверх нее:
Вот так будет выглядеть псевдокод новых процедур:
new_handler:
xor eax, eax ; return *(0);
mov eax, [eax] ; моментально валит игру
ret
set_handler:
push new_handler
call _set_purecall_handler ; _set_purecall_handler(new_handler);
add esp, 4 ; cdecl, восстанавливаем стек
ret
Компилируем (в моем случае руками вбиваем в Cheat Engine) и вставляем в код:
Теперь нужно найти подходящее место для вызова этой процедуры. Действительно подходящего я не нашёл, но нашел одну замечательную пустую функцию прямо в мэйн лупе игры, так что ее вызов подменям на вызов написанной нами функцией. Делаем патч и можно тестить.
Единственная проблема заключается в том, что ошибка эта довольно редкая, а часами бесцельно играть не хочется. Я все же решил немного потестировать сам, и был приятно удивлен — игра упала буквально через 10 минут геймплея, причем упала на только что написанном мною участке. Перемещаемся по стеку вызовов немного выше:
0043E005 call dword ptr [edx+80h]
Ничего не могу сказать, кроме как: «да, это вызов виртуальной функции». Первая же мысль — а что, если без него? Выпиливаем его nop'ами, тестируем — вроде живем. Игра работает как надо. Побочных эффектов нет. Собираем патч, отсылаем на тестирование. Через день прилетает дамп, где та же процедура падает несколькими байтами ниже. Выпиливаю и ее — игра начинает падать. Все ведет к тому, что нужно думать над более серьезным решением. Но в голову ничего не лезет, поэтому откладывается на неопределенный срок.
За ночь я успел все обдумать, и пришёл к выводу. Вы скажете, что С++ не умеет в рантайме определять тип объекта? А я скажу, что может. И очень просто — по адресу виртуальной таблицы в памяти. Изучив дампы, я пришел к выводу, что периодически в процедуру прилетает неправильный класс (vtbl @ 0x00890970), а значит мы можем отловить эту ситуацию:
cmp edx, 00890970h
jnz good_class
xor eax, eax
jmp return
good_class:
call dword ptr[edx+80h]
jmp continue
Но есть одна загвоздка: это занимает достаточно много места, и это необходимо встроить в процедуру. Найти достаточно места не получится, все что есть — пара пустых кусков в несколько байт перед функцией. Спасибо уже и на том, что их много и они близко. Поэтому пишем спагетти и прыгаем из одного места в другое чуть ли не после каждой инструкции:
Патчим и запускаем. И получаем всё ту же проблему: этот крах настолько редкий, что за почти 4 часа тестирования этот кусок кода был запущен всего пару раз, и все разы на вход был получен правильный класс.
Можно было оставить и так, но мне нужно было подтверждение тому, что это действительно работает. Поэтому отправляемся реверсить дальше и пытаться вызвать исключительную ситуацию руками.
Быстрый осмотр показал, что игра может упасть, если один из аргументов не равен нулю. Сама процедура вызывается всего в двух местах, причем в одном из случаев вызывается с тем самым аргументом, выставленным в 0. Значит смотрим другую функцию.
убираем по максимуму «лишние» проверки и пытаемся насильно вызвать эту функцию. Запускаем тестирвоание и наконец-то получаем неправильный класс на вход. Ждём, пока отладчик студии допечатает весь текст, игра отвисает и… продолжает работать. Ура!
Скриншот мыльный, ибо запись со стрима
Заключение
Решение найдено — игра больше не падает, даже если на вход подали что-то не то. Это заметно на скриншоте выше — часть заграждения отсутствует, потому что игра попыталась поставить туда что-то не то. Что именно — загадка, покрытая мраком, но я уверен, что рано или поздно мы узнаем и это.
В целом, ситуация действительно заметно улучшилась — KuruHS смог полноценно провести в игре порядка 20 часов без единого падения, что раньше было бы просто невозможно.
Весь фикс я решил оформить в виде asi скрипта, по принципу Widescreen патчей от ThirteenAG. Почитать исходники и скачать скрипты можно на гитхабе.
Спасибо за внимание!
Комментарии (38)
kinderandry
16.02.2018 18:14Замечал такое в некоторых пиратках — типа Black edition — в блок-посте не хватало машин. Получается, пираты решили проблемы разрабов?
GrimMaple Автор
16.02.2018 18:16Не факт. Мы с одним товарищем посчитали хэши файлов пиратки и идентичной лицухи. За исключением очевидно отличающегося exe файла там рядышком отличалась целая куча других файлов, в том числе машин, трасс и прочего. Пиратская версия явно отличается от того, во что играли люди с лицензией.
khim
16.02.2018 18:36Скорее всего утекла поздняя бета, а когда вышел полноценный релиз, то повторно ломать никто не захотел…
vient
16.02.2018 21:58Пробовали использовать инструменты типа frida для быстрой проверки, что логика патча верна?
GrimMaple Автор
16.02.2018 22:00Я не думаю, что такой инструмент в принципе существует. Слишком уж неоднозначно поведение всей программы после исправления пары инструкций (из нескольких миллионов). Перестанет падать в одном месте — может спокойно начать падать в другом, казалось бы, совершенно не связанном с патчем месте. Если есть — буду только рад узнать о нём :)
vient
17.02.2018 23:35Я неправильно выразился, имел в виду, что впатчивать ассемблерный код достаточно долго. Чтобы не потратить время на впиливание патча, который по какой-то причине окажется неправильным, можно сначала делать патчи, например, на Javascript, как во фриде, а потом уже удачные варианты оформлять в виде бинарного патча.
creker
16.02.2018 22:23+1Ах, 2005-ый, когда операции с памятью были достаточно дорогими, чтобы ей разбрасываться как попало…
Справедливости ради, актуальности это нисколько не потеряло и сегодня. Игры сплошь состоят из самописных аллокаторов. Иначе никак.AllexIn
17.02.2018 11:07+1ЭЭээ?
Последний раз в геймдеве видел кастомный аллокатор в 2006 году.
Сейчас, только для логирования выделения памяти и детекта утечек, в продакшн идёт стандартное всё.
Зачем в современном мире кастомный аллокатор?(Если мы не говорим о встраивании GC, как в UE)creker
17.02.2018 13:05Я по аналогии говорил про крупные проекты. CryEngine открыт, аллокаторов своих там полно. Вполне стандартные вариации для геймдева. Фростбайт свое имеет, разработчики killzone тоже немного делились. Да и вообще, по всем презентация по оптимизации видно, что ничего не изменилось. Зачем? Оптимизация конечно, работа с памятью для игр это один из главных источников тормозов сегодня. Потоков много, компонентов дофига, кэш промахи дешевле не стали. Недавно вон на С++ конференции было лекция про аллокаторы с кучей статистики об их производительности. Многие варианты в геймдеве тоже используют — фиксированный пул, бакеты, свои вариации кучи и т.д. Все зависит от специфики подсистемы.
Marui
17.02.2018 17:51+11) Консоли
2) placement_new/memory pool
3) алокации для мелких и больших объектов могут делаться поразному
Тоесть я согласен с тем, что его не пишут руками по 100 раз на проект. Окей, по 1 разу на платформу пишут. Раз в 7-10 лет.
crocodile2u
17.02.2018 10:12+2Заголовок прочитал — думал про NFS, а тут про NFS :-)
Но хорошая статья, спасибо!
perfect_genius
17.02.2018 17:10Планируете постепенно полностью отреверсить всю игру?
GrimMaple Автор
17.02.2018 17:52В идеале, я хотел бы сделать OpenNFS. Но для начала нужно хотя бы работу найти, а то кушать нечего :)
perfect_genius
17.02.2018 18:02А если для OpenNFS откроете страницу на Патреоне? Думаю, фанаты поддержали бы.
GrimMaple Автор
17.02.2018 18:12А я вот не думаю, что кто-то решит нечто подобное поддерживать.
Плюс мне кажется это не совсем честным, так как проект будет открытым и по логике вещей все должны будут получать что-то за работу. Хотя я мог бы попробовать сделать страницу на Патреоне, чтобы посмотреть, как далеко она уйдет. Я предсказываю, что недалеко с:perfect_genius
17.02.2018 18:51Вот сколько потенциальных фанатов:
Заголовок спойлераGrimMaple Автор
17.02.2018 18:56Нужно будет всё взвесить и продумать. В конце концов, попытаться всегда можно, а получится/не получится — это уже не так уж и важно.
Спасибо за идею :)khim
17.02.2018 19:09Идея хорошая, вот только… привлекать народ на Патреон — та ещё работёнка. Если просто страничку создать и никак её не раскручивать — привлечёте $10 в месяц. А если раскручивать — останется ли время на то, чтобы код писать?
perfect_genius
19.02.2018 10:19Т.е. создать темы в группах в ВК, FB, Reddit и ModDB будет маловато?
khim
19.02.2018 14:43Нужно регулярно поставлять какой-нибудь контент, который будет людей привлекать. Если вы книжку пишите или что-то подобное, то это легко… а вот если игру…
Хотя можно какие-то наработки выкладывать, полуфабрикаты…
AllexIn
17.02.2018 18:54Всё же в контент упирается. Чисто с технической точки зрения современные движки позволяют за месяц неспешной работы сделать аналогичный геймплей.
VEG
17.02.2018 19:27Уже существует OpenNFS — попытка сделать открытый движок для The Need For Speed. К сожалению, до финиша проект так и не доведён (например, вообще нет обработки коллизий).
Также можно вспомнить про Cry for Speed (и ещё вот) — попытка сделать ремейк Need For Speed III. К сожалению, проект совсем заглох, хотя и трудилось над ним несколько человек.GrimMaple Автор
17.02.2018 20:10У OpenAything помимо проблем с временем и мотивацией есть куча других проблем, например проблем с легальностью. И в случае с EA нет совершенно никакой возможности предсказать, как они отреагируют на появление такого проекта. Могут забить, могут просто пригрозить, а могут и в суд побежать. Поэтому я точно не стал бы делать нечто подобное, не получив от EA чёткий ответ: «Нам все равно, парень, делай на здоровье, только не распространяй наши ресурсы»
amarao
Круто. Патчить любой ценой.
А на месте EA я бы открыл сырцы. Ресурсы — как были, за деньги. А сырцы доступны.
GrimMaple Автор
EA не из тех контор, которые просто откроют исходники. Они очень переживают за свои IP, спасибо хотя бы на том, что не срубают на корню все моды, как некоторые другие конторы
на букву N.amarao
Кстати, если они откроют исходники под ограниченной лицензией, это всё равно будет лучше, чем просто закрытые.
GrimMaple Автор
Мне разрешили делать с их играми что угодно, покуда я не использую их IP. Я попробую написать им об открытии сырцов (или хотя бы узнать условия, на которых они готовы были бы их открыть)
amarao
Но вы используете их IP.
Возможно, речь идёт про «не используете их IP с коммерческими целями». Это совсем другое.
GrimMaple Автор
Вот так выглядел изначальный ответ. Я не силён во всех бюрократических аспектах, поэтому вы, скорее всего, правы. Но на практике им все равно, пока это не продаётся.
amarao
Прикрылись. Если буквально — то «возражают». Если по духу — «можно, пока не начинаете тырить в свою проект наш код».
Вероятнее всего, они имели в виду, что если вы будете кому-то давать код проекта (проект), то он не должен включать в себя саму игрушку.
VEG
А куда вы вообще пишете? Я вот Need For Speed III: Hot Pursuit долгое время занимался. Я пытался связаться с разработчиками на предмет может у кого завалялись ранние беты с лучшим качеством ресурсов или отладочная информация. Без результата. Те кто ответили сказали что ничего от тех времён (1998 год) не осталось =) Заполучить исходники было бы пределом мечтаний.
GrimMaple Автор
Я написал в Mass Media отдел, оттуда меня отфутболили в специальный отдел разрешений (Permission Requests Team).
С Most Wanted попроще, так как есть отладочная информация для PS2 демо-версии. Не идеал, но хоть что-то. Думаю, при желании NFSIII можно зареверсить и ручками, ибо там явно меньше кода. А можно и OpenNFSIII сделать ^_^
F376
https://github.com/electronicarts/EASTL/blob/master/include/EASTL/allocator_malloc.h
https://github.com/electronicarts/EASTL/blob/master/include/EASTL/allocator.h
GrimMaple Автор
Интересная находка, но не похоже на тот код, что есть в игре. Прошло уже слишком много времени, исходников конкретно этой игры может уже просто не быть.
boblenin
И на букву R*