- Python 99.4%
- Batchfile 0.6%
| .gitignore | ||
| build_luau.bat | ||
| cfg.py | ||
| check_syntax.py | ||
| decomp.py | ||
| disasm.py | ||
| ir.py | ||
| LICENSE | ||
| luau_bc.py | ||
| README.md | ||
| roundtrip.py | ||
| unluau.py | ||
unluau — декомпилятор Luau
Восстанавливает читаемый исходник Lua из скомпилированных Luau-скриптов космического сассаута Нужен только Python, внешних зависимостей нет.
Как декомпилировать
Всё дерево целиком:
python unluau.py -o out example
Один файл в консоль:
python unluau.py example/def/joints.lua
Ключи:
| Ключ | Что делает |
|---|---|
-o DIR |
куда писать результат (структура папок сохраняется) |
--no-banner |
не добавлять комментарий-шапку в начало файла |
--width N |
ширина, после которой конструктор таблицы разбивается на строки (по умолчанию 110) |
--disasm-on-error |
если файл не разобрался — положить дизассемблер вместо исходника |
-q |
без итоговой сводки |
Файлы, которые не являются байткодом (например scripts/lib/readme.txt),
копируются как есть.
Проверить, что всё получилось:
python check_syntax.py out # синтаксис всех файлов
python disasm.py example/def/joints.lua # дизассемблер, если нужно свериться
Как скомпилировать обратно
Нужен компилятор Luau. Собирается из репозитория Luau (нужны Visual Studio с C++ и CMake):
cmd /c build_luau.bat
Получится luau-repo\build\Release\luau-compile.exe. Компиляция файла:
luau-repo\build\Release\luau-compile.exe --binary -O1 -g0 out\def\joints.lua > joints.lua
Как получить именно ту версию байткода, что в игре
Это важный момент. По умолчанию компилятор выдаёт байткод версии 12, а игра
ждёт версию 6. Версию видно по первому байту: у оригиналов это 0x06.
Что проверено, а что нет: прогон по всем 422 файлам подтверждает, что команда ниже даёт заголовок v6 и не использует ничего из более новых версий. Загрузит ли игра такие файлы — проверить нельзя, её виртуалки тут нет.
Версия выбирается в BytecodeBuilder::getVersion() и зависит от двух вещей:
-
Фича-флаги поднимают версию выше базовой:
--fflags=LuauBytecodeCostModel=false,LuauEmitCallFeedback=falseБез флагов — v12, с этими двумя — v9. Ниже флагами не опустить.
-
Базовая версия — константа
LBC_VERSION_TARGETвluau-repo/Common/include/Luau/Bytecode.h. Флагами ниже неё не опуститься, поэтому её надо поставить в6и пересобрать:LBC_VERSION_TARGET = 6, // было 9Здесь это уже сделано,
luau-compile.exeсобран с этой правкой. Вернуть как было — поставить9и пересобрать.
Одного этого мало: компилятор пишет константы TABLE_WITH_CONSTANTS (появились
в v7) без оглядки на версию, и v6-виртуалка их не поймёт. Эти константы
появляются при оптимизации таблиц, поэтому компилировать надо с -O0.
Итоговая команда:
luau-compile.exe --binary -O0 -g0 ^
--fflags=LuauBytecodeCostModel=false,LuauEmitCallFeedback=false ^
out\def\joints.lua > joints.lua
Это проверено на всех 422 файлах: при -O0 в выходном байткоде встречаются
только константы NUMBER, STRING, TABLE (все существуют в v6), а самый
старший используемый опкод — FORGPREP (индекс 76), тогда как сама игра
использует опкоды вплоть до индекса 80. То есть за пределы v6 компилятор
не выходит.
Цена -O0 — код не оптимизируется, файлы получаются крупнее и работают
чуть медленнее оригинала. Для мода это обычно не важно, но если нужен -O1,
придётся дополнительно править BytecodeBuilder.cpp, чтобы он не использовал
TABLE_WITH_CONSTANTS.
Альтернатива без правки исходников — собрать старый релиз Luau, у которого
LBC_VERSION_TARGET изначально равен 6 (примерно линейка 0.5xx–0.6xx).
Проверить результат:
python -c "print(open('joints.lua','rb').read()[0])" # должно быть 6
Как это работает, вкратце
-
Разбор байткода — формат вычитан из
luau-repo/VM/src/lvmload.cpp: таблица строк, прототипы функций, константы, отладочная информация. -
Граф потока управления — код режется на базовые блоки по целям переходов.
-
Анализ живых значений — обратный поток данных по блокам. Регистр становится именованной переменной, только если его значение живо на выходе из блока; всё остальное подставляется прямо в выражение. Именно это отличает читаемый код от простыни присваиваний вида
v1 = x; v2 = v1. -
Символьное исполнение — по инструкциям строится дерево выражений: вызовы, индексация, арифметика, конструкторы таблиц, замыкания с захватом.
-
Восстановление структуры —
if/elseif/else,while,repeat, оба видаfor,break,continue, цепочкиand/or, ранниеreturn, рекурсивныеlocal function. -
Расстановка
local— для каждой переменной ищется самый глубокий блок, охватывающий все её использования. Это важно для замыканий в циклах: переменная должна объявляться внутри тела, иначе все итерации захватят одну ячейку.
Почему не сработали готовые открывашки
Публичные декомпиляторы Luau заточены под Roblox — это версии байткода 1–5,
причём обычно со скриптами, где сохранена отладочная информация. Здесь версия 6
и -g0, поэтому такие инструменты либо падают, либо выдают R1 = R2[R3].
Здесь формат прочитан по исходникам самой VM, а имена переменных не «угадываются», а честно генерируются — с анализом живых значений, чтобы временные значения не превращались в лишние переменные.
Известные ограничения
Имена локальных переменных выдуманы — v1, v2, arg1. В байткоде их нет,
скрипты собраны с -g0. Имена глобалов, полей, методов и все константы —
настоящие. Комментарии и разбиение на строки не восстановить.
Методы записаны через точку — function Foo.bar(self, x) вместо
function Foo:bar(x). Для Lua это одно и то же, но так гарантированно
не теряется параметр.
28 мест не удалось структурировать — помечены комментарием
-- unstructured branch at pc N. Там сложный поток управления, код рядом
присутствует, но условие перехода могло потеряться — стоит перепроверить руками.
Из них 21 в scripts/lib/mobdebug/mobdebug.lua,
2 в scripts/lib/std/io.lua, остальные 5 — по одному на файл.
Порядок ключей в таблицах может отличаться от исходного — в Lua это ни на что не влияет.
Файлы
| Файл | Назначение |
|---|---|
luau_bc.py |
чтение байткода: строки, прототипы, константы, отладочная информация |
cfg.py |
базовые блоки, чтение/запись регистров, анализ живых значений |
ir.py |
дерево выражений и операторов, генератор исходника Lua |
decomp.py |
символьное исполнение и восстановление структуры управления |
disasm.py |
дизассемблер (отладка и запасной вариант) |
unluau.py |
точка входа CLI |
check_syntax.py |
проверка синтаксиса результата |
roundtrip.py |
обратная компиляция и сверка с оригиналом |
build_luau.bat |
сборка luau-compile.exe из luau-repo |
build_luau.bat конфигурирует проект с включёнными тестами, но собирает только
компилятор — в luau-repo/CMakeLists.txt цель Luau.UnitTest упоминается в
блоке LUAU_BUILD_CLI, хотя создаётся только при LUAU_BUILD_TESTS, и без
этого конфигурация падает.