docs(script): describe native condition evaluation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
This commit is contained in:
3 files changed
+142
-24
No files matched your search
@@ -31,9 +31,25 @@ primitives, sound events и reads в `Effect.dll`.
|
|||||||
checks. Полная instruction grammar `.scr`, semantics opcodes и serialization
|
checks. Полная instruction grammar `.scr`, semantics opcodes и serialization
|
||||||
state ещё не восстановлены.
|
state ещё не восстановлены.
|
||||||
|
|
||||||
|
Для GOG `ai.dll` статически восстановлены пять ветвей evaluator-а 16-byte
|
||||||
|
записей условий по RVA `0x5180`, включая проверки типов, поиск объектов,
|
||||||
|
короткие выходы из обхода списков и запись байта завершения. Однако не найден
|
||||||
|
код-производитель этого массива и контракт его владельца с runtime. Reader
|
||||||
|
`FUN_10011b20` хранит описатели событий и вложенных записей `.scr` во внутренних
|
||||||
|
контейнерах с шагом `0x14` и `0x20`; сами дисковые структуры имеют переменную
|
||||||
|
длину из-за length-prefixed имён и списков ссылок. Связь этих контейнеров с
|
||||||
|
массивом evaluator-а не доказана. Виртуальные методы,
|
||||||
|
через которые проходят проверки, остаются внешней границей, а сам evaluator
|
||||||
|
не сравнивался с работающим оригиналом. Описание этих ветвей — статический
|
||||||
|
вывод по одному известному `ai.dll`, а не утверждение о совпадении во время
|
||||||
|
исполнения.
|
||||||
|
|
||||||
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
|
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
|
||||||
table с instruction sizes, построить disassembler и сравнить выполнение
|
table с instruction sizes, построить disassembler и сравнить выполнение
|
||||||
коротких scripts с оригиналом.
|
коротких scripts с оригиналом. Для evaluator-а условий отдельно проследить
|
||||||
|
создание записей `this + 0x34`, выполнить те же входные данные в оригинале и
|
||||||
|
сверить байт завершения, порядок поиска, ранние выходы и граничный случай
|
||||||
|
`payload == count`.
|
||||||
|
|
||||||
## Saves and campaign state
|
## Saves and campaign state
|
||||||
|
|
||||||
|
|||||||
@@ -42,12 +42,12 @@ budget. Опасное удаление идёт через World3D queue и о
|
|||||||
|
|
||||||
До восстановления opcode table package mode читает header/strings/symbols/
|
До восстановления opcode table package mode читает header/strings/symbols/
|
||||||
event offsets/raw bytecode losslessly. Статический анализ уже выделил отдельный
|
event offsets/raw bytecode losslessly. Статический анализ уже выделил отдельный
|
||||||
five-way evaluator condition records (`ai.dll` VA `0x10005180`): tags `1..5`,
|
evaluator записей условий (`ai.dll` RVA `0x5180`). Его подтверждённые ветви и
|
||||||
type guards, object lookup и completion flag. Это не следует выдавать за
|
точные границы описаны ниже; он не является dispatcher-ом инструкций или jump
|
||||||
instruction dispatcher или jump table `.scr`: bytecode opcode table всё ещё
|
table `.scr`. Bytecode opcode table всё ещё требует отдельного доказательства.
|
||||||
требует отдельного доказательства. Unknown opcode нельзя пропустить как один
|
Unknown opcode нельзя пропустить как один byte: это ломает синхронизацию. Для
|
||||||
byte: это ломает синхронизацию. Для каждого доказанного opcode фиксируются
|
каждого доказанного opcode фиксируются number, size, operands, control flow,
|
||||||
number, size, operands, control flow, effects, errors и минимальный test.
|
effects, errors и минимальный test.
|
||||||
|
|
||||||
GOG `ai.dll` доказывает этот framing двумя consumer-ами: loader по
|
GOG `ai.dll` доказывает этот framing двумя consumer-ами: loader по
|
||||||
`0x10001000` открывает `<bundle>.scr`, `varset.var`, `<bundle>.fml`, затем
|
`0x10001000` открывает `<bundle>.scr`, `varset.var`, `<bundle>.fml`, затем
|
||||||
@@ -65,6 +65,80 @@ sentinel records. Поэтому `ScriptInstruction::dispatch_selector()` воз
|
|||||||
(`Handler(0)`, VA `0x10008034`) только устанавливает current context и flag
|
(`Handler(0)`, VA `0x10008034`) только устанавливает current context и flag
|
||||||
`+0x50 = 1`; это не даёт ему игрового имени и не заменяет runtime trace.
|
`+0x50 = 1`; это не даёт ему игрового имени и не заменяет runtime trace.
|
||||||
|
|
||||||
|
### Evaluator записей условий: `ai.dll` RVA `0x5180`
|
||||||
|
|
||||||
|
Ниже описан отдельный слой готовых условий, а не декодирование instructions.
|
||||||
|
Результат привязан к GOG `ai.dll` версии `1.00`, SHA-256
|
||||||
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`;
|
||||||
|
PE image base — `0x10000000`, адрес функции — VA `0x10005180` / RVA `0x5180`.
|
||||||
|
Функция занимает 910 байт и имеет единственного прямого caller-а
|
||||||
|
`FUN_10004ee0` (RVA `0x4ee0`). Это статический вывод по машинному коду; сам
|
||||||
|
evaluator и его внешний объектный интерфейс здесь не запускались.
|
||||||
|
|
||||||
|
Функция принимает signed index в массив записей по `this + 0x34`. Проверенный
|
||||||
|
доступ `FUN_100070c0` возвращает 16-byte запись:
|
||||||
|
|
||||||
|
| Смещение | Поле | Роль в evaluator-е |
|
||||||
|
| ---: | --- | --- |
|
||||||
|
| `+0x00` | `u32 tag` | Выбор одной из пяти ветвей |
|
||||||
|
| `+0x04` | `u32 subtype` | Дополнительная проверка формата ветви |
|
||||||
|
| `+0x08` | payload | Ключ объекта или индекс списка, в зависимости от tag |
|
||||||
|
| `+0x0c` | byte completion | Записывается в `1` только после успешной проверки |
|
||||||
|
|
||||||
|
Общие проверки допускают только пары `(tag 1..3, subtype 0)` и
|
||||||
|
`(tag 4..5, subtype 1)`. Неизвестный tag и неподходящий subtype не завершают
|
||||||
|
условие. При провале evaluator не очищает и не меняет байт завершения; при
|
||||||
|
успехе он устанавливает его в `1`. Вызывающая функция `0x4ee0` отдельно
|
||||||
|
пропускает уже завершённые записи, поэтому этот пропуск нельзя приписывать
|
||||||
|
самому evaluator-у. Прямая запись evaluator-а в запись — только этот байт, но
|
||||||
|
вызываемые виртуальные методы могут иметь внешние эффекты.
|
||||||
|
|
||||||
|
Пары tag/subtype проверяются так:
|
||||||
|
|
||||||
|
| Запись | Условие установки флага завершения |
|
||||||
|
| --- | --- |
|
||||||
|
| `(1, 0)` | Менеджер объектов, полученный из контекста `this + 0x60` по полю `+0x35c`, ищет payload через виртуальный слот `+0x1c`; поиск должен вернуть null. Найденный объект оставляет условие незавершённым. |
|
||||||
|
| `(2, 0)` | Поиск payload через тот же слот `+0x1c` должен найти объект. Затем его слот `+0x10` должен вернуть `0`, либо слот `+0x18` должен заполнить переданную структуру результата так, чтобы её первый `u32` был `1`, `0x12` или `0x13`. |
|
||||||
|
| `(3, 0)` | Поиск должен найти объект; его слот `+0x44` должен вернуть тот же `u32`, что и первый word контекста, на который указывает поле `this + 0x60`. |
|
||||||
|
| `(4, 1)` | Payload выбирает 32-byte запись в списке `this + 0x4c`; вложенный в неё список дочерних ссылок находится по смещению `+0x0c`. Ни один проверяемый дочерний ключ не должен разрешиться в объект. |
|
||||||
|
| `(5, 1)` | Используется тот же список. Отсутствующий объект пропускается; найденный объект проходит проверку, только если его слот `+0x10` вернул `0`. Первый найденный объект с ненулевым результатом завершает обход неуспехом. |
|
||||||
|
|
||||||
|
Для tag 2 структура размером `0x138` байт инициализируется функцией
|
||||||
|
`FUN_10004330`, затем передаётся в slot `+0x18`. x87-значение, которое
|
||||||
|
возвращает этот вызов, инструкция `FSTP ST0` по RVA `0x5256` сразу удаляет со
|
||||||
|
стека FPU. Следовательно, ветвь принимает решение только по первому word
|
||||||
|
выходной структуры, а не по возвращённому числовому значению.
|
||||||
|
|
||||||
|
У tag 4 и 5 есть важная граница индекса. Сравнение payload с количеством
|
||||||
|
элементов — unsigned `CMP` с условием `JA`: только `payload > count` сразу
|
||||||
|
оставляет запись незавершённой. Значение `payload == count` проходит эту
|
||||||
|
проверку и доходит до проверенного доступа `FUN_10002e20`; тот отвергает
|
||||||
|
`index >= count` и выбрасывает C++ exception. Это не эквивалентно обычной проверке
|
||||||
|
`payload < count` с безопасным отказом для всех неверных индексов.
|
||||||
|
|
||||||
|
Сами дочерние ссылки списка читаются как signed `i32`. Ровно `-1` — sentinel,
|
||||||
|
который пропускается. Остальные значения проверяются как индексы в контекстном
|
||||||
|
массиве `context + 0x10` с 32-byte элементами; в элементе по `+0x04` берётся
|
||||||
|
ключ следующего поиска объекта. Для tag 4 первый найденный объект немедленно
|
||||||
|
прерывает обход с незавершённым результатом; для tag 5 обход прекращается на
|
||||||
|
первом найденном объекте с ненулевым результатом slot `+0x10`. Если обход
|
||||||
|
достигает конца без такого запрета, соответствующая ветвь устанавливает byte
|
||||||
|
completion.
|
||||||
|
|
||||||
|
Из этих ветвей нельзя выводить языковые имена вроде «объект уничтожен» или
|
||||||
|
«объект принадлежит игроку»: в коде подтверждены только поиски, виртуальные
|
||||||
|
вызовы, сравнения и коды результата. Неизвестно и то, какая функция создаёт
|
||||||
|
массив 16-byte записей. Считыватель `FUN_10011b20` (RVA `0x11b20`) отдельно
|
||||||
|
разбирает `.scr` с переменной длиной: событие содержит длину и байты имени,
|
||||||
|
счётчик вложенных записей, а вложенная запись — семь заголовочных `u32` и список
|
||||||
|
ссылок. При разборе reader хранит описатели событий с шагом `0x14` и описатели
|
||||||
|
вложенных записей с шагом `0x20`; это шаги элементов внутренних контейнеров, а не
|
||||||
|
фиксированные размеры записей на диске. Связь этих контейнеров с массивом
|
||||||
|
`this + 0x34` не найдена, поэтому приравнивать их нельзя. Не установлены и
|
||||||
|
объект-владелец массива, соглашение его runtime-вызова и предметные эффекты
|
||||||
|
внешних виртуальных вызовов; границы перечислены в
|
||||||
|
[`knowledge-boundaries.md`](knowledge-boundaries.md).
|
||||||
|
|
||||||
`Handler(1)` — второй table entry, VA `0x10007fd0`, — не создаёт игровую
|
`Handler(1)` — второй table entry, VA `0x10007fd0`, — не создаёт игровую
|
||||||
команду. Он сохраняет active VM context, берёт один instruction-derived index
|
команду. Он сохраняет active VM context, берёт один instruction-derived index
|
||||||
через current event/instruction offsets `+0x48/+0x4c`, а затем разрешает его
|
через current event/instruction offsets `+0x48/+0x4c`, а затем разрешает его
|
||||||
|
|||||||
+45
-17
@@ -181,25 +181,53 @@ IDs и отправляет команды через игровые interfaces,
|
|||||||
открытым направлением. До появления decompiler-а `.scr` binary body сохраняется
|
открытым направлением. До появления decompiler-а `.scr` binary body сохраняется
|
||||||
lossless, а доказанные symbol/event tables документируются отдельно.
|
lossless, а доказанные symbol/event tables документируются отдельно.
|
||||||
|
|
||||||
### Подтверждённый evaluator выражений
|
### Подтверждённый evaluator записей условий
|
||||||
|
|
||||||
Ghidra 12.1.2 decompile GOG `ai.dll` фиксирует отдельный evaluator по VA
|
У сценарного слоя есть не только последовательность инструкций, но и отдельные
|
||||||
`0x10005180` (не dispatcher инструкций `.scr`). Он получает индекс записи,
|
короткие проверки состояния. В GOG `ai.dll` версии `1.00` evaluator таких
|
||||||
ищет её в контейнере `this + 0x34`, переключается по `u32 tag` в offset `+0`
|
проверок расположен по RVA `0x5180` (VA `0x10005180` при image base
|
||||||
и при успешном результате пишет completion byte в `+0x0c`. Из кода доказаны
|
`0x10000000`; SHA-256 сборки —
|
||||||
ровно пять ветвей `tag = 1..5`; `tag = 1..3` требуют `u32` subtype в
|
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`). Он
|
||||||
`+0x04 == 0`, а `tag = 4..5` — `+0x04 == 1`. Поле payload находится по
|
получает индекс 16-byte записи и смотрит на её tag, subtype и payload. Для tags
|
||||||
`+0x08`.
|
1–3 допустим subtype `0`, для tags 4–5 — subtype `1`; другие сочетания не
|
||||||
|
завершают условие. Байт по смещению `+0x0c` — флаг завершения: evaluator ставит
|
||||||
|
`1`, когда проверка проходит, и оставляет запись как есть, когда не проходит.
|
||||||
|
Вызывающая функция позже пропускает уже завершённые условия.
|
||||||
|
|
||||||
Ветки 1--3 делают lookup через object/interface, достижимый от
|
Tag выбирает конкретное правило. Для tag 1 запись завершается, когда поиск
|
||||||
`this + 0x60 + 0x35c`, и используют его virtual slot `+0x1c`. Ветка 2
|
объекта по payload ничего не нашёл. Для tag 2 объект, наоборот, должен
|
||||||
дополнительно читает virtual slots `+0x10`/`+0x18` возвращённого объекта и
|
существовать: затем игра проверяет одно из его свойств и, в более сложной
|
||||||
разрешает result tags `1`, `0x12`, `0x13`. Ветка 3 сравнивает virtual slot
|
ветви, принимает только три числовых кода результата — `1`, `0x12` и `0x13`.
|
||||||
`+0x44` с текущим object value. Ветки 4--5 используют `payload` как index в
|
Для tag 3 найденное значение сравнивается с первым числом текущего контекста.
|
||||||
контейнере `this + 0x4c`, обходят его child indices и делают тот же lookup;
|
Мы не знаем из одного этого кода, каким игровым словам соответствуют эти поля,
|
||||||
их разные success-guards пока не именуются семантически. Это доказывает
|
поэтому не называем их владельцем, жизнью или приказом.
|
||||||
typed condition/evaluation layer, но **не** формат `.scr`, размеры инструкций
|
|
||||||
или связь чисел tag с языковыми операторами.
|
Теги 4 и 5 обходят дочерние ссылки выбранного списка. Значение `-1` означает
|
||||||
|
«элемента нет» и пропускается. Тег 4 завершается, только если ни один дочерний
|
||||||
|
ключ не нашёл объект. Тег 5 тоже пропускает отсутствующий объект, но для
|
||||||
|
найденного смотрит результат другого виртуального вызова: ноль разрешает
|
||||||
|
продолжить, а первый ненулевой результат прекращает обход и оставляет условие
|
||||||
|
незавершённым. Так различаются два алгоритма проверки набора объектов без
|
||||||
|
предположений о том, почему сценарист выбрал один из них.
|
||||||
|
|
||||||
|
У списков обнаружена нетипичная граница. Программа сначала сравнивает индекс
|
||||||
|
списка с его длиной как unsigned число и отбрасывает только значения строго
|
||||||
|
больше длины. Равный длине индекс проходит первую проверку, но следующий
|
||||||
|
проверенный доступ обнаруживает выход за границу и возбуждает C++ exception.
|
||||||
|
Поэтому в будущем движке это нельзя незаметно заменить на «все неверные
|
||||||
|
индексы просто дают ложь».
|
||||||
|
|
||||||
|
Это статическая реконструкция ветвей, а не доказательство исполнения условия
|
||||||
|
в работающем оригинале. Сам вызов RVA `0x5180` и внешний object interface не
|
||||||
|
проверялись динамически; неизвестен и код, который создаёт эти 16-byte записи.
|
||||||
|
Считыватель `.scr` хранит описатели событий и вложенных записей во внутренних
|
||||||
|
контейнерах с шагом `0x14` и `0x20` соответственно. На диске же `.scr` использует
|
||||||
|
переменные поля: имя события задаётся длиной, а вложенная запись содержит
|
||||||
|
заголовок и список ссылок. Связь этих структур с evaluator-ом не доказана.
|
||||||
|
Подробные смещения,
|
||||||
|
виртуальные слоты и границы анализа приведены в
|
||||||
|
[`приложении о сценарной VM`](../appendices/script-vm.md#evaluator-записей-условий-ai-dll-rva-0x5180)
|
||||||
|
и [`границах знаний`](../appendices/knowledge-boundaries.md#script-vm).
|
||||||
|
|
||||||
### TRF и preload-данные
|
### TRF и preload-данные
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user