Компиляция

Игра подготавливает список файлов и передает его компилятору, который возвращает один большой блок содержащий байт-код со всеми функциями и таблицу строк используемую для имен переменных, функций и для литеральных строк.
Компилятор не генерирует списка функций, так что потоку приходится самому проходиться по всему байт-коду в поисках специальных меток указывающих на начало или конец функции.
То же касается и меток для прыжков.
Для инициализации глобальных переменных генерируются функции "<init>", по одной на каждый файл исходного кода. Вызвать их через ExecuteFunc не выйдет, так как поток хранит их в отдельном списке.
Качество генерируемого байт-кода оставляет желать лучшего, в чем можно убедиться воспользовавшись этой программой.

Инструкции

Размер одной инструкции равен 8-ми байтам.
Их структура следующая:
type
	TInstruction = packed record
		p3, p2, p1, id: Byte;
		p4: Dword;
	end;
Старший байт первого двойного слова (id) хранит в себе номер операции, прочие поля могут содержать параметры, необходимые для выполнения действия.
Одно-байтовые параметры (p1, p2, p3) обычно содержат номер регистра или тип данных, а большой четырех-байтовый (p4) содержит индекс строки с именем переменной или функции.
Также он используется для хранения номера метки, на которую нужно совершить прыжок.
Список инструкций
Имя id p1 p2 p3 p4 Назначение
endscript 0x01 Сообщает парсеру о конце кода.
debug 0x02 метка Прыгает на указанную метку, пропуская отладочный код. В релизной версии игры, ведет себя как обычный jump.
function 0x03 тип имя Сообщает парсеру, что дальше идет код функции.
endfunction 0x04 Сообщает парсеру о конце функции.
local 0x05 тип имя Объявляет локальную переменную, которая будет уничтожена по выходу из функции.
global 0x06 тип имя Объявляет глобальную переменную.
constant 0x07 тип имя Объявляет константу.
argument 0x08 тип номер имя Объявляет локальную переменную, в которую помещается значение аргумента с указанным индексом.
parent 0x09 название типа Указывает родителя для следующих объявленных типов.
child 0x0A название типа Объявляет тип, являющийся потомком указанного ранее.
cleanstack 0x0B количество Выталкивает из стека указанное количество значений.
literal 0x0C целевой регистр тип значение Помещает в регистр литерал указанного типа.
move 0x0D целевой регистр исходный регистр Копирует значение из одного регистра в другой.
get 0x0E целевой регистр тип исходная переменная Копирует в регистр значение переменной.
code 0x0F целевой регистр тип функция Помещает в регистр указатель на функцию.
get[] 0x10 целевой регистр регистр с индексом тип исходная переменная Копирует в регистр значение элемента массива.
set 0x11 исходный регистр целевая переменная Присваивает переменной значение регистра.
set[] 0x12 регистр с индексом исходный регистр целевая переменная Присваивает элементу массива значение регистра.
push 0x13 регистр Толкает значение регистра в стек.
pop 0x14 регистр Выталкивает значение из стека в регистр.
ncall 0x15 функция Вызывает нативную функцию.
call 0x16 функция Вызывает функцию. После вызова, необходимо очистить стек от аргументов.
i2r 0x17 регистр Конвертирует целое число в вещественное.
and 0x18 целевой регистр регистр А регистр Б Логическое И.
or 0x19 целевой регистр регистр А регистр Б Логическое ИЛИ.
equal 0x1A целевой регистр регистр А регистр Б Равно.
not equal 0x1B целевой регистр регистр А регистр Б Неравно.
less equal 0x1C целевой регистр регистр А регистр Б Меньше или равно.
greater equal 0x1D целевой регистр регистр А регистр Б Больше или равно.
less 0x1E целевой регистр регистр А регистр Б Меньше.
greater 0x1F целевой регистр регистр А регистр Б Больше.
add 0x20 целевой регистр регистр А регистр Б Сложение.
sub 0x21 целевой регистр регистр А регистр Б Вычитание.
mul 0x22 целевой регистр регистр А регистр Б Умножение.
div 0x23 целевой регистр регистр А регистр Б Деление.
modulo 0x24 целевой регистр регистр А регистр Б Берет остаток от деления.
negate 0x25 регистр Меняет знак числа в регистре на противоположный.
not 0x26 регистр Инвертирует логическое значение в регистре.
return 0x27 Возвращает управление вызывающей подпрограмме.
label 0x28 метка Сообщает парсеру о метке.
jump+ 0x29 регистр метка Совершает прыжок на указанную метку, при условии, что значение в регистре истинно.
jump- 0x2A регистр метка Совершает прыжок на указанную метку, при условии, что значение в регистре ложно.
jump 0x2B метка Совершает прыжок на указанную метку.

Исполнение

Потоки

Для запуска кода используются потоки. Сначала создается главный поток, а затем его используют как прототип, порождая дочерние копии, которые ссылаются на его таблицу строк, глобальных переменных и прочее, но имеют личный стек и регистры.
Игра вызывает функцию "config" когда отображает лобби, и функцию "main" примерно на двух третях полосы загрузки. Инициализаторы глобальных переменных вызываются непосредственно перед вызовом первой функции.
Каждый раз, когда игра вызывает jass-функцию, она создает новый поток, который затем уничтожает, если только он не впал в спячку (TriggerSleepAction).
В последнем случае, игра запускает таймер на указанный период времени, по истечению которого шлет сетевую команду на возобновление потока выполнения триггера.

Переменные

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

Регистры

Каждый поток распологает 256-ю регистрами.
Для доступа к ним требуется меньше времени, так как для хранения используется массив.
Все инструкции, требующие данные, принимают в качестве параметров именно их.
Если нужно изменить значение переменной, то придется сначала скопировать её значение в регистр, провести все необходимые операции и только потом поместить результат обратно.

Типы значений

Список типов
Имя Комментарий
nothing Отсутствующее значение. Переменная с таким значением считается неинициализированной и выполнение обрывается при попытке доступа к ней.
retaddr Индекс инструкции в регионе байт-кода умноженный на 2. Безопаснее код от использования индексов не стал, так как никто не проверяет их на выход за границы.
null Тип используемый для нулевых литералов.
code Указатель на инструкцию для виртуальной машины.
integer 32-битное знаковое целое.
real 32-битное вещественное.
string Индекс для таблицы строк.
handle Индекс для таблицы хэндлов.
boolean Логическое значение.
int array
real array
string array
handle array
boolean array

Массивы

Переменная массив является указателем на объект, содержащим список значений соотвествующего типа.

Небезопасные типы

Если бы все типы использовали индексы для таблиц вместо реальных указателей, то у писателей вирусов возникли бы большие проблемы.

Счетчик ссылок

Дескрипторы (handle)

При помещении в переменную значения с типом handle, интерпретатор увеличивает счетчик ссылок на новое значение и уменьшает таковой у старого, если оно было представлено.
Значения из регистров и стека не учитываются системой.
Когда счетчик упадет до нуля слот в таблице хэндлов освободится и сможет быть переиспользован.
Если при создании хэндла был указан специальный флаг, то по его освобождению объект, содержащийся внутри, также будет уничтожен. Это касается таких типов как, например, location, sound и timer, но почему-то не group, force, или rect.
Вот только когда хэндл создается, количество ссылок на него уже равняется единице.
Чтобы избавиться от этой изначальной ссылки, нужно вызвать функцию уничтожения, например, DestroyLocation(l).
Под типом handle в JASS'е может подразумеваться также и простая числовая константа, как те, что возвращаются нативками вроде ConvertPlayerColor.
В случае с ними, ни о каком подсчете ссылок не может идти и речи, так как за ними не стоит каких-либо объектов.
Чтобы различать их, игра прибавляет к индексу настоящих хэндлов число 0x100000, а все что меньше него игнорирует.
Ну, а всё, что больше, игра принимает без проверок на выход за границы таблицы хэндлов.

Строки (string)

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

Стековый кадр

Для каждой вызванной функции создается стековый кадр, в котором хранится таблица с локальными переменными, а также стек в который толкаются аргументы для вызова других функций, адрес возврата и промежуточные результаты вычислений.
Аргументы хранятся вместе с локальными переменными и инициализируются значениями из стекового кадра вызывающей функции.
В стек помещается не более 32-ух элементов, чего максимум хватит на вызов функции с 31-им аргументом плюс еще один слот уйдет на адрес возврата и то при условии, что вызов не будет происходить в сложном выражении.
При попытке вытолкнуть значение из пустого стека или толкнуть в заполненый — игра крашнется.
Когда стековый кадр уничтожается, он удаляет все переменные.
К сожалению, разработчики вставили в код специальную проверку на то, является ли переменная аргументом и только лишь в том случае обнуляют её, а иначе пусть утекают ссылки на таблицу хэндлов.

Вызов функций

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

Вызов нативных функций

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

Сигнатура

Для описания принимаемых и возвращаемых значений используется строковая сигнатура.
Сначала идет список типов аргументов, заключенный в круглые скобки, после которого располагается тип возвращаемого значения.
Для обозначения каждого типа существует свой символ.
Таблица символов
Символ JASS тип
V nothing
C code
I integer
R real
S string
H handle
B boolean
После символа "H", обозначающего хэндл, должно идти имя типа, заканчивающееся точкой с запятой (";"). Например: "Hunit;" или "Hhandle;".
Это имя используется для проверок во время компиляции, а во время исполнения оно игнорируется.
Примеры сигнатур
// "(Hplayer;S)V"
native SetPlayerName takes player whichPlayer, string name returns nothing

// "(C)Hconditionfunc;"
native Condition takes code func returns conditionfunc

Передача параметров и приём результата

В основном, типы используются как есть, но некоторые нуждаются в преобразовании прежде чем будут переданы наружу или приняты обратно.
Тип При передаче При получении
real Указатель на значение По значению, но через регистр EAX, а не через стек FPU.
string Указатель на объект строку. Индекс строки в таблице главного потока карты.
code Индекс в таблице кода. Указатель на инструкцию.
Так как игра передает строки в виде указателей на элементы таблицы строк, при первом же перевыделении памяти, что может произойти после добавления туда новых элементов, такой указатель сломается и будет указывать в случайную область памяти и использование подобного параметра может привести к непредсказуемым последствиям.
Стандартные нативки обычно не обращаются ко входным строкам после совершения действий, которые могли привести к их поломке, так что пока обходилось.
Из-за того, что нативки возвращают строки в виде индексов из таблицы строк главного потока карты, все прочие потоки, не являющиеся дочерними ему, например те, что исполняют прелоад скрипты или ИИ, не могут получить результат от нативок с таким типом. Индекс не будет подходить для их таблицы строк.
Будет получена случайная строка или произойдет выход за границы таблицы с печальным концом.
Не без проблем и тип code, ведь нативки ожидают получить хэндл кода из таблицы главного потока, а значит, если попытаться передать ссылку на функцию из потока, не связанного с ним, то вызвана будет функция, находящая по тому же индексу, но в таблице главного потока. Или же игра полезет за пределы таблицы и крашнется.

Пробленые функции и операции

Склеивание строк

В теории, строки не имеют ограничений на длину, но при попытке склеить две строки могут возникнуть проблемы:
  • От левого операнда будут взяты лишь первые 4096 байт, а прочие будут проигнорированы.
  • Для правого операнда ограничений нет, но если сумарный размер строк превысит 4099 байт, то произойдет переполнение стэка и игра крашнется.
В остальных случаях, как если строка получена в результате вызова нативной функции — ограничений нет.

Функция StringHash

Возвращает хэш-сумму строки.
Перед процедурой строка проходит следующие преобразования:
  • Все латинские буквы делаются заглавными.
  • Все косые черты "/" меняются на "\".
Обрабатываются лишь первые 1023 байта переданой строки.

Функция SubString

Создает новую строку на основе указаного участка переданой строки.
Использует только младшие два байта входящих параметров start и end, воспринимая их как int16, ограниченый значениями от -32768 до 32767.
Следит за тем, чтобы переданые позиции не превышали размер строки, но не за тем, чтобы они не были меньше нуля.
В случае, если копируется регион начинающийся с первого байта, то если хэши оригинальной строки и результата совпадают, то может сработать неправильно и вернуть ту же строку, что и получила на входе.
`
ОЖИДАНИЕ РЕКЛАМЫ...
10
Очень интересно, спасибо!
Не постесняюсь задать глупый вопрос (я не силен в теме) - а может ли такая "виртуальная машина" поддаться какому-то модингу, или это слишком глубоко в движке игры?
Возможно ли ожидать в будущем какие-то глобальные моды завязанные на изменения конфигурации такой вм и добавлении например новых фич? (опять же, я не силен в вопросе, ну например какой-то менеджмент памяти, контроль утечек и т.д.)
19
Если убрать проверку на аргументы, то игра начнет автоматически обнулять переменные и тогда перестанут утекать слоты таблицы хэндлов.
Также можно уничтожать объекты на которые ссылались освобожденные хэндлы и забыть об ошибках выделения памяти из-за мусорных точек, групп и прочего.
Но в таком случае, мод должен будет присутствовать на всех клиентах, а иначе десинхронизация.
23
Очень хорошая статья! Жаль, непонятно, возможно ли понять, в каком файле игры всё это прописано, можно ли исправить и будет ли исправленный файл кушать игра.
30
а может ли такая "виртуальная машина" поддаться какому-то модингу, или это слишком глубоко в движке игры?
Китайцы уже давно доработали байт машину.
30
Но в таком случае, мод должен будет присутствовать на всех клиентах
UjAPI может решить проблему присутствия кода на всех машинах.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.