УДК 681.3.06
Г.В. Борзов, М.М. Ляпунов
ПРОГРАММНЫЕ ТРЮКИ НА МАКРО-11
Трюками мы называем необычные приёмы программирования. Трюк должен быть лаконичным и давать выигрыш в быстродействии или в объёме программы. Каждый трюк несёт в себе элемент неожиданности, например, использует команду очистки для передачи управления или наоборот.
Отношение к трюкам может быть различным. Некоторые уравновешенные люди признают, что в этом что-то есть, но избегают фокусов, дабы не усложнять жизнь. Другие видят в трюках эстетические свойства и восхищаются ими настолько, что забывают о назначении программы. Третьи находят прямую связь между трюками, трюкачеством и «бит-жонглёрством» и считают все это безусловно вредными привычками плохо воспитанных программистов. Четвертые воспринимают трюк как анекдот для роботов (или для тех программистов, которые в своём развитии (деградации) достигли уровня роботов).
Вероятно, в каждом из этих мнений есть что-то от истины. Мы считаем, что к месту употреблённый трюк, снабжённый, когда надо, комментариями, ничего, кроме пользы, принести не может. Некоторые трюки, входя в обиход, становятся привычными и воспринимаются как нормальные приёмы программирования. В большинстве случаев чистый выигрыш от них невелик, но бывают ситуации, когда время работы программы жёстко ограничено, и только трюк может спасти положение. Наконец, знакомство с трюками полезно и тем, кто их не использует, так как позволяет глубже осознать особенности ЭВМ и чувствовать себя свободнее.
Из-за своей необычности трюки, как правило, реализуются только на языках ассемблерного типа или непосредственно в машинных кодах. Некоторые трюки для машин с системой команд «Электроники 60» описаны ниже. Часть из них мы обнаружили в недрах операционных систем, часть изобрели сами (вероятно, не впервые).
Вокруг С-разряда
1. Выход из подпрограммы.
Часто бывает удобно покидать подпрограмму с признаком успешного или неуспешного её завершения в С-разряде. Обычно конец такой подпрограммы имеет вид
12$: CLC ; УСПЕШНОЕ ЗАВЕРШЕНИЕ
RETURN
13$: SEC ; НЕУСПЕШНОЕ ЗАВЕРШЕНИЕ
RETURN
Одну команду здесь можно сэкономить:
12$: TST (PC)+ ; УСПЕШНОЕЗАВЕРШЕНИЕ
13$: SEC ; НЕУСПЕШНОЕ ЗАВЕРШЕНИЕ
RETURN
В данном случае команда SEC используется в качестве операнда команды TST, которая сбрасывает С-разряд и передаёт управление команде RETURN (RTS PC).
2. BLO = BCS, BHIS = BCC.
Эти пары команд эквивалентны полностью. Точнее говоря, для компьютера это не четыре команды, а только две, каждой из которых в МАКРО-11 соответствует по две мнемоники. Для программиста естественно употреблять BLO и BHIS после команд сравнения беззнаковых двоичных чисел (например, адресов), а ВСС и BCS - в том случае, когда явно анализируется С-разряд. Используя эквивалентность мнемоник, иногда достаточно поставить операнды в командах сравнения в правильной последовательности, чтобы заметно сократить размер программы. Подпрограмма из следующего примера получает на входе в R0 код символа ASCII и выясняет, является ли этот символ буквой, т.е. лежит ли его код в одном из диапазонов 101...132 (A...Z) и 140...176 (Ю...Ч). Результат возвращается в С-разряде (0 - буква, 1 - не буква):
LETTER: CMP R0,#’A
BLO 99$
CMP #’Z,R0
BHIS 99$
CMP R0,#'Ю
BLO 99$
CMP #'Ч,R0
99$: RETURN
3. Сохранение в стеке и восстановление С-разряда может быть выполнено командами
ROL -(SP)
и
ROR (SP)+
4. Команды, не изменяющие С-разряд.
Тот факт, что команды MOV, BIC, INC и некоторые другие не изменяют значения С-разряда, может послужить причиной их нестандартного применения. Так, завершение подпрограммы из примера 1 может быть развито следующим образом:
12$: TST (PC)+ ; УСПЕШНЫЙ ВЫХОД (С=0)
13$: SEC ; НЕУДАЧА (С=1)
MOV (SP)+,R0 ; ВОССТАНОВЛЕНИЕ R0
MOV (SP)+,R1 ; И R1 ИЗ СТЕКА
BIC R2,R2 ; ОЧИСТКА R2
INC (SP)+ ; ПРОДВИЖЕНИЕ УКАЗАТЕЛЯ СТЕКА
RETURN
Для продвижения указателя стека на два слова можно использовать команду
MOV (SP)+,(SP)+
Она не меняет С-разряда, но устанавливает признаки N и Z, поэтому её можно применить для анализа знака числа в представлении с плавающей запятой, расположенного в стеке, и при этом удалить его из стека.
5. С-разряд и прерывания.
При выполнении процедуры прерывания С-разряд получает новое значение из младшего разряда второго слова вектора прерывания. Это обстоятельство используется в программе, которая подсчитывает число тактов в 32-разрядном слове:
.ASECT
. =100
.WORD TIMER,341
.PSECT
TIME1: .WORD 0 ; МЛАДШИЕРАЗРЯДЫ
TIME2: .WORD 0 ; СТАРШИЕРАЗРЯДЫ
TIMER: ADC TIME1
ADC TIME2
RTI
Это короче, чем комбинация
TIMER: INC TIME1
BNE 99$
INC TIME2
99$: RTI
Более сложный пример использования С-разряда при прерываниях - программа в разделе «большой трюк» (см. ниже).
Ненормальный JSR
1. Кратное выполнение подпрограмм.
Команда CALL @PC (JSR PC,@PC) передаёт управление непосредственно следующей за ней инструкции, записывая её же адрес в стек, и при этом занимает только одно слово в памяти. Следующий пример показывает, как это применяется для выполнения подпрограммы 2, 4 или вообще 2 в степени N раз:
... ... ...
REPT4: CALL @PC
REPT2: CALL @PC
REPT1: ...
...
RETURN
Управление этой подпрограмме передаётся командой CALL REPT1 (однократное выполнение), CALL REPT2 (двукратное) или CALL REPT4 (четырёхкратное).
2. Как стереть всю память одной командой?
Для этого достаточно записать по адресу 0 команду JSR PC,-(PC) (код 004747), занести в указатель стека адрес конца памяти и запустить компьютер с адреса 0. Команда передаёт управление сама себе за счёт адресации с автоуменьшением PC и каждый раз записывает «адрес возврата» (т.е. число 0) в стек. Когда стек заполнит всю память, он обнулит и ячейку с нулевым адресом, где хранится команда; при очередном цикле процессор прочитает этот нуль, воспримет его как команду HALT и остановится.
Этот трюк напоминает курьёзные шахматные задачи типа «поставить мат в полхода». Как ни странно, нам удавалось употреблять его с пользой - при отладке аппаратуры и в программах начальной загрузки сателлитных микро-ЭВМ.
Есть ещё один похожий трюк, который удалось применить только для проверки степени владения знаниями у изучающих систему команд. Задаётся вопрос: какая команда может переписать сама себя в другую ячейку памяти и передать своей копии управление? Правильный ответ: MOV -(PC), -(PC) (014747).
3. JSR для сохранения регистров.
Пусть имеется несколько однотипных устройств, для которых желательно использовать общую программу обработки прерывания. Адрес входа в эту программу будет присутствовать в первом слове каждого вектора прерывания; для идентификации устройств удобно использовать младшие четыре разряда второго слова каждого вектора, записывая в них номер конкретного устройства. После прерывания этот номер нужно извлечь из младших разрядов слова состояния процессора. Единственное неудобство состоит в том, что для обработки номера требуется регистр, а его значение, естественно, надо предварительно сохранить; если это делать, как обычно, командой MOV RX,-(SP), то эта команда нарушит слово состояния и номер будет потерян. Обходные пути преодоления этой «мелочи» состоят в предварительной записи слова состояния в ячейку памяти или в стек. Существенно лучше (как по затратам памяти, так и по быстродействию) использовать команду JSR RX,@PC, которая не нарушает порядка выполнения команд и занимает только одно слово:
.ASECT
.=VECT1
.WORD INT,341
.=VECT2
.WORD INT,342
.....
INT: JSR R0,@PC
MFPS R0 ; ИЛИ MOV @#177776,R0
BIC #177760,R0
MOV (SP)+,R0
RTI
4. JSR в качестве RTS.
Этот пример можно рассматривать как расширение предыдущего. Подпрограмма SAVERG сохраняет в стеке значения регистров R0...R5 и при этом не нарушает слова состояния процессора. Обращение к подпрограмме производится командой JSR R5,SAVERG:
SAVERC: JSR R4,@PC
JSR R3,@PC
JSR R2,@PC
JSR R1,@PC
JSR R0,@R5
Последняя команда заодно возвращает управление вызывающей программе. Значения регистров сохраняются в стеке, но не сохраняются в самих регистрах; при обработке прерываний это, как правило, и не нужно.
Варианты параллельного ветвления
Пусть в регистре R0 содержится число в диапазоне 0...N и требуется в соответствии с ним передать управление одной из меток L0...LN. Тривиальное решение состоит в (N+1)-кратном повторении пары команд CMP R0,#1 и BEQ L1. При малых N такая программа выглядит нормально; когда N больше пяти, она становится громоздкой и неэффективной. Немногим лучше вариант с повторением пары DEC R0 и BEQ L1 (или BMI L1), а также вариант циклического просмотра таблицы. Предпочтительнее параллельная организация ветвления, когда число команд, выполняемых для анализа R0 и передачи управления, не зависит от значения N. Например:
1. Параллельное ветвление с неявной таблицей переходов;
ASL R0
ADD R0,PC
BR LO
BR L1
...
BR LN
2. Параллельное ветвление с явной таблицей переходов:
ASL R0
JMP @TABLE(R0) ; ИЛИ CALL @TABLE(R0)
TABLE: .WORD L0,L1,...LN
Первый вариант хорош тем, что содержит только позиционно-независимые коды.
3. Рассеянная таблица.
Бывает так, что число N велико, но фактически в диапазоне 0...N только некоторые значения важны; если число в R0 не совпало ни с одним из них, то требуется передать управление на метку NOACT (ничего не делать). Решение, приведённое в предыдущем примере, здесь вполне пригодно, но может потребовать большой таблицы, безыдейно заполненной главным образом словами .WORD NOACT. Положение несколько улучшается, если использовать не одну таблицу, а две, причём во второй (короткой) хранить адреса переходов, в первой - относительные адреса второй. Экономия памяти достигается благодаря тому, что значения относительных адресов малы, и первая таблица составлена не из слов, а из байтов:
MOVB TABLE1(R0),R0
JMP @TABLE2(R0) ; ИЛИCALL @TABLE2(R0)
...
TABLE1: .BYTE 0,0,...A1-TABLE2,0,...
.BYTE A2-TABLE2,0...A3-TABLE2,0...
...
TABLE2: .WORD NOACT
A1: .WORD L1
A2: .WORD L2
...
AK: .WORD LK
В приведённом варианте программы число меток L1...LK не может превышать 63 (десятичных) из-за расширения знака байта во время выполнения команды MOVB. Располагая метку TABLE2 посередине таблицы, можно это число увеличить до 127. Это может основательно испортить внешний вид программы; предпочтительнее следующая запись:
MOVB TABLE1(R0),R0
JMP @TABLE2+200(R0)
; ...
TABLE1:
.IRP LAB,<X,X,.,.A1,X,...A2...>
.BYTE LAB-TABLE2-200
.ENDM
TABLE2:
X: .WORD NOACT
A1: .WORD L1
A2: .WORD L2
...
AK: .WORD LK
Благодаря байтовой организации TABLE1 команда ASL R0 становится ненужной, поэтому потери быстродействия по сравнению с предыдущим вариантом невелики.
Разное
1. Кольцевой буфер
Пусть имеется устройство ввода, поставляющее процессору байты, и пусть требуется обрабатывать прерывание от этого устройства, просто записывая эти байты в кольцевой буфер. Это можно сделать, например, следующим образом;
.ASECT
.=VECTOR
.WORD INT,340
.PSECT
LENGTH=100
RING: .BLKB LENGTH
POINTR: .WORD RING
...
INT: MOVB @#DATARG,@POINTR
INC POINTR
CMP POINTR,#RING+LENGTH
BNE 1$
MOV #RING,POINTR
1$: ...
...
RTI
Как видно отсюда, «закольцовывание» требует четырёх довольно длинных команд на модификацию указателя. При обслуживании нескольких быстродействующих устройств потери времени могут стать слишком большими. Попытка модифицировать указатель, загружая его значение в регистр, не даёт эффекта, так как требует дополнительных затрат на сохранение и восстановление. Предлагаемое ниже решение может показаться «нечестным» и неожиданным:
.ASECT
.=VECTOR
.WORD INT,340
LENGTH=400
RING=1000
.PSECT
POINTR: .WORD RING
INT: MOVB @#DATARG,@POINTR
INCB POINTR ; МОДИФИЦИРУЕТСЯ ТОЛЬКО
; МЛАДШИЙ БАЙТ УКАЗАТЕЛЯ
...
RTI
Применимость этого приёма ограничивается тем, что кольцевой буфер должен иметь длину в точности 256 (десятичных) байтов и должен быть расположен на границе 256-байтового блока.
2. Двойной вход в подпрограмму
Часто в практике программирования возникают две почти одинаковые подпрограммы, отличающиеся какой-либо мелочью, расположенной, к сожалению, где-нибудь в середине алгоритма. В таких случаях естественнее писать не две подпрограммы, а только одну, но с двумя точками входа. Факт передачи управления на ту или иную точку входа отмечается флагом, который в дальнейшем анализируется для ветвления. Следующий способ реализует это довольно экономным образом:
ENTER1: MOV (PC)+,R5
ENTER2: CLR R5
...
TST R5
BEQ ...
... ...
RETURN
Здесь флаг располагается в регистре R5. При входе на ENTER2 флаг обнуляется; при входе на ENTER1 команда CLR R5 используется как операнд команды MOV, т.е. в R5 попадает код команды CLR R5, который нулю не равен.
«Большой трюк»: реентерабельный сопрограммный коммутатор прерываний ввода-вывода.
Этот пример является комбинацией нескольких предыдущих. Пусть имеется несколько однотипных устройств ввода-вывода (например, терминалов), подключённых к одному процессору. Требуется организовать работу с ними по прерываниям. Алгоритм управления предполагается достаточно сложным и не независимым, т. е. способ обработки прерывания ввода зависит от того, какие действия выполнены к данному моменту с каналом вывода, и наоборот. В таких условиях удобно реализовать алгоритм как сопрограмму - забыть на время о том, что существует фоновый процесс и прерывания как таковые, и писать программу так, как если бы весь процессор был предназначен исключительно для управления вводом-выводом. В тот момент, когда по алгоритму требуется ожидание готовности ввода или вывода, программа должна обращаться к специальной процедуре ожидания, которую можно представить себе следующим образом:
WAITIO: TSTB @#177560 ; ГОТОВНОСТЬ ВВОДА ЕСТЬ?
BMI 1$ ; ДА НА ВОЗВРАТ С С=0
TSTB @#177654 ; ГОТОВНОСТЬ ВЫВОДА ЕСТЬ?
BPL WAITIO ; НЕТ - ПРОДОЛЖИТЬ ОЖИДАНИЕ
SEC ; С=1 - ПРИЗНАК ГОТОВНОСТИ ВЫВОДА
1$: RETURN
(для определённости здесь использованы адреса регистров системного терминала 177560...177566). Вызов процедуры прерывания осуществляется командой CALL WAITIO; по окончанию ожидания управление возвращается на следующую за CALL WAITIO команду, т.е. процедура ожидания является обычной подпрограммой по отношению к программе управления.
Следующий шаг должен состоять в замене процедуры ожидания другой специальной программой - сопрограммным коммутатором, который вместо малополезного повторения TSTB будет передавать управление фоновому процессу (командой RTI) и вновь получать управление по прерыванию. Сопрограммный коммутатор желательно организовать так, чтобы обслуживание всех однотипных устройств производилось параллельно и независимо по одной и той же программе.
Следующую реализацию сопрограммного коммутатора мы сочли возможным отнести к трюкам, так как она лаконична (по сравнению с перечнем функций) и основана на нестандартном применении команд:
; ОПИСАНИЕ ВЕКТОРОВ ПРЕРЫВАНИЯ
.ASECT
.=VECT1
.WORD INT1,340,INT1,341
.=VECT2
.WORD INT2,340,INT2,341
................
.=VECTN
.WORD INTN,340,INTN,341
;--------------------------
; СОПРОГРАММНЫЕ КОММУТАТОРЫ
; И ОБЛАСТИ ПЕРЕМЕННЫХ
; (ПО ЧИСЛУ УСТРОЙСТВ)
;--------------------------
.PSECT
INT1: JSR R5,@(PC)+ ; СОПРОГРАММНЫЙ
.WORD START ; КОММУТАТОР
BASE: MOV (SP)+ ,-(R5) ; ДЛЯ ПЕРВОГО
MOV (SP)+,R5 ; УСТРОЙСТВА
RTI ;
; ЗДЕСЬ ДОЛЖНЫ РАСПОЛАГАТЬСЯ ОПИСАНИЯ КОНСТАНТ И
; ПЕРЕМЕННЫХ ДЛЯ ПЕРВОГО УСТРОЙСТВА, НАПРИМЕР:
ADDRIN: .WORD 177562
ADDROU: .WORD 177566
POINTR: .WORD BUFFER
BUFFER: .BLKB 100
; И Т.Д.
;-------------------------------------------------
INT2: JSR R5,@(PC)+ ; СОПРОГРАММНЫЙ
.WORD START ; КОММУТАТОР
MOV (SP)+,-(R5) ; ДЛЯ ВТОРОГО
MOV (SP)+,R5 ; УСТРОЙСТВА
RTI
; КОНСТАНТЫ И ПЕРЕМЕННЫЕ ДЛЯ ВТОРОГО УСТРОЙСТВА,
; РАСПОЛОЖЕННЫЕ В ТОМ ЖЕ ПОРЯДКЕ, ЧТО И ДЛЯ ПЕРВОГО.
.WORD 177272
.WORD 177276
.WORD .+2
.BLKB 100
; И Т.Д.
;-------------------------------------------------
... ... ...
INTN: JSR R5,@(PC)+ ; СОПРОГРАММНЫЙ
.WORD START ; КОММУТАТОР
; ДЛЯ N-ГО
... ; УСТРОЙСТВА
... ;
... ;
;------------------------------------
; ПРОГРАММА УПРАВЛЕНИЯ ВВОДОМ/ВЫВОДОМ
;------------------------------------
START: BCS 1$ ; НА ОБРАБОТКУ ПРЕРЫВАНИЯ ВЫВОДА
; ОБРАБОТКА ПРЕРЫВАНИЯ ВВОДА:
; ОБРАЩЕНИЕ К ОБЛАСТИ ПЕРЕМЕННЫХ ДОЛЖНО
; ПРОИЗВОДИТЬСЯ ИНДЕКСНО ПО R5, НАПРИМЕР:
MOVB @ADDRIN-BASE(R5),@POINTR-BASE(R5)
INCB POINTR-BASE(R5)
...
...
CALL @R5 ; ОБРАЩЕНИЕ К КОММУТАТОРУ
; ДЛЯ ОЖИДАНИЯ ГОТОВНОСТИ
BCS 2$ ; НА ОБРАБОТКУ ПРЕРЫВАНИЯ ВЫВОДА
...
...
1$: ...
...
CALL @R5 ; ЕЩЁ ОДНО ОБРАЩЕНИЕ К КОММУТАТОРУ
BCS 3$ ; НА ОБРАБОТКУ ВЫВОДА
...
...
BR START ; ЭТА КОМАНДА ЗДЕСЬ ПОСТАВЛЕНА
; ДЛЯ ИЛЛЮСТРАЦИИ ТОГО, ЧТО
; КОНЦА АЛГОРИТМ НЕ ИМЕЕТ
;-------------------------------------------------
Описанный коммутатор не полностью эквивалентен процедуре ожидания, основанной на командах TSTB: нельзя использовать регистры и необходимо возвращать стек в исходное состояние перед каждым обращением к коммутатору. При необходимости можно достигнуть полной эквивалентности (ценой некоторой потери быстродействия). Для этого достаточно ввести собственные стековые буферы в области переменных каждого устройства и команды сохранения / восстановления регистров (включая указатель стека). Правда, после таких изменений программа заметно теряет лаконичность, и её уже трудно назвать трюком.
Телефон для справок: 196-94-05. Москва.
Статья поступила 9 апреля 1987 г.