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

Перевод Портируем Quake 3 на Rust



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

Тестирование на небольших программах командной строки постепенно устаревает, поэтому для переноса мы выбрали ничто иное, как Quake 3. Стоит ли говорить, что буквально через пару дней мы уже были первыми, кто играл в Rust-версию этого популярного шутера.

Подготовка: исходники Quake 3


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

Для начала мы убедились, что сможем воссоздать проект как есть:

$ make release

Сборка ioquake3 создает несколько библиотек и исполняемых файлов:

$ tree --prune -I missionpack -P "*.so|*x86_64". build     debug-linux-x86_64         baseq3            cgamex86_64.so          # клиент             qagamex86_64.so         # игровой сервер            uix86_64.so             # ui         ioq3ded.x86_64              # исполняемый файл выделенного сервера         ioquake3.x86_64             # основной исполняемый файл         renderer_opengl1_x86_64.so  # модуль рендеринга opengl1         renderer_opengl2_x86_64.so  # модуль рендеринга opengl2

Клиентскую, серверную и UI библиотеки можно собрать в виде Quake VM либо как нативные общие библиотеки x86. Мы предпочли второй вариант. Переносить VM на Rust и использовать версии QVM было бы существенно проще, но задачей было протестировать C2Rust максимально тщательно.

Сосредоточились мы на UI, игровом сервере, клиенте, модуле рендеринга OpenGL1 и основном исполняемом файле. Можно было также перевести модуль OpenGL2, но мы решили его не трогать, так как он активно использует файлы шейдеров .glsl, которые система сборки включает в исходники Си в виде строковых литералов. Конечно, можно было добавить поддержку скрипта кастомной сборки для встраивания кода GLSL в строки Rust после транспиляции, но нас остановило отсутствие надежного автоматического способа транспилировать эти автогенерируемые временные файлы.

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

Транспиляция


Чтобы сохранить используемую в Quake 3 структуру каталогов и не прибегать к изменению ее исходного кода, нужно было создать в точности такие же двоичные файлы, что и в нативной сборке, то есть четыре общие библиотеки и один двоичный файл. Поскольку C2Rust использует для сборки файлов Cargo, каждому исполняемому файлу требуется собственный контейнер Rust с соответствующим файлом Cargo.toml. Чтобы C2Rust на выходе создал для каждого исполняемого файла контейнер, ему нужно предоставить список двоичных файлов вместе с соответствующими им объектными или исходными файлами, а также вызов компоновщика, определяющего прочие детали, такие как зависимости.

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

В большинстве инструментов, создающих такую базу данных, подобное ограничение присутствует преднамеренно, например cmake с CMAKE_EXPORT_COMPILE_COMMANDS, bear и compiledb. Насколько нам известно, единственным инструментом, включающим команды компоновки, является build-logger из CodeChecker, который мы не задействовали только потому, что узнали о нем после написания собственных оберток (приводятся ниже). Это означало невозможность использовать файл compile_commands.json, создаваемый любым типовым инструментом для транспиляции мультибинарной программы Си.

Тогда мы решили написать собственные скрипты оберток, которые внесут в базу данных все необходимые вызовы компилятора и компоновщика, а затем преобразуют их в расширенный compile_commands.json. Вместо обычной сборки, запускаемой командой:

$ make release

Мы добавили обертки для перехвата процесса сборки:

$ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc

Обертки создают каталог с файлами JSON, по одному файлу за вызов. Второй скрипт агрегирует все эти файлы в новый compile_commands.json, который содержит уже и команды компиляции, и команды сборки. Мы расширили C2Rust для считывания компонующих команд из базы данных и создания отдельного контейнера для каждого связанного двоичного файла. Кроме того, теперь C2Rust считывает зависимости каждого исполняемого файла и автоматически добавляет их в файл build.rs его контейнера.

В качестве облегчения процесса все исполняемые файлы можно собрать за раз, если они будут находиться в одном рабочем пространстве. C2Rust производит высокоуровневый файл рабочего пространства Cargo.toml, позволяя собирать проект одной командой cargo build в каталоге quake3-rs:

$ tree -L 1. Cargo.lock Cargo.toml cgamex86_64 ioquake3 qagamex86_64 renderer_opengl1_x86_64 rust-toolchain uix86_64$ cargo build --release

Исправление недочетов


При первой попытке собрать переносимый код возникла пара проблем с исходниками Quake 3, которые C2Rust не мог корректно обработать или не обрабатывал совсем.

Указатели на массивы


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

int array[1024];int *p;// ...if (p >= &array[1024]) {   // error...}

Стандарт Си (загляните, например, в C11, Section 6.5.6) допускает указатели на элемент, выходящий за границу массива. Проблема же в том, что Rust это запрещает даже при получении только адреса элемента. Примеры этого шаблона мы нашли в функции AAS_TraceClientBBox.

Компилятор Rust также отметил похожий, но уже действительно ошибочный пример в G_TryPushingEntity, где условие представлено как >, а не >=. После этого условия выходящий за границы указатель разыменовывался, что являлось реальной ошибкой безопасности памяти.

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

Члены динамических массивов


На первый проверочный запуск игры Rust отреагировал паникой:

thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', quake3-client/src/cm_polylib.rs:973:17

Заглянув в cm_polylib.c, мы заметили разыменовывание поля p в следующей структуре:

typedef struct{intnumpoints;vec3_tp[4];// переменный размер} winding_t;

Поле p здесь это более ранняя несовместимая с C99 версия члена массива переменной длины, которая до сих пор принимается gcc. C2Rust распознает членов динамических массивов с синтаксисом С99 (vec3_t p[]) и реализует простую эвристику для попутного обнаружения более ранних версий этого шаблона (массивов с размером 0 и 1 в конце структур; в исходниках ioquake3 мы нашли несколько таких).

Панику удалось устранить, изменив вышеприведенную структуру на синтаксис C99:

typedef struct{intnumpoints;vec3_tp[];// переменный размер} winding_t;

Реализация автоматического исправления этого шаблона для общих случаев (массивы с размером не 0 или 1) оказалась бы слишком сложной, поскольку пришлось бы различать членов стандартных массивов от членов массивов с произвольными размерами. Вместо этого мы рекомендуем корректировать исходный код Си вручную как сделали сами в случае с ioquake3.

Связанные операнды во встроенном ассемблере


Еще одним источником сбоев был встроенный в Си код ассемблера из системного заголовка /usr/include/bits/select.h:

# define __FD_ZERO(fdsp)                                            \  do {                                                              \    int __d0, __d1;                                                 \    __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS               \                          : "=c" (__d0), "=D" (__d1)                \                          : "a" (0), "0" (sizeof (fd_set)           \                                          / sizeof (__fd_mask)),    \                            "1" (&__FDS_BITS (fdsp)[0])             \                          : "memory");                              \  } while (0)

Он определяет внутреннюю версию макроса __FD_ZERO. Это определение вызывает редкий граничный случай встроенного ассемблерного кода gcc: связанные входные/выходные операнды разного размера.

Выходной операнд =D (_d1) привязывает регистр edi к переменной _d1 в качестве 32-битного значения, в то время как 1 (&__FDS_BITS (fdsp)[0]) привязывает тот же регистр к адресу fdsp->fds_bits в качестве 64-битного указателя. gcc и clang исправляют это несоответствие, взамен используя 64-битный регистр rdi и затем усекая его значение перед присваиванием к _d1. Rust же по умолчанию использует семантику LLVM, которая оставляет этот случай неопределенным. В отладочных сборках (не релизных, которые работали корректно) оба операнда присваивались регистру edi, обуславливая преждевременное усечение указателя до 32 бит еще до достижения встроенного кода ассемблера, что и вызывало сбои.

Поскольку rustc передает встроенный ассемблерный код Rust в LLVM с минимальными изменениями, мы решили исправить этот частный случай в C2Rust. Для этого мы реализовали новый контейнер c2rust-asm-casts, корректирующий проблему через систему типов Rust с помощью типажа (trait) и вспомогательных функций, которые автоматически расширяют и усекают значения связанных операндов до внутреннего размера, достаточного для хранения обоих операндов.

Вышеприведенный код корректно транспилируется в следующее:

let mut __d0: c_int = 0;let mut __d1: c_int = 0;// Ссылка на выходное значение первого операндаlet fresh5 = &mut __d0;// Внутреннее хранилище для первого связанного операндаlet fresh6;// Ссылка на выходное значение второго операндаlet fresh7 = &mut __d1;// Внутреннее хранилище для второго операндаlet fresh8;// Входное значение первого операндаlet fresh9 = (::std::mem::size_of::<fd_set>() as c_ulong).wrapping_div(::std::mem::size_of::<__fd_mask>() as c_ulong);// Входное значение второго операндаlet fresh10 = &mut *fdset.__fds_bits.as_mut_ptr().offset(0) as *mut __fd_mask;asm!("cld; rep; stosq"     : "={cx}" (fresh6), "={di}" (fresh8)     : "{ax}" (0),       // Приведение входных операндов к внутреннему типу хранилища       // с дополнительным нулевым или знаковым расширением       "0" (AsmCast::cast_in(fresh5, fresh9)),       "1" (AsmCast::cast_in(fresh7, fresh10))     : "memory"     : "volatile");// Приведение операндов к внешнему типу (с выведением типов) и усечениеAsmCast::cast_out(fresh5, fresh9, fresh6);AsmCast::cast_out(fresh7, fresh10, fresh8);

Обратите внимание, что код выше не требует типов для любых входных или выходных значений инструкций ассемблера, полагаясь на вывод типов Rust (главным образом типов fresh6 и fresh8).

Выравнивание глобальных переменных


Последним источником сбоев была следующая глобальная переменная, где хранится константа SSE:
static unsigned char ssemask[16] __attribute__((aligned(16))) ={"\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00"};

На данный момент Rust поддерживает атрибут выравнивания для типов структур, но не глобальных переменных, то есть элементов static. Мы продолжаем искать универсальный способ решения этой проблемы в Rust или C2Rust, но для ioquake3 пока что решили ее вручную с помощью небольшого патча. Этот патч заменяет Rust-эквивалент ssemask на:

#[repr(C, align(16))]struct SseMask([u8; 16]);static mut ssemask: SseMask = SseMask([    255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0,]);

Запуск quake3-rs


Выполнение cargo build --release генерирует двоичные файлы, но все они генерируются в target/release с использованием структуры каталогов, не распознаваемой бинарником ioquake3. Мы написали скрипт, который создает символические ссылки на текущую директорию, чтобы дублировать верную структуру каталогов (включая ссылки на файлы .pk3, содержащие ресурсы игры):

$ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks

Путь /path/to/paks должен указывать на каталог, содержащий файлы .pk3.

Ну а теперь пора запускать игру! При запуске нужно передать команду +set vm_game 0 и пр., чтобы загрузить эти модули как общие библиотеки Rust, а не ассемблерный код QVM, а также команду cl_renderer, чтобы использовать OpenGL1.

$ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1"

Иии



Перед нами рабочая Rust-версия Quake3!



Вот видео, в котором мы транспилируем игру, загружаем ее и недолго тестируем игровой процесс:


Транспилированный исходный код лежит в ветке transpiled нашего репозитория. Там также есть ветка refactored, содержащая те же файлы, но уже с примененными командами рефакторинга.

Инструкции по транспиляции


Если вы захотите повторить аналогичный процесс переноса и запуска Quake 3, то имейте в виду, что вам понадобятся либо оригинальные ресурсы игры, либо скачанные из интернета демоверсии таких ресурсов. Помимо этого, потребуется установить C2Rust (минимальная необходимая ночная версия Rust на момент написания это nightly-2019-12-05, но мы рекомендуем заглянуть в репозиторий C2Rust или на сайт crates.io на предмет наличия последней):

$ cargo +nightly-2019-12-05 install c2rust

а также копии репозиториев C2Rust и ioquake3:

$ git clone git@github.com:immunant/c2rust.git$ git clone git@github.com:immunant/ioq3.git

В качестве альтернативы установке c2rust вышеприведенной командой вы можете собрать C2Rust вручную с помощью cargo build --release. В обоих случаях вам все равно потребуется репозиторий C2Rust, так как там находятся скрипты оберток компилятора, необходимые для транспиляции ioquake3.

Мы предоставляем скрипт, который автоматически транспилирует код Си и применяет патч ssemask. Чтобы его использовать, выполните из верхнего уровня репозитория ioq3следующую команду:

$ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary>

Эта команда должна создать подкаталог quake3-rs, содержащий код Rust, где можно будет последовательно выполнять cargo build --release и остальные ранее описанные шаги.


Источник: habr.com
К списку статей
Опубликовано: 14.06.2021 20:18:14
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Ненормальное программирование

Разработка игр

Rust

Ruvds_перевод

Quake 3

Игры

Портирование

Категории

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

  • Имя: Макс
    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