УДК 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 г.

Performed by © gid, 2012-2024.