Русский
Русский
English
Статистика
Реклама

Обратная разработка

Перевод Как я сократил время загрузки GTA Online на 70

01.03.2021 16:18:41 | Автор: admin
GTA Online. Многопользовательская игра, печально известная медленной загрузкой. Недавно я вернулся, чтобы завершить несколько ограблений и был потрясён, что она загружается настолько же медленно, как и в день своего выпуска, 7 лет назад.

Пришло время докопаться до сути.

Разведка


Сначала я хотел проверить, вдруг кто-то уже решил проблему. Но нашёл только рассказы о великой сложности игры, из-за чего она так долго загружается, истории о том, что сетевая p2p-архитектура мусор (хотя это не так), некоторые сложные способы загрузки в сюжетный режим, а потом в одиночную сессию, и ещё пару модов, чтобы скипнуть видео с логотипом R* во время загрузки. Ещё немного почитав форумы, я узнал, что можно сэкономить колоссальные 10-30 секунд, если использовать все эти способы вместе!

Тем временем на моём компе

Бенчмарк


Story mode load time:  ~1m 10sOnline mode load time: ~6m flatStartup menu disabled, time from R* logo until in-game (social club login time isn't counted).Old but decent CPU:   AMD FX-8350Cheap-o SSD:          KINGSTON SA400S37120GWe have to have RAM:  2x Kingston 8192 MB (DDR3-1337) 99U5471Good-ish GPU:         NVIDIA GeForce GTX 1070

Знаю, что моё железо устарело, но чёрт возьми, что может замедлить загрузку в 6 раз в онлайн-режиме? Я не мог измерить разницу при загрузке из сюжетного режима в онлайн, как это делали другие. Даже если это сработает, разница небольшая.

Я (не) одинок


Если доверять этому опросу, проблема достаточно широко распространена, чтобы слегка раздражать более 80% игроков. Прошло уже семь лет!



Я немного поискал информацию о тех ~20% счастливчиках, которые загружаются быстрее трёх минут, и нашёл несколько бенчмарков с топовыми игровыми ПК и временем загрузки онлайн-режима около двух минут. Я бы кого-нибудь убил хакнул за такой комп! Действительно похоже на железячную проблему, но что-то не складывается

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

Высокоточные измерения


Вооружившись таким мощным инструментом, как Диспетчер задач, я приступил к поиску узкого места.



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

Использование диска? Нет! Использование сети? Есть немного, но через несколько секунд падает в основном до нуля (кроме загрузки вращающихся информационных баннеров). Использование GPU? Ноль. Память? Вообще ничего

Что это, майнинг биткоинов или что-то такое? Чую здесь код. Очень плохой код.

Единственный поток


На моём старом процессоре AMD восемь ядер, и он ещё молодцом, но это старая модель. Его сделали ещё тогда, когда производительность одного потока у AMD была намного ниже, чем у Intel. Наверное, это главная причина таких различий во времени загрузки.

Что странно, так это способ использования CPU. Я ожидал огромное количество операций чтения с диска или массу сетевых запросов, чтобы организовать сеансы в сети p2p. Но такое? Вероятно, здесь какая-то ошибка.

Профилирование


Профилировщик отличный способ найти узкие места в CPU. Есть только одна проблема большинство из них полагаются на инструментирование исходного кода, чтобы получить идеальную картину происходящего в процессе. А у меня нет исходного кода. Мне также не требуются идеальные показания в микросекундах, у меня узкое место на 4 минуты.

Итак, добро пожаловать в образцы стека (stack sampling). Для приложений с закрытым исходным кодом есть только такой вариант. Сбросьте стек запущенного процесса и местоположение указателя текущей инструкции, чтобы построить дерево вызовов в заданные интервалы. Затем наложите их и получите статистику о том, что происходит. Я знаю только один профилировщик, который может проделать это под Windows. И он не обновлялся уже более десяти лет. Это Люк Stackwalker! Кто-нибудь, пожалуйста, подарите Люку немножко любви :)



Обычно Люк группировал бы одинаковые функции, но у меня нет отладочных символов, поэтому пришлось смотреть на соседние адреса, чтобы искать общие места. И что же мы видим? Не одно, а целых два узких места!

Вниз по кроличьей норе


Позаимствовав у моего друга совершенно законную копию стандартного дизассемблера (нет, я действительно не могу его себе позволить когда-нибудь освою гидру), я пошёл разбирать GTA.



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

Похоже, здесь применили какую-то обфускацию/шифрование, заменив большинство инструкций тарабарщиной. Не волнуйтесь, нужно просто сбросить память игры, пока она выполняет ту часть, на которую мы хотим посмотреть. Инструкции должны быть деобфусцированы перед запуском тем или иным способом. У меня рядом лежал Process Dump, так что я взял его, но есть много других инструментов для подобных задач.

Проблема первая: это strlen?!


Дальнейший разбор дампа выявил один из адресов с некоей меткой strlen, которая откуда-то берётся! Спускаясь вниз по стеку вызовов, предыдущий адрес помечен как vscan_fn, и после этого метки заканчиваются, хотя я вполне уверен, что это sscanf.

Куда ж без графика

Он что-то парсит. Но что? Логический разбор займёт целую вечность, поэтому я решил сбросить некоторые образцы из запущенного процесса с помощью x64dbg. Через несколько шагов отладки выясняется, что это JSON! Он парсит JSON. Колоссальные десять мегабайт JSON'а с записями 63 тыс. предметов.

...,{    "key": "WP_WCT_TINT_21_t2_v9_n2",    "price": 45000,    "statName": "CHAR_KIT_FM_PURCHASE20",    "storageType": "BITFIELD",    "bitShift": 7,    "bitSize": 1,    "category": ["CATEGORY_WEAPON_MOD"]},...

Что это? Судя по некоторым ссылкам, это данные для сетевого торгового каталога. Предполагаю, он содержит список всех возможных предметов и обновлений, которые вы можете купить в GTA Online.

Проясним некоторую путаницу: я полагаю, что это предметы, приобретаемые за игровые деньги, не связанные напрямую с микротранзакциями.

10 мегабайт? В принципе, не так уж и много. Хотя sscanf используется не самым оптимальным образом, но, конечно, это не так уж плохо? Что ж



Да, такая процедура займёт некоторое время Честно говоря, я понятия не имел, что большинство реализаций sscanf вызывают strlen, поэтому не могу винить разработчика, который написал это. Я бы предположил, что он просто сканировал байт за байтом и мог остановиться на NULL.

Проблема вторая: давайте использовать хэш-массив?


Оказывается, второго преступника вызывают сразу за первым. Даже в одной и той же конструкции if, как видно из этой уродливой декомпиляции:



Все метки мои, и я понятия не имею, как на самом деле называются функции/параметры.

Вторая проблема? Сразу после разбора элемента он хранится в массиве (или встроенном списке C++? не уверен). Каждая запись выглядит примерно так:

struct {    uint64_t *hash;    item_t   *item;} entry;

А перед сохранением? Он проверяет весь массив, сравнивая хэш каждого элемента, есть он в списке или нет. С 63 тыс. записей это примерно (n^2+n)/2 = (63000^2+63000)/2 = 1984531500, если я не ошибаюсь в расчётах. И это в основном бесполезные проверки. У вас есть уникальные хэши, почему не использовать хэш-карту.



Во время реверс-инжиниринга я назвал его hashmap, но это явно не_hashmap. И дальше ещё интереснее. Этот хэш-массив-список пуст перед загрузкой JSON. И все элементы в JSON уникальны! Им даже не нужно проверять, есть они в списке или нет! У них даже есть функция прямой вставки элементов! Просто используйте её! Серьёзно, ну ребята, что за фигня!?

PoC


Всё это конечно классно, но никто не воспримет меня всерьёз, пока я не напишу реальный код для ускорения загрузки, чтобы сделать кликбейтный заголовок для поста.

План такой. 1. Написать .dll, 2. внедрить её в GTA, 3. зацепить некоторые функции, 4. ???, 5. профит. Всё предельно просто.

Проблема с JSON нетривиальная, я не могу реально заменить их парсер. Более реалистичным кажется заменить sscanf на тот, который не зависит от strlen. Но есть ещё более простой способ.

  • зацепить strlen
  • подождать длинной строки
  • закэшировать начало и длину
  • если поступит ещё вызов в пределах диапазона строки, вернуть закэшированное значение

Что-то вроде такого:

size_t strlen_cacher(char* str){  static char* start;  static char* end;  size_t len;  const size_t cap = 20000;  // if we have a "cached" string and current pointer is within it  if (start && str >= start && str <= end) {    // calculate the new strlen    len = end - str;    // if we're near the end, unload self    // we don't want to mess something else up    if (len < cap / 2)      MH_DisableHook((LPVOID)strlen_addr);    // super-fast return!    return len;  }  // count the actual length  // we need at least one measurement of the large JSON  // or normal strlen for other strings  len = builtin_strlen(str);  // if it was the really long string  // save it's start and end addresses  if (len > cap) {    start = str;    end = str + len;  }  // slow, boring return  return len;}


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

char __fastcall netcat_insert_dedupe_hooked(uint64_t catalog, uint64_t* key, uint64_t* item){  // didn't bother reversing the structure  uint64_t not_a_hashmap = catalog + 88;  // no idea what this does, but repeat what the original did  if (!(*(uint8_t(__fastcall**)(uint64_t*))(*item + 48))(item))    return 0;  // insert directly  netcat_insert_direct(not_a_hashmap, key, &item);  // remove hooks when the last item's hash is hit  // and unload the .dll, we are done here :)  if (*key == 0x7FFFD6BE) {    MH_DisableHook((LPVOID)netcat_insert_dedupe_addr);    unload();  }  return 1;}

Полный исходный код PoC здесь.

Результаты


Ну и как оно работает?

Original online mode load time:        ~6m flatTime with only duplication check patch: 4m 30sTime with only JSON parser patch:       2m 50sTime with both issues patched:          1m 50s(6*60 - (1*60+50)) / (6*60) = 69.4% load time improvement (nice!)

Да, чёрт возьми, получилось! :))

Скорее всего, это не решит всех проблем с загрузкой в разных системах могут быть и другие узкие места, но это такая зияющая дыра, что я понятия не имею, как R* пропустила её за все эти годы.

Краткое содержание


  • При запуске GTA Online есть узкое место, связанное с однопоточным вычислением
  • Оказалось, GTA изо всех сил пытается распарсить 1-мегабайтный файл JSON
  • Сам парсер JSON плохо сделан/наивен и
  • После парсинга происходит медленная процедура удаления дублей

R*, пожалуйста, исправьте


Если информация каким-то образом дойдёт до инженеров Rockstar, то проблему можно решить в течение нескольких часов силами одного разработчика. Пожалуйста, ребята, сделайте что-нибудь с этим :<

Вы можете либо перейти на хэш-карту для удаления дублей, либо полностью пропустить дедупликацию при запуске как быстрое исправление. Для парсера JSON просто замените библиотеку на более производительную. Не думаю, что есть более простой вариант.

ty <3
Подробнее..

Перевод Реверс-инжиниринг GPU Apple M1

05.03.2021 10:06:07 | Автор: admin
image

Новая линейка компьютеров Apple Mac содержит в себе разработанную самой компанией SOC (систему на чипе) под названием M1, имеющую специализированный GPU. Это создаёт проблему для тех, кто участвует в проекте Asahi Linux и хочет запускать на своих машинах Linux: у собственного GPU Apple нет ни открытой документации, ни драйверов в open source. Кто-то предполагает, что он может быть потомком GPU PowerVR, которые использовались в старых iPhone, другие думают, что GPU полностью создан с нуля. Но слухи и домыслы неинтересны, если мы можем сами заглянуть за кулисы!

Несколько недель назад я купила Mac Mini с GPU M1, чтобы изучить набор инструкций и поток команд, а также разобраться в архитектуре GPU на том уровне, который ранее не был публично доступен. В конечном итоге я хотела ускорить разработку драйвера Mesa для этого оборудования. Сегодня я достигла своего первого важного этапа: теперь я достаточно понимаю набор команд, чтобы можно было дизассемблировать простые шейдеры при помощи свободного и open-source тулчейна, выложенного на GitHub.

Процесс декодирования набора команд и потока команд GPU аналогичен тому процессу, который я использовала для реверс-инжиниринга GPU Mali в проекте Panfrost, изначально организованном проектами свободных драйверов Lima, Freedreno и Nouveau. Обычно для реверс-инжиниринга драйвера под Linux или Android пишется небольшая библиотека-обёртка, инъектируемая в тестовое приложение при помощи LD_PRELOAD, подключающей важные системные вызовы типа ioctl и mmap для анализа взаимодействия пользователя и ядра. После вызова передать буфер команд библиотека может выполнить дамп всей общей памяти (расширенной) для её анализа.

В целом тот же процесс подходит и для M1, но в нём есть особенности macOS, которые нужно учитывать. Во-первых, в macOS нет LD_PRELOAD; её аналогом является DYLD_INSERT_LIBRARIES, имеющая дополнительные функции защиты, которые для выполнения нашей задачи можно достаточно легко отключить. Во-вторых, хотя в macOS существуют стандартные системные вызовы Linux/BSD, они не используются для графических драйверов. Вместо них для драйверов ядра и пространства пользователя используется собственный фреймворк Apple IOKit, критической точкой входа которого является IOConnectCallMethod (аналог ioctl). Такие различия достаточно просто устранить, но они добавляют слой дистанцирования от стандартного инструментария Linux.

Более серьёзной проблемой является ориентирование в мире IOKit. Так как Linux имеет лицензию copyleft, (законные) драйверы ядра выложены в open source, то есть интерфейс ioctl открыт, хотя и зависит от производителя. Ядро macOS (XNU) имеет либеральную лицензию, не обладающую такими обязательствами; интерфейс ядра в нём проприетарен и не задокументирован. Даже после обёртывания IOConnectCallMethod пришлось попотеть, чтобы найти три важных вызова: выделение памяти, создание буфера команд и передача буфера команд. Обёртывание вызовов выделения памяти и создания буфера необходимы для отслеживаемой видимой GPU памяти (а именно это мне было интересно исследовать), а обёртывание вызова передачи необходимо для подбора времени дампа памяти.

Преодолев эти препятствия, мы можем наконец перейти к двоичным файлам шейдеров, которые сами по себе являются чёрными ящиками. Однако далее процесс становится стандартным: начинаем с самого простого фрагментного или вычислительного шейдера, вносим небольшое изменение во входящий исходный код и сравниваем получившиеся двоичные файлы. Многократное повторение этого процесса скучное занятие, но позволяет быстро выявить основные структуры, в том числе и опкоды.

Открытия, сделанные в задокументированном процессе изучения при помощи свободного дизассемблера ПО, подтверждают наличие у GPU следующих особенностей:

Во-первых, архитектура скалярна. В отличие от некоторых GPU, которые являются скалярными для 32 бит, но векторизованными для 16 бит, GPU процессора M1 скалярен при всех битовых размерах. Хотя на ресурсах по оптимизации Metal написано, что 16-битная арифметика должна быть значительно быстрее, кроме снижения использования регистров она ведёт к повышению количества потоков (занятости процессора). Исходя из этого, можно предположить, что оборудование является суперскалярным и в нём больше 16-битных, чем 32-битных АЛУ, что позволяет получать большее преимущество от графических шейдеров низкой точности, чем у чипов конкурентов, одновременно значительно снижая сложность работы компилятора.

Во-вторых, архитектура, похоже, выполняет планирование аппаратно это часто встречается среди десктопных GPU, но менее распространено во встроенных системах. Это тоже упрощает компилятор ценой большего количества оборудования. Похоже, команды минимально тратят лишние ресурсы при кодировании, в отличие от других архитектур, вынужденных перегружать команды nop для того, чтобы уместиться в сильно ограниченные наборы инструкций.

В-третьих, поддерживаются различные модификаторы. АЛУ с плавающей запятой могут выполнять модификаторы clamp (насыщенность), операций НЕ и абсолютных значений бесплатно, без лишних затрат распространённая особенность архитектуры шейдеров. Кроме того, большинство (или все?) команды позволяют бесплатно выполнять преобразование типов между 16-битными и 32-битными значениями и для адресата, и для источника, что позволяет компилятору более агрессивно использовать 16-битные операции, не рискуя тратить ресурсы на преобразования. Что касается целочисленных значений, то для некоторых команд есть различные бесплатные побитовые отрицания и сдвиги. Всё это не уникально для оборудования Apple, но заслуживает упоминания.

Наконец, не все команды АЛУ имеют одинаковые тайминги. Команды типа imad (используется для перемножения двух целых чисел и прибавления третьего) по возможности избегаются и вместо них используются многократные команды целочисленного сложения iadd. Это тоже позволяет предположить наличие суперскалярной архитектуры; оборудование с программным планированием, с которым я взаимодействую на своей повседневной работе, не может использовать различия в длине конвейеров, непреднамеренно замедляя простые команды для соответствия скорости сложных.

По своему предыдущему опыту работы с GPU я ожидала найти какой-нибудь ужас в наборе инструкций, повышающий сложность компилятора. Хотя написанное выше охватывает только небольшую площадь поверхности набора инструкций, пока всё кажется разумным и логичным. Отсутствуют сложные трюки для оптимизации, однако отказ от трюков позволяет создать простой, эффективный дизайн, выполняющий одну задачу, и справляющийся с ней хорошо. Возможно, проектировщики оборудования Apple поняли, что простота превыше всего.

Часть вторая


В первой части я начала изучать GPU Apple M1, надеясь разработать бесплатный драйвер с открытым исходным кодом. Теперь мне удалось достичь важного этапа: отрисовать треугольник с помощью собственного кода в open source. Вершинные и фрагментные шейдеры написаны вручную на машинном коде, а взаимодействие с оборудованием выполняется с помощью драйвера ядра IOKit, подобно тому, как это происходило в системном драйвере пользовательского пространства Metal.


Треугольник, отрендеренный на M1 кодом в open source

Основная часть нового кода отвечает за построение различных буферов команд и дескрипторов, находящихся в общей памяти и используемых для управления поведением GPU. Любое состояние, доступное из Metal, соответствует битам в этих буферах, поэтому следующей важной задачей будет их анализ. Пока меня больше интересует не содержимое, а связи между ними. В частности, структуры, содержащие указатели друг на друга, иногда встроенные на несколько слоёв вглубь. Процесс подготовки треугольника в моём проекте позволяет взглянуть с высоты птичьего полёта на то, как взаимодействуют все эти отдельные части в памяти.

Например, предоставляемые приложением данные вершин хранятся в собственных буферах. Внутренняя таблица в ещё одном буфере указывает на эти буферы вершин. Эта внутренняя таблица передаётся непосредственно на вход вершинного шейдера, заданного в другом буфере. На это описание вершинного шейдера, в том числе и на адрес кода в исполняемой памяти, указывает ещё один буфер, на который сам ссылается основной буфер команд, на который ссылается вызов IOKit на передачу буфера команд. Ого!

Другими словами, код демо пока не предназначен для демонстрации понимания мелких подробностей буферов команд, а скорее служит для демонстрации того, что ничто не упущено. Так как виртуальные адреса GPU меняются при каждом запуске, демо подтверждает, что идентифицированы все необходимые указатели и их можно свободно перемещать в памяти с помощью нашего собственного (тривиального) распределителя. Так как в macOS присутствует некоторая магия, связанная с распределением памяти и буферов команд, наличие этого работающего кода позволяет нам спокойно двигаться дальше.

Я использовала поэтапный процесс подготовки. Поскольку моя обёртка IOKit располагается в то же адресном пространстве, что и приложение Metal, обёртка способна модифицировать буферы команд непосредственно перед передачей в GPU. В качестве первого hello world я задала кодирование в памяти цвета очистки render target и показала, что могу изменять этот цвет. Аналогично, узнав о наборе инструкций для вывода дизассемблера, я заменила шейдеры написанными самостоятельно эквивалентами и убедилась, что могу исполнять код в GPU, доказать, что написала машинный код. Однако мы не обязаны останавливаться на этих нодах листьев системы; изменив код шейдера, я попыталась загрузить код шейдера в другую часть исполняемого буфера, модифицировав указатель командного буфера на код, чтобы компенсировать это. После этого я смогу попробовать самостоятельно загружать команды для шейдера. Проводя разработку таким образом, я смогу создать все необходимые структуры, тестируя каждую из них по отдельности.

Несмотря на затруднения, этот процесс оказался гораздо более успешным, чем попытка перейти непосредственно к построению буферов, например, при помощи воспроизведения. Я использовала этот альтернативный способ несколько лет назад для создания драйверов Mali, однако у него есть серьёзный недостаток чудовищно сложная отладка. Если в пяти сотнях строк магических чисел присутствует хотя бы одна опечатка, то никакой обратной связи, кроме ошибки GPU, вы не получите. Однако при последовательной работе можно выявлять причины ошибок и немедленно их исправлять, что оказывается быстрее и удобнее.

Однако затруднения всё-таки были! Моё временное ликование, вызванное возможностью изменения цветов очистки, пропало, когда я попыталась выделить буфер для цветов. Несмотря на то, что GPU кодировал те же биты, что и раньше, ему не удавалось корректно выполнить очистку. Думая, что где-то ошиблась в способе модификации указателя, я попыталась поместить цвет в неиспользованную часть памяти, уже созданную драйвером Metal, и это сработало. Содержимое осталось тем же, как и способ модификации указателей, но GPU почему-то не нравилось моё распределение памяти. Я думала, что как-то неправильно распределяю память, но использованные для вызова распределения памяти IOKit были побитово идентичны тем, что использовались Metal, что подтверждалось wrap. Моей последней отчаянной попыткой стала проверка, должна ли память GPU отображаться явным образом через какой-то побочный канал, например, через системный вызов mmap. У IOKit есть устройство-независимый вызов memory map, но никакие трассировки не позволили обнаружить свидетельств отображений через сторонние системные вызовы.

Появилась проблема. Утомившись от потраченного на устранение невозможного бага времени, я задалась вопросом, нет ли чего-то магического не в системном вызове, а в самой памяти GPU. Глупая теория, потому что если это так, то появляется серьёзная проблема курицы и яйца: если распределение памяти GPU должно быть одобрено другим распределением GPU, то кто одобряет первое распределение?

Чувствуя себя глупо и ощущая отчаяние, я решила протестировать теорию, вставив вызов распределения памяти посередине потока выполнения приложения, чтобы каждое последующее распределение находилось по другому адресу. Выполнив дамп памяти GPU до и после этого изменения и изучив их отличия, я обнаружила свой первый кошмар: наличие вспомогательного буфера в памяти GPU, отслеживающего все требуемые распределения. В частности, я заметила, что значения в этом буфере увеличиваются на единицу с предсказуемым смещением (через каждые 0x40 байт), и это намекает о содержании в буфере массива дескрипторов распределений. И в самом деле, эти значения точно соответствовали дескрипторам, возвращаемым из ядра при вызовах распределения памяти GPU.

Закрыв глаза на очевидные проблемы этой теории, я всё равно её протестировала, модифицировав эту таблицу и добавив в конец дескриптор моего нового распределения, а также изменив структуру данных заголовка так, чтобы увеличить количество элементов на единицу. Это не помогло. Несмотря на разочарование, это всё равно не позволяло полностью отказаться от теории. На самом деле, я заметила в элементах таблицы нечто любопытное: не все они соответствовали действительным дескрипторам. Действительными были все элементы, кроме последнего. Диспетчеры ядра имеют индексацию с 1, однако в каждом дампе памяти последний дескриптор всегда был 0, несуществующим. Вероятно, он используется как контрольное значение, аналогично NULL-terminated string в C. Однако при таком объяснении возникает вопрос: почему? Если заголовок уже содержит количество элементов, то контрольное значение избыточно.

Я продолжила разбираться дальше. Вместо добавления ещё одного элемента с моим дескриптором, я скопировал последний элемент n в дополнительный элемент n + 1 и переписала элемент n (теперь второй с конца) новым дескриптором.

Внезапно отобразился нужный мне цвет очистки.

Итак, загадка решена? Код заработал, так что в каком-то смысле да. Но едва ли это объяснение может нас удовлетворить; на каждом этапе непонятное решение будет создавать новые вопросы. Проблему курицы и яйца решить проще всего: эта таблица отображений вместе с корневым буфером команд распределяется специальным селектором IOKit, не зависящим от распределения общего буфера, а дескриптор таблицы отображений передаётся с селектором буфера команд. Более того, идея передачи требуемых дескрипторов вместе с передачей буфера команд не уникальна; подобный механизм используется в стандартных драйверах Linux. Тем не менее, обоснование для использования 64-байтных элементов таблицы в общей памяти вместо простого массива на стороне CPU остаётся совершенно непонятной.

Но даже оставив позади проблемы с распределением памяти, двигаться вперёд пришлось не без труда. Однако благодаря терпению мне удалось полностью воссоздать память GPU параллельно с Metal, используя проприетарное пространство пользователя только для инициализации устройства. Наконец, остался последний прыжок веры отказ от синхронизации с IOKit, после чего мне удалось получить свой первый треугольник.

Для второй части статьи пришлось внести в код изменения объёмом примерно 1700 строк кода, сам код выложен на GitHub. Я собрала простое демо, анимирующее на экране треугольник с помощью GPU. Интеграция с оконной системой на этом этапе практически отсутствует: требуется XQuartz, а в ПО с наивным скалярным кодом возникает detiling буфера кадров. Тем не менее, скорости CPU M1 с лихвой хватает, чтобы с этим справиться.

Теперь, когда готова начальная загрузка каждой части драйвера пространства пользователя, двигаясь дальше, мы сможем по отдельности работать над набором инструкций и буферами команд. Можно будет разобрать мелкие подробности и побитово преобразовать код из сотен необъяснимых магических констант в настоящий драйвер.
Подробнее..

Перевод Исправляем графический баг Mass Effect, возникающий на современных процессорах AMD

27.07.2020 10:17:57 | Автор: admin
image

Введение


Mass Effect популярная франшиза научно-популярных RPG. Первая часть сначала была выпущена BioWare в конце 2007 года эксклюзивно для Xbox 360 в рамках соглашения с Microsoft. Спустя несколько месяцев, в середине 2008 года, игра получила порт на PC, разработанный Demiurge Studios. Порт был достойным и не имел заметных недостатков, пока в 2011 году AMD не выпустила свои новые процессоры на архитектуре Bulldozer. При запуске игры на PC с современными процессорами AMD в двух локациях игры (Новерия и Илос) возникают серьёзные графические артефакты:


Да, выглядит некрасиво.

Хоть это и не делает игру неиграбельной, такие артефакты раздражают. К счастью, решение существует, например, можно отключить освещение консольными командами или модифицировать карты игры, удалив поломанные источники освещения, но, похоже, никто никогда так и не понял полностью причины этой проблемы. В некоторых источниках утверждается, что эту проблему позволяет устранить и мод FPS Counter, но мне не удалось найти информацию о нём: исходный код мода, похоже, не выложен онлайн, а документация о том, как мод исправляет ошибку, отсутствует.

Почему эта проблема так интересна? Баги, возникающие только на оборудовании отдельных производителей, встречаются довольно часто, и в играх они встречаются уже много десятилетий. Однако, по моей информации, это единственный случай, когда проблема с графикой вызвана процессором, а не графической картой. В большинстве случаев проблемы возникают у продуктов определённого производителя GPU и никак не касаются CPU, однако в данном случае всё совсем наоборот. Поэтому эта ошибка уникальна, а значит, её стоит исследовать.

Почитав онлайн-обсуждения, я пришёл к выводу, что проблема, похоже, касается чипов AMD FX и Ryzen. В отличие от более старых процессоров AMD, в этих чипах нет набора команд 3DNow!. Возможно, ошибка никак с этим не связана, но в целом у сообщества геймеров сложился консенсус о том, что это причина бага и что обнаружив процессор AMD, игра пытается использовать эти команды. Учитывая то, что случаи возникновения этого бага на процессорах Intel неизвестны, и что команды 3DNow! использовала только AMD, неудивительно, что сообщество посчитала причиной этот набор команд.

Но в них ли проблема, или ошибку вызывает нечто совершенно другое? Давайте выясним!

Часть 1 Исследования


Прелюдия


Хотя воссоздать эту проблему чрезвычайно просто, мне долгое время не удавалось оценить её по простой причине у меня не было под рукой PC с процессором AMD! К счастью, на этот раз я занимаюсь исследованиями не в одиночку Рафаэль Ривера поддержал меня в процессе изучения, предоставив тестовую среду с чипом AMD, а также поделился своими предположениями и мыслями, пока я делал слепые догадки, как это обычно бывает, когда я ищу источники таких неизвестных проблем.

Так как теперь у нас была тестовая среда, первой, разумеется, мы протестировали теорию cpuid если люди правы, предполагая, что следует винить команды 3DNow!, то в коде игры должно быть место, проверяющее их наличие, или хотя бы определяющее изготовителя CPU. Однако в таких рассуждений есть ошибка; если бы игра действительно пыталась использовать команды 3DNow! на любом чипе AMD без проверки возможности их поддержки, то она, скорее всего, вылетела бы при попытке выполнения недопустимой команды. Более того, краткое исследование кода игры показывает, что она не проверяет возможности CPU. Следовательно, что бы ни было причиной ошибки, не похоже, что она вызвана неправильным определением функцональности процессора, потому что игре она вообще не интересна.

Когда случай начал казаться не подлежащим отладке, Рафаэль сообщил мне о своём открытии отключение PSGP (Processor Specific Graphics Pipeline) устраняет проблему и все персонажи освещаются правильно! PSGP не самое подробно задокументированное понятие; если вкратце, то это legacy-функция (касающаяся только старых версий DirectX), позволяющая Direct3D выполнять оптимизации под конкретные процессоры:

В предыдущих версиях DirectX существовал путь выполнения кода под названием PSGP, позволявший производить обработку вершин. Приложения должны были учитывать наличие этого пути и поддерживать путь для обработки вершин ядрами процессора и графической карты.

При таком подходе логично, что отключение PSGP устраняет артефакты на AMD путь, выбиравшийся современными процессорами AMD был каким-то образом испорчен. Как его отключить? На ум приходят два способа:

  • Можно передать функции IDirect3D9::CreateDevice флаг D3DCREATE_DISABLE_PSGP_THREADING. Он описывается следующим образом:

    Ограничивает вычисления только основным потоком приложения. Если флаг не установлен, то среда выполнения может выполнять программную обработку вершин и другие вычисления в рабочем потоке (worker thread), чтобы повысить производительность в многопроцессорных системах.

    К сожалению, установка этого флага не решает проблему. Похоже, что несмотря на наличие в названии флага букв PSGP, это не то, что нам нужно.
  • DirectX задаёт два элемента регистра для отключения PSGP в D3D и для отключения PSGP только для D3DX DisablePSGP и DisableD3DXPSGP. Эти флаги можно устанавливать для всей системы или только для процесса. Подробности о его установке для конкретного процесса см. в руководстве Рафаэля Риверы по включению флагов Direct3D для отдельных приложений.

Похоже, что DisableD3DXPSGP способен решить эту проблему. Следовательно, если вы не любите скачивать сторонние исправления/модификации или хотите устранить проблему, не внося никаких изменений в игру, то это вполне рабочий способ. Если вы установите этот флаг только для Mass Effect, а не для всей системы, то всё будет в порядке!

PIX


Как обычно, при возникновении проблем с графикой их, скорее всего, поможет диагностировать PIX. Мы выполнили захват схожих сцен на оборудовании Intel и AMD, а затем сравнили результаты. Сразу же бросилось в глаза одно различие в отличие от моих предыдущих проектов, где захваты не записывали в себя баг и один и тот же захват мог выглядеть на разных PC по-разному (что указывает на баг драйвера или d3d9.dll), эти захваты записывали в себя баг! Другими словами, если открыть сделанный на железе AMD захват на PC с процессором Intel, то баг будет отображаться.

Захват с AMD на Intel выглядит точно так же, как выглядел на оборудовании, где был сделан:


О чём это нам говорит?

  • Так как PIX не делает скриншоты, а захватывает последовательности команд D3D и выполняет их в оборудовании, мы видим, что при выполнении на компьютере с Intel команд, захваченных в системе с AMD получается тот же баг.
  • Это определённо даёт нам понять, что разница вызвана не отличиями в том, как выполняются команды (а именно так и получаются специфичные для конкретных GPU баги), а в том, какие команды выполняются.

Другими словами, это почти точно не баг драйвера. Похоже, что входящие данные, подготавливаемые для GPU, каким-то образом искажаются 1. Это и в самом деле очень редкий случай!

На этом этапе для нахождения бага необходимо обнаружить все расхождения между захватами. Это скучная работа, но иного пути нет.

После долгого изучения захваченных данных моё внимание привлёк вызов отрисовки целого тела персонажа:


В захвате Intel этот вызов выводит бОльшую часть тела персонажа вместе с освещением и текстурами. В захвате AMD он выводит сплошную чёрную модель. Похоже, мы взяли нужный след.

Первым очевидным кандидатом на проверку будут соответствующие текстуры, но с ними, похоже, всё в порядке и в обоих захватах они одинаковы. Однако странно выглядят некоторые константы пиксельных шейдеров. В них не только содержатся NaN (Not a Number), но они также есть только в захвате AMD:


1.#QO обозначает NaN

Выглядит многообещающе часто бывает, что значения NaN вызывают странные графические артефакты. Довольно забавно, что в версии Mass Effect 2 для PlayStation 3 была очень похожая проблема в эмуляторе RPCS3, тоже связанная с NaN!

Однако не стоит пока слишком радоваться это могут быть значения, оставшиеся от предыдущих вызовов и не используемые в текущем. К счастью, в нашем случае чётко видно, что эти NaN передаются в D3D для этой конкретной отрисовки

49652IDirect3DDevice9::SetVertexShaderConstantF(230, 0x3017FC90, 4)49653IDirect3DDevice9::SetVertexShaderConstantF(234, 0x3017FCD0, 3)49654IDirect3DDevice9::SetPixelShaderConstantF(10, 0x3017F9D4, 1) // Submits constant c1049655IDirect3DDevice9::SetPixelShaderConstantF(11, 0x3017F9C4, 1) // Submits constant c1149656IDirect3DDevice9::SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID)49657IDirect3DDevice9::SetRenderState(D3DRS_CULLMODE, D3DCULL_CW)49658IDirect3DDevice9::SetRenderState(D3DRS_DEPTHBIAS, 0.000f)49659IDirect3DDevice9::SetRenderState(D3DRS_SLOPESCALEDEPTHBIAS, 0.000f)49660IDirect3DDevice9::TestCooperativeLevel()49661IDirect3DDevice9::SetIndices(0x296A5770)49662IDirect3DDevice9::DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 2225, 0, 3484) // Draws the character model

и используемый в этой отрисовке пиксельный шейдер ссылается на обе константы:

// Registers:////   Name                     Reg   Size//   ------------------------ ----- ----//   UpperSkyColor            c10      1//   LowerSkyColor            c11      1

Похоже, обе константы берутся напрямую из Unreal Engine и, судя по их названию, они могут влиять на освещение. Бинго!

Тест в игре подтверждает нашу теорию на машине с Intel вектор из четырёх значений NaN никогда не передаётся как константы пиксельного шейдера; однако на машине с AMD значения NaN начинают появляться сразу, как игрок входит в место, где ломается освещение!

Значит ли это, что работа сделана? Далеко нет, потому что обнаружение сломанных констант только половина успеха. По-прежнему остаётся вопрос откуда они берутся и можно ли их заменить? Во внутриигровом тесте замена значений NaN частично устранила проблему уродливые чёрные пятна пропали, но персонажи всё равно выглядят слишком тёмными:


Почти правильно но не совсем.

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

Увы, любые попытки отследить источники этих констант указывали на нечто, напоминающее поток рендера, а не истинное место передачи. Хоть отладка этого и возможна, очевидно, что нам нужно попробовать новый подход, прежде чем тратить потенциально бесконечное количество времени на отслеживание потоков данных между внутриигровыми и/или относящимися к UE3 структурами.

Часть 2 Приглядимся внимательнее к D3DX


Сделав шаг назад, мы поняли, что ранее кое-что упустили. Вспомним, что для исправления игры нужно добавить в реестр одну из двух записей DisablePSGP и DisableD3DXPSGP. Если предположить, что их названия говорят об их назначении, то DisableD3DXPSGP должен быть подмножеством DisablePSGP, причём первый отключает PSGP только в D3DX, а последний и в D3DX, и в D3D. Сделав такое предположение, обратим своё взгляд на D3DX.

Mass Effect импортирует набор функций D3DX, компонуя d3dx9_31.dll:

D3DXUVAtlasCreateD3DXMatrixInverseD3DXWeldVerticesD3DXSimplifyMeshD3DXDebugMuteD3DXCleanMeshD3DXDisassembleShaderD3DXCompileShaderD3DXAssembleShaderD3DXLoadSurfaceFromMemoryD3DXPreprocessShaderD3DXCreateMesh

Если бы я увидел этот список, не зная информации, полученной нами из захватов, то предположил бы, что вероятными виновниками могут быть D3DXPreprocessShader или D3DXCompileShader шейдеры могли быть неверно оптимизированы и/или скомпилированы на AMD, но их починка может быть безумно сложной задачей.

Однако у нас уже есть знания, поэтому для нас из списка выделяется одна функция D3DXMatrixInverse является единственной функцией, которую можно использовать для подготовки констант пиксельного шейдера.

Эта функция вызывается только из одного места в игре:

int __thiscall InvertMatrix(void *this, int a2){  D3DXMatrixInverse(a2, 0, this);  return a2;}

Однако оно реализовано не очень хорошо. Краткое изучение d3dx9_31.dll показывает, что D3DXMatrixInverse не трогает выходные параметры и в случае невозможности инверсии матрицы (потому что входящая матрица вырожденная) возвращает nullptr, однако игру это совершенно не волнует. Выходная матрица может остаться неинициализированной, ай-яй! На самом деле инвертирование вырожденных матриц происходит в игре (чаще всего в главном меню), но что бы мы ни делали для того, чтобы игра обрабатывала их лучше (например, обнуляли выходные данные или присваивали им единичную матрицу), графически ничего не менялось. Вот так дела.

Опровергнув эту теорию, мы вернулись к PSGP что же конкретно PSGP делает в D3DX? Рафаэль Ривера изучил этот вопрос, и логика этого конвейера оказалась довольно простой:

AddFunctions(x86)if(DisablePSGP || DisableD3DXPSGP) {  // All optimizations turned off} else {  if(IsProcessorFeaturePresent(PF_3DNOW_INSTRUCTIONS_AVAILABLE)) {    if((GetFeatureFlags() & MMX) && (GetFeatureFlags() & 3DNow!)) {      AddFunctions(amd_mmx_3dnow)      if(GetFeatureFlags() & Amd3DNowExtensions) {        AddFunctions(amd3dnow_amdmmx)      }    }    if(GetFeatureFlags() & SSE) {      AddFunctions(amdsse)    }  } else if(IsProcessorFeaturePresent(PF_XMMI64_INSTRUCTIONS_AVAILABLE /* SSE2 */)) {    AddFunctions(intelsse2)  } else if(IsProcessorFeaturePresent(PF_XMMI_INSTRUCTIONS_AVAILABLE /* SSE */)) {    AddFunctions(intelsse)  }}

Если PSGP не отключен, то D3DX выбирает функции, оптимизированные под использование конкретного набора команд. Это логично и возвращает нас к исходной теории. Как оказалось, в D3DX есть функции, оптимизированные под AMD и набор команд 3DNow!, поэтому игра, в конечном итоге, всё-таки косвенно их использует. Современные процессоры AMD, в которых отсутствуют команды 3DNow!, идут по тому же пути, что и процессоры Intel то есть, по intelsse2.

Подведём итог:

  • При отключении PSGP и Intel, и AMD проходят по обычному пути выполнения кода x86.
  • Процессоры Intel всегда проходят по пути кода intelsse22.
  • Процессоры AMD с поддержкой 3DNow! проходят по пути выполнения кода amd_mmx_3dnow или amd3dnow_amdmmx, а процессоры без 3DNow проходят по intelsse2.

Получив эту информацию, мы выдвинем гипотезу вероятно, что-то не так с командами AMD SSE2, и результаты инвертирования матрицы, вычисляемые на AMD по пути intelsse2, или слишком неточны, или полностью неверны.

Как нам проверить эту гипотезу? Тестами, естественно!

P.S.: Вы можете подумать в игре используется d3dx9_31.dll, но последняя библиотека D3DX9 имеет версию d3dx9_43.dll, и, скорее всего, эту ошибку устранили в более новых версиях?. Мы попробовали проапгрейдить игру, чтобы она компоновала самую новую DLL, но ничего не изменилось.

Часть 3 Независимые тесты


Мы подготовили простую независимую программу для проверки точности инвертирования матриц. На протяжении короткой игровой сессии в месте возникновения бага мы записали все входящие и выходящие данные D3DXMatrixInverse в файл. Этот файл считывается независимой тестовой программой, а результаты пересчитываются заново. Для проверки правильности выходные данные игры сравниваются с данными, вычисленными тестовой программой.

После нескольких попыток на основании данных, собранных с чипов Intel и AMD со включенным/отключенным PSGP мы сравнили результаты разных машин. Результаты показаны ниже с указанием успешных (, результаты равны) и ошибочных (, результаты не равны) прогонов. В последнем столбце указано, обрабатывает ли игра данные правильно, или глючит. Мы намеренно не учитываем неточность вычислений с плавающей запятой и сравниваем результаты при помощи memcmp:

Источник данных Intel SSE2 AMD SSE2 Intel x86 AMD x86 Приняты ли данные игрой?
Intel SSE2
AMD SSE2
Intel x86
AMD x86

Результаты тестов D3DXMatrixInverse

Любопытно, результаты демонстрируют, что:

  • Вычисления с SSE2 не переносятся между машинами с Intel и AMD.
  • Вычисления без SSE2 переносятся между машинами.
  • Вычисления без SSE2 принимаются игрой, несмотря на то, что отличаются от вычислений на Intel SSE2.

Поэтому встаёт вопрос: что же конкретно не так с вычислениями с AMD SSE2, из-за чего они приводят к глитчам в игре? У нас нет на него точного ответа, но похоже, что это результат двух факторов:

  • Реализация D3DXMatrixInverse на SSE2 может быть плохой численно похоже, некоторые команды SSE2 дают разные результаты на Intel/AMD (вероятно, из-за разных режимов округления), а функция написана так, что не может устранять эти неточности.
  • Код написан таким образом, что он слишком чувствителен к проблемам с неточностью.

На данном этапе мы уже готовы создать исправление, которое заменит D3DXMatrixInverse на переписанную x86-вариацию функции D3DX, и на этом закончить. Однако у меня возникла ещё одна случайная мысль D3DX устарел и был заменён на DirectXMath. Я решил, что если уж мы всё равно хотим заменить эту матричную функцию, то можно поменять её на XMMatrixInverse, которая является современной заменой функции D3DXMatrixInverse. В XMMatrixInverse тоже используются команды SSE2, то есть она будет такой же оптимальной, как и с функцией из D3DX, но я был почти уверен, что ошибки в ней будут такие же.

Я быстренько написал код, отправил его Рафаэлю, и

Он отлично заработал! (?)

В конечном итоге, то, что мы считали проблемой, возникающей из-за небольших отличий команд SSE2, может быть исключительно численной проблемой. Несмотря на то, что XMMatrixInverse тоже использует SSE2, она дала идеальные результаты и на Intel, и на AMD. Поэтому мы заново прогнали те же тесты и результаты оказались неожиданными, если не сказать больше:

Источник данных Intel AMD Приняты ли данные игрой?
Intel
AMD

Результаты тестов с XMMatrixInverse

Игра не только хорошо работает, но и результаты идеально совпадают и переносятся между машинами!

Учтя это, мы пересмотрели свою теорию о причинах бага без всяких сомнений, в нём виновата игра, слишком чувствительная к проблемам; однако после проведения дополнительных тестов нам показалось, что D3DX писался под быстрые вычисления, а DirectXMath больше волнует точность вычислений. Это выглядит логично, ведь D3DX продукт 2000-х и вполне разумно, что его основным приоритетом была скорость. DirectXMath был разработан позже, поэтому авторы могли уделить больше внимания точным, детерминированным вычислениям.

Часть 4 Соединяем всё вместе


Статья оказалась довольно длинной, надеюсь, вы не устали. Подведём итог тому, что мы сделали:

  • Мы убедились, что игра не использует команды 3DNow! напрямую (их используют только системные DLL).
  • Мы выяснили, что отключение PSGP устраняет проблему на процессорах AMD.
  • При помощи PIX мы нашли виновника значения NaN в константах пиксельного шейдера.
  • Мы нашли источник этих значений D3DXMatrixInverse.
  • Мы изучили эту функцию и выяснили, что она не даёт одинаковых результатов на процессорах Intel и AMD, когда используются команды SSE2.
  • Мы случайно обнаружили, что XMMatrixInverse не имеет этого недостатка и является вполне достойной заменой.

Единственное, что нам осталось реализовать правильную замену! Здесь на сцену выходит SilentPatch for Mass Effect. Мы решили, что самым чистым решением этой проблемы будет создание подменной d3dx9_31.dll, которая будет перенаправлять все экспортированные Mass Effect функции на системную DLL, за исключением функции D3DXMatrixInverse. Для этой функции мы разработали замену на основе XMMatrixInverse.

Заменная DLL обеспечивает очень чистую и надёжную установку, она отлично работает с версиями игры с Origin и Steam. Её можно использовать сразу, без необходимости ASI Loader или любого другого стороннего ПО.

Насколько мы понимаем, теперь игра выглядит так, как должна, без малейшего ухудшения освещения:

image

image

Новерия

image

image

Илос

Загрузки


Модификацию можно скачать в Mods & Patches. Нажмите сюда, чтобы сразу перейти к странице игры:

Скачать SilentPatch for Mass Effect

После скачивания достаточно извлечь архив в папку игры, и на этом всё! Если не знаете, что делать дальше, то прочитайте инструкции по настройке.



Полный исходный код мода опубликован на GitHub и его можно свободно использовать как отправную точку:

Исходники на GitHub

Примечания


  1. Теоретически, это также мог быть баг внутри d3d9.dll, что немного усложнило бы ситуацию. К счастью, это было не так.
  2. Разумеется, если предположить, что у них есть набор команд SSE2, но любой процессор Intel без этих команд намного слабее, чем минимальные системные требования Mass Effect.
Подробнее..

Перевод Хотите как в (средневековой) Европе? легализуем гей-браки в Crusader Kings III с помощью Ghidra

16.04.2021 10:20:09 | Автор: admin
image

Crusader Kings III отличная игра. Замечательна она не только своим официальным контентом, но и мощными инструментами моддинга. Ещё до её выпуска меня привлекли обещания разработчиков о расширении возможностей моддинга.

Хотя игра позволяет игроку реформировать средневековые культуры, привив им терпимость к однополым парам, в CK3 версии 1.3.1 пока нет возможности заключения однополых браков. Однако они должны быть приемлемы; ведь для этого и нужны моды!

Проблема моддинга


Довольно большой объём кода CK3 реализован на собственном скриптовом языке Paradox Interactive (он немного напоминает LISP без скобок). Также в игре есть удобные средства для создания, упаковки и распространения игроками собственных скриптов, например, через Мастерскую Steam.

Сделать так, чтобы два персонажа поженились (без каких-то сложных условностей), довольно просто. Достаточно такого скрипта:

do_ye_a_marriage = {    category = interaction_category_friendly    auto_accept = yes    is_shown = { always = yes }    on_auto_accept = {        scope:actor = {            marry = scope:recipient        }    }}

Он добавляет опцию в меню, открываемое при нажатии правой клавишей мыши на персонаже:


При её выборе герцогиня Тосканская без лишних вопросов выйдет за того персонажа, которым вы играете.


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

ничего не случится? Ну, по крайней мере, в игре. CK3 ведёт довольно подробные логи ошибок, которые в Windows обычно хранятся в папке C:\Users\<Username>\Documents\Paradox Interactive\Crusader Kings III\logs\error.log. В них мы увидим следующее:

[jomini_script_system.cpp:169]: Script system error!  Error: marry effect [ Svend Estrid of k_denmark (Internal ID: 15228 - Historical ID 101515) is not allowed to marry Erik Stenkiling of k_sweden (Internal ID: 17969 - Historical ID 100525) ]

К сожалению, оказалось, что логика женитьбы двух персонажей находится не в файлах скриптов, а в двоичном файле игры, а игровой движок в одностороннем порядке запрещает однополые браки. Из-за этого попытка поженить двух однополых персонажей Cruisader Kings 3 становится недопустимой операцией.

Эту особенность довольно быстро выявили моддеры после выпуска игры. Я создал тему, ставшую основным тредом на официальных форумах Paradox для просьб о внесении изменений. С течением времени патчи умудрялись только ухудшать ситуацию, и из сообщений на форумах и вне их стало понятно, что устранение проблемы будет сложным и не является основным приоритетом.

Моддеры игр, привыкшие самостоятельно браться за решение проблем с геймплеем, могут при наличии достаточного количества времени исправить что угодно. Вдохновившись недавним исправлением GTA Online, я решил потратить немного времени на поверхностный реверс-инжиниринг движка игры. Благодаря нему мне удалось создать неофициальный 390-байтный патч, обходящий встроенные запреты на однополые браки, а также реализующий другие эффекты, необходимые однополым партнёрам, чтобы брать приёмных детей. После публикации патча на официальных форумах последовала такая реакция:


Не совсем то, на что я надеялся Беспокоило также то, что в дополнение к бану и удалению треда с патчем удалялись любые посты с упоминанием патча, в том числе целые страницы из треда с просьбами игроков.

Я мог бы попробовать найти какой-нибудь другой сайт, который бы согласился выложить патч, но у этого решения были свои недостатки. Созданный мной патч подходил только для текущей Steam-версии CK3 в моей ОС и с моей архитектурой процессора. Но я могу дать вам нечто более универсальное: знания!

Итак, давайте без лишних предисловий изучим пошаговую инструкцию, позволяющую любому, обладающему начальными техническими знаниями, выполнить реверс-инжиниринг игры наподобие Crusader Kings 3.

(Если у вас не так много технических знаний или вы просто нетерпеливый моддер вроде меня, то можете сразу переходить к разделу Быстрый патч ближе к концу статьи)


Подготавливаем инструменты


Ghidra это пакет инструментов для реверс-инжиниринга, созданный Агентством национальной безопасности правительства США, чтобы взламывать китайцев. В 2019 году он был выложен в open source и с тех пор любопытные кодеры могут с его помощью изучать всевозможные программы.

Не буду писать руководство по установке, оно есть на официальном веб-сайте. Запустив программу и прощёлкав туториал, вы увидите такое окно:


Для начала нужно будет выполнить формальности создания нового проекта. Эта операция выполняется через меню File -> New Project.... Укажите название проекта и место его хранения, после чего можно будет импортировать файл игры.

Для этого нужно будет найти, где находится ck3.exe. Мой расположен в версии Steam, установленной в D:\SteamLibrary\steamapps\common\Crusader Kings III\binaries, но вам, возможно, придётся поискать его самостоятельно.

Найдя файл ck3.exe, можно импортировать его через интерфейс File -> Import File.... Это будет выглядеть примерно так:


После нажатия на OK и загрузки двоичного файла вы увидите отчёт, в котором можно просто нажать OK, и новый файл в панели Active Project. Дважды щёлкните на этом файле, чтобы начать веселье.

Интерфейс Ghidra поначалу пугает, но после освоения он кажется вполне простым. Помните, что если вы случайно выйдете из панели навигации, то её обычно можно вернуть через меню Windows в верхней панели.

При первом открытии двоичного файла вам предложат начать его анализ:


Ghidra может анализировать большой объём информации файла. Если вы пока не уверены, что ищете конкретно, то проще всего оставить стандартные настройки и нажать на Analyze. Для двоичного файла размера ck3.exe анализ может занять много времени. Для анализа своего файла я оставил программу работать на ночь. После завершения анализа можно нажать на кнопку сохранения в левом верхнем углу, чтобы анализ сохранился при повторном открытии файла.

Идём по следу


И здесь нам открывается целый мир. Иногда самое сложное в реверс-инжиниринге конкретной функции разобраться, с чего начинать. К счастью, у нас есть зацепка: то самое сообщение об ошибке, с которого всё началось. Часто используемые тексты обычно хранятся как последовательность в файле программы. Текст типа Svend Estrid of k_denmark, вероятно, генерируется динамически, однако текст is not allowed to marry, скорее всего, хранится в самом файле.

В панели меню выберите Search -> For Strings... и нажмите на Search, не меняя стандартных опций. Здесь вы сможете искать строку текста при помощи поля Filter. После инициализации программа будет (медленно) обыскивать файл игры в поисках строк, соответствующих фильтру.


Нам повезло! Дважды щёлкнув на эту строку, мы перейдём к строке текста в панели Listing (в другом окне; привыкайте к переключению окон).


Здесь отображается информация о строке текста и её расположении в памяти. Пока сама строка даёт нам не так много сведений. Нам больше интересен текст, использующий эту строку. Нажав правой клавишей мыши на выделенной строке, можно выбрать References -> Show References to Address, чтобы найти такие тексты.


Похоже, на строку ссылаются два участка памяти. Можно начать с первого. Дважды щёлкнем на первой строке, чтобы перейти к её местоположению. В основном окне местоположение при должно открыться в панелях Listing и Decompile (если у вас нет панели Decompile, то её можно открыть через меню Window -> Decompile).

Изучаем внутренности


На панель Decompile выводится много тарабарщины. Игры наподобие CK3 обычно пишутся на чём-то вроде C++, а затем разработчики обычно удаляют из файлов игры имена функций и переменных, чтобы уменьшить их размер. Чтобы компенсировать это, Ghidra даёт большинству функций и переменных произвольные имена.

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


Мы можем изучить вызываемую функцию, нажав на FUN_14034cb90 (спойлер: по большей части она просто выводит ошибку), но нам интереснее сделать так, чтобы она просто не работала. Поднявшись к началу блока if, в котором находится эта функция, мы видим следующее:


Итак, cVar2 является результатом функции FUN_140a1b9c0, и если этот результат равен нулю, срабатывает код обработки ошибок. Учитывая то, что ошибка связана с невозможностью женитьбы двух персонажей, вполне можно предположить, что lVar7 и lVar3 как раз и являются этими проверяемыми персонажами. Если мы дважды щёлкнем на FUN_140a1b9c0, то можем посмотреть, что у функции внутри.

И здесь нам очень поможет знание программирования на C. Мы ищем место, где сравниваются друг с другом входящие параметры и возвращается ноль. Ближе к началу функции я нашёл такой блок:


Он получает наши входящие данные (которые, как предполагается, представляют собой некие структуры персонажей) и сравнивает байт по смещению 0x124 в каждом из них. Если байты равны, то вся функция возвращает ноль. Похоже, что она может сравнивать пол персонажей, поскольку пол персонажа можно уместить в один байт (а от тех, кто играет с Cheat Engine я знаю, что это единственный бит, 0 мужчина, 1 женщина).

Начинается колдовство с битами


Лучше всего проверить нашу догадку о конструкции if, попробовав её обойти. При нажатии на if в панели Decompiler панель Listing перейдёт к соответствующей ассемблерной команде.


Это самый нижний уровень, на который мы можем опуститься при исследовании двоичного файла. JZ это сокращение от Jump if Zero. Команда берёт результат предыдущего сравнения (в данном случае это два пола) и переходит к новой позиции в памяти, если это сравнение приводит к нулю (что бывает, когда оба пола равны). Если мы проследуем по чёрной линии в левой части команд, то она приведёт нас к коду, который заставляет функцию возвращать ноль.

Один из способов удаления такой команды изменение места, в которое она осуществляет переход. Для этого нажмём правой кнопкой мыши на строке в панели Listing и выберем Patch Instruction.


Обратите внимание, что теперь место перехода выглядит очень похожим на место в памяти. Если мы хотим, чтобы наша команда JZ никогда не выполняла переход, то мы можем изменить её место перехода на адрес следующей строки, то есть 140a1ba00. В результате этого мы будем оказываться там вне зависимости от равенства значений полов персонажей.


Обратите внимание, что стрелка слева сменилась на новое место перехода. После того, как Ghidra завершит повторную декомпиляцию функции, отредактированной нами конструкции if там уже больше не будет.

Тестируем изменения


Все изменения мы пока выполняли только в проекте Ghidra. Если мы хотим протестировать изменения, нужно вернуть их в двоичный файл. Известно, что Ghidra имеет проблемы с экспортом файлов в используемом нами виде, поэтому придётся воспользоваться свободным скриптом, чтобы компенсировать это.

Официальные инструкции, написанные по ссылке, у меня не сработали, поэтому я поместил файл SavePatch.py в папку <папка установки Ghidra>\Ghidra\Features\Python\ghidra_scripts\. После этого нужно выделить перетаскиванием ассемблерной строки в панели Listing, чтобы вся она стала зелёной. А затем нажать на кнопку Script Manager рядом с верхней частью интерфейса:


Введите SavePatch в поле Filter, отобразится скачанный скрипт.


Дважды нажмите на найденную строку, после чего нужно будет указать путь к файлу ck3.exe, который мы изначально импортировали, и выбрать его (возможно, стоит сначала скопировать его как резервную копию). Если всё пройдёт правильно, то ваша игра на диске будет пропатчена модифицированной командой перехода.

Если мы снова запустим игру и попробуем применить к королю Швеции do_ye_a_marriage, то

всё равно ничего не получим. Но в логах ошибок появилось новое сообщение:

[pdx_assert.cpp:568]: Assertion failed: Can only marry characters of different gender.

Раунд второй


Похоже, у разработчиков Paradox есть какие-то дополнительные проверки для собственного удобства. Однако мы можем ещё раз воспользоваться той же методикой. Search -> For Strings..., фильтр по Can only marry characters of different gender, затем на найденной строке References -> Show References to Address.


На этот раз только одна находка. Теперь, когда мы представляем, что искать, можно с лёгкостью найти в этой функции соответствующий код.


Эта конструкция похожа на первую. Она сравнивает два параметра по смещению 0x124 и проверяет их равенство. После выбора if соответствующий ассемблерный код выглядит немного иначе:


Наша подсвеченная команда это JNZ, или команда Jump if Not Zero, а стрелка указывает на место после строки Can only marry. Поэтому нам нужно, чтобы эта команда всегда выполняла переход. Для этого снова нажмём правой клавишей мыши на выделенной строке и выберем Patch Instruction. Затем заменим JNZ на JMP, то есть на команду безусловного перехода.


Чтобы применить это изменение к игре, повторим тот же процесс: выделение, Script Manager, выбор SavePatch.py.

После этого можно запустить игру, и


наконец-то после всего пережитого добиться личной унии Дании и Швеции, о которой мы всегда мечтали.

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

Король и я, тоже король


Но что делают все эти изменения? В CK3 удалённые нами проверки дублируются в файлах скриптов, поэтому базовая игра не должна вообще видеть никаких изменений. Я уже довольно много играл с простыми модами, реализующими гей-браки, и пока самым заметным багом был такой:


Оказалось, что эффект marry в CK3 не имеет проверки на женитьбу на себе. Некоторые проверки, например set_father, явным образом препятствуют тому, чтобы персонаж становился сам себе отцом, но любой персонаж всегда имеет с самим собой один пол, поэтому запрет гей-браков служит и запретом на брак с собой. Вероятно, моддерам стоит ввести явную проверку этого в своих модах.


Впрочем, игра, несмотря на это, была довольно стабильной. Отдаю должное разработчикам CK3, движок получился очень надёжным.

(Если это случайно прочтёт кто-то из разработчиков CK3, то я бы рекомендовал всё-таки добавить специальную проверку женитьбы на самом себе.)

Быстрый патч


Но что если вы не хотите начинать карьеру в сфере реверс-инжиниринга ПО? Ну, если у вас Windows 10 и Steam-версия CK3 1.3.1, то вы можете открыть файл ck3.exe в любом шестнадцатеричном редакторе и внести следующие изменения:


Результат будет таким же, только меньше мороки. Однако при создании модов учитывайте проблему женитьбы на себе (впрочем, если вы нарцисс, то не берите в голову).
Подробнее..

Перевод Исправляем кривой запуск первого Mass Effect

23.11.2020 10:05:21 | Автор: admin
image

Часть 1


В последнее время я работал над собственным форком ME3Explorer [неофициальный редактор игр серии Mass Effect], содержащим множество важных улучшений и даже новые инструменты. Также я поработал над Mod Manager 5.1, который имеет удобные новые функции импорта сторонних модов, однако был отодвинут на второй план, пока я работал над новым фронтендом установщика ALOT.

ALOT Installer с манифестом 2017 года

Для его реализации я сотрудничал с CreeperLava и Aquadran; он должен упростить жизнь конечным пользователям, устанавливающим ALOT и его аддон (сторонние текстуры). Одна из моих проблем заключалась в том, что Origin не запускал игру после установки ALOT, если не запустить его с правами администратора. И поскольку запуск Origin при загрузке невозможно выполнить с правами админа, это очень раздражает. К тому же это влияет на мод MEUITM. Поэтому я начал разбираться, почему это происходит. Дело оказалось в идеальном сочетании реализации защиты, плохого кода и желания упростить жизнь других людей.

Давайте посмотрим, как работает Mass Effect с Origin в неизменённом состоянии под Windows 10.

  • Пользователь запускает MassEffect.exe. Файл немедленно запрашивает повышение прав до администраторских.
  • В конце образа MassEffect.exe есть код вызова Origin для запуска игры, это некая DRM. Он вызывает Origin, а затем выполняет выход.
  • Origin проверяет права пользователя на запуск игры, а затем запускает MassEffect.exe в соответствии с указаниями в реестре (не тот, который вы запустили сами), после чего пытается запустить исполняемый файл.
  • Origin не может запустить исполняемый файл, потому что он требует повышения прав. Чтобы DRM работала, она должна иметь возможность взаимодействия с процессом, поэтому она повышает права одного из внутренних сервисов, чтобы он мог общаться с игрой в целях DRM-защиты.
  • MassEffect.exe выполняется с правами администратора. Origin обменивается данными с MassEffect.exe и выполнение игры продолжается, как это было бы в случае с DVD-версией.

Всё это работает (через два UAC-запроса) на немодифицированной игре. Но если установить MEUITM или ALOT, то вы больше не сможете запускать игру через Origin как стандартный пользователь. Что за дела?

Сигнатуры файлов


И MEUITM, и ALOT модифицируют исполняемый файл MassEffect.exe, чтобы он мог использовать Large Address Aware. Это позволяет 32-битному процессу Mass Effect использовать до 4 ГБ ОЗУ вместо обычного 32-битного ограничения в 2 ГБ. При модификации флага LAA цифровая сигнатура MassEffect.exe оказывается поломанной сигнатура используется для проверки того, что файл не модифицирован. После модификации файла сигнатура становится неверной.

Origin при выполнении процесса с повышенными правами проверяет, подписан ли EXE компанией EA и правилен ли он. Если он не подписан EA, то он не повышает права модуля обмена данными DRM. Mass Effect загружается, а затем немедленно закрывается, потому что разблокировка DRM не работает, ведь со стороны Origin ей не с чем общаться, поскольку отказано в повышении прав.

То есть при модификации EXE Origin будет отказываться запускать исполняемый файл игры с повышенными правами. Но нам нужен LAA, поэтому необходимо как-то обойти эту проблему. Наша единственная надежда заключается в том, чтобы помешать MassEffect.exe запускаться с правами администратора. Сначала нужно разобраться, как задан его запуск с правами администратора.

Изучив манифест EXE, я увидел, что он запускается как инициатор вызова пользователь, запускающий EXE. Это означает, что этот исполняемый файл не должен требовать администраторских прав. Я проверил свои параметры совместимости, там тоже ничего не было. Каким-то образом права повышаются при запуске, но не через сам exe и не из-за моих настроек. В чём же хитрость? В Microsoft Windows Compatibility Database.

mirh (я встречался с ним в кругах любителей моддинга) провёл исследование того, почему Mass Effect вынужден запускаться с правами администратора. Он соответствует критериям базы данных в ней есть запись для Mass Effect, в которой указано, что нужно всегда принудительно запускать параметры совместимости. Это логично пользователь не должен конфигурировать параметры, если MS уже знает, какие из них работают (теоретически).


Как видите, для этой игры есть две записи MassEffect.exe (игра) и загрузчик (который, к сожалению, не включён в версию Origin). Для совместимости включается RunAsHighest (что означает права администратора). Критерии включения таковы:

  • EXE имеет название MassEffect.exe
  • Название компании в манифесте: BioWare
  • Название продукта в манифесте: Mass Effect
  • Версия продукта равна или меньше 1.2.0.0.

Эти критерии соответствуют всем известным версиям игры, в том числе, полагаю, и пиратским. Поэтому из-за совпадения всех этих критериев exe принудительно запускается с правами администратора. Это можно легко проверить, переименовав MassEffect.exe, после чего ему не потребуются администраторские права. (Однако Origin будет недоволен).

Исправление


Итак, теперь у нас есть понимание, как это исправить, но почему в базе данных есть эта запись? Поскольку Demiurge/Bioware не поддерживают идею Least User Access (LUA), Mass Effect при самом первом запуске требует прав администратора для выполнения записи в ключ реестра HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432NODE\AGEIA Technologies. Если этот ключ не существует, он пытается создать его без прав администратора у него нет для этого доступа, и игра просто вываливается. Похоже, в этом ключе содержится некая информация о том, что сейчас называется PhysX. Вероятно, запись в реестр мог внести и установщик игры, но разработчики реализовали это в самой игре.

Именно поэтому Microsoft вынуждает игру всегда запускаться с правами администратора, из-за этого единственного пункта. Это логично если заставить её запускаться под администратором, но пользователю не нужно будет беспокоиться о параметрах совместимости. Однако из-за этой комбинации трёх проблем (LAA портит сигнатуру, MS принуждает запускаться игру с правами администратора, Origin отказывается работать с процессами с повышенными правами, имеющими сломанную сигнатуру EA) Mass Effect не запускается с Origin и LAA.

Как же нам это исправить? Просто изменим в EXE название продукта с Mass Effect на Mass_Effect. Серьёзно, это всё. Проверка критериев не срабатывает, игре больше не нужны права администратора и Origin доволен (если не считать постоянного ворчания из-за обновлений). В MEUITM и ALOT Installer мы добавили код, создающий ключ реестра с правами записи для текущего пользователя, поэтому если Mass Effect нужно создать эти ключи (допустим, если его никогда не запускали), то игра будет довольна.

Часть 2


Mass Effect на PC: что ожидать от порта с консолей середины 2000-х


Если вы не знали, Mass Effect вышла на PC в 2008 году, она была портирована с Xbox 360 студией под названием Demiurge, которая также разработала Pinnacle Station для Mass Effect. Это очень посредственный порт, не особо хорошо переживший смену времён. Он приемлем как игра, но имел множество проблем даже на момент выхода. LOD частиц работали неправильно, LOD текстур считывались в обратном порядке, параметры ini случайным образом сбрасывались на значения по умолчанию проблем было довольно много. Но не было ничего, что бы полностью ломало игру.

Ну, или типа того. Была одна проблема, но вызванная не конкретно самой Mass Effect. Серьёзная проблема заключается в том, что Mass Effect требует для запуска прав администратора потому что Demiurge, похоже, считала, что все должны запускать игру как администратор это вполне могло быть приемлемым, если бы игра разрабатывалась во время, когда была только Windows XP, однако на момент выпуска игры уже больше года существовала Windows Vista. Но даже Windows XP имела концепцию LUA (Least User Access) с разделёнными аккаунтами пользователей. Подробнее об этом можно прочитать в первой части статьи.

Ух ты, PhysX, моя любимая библиотека физики!



Наверно, у меня небольшая неприязнь к этому SDK.

Mass Effect для PC работает на немного модифицированной версии Unreal Engine 3, который был выпущен примерно в конце 2006. По словам некоторых бывших разработчиков из BioWare, эта версия Unreal Engine тогда была немного сыроватой, если не сказать больше. Согласно рассказам этих разработчиков, было очень сложно работать с ней, потому что Epic Games сосредоточенно работала над Gears of War и не уделяла особо много времени своим партнёрам, тоже использующим движок.

Для расчёта физических взаимодействий Unreal Engine 3 использует PhysX, поэтому Epic Games создала dll, реализующую интерфейс между PhysX и форматами данных Unreal Engine через файл под названием PhysXLoader.dll, который загружает библиотеки PhysX с обеих сторон. PhysX это библиотека симуляции физики, приобретённая компанией AGEIA Technologies в середине 2000-х перед тем, как саму AGEIA в начале 2008 года купила Nvidia. Возможно, вы помните карты Physics Processing Unit (PPU) они использовали PhysX до того, как Nvidia похоронила эту идею.


PhysXLoader.dll, PhysXCore.dll и NxCooking.dll составляют библиотеки PhysX для Mass Effect.

Все три части Mass Effect используют PhysX, однако Mass Effect 2 и Mass Effect 3 используют установленную в систему PhysX, а Mass Effect локальную PhysX игры. Кроме того, в Mass Effect 2 и Mass Effect 3 применяется современная версия PhysX, а не устаревшая, которая была выпущена AGEIA. После приобретения Nvidia изменила некоторые пути внутри библиотеки, отделив устаревшие части от современных версий.

Но, похоже, это не мешает программе удаления старой PhysX удалять файлы/ключи реестра современной PhysX, поэтому в процессе тестирования моего исправления другие копии Mass Effect 2/3 не работали даже после установки современного дистрибутива PhysX. Очень бесит, что BioWare не смогла просто установить библиотеку на 8 МБ вместе с игрой в комплекте с игрой всё равно поставляется установщик PhysX, то есть это даже не экономило место!

Ну да ладно

Проблема PhysXLoader.dll компании Epic Games в том, что она может загружать PhysXCore.dll локально или из установленной в систему версии


Что? Как это может быть проблемой? Разве нельзя просто загружать локальную dll, и если она не существует, загружать системную? Почему это вообще проблема?


Вы не поверите, сколько фейспалмов я испытывал в процессе создания этого исправления.

При запуске Mass Effect записывает в реестр Windows HKEY_LOCAL_MACHINE два значения:

REG_BINARY HKLM\SOFTWARE\AGEIA Technologies enableLocalPhysXCore [mac-адрес, 6 байт]

REG_DWORD HKLM\SOFTWARE\AGEIA Technologies EpicLocalDllHack [1]

*Mass Effect это 32-битная программа, поэтому на 64-битной системе она выполняет запись в HKLM\SOFTWARE\WOW6432Node\AGEIA Technologies (на случай, если вы захотите проверить сами).

Запомните эти значения реестра, они будут важны в дальнейшем!

Именно из-за этих значений реестра Mass Effect требует административных прав. В первой части мы говорили о том, почему этих операций было достаточно, чтобы Microsoft внесла Mass Effect в базу данных совместимостей, заставляющую игру запускаться под администратором при совпадении определённых критериев исполняемого файла. Мы обошли эту проблему, изменив критерии так, чтобы они больше не совпадали.

Нам нужно изменить исполняемый файл, чтобы включить Large Address Aware, благодаря чему игра сможет загружать текстуры повышенного разрешения без переполнения памяти, поэтому нет никакого способа избежать порчи сигнатуры. Это, в свою очередь, привело к тому, что Origin больше не мог запускать игру, потому что он не может повышать права игры без правильной сигнатуры EA. Но если игра не имеет возможности записывать эти ключи реестра при запуске, то она может вылететь

Итак, это само по себе уже длинная цепь проблем, но мы обошли необходимость прав администратора в Mass Effect, просто дав аккаунту пользователя разрешение на этот конкретный ключ реестра AGEIA Technologies. Это позволит процессу игры записывать нужные ему значения. Я предполагал, что игра вылетает, потому что ей запрещался доступ для записи, а Demiurge не озаботилась написать try/catch вокруг кода записи в реестр.

Вероятно, не стоит называть значения реестра hack


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


Два значения реестра, записываемые Mass Effect.

Модератор PC Gaming Wiki под ником mirh долгие годы бил тревогу о том, что мы каким-то образом ломали другие игры в ALOT Installer, даже несмотря на то, что наше приложение никак не меняла способ записи Mass Effect этих значений, поэтом наше изменение никак не может сломать другие игры.

Спустя много месяцев он написал довольно подробное обоснование того, почему ALOT Installer (то есть на самом деле это была Mass Effect) ломает другие игры: находящийся в реестре enableLocalPhysXCore используется другими играми, работающими с PhysXLoader.dll. Когда я писал версию V4 установщика ALOT Installer, то сказал mirh, что серьёзнее рассмотрю его идею решения, не позволяющего ломать другие игры, хотя тогда я ещё не понимал, как ключ реестра с MAC-адресом системы может ломать другие игры и зачем вообще используется MAC-адрес.

Похоже, mirh был уверен, что эта enableLocalPhysXCore позволяет Mass Effect использовать PhysXCore.dll/NxCooking.dll в локальной папке, а не загружаться из установленного дистрибутива PhysX. Mass Effect не устанавливает дистрибутив PhysX, поэтому не может полагаться на её существование и вынуждена использовать локальные библиотеки.

Держитесь, теперь начинается нечто совершенно тупое:

MAC-адрес, сохраняемый в реестр файлом MassEffect.exe, считывается библиотекой PhysXLoader.dll и сравнивается с MAC-адресом вашей системы, чтобы определить, нужно ли загружать библиотеки PhysX из локальной папки или из системной.


Какой MAC-адрес?

\_()_/


Итак, Mass Effect работает следующим образом:

  1. В самом начале процесса загрузки MassEffect.exe MAC-адрес вашей системы считывается и записывается в реестр как enableLocalPhysXCore (вместе с EpicLocalDllHack)
  2. MassEffect.exe загружает PhysXLoader.dll
  3. PhysXLoader.dll считывает значение enableLocalPhysXCore и сравнивает с ним MAC-адрес вашей системы
  4. Если они совпадают, она использует PhysX из локальной папки, если нет, то версию дистрибутива PhysX из системы

Да, вы всё поняли правильно.

Оказалось, что другие игры, например, Mirrors Edge, имеют PhysXLoader.dll, которая тоже считывает эти значения (так как они основаны на одинаковом коде), но в этих играх нет локальных библиотек PhysX. Поэтому эти игры загружаются, видят enableLocalPhysXCore и пытаются загрузить локальную библиотеку, терпят неудачу и игра не запускается. Эту информацию я получил от mirh сам я не тестировал другие игры, поломанные этим значением реестра.

Обычно этого значения не существует, и игра должна использовать PhysX. Это поведение можно протестировать в Mass Effect, запретив доступ на запись к ключу реестра, удалив значения и установив старую версию PhysX она будет использовать системные библиотеки. Если системная PhysX не установлена, приложение не загрузится именно поэтому мы изначально разрешали записывать эти ключи Mass Effect, в противном случае бы казалось, что установщик портит Mass Effect, хотя на самом деле виновата ужасная реализация со стороны Epic Games.


Сложно представить сценарий, при котором это было бы хорошей идеей.

Если вы реализуете интерфейс с библиотекой, имеющей экспорт, который можно вызвать для инициализации/загрузки PhysX SDK, то разве нельзя просто передать ей булево значение, приказывающее ей загрузиться локально? Почему она вообще не начинает с локального поиска? И что за дела с MAC-адресом? Почему он находится в реестре, где ведёт себя как глобальный параметр???

Всё это выглядит как ужасные архитектурные решения, а после дизассемблирования PhysXLoader.dll кажется, что всё это и есть ужасные архитектурные решения. Давайте внимательнее присмотримся к Mass Effect и рассмотрим процесс её исправления от начала до конца.

Находим начальную точку


Предупреждение: я совершенный новичок в реверс-инжиниринге. Я создавал ассемблерные моды для игр Megaman Battle Network (и написал неплохое руководство по созданию хуков), проектировал моды на ActionScript2 P-Code и работал с байт-кодом UnrealScript, но никогда не углублялся в ассемблер x86. Я множество раз открывал IDA и могу находить нужные мне вещи, но никогда не понимал их. Уверен, что для более опытных реверс-инженеров этот процесс намного проще.


Сложно получать удовольствие от реверс-инжиниринга, если почти ничего не понимаешь в том, с чего начать. Это режим графа IDA, который очень помогает визуализировать ассемблер, но его всё равно очень сложно понять в большом двоичном файле на 20 МБ.

Недавно (пару лет назад), Агентство национальной безопасности США (АНБ) выпустило Ghidra бесплатный тулкит для реверс-инжиниринга с открытыми исходниками, который может отреверсировать ассемблерный код в довольно читаемый код на C; его бесконечно проще читать, чем ассемблерные графы IDA. И IDA, и Ghidra имеют свои сильные стороны: в IDA есть отладчик, позволяющий пошагово пройти по ассемблеру и посмотреть, какие пути кода будут выполняться, а также она может находить Unicode-строки (которые используются в Mass Effect ). Ghidra может рекомпилировать ассемблерный код из его декомпилированного кода на C (иногда), имеет преобразователь из ассемблера в C (простите, не знаю его названия), обладает открытыми исходниками и работает на куче платформ и со множеством двоичных форматов.


Ghidra это отличный инструмент для новичков в реверс-инжиниринге, поскольку она позволяет просматривать ассемблер как код на C, хоть и без имён переменных.

Итак, в начале я знал, что Mass Effect записывает enableLocalPhysXCore и EpicLocalDllHack. Давайте начнём с изучения MassEffect.exe, найдём эти строки и посмотрим, что на них ссылается. Открыв шестнадцатеричный редактор, я знал, что это unicode-строки, поэтому я буду искать их в IDA, потому что Ghidra, похоже, не поддерживает эту функцию.


Окно IDA Strings. Я наконец узнал, что эта полезная вкладка открывается по Shift + F12.

Поискав внутри окна IDA Strings строку enableLocalPhysXCore, я её нашёл. При двойном нажатии программа переносит нас к области данных исполняемого файла, в которой она задаётся:


На изображении вы видите, где задаются enableLocalPhysXCore, EpicLocalDLLHack и даже ключ реестра, все они находятся прямо рядом друг с другом.

Выше мы видим задание строк, которые, похоже, связаны с нашей целью. Над заданием текста мы видим DATA XREF, то есть что-то напрямую ссылается на эти данные вероятно, записывает их. Давайте дважды щёлкнем на XREF и посмотрим, куда это нас отправит.


Режим IDA View, а не Graph View.

Изучив это, мы видим, что здесь записывается RegSetValueExW. Я очень слабый разработчик на C, поэтому после гугления я понял, что это подготовка стека для вызова на C метода из Windows API, что можно увидеть по отображаемому IDA названию параметра, например, lpData и dwType. Мы знаем, что значению enableLocalPhysXCore присваивается MAC-адрес системы. Давайте посмотрим, где выполняется это присваивание. Чтобы выглядело логичнее, переключимся на режим графа.


В третьем блоке мы видим, что eax записывается в стек для lpData, а также записывается в стек для этого загадочного вызова sub_10929393. В этой подпроцедуре нет других вызовов с заданными названиями, поэтому вероятно именно там получается MAC. Давайте перейдём к ней.


Похоже, это какая-то подпроцедура-обёртка, или так это трактовала IDA, но она просто указывает на другую подпроцедуру. Давайте перейдём к ней.


Эта подпроцедура содержит названия, взятые из Windows API, и они показывают нам, что это как-то связано с сетью. Нас не волнует MAC-адрес, но давайте зададим название этой подпроцедуре. Назовём её GetMacAddress. Вернёмся к исходной подпроцедуре, которую изучали, и тоже переименуем её похоже, это что-то типа SetupPhysXSDKLoad, поэтому назовём её так.


Это относительно небольшая подпроцедура, всё, что она делает это производит запись двух вышеупомянутых значений реестра. Кроме ранних этапов загрузки двоичного файла, на эту подпроцедуру больше нет других ссылок, поэтому на этом этапе я вполне уверен, что исполняемый файл Mass Effect никогда не считывает эти значения, и пока я не знаю, что же делает EpicLocalDllHack.

Вскрываем PhysXLoader.dll


Теперь мы знаем, что исполняемый файл Mass Effect никогда не считывает этот ключ; значит, это делает одна из dll. Здесь я этого не показал, но в ProcMon (отличном инструменте для моддинга и подобных вещей в целом) я вижу, что значение реестра считывается непосредственно перед загрузкой библиотеки в процессе MassEffect.exe и перед загрузкой локальной dll. Я увидел, что после того, как запретил Mass Effect доступ на запись в эту папку, он считывает системную библиотеку, и игра не загружается, если не установлена системная версия старой PhysX.

Первой из dll загружается PhysXLoader, после которой загружается PhysXCore.dll, поэтому логично будет анализировать её. Давайте откроем её в IDA и посмотрим, где там используется enableLocalPhysXCore. Также я открою эту dll в Ghidra, чтобы лучше понимать, что происходит. Проделав ту же последовательность действий по поиску мест использования строки enableLocalPhysXCore, мы находим подпроцедуру:


Подпроцедуру читать не так уж сложно, особенно в режиме графа мы видим, что есть цикл, идущий из левого блока в блок над ним. Тем не менее, всё это не так просто читать для новичка, поэтому давайте посмотрим, как это выглядит в Ghidra. Я использую адрес этой подпроцедуры, чтобы перейти к ней в Ghidra (0x10001640).


Это даёт нам чуть больше понимания происходящего вызовов подпроцедуры, цикла, возвращаемого подпроцедурой типа. Мы можем находить элементы в одном из инструментов и помечать их в обоих, чтобы разобраться в том, что происходит. Мы сразу же видим, что есть вызов подпроцедуры, передающий путь к ключу реестра, название значения, а также две переменные. В цикле видим сравнение 6 элементов под индексу для определения их равенства.

Чтобы немного сократить пост, скажу, что эта подпроцедура поиска в реестре и в самом деле является простой обёрткой для получения информации из реестра. После реверс-инжиниринга (который в основном заключался в сопоставлении входящих данных подпроцедуры с тем, чем они являются в вызовах windows api) выяснилось, что её параметры являются подключом, именем значения, возвращаемым указателем на данные и возвращаемым размером данных.

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


Мы знаем, что Mass Effect записала в реестр 6-байтный mac-адрес, и что PhysXLoader.dll просто считала это значение из реестра, и что подпроцедура сравнивает что-то побайтно 6 раз. Логически мы можем предположить, что local_14 с показанного выше изображения это MAC-адрес. Зная это, мы также можем предположить, что FUN_10001580 получает MAC-адрес и задаёт его, поэтому мы переименуем ещё несколько элементов подпроцедуры.

Похоже, что вызов подпроцедуры не выполняет саму загрузку, он просто проверяет ключ и совпадение MAC-адреса. Зная название и информацию о действиях этого ключа, мы можем дать этой подпроцедуре обоснованное название ShouldUseLocalPhysX. Однако сравнение декомпиляции этой подпроцедуры в IDA и Ghidra приводит к немного различающимся результатам, и Ghidra, похоже, ошибается:


IDA показывает, что al присваивается 1 при нормальном выходе из цикла и 0 (xor al,al), если какие-то байты не совпадают. Ghidra этого не показывает, на самом деле она показывает, что возвращаемый тип равен void, что кажется ошибкой.

Немного погуглив информацию для этой части поста, я узнал, что EAX обычно используется как регистр возврата для x86, а регистр al это нижние 8 бит EAX. Я не имею достаточно опыта в Ghidra, чтобы знать, как сменить тип сигнатуры для этого вида возвращаемых нижних 8 битов; возможно, Ghidra пока этого не поддерживает, или я упустил какую-то настройку, которую нужно использовать.


Дизассемблированная подпроцедура, вызывающая ту, которая ищет enableLocalPhysXCore.

Однако если мы взглянем на ссылки на эту подпроцедуру (их две скорее всего, по одной на каждую библиотеку) в IDA и Ghidra, то увидим, что при вызове ShouldUseLocalPhysX она проверяет, не равен ли al нулю. Если он не равен нулю, то она загружает локальную PhysXCore.dll. Если равен, то она ищет библиотеку через системную установку PhysX, которая находится по ещё одному значению реестра в ключе AGEIA Technologies под названием PhysXCore Path. На самом деле это нам неинтересно, потому что мы хотим заставить PhysX всегда загружаться локально, вне зависимости от значения enableLocalPhysXCore.

Посмотрев на другую перекрёстную ссылку, можно и в самом деле увидеть, что она загружает библиотеку NxCooking, используя ShouldUseLocalPhysX таким же образом:


Вооружённые этим знанием, мы можем решить эту проблему несколькими способами. Я много занимался моддингом функций в байт-коде UnrealScript, который очень долгое время нельзя было увеличивать и уменьшать в размере, поэтому нахождение способов успешного или неуспешного прохождения логических проверок без изменения размера скрипта оказалось интересной задачей.

Например, если мне нужно было удалить проверку if, то мне приходилось находить способ изменить сравнения таким образом, чтобы они всегда были true или false. Один из способов возврата false проверкой if заключается в изменении ссылок на объект и токенов байт-кода сравнений, чтобы создать условный оператор вида if (objectA != objectA), всегда возвращающий false (если они не равны null). Мне нужно найти способ, чтобы в ShouldUseLocalPhysX всегда получался результат true.

Когда я писал таблицу символов для Megaman Battle Network 3, то научился всегда комментировать всё, что узнал об дизассемблированном коде. Я работал часами, совершенно забывая, что уже сделал, но мог вернуться к своим комментариям, и снова во всём разобраться.

Иногда мои символы/комментарии на отдельных строках, например, названия подпроцедур или таблицы указателей, встречались в других подпроцедурах, давая мне полезный контекст, которого бы я иначе не увидел. Ниже я прокомментировал и пометил несколько элементов, упрощающих чтение этой подпроцедуры.

Патчим худшую в мире проверку boolean



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

В x86 есть удобная однобайтная команда nop, которая в буквальном смысле не делает ничего, но занимает один байт. Также удобно то, что команда перехода в этот блок занимает 2 байта и состоит из 0x75 (jnz rel8) и 0x19 (относительного смещения).


[ЗАБАВНАЯ ИСТОРИЯ] Увидев это однобайтное смещение, я вспомнил времена, когда я работал над моддингом Megaman Battle Network. Тогда от команд перехода/ветвления зависела возможность моддинга отдельных частей ROM. При написании хука (перенаправляющего счётчик программы к вашему собственному коду) вам нужно найти команду перехода или ветвления, относительное смещение которой можно модифицировать так, чтобы оно указывало на ваш код. Затем нужно записать регистры в стек, запустить код, а затем вернуть стек обратно, чтобы подпроцедура выполняла выход правильным образом.

ARM (а конкретнее THUMB) имеет ограниченные команды ветвления, использующие в качестве относительных смещений разные размеры, которые не всегда могли перейти в любую точку ROM из-за своего местоположения в ROM. Так как игра была написана на ассемблере, находить свободное место временами было сложновато иногда приходилось соединять в цепочку несколько хуков, пока не удавалось переместить счётчик программы в свободную область, чтобы писать новый ассемблерный код. Этот jnz использует опкод 0x75, что даёт jnz rel8, то есть он может переходить только на расстояние до 128 байт (или, если переход возможен только вперёд, на 255?), что было бы настоящей проблемой, если бы я выполнял моддинг ассемблера так же, как мы работали раньше, когда не было мощных инструментов наподобие IDA и Ghidra. [КОНЕЦ ЗАБАВНОЙ ИСТОРИИ]

После замены nop-ами этого jnz наша подпроцедура ShouldUseLocalPhysX выглядит так:


Теперь в блок условия неравенства попасть нельзя. Проверка по-прежнему выполняется, но она никогда не возвращает false. Будет всегда использоваться локальное ядро PhysX.

Недостатки


Файл PhysXLoader.dll подписан Epic Games, поэтому это очевидно разрушает сигнатуру, ведь мы модифицировали файл. Игра не проверяет сигнатуры при загрузке, поэтому это не проблема. Некоторые антивирусы могут жаловаться на сломанные сигнатуры, но со временем обычно перестают. Кроме написания патча внутри памяти (как мы делаем это в загрузчике мода asi), нам нужно будет модифицировать двоичный файл библиотеки.

Получившееся поведение


Благодаря пропатченной dll игра работает как со значением реестра, так и без него, то есть Mass Effect для запуска больше не требуются права администратора. Дизассемблирование этого кода сопровождалось сильной руганью, потому что я не мог смириться с тупостью реализации этой проверки проверяется не только значение в реестре, но и MAC-адрес. В процессе отладки и пошагового выполнения команд я на самом деле сломал игру, потому что включил VPN и мой MAC-адрес сменился.

Этот процесс оказался хорошим опытом учёбы, я намного больше узнал о Ghidra и IDA, а также о других проблемах в PC-версии Mass Effect. Этот патч автоматически применяется в процессе установки ALOT Installer, поэтому пользователям не придётся беспокоиться о задании ключа enableLocalPhysXCore. Также мы модифицировали исполняемый файл Mass Effect для записи значения enableLocalPhysXCor_, чтобы наши пропатченные версии не записывали значение, портящее игры. Ванильные исполняемые файлы Mass Effect всё равно портят другие игры, но защита программ от криво написанных загрузчиков PhysX уже не входит в мои задачи.

О, а что насчёт EpicLocalDllHack? Ну, оно в буквальном смысле ничего не делает. Абсолютно бесполезное значение, оно никогда не считывается. Единственная возможная причина его существования, которую я могу придумать сохранение ключа реестра на случай деинсталляции дистрибутива PhysX, потому что оно не находится в списке значений дистрибутива PhysX, но это всё только догадки.

Разве добавление параметра PreferLocalSDK для PhysXLoader.dll это слишком сложно для Epic Games?
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru