Раздел:
Разное

Компиляция

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

Инструкции

Размер одной инструкции равен 8-ми байтам.
Их структура следующая:
type
	TInstruction = packed record
		p3, p2, p1, id: Byte;
		p4: Dword;
	end;
Старший байт первого двойного слова (id) хранит в себе номер операции, прочие поля могут содержать параметры, необходимые для выполнения действия.
Одно-байтовые параметры (p1, p2, p3) обычно содержат номер регистра или тип данных, а большой четырех-байтовый (p4) содержит индекс строки с именем переменной или функции.
Также он используется для хранения номера метки, на которую нужно совершить прыжок.
Список инструкций
Имяidp1p2p3p4Назначение
endscript0x01Сообщает парсеру о конце кода.
debug0x02меткаПрыгает на указанную метку, пропуская отладочный код. В релизной версии игры, ведет себя как обычный jump.
function0x03типимяСообщает парсеру, что дальше идет код функции.
endfunction0x04Сообщает парсеру о конце функции.
local0x05типимяОбъявляет локальную переменную, которая будет уничтожена по выходу из функции.
global0x06типимяОбъявляет глобальную переменную.
constant0x07типимяОбъявляет константу.
argument0x08типномеримяОбъявляет локальную переменную, в которую помещается значение аргумента с указанным индексом.
parent0x09название типаУказывает родителя для следующих объявленных типов.
child0x0Aназвание типаОбъявляет тип, являющийся потомком указанного ранее.
cleanstack0x0BколичествоВыталкивает из стека указанное количество значений.
literal0x0Cцелевой регистртипзначениеПомещает в регистр литерал указанного типа.
move0x0Dцелевой регистрисходный регистрКопирует значение из одного регистра в другой.
get0x0Eцелевой регистртиписходная переменнаяКопирует в регистр значение переменной.
code0x0Fцелевой регистртипфункцияПомещает в регистр указатель на функцию.
get[]0x10целевой регистррегистр с индексомтиписходная переменнаяКопирует в регистр значение элемента массива.
set0x11исходный регистрцелевая переменнаяПрисваивает переменной значение регистра.
set[]0x12регистр с индексомисходный регистрцелевая переменнаяПрисваивает элементу массива значение регистра.
push0x13регистрТолкает значение регистра в стек.
pop0x14регистрВыталкивает значение из стека в регистр.
ncall0x15функцияВызывает нативную функцию.
call0x16функцияВызывает функцию. После вызова, необходимо очистить стек от аргументов.
i2r0x17регистрКонвертирует целое число в вещественное.
and0x18целевой регистррегистр Арегистр БЛогическое И.
or0x19целевой регистррегистр Арегистр БЛогическое ИЛИ.
equal0x1Aцелевой регистррегистр Арегистр БРавно.
not equal0x1Bцелевой регистррегистр Арегистр БНеравно.
less equal0x1Cцелевой регистррегистр Арегистр БМеньше или равно.
greater equal0x1Dцелевой регистррегистр Арегистр ББольше или равно.
less0x1Eцелевой регистррегистр Арегистр БМеньше.
greater0x1Fцелевой регистррегистр Арегистр ББольше.
add0x20целевой регистррегистр Арегистр БСложение.
sub0x21целевой регистррегистр Арегистр БВычитание.
mul0x22целевой регистррегистр Арегистр БУмножение.
div0x23целевой регистррегистр Арегистр БДеление.
modulo0x24целевой регистррегистр Арегистр ББерет остаток от деления.
negate0x25регистрМеняет знак числа в регистре на противоположный.
not0x26регистрИнвертирует логическое значение в регистре.
return0x27Возвращает управление вызывающей подпрограмме.
label0x28меткаСообщает парсеру о метке.
jump+0x29регистрметкаСовершает прыжок на указанную метку, при условии, что значение в регистре истинно.
jump-0x2AрегистрметкаСовершает прыжок на указанную метку, при условии, что значение в регистре ложно.
jump0x2BметкаСовершает прыжок на указанную метку.

Исполнение

Потоки

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

Переменные

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

Регистры

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

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

Список типов
ИмяКомментарий
nothingОтсутствующее значение. Переменная с таким значением считается неинициализированной и выполнение обрывается при попытке доступа к ней.
retaddrИндекс инструкции в регионе байт-кода умноженный на 2. Безопаснее код от использования индексов не стал, так как никто не проверяет их на выход за границы.
nullТип используемый для нулевых литералов.
codeУказатель на инструкцию для виртуальной машины.
integer32-битное знаковое целое.
real32-битное вещественное.
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 тип
Vnothing
Ccode
Iinteger
Rreal
Sstring
Hhandle
Bboolean
После символа "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.
Следит за тем, чтобы переданые позиции не превышали размер строки, но не за тем, чтобы они не были меньше нуля.
В случае, если копируется регион начинающийся с первого байта, то если хэши оригинальной строки и результата совпадают, то может сработать неправильно и вернуть ту же строку, что и получила на входе.
`
ОЖИДАНИЕ РЕКЛАМЫ...
0
10
1 год назад
Отредактирован Slonick
0
Очень интересно, спасибо!
Не постесняюсь задать глупый вопрос (я не силен в теме) - а может ли такая "виртуальная машина" поддаться какому-то модингу, или это слишком глубоко в движке игры?
Возможно ли ожидать в будущем какие-то глобальные моды завязанные на изменения конфигурации такой вм и добавлении например новых фич? (опять же, я не силен в вопросе, ну например какой-то менеджмент памяти, контроль утечек и т.д.)
0
14
1 год назад
Отредактирован IceFog
0
Если убрать проверку на аргументы, то игра начнет автоматически обнулять переменные и тогда перестанут утекать слоты таблицы хэндлов.
Также можно уничтожать объекты на которые ссылались освобожденные хэндлы и забыть об ошибках выделения памяти из-за мусорных точек, групп и прочего.
Но в таком случае, мод должен будет присутствовать на всех клиентах, а иначе десинхронизация.
0
17
1 год назад
0
Очень хорошая статья! Жаль, непонятно, возможно ли понять, в каком файле игры всё это прописано, можно ли исправить и будет ли исправленный файл кушать игра.
0
29
1 год назад
0
а может ли такая "виртуальная машина" поддаться какому-то модингу, или это слишком глубоко в движке игры?
Китайцы уже давно доработали байт машину.
0
29
1 год назад
0
Но в таком случае, мод должен будет присутствовать на всех клиентах
UjAPI может решить проблему присутствия кода на всех машинах.
0
5
1 год назад
0
а на гуи можно?
Чтобы оставить комментарий, пожалуйста, войдите на сайт.