Первый способ написания и загрузки постоянной функции в DOS состоит в том, чтобы, возвращая управление DOS, программа оставалась в памяти резидентной. Такую функцию существляет прерывание INT 27H.
Пример представлен на Фиг. 10.1. Приведенная здесь программа предназначена для обслуживания буфера печати. Обычно при выдаче на печать символа программа обращается к прерыванию INT 17H - драйверу печати BIOS. Эта функция выдает символ на принтер после проверки ошибок и ожидания готовности принтера. Как правило, при этом обеспечивается достаточная производительность. Но допустим, что вы пишете несколько программ и хотите вывести их на принтер. Если вы попытаетесь сделать это, то не сможете обратиться к системе до тех пор, пока принтер не закончит работу. Чтобы продолжить редактирование или ассемблирование другой части программы, вам придется ждать завершения печати.
A
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:27
Фиг. 10.1 Буфер для печати Page 1-1
PAGE ,132
TITLE Фиг. 10.1 Буфер для печати
0000 ABS0 SEGMENT AT 0
0020 ORG 4*8H
0020 ???????? TIMER_INT DD ? ; Аппратное прерывание от таймера
005C ORG 4*17H
005C ???????? PRINTER_INT DD ? ; Прерывание к BIOS для печати
0408 ORG 408H
0408 ???? PRINTER_BASE DW ? ; Базовый адрес адаптера принтера
040A ABS0 ENDS
0000 CODE SEGMENT
0100 ORG 100H
ASSUME CS:CODE,DS:CODE,ES:CODE
0100 EB 09 90 JMP START
0103 ???????? PRINT_VECTOR DD ? ; Место для хранения исходного вектора 17h
0107 ???????? TIMER_VECTOR DD ? ; Место для хранения исходного вектора 9h
010B START:
010B 2B C0 SUB AX,AX ; Установка регистра ES на сегмент ABS0
010D 8E C0 MOV ES,AX
ASSUME ES:ABS0
010F 26: A1 005C R MOV AX,WORD PTR PRINTER_INT
0113 26: 8B 1E 005E R MOV BX,WORD PTR PRINTER_INT+2
0118 26: 8B 0E 0020 R MOV CX,WORD PTR TIMER_INT
011D 26: 8B 16 0022 R MOV DX,WORD PTR TIMER_INT+2
0122 A3 0103 R MOV WORD PTR PRINT_VECTOR,AX
0125 89 1E 0105 R MOV WORD PTR PRINT_VECTOR+2,BX
0129 89 0E 0107 R MOV WORD PTR TIMER_VECTOR,CX
012D 89 16 0109 R MOV WORD PTR TIMER_VECTOR+2,DX
;----- Во время занесения векторов прерываний прерывания запрещены
0131 FA CLI
Фиг. 10.1 Буфер печати (начало)
0132 26: C7 06 005C R 0162 MOV WORD PTR PRINTER_INT,offset PRINT_HANDLER
R
0139 26: 8C 0E 005E R MOV WORD PTR PRINTER_INT+2,CS
013E 26: C7 06 0020 R 0196 MOV WORD PTR TIMER_INT,offset TIMER_HANDLER
R
0145 26: 8C 0E 0022 R MOV WORD PTR TIMER_INT+2,CS
014A B0 36 MOV AL,00110110b
014C E6 43 OUT 43H,AL
014E B0 00 MOV AL,0 ; Увеличение скорости работы таймера в 256 раз
0150 E6 40 OUT 40H,AL
0152 B0 01 MOV AL,1
0154 E6 40 OUT 40H,AL
0156 FB STI
0157 8D 16 28FE R LEA DX,BUFFER_END ; Занесение адреса конца программы
015B CD 27 INT 27H ; Выход с сохранением программы в памяти
015D 00 TIMER_COUNT DB 0
015E 01EE R BUFFER_HEAD DW BUFFER_START
0160 01EE R BUFFER_TAIL DW BUFFER_START
;----- Эта подпрограмма управляет вызовом прерывания 17h
0162 PRINT_HANDLER PROC FAR
ASSUME CS:CODE,DS:nothing,ES:nothing
0162 0A E4 OR AH,AH
0164 74 05 JZ BUFFER_CHARACTER ; Проверка на функцию вывода символа
0166 2E: FF 2E 0103 R JMP PRINT_VECTOR ; Переход на стандартный обработчик
; прерывания 17h
016B BUFFER_CHARACTER:
016B FB STI
016C 53 PUSH BX
016D 51 PUSH CX
016E 56 PUSH SI
016F 2B C9 SUB CX,CX ; Счетчик отсчетов таймера
0171 PRINT_LOOP:
0171 2E: 8B 1E 0160 R MOV BX,BUFFER_TAIL ; Выборка адреса конца буфера
0176 8B F3 MOV SI,BX
0178 E8 01E2 R CALL ADVANCE_POINTER ; Перемещение указателя на следующий байт
017B 2E: 3B 1E 015E R CMP BX,BUFFER_HEAD ; Проверка на наличие места в буфере
0180 74 0E JE BUFFER_FULL ; Нет места,ожидается пока оно появится
0182 2E: 88 04 MOV CS:[SI],AL ; Вывод символа в буфер
0185 2E: 89 1E 0160 R MOV BUFFER_TAIL,BX ; Занесение нового адреса конца буфера
018A B4 00 MOV AH,0 ; Код возврата из прерывания 17h
018C PRINT_RETURN:
018C 5E POP SI
018D 59 POP CX
018E 5B POP BX
018F CF IRET
0190 BUFFER_FULL:
0190 E2 DF LOOP PRINT_LOOP ; Повторить цикл проверки занятости буфера
0192 B4 01 MOV AH,1 ; Буфер занят слишком долго,ошибка
0194 EB F6 JMP PRINT_RETURN
0196 PRINT_HANDLER ENDP
Фиг. 10.1 Буфер печати (продолжение)
;----- Эта программа вызывает 4660 раз в секунду
0196 TIMER_HANDLER PROC FAR
ASSUME CS:CODE,DS:nothing,ES:nothing
0196 50 PUSH AX
0197 53 PUSH BX
0198 2E: 8B 1E 015E R MOV BX,BUFFER_HEAD
019D 2E: 3B 1E 0160 R CMP BX,BUFFER_TAIL ; Есть ли что-нибудь в буфере?
01A2 75 14 JNZ TEST_READY ; Переход,если буфер не пуст
;----- Эта подпрограмма управляет таймером в скоростном режиме
01A4 TIMER_RETURN:
01A4 5B POP BX
01A5 2E: FE 06 015D R INC TIMER_COUNT ; Увеличение счетчика делителя таймера
01AA 75 06 JNZ SKIP_NORMAL
01AC 58 POP AX ; Это выполняется один раз на 256 прерываний
01AD 2E: FF 2E 0107 R JMP TIMER_VECTOR ; Переход на стандартную программу обработки
; прерывания от таймера
01B2 SKIP_NORMAL:
01B2 B0 20 MOV AL,20H
01B4 E6 20 OUT 20H,AL ; Конец прерывания
01B6 58 POP AX
01B7 CF IRET
;----- Символ в буфере,производится попытка напечатать его
01B8 TEST_READY:
01B8 52 PUSH DX
01B9 1E PUSH DS
01BA 2B D2 SUB DX,DX
01BC 8E DA MOV DS,DX ; Установка регистра DS на сегмент ABS0
ASSUME DS:ABS0
01BE 8B 16 0408 R MOV DX,PRINTER_BASE
01C2 42 INC DX ; Установка на порт состояния
01C3 EC IN AL,DX
01C4 A8 80 TEST AL,80H ; Проверка готовности принтера
01C6 74 16 JZ NO_PRINT
01C8 4A DEC DX ; Установка на порт данных
01C9 2E: 8A 07 MOV AL,CS:[BX] ; Выбрка выводимого символа
01CC E8 01E2 R CALL ADVANCE_POINTER
01CF 2E: 89 1E 015E R MOV BUFFER_HEAD,BX
01D4 EE OUT DX,AL ; Вывод символа в порт принтера
01D5 83 C2 02 ADD DX,2 ; Установка на порт управления
01D8 B0 0D MOV AL,0DH
01DA EE OUT DX,AL ; Передача символа из порта в принтер
01DB B0 0C MOV AL,0CH
01DD EE OUT DX,AL
01DE NO_PRINT:
01DE 1F POP DS
01DF 5A POP DX
01E0 EB C2 JMP TIMER_RETURN ; Возврат через подпрограмму управления
01E2 TIMER_HANDLER ENDP ; таймером
01E2 ADVANCE_POINTER PROC NEAR
01E2 43 INC BX ; Сдвиг указателя
Фиг. 10.1 Буфер печати (продолжение)
01E3 81 FB 28FE R CMP BX,offset BUFFER_END
01E7 75 04 JNE ADVANCE_RETURN ; Проверка на конец циклического буфера
01E9 8D 1E 01EE R LEA BX,BUFFER_START ; Установка указателя на начало буфера
01ED ADVANCE_RETURN:
01ED C3 RET
01EE ADVANCE_POINTER ENDP
01EE BUFFER_START LABEL BYTE
01EE 2710[ DB 10000 DUP (?)
??
]
28FE BUFFER_END LABEL BYTE
28FE CODE ENDS
END
Фиг. 10.1 Буфер печати (продолжение)
Приведенная в примере программа может облегчить решение задачи. Конечно, это не обойдется вам даром. Программа отводит под буфер печати некоторую область памяти, которая будет постоянно за ним закреплена. DOS изымает эту область из общего объема памяти, предоставляемой пользователю. Например, если в системе 96K байт памяти, а 10 кбайт отводится под буфер печати, то пользоваться Макроассемблером уже не удастся. Для макроассемблера требуется 96 кбайт, а после создания буфера печати останется лишь 86 кбайт. Поэтому, прежде чем организовать буферизацию печати, убедитесь, что в системе останется еще достаточный объем памяти.
Затем начинает выполняться вторая часть программы. Эта процедура извлекает символы из буфера и пересылает их на принтер. Она управляется прерыванием от таймера. При каждом прерывании от таймера процедура вывода на печать также получает управление. Если в буфере имеется символ, и если устройство печати находится в состоянии "готово", то подпрограмма пересылает этот символ на принтер. Таким образом, символы извлекаются из буфера и пересылаются на принтер со скоростью работы этого устройства. Поскольку программа вывода на печать работает в фоновом режиме, одновременно могут выполняться другие задания, например, редактирование или ассемблирование.
Обратимся к программе, представленной на Фиг. 10.1, и рассмотрим, как взаимодействуют ее компоненты. Во-первых, в ней описан сегмент ABS0, содержащий вектор прерываний, с которым программа имеет дело. Приведенная в примере программа заменяет как прерывание вывода на печать INT 17H, так и прерывание от таймера INT 8. Заметим также, что в сегменте ABS0 определяется адрес PRINTER_BASE. В этой ячейке находится базовый адрес для устройства печати 0. В данном примере предполагается, что все операции печати производятся на системном устройстве печати.
Прежде чем разблокировать прерывания, программа изменяет текущее значение счетчика таймера. Обычно прерывания от таймера происходят примерно 18 раз в секунду. Устройство печати может печатать по 80 символов в секунду. Если бы процедура вывода на печать выдавала по одному символу при каждом прерывании от таймера, то максимальная скорость печати составила бы 18 символов в секунду. Если ускорить таймер, прерывания от таймера будут происходить чаще. Это позволит программе выдавать на печать все 80 символов в секунду. В приведенном примере в таймер загружается значение счетчика 256, оно в 256 раз меньше стандартного значения. Компенсируется это увеличение скорости при помощи процедуры TIMER_HANDLER.
Второе, на что следует обратить внимание - это использование сохраненного вектора прерываний печати. Можно было бы обратиться к листингу BIOS, приведенному в техническом справочнике, и найти начальный адрес процедуры печати. Затем включить этот адрес непосредственно в код программы так же, как это делается для других абсолютных адресов. Однако в результате программа оказалась бы жестко к этому адресу в системе BIOS. Если фирма IBM изменит процедуры BIOS и, таким образом, - адрес процедуры печати, то рассмотренная программа не сможет больше работать. Конечно, если пишите эту программу для своей собственной машины, а покупать новую или продавать свою программу не собираетесь, то указанных проблем не возникнет. Однако в общем случае надо избегать использования абсолютных адресов, если есть выбор. В приведенном примере процедура инициализации легко может использовать вектор прерываний печати для определения адреса процедуры печати BIOS в ПЗУ.
В оставшейся части процедуры PRINT_HANDLER символ помещается в буфер печати. Перед тем, как поместить символ программа проверяет, есть ли в буфере место. Если буфер полон, программа ждет, пока освободится место. Это ожидание не вызовет проблем, поскольку и стандартная процедура BIOS ждет, чтобы принтер был готов принять символ. Из соображений безопасности в регистре CX накапливается число проходов по ветви "занято". Если это число становится равным 64K, а буфер по-прежнему полон, то это может означать какой-то сбой. В этом случае процедура PRINT_HANDLER так же, как и BIOS, выдает сообщение о превышении допустимого времени ожидания.
Ускорение таймера в 256 раз было выбрано потому, что это было просто сделать. Однако если брать в расчет производительность, то лучше было бы ускорить работу таймера в 5 раз, поскольку на обработку каждого прерывания от таймера тратится по меньшей мере 10 микросекунд, и даже больше, если в буфере печати есть символы. Время, затраченное на обработку прерываний, идет в ущерб выполнению системой других заданий, например ассемблирования. При такой частоте прерываний от таймера, становится заметным замедление работы. Для оптимизации производительности следует ускорять таймер менее, чем в 256 раз.
Что же происходит в процедуре работы с таймером, когда в буфере есть символы, предназначенные для печати? Программа считывает порт состояния, чтобы определить, готов ли принтер к приему символа. Поскольку в процедуре используется базовый адрес из области данных BIOS, то наша подпрограмма будет работать и с автономным адаптером устройства печати, и с портом адаптера монохромного дисплея. Если устройство печати не готово, процедура возвращает управление на метку TIMER_RETURN, где в случае необходимости поддерживаются стандартные функции таймера. Процедура вывода на печать не ждет, когда устройство печати освободится, если оно занято. Мы знаем, что прерывание от таймера очень скоро повторится, тогда мы и повторим попытку вывода. Ожидание готовности устройства печати здесь связывало бы бы всю систему. Результат был бы таким же, как и в случае отсутствия буферизации печати.
Если принтер готов, программа извлекает символ из буфера и передает его на принтер. И в данном случае программа вновь не делает всего, что следовало бы. Подпрограмма, входящая в BIOS, делает проверку на ситуацию ошибки при передаче каждого символа. То же самое следовало бы делать и в нашей процедуре. Но что же произойдет в случае сбоя? Если процедура вывода обнаружила ошибку, то как она сможет сообщить программе, что это произошло во время печати? В некоторых случаях к этому моменту программа передававшая даные для печати уже завершила свою работу. Наилучший выход может состоять в проверке ошибок при каждой пересылке символа на принтер процедурой работы с таймером. При обнаружении ошибки процедура PRINT_HANDLER должна выдать сообщение об ошибке, что далее все программы будут производить вывод на печать через прерывание INT 17H. Возможно, это не идеальный вариант, но, вероятно, лучший.
Прежде чем закончить рассмотрение примера, следует обратить внимание еще на одну проблему. Существуют и другие процедуры, изменяющие частоту прерываний от таймера. BASICA - расширенная версия интерпретатора Бейсика, для ускорения таймера используется прием, во многом аналогичный приведенному. При вызове программы BASICA после установки буферизованной печати, процедура TIMER_HANDLER получает прерывания уже не с той частотой, которая предполагается. Поскольку процедура TIMER_HANDLER ограничивает передачу управления прерыванием от таймера процедуре BIOS, текущее время замедлится в 256 раз. BASICA осуществляет также инициализацию устройства печати, что, как мы уже видели, мешает выводу на печать. Это означает, что программа буферизации печати будет работать не для всех приложений. Однако она иллюстрирует использование прерывания INT 27H для создания постоянной системной функции. Приведенный пример иллюстрирует также метод переопределения векторов BIOS для подцепления новой функции к уже имеющимся программам.