Kailo
Участник
- Сообщения
- 194
- Реакции
- 896
Контекст
Контекст - набор данных, совокупность которых образует сам объект чего-либо как таковой. В данном случае контекст плагина.
Контекст плагина состоит из:
- кода
- памяти (memory)
- указателя стека (stack pointer)
- указателя кучи (heap pointer)
- указателя стекового кадра aka. фрейм (frame pointer)
- сохраненное значение указателя стека (для проверки на утечку)
- сохраненное значение указателя кучи (для проверки на утечку)
Все указатели являются адресами в памяти плагина.
Виртуальная память
Память условно разделяется на две части:
1) данные
2) память (немного тавтологии):
Данные, расположены начиная от начала памяти плагина. Размер этой секции определяется компилятором во время процесса компиляции и записан в smx. В секции содержатся глобальные и статические локальные переменные, константные массивные значения используемые в плагине. Так как этот раздел памяти начинается с самого начала памяти, то адреса памяти и адреса глобальных переменных плагина совпадают.
Память, разделена на две составляющие: кучу и стек. Раздел памяти начинается сразу после конца раздела данных. В начале располагается куча, а за ней стек.
Размер памяти выделяемой плагину определяется при компиляции, вот пример лога компиляции:
Куча предназначена для хранения временных переменных и динамических массивов.
На временные переменные ни как нельзя повлиять кодом, т.к. они создаются компилятором для нужд исполнения кода, и разрушаются сразу же как исполнении свою функцию. К примеру если вы используете вызов функции, которая возвращает массив или передаете константное значение в роли массива, который может быть изменен при вызове.
Стек вызовов
Сначала, коротко о том что такое стек, для тех кто не в теме, можно прочитать в Мини-уроки: N. Стек (ожидается).
Стек в виртуальной машине SourcePawn не является типичным стеком вызовов, так как не хранит адрес возврата (адрес функции из которая была вызвана текущая и куда надо вернуть управление по завершению текущий функции) или значения регистров (каждая функция может использовать регистры, и при этом изменять их значения. Но т.к. функция, вызвавшая текущую могла хранить какую-нибудь важную информацию в них, то перед использованием регистров их старые значения сохраняются в стек при вызове, а в конце вызова восстанавливаются), а содержит только данные стековых кадров.
Стековый кадр состоит из:
- аргументов вызова функции
- количества параметров при вызове
- информации о старом кадре
Приведу пример для наглядности.
Имея следующий код:
Состояние стека (вершина стека сверху)
Вначале указан адрес ячейки в HEX, от начала стека.
Важно! Стек вызовов виртуальной машины SP расположен в памяти перевернуто.
Адреса в примере не соответствуют реальности и даны лишь для примера.
Т.е. Он начинается от конца памяти и идет к началу.
Подробнее в Википедия:Стек вызовов.
Контекст вызова,
Интерпретатор
Теперь коротко почему в стеке не хранится адрес возврата и регистры, и где они прячутся.
Это связано с реализацией передачи вызовов в самой виртуальной машине.
Вызов функций происходит через контекст плагина. При вызове инициализируется интерпретатор. Он служит для обработки и реализации инструкций (байткода).
Контекст каждого интерпретатора содержит:
- регистры
- статус исполнения (произошел ли возврат)
- указатель на следующую инструкцию для чтения
- указатель на контекст плагина
Это не все, но остальное не столь важно.
Если интерпретатор встречает вызов функции, то он инициализирует новый интерпретатор для чтения этой функции и передает ему управление, до тех пор как не произойдет возврат. И т.к. контексты каждого вызова не зависимы, нет нужны сохранять значения адреса возврата или регистров в стеке вызовов.
По возврату интерпретатор вызывающей функции копирует значение возврата из интерпретатора вызываемой и разрушает его.
Если с внутренними вызовами функции внутри плагина все выглядит вполне понятно, то остается понять как происходит "начальный вызов" (когда в нашем плагине вызывается форвард или обработчик) или же вызов функций внутри других плагинов при использовании Call_* функций.
Для этого у контекста плагина есть функция Invoke. Она порождает первоначальный интерпретатор и запоминает начальное значение фрейма в этот момент (invoke frame).
Так же, для передачи данных между плагинами, и корректным доступом к аргументом, по необходимости выделяет в куче память для аргументов, и по завершению копирует их значения в источник вызова.
Регистры
Если я не ошибаюсь в терминологии, то виртуальную машину SP правильно назвать регистровой машиной (еще бывают стековые), т.к. основные операции производятся внутри регистров.
Регистры представляют собой ячейки (англ. cell) - переменные размером 4 байта.
Существует два регистра:
- Основной регистр (англ. primary, сокр. pri)
- Вспомогательный регистр (англ. alternative, сокр. alt)
О том в каких случаях какие регистры используются, см. Набор инструкций (байткод) SP ниже в статье.
Соглашение о вызовах
Обычный вызов функции
Вызовы функций происходят по их адресу в ".code" секции, которая предварительно было скопирована из smx в контекст плагина.
Если коротко:
1) Передача аргументов
Производится через стек в обратном порядке.
2) В стек заносится кол-во аргументов
3) Сохранение значений строго фрейма в стеке и установка текущий вершины стека как текущего фрейма.
4) Локальные переменные и память выделяемая в куче должны быть освобождены вызванной функций создавший их до возврата, иначе получите ошибку (утечка стека - "stack leak" или утечка кучи - "heap leak").
5) При возврате, возвращаемое значение заносится в основной регистр.
6) Вызывающая функция производит очистку стека от аргументов, т.к. их количество было передано при вызове.
Вызов с возвратом массивов (частный случай обычного вызова)
По сути происходит обычный вызов, со скрытой передачей дополнительного аргумента.
Во-первых, на стадии компиляции, та функция что возвращает массив сообщает размер того массива что нужно вернуть (возвращать можно только массивы статического размера).
При вызове функции создается временная перемененная в куче такого же размера как и возвращаемый массив. Её адрес пересылается как последний аргумент функции.
При возврате, до настоящего возврата происходит получение значение этого аргумента и происходит копирование содержания локального массива подлежащего передачи в скрыто переданный массив (Если у нас функций с переменном числом аргументов, то сначала вычисляется адрес аргумента: получаем кол-во аргументов при вызове и прибавляем к значению фрейма столько, что наш адрес указывал на последний аргумент). А в реальности функция возвращает просто 0.
От теории к практики
Для того, чтобы было легче понять выше сказанное, приведу примеры и пояснения.
Фрейм - адрес в памяти (внутри стека), который указывает на место между локальными переменными и сохраненный информацией о старом фрейме.
Помним что стек расположен перевернуто в памяти!
Локальные переменные
Мы создаем новую локальную переменную - разберем что значит "создаем":
Локальные переменные создаются на вершине стека.
Во-первых рассмотрим обычные переменные (не массивы):
Раньше их было два вида, сейчас один, но рассмотрим оба.
1) Не инициализированные.
(Кто еще не понял сути "decl" надеюсь сейчас разберется.)
У них нет значения, которому они должны быть равны после создания ("инициализированы"), значит нам надо лишь сместить вершину стека на размер нашей переменной.
В байткоде это выглядит как
Внимание! Сложите в голове пазл: стек перевернут, вершина его движется справа-налево (если представлять память как линию с началом слева и концом справа). А следовательно для увеличения размера стека его вершина должна сместиться влево, где адрес меньше. Следовательно - при увеличении стека мы вычитаем, а при уменьшении прибавляем.
(инструкция stack прибавляет переданное значение к стеку, т.е. sp + -4 = указатель стека сместился влево и мы "выделили" память переменной размером 4 байта)
2) Инициализированные
У них передается значение для установки переменной, поэтому мы можем использовать инструкцию для внесения значения в стек, она автоматически изменит значение вершины стека.
в байткоде выглядит как
Заметьте, что SP использует концепцию "зануления", т.е. при отсутствии явного указания значения для инициализации, инициализирует значением 0.
Закрепим разницу:
Думаю, что к массивам, мы пока обращаться не будем.
Теперь произведем синтез фрейма с локальными переменными. Собственно локальными их называют, т.к. у них вместо адреса в памяти смещение от фрейма до начала переменной.
Используя последний пример с тремя переменными:
Адрес myvar = -4;
Адрес myvar2 = -8;
Адрес myvar3 = -12;
Так же помните, что переменные могут иметь область существования и внутри функции, к примеру в if условии или цикле. Т.е. в разные моменты времени по одному и тому же адресу будут разные переменные:
myvar2 и myvar3 имею одинаковый адрес = -8.
Освобождение памяти aka. разрушение переменных происходит смещением вершины стека.
Аргументы вызова
Собственно аргументы не так сильно отличаются от локальных переменных. Они расположены справа от фрейма и имеют положительные смещения, но важно не забывать о информации о старом фрейме и количестве переменных. Мы должны пропустить 3 ячейки (0, адрес старого фрейма и переменная с количеством аргументов), получаем, что смещение первого аргумента 12 байт, второго 16 байт, и т.д.
Теперь посмотрим как выглядит вызов в байткоде (к примеру вызов функции FillAmmoToMax их функции OnPlayerSpawn из примера выше):
Возврат значения происходит всегда, даже когда явно не указано!
При "void", или "return;", или отсутствии любого return в функции, он туда все равно добавляется и возвращает 0.
Набор инструкций
Подробнее о инструкциях вы можете прочитать в
Байт-код SourcePawn (Справочник)
Заключение
Виртуальная машина SourcePawn не является в полной мере копией вымышленного процессора.
Контекст - набор данных, совокупность которых образует сам объект чего-либо как таковой. В данном случае контекст плагина.
Контекст плагина состоит из:
- кода
- памяти (memory)
- указателя стека (stack pointer)
- указателя кучи (heap pointer)
- указателя стекового кадра aka. фрейм (frame pointer)
- сохраненное значение указателя стека (для проверки на утечку)
- сохраненное значение указателя кучи (для проверки на утечку)
Все указатели являются адресами в памяти плагина.
Виртуальная память
Память условно разделяется на две части:
1) данные
2) память (немного тавтологии):
a) Куча
б) Стек
Данные, расположены начиная от начала памяти плагина. Размер этой секции определяется компилятором во время процесса компиляции и записан в smx. В секции содержатся глобальные и статические локальные переменные, константные массивные значения используемые в плагине. Так как этот раздел памяти начинается с самого начала памяти, то адреса памяти и адреса глобальных переменных плагина совпадают.
Память, разделена на две составляющие: кучу и стек. Раздел памяти начинается сразу после конца раздела данных. В начале располагается куча, а за ней стек.
Размер памяти выделяемой плагину определяется при компиляции, вот пример лога компиляции:
C-подобный:
SourcePawn Compiler 1.8.0.6041
Copyright (c) 1997-2006 ITB CompuPhase
Copyright (c) 2004-2015 AlliedModders LLC
Code size: 3568 bytes
Data size: 2400 bytes
Stack/heap size: 16384 bytes
Total requirements: 22352 bytes
Куча предназначена для хранения временных переменных и динамических массивов.
На временные переменные ни как нельзя повлиять кодом, т.к. они создаются компилятором для нужд исполнения кода, и разрушаются сразу же как исполнении свою функцию. К примеру если вы используете вызов функции, которая возвращает массив или передаете константное значение в роли массива, который может быть изменен при вызове.
Стек вызовов
Сначала, коротко о том что такое стек, для тех кто не в теме, можно прочитать в Мини-уроки: N. Стек (ожидается).
Стек в виртуальной машине SourcePawn не является типичным стеком вызовов, так как не хранит адрес возврата (адрес функции из которая была вызвана текущая и куда надо вернуть управление по завершению текущий функции) или значения регистров (каждая функция может использовать регистры, и при этом изменять их значения. Но т.к. функция, вызвавшая текущую могла хранить какую-нибудь важную информацию в них, то перед использованием регистров их старые значения сохраняются в стек при вызове, а в конце вызова восстанавливаются), а содержит только данные стековых кадров.
Стековый кадр состоит из:
- аргументов вызова функции
- количества параметров при вызове
- информации о старом кадре
- указатель предыдущего кадрового ферейма
- ранее просто значение 0 (ноль), сейчас старый указатель кучи (изменения еще не были добавлены в стабильную версию SM).
- локальных переменныхПриведу пример для наглядности.
Имея следующий код:
PHP:
public void OnPlayerSpawn(int client, int team)
{
int myweapon = GetClientWeapon(client); // функция вернула 307
if (myweapon != -1)
{
FillAmmoToMax(myweapon);
}
}
// Ну скажем у нас модификация где нет обойм у оружия
void FillAmmoToMax(int weapon)
{
int ammo = GetAmmo(weapon); // функция вернула 294
int maxammo = GetMaxAmmo(weapon); // функция вернула 400
if (ammo < maxammo)
{
// В этот момент мы решили посмотреть состояние стека, с.м. ниже
SetAmmo(weapon, maxammo);
}
}
Вначале указан адрес ячейки в HEX, от начала стека.
PHP:
/* Здесь и выше, свободная память стека */
/* Конец кадра FillAmmoToMax */
/* Конец локальных переменных кадра */
0x80: 400 // переменная maxammo
0x7С: 294 // переменная ammo
/* Начало локальных переменных кадра */
/* Конец информации о старом кадре */
0x78: 0 // просто 0
0x74: 0x64 // указатель кадра вызвавшей функции
/* Начало информации о старом кадре */
/* Конец аргументов вызова */
0x70: 1 // кол-во аргументов при вызове
0x6C: 307 // weapon аргумент
/* Начало аргументов вызова */
/* Начало кадра FillAmmoToMax */
/* Конец кадра OnPlayerSpawn */
/* Конец локальных переменных кадра */
0x68: 307 // переменная myweapon
/* Начало локальных переменных кадра */
/* Конец информации о старом кадре */
0x64: 0 // просто 0
0x60: ? // указатель кадра вызвавшей функции
/* Начало информации о старом кадре */
/* Конец аргументов вызова */
0x5C: 2 // кол-во аргументов при вызове
0x58: 1 // client аргумент
0x54: 2 // team аргумент
/* Начало аргументов вызова */
/* Конец кадра OnPlayerSpawn */
/* И так далее... */
Важно! Стек вызовов виртуальной машины SP расположен в памяти перевернуто.
Адреса в примере не соответствуют реальности и даны лишь для примера.
Т.е. Он начинается от конца памяти и идет к началу.
Подробнее в Википедия:Стек вызовов.
Контекст вызова,
Интерпретатор
Теперь коротко почему в стеке не хранится адрес возврата и регистры, и где они прячутся.
Это связано с реализацией передачи вызовов в самой виртуальной машине.
Вызов функций происходит через контекст плагина. При вызове инициализируется интерпретатор. Он служит для обработки и реализации инструкций (байткода).
Контекст каждого интерпретатора содержит:
- регистры
- статус исполнения (произошел ли возврат)
- указатель на следующую инструкцию для чтения
- указатель на контекст плагина
Это не все, но остальное не столь важно.
Если интерпретатор встречает вызов функции, то он инициализирует новый интерпретатор для чтения этой функции и передает ему управление, до тех пор как не произойдет возврат. И т.к. контексты каждого вызова не зависимы, нет нужны сохранять значения адреса возврата или регистров в стеке вызовов.
По возврату интерпретатор вызывающей функции копирует значение возврата из интерпретатора вызываемой и разрушает его.
Если с внутренними вызовами функции внутри плагина все выглядит вполне понятно, то остается понять как происходит "начальный вызов" (когда в нашем плагине вызывается форвард или обработчик) или же вызов функций внутри других плагинов при использовании Call_* функций.
Для этого у контекста плагина есть функция Invoke. Она порождает первоначальный интерпретатор и запоминает начальное значение фрейма в этот момент (invoke frame).
Так же, для передачи данных между плагинами, и корректным доступом к аргументом, по необходимости выделяет в куче память для аргументов, и по завершению копирует их значения в источник вызова.
Регистры
Если я не ошибаюсь в терминологии, то виртуальную машину SP правильно назвать регистровой машиной (еще бывают стековые), т.к. основные операции производятся внутри регистров.
Регистры представляют собой ячейки (англ. cell) - переменные размером 4 байта.
Существует два регистра:
- Основной регистр (англ. primary, сокр. pri)
- Вспомогательный регистр (англ. alternative, сокр. alt)
О том в каких случаях какие регистры используются, см. Набор инструкций (байткод) SP ниже в статье.
Соглашение о вызовах
Обычный вызов функции
Вызовы функций происходят по их адресу в ".code" секции, которая предварительно было скопирована из smx в контекст плагина.
Если коротко:
1) Передача аргументов
Производится через стек в обратном порядке.
2) В стек заносится кол-во аргументов
3) Сохранение значений строго фрейма в стеке и установка текущий вершины стека как текущего фрейма.
4) Локальные переменные и память выделяемая в куче должны быть освобождены вызванной функций создавший их до возврата, иначе получите ошибку (утечка стека - "stack leak" или утечка кучи - "heap leak").
5) При возврате, возвращаемое значение заносится в основной регистр.
6) Вызывающая функция производит очистку стека от аргументов, т.к. их количество было передано при вызове.
Вызов с возвратом массивов (частный случай обычного вызова)
По сути происходит обычный вызов, со скрытой передачей дополнительного аргумента.
Во-первых, на стадии компиляции, та функция что возвращает массив сообщает размер того массива что нужно вернуть (возвращать можно только массивы статического размера).
При вызове функции создается временная перемененная в куче такого же размера как и возвращаемый массив. Её адрес пересылается как последний аргумент функции.
При возврате, до настоящего возврата происходит получение значение этого аргумента и происходит копирование содержания локального массива подлежащего передачи в скрыто переданный массив (Если у нас функций с переменном числом аргументов, то сначала вычисляется адрес аргумента: получаем кол-во аргументов при вызове и прибавляем к значению фрейма столько, что наш адрес указывал на последний аргумент). А в реальности функция возвращает просто 0.
От теории к практики
Для того, чтобы было легче понять выше сказанное, приведу примеры и пояснения.
Фрейм - адрес в памяти (внутри стека), который указывает на место между локальными переменными и сохраненный информацией о старом фрейме.
Помним что стек расположен перевернуто в памяти!
Локальные переменные
Мы создаем новую локальную переменную - разберем что значит "создаем":
Локальные переменные создаются на вершине стека.
Во-первых рассмотрим обычные переменные (не массивы):
Раньше их было два вида, сейчас один, но рассмотрим оба.
1) Не инициализированные.
(Кто еще не понял сути "decl" надеюсь сейчас разберется.)
У них нет значения, которому они должны быть равны после создания ("инициализированы"), значит нам надо лишь сместить вершину стека на размер нашей переменной.
PHP:
decl myvar;
PHP:
stack -4
(инструкция stack прибавляет переданное значение к стеку, т.е. sp + -4 = указатель стека сместился влево и мы "выделили" память переменной размером 4 байта)
2) Инициализированные
У них передается значение для установки переменной, поэтому мы можем использовать инструкцию для внесения значения в стек, она автоматически изменит значение вершины стека.
PHP:
new myvar = 6;
new myvar2;
PHP:
push.c 6
push.c 0
Закрепим разницу:
PHP:
new myvar;
decl myvar2;
new myvar3 = 777;
// байткод
push.c 0
stack -4
puch.c 777
Думаю, что к массивам, мы пока обращаться не будем.
Теперь произведем синтез фрейма с локальными переменными. Собственно локальными их называют, т.к. у них вместо адреса в памяти смещение от фрейма до начала переменной.
Используя последний пример с тремя переменными:
Адрес myvar = -4;
Адрес myvar2 = -8;
Адрес myvar3 = -12;
Так же помните, что переменные могут иметь область существования и внутри функции, к примеру в if условии или цикле. Т.е. в разные моменты времени по одному и тому же адресу будут разные переменные:
PHP:
int myvar;
if ()
{
int myvar2;
}
int myvar3;
Освобождение памяти aka. разрушение переменных происходит смещением вершины стека.
PHP:
stack 8 // освобождаем память от 2х пременных
Аргументы вызова
Собственно аргументы не так сильно отличаются от локальных переменных. Они расположены справа от фрейма и имеют положительные смещения, но важно не забывать о информации о старом фрейме и количестве переменных. Мы должны пропустить 3 ячейки (0, адрес старого фрейма и переменная с количеством аргументов), получаем, что смещение первого аргумента 12 байт, второго 16 байт, и т.д.
Теперь посмотрим как выглядит вызов в байткоде (к примеру вызов функции FillAmmoToMax их функции OnPlayerSpawn из примера выше):
PHP:
public void OnPlayerSpawn(int client, int team)
{
int myweapon = GetClientWeapon(client); // функция вернула 307
if (myweapon != -1)
{
FillAmmoToMax(myweapon);
}
}
void FillAmmoToMax(int weapon)
PHP:
push.s -4 // передаем значение myweapon как аргумент вызова weapon
push.c 1 // передаем кол-во аргументов при вызове
call 2408 // производим вызов функции по адресу 2408, в нашем случае это FillAmmoToMax
// По завершению вызова аргументы уже будут отчищены из стека а pri регистр содержать вернувшиеся значение.
При "void", или "return;", или отсутствии любого return в функции, он туда все равно добавляется и возвращает 0.
Набор инструкций
Подробнее о инструкциях вы можете прочитать в
Байт-код SourcePawn (Справочник)
Заключение
Виртуальная машина SourcePawn не является в полной мере копией вымышленного процессора.
Последнее редактирование: