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