Добавлен IceFog,
опубликован
Раздел:
Разное
Компиляция
Игра подготавливает список файлов и передает его компилятору, который возвращает один большой блок содержащий байт-код со всеми функциями и таблицу строк используемую для имен переменных, функций и для литеральных строк.
Компилятор не генерирует списка функций, так что потоку приходится самому проходиться по всему байт-коду в поисках специальных меток указывающих на начало или конец функции.
То же касается и меток для прыжков.
То же касается и меток для прыжков.
Для инициализации глобальных переменных генерируются функции "<init>", по одной на каждый файл исходного кода. Вызвать их через ExecuteFunc не выйдет, так как поток хранит их в отдельном списке.
Качество генерируемого байт-кода оставляет желать лучшего, в чем можно убедиться воспользовавшись этой программой.
Инструкции
Размер одной инструкции равен 8-ми байтам.
Их структура следующая:
Их структура следующая:
type
TInstruction = packed record
p3, p2, p1, id: Byte;
p4: Dword;
end;
Старший байт первого двойного слова (id) хранит в себе номер операции, прочие поля могут содержать параметры, необходимые для выполнения действия.
Одно-байтовые параметры (p1, p2, p3) обычно содержат номер регистра или тип данных, а большой четырех-байтовый (p4) содержит индекс строки с именем переменной или функции.
Также он используется для хранения номера метки, на которую нужно совершить прыжок.
Одно-байтовые параметры (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.
Если при создании хэндла был указан специальный флаг, то по его освобождению объект, содержащийся внутри, также будет уничтожен. Это касается таких типов как, например, location, sound и timer, но почему-то не group, force, или rect.
Вот только когда хэндл создается, количество ссылок на него уже равняется единице.
Чтобы избавиться от этой изначальной ссылки, нужно вызвать функцию уничтожения, например, DestroyLocation(l).
Чтобы избавиться от этой изначальной ссылки, нужно вызвать функцию уничтожения, например, DestroyLocation(l).
Под типом handle в JASS'е может подразумеваться также и простая числовая константа, как те, что возвращаются нативками вроде ConvertPlayerColor.
В случае с ними, ни о каком подсчете ссылок не может идти и речи, так как за ними не стоит каких-либо объектов.
Чтобы различать их, игра прибавляет к индексу настоящих хэндлов число 0x100000, а все что меньше него игнорирует.
В случае с ними, ни о каком подсчете ссылок не может идти и речи, так как за ними не стоит каких-либо объектов.
Чтобы различать их, игра прибавляет к индексу настоящих хэндлов число 0x100000, а все что меньше него игнорирует.
Ну, а всё, что больше, игра принимает без проверок на выход за границы таблицы хэндлов.
Строки (string)
Для строк существует отдельная таблица, элементы которой тоже имеют счетчик ссылок.
При создании строки, её счетчик равен единице, как и в случае с хэндлами.
Но в отличие от них, у строк нет функции для уничтожения вроде DestroyString, а значит любая созданая строка будет существовать до конца игры.
Но в отличие от них, у строк нет функции для уничтожения вроде DestroyString, а значит любая созданая строка будет существовать до конца игры.
Более того, каждый раз, когда игра возвращает хэндл строки, что происходит после склеивания строк или вызова нативок их возвращающих, счетчик ссылок зачем-то увеличивается.
В результате такого бесконтрольного роста, рано или поздно может произойти переполнение счетчика, что приведет к уничтожению строки, которая все еще будет в пользовании.
Правда, для этого придется провернуть такое действие около четырех миллиардов раз (максимальное количество значений для 32-ух битных целых).
Правда, для этого придется провернуть такое действие около четырех миллиардов раз (максимальное количество значений для 32-ух битных целых).
Стековый кадр
Для каждой вызванной функции создается стековый кадр, в котором хранится таблица с локальными переменными, а также стек в который толкаются аргументы для вызова других функций, адрес возврата и промежуточные результаты вычислений.
Аргументы хранятся вместе с локальными переменными и инициализируются значениями из стекового кадра вызывающей функции.
В стек помещается не более 32-ух элементов, чего максимум хватит на вызов функции с 31-им аргументом плюс еще один слот уйдет на адрес возврата и то при условии, что вызов не будет происходить в сложном выражении.
При попытке вытолкнуть значение из пустого стека или толкнуть в заполненый — игра крашнется.
Когда стековый кадр уничтожается, он удаляет все переменные.
К сожалению, разработчики вставили в код специальную проверку на то, является ли переменная аргументом и только лишь в том случае обнуляют её, а иначе пусть утекают ссылки на таблицу хэндлов.
К сожалению, разработчики вставили в код специальную проверку на то, является ли переменная аргументом и только лишь в том случае обнуляют её, а иначе пусть утекают ссылки на таблицу хэндлов.
Вызов функций
Перед вызовом функции, аргументы толкаются в стек в порядке объявления, после вызова их нужно оттуда убрать.
Затем инструкция вызова толкает в стек адрес возврата, который указывает на инструкцию следующую за текущей и создает новый стековый кадр.
Затем инструкция вызова толкает в стек адрес возврата, который указывает на инструкцию следующую за текущей и создает новый стековый кадр.
В случае, если функция возвращает результат, она записывает его в нулевой регистр.
Когда функция возвращается, текущий стековый кадр уничтожается, а из последнего элемента предыдущего извлекается адрес возврата и управление передается ему.
Если же стековых кадров не осталось или текущий кадр был отмечен как финальный, то выполнение прерывается и управление возвращается игре.
Вызов нативных функций
Нативки вызываются также, как и обычные функции, с поправкой на то, что чистить стек не нужно и адрес возврата в стек не толкается.
При вызове нативной функции игра каждый раз заново парсит её сигнатуру, чтобы получить список аргументов с их типами.
Затем значения из стека виртуальной машины копируются в стек реальный.
Вызов производится с применением конвенции cdecl.
По завершению, результат выполнения помещается в предназначеный для этого нулевой регистр.
Затем значения из стека виртуальной машины копируются в стек реальный.
Вызов производится с применением конвенции 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.
Следит за тем, чтобы переданые позиции не превышали размер строки, но не за тем, чтобы они не были меньше нуля.
В случае, если копируется регион начинающийся с первого байта, то если хэши оригинальной строки и результата совпадают, то может сработать неправильно и вернуть ту же строку, что и получила на входе.
Следит за тем, чтобы переданые позиции не превышали размер строки, но не за тем, чтобы они не были меньше нуля.
В случае, если копируется регион начинающийся с первого байта, то если хэши оригинальной строки и результата совпадают, то может сработать неправильно и вернуть ту же строку, что и получила на входе.
`
ОЖИДАНИЕ РЕКЛАМЫ...
Чтобы оставить комментарий, пожалуйста, войдите на сайт.
Отредактирован Slonick
Не постесняюсь задать глупый вопрос (я не силен в теме) - а может ли такая "виртуальная машина" поддаться какому-то модингу, или это слишком глубоко в движке игры?
Возможно ли ожидать в будущем какие-то глобальные моды завязанные на изменения конфигурации такой вм и добавлении например новых фич? (опять же, я не силен в вопросе, ну например какой-то менеджмент памяти, контроль утечек и т.д.)
Отредактирован IceFog