Перехват прерываний и создание резидентных программ
Вначале вспомним, что такое прерывания и как устроена система прерываний нашего процессора.
Под прерыванием понимается некое событие, заставляющее процессор прервать выполнение текущей программы и перейти на подпрограмму обработки этого события. Прерываемая программа обычно называется фоновой программой, а подпрограмма обработки – обработчиком. После того, как обработчик заканчивает свою работу, управление возвращается фоновой программе, в ту точку (той команде фоновой программы), на которой она была прервана.
При любом прерывании выполняется следующая последовательность действий:
· Процессор автоматически сохраняет в стеке адрес возврата, то есть адрес той команды фоновой программы, на которую мы должны будем вернуться из обработчика. Для этого процессор заталкивает в стек содержимое трех регистров: f(регистр флагов), cs и ip. Пара cs:ip как раз и задает адрес возврата.
· После того, как адрес возврата сохранен, процессор загружает в регистры cs и ipадрес первой команды обработчика, который он берет из таблицы прерываний (смотри ниже), тем самым, передавая управление обработчику.
· В конце обработчика программист пишет команду iret(возврат из прерывания). По этой команде процессор выталкивает из стека адрес возврата (в регистры csи ip) и флаги (в регистр f). Тем самым происходит возврат в фоновую программу. Естественно, этот возврат будет правильным, только если вершина стека в момент выполнения команды iretнастроена надлежащим образом, иначе в наши регистры вытолкнется «мусор» и компьютер зависнет.
Открытым остался вопрос, а откуда процессор берет начальный адрес обработчика? В процессорах фирмы Intel каждому прерыванию присвоен номер (чаще говорят, тип). Тип прерывания может лежать в диапазоне 0 – 255, то есть всего возможно 256 различных прерываний. Реально в компьютерах задействованы не все 256 прерываний, но для задействованного прерывания обязательно должен быть обработчик, расположенный в известном месте памяти. Для того чтобы по известному типу процессор мог определить начальный адрес обработчика, в младшем килобайте памяти (адреса 00000h – 003ffh) создается таблица прерываний. В этой таблице последовательно записаны адреса обработчиков для различных типов (начиная с типа 0). При этом каждый такой адрес задается в виде пары сегмент:смещение(сегментзагружается в cs, смещение– в ip). Такая пара однозначно задает адрес ячейки памяти, в которой располагается первая команда обработчика прерывания данного типа. Обычно такую пару называют вектором прерывания. Формат таблицы прерываний показан на рис 3.3. Здесь displи disphсоответственно младший и старший байты поля смещение, а segl и segh- младший и старший байты поля сегмент.Таким образом, любой вектор занимает в памяти 4 байта. Для того чтобы по типу найти адрес вектора, надо этот тип умножить на 4. Например, тип = 2, адрес вектора прерываний для этого типа равен 2*4 = 8, то есть искомый вектор располагается в ячейках памяти 8, 9, 10 и 11.
Рис 3.3
Пусть, например, у нас в памяти (в таблице прерываний) такая ситуация:
адрес | содержимое |
00004h | 22h |
00005h | 07h |
00006h | 91h |
00007h | c0h |
тогда пара c091:0722hпредставляет собой вектор для прерывания типа 1, а начальный адрес обработчика для этого типа получается из этой пары так:
A = (c091h)*16 + 0722h = c0910h + 0722h = c1032h.
Обратный пример. Пусть мы написали обработчик для прерывания типа 2 и расположили его в памяти, начиная с адреса 55a60h. Как нам преобразовать этот адрес в вектор? Надо представить этот адрес в виде пары, а это неоднозначная операция, то есть, как правило, существует несколько правильных пар, задающих один и тот же адрес. Например, наш адрес можно представить в виде пары 55a6:0000h(или 55a0:0060h или….). Теперь надо записать вектор в таблицу:
адрес | содержимое |
00008h | 00h |
00009h | 00h |
0000ah | a6h |
0000bh | 55h |
Что же такое резидентная программа и чем она отличается от обычных программ? Обычная программа при своем запуске загружается в память, а после того, как она отработала, полностью из памяти выгружается. Конечно, физически эта программа из памяти не удаляется, DOS просто помечает эту область памяти как свободную. Резидентная программа остается в памяти даже после того, как она отработала. То есть такая программа постоянно находится в памяти и активизируется при наступлении определенного события. Таким событием может, например, быть нажатие определенной комбинации клавиш или истечение заданного кванта времени.
Для того чтобы резидентная программа имела возможность отслеживать «свое событие», она должна перехватывать соответствующее прерывание. Например, при реакции на определенные клавиши наиболее удобно перехватывать аппаратное прерывание от клавиатуры, которому в системе присвоен тип 9. При активации резидентной программы через заданные промежутки времени можно перехватывать аппаратное прерывание от таймера – тип 8 (лучше не тип 8, а тип 1сh).
При этом автор резидентной программы должен, хотя бы в общих чертах, представлять, что делает стандартный обработчик того прерывания, которое эта программа перехватывает.
Посмотрим, что происходит, когда мы нажимаем какую-то клавишу на клавиатуре. Контроллер клавиатуры выставляет СКЭН – код нажатой клавиши в порт 60h и формирует запрос на контроллер прерываний. Последний передает этот запрос на процессор и сообщает процессору тип данного прерывания (тип 9). Обработчик 9-го прерывания читает порт 60h, переводит СКЭН – код в ASCII – код (если нажата символьная клавиша, для функциональных клавиш ASCII – код = 0) и помещает и СКЭН и ASCII коды в кольцевой буфер клавиатуры, расположенный в области переменных BIOS. Адрес элемента буфера, в который была записана информация о нажатой клавише, храниться в ячейке памяти с адресом 0041ah. Прикладные программы и операционная система считывают информацию о нажатых клавишах уже из этого буфера (подробно буфер клавиатуры описан в Приложении). Для этого они либо используют соответствующие сервисные прерывания (например, int 16h), либо работают с буфером напрямую:
; читаем из буфера информацию о нажатой клавише
Mov ax, 40h
mov es, ax;начальный адрес сегмента 00400h
mov bx, es:[1ah]; в bx смещение (относительно es) ячейки, в которой
; записана информация о клавише
mov ax, es:[bx] ; теперь в ah– СКЭН, а в al– ASCII коды
Отметим еще один нюанс, с которым Вам, возможно, придется столкнуться. При нажатии клавиши происходитне одно прерывание, а два. Первое прерывание возникает, когда мы эту клавишу нажимаем, второе – когда отпускаем. И в том и в другом случае контроллер клавиатуры выставляет в порт 60h соответственно СКЭН – код нажатия и СКЭН – код отжатия, после чего вызывается обработчик 9-го прерывания. Для любой клавиши справедливо:
Код отжатия = код нажатия + 128
иначе говоря, код отжатия всегда характеризуется наличием единицы в старшем разряде. Например, код нажатия клавиши 5– 06h, код отжатия – 86h, код нажатия клавиши «стрелка - вверх» на цифровой клавиатуре – 48h, отжатия – c8h. Иногда такая ситуация начинает мешать и, для правильной работы резидента, код отжатия приходится отсекать.
Примечание:для некоторых функциональных клавиш, появившихся на расширенной клавиатуре (101 клавиша и более), СКЭН – код (как нажатия, так и отжатия) представляет собой последовательность байт (два байта, а иногда (если, например, включен NUM LOCK) и четыре). Например, когда мы нажимаем клавишу «стрелка – вверх», расположенную не на цифровой клавиатуре, возникают два прерывания и код нажатия имеет вид e0 48h, также два прерывания возникает, и когда мы эту клавишу отпускаем, а код отжатия имеет вид 0e c8h. При включенном режиме NUM LOCK, код нажатия этой клавиши имеет вид e0 2a e0 48h, а код отжатия – e0 c8 e0 aah(и при нажатии и при отжатии возникают по четыре прерывания). Эту ситуацию тоже иногда приходится учитывать.
Обработчик прерывания типа 8 обрабатывает прерывания от 0-го канала таймера, который отведен для службы системного времени. Таймер тикает примерно 20 раз в секунду и по каждому тику обработчик прибавляет единицу к системным часам, расположенным в области переменных BIOS. Пользователю не рекомендуется перехватывать 8-е прерывание. Специально для пользователя в конце стандартного обработчика 8-го прерывания стоит командаint 1ch. Обработчик этого программного прерывания по сути дела фиктивен, поскольку состоит из единственной команды iret. При работе с таймером пользователю как раз и рекомендуется перехватывать прерывание 1ch.
Для того чтобы перехватить какое-либо прерывание, надо определить, где в таблице прерываний располагается соответствующий вектор, и записать на его место новый вектор, указывающий на адрес первой команды нашего резидента. При этом в общем случае желательно сохранить старый вектор в каком-то известном нашему резиденту месте памяти. Все это можно сделать «вручную», но удобнее воспользоваться средствами DOS:
· функция 35h прерывания 21h
Входные параметры:в al– тип перехватываемого прерывания.
Возвращает в паре регистров es:bx вектор для прерывания, тип которого задан в al.
· функция 25h прерывания 21h
Входные параметры:в ds:dx– новый вектор, который мы записываем в таблицу, в al – тип прерывания, задающий место в таблице, куда производится запись вектора.
Определить, что заносить в ds и в dx в качестве вектора достаточно просто. В dsдолжен быть начальный адрес сегмента памяти, в который загружен наш резидент, а так как, при запуске программы на выполнение, DOS именно так и настраивает ds, в этом регистре и так находится правильная информация, перенастраивать его не надо. В dxдолжно находиться смещение первой команды нашего резидента. Для того чтобы определить это смещение, достаточно поставить на эту команду метку (например, met:) и написать команду mov dx, offset met. После этого в dx будет нужное значение.
Таким образом, для перехвата прерывания надо выполнить примерно следующую последовательность действий:
1. прочитать из таблицы прерываний вектор системного обработчика и запомнить его в памяти, доступной нашему резиденту, с тем, чтобы в дальнейшем у нас была возможность вызвать правильный обработчик перехватываемого прерывания;
2. на место старого вектора записать в таблицу новый вектор, указывающий на нашу резидентную программу.
После этого при возникновении соответствующего прерывания вместо системного обработчика будет вызвана наша резидентная программа. Последняя обычно определяет, касается ли ее произошедшее событие или нет (например, нажата ли нужная комбинация клавиш). Если событие «наше» резидент производит требуемую обработку, если «не наше» - передает управление системному обработчику, адрес которого мы сохранили.
В принципе, если мы собираемся полностью игнорировать системный обработчик, пункт 1 можно не выполнять, но такой вариант на практике используется редко. Например, если мы собираемся игнорировать системный обработчик прерываний от клавиатуры, наш резидент должен сам обрабатывать нажатие всех клавиш, что весьма затруднительно.
Обычно резидентная программа пишется в СОМ формате и имеет структуру, показанную на рис 3.4.
PSP |
область данных резидентной части |
РЕЗИДЕНТНАЯ ЧАСТЬ |
ЗАГРУЗОЧНАЯ ЧАСТЬ |
область данных загрузочной части |
Рис. 3.4.
При запуске программы управление передается загрузочной части, которая с помощью рассмотренных выше действий подменяет вектор системного обработчика в таблице прерываний новым вектором, указывающим на нашу резидентную программу. После этого программа завершает свою работу, оставляя PSP и резидентную часть в памяти. Загрузочную часть, как правило, для экономии места из памяти удаляют. Именно поэтому загрузочную часть и располагают после резидентной части.
Завершить работу программы, оставив ее (или ее часть) в памяти, можно, например, с помощью прерывания int 27h. Входным параметром для этого прерывания является размер (в байтах) оставляемой в памяти части программы (начиная с начала программы, то есть с PSP). Этот входной параметр задается в dx. Определить размер оставляемой части очень просто. Достаточно поставить метку на первую команду загрузочной части (например, start:) и написать команду mov dx, offset start.
При запуске (установке) резидентной программы желательно производить проверку на повторную установку резидента, иначе в памяти может оказаться несколько копий нашей программы. Отсутствие такой проверки приводит, как минимум, к нерациональному использованию памяти, а в худшем случае вообще искажает работу резидента. Например, наш резидент при нажатии некоторой «горячей» клавиши инвертирует цвета экрана и в памяти установлено две копии этого резидента. Тогда при нажатии «горячей» клавиши первая копия проинвертирует цвета, а вторая их восстановит. В результате мы ничего не увидим.
Проверку на повторную установку можно производить различными способами. Мы здесь рассмотрим только один из них. В области данных резидентной части выделяется байт (или больше) и туда записывается «ключ». Ключ это любое число, желательно редкое, например 5555h. После того как запускается наша программа, и загрузочная часть считывает из таблицы прерываний вектор, относительно этого вектора (вернее, относительно его поля сегмент) в памяти определяется ячейка, соответствующая ключу. Ее содержимое сравнивается с известным нам ключом и, если сравнение произошло, значит, наша программа (с вероятностью 99,9..%) уже установлена, поскольку маловероятно, что другая программа имеет в соответствующей ячейке памяти число, совпадающее с нашим ключом. В этом случае загрузочная часть выводит на экран сообщение о том, что программа уже установлена, и завершает свою работу, ничего не оставляя в памяти. Подобный способ проверки имеет серьезный недостаток: он работает, только если наш резидент последний, кто перехватил соответствующее прерывание. Если после нас наше прерывание перехватил «чужой» резидент, именно в нем мы и будем искать наш ключ и конечно не найдем, хотя наш резидент установлен в памяти.
Последовательность действий в загрузочной части может быть примерно следующей:
1. Считать из таблицы прерываний старый вектор (функция 35h прерывания 21h);
2. Произвести проверку на повторную установку, при положительном результате перейти к пункту 6;
3. Сохранить старый вектор в известном (резиденту!!) месте памяти;
4. Поместить в таблицу прерываний (на место старого вектора) новый вектор, указывающий на наш резидент (функция 25h прерывания 21h);
5. Завершить программу, оставив ее резидентной (прерывание 27h);
6. Вывести сообщение о том, что программа уже установлена, и завершить программу, ничего не оставляя в памяти (функция 4ch прерывания 21h).
Последовательность действий в резидентной части определяется назначением нашего резидента. Однако если рассматривать эту последовательность на глобальном уровне, можно выделить три основных схемы действий резидентной части:
Схема 1.Сохраняем (в стеке) значения всех регистров фоновой программы, в том числе и сегментных, которые портит наш резидент. Если этого не сделать фоновая программа, при возврате, получит испорченное содержимое регистров и вряд ли будет работать корректно. Выполняем требуемую обработку. Восстанавливаем значения регистров из стека (в обратном порядке!). Передаем управление системному обработчику (командой jmp far), адрес которого нам сохранила загрузочная часть. Системный обработчик впоследствии сам вернет управление фоновой программе (командой iret).
Схема 2. Сохраняем значения регистров. Передаем управление системному обработчику, выполнив команды:
Pushf
Call far
Первая команда заталкивает в стек содержимое регистра флагов, вторая –вызывает системный обработчик как подпрограмму. (Возврат из этой «подпрограммы» происходит по команде iret, которая выталкивает из стека адрес возврата и флаги! Именно поэтому нужна команда pushf.) При возврате из системного обработчика управление снова получает наш резидент. Он выполняет требуемые действия, восстанавливает регистры и возвращает управление фоновой программе (командой iret).
Схема 3.Сохраняем значения регистров. Выполняем требуемую обработку. Восстанавливаем регистры. Возвращаем управление фоновой программе (командой iret). То есть в этой схеме мы полностью игнорируемсистемный обработчик и, следовательно, должны работать «за него». В частности, если мы перехватываемаппаратное прерывание (от таймера, от клавиатуры и. т. д.) мы должны не забыть снять «штору», которую ставит контроллер прерываний, послав число 20hв порт 20h. То есть здесь мы должны знать и учитывать все нюансы работы аппаратуры. Именно поэтому эта схема на практике используется редко.
Компилируя три рассмотренные выше схемы между собой можно получить более сложные схемы действий резидентной части.
Наибольшее число ошибок, приводящих к зависанию резидента, связано с тем, что при написании резидентной части программист (по незнанию или по невнимательности) не учитывает одно важное обстоятельство. Когда из фоновой программы мы попадаем в наш резидент, единственным сегментным регистром, содержимое которого указывает на наш резидент, является регистр СS(в него загрузилась информация из таблицы прерываний). Все остальные сегментные регистры указывают на фоновую программу!
К чему это может привести? Пусть в области данных резидентной части нашей программы выделен байт под переменную, которую мы назвали flag. Пусть при написании резидентной части программист решил присвоить этой переменной значение 5, для чего написал команду:
Mov flag, 5
Программист допустил (скорее всего) грубую ошибку. Ведь в этой команде по умолчанию начальный адрес сегмента задает содержимое регистра ds, а оно, как мы помним, пришло из фоновой программы и указывает на фоновую программу. То есть реально наша пятерка запишется не в переменную flag, а в какую-то ячейку памяти фоновой программы. Велика вероятность, что после этого фоновая программа просто перестанет корректно работать и зависнет (после нашего возврата в эту фоновую программу). А как же нам правильно обратиться к нашей переменной? Например, это можно сделать так:
Mov cs:flag, 5
То есть можно, например, придерживаться следующего правила: во всех командах резидентной части, в которых идет обращение к данным, расположенным в области данных этой резидентной части, используем префикс замены сегмента cs:.
Отметим еще одно обстоятельство. В резидентной части программы не рекомендуется использовать сервисные прерывания DOS. Использовать эти прерывания можно при соблюдении определенных правил, которые в данном пособии не приводятся. Бездумное использование прерываний DOS может привести к неустойчивой работе программы. Прерывания BIOS можно использовать без ограничений.
Далее приводятся примеры двух тривиальных резидентных программ. Обе программы делают одно и тоже: отслеживают нажатие комбинации клавиш ALT/t и выводят сообщение об этом «событии» на экран. Однако первая программа написана по схеме 1, а вторая по схеме 2. Приведем данные, необходимые для понимания этих программ:
· если нажата клавиша ALT, бит 3 в ячейке памяти 00417h установлен в единицу;
· скэн-код клавиши t равен 14h, ASCII равен 74h;
· скэн-код комбинации клавиш ALT/t равен 14h, ASCII равен 0.
ПРОГРАММА 1.Перехватывает аппаратное прерывание от клавиатуры (тип 9). Реализована схема 1. Необходимо учитывать, что программа предназначена для работы в текстовом режиме. Например, в графическом (не полноэкранном) режиме FAR она может работать некорректно.
; Заголовок СОМ-программы
Code segment
Assume cs:code,ds:code
Org 100h
; Переход на загрузочную часть программы
F10: jmp start
; Область данных резидентной части.
key dw 5555h; ключ расположен в ячейке
; со смещением 103h (100h байт
; PSP и 3 байта jmp start)
soob db 'нажата Alt/t'
oldvect dd 0; здесь запоминаем адрес
; системного обработчика
; Здесь начинается резидентная часть программы. Очень важным является
; то, что когда мы попадаем сюда из фоновой программы, все сегментные
; регистры, кроме CS, содержат данные фоновой программы. Поэтому там,
; где используются команды, по умолчанию берущие базовый адрес из DS,
; необходимо использовать префикс замены сегмента CS:.
newvect:
; Сохраняем в стеке все регистры, которые можем испортить
Push ax
Push es
Push bx
Push cx
push dx
push si
; Выясняем, нажата ли ALT/t
in al, 60h
cmp al, 14h; нажата t?
jne exit
mov ax, 40h
mov es, ax
mov al, es:[17h]
and al, 1000b; нажата ALT?
jz exit
; Запоминаем позицию курсора (в SI).
Mov ah, 3
Mov bh, 0
int 10h
mov si, dx
; Выводим сообщение, начиная с текущей позиции курсора, что Alt/t нажата.
Mov cx, 12
Mov bx, offset soob
M1: mov ah, 0eh
mov al, cs:[bx]
Int 10h
Inc bx
Loop m1
; Вводим задержку секунд на 5, чтобы полюбоваться надписью.
Mov ah, 0
Int 1ah
Mov bx, dx
Add bx, 50
M2: mov ah, 0
Int 1ah
Cmp bx, dx
Ja m2
; Восстанавливаем курсор в старой позиции. Старые координаты берутся из SI.
Mov dx, si
Mov ah, 2
Mov bh, 0
Int 10h
; Стираем нашу надпись пробелами. Курсор при этом не смещается,
; а остается в нужной (старой) позиции.
Mov ah, 0ah
mov al, ' '
Mov cx, 12
Mov bh, 0
Int 10h
exit:
; Восстанавливаем в регистрах информацию фоновой программы.
Pop si
Pop dx
Pop cx
Pop bx
Pop es
Pop ax
; Вызываем правильный системный обработчик. Так как ячейка oldvect
; описана как двойное слово, команде jmp автоматически будет присвоен тип far.
Jmp cs:oldvect
; Здесь кончается резидентная часть и начинается загрузочная часть программы.
start:
; Получаем вектор правильного (системного) обработчика (в ES:BX).
Mov ah, 35h
Mov al, 9
Int 21h
; Производим проверку (сравнивая с ключом) на повторную установку программы.
cmp word ptr es:[103h], 5555h
Jz inst
; Запоминаем вектор правильного (системного) обработчика.
Mov word ptr oldvect, bx
mov word ptr oldvect+2, es
; Устанавливаем вектор своего обработчика.
Mov dx, offset newvect
Mov ah, 25h
Mov al, 9
Int 21h
; Завершаем программу, оставляя резидентной в памяти ее часть,
; от начала PSP до метки start.
Mov dx, offset start
Int 27h
; Если программа уже установлена в памяти, выдаем сообщение об
; этом и завершаем программу, ничего не оставляя в памяти.
Inst: mov ah, 9
Mov dx, offset soob2
Int 21h
Mov ah,4ch
Int 21h
; Область данных загрузочной части.
soob2 db 'Программа уже установлена$'
Code ends
End f10
ПРОГРАММА 2.Перехватывает прерывание BIOS int 16h. Это прерывание возвращает в axиз буфера клавиатуры СКЭН и ASCII коды нажатой клавиши. Основная ветвь резидентной части программы реализует схему 2. То есть, попадая в наш резидент, мы сразу вызываем стандартный обработчик int 16h. Этот обработчик возвращает нам в ax код клавиши, с которым мы и работаем. Поскольку при этом возврат управления фоновой программе осуществляет наш резидент, он и должен обеспечить передачу (в регистре ax) полученного кода клавиши фоновой программе. В резидентной части программы имеется побочная ветвь, реализующая схему 1. Связано это с тем, что прерывание int 16hимеет целый ряд функций, из которых нам интересны только две: 0и 10h. Остальные функции как раз и отсекаются этой побочной ветвью.
; Заголовок СОМ-программы
Code segment
Assume cs:code,ds:code
Org 100h
; Переход на загрузочную часть программы
F10: jmp start
; Область данных резидентной части.
key dw 5555h; ключ расположен в ячейке
; со смещением 103h (100h байт
; PSP и 3 байта jmp start)
soob db 'нажата Alt/T'
oldvect dd 0; здесь запоминаем адрес
; системного обработчика
; Здесь начинается резидентная часть программы. Очень важным является
; то, что когда мы попадаем сюда из фоновой программы, все сегментные
; регистры, кроме CS, содержат данные фоновой программы. Поэтому там,
; где используются команды, по умолчанию берущие базовый адрес из DS,
; необходимо использовать префикс замены сегмента CS:.
newvect:
; Сохраняем в стеке все регистры, которые можем испортить
Push es
Push bx
Push cx
push dx
push si
push di
; Это функция 0 ?
cmp ah, 0
je resid
; Это функция 10h?
cmp ah, 10h
jne exit1; если «не наши» функции, отдаем управление системному
; обработчику
resid:
; Вызываем стандартный обработчик
pushf
call cs:oldvect
; Если в AHвернулось 14h,а в AL- 0 -это код Alt/t.
Cmp ax, 1400h
jne exit
mov di, ax; сохраняем код клавиши, чтобы вернуть его фоновой программе
; Запоминаем позицию курсора (в SI).
Mov ah,3
Mov bh,0
int 10h
mov si, dx
; Выводим сообщение, начиная с текущей позиции курсора, что Alt/t нажата.
Mov cx,12
Mov bx,offset soob
M1: mov ah,0eh
mov al,cs:[bx]
Int 10h
Inc bx
Loop m1
; Вводим задержку секунд на 5, чтобы полюбоваться надписью.
Mov ah, 0
Int 1ah
Mov bx, dx
Add bx, 50
M2: mov ah, 0
Int 1ah
Cmp bx, dx
Ja m2
; Восстанавливаем курсор в старой позиции. Старые координаты берутся из SI.
Mov dx, si
Mov ah,2
Mov bh,0
Int 10h
; Стираем нашу надпись пробелами. Курсор при этом не смещается,
; а остается в нужной (старой) позиции.
Mov ah,0ah
mov al,' '
Mov cx,12
Mov bh,0
Int 10h
mov ax, di; восстанавливаем код клавиши, который надо вернуть фоновой
; программе
exit:
; Восстанавливаем в регистрах информацию фоновой программы.
Pop di
Pop si
Pop dx
Pop cx
Pop bx
Pop es
; Возвращаем управление фоновой программе.
Iret
; Возвращаем управление стандартному обработчику
exit1:
pop di
Pop si
Pop dx
Pop cx
Pop bx
Pop es
Jmp cs:oldvect
; Здесь кончается резидентная часть и начинается загрузочная часть
; программы.
start:
; Получаем вектор правильного (системного) обработчика (в ES:BX).
Mov ah,35h
Mov al, 16h
Int 21h
; Производим проверку (сравнивая с ключом) на повторную установку программы.
cmp word ptr es:[103h],5555h
Jz inst
; Запоминаем вектор правильного (системного) обработчика.
Mov word ptr oldvect,bx
mov word ptr oldvect+2,es
; Устанавливаем вектор своего обработчика.
Mov dx,offset newvect
Mov ah,25h
Mov al, 16h
Int 21h
; Завершаем программу, оставляя резидентной в памяти ее часть,
; от начала PSP до метки start.
Mov dx,offset start
Int 27h
; Если программа уже установлена в памяти, выдаем сообщение об
; этом и завершаем программу, ничего не оставляя в памяти.
Inst: mov ah,9
Mov dx,offset soob2
Int 21h
Mov ah,4ch
Int 21h
; Область данных загрузочной части.
soob2 db 'Программа уже установлена$'
Code ends
End f10
Неприятной особенностью многих резидентных программ является их критичность к операционной среде, в которой они запускаются. Зачастую резидент, успешно работающий в Volkov commander, не работает в DOS Navigator и наоборот. Это связано с тем, что мы не знаем или не учитываем специфику работы конкретной среды.
В заключение этого раздела по традиции приведем варианты заданий к лабораторной работе.
ЗАДАНИЯ К ЛАБОРАТОРНОЙ РАБОТЕ
1. Через временной интервал (например, 10 секунд) на экран выводится какое-либо сообщение. Через 10-20 секунд сообщение с экрана снимается, и работа ПЭВМ продолжается обычным образом.
2. При нажатии любой клавиши на экран выдается просьба нажать эту клавишу еще раз. При повторном нажатии просьба с экрана снимается, и работа ПЭВМ продолжается обычным образом.
3. При нажатии клавиши ENTER в центр экрана выводится сообщение: "Отдыхаю, подождите минутку". Через 10-20 секунд сообщение снимается, и работа ПЭВМ продолжается обычным образом.
4. ПЭВМ реагирует на клавишу "стрелка - вверх" как на клавишу "стрелка - вниз" (и наоборот), на клавишу "стрелка - влево" как на клавишу "стрелка - вправо" (и наоборот).
5. При нажатии клавиши F1 программа очищает экран и безостановочно выводит на экран сообщение "Не хочу вам помогать!", прокручивая при этом экран вверх. Секунд через 10-20 этот процесс прекращается, восстанавливается экран, и работа ПЭВМ продолжается обычным образом.
6. Через равные промежутки времени (например 30 секунд) резидент блокирует / разблокирует клавиатуру.
7. При нажатии клавиши Т сообщается текущее системное время.
8. Нажатие клавиши D замедляет / восстанавливает реакцию системы на нажатие клавиш клавиатуры. Замедление реакции должно быть достаточным, для того чтобы его можно было заметить визуально.
9. При нажатии клавиши R очищается правая, а при нажатии L - левая половина экрана. Через 10-20 секунд после нажатия любой из этих клавиш экран восстанавливается.
10. Нажатие клавиши G меняет размер курсора. (Как правило, любая среда, в которой мы работаем, например, Dos Navigator, при возврате ей управления восстанавливает размер курсора. То есть, чтобы гарантированно увидеть наше изменение курсора, можно, например, запускать нашу программу в Command.com).
11. Нажатие клавиши I инвертирует цвета экрана.
12. После установки резидента система перестает реагировать на нажатие клавиши F7. На другие клавиши система реагирует обычным образом.
Работа со звуком.
В состав любой ПЭВМ фирмы IBM входит микросхема таймера i8254. Схема ее использования в ПЭВМ представлена на рис. 3.5.
Рис. 3.5
Микросхема таймера имеет три канала, соответственно каналы 0,1 и 2. Канал 0 отводится для службы системного времени, канал 1- для регенерации памяти, а канал 2- для управления работой динамика.
Порты таймера имеют следующие системные адреса:
канал 0 - 40h;
канал 1 - 41h;
канал 2 - 42h;
регистр управляющего слова (РУС) - 43h.
Через РУС производится настройка каналов, через остальные порты -загрузка/считывание информации в/из соответствующих каналов.
Для работы со звуком нам нужен канал 2 и РУС. Каналы 0 и 1 перезагружать и перенастраивать категорически запрещено, так как система может выйти из строя (во всяком случае, до перезапуска).
Работа канала 2 заключается в том, что он делит опорную частоту (fоп=1.19... МГц.) на коэффициент пересчета Кпр, который заранее загружается в канал. Получаемая fвых=fоп/Кпр подается на динамик. Надо учитывать, что работой канала 2 и подачей fвыхна динамик управляют два младших бита порта 61h. Если бит 0 порта 61h равен единице, то работа канала 2 (счет) разрешается. Если бит 1 порта 61h равен единице, то разрешается подача fвых на динамик. Таким образом, звук будет воспроизводиться, только если оба этих бита установлены в единицу.
Коэффициент пересчета для любой ноты можно определить исходя из выражения:
Кпр=fоп/fноты.
При этом учитываются следующие соотношения:
частота ноты "до" 1-й октавы (fдо) = 32.625 Гц;
частота ноты "до" 2-й октавы = 2*fдо;
частота ноты "до" 3-й октавы = 4*fдо;
и так далее;
частота "до-диез" = а*fдо;
частота "ре" = а*fдо-диез;
и так далее,
где а =1.06 (приблизительно).
Если нам необходимо сыграть на компьютере мелодию, то проще подсчитать коэффициенты всех нот заранее или взять их готовыми (смотри Приложение), так как человечеству эти коэффициенты давно известны. Таким образом, для мелодии желательно подготовить следующие данные:
а) коэффициент пересчета и время звучания для каждой ноты;
б) длительности всех пауз между нотами.
После того как все эти числа известны, надо последовательно выполнять следующие действия:
1. Запретить звучание, для чего установить в ноль оба младших бита порта 61h (не меняя, при этом, остальные биты этого порта);
2. Настроить канал 2, выполнив две команды:
; настраиваем канал 2 на передачу двух байт Кпр., на режим 3 и на
; двоичный счет
mov al, 0b6h Наши рекомендации