Данный материал родился после прочтения в "ИНФО" статьи Ю.Гиматова [1]. Статья содержит целый ряд интересных идей и примеров, хотя некотрые из них применимы не всегда. В частности, самомодификация кода программ действительно встречается на практике редко, но отнюдь не по причине "недогадливости" программистов. Можно привести ряд серьезных возражений против такого приема: принципиальная невозможность работы формирующей себя описанным способом программы в ПЗУ, нарушение процесса проверки с помощью контрольной суммы, получение при определенных условиях неидентичных копий на магнитных носителях для одной и той же программы, наконец, значительное ухудшение "читаемости" программы, что в большинстве случаев нежелательно. Впрочем, приводимые аргументы направлены не столько на критику изложенных в [1] идей, сколько против их абсолютизации.
Легко получить компромиссное решение, которое, сохранив идею самомодифицирования, свободно от перечисленных недостатков. Для этого формировать изменяющуюся часть кода программы следует не
внутри нее самой, а в некоторой рабочей области ОЗУ в виде подпрограммы, которая в дальнейшем вызывается обычным образом. Идея эта достаточно проста и в более детальном обсуждении не нуждается.
Существует и другой интересный способ реализации самомодифицирующейся программы: он далеко не так очевиден, но компактен и логически настолько красив, что, по-моему, вполне заслуживает
отдельного рассмотрения на страницах журнала. Для реализации цикла, в теле которого при различных проходах выполняются различные действия, использована идея соподпрограмм. Подчеркну, что такая программная конструкция существует только для процессоров с системой команд типа "PDP-11" с их развитой системой адресации: в "INTEL"-процессорах и тем более в языках высокого уровня такая возможность не предусмотрена!
Что такое соподпрограмма? Это особый тип организации программы, когда две ее части ПО ОЧЕРЕДИ вызывают одна другую (см., например, [2]). Как известно, в классическом случае основная программа вызывает подпрограмму, но не наоборот. Поскольку обе соподпрограммы функционально равноправны, то инструкция возврата RET в них не используется: вместо нее одна соподпрограмма просто вызывает другую и наоборот. Любители аналогий здесь могут представить себе двух баскетболистов, перекидывающих друг другу мяч, но при этом по неизвестной причине всегда может бежать только спортсмен, владеющий мячом (другой же замирает в точке передачи мяча).
Инструкция вызова соподпрограммы имеет вид
JSR PC, @(SP)+
и кодируется всегда восьмеричным словом 004736. При выполнении этой команды процессор сначала извлекает из стека адрес возврата в альтернативную соподпрограмму (о чем говорит запись операнда
в виде
@(SP)+ ) и сохраняет в своей внутренней памяти. Затем, как и при обычном вызове подпрограммы, в стек аппаратно заностится адрес следующей за
JSR PC инструкции и осуществляется переход по считанному ранее адресу. Нетрудно заметить, что рассматриваемая модификация команды отличается от "классического" применения инструкции
JSR только тем, что адрес начала подпрограммы берется из стека. Такой прием как раз и позволяет передать управление по адресу альтернативной соподпрограммы и сохранить в стеке адрес для последующего возврата оттуда. Для обратного перехода в исходную соподпрограмму достаточно проделать в точности те же самые действия, т.е. снова выполнить ту же инструкцию процессора.
Естественно, что для обеспечения нормальной взаимосвязи соподпрограмм необходимо перед их использованием входной адрес второй соподпрограммы занести в стек. После окончания работы с
соподпрограммами ненужный "этаж" стека следует освободить.
Описанная программная конструкция применяется довольно редко и кажется достаточно экзотической. Тем не менее, на ее основе удается очень легко реализовать цикл с модифицируемыми командами: тело цикло в этом случае служит одной соподпрограммой, а в роли другой выступают подставляемые в него по очереди команды, разделенные инструкциями JSR PC, @(SP)+ .
Для иллюстрации рассмотрим следующую наглядную задачу, аналогичную примеру 2 в статье [1]. Пусть нам необходимо обойти все точки расположенного на экране прямоугольника и закрасить их. Сделаем это, однако, не традиционным построчным сканированием, а более эффектным обходом по "закручивающейся спирали", т.е. сначала нарисуем внешнюю границу прямоугольника, затем границу прямоугольника, оставшегося незакрашенным и т.д.
Обозначим координаты левого верхнего угла прямоугольника X1 и Y1, а правого нижнего - X2 и Y2. Тогда
X2 = X1 + DX, Y2 = Y1 + DY,
где
DX и
DY - длины сторон прямоугольника. Входные параметры
X1 и
X2 поместим в R1 и R2, а
DX и
DY - в R4 и R5 соответтвенно. При работе программы в R3 будет храниться адрес небольшого массива из четырех чисел:
X1, Y1, X2, Y2. Чтобы не использовать конкретные адреса памяти, массив разместим в стековой области (правда, задавать его при этом придется в обратном порядке!). Регистр R4 будет служить счетчиком точек.
Текст программы на ассемблере "БК-0010" и результирующие коды с подробными комментарями приведены в приложении. Основная часть программы состоит из подготовительных вычислений и короткого основного цикла, начинающегося после метки L2. Для каждой точки вызывается стандартная процедура закраски EMT 30 (подробности ее работы описаны, например, в [3]) и происходит обращение к соподпрограмме NEXTXY, возвращающей в R1 и R2 координаты следующей точки спирали. NEXTXY, пожалуй, является наиболее интересной частью программы. Первоначально вход в нее соответствует метке M2, адрес которой предусмотрительно заранее был занесен в стек. Пока текущее значение X не совпадет с X2, новая точка получается увеличением на единицу абсциссы, хранящейся в регистре R1. Когда X=X2, вход в соподпрограмму перемещается на метку M4, что при последующих обращениях приводит к сохранению прежнего значения X и инкрементированию Y вплоть до точки Y=Y2. Аналогично происходит движение и по двум оставшимся сторонам прямоугольника, с той лишь разницей, что координаты при этом уменьшаются. После того, как нарисован полный прямоугольник, его вершины сдвигаются "внутрь" и процедура повторяется.
Корректность окончания процесса существенно зависит от соотношения сторон прямоугольника. Если DY > DX, то в случае нечетного количества точек по координате X остается незакрашенная вертикальная линия и общий алгоритм закраски нарушается. Чтобы исправить положение, в конце NEXTXY ставится специальная проверка, которая в случае X1=X2 гарантирует нормальное завершение.
Соподпрограмма NEXTXY может быть использована для смещения по спирали и в более сложных случаях, причем ее вызов можно помещать в нескольких местах программы. С ее помощью, например, легко определить координаты точки, расположенной на середине спирали и т.п.
Приводимая программа является полностью переносимой, т.е. может работать без изменений при загрузке с произвольного адреса ОЗУ. Для заданных в ее тексте параметров, на экране рисуется прямоугольник с вершинами в точках (8,8) и (64,128). Разумеется, заменив константы в первых четырех командах, размеры и местоположение изображения легко изменить.
Видеоэффект закраски по спирали любопытен и сам по себе, так что его можно использовать как элемент более сложных программ, в том числе и на Бейсике.