Автор:
mOleg
Дата публикации: 2008-11-26
публикуется впервые
Скрипты и плагины в Форте
Аннотация
В статье предлагается размышление о том, как можно реализовать в форт-программах скрипты<sup>
1</sup> и плагины<sup>
2</sup>, то есть куски исходного текста и бинарного кода, создаваемые не разработчиком программы, а ее пользователем для получения дополнительной функциональности уже готовой программы. Так как обычно программы, написанные на форте, уже содержат в себе компилятор<sup>
3</sup>, а так же, обычно, обращаются к функциям по их именам, такая идея кажется очень удобной и перспективной, даже присущей Форту. Часто создатель программы не может предусмотреть всех возможных направлений использования программы, а так же ряда удобств, которые реализовать очень просто пользователю, не отвлекая программиста. Однако, при реализации такого подхода, если не забывать об устойчивости программы (то есть скрипты и плагины не должны «ронять» работающую программу), а так же о безопасности (чтобы кто-то не написал деструктивный код в виде скрипта), возникают довольно серьезные проблемы.
Введение
Форт устроен очень удобно с точки зрения встраивания новой функциональности в уже существующие программы, то есть на первый взгляд очень легко и естественно можно реализовать скрипты. В реальности же все не так гладко, потому что некорректное использование ряда слов<sup>
4</sup> гарантированно приводит к критическому завершению работы программы. В ряде случаев форт слишком открыт для вмешательств, что одновременно является и его сильной стороной и его большой слабостью.
На данный момент времени все Форты (виденные мною) устроены по уязвимой схеме, в которой данные, код, различные критические внутренние структуры доступны вмешательству, поэтому могут быть «исправлены» или запорчены случайно в результате ошибки или преднамеренно, в том числе злонамеренно. Это одновременно и хорошо, если управляемо, и плохо, так как невозможно обезопасить скрипты. Из-за стремления к быстродействию, нежелания реализовывать сложные схемы работы с памятью, а так же, зачастую, неизбежного наследования модели памяти, предлагаемой операционной системой, под которой работает форт-программа, делать скрипты в форте не так просто, как может показаться на первый взгляд. Существуют методики создания безопасных интерфейсов, но все они имеют свои ограничения и проблемы.
Анализ
Наиболее опасными словами в Форт-системе, а значит и в программе, в первую очередь являются слова для работы с памятью: @ ! CMOVE и подобные им; после них, конечно же, идут системные переменные и слова для работы с ними, например: S0 R0 SP! RP! DP LAST и им подобные; слова, работающие со стеком возвратов: >R R> RDROP R@ и т.п.; а так же слова или конструкции слов, передающие управление на указанный адрес (а не на фиксированную метку): EXECUTE CATCH и подобные, либо пары слов: >R EXIT и т.п., а так же RECURSE. Соответственно, за ними идут слова, работающие с файлами. Не стоит забывать о потенциальной опасности переполнения стеков, утечек памяти (что критично в программах, которые должны работать непрерывно на протяжении длительного времени), а так же потенциальной опасности несовместимости некоторых слов (то есть такая последовательность «безопасных» слов, которая приводит к исключению, например, деление на нуль).
Стоит отметить, что плагины в форте удобно представлять их исходными текстами (то есть скриптами), так как скорость компиляции кода очень высока, а плагины редко бывают очень большими. В то же время бинарные плагины лучше оформлять как пару: dll-библиотека + инициализирующий исходный текст, который подключит dll, импортирует нужные функции, произведет необходимые инициализирующие действия и передаст управление на необходимый код. Однако же, контролировать dll мы не в состоянии, поэтому этот вариант дальше в тексте рассматриваться не будет. Реализация же поддержки бинарного кода внутри форт-системы вещь редкая и достаточно нетривиальная (я знаю только одну такую форт систему SMAL32) поэтому дальше будут рассматриваться только скрипты.
Итак, первый вывод: при необходимости обеспечения надежной работы программы, поддерживающей механизм скриптов, необходимо изолировать опасные функции, имеющиеся в словарях программы от скриптов.
Второе: при работе с файлами необходимо контролировать действия плагина, например, не давать открывать dll и исполнимые файлы, не давать возможности выходить за пределы определенных каталогов, не давать открывать самостоятельно файловые потоки и тому подобное.
Третье: так как обойтись при решении более-менее серьезной задачи без переменных и массивов в памяти (в том числе динамических) невозможно, необходим некий набор слов для работы с памятью, не позволяющий залазить в системные области программы.
Четвертое: все интерфейсные слова необходимо стараться делать таким образом, чтобы они поглощали и возвращали значения, а не ссылки на них. То есть, надо учиться обходиться без реальных адресов.
Пятое: не желательно предоставление возможности подключения внешних динамических библиотек, потому что их невозможно контролировать. То есть скрипты должны рассчитывать только на возможности, предоставляемые базовой программой.
Изоляция опасных слов
Изолировать опасные функции от плагина можно в отдельный словарь, который при сборке и работе плагина только и будет доступен. Для SPF это может выглядеть так:
<pre>
\ создаем словарь, в котором будут находиться общие для всех плагинов слова
VOCABULARY Interface
\ создаем словарь, в котором будут находиться все плагины
VOCABULARY Plugins
\ создается словарь, определяемый именем плагина
\ (вероятно это будет имя файла, содержащего исходный текст плагина,
\ но в данном случае имя плагина будет идти за определяющим словом Plugin:
: Plugin: ( / name --> )
ALSO Plugins DEFINITIONS \ в словаре Plugins
VOCABULARY \ создается словарь с именем name
ONLY Interface \ оставляется в контексте только интерфейсный словарь
ALSO \ поверх интерфейсного словаря
VOC-LIST @ CELL + CONTEXT ! \ кладется созданный словарь name
DEFINITIONS \ и делается текущим
;
\ добавляем слова, доступные любому плагину, то есть: создаем интерфейс
ALSO Interface DEFINITIONS
: WORDS WORDS ; \ в данном случае создается заголовок в словаре Interface
: BYE TERMINATE ; \ для уже существующих в словаре FORTH слов.
: ORDER ORDER ;
: : : ;
: ; [COMPILE] ; ; IMMEDIATE
\ …и другие слова, доступные плагину…
PREVIOUS DEFINITIONS
\ Теперь можно попробовать посмотреть что получилось:
Plugin: test
ORDER \ enter –
\ должен показать в контексте два словаря: Interface test и
\ test текущим словарем, то есть словарем, в который будет в дальнейшем вестись
\ компиляция
BYE
\ позволит нам завершить процесс плагина
</pre>
Впрочем, каждый скрипт должен работать в собственном потоке, что удобно уже потому, что завершение потока не приведет к завершению программы. Так же удобно автоматическое освобождение блоков памяти, занятых в хипе при завершении процесса. Поэтому так же необходимо предусмотреть некий механизм, инициализирующий контекст таким образом, чтобы в нем были только необходимые для работы словари, и, соответственно, слова. Кроме того, если предполагать, что скрипт может быть завершен принудительно, либо в результате ошибки, либо после выполнения предназначенных действий самостоятельно, стоит позаботиться об удалении кода скрипта, словаря, в котором этот скрипт находится. Перечисленные действия реализовать, например, в SPF не так уж и просто, поэтому в рамках данной статьи они рассматриваться не будут. Компилирующие структуры должны использовать изолированный стек, не давая перехватить и подменить адреса переходов.
Изоляция адресного пространства
Изолировать пространство данных процесса тоже достаточно просто на первый взгляд: надо выделить память в хипе, необходимого размера (этот размер может задавать сам скрипт), и работать с этой памятью, как с именованным массивом, при этом обязательно контролировать выход за пределы массива (либо просто ограничивать адресуемую область). Например, если базовый адрес памяти хранится в переменной Memory , а размер этой памяти в переменной Limit , можно переписать слова @ ! следующим образом:
<pre>
: ! ( n addr --> )
Limit OVER > THROW \ если хочется контролировать выход за диапазон, либо:
\ Limit UMIN \ если хочется ограничить выход за предел диапазона
Memory + \ получить реальный адрес переменной
! \ сохранить значение
;
</pre>
Кстати, переменная Limit должна быть на один CELL меньше, реально выделенной памяти. Точнее, реально выделенная память должна быть на один CELL больше запрашиваемой, которая будет храниться в Limit, либо придется учитывать размерность данных, записываемых в массив, чтобы не выйти за его границы при работе с крайними адресами.
Аналогично будет выглядеть слово @ :
<pre>
: @ ( addr --> n )
Limit UMIN \ обязательно беззнаковый, чтобы не нарваться на отрицательные адреса.
Memory +
@ ;
</pre>
Пересылки будут выглядеть чуточку сложнее, но для иллюстрации идеи достаточно рассмотрения слов @ и ! . Проблема только в том, что постоянная проверка выхода за адресуемый диапазон отнимает время, но с потерей производительности в любом случае придется мириться.
В принципе, похожий механизм уже используется в системе при работе с USER переменными, однако,
USER переменные возвращают реальные адреса, с которыми работает классический @ и ! , что нас никоим образом не устраивает, хотя можно ограничиться только USER-VALUE переменными. К тому же USER область по умолчанию не очень велика, и содержит много не нужных и даже опасных данных, например, переменные S0 R0 .
Кроме того, как уже отмечалось, код самого скрипта необходимо компилировать не в базовую область программы, а в какое-то отдельное адресное пространство (в SPF для этого можно использовать TEMP-WORDLIST). Так же не стоит забывать о том, что одновременное компилирование нескольких слов в память форт может не поддерживать, по крайней мере, в SPF этого делать нельзя даже во временные словари.
Другие варианты
Перечисленные методики обладают неприятным качеством – они ограничивают возможности скрипта, методики работы в рамках форт-системы, то есть требуют создания некоего специфичного проблемно-ориентированного языка с ограниченной функциональностью, что не всегда приемлемо. То есть получившийся язык не будет обладать гибкостью классического форта и будет иметь другой, пусть и близкий, набор слов (наследование традиционных имен может привести к путанице). Возможным выходом из данного положения видится создание защищенной виртуальной машины и форта на ее основе, который бы просто не давал возможности выходить на системный уровень и не давал непредвиденным заранее образом влиять на базовую программу, либо такая машина должна подключаться к готовой программе. Однако же, вся гибкость, мощьность, свобода форта не обязательно необходимы при создании интерфейсов для скриптов (или плагинов), поэтому без специфичных ВМ все-таки можно обойтись в большом количестве случаев.
1. исходные тексты, подключаемые средствами программы, позволяющие автоматизировать некую задачу, которую без сценария пользователь делал бы вручную, используя интерфейс программы.
2. независимые бинарные модули, подключаемые к программе через специализированный интерфейс
3. очень редко форт содержит в себе только код, относящийся к программе, обычно форт-программа наследует компилятор, на котором она создана
4. в современном программировании принято программу разделять на небольшие куски, так называемые подпрограммы или функции – в форте такие куски принято называть определениями или словами