Резидентный обработчик клавиатуры (перехват нажатий клавиш и запись в файл)
Министерство образования Украины
Одесская государственная академия холода
Институт информационных технологий
Кафедра «Информационных систем»
Разработка резидентного обработчика прерываний от клавиатуры
Курсовой проект по дисциплине
«Системы программирования и операционные системы»
Руководитель Ненов А. Д. Исполнитель
Ст. гр. 333А Лазанюк А. С.
Зач. книжка № 983214
Защищён с оценкой _____________________
(личная подпись)
_______________
г. Одесса 2000 г.
Содержание:
Задание……………………………………………………………………………………………….2
Краткие теоретические сведенья
Резидентный обработчик прерываний………………………………………………………...3
Защита резидентной программы от повторной установки…………………………………..5
Выгрузка резидентной программы из памяти………………………………………………...8
Перехват прерываний…………………………………………………………………………11
Обработчик прерываний………………………………………………………………………12
Прерывания от внешних устройств…………………………………………………………..12
Резидентный обработчик прерываний от клавиатуры с подключением до системного обработчика…………………………………………………………………………………….14
Описание программы
Описание для пользователя…………………………………………………………………...19
Описание для программиста………………………………………………………………….20
Листинг программы………………………………………………………………………..….24
Рекомендации по улучшению………………………………………………………………...32
Список используемой литературы…………………………………………………………..….33
1. Задание
Разработка резидентного обработчика прерываний от клавиатуры с подключением до системного. Данный обработчик должен производить запись скэн-кодов всех нажимаемых клавиш, а также фиксировать байт флагов клавиатуры при каждом нажатии. Обработчик должен иметь механизм выгрузки из оперативной памяти встроенный в него самого. Также программа должна иметь защиту от повторной установки в оперативную память.
2. Краткие теоретические сведенья
2.1. Резидентный обработчик прерываний
Большой класс программ, обеспечивающих функционирование вычислительной системы (драйверы устройств, программы шифрации и защиты данных, русификаторы, обслуживающие программы типа электронных блокнотов или калькуляторов и др.), должны постоянно находиться в памяти и быстро реагировать на запросы пользователя или на какие-то события, происходящие в вычислительной системе. Такие программы носят названия программ, резидентных в памяти (Terminate and Stay Resident, TSR), или просто резидентных программ. Сделать резидентной можно как программу типа СОМ, так и программу типа
ЕХЕ, однако ввиду того, что резидентная программа должна быть максимально компактной, чаще всего в качестве резидентных используют программы типа СОМ.
Рассмотрим типичную структуру резидентной программы и системные средства оставления ее в памяти после инициализации (рис. 2.1).
text segment 'code'
assume CS:text,DS:text
org 100h main proc
jmp init ;Переход на секцию инициализации
; Данные резидентной секции программы
. . .
entry: ; Текст резидентной секции программы
. . .
main endp
init proc ;Секция инициализации
. . .
mov DX, (init-main+10Fh)/16;Paзмер в параграфах
mov АН,3100h ;функция "Завершить и оставить в
int 21h ; памяти" init endp text ends
end main
Рис 2.1. Типичная структура резидентной программы.
Программа пишется в формате СОМ, поэтому в ней предусматривается только один сегмент, с котором связываются сегментные регистры CS и DS; в начале сегмента резервируется l00h байт дня PSP.
При запуске программы с клавиатуры управление передается (в соответствии с параметром директивы end) на начало процедуры main. Командой jmp сразу же осуществляется переход на секцию инициализации, которая может быть оформлена в виде отдельной процедуры или входить в состав процедуры main. В секции инициализации, в частности, подготавливаются условия для работы программы уже в резидентном состоянии. Последними строками секции инициализации вызывается функция DOS 31h, которая выполняет завершение программы с оставлением в памяти указанной ее части. Эта функция не может оставлять резидентными программы размером больше 64 Кб, но многие программы, написанные на ассемблере, соответствуют этому условию. Так как резидентные программы уменьшают объем основной памяти, их всегда пишут на ассемблере и оптимизируют для достижения минимального размера.
Размер резидентной части программы (в параграфах) передается DOS в регистре DX. Определить размер резидентной секции можно, например, следующим образом. К разности смещений mil-main, которая равна длине резидентной части программы в байтах, прибавляется размер PSP (l00h) и еще число 15 (Fh) для того, чтобы после целочисленного деления на 16 результат был округлен в большую сторону.
С целью экономии памяти секция инициализации располагается я конце программы и отбрасывается при ее завершении.
Точка входа main
при загрузке jmp init
. Резидентные
поля данных Резидентная часть
Точка входа entry программы
при вызове . Резидентные
коды
iret
init
. Секция
инициализации Завершение программы
Функция DOS 31h с составлением в памяти
её резидентной части
Рис. 2.2 Взаимодействие элементов резидентной программы.
Функция 31h, закрепив за резидентной программой необходимую для ее функционирования память, передает управление командному процессору и вычислительная система переходит в исходное состояние. Наличие программы, резидентной в памяти, никак не отражается на хода вычислительного процесса, за исключением того, что уменьшается объем свободной памяти. Одновременно в память может быть загружено любое число резидентных программ.
На рис. 2.2 показаны элементы резидентной программы и их взаимодействие.
Любая резидентная программа имеет по крайней мере две точки входа. При запуске с клавиатуры программы типа .СОМ управление всегда передается на первый байт после PSP (IP=l00h). Поэтому практически всегда первой командой резидентной программы является команда jmp, передающая управление на начало секции инициализации.
После отработки функции DOS 31h программа остается в памяти в пассивном состоянии. Для того, чтобы активизировать резидентную программу, ей надо как-то передать управление и, возможно, параметры. Вызвать к жизни резидентную программу можно разными способами, но наиболее употребительным является механизм аппаратных или программных прерываний. В этом случае в секции инициализации необходимо заполнить соответствующий вектор адресом резидентной части программы (точка entry на рис. 2.2). Адрес entry образует вторую точку входа в программу, через которую осуществляется ее активизация. Очевидно, что резидентная секция программы должна заканчиваться командой выхода из прерывания iret.
Поля данных резидентной части программы переместились в начало программы после команды imp. Это довольно естественное место дня резидентных данных, потому что и при первом запуске, и при активизации сюда никогда не будет передано управление. При заполнении в секции инициализации векторов не возникает проблем с перенастройкой регистра DS, так как в программе типа СОМ все регистры указывают на единственный сегмент программы. В секции инициализации предусмотрен, как это обычно делается, вывод на экран сообщения о загрузке программы в память.
После запуска программы она остается в памяти и, активизируясь фактически аппаратными прерываниями от клавиатуры (а более точно – программой BIOS, активизируемой аппаратными прерываниями от клавиатуры).
2.2. Защита резидентной программы от повторной установки
Как правило, в секции инициализации загружаются векторы прерываний, через которые будет активизироваться программа. Последними строками секции инициализации вызывается функция DOS 31h, которая выполняет завершение программы с оставлением в памяти ее резидентной части.
Если программу запустить с клавиатуры повторно, в память будет загружена и останется резидентной ее вторая копия. Это плохо не только потому, что понапрасну расходуется память, более неприятным является вторичный перехват тех же векторов. Если резидентная программа после ее активизации не обращается к старому содержимому перехваченных ею векторов, то вторая копия полностью лишит первую работоспособности, и тогда повторная загрузка приведет только к расходованию памяти. Если, однако, как это обычно и имеет место, резидентная программа в процессе своей работы передаст управление старому обработчику перехваченного ею прерывания, то новая копия резидентной программы, сохранившая в процессе инициализации адрес первой копии в качестве содержимого перехватываемого вектора, будет при каждой активизации вызывать и первую копию. В результате резидентная программа будет фактически выполняться при каждом вызове дважды. Во многих случаях такое повторное выполнение нарушит правильную работу программы. Поэтому обязательным элементом любой резидентной программы является процедура защиты ее от повторной загрузки, или, как говорят, установки.
Наиболее распространенным методом защиты резидентной программы от повторной установки является использование прерывания 2Fh, специально предназначенного для связи с резидентными программами. При вызове этого прерывания в регистре АН задается номер функции (от 00h до FFh), а в регистре AL - номер подфункции (в том же диапазоне). 00h - 7Fh зарезервировано для DOS/Windows 0B8h - 0BFh зарезервировано для сетевых функций 0C0h - 0FFh отводится для программ.
Для того, чтобы резидентная программа могла отозваться на вызов прерывания int 2Fh, в ней должен иметься обработчик этого прерывания. Фактически все резидентные программы, как системные, так и прикладные, имеют такие обработчики, через которые осуществляется не только проверка на повторную установку, но и вообще связь с резидентной программой: смена режима ее работы или получение от неё в транзитную программу каких-то параметров. Задание действия, которое надлежит выполнить обработчику прерывания 2Fh конкретной резидентной программы, осуществляется с помощью номера подфункции, помещаемого перед вызовом прерывания в регистр AL
Таким образом, обработчик прерывания 2Fh резидентной программы должен, прежде всего, проверить номер функции в регистре АН; при обнаружении "своей" функции обработчик анализирует содержимое регистра AL и выполняет затребованные действия, после чего командой iret передаст управление вызвавшей его программе. Если, однако, обработчик обнаружил в регистре АН "чужую" функцию, он должен командой jmp CS:old_2fh передать управление по цепочке тому обработчику, адрес которого был ранее в векторе 2Fh. В результате вызов int 2Fh из любой программы будет проходить по цепочке через все загруженные резидентные программы, пока не достигнет "своей" программы или не вернет управление в вызвавшую программу через обработчик DOS (который, очевидно, всегда будет самым последним в цепочке).
Естественно, для коммуникации с резидентной программой должен быть установлен некоторый интерфейс. Обычно при проверке на повторную установку резидентная программа, если она уже находится в памяти, возвращает в регистре AL значение FFh, которое является признаком запрета вторичной загрузки. Иногда для большей надежности идентификации "своей" функции резидентная программа, помимо значения FFh в регистре AL, возвращает еще какие-то обусловленные заранее коды в других регистрах. Часто через дополнительные регистры передастся символьная информация, например, имя программы. В этом случае, если вызвавшая программа с именем DUMP.COM (т.е. вторая копия резидентной программы, выясняющая, можно ли ей остаться резидентной в памяти) получает после вызова int 2Fh в регистре AL значение FFh, а в регистрах СХ и DX символьные коды 'DU' и 'МР', она может быть уверена, что ее первая копия уже находится в памяти. Если же в регистре AL вернулся код FFh, а в регистрах СХ и DX -коды, например, 'ОК' и 'RB', это, скорее всего означает, что закрепленная за нашей программой функция мультиплексного прерывания ухе используется другой резидентной программой. В этом случае стоит сменить функцию, чтобы не возбуждать конфликтных ситуаций.
В резидентную часть следует включить обработчик прерывания 2Fh. Его расположение в пределах текста программы не имеет особого значения; мы поместили его в начале резидентной части. Секция инициализации претерпела большие изменения. Она должна начинаться с вызова прерывания 2Fh с соответствующей функций для проверки на повторную установку. Если первая копия программы уже загружена, текущую программу следует завершить не функцией 3th (завершить и оставить в памяти), а обычной функцией завершения 4Ch. Если же нашей программы в памяти нет, то в секции инициализации, помимо заполнения ее "рабочего" вектора, в данном случае 03h, следует также установить наш обработчик мультиплексного прерывания.
Среди функций мультиплексного прерывания, предназначенных для прикладных программ, мы произвольно выбрали для нашей программы функцию F1h, а для проверки на повторную установку подфункцию 00h. Резидентный обработчик прерывания 2Fh, включенный в нашу программу, проверяет номера функции и подфункции и при обнаружении каких-либо других кодов передает управление следующему обработчику этого прерывания. Если же вызвана функция F1h с подфункцией 00h, обработчик устанавливает в регистре AL значение FFh ("я уже загружен") и возвращает управление в вызвавшую программу командой iret.
Секция инициализации начинается с проверки на повторную установку. После загрузки в регистр АН номера функции (F1h), а в регистр AL - номера подфункции (00h), вызывается прерывание 2Fh. После возврата из прерывания анализируется содержимое регистра AL Если обработчик вернул значение FFh, программа должна завершиться без оставления в памяти. Эти действия выполняются по метке installed. Если возвращено другое значение, инициализация продолжается (для надежности стоило проверить, возвращен ли именно 0). Сохраняется старое содержимое вектора 2Fh, устанавливается наш обработчик этого прерывания, после чего выполняются все действия по установке, предусмотренные в старом варианте программы динамического дампа. При переходе на метку installed на экран выводится сообщение о невозможности повторной установки и выполняется функция завершения 4Сh с кодом возврата 01h. Последнее, конечно, имеет символический характер, поскольку этот код в дальнейшем не анализируется.
2.3. Выгрузка резидентной программы из памяти
Следует заметить, что в DOS отсутствуют средства выгрузки резидентных программ. Единственный предусмотренный для этого механизм - перезагрузка компьютера. Практически, однако, большинство резидентных
программных продуктов имеют встроенные средства выгрузки. Обычно выгрузка резидентной программы осуществляется соответствующей командой, подаваемой с клавиатуры и воспринимаемой резидентной программой. Для этого резидентная программа должна перехватывать прерывания, поступающие с клавиатуры, и "вылавливать" команды выгрузки. Другой, боже простой способ заключается в запуске некоторой программы, которая с помощью, например, мультиплексного прерывания 2Fh передает резидентной программе команду выгрузки. Чаще всего в качестве "выгружающей" используют саму резидентную программу, точнее, ее вторую копию, которая, если ее запустить в определенном режиме, не только не пытается остаться в памяти, но, наоборот, выгружает из памяти свою первую копию.
Выгрузку резидентной программы из памяти можно осуществить разными способами. Наиболее простой - освободить блоки памяти, занимаемые программой (собственно программой и ее окружением) с помощью функции DOS 49h. Другой, более сложный - использовать в выгружающей программе функцию завершения 4Ch, заставив ее завершить не саму выгружающую, а резидентную программу, да еще после этого вернуть управление в выгружающую. В любом случае перед освобождением памяти необходимо восстановить все векторы прерываний, перехваченные резидентной программой. Следует подчеркнуть, что восстановление векторов представляет в общем случае значительную и иногда даже неразрешимую проблему. Во-первых, старое содержимое вектора, которое хранится где-то в полях данных резидентной программы, невозможно извлечь "снаружи", из другой программы, так как нет никаких способов определить, где именно его спрятала резидентная программа в процессе инициализации. Поэтому выгрузку резидентной программы легче осуществить из нее самой, чем из другой программы. Во-вторых, даже если выгрузку осуществляет сама резидентная программа, она может правильно восстановить старое содержимое вектора лишь в том случае, если этот вектор не был позже перехвачен другой резидентной программой. Если же это произошло, в таблице векторов находится уже адрес не выгружаемой, а следующей резидентной программы, и если восстановить старое содержимое вектора, эта следующая программа "повиснет", лишившись средств своего запуска. Поэтому надежно можно выгрузить только последнюю из загруженных резидентных программ.
В нашей программе подфункция 00h прерывания 2Fh служит для проверки на повторную установку, а подфункция 01h - для выгрузки. В секцию инициализации добавлены строки сохранения старого содержимого вектора 09h. Это выполняется точно так же, как и для вектора 2Fh - с помощью функции DOS 35h. Старый вектор сохраняется в ячейке old_09h, размещаемой в резидентной части программы. Поскольку выгрузка программы выполняется с помощью прерывания 2Fh, текст обработчика этого прерывания усложняется.
Резидентный обработчик прерывания 2Fh прежде всего проверяет номер функции, поступивший в регистре АН, Если этот номер отличается от F1h, управление передается следующему обработчику по цепочке. Далее анализируется содержимое регистра AL. Если AL=00h, выполняются действия по защите от повторной загрузки. Если AL=01h, осуществляется переход на метку uninstall для выполнения действий по выгрузке программы. При любом другом номере подфункции управление передается следующему обработчику по цепочке.
По метке uninstall осуществляется сохранение используемых далее регистров (что делается скорее для красоты, чем по необходимости) и функцией DOS 25h восстанавливается из ячеек old_09h и old_2Fh исходное содержимое соответствующих векторов. Далее из ячейки со смещением 2Ch относительно начала PSP в регистр ES загружается адрес окружения программы. Сегментный адрес освобождаемого блока памяти - единственный параметр, требуемый для выполнения функции DOS 49h. Размер освобождаемого блока DOS известен, он хранится в блоке управления памятью (МСВ). Далее освобождается блок памяти с самой программой. Сегментный адрес этого блока (адрес PSP) находится в регистре CS. Наконец, командой iret управление передастся в программу, вызвавшую прерывание 2Fh.
Функция 49h оповещает DOS о том, что данный блок памяти свободен и может впредь использоваться DOS. Это, однако, не мешает выполняться завершающим строкам программы (в данном случае – команде iret), поскольку освобождение памяти не разрушает ее содержимого. Наша резидентная программа физически сотрется лишь после того, как в память будет загружена очередная выполняемая программа.
Если программа запускается с клавиатуры с указанием каких-либо параметров (имен файлов, ключей, определяющих режим работы программы и проч.), то DOS, загрузив программу в память, помещает все символы, введенные после имени программы (так называемый хвост команды) в префикс программного сегмента программы, начиная с относительного адреса 80h. Хвост команды помещается в PSP во вполне определенном формате. В байт по адресу 80h DOS заносят число символов в хвосте команды (включая пробел, разделяющий на командной строке саму команду и ее хвост). Далее (начиная с байта по адресу 81h) следуют все символы, введенные с клавиатуры до нажатия клавиши <Enter>. Завершается хвост колом возврата каретки (13).
К данным секции инициализации добавилась строка с ожидаемым хвостом команды и байтовый флаг запроса на выгрузку.
Поскольку действия программы при её запуске зависят от того, введена ли команда запуска с параметром или нет, наличие хвоста в PSP анализируется в самом начале секции инициализации. При запуске программы типа СОМ все сегментные регистры указывают на начало PSP. Байт с длиной хвоста (возможно, нулевой) помещается в регистр CL и сравнивается с нулем. Если в нем 0, команда запуска была введена без параметров и инициализация программы продолжается обычным образом. Если хвост имеет ненулевую длину, начинается его анализ.
Обнулением регистра СН длина хвоста "расширяется" на весь регистр СХ, что нужно для организации цикла. Регистр DI настраивается на первый байт хвоста, а регистр SI – на начало поля tail с ожидаемой формой параметра. Регистр AL подготавливается для выполнения команды сканирования строки. Команда scasb сравнивает в цикле байты хвоста с содержимым AL (кодом пробела). Сравнение ведется до тех пор, пока не будет найден первый символ, отличный от пробела. Эта операция необходима из-за того, что оператор при вводе команды выгрузки может отделить параметр команды от самой команды любым числом пробелов, которые попадут в хвост команды в PSP и помешают анализировать введенный параметр.
Выход из цикла выполнения команды scasb осуществляется, когда команда проанализировала первый после пробела символ. После этого регистр DI указывает на второй символ параметра. Команда dec DI корректирует указатель DI, направляя его на первый значащий символ введенного параметра. Далее командой сравнения строк cmpsb осуществляется сравнение трех оставшихся символов хвоста. Если символы совпадают с параметром 'off', записанным в программе, устанавливается флаг запроса на выгрузку. Если результат сравнения оказался отрицательным, флаг запроса не устанавливается (и, следовательно, неправильный параметр просто не воспринимается). В любой случае осуществляется переход на продолжение программы, начинающей проверять, не установлена ли уже эта программа в памяти. Если программа еще не установлена, введенный параметр не имеет смысла. Инициализация осуществляется обычным образом: сохраняются и устанавливаются векторы и программа завершается с оставлением в памяти.
При наличии в памяти резидентной копии этой программы осуществляется переход на метку installed, где прежде всего проверяется, установлен ли флаг запроса на выгрузку. Если флаг сброшен, выводится сообщение о невозможности повторной загрузки и программа завершается с кодом возврата 1. Если флаг запроса установлен, выполняется выгрузка программы, которая заключается в вызове мультиплексного прерывания 2Fh с функцией F1h и подфункцией 01h. Резидентный обработчик этого прерывания, включенный в состав нашей резидентной программы, отработает эту подфункцию, восстановит векторы и освободит занятые программой блоки памяти. После возврата управления из обработчика в текущую программу будет выведено сообщение об успешной выгрузке и программа будет завершена функцией 4Ch с нулевым кодом возврата.
Составленная нами программа не избавлена от недостатков. Так, в ней анализируются всегда только 3 значащих символа хвоста. Таким образом, программа будет выгружена и при вводе команды (имя).com onset. Другой недостаток заключается в том, что результат сравнения записанного в программе хвоста с введенным с клавиатуры параметром будет положительным, только если с клавиатуры введены строчные буквы. Команда (имя) OFF не приведет к выгрузке программы. По-настоящему следовало включить в программу перед анализом хвоста преобразование символов параметра в прописные буквы.
2.4. Перехват прерываний
В архитектуре процессоров 80х86 предусмотрены особые случаи, когда процессор прекращает (прерывает) выполнение текущей программы и немедленно передает управление программе-обработчику, специально написанной для обработки подобной ситуации. Такие особые ситуации делятся на два тина: прерывания и исключения, в зависимости от того, вызвало ли эту ситуацию какое-нибудь внешнее устройство или выполняемая процессором команда. Исключения делятся далее на три типа: ошибки, ловушки и остановы, в зависимости от того, когда по отношению к вызвавшей их команде они происходят. Ошибки появляются перед выполнением команды, поэтому обработчик такого исключения получит в качестве адреса возврата адрес ошибочной команды (начиная с процессоров 80286). Ловушки происходят сразу после выполнения команды, так что обработчик получает в качестве адреса возврата адрес следующей команды. И наконец, остановы могут возникать в любой момент и вообще не предусматривать средств возврата управления в программу.
Команда INT (а также INTO и INT3) используется в программах как раз для того, чтобы вызывать обработчики прерываний (или исключений). Фактически они являются исключениями ловушки, поскольку адрес возврата, который передастся обработчику, указывает на следующую команду, но так как эти команды были введены до разделения особых ситуаций на прерывания и исключения, их практически всегда называют командами вызова прерываний. Ввиду того, что обработчики прерываний и исключений в DOS обычно не различают механизм вызова, с помощью команды INT можно передавать управление, как на обработчики прерываний, так и исключений. Как показано в главе 4, программные прерывания, то есть передача управления при помощи команды INT, являются основным средством вызова процедур DOS и BIOS, потому что в отличие от вызова через команду CALL здесь не нужно знать адреса вызываемой процедуры - достаточно только номера. С другой стороны интерфейса рассмотрим, как строится обработчик программного прерывания.
2.5. Обработчики прерываний
Когда в реальном режиме выполняется команда INT, управление передается по адресу, который считывается из специального массива, таблицы векторов прерываний, начинающегося в памяти по адресу 0000h:0000h. Каждый элемент такого массива представляет собой дальний адрес обработчика прерывания в формате сегмент:смещение или 4 нулевых байта, если обработчик не установлен. Команда INT помещает в стек регистр флагов и дальний адрес возврата, поэтому, чтобы завершить обработчик, надо выполнить команды popf и retf или одну команду iret, которая в реальном режиме полностью им аналогична.
После того как обработчик написан, следующий шаг - привязка его к выбранному номеру прерывания. Это можно сделать, прямо записав его адрес в таблицу векторов прерываний.
Хотя прямое изменение таблицы векторов прерываний и кажется достаточно удобным, все-таки это не лучший подход к установке обработчика прерывания, и пользоваться им следует только в исключительных случаях, например, внутри обработчиков прерываний. Для обычных программ DOS предоставляет две системные функции: 25h и 35h - установить и считать адрес обработчика прерывания, которые и рекомендуются к использованию в обычных условиях.
Обычно обработчики прерываний применяют с целью обработки прерывания от внешних устройств или с целью обслуживания запросов других программ.
2.6. Прерывания от внешних устройств
Прерывания от внешних устройств или аппаратные прерывания, - это то, что понимается под термином «прерывание». Внешние устройства (клавиатура, дисковод, таймер, звуковая карта и т. д.) подают сигнал, по которому процессор прерывает выполнение программы и передает управление на обработчик прерывания. Всего на персональных компьютерах используется 15 аппаратных прерываний, хотя теоретически возможности архитектуры позволяют довести их число до 64.
IRQ1 (INT 9) - прерывание клавиатуры, вызывается при каждом нажатии и отпускании клавиши на клавиатуре. Стандартный обработчик этого прерывания выполняет довольно много функций, начиная с перезагрузки по Ctrl-Alt-Del и заканчивая помещением кода клавиши в буфер клавиатуры BIOS.
Самые полезные для программ аппаратные прерывания — прерывания системного таймера и клавиатуры. Так как стандартные обработчики этих прерываний выполняют множество функций, от которых зависит работа системы, их нельзя заменять полностью.
прерванной программе. Этот способ применяют, если нужно, чтобы сначала отработал новый обработчик, а потом он передал управление старому
Резидентные программы, перехватывающие аппаратные прерывания, обладают свойством выполнятся одновременно с какой-либо другой программой. Именно для этого и применяется механизм аппаратных прерываний - они позволяют процессору выполнять одну программу, в то время как отдельные программы следят за временем, считывают символы из клавиатуры и помещают их в буфер, получают и передают данные через последовательные и параллельные порты и даже обеспечивают многозадачность, переключая процессор между разными задачами по прерыванию системного таймера.
Разумеется, обработка прерываний не должна занимать много времени: если прерывание происходит достаточно часто (например, прерывание последовательного порта может происходить 28 800 раз в секунду), его обработчик обязательно должен выполняться за более короткое время. Если, например, обработчик прерывания таймера будет выполняться 1/32,4 секунды, то есть половину времени между прерываниями, вся система станет работать в два раза медленнее. А если еще одна программа с таким же долгим обработчиком перехватит это прерывание, система остановится совсем. Именно поэтому обработчики прерываний принято писать исключительно на ассемблере.
2.7. Резидентный обработчик прерываний от клавиатуры с подключением до системного обработчика
Практически любая программа, в которой предусмотрено управление ходом ее выполнения с помощью команд, подаваемых с клавиатуры, имеет в своем составе обработчик прерываний от клавиатуры. В зависимости от стоящих перед ним задач, обработчик может подключаться до системного, выполняя обработку скэн-кодов нажимаемых клавиш, или после системною, работая в этом случае с кодами ASCII. возникающими на выходе системного обработчика. Нередки случаи, когда прикладной обработчик выполняет часть своих функций до системного, а часть - после. Настоящая и несколько последующих статей посвящены этому важному для прикладного программиста вопросу.
Для того чтобы написать, обработчик прерываний от клавиатуры, необходимо хорошо представлять, каким образом вводятся, куда попадают и как обрабатываются символы, вводимые с клавиатуры. Процесс взаимодействия системы с клавиатурой показан на рис. 2.3.
IRQ INT Адрес системного
Аппаратное Контроллер Микро- обработчика int09h
прерывание прерываний Вектор09 процессор из вектора 09
на IRQ1
IRQ7 Запуск систем
Нажатие или обработчика int09h
отпускание Байт флагов
любой клавиши Системный клавиатуры
Контроллер Порт 60h обработчик [40h:17h]
клавиатуры Скэн-код int09h | 7 | 6| 5| 4| 3| 2| 1| 0|
Клавиатура
Ins
Скэн-код Код Caps Lock
Кольцевой буфер ASCII Num Lock
40h:1Eh ввода Scroll Lock
40h:1Ah Alt
Адрес головного Скэн ASCII Ctrl
символа Скэн ASCII Shift левый
Скэн ASCII Shift правый
Программа Скэн ASCII
пользователя Адрес хвостового
символа
Запрос на ввод 40h:3Ch
с клавиатуры Ввод самого
“старого” символа
Рис. 2.3. Процесс взаимодействия системы с клавиатурой.
Работой клавиатуры управляет специальная электронная схема - контроллер клавиатуры. В его функции входит распознавание нажатой клавиши и помещение закрепленного за ней кода в свой выходной регистр (порт) с номером 60h. Код клавиши, поступающий в порт, называется скэн-кодом и является, по существу, порядковым номером кла-
виши. При этом каждой клавише присвоены два скэн-кода, отличающиеся друг от друга на 80h. Один скэн-код (меньший, код нажатия) засылается контроллером в порт 60h при нажатии клавиши, другой (больший, код отпускания) - при ее отпускании.
Скэн-код однозначно указывает на нажатую клавишу, однако, по нему нельзя определить, работает ли пользователь на нижнем или верхнем регистре. С другой стороны, скэн-коды присвоены всем клавишам клавиатуры, в том числе управляющим клавишам <Shift>, <Ctrl>, <Alt>, <Caps Lock> и др. Таким образом, очевидно, что определение введенного символа должно включать в себя не только считывание скэн-кода нажатой клавиши, но и выяснение того, не были ли перед этим нажаты, например, клавиши <Shift> (верхний регистр) или <Caps Lock> (фиксация верхнего регистра). Всем этим анализом занимается программа обработки прерываний от клавиатуры.
Как нажатие, так и отпускание любой клавиши вызывает сигнал аппаратного прерывания, заставляющий процессор прервать выполняемую программу и перейти на программу системного обработчика прерываний от клавиатуры, входящего в систему BIOS. Поскольку обработчик вызывается через вектор 09h, его иногда называют программой int09h.
Программа int09h, помимо порта 60h, работает еще с двумя областями оперативной памяти: кольцевым буфером ввода, располагаемым по адресам от 40h:lEh до 40h:3Dh, куда в конце концов помещаются коды ASCII нажатых клавиш, и битом флагов клавиатуры, находящимся по адресу 40h:17h, где фиксируется состояние управляющих клавиш (<Shift>, <Caps Lock>, <Num Lock> и др.).
Программа int09h, получив управление в результате прерывания от клавиатуры, считывает из порта 60h скэн-код и анализирует его значение. Если скэн-код принадлежит одной из управляющих клавиш, и, к тому же, представляет собой код нажатия, в байте флагов клавиатуры устанавливается бит (флаг), соответствующий нажатой клавише. Например, при нажатии правой клавиши <Shift> в байте флагов устанавливается бит 0, при нажатии левой клавиши <Shift> - бит 1, при нажатии любой клавиши <Ctrl> - бит 2 и т.д. Биты флагов сохраняют свое состояние, пока клавиши (по одиночке или в любых комбинациях) остаются нажатыми. Если управляющая клавиша отпускается, программа int09h получает скэн-код отпускания и сбрасывает соответствующий бит в байте флагов. Кроме состояния указанных клавиш, в байте флагов фиксируются еще режимы <Scroll Lock>, <Num Lock>, <Caps Lock> и <Insert> (см. рис. 2.3).
Компьютеры PC/AT имеют второй байт флагов клавиатуры, находящийся по адресу 40h:18h, и отражающий состояние управляющих клавиш на расширенной (101-клавишной) клавиатуре.
При нажатии обычной, не управляющей клавиши, программа int09h считывает из порта 60h ее скэн-код нажатия и по таблице трансляции скэн-кодов в коды ASCII формирует двухбайтовый код, старший байт которого содержит скэн-код, а младший код ASCII. При этом если скэн-код характеризует клавишу, то код ASCII определяет закрепленный за ней символ.
Поскольку за каждой клавишей закреплено, как правило, не менее двух символов ("а" и "А", "1" и "!", "2" и "@" и т.д.), то каждому скэн-коду соответствуют, как минимум, два кода ASCII. В процессе трансляции программа int09h анализирует состояние флагов, так что если нажата, например, клавиша Q (скэн-код 10h, код ASCII буквы Q - 51h, а буквы q - 7lh), то формируется двухбайтовый код 1071h, но если клавиша Q нажата при нажатой клавише <Shift> (смена регистра), то результат трансляции составит 1051h. Тот же код 1051h получится, если при нажатии клавиши Q был включен режим <Caps Lock> (заглавные буквы), однако при включенном режиме <Caps Lock> и нажатой клавише <Shift> образуется код 1071h, поскольку в такой ситуации клавиша <Shift> на время нажатия переводит клавиатуру в режим нижнего регистре (строчные буквы).
Полученный в результате трансляции двухбайтовый код засылается программой int09h в кольцевой буфер ввода, который служит для синхронизации процессов ввода данных с клавиатуры и приема их выполняемой компьютером программой. Объем буфера составляет 16 слов, при этом коды символов извлекаются из него в том же порядке, в каком они в него поступали. За состоянием буфера следят два указателя. В хвостовом указателе (слово по адресу 40:lCh) хранится адрес первой свободной ячейки, в головном указателе (40: 1Ah) - адрес самого старого кода, принятого с клавиатуры и еще не востребованного программой. Оба адреса представляют собой смещения относительно начала области данных BIOS, т.е. числа от 1Eh до 3Ch. В начале работы, когда буфер пуст, оба указателя - и хвостовой, и головной, указывают на первую ячейку буфера.
Программа int09h, сформировав двухбайтовый код, помещает его в буфер по адресу, находящемуся в хвостовом указателе, после чего этот адрес увеличивается на 2, указывая опять на первую свободную ячейку. Каждое последующее нажатие на какую-либо клавишу добавляет в буфер очередной двухбайтовый код и смещает хвостовой указатель.
Выполняемая программа, желая получить код нажатой клавиши, должна обратиться для этого к каким-либо системным средствам - функциям ввода с клавиатуры DOS (прерывание 21h) или BIOS (прерывание 16h). Системные программы с помощью драйвера клавиатуры (точнее говоря, объединенного драйвера клавиатуры и экрана, так называемого драйвера консоли с именем CON) считывают из кольцевого буфера содержимое ячейки, адрес которой находится в головном указателе, и увеличивает этот адрес на 2. Таким образом, программный запрос на ввод с клавиатуры фактически выполняет прием кода не с клавиатуры, а из кольцевого буфера.
Хвостовой указатель, перемещаясь по буферу в процессе занесения в него кодов, доходит, наконец, до конца буфера (адрес 40h:3Ch). В этом случае при поступлении очередного кода адрес в указателе не увеличивается, а, наоборот, уменьшается на длину буфера. Тем самым указатель возвращается в начало буфера, затем продолжает перемещаться по буферу до его конца, опять возвращается в начало и так далее по кольцу. Аналогичные манипуляции выполняются и с головным указателем.
Равенство адресов в обоих указателях свидетельствует о том, что буфер пуст. Если при этом программа поставила запрос на ввод символа с клавиатуры, то драйвер консоли будет ждать поступления кода в буфер, после чего этот код будет передан в программу. Если же хвостовой указатель, перемещаясь по буферу в процессе его заполнения, подошел к головному указателю "с обратной стороны" (это произойдет, если оператор нажимает на клавиши, а выполняемая в настоящий момент программа не обращается к клавиатуре), прием новых кодов блокируется, а нажатие на клавиши возбуждает предупреждающие звуковые сигналы.
Если компьютер не выполняет никакой программы, то активной является программа командного процессора COMMAND.COM. Активность COMMAND.COM заключается в том, что он, поставив запрос к DOS на ввод с клавиатуры (с помощью функции 0Ah прерывания 21h) ожидает ввода с клавиатуры очередной команды пользователя. Как только в кольцевом буфере ввода появляется код символа, функция 0Ah переносит его во внутренний буфер DOS, очищая при этом кольцевой буфер ввода, а также выводит символ на экран. При получении кода клавиши <Enter> (0Dh) функция 0Ah завершает свою работу, а командный процессор предполагает, что ввод команды закончен, анализирует содержимое буфера DOS и приступает к выполнению введенной команды. При этом командный процессор работает практически лишь с младшими половинами двухбайтовых кодов символов, именно, с кодами ASCII.
Если компьютер выполняет какую-либо программу, ведущую диалог с оператором, то, как ухе отмечалось, ввод данных с клавиатуры (а точнее из кольцевого буфера ввода) и вывод их на экран с целью эхо контроля организует эта программа, обращаясь непосредственно к драйверу BIOS (int 16h) или к соответствующей функции DOS (int 21h). Может случиться, однако, что выполняемой программе не требуется ввод с клавиатуры, а оператор нажал какие-то клавиши. В этом случае вводимые символы накапливаются (с помощью программы int09h) в кольцевом буфере ввода и, естественно, не отображаются на экране. Так можно ввести до 15 символов. Когда программа завершится, управление будет передано COMMAND. СОМ, который сразу же обнаружит наличие символов в кольцевом буфере, извлечет их оттуда и отобразит на экране. Такой ввод с клавиатуры называют вводом с упреждением.
До сих пор речь шла о символах и кодах ASCII, которым соответствуют определенные клавиши терминала и которые можно отобразить на экране. Это буквы (прописные и строчные), цифры, знаки препинания и специальные знаки, используемые в программах и командных строках, например, |, $, * и др. Однако имеется ряд клавиш, которым не назначены отображаемые на экране символы. Это, например, функциональные клавиши <F1>, <F2>...<F10>; клавиши управления курсором <Home>, <End>, <PgUp>, <PgDn>, <Стрелка вправо>, <Стрелка вниз> и др. При нажатии этих клавиш в кольцевой буфер ввода засылается расширенный код ASCII, в котором младший байт равен нулю, а старший является скэн-кодом нажатой клавиши. Расширенный коды ASCII поступают в буфер ввода и в случае нажатия комбинаций управляющих и функциональных клавиш, например, <Shift>/<FI>, <Ctrl>/<Home> (на дополнительной цифровой клавиатуре), <Alt>/<Insert> и др. В этом случае, однако, в старший байт расширенного кода ASCII помещается уже не скэн-код клавиши, а некоторый код, специально назначенный этой комбинации клавиш. Естественно, этого кода нет среди "обычных" скэн-кодов. Например, клавиша <F1>, скэн-код которой равен 3Bh, может генерировать следующие расширенные коды ASCII:
<F1> ЗB00h <Ctrl>/<F1> 5E00h <Alt>/<F1> 6800h <Shift>/<F1> 5400h
Итак, прерывание, возникающее при нажатии или отпускании любой клавиши, обрабатывается по относительно сложному алгоритму с системным обработчиком, содержащимся в BIOS. Рассмотрим примеры вмешательства в этот процесс. Ниже приведен пример прикладной программы, выполняющей некоторую обработку поступающих с клавиатуры данных еще до активизации системного обработчика.
3. Описание программы
3.1. Описание для пользователя
Приведённая ниже программа осуществляет перехват прерывания от клавиатуры, и производит запись скэн-кодов клавиш и байта флагов клавиатуры в файл с именем « s_code&f.txt ». При этом фиксируются только нажатия клавиш. Запись происходит при каждом шестнадцатом нажатии клавиши. Это сделано, во-первых, для уменьшения вероятности потери «ценных» нажатий при экстренном выключении компьютера, во-вторых, для экономии оперативной памяти, в-третьих, для сохранения нормальной работоспособности компьютера. Файл « s_code&f.txt » создаётся в родительском каталоге программы. Если при инсталляции файл уже существует, то программа, автоматически, запишет в конец текущую дату и время, после этого будет осуществляться запись скэн-кодов и флагов в обычном режиме после даты и времени. Программа является резидентной. После того как она будет успешно инсталлирована, на экране появится соответствующая надпись “Program installed”. В ней предусмотрена защита от повторной установки. Таким образом одновременно в оперативной памяти компьютера не может находится больше одной копии программы, что практически сводит к нулю шансы не корректной работы. При попытке запустить программу после того как она уже была инсталлирована, на экране появится соответствующая надпись “Program already installed”. Также эту программу можно выгрузить из оперативной памяти после того как потребность в ней отпадёт. Для этого следует запустить программу с ключом “off”, т.е. в командной строке написать <имя программы> off . После этого вы увидите строку “Program is DIE”, сигнализирующую об успешной выгрузке программы. При этом содержимое буфера будет записано в файл. Таким образом, в файл будут записаны все нажатия клавиш вплоть до выгрузки программы. Если данную программу записать, например, в autoexec.bat, то можно будет проследить время начала работы пользователя и какие кнопки он после этого нажимал.
Данная программа работает только в среде MS-DOS.
3.2 Описание для программиста
Программа пишется в формате СОМ, поэтому в ней предусматривается только один сегмент, с котором связываются сегментные регистры CS и DS; в начале сегмента резервируется 256 байт дня PSP.
Инициализация.
При запуске программы с клавиатуры управление передается (в соответствии с параметром директивы end) на начало процедуры main. Командой jmp сразу же осуществляется переход на секцию инициализации, которая оформлена в виде отдельной процедуры. В секции инициализации подготавливаются условия для работы программы уже в резидентном состоянии.
В начальной части инициализации мы проверяем наличие хвоста в PSP, если же в командной строке кроме имени команды ничего не было – переходим на дальнейший анализ:
mov cl,es:80h
cmp cl,0
je live
Если хвост присутствует, проверим не был ли введён ожидаемый параметр “off”. При положительном результате проверки устанавливаем флаг требования выгрузки “flag” в единицу и переходим на дальнейший анализ.
Затем вызываем мультиплицированное прерывание int2Fh c функцией F1h и подфункцией проверки на повторную установку 00h. Если наш обработчик находится в оперативной памяти – он возвратит AL=FFH, и программа перейдёт на метку installed. Проверим установлен ли флаг требования выгрузки “flag” . Если flag =1 перейдём на метку unins, где перешлём в первую (резидентную) копию программы запрос на выгрузку из оперативной памяти по средствам прерывания int2Fh и функцией F2h с подфункцией 01h. После чего происходит вывод строки “Program is DIE” на экран сигнализирующей об успешном удалении резидентной части программы. После чего выйдем из программы, обычным образом, функцией 4С00h.
Если флаг требования выгрузки “flag”=0, это говорит о том, что введена неизвестная команда, а наш резидент уже инсталлирован. В этом случае выведем на экран предупреждающую надпись о невозможности повторной установки программы “Program already installed” сопровождаемую звуковым сигналом. После этого завершим программу функцией 4Ch с кодом возврата 01h.
Если после прерывания int2Fh c функцией F200h, возвратиться ALFFh, то нашего обработчика в памяти не оказалось. Сохраним смещения и сегменты системных обработчиков int09h и int2Fh, а затем заполним векторы смещениями наших обработчиков.
mov ax,352fh
int 21h
mov word ptr cs:old_2fh,bx
mov word ptr cs:old_2fh+2,es
mov ax,252fh
mov dx,offset new_2fh
int 21h
mov ax,3509h
int 21h
mov word ptr cs:old_09h,bx
mov word ptr cs:old_09h+2,es
mov ax,2509h
mov dx,offset new_09h
int 21h
После этого произведём поиск рабочего файла «s_code&f.txt» в текущем каталоге. Если файл не будет найден, то запустится процедура div_f, которая создаст рабочий файл и запишет в него строку «Skencode&Klav_flag file». В дальнейшем в этот файл будут записываться скэн-коды и байт флагов клавиатуры. Если файл уже существует, будет вызвана процедура div2_f, которая допишет в конец файла текущую дату и время.
Выведем на экран строку «Program installed» подтверждающую установку программы. Последними строками этой части инициализации вызывается функция DOS 31h, которая выполняет завершение программы с оставлением в памяти указанной ее части. Размер резидентной части программы (в параграфах) передается DOS в регистре DX. Размер резидентной секции определяется разностью смещений end_res-main, которая равна длине резидентной части программы в байтах, прибавляется размер PSP (l00h) и еще число 15 (Fh) для того, чтобы после целочисленного деления на 16 результат был округлен в большую сторону.
mov ax,3100h
mov dx,(end_res-main+10fh)/16
int 21h
С целью экономии памяти секция инициализации располагается я конце программы и отбрасывается при ее завершении.
Функция 31h, закрепив за резидентной программой необходимую для ее функционирования память, передает управление командному процессору и вычислительная система переходит в исходное состояние. Наличие программы, резидентной в памяти, никак не отражается на хода вычислительного процесса, за исключением того, что уменьшается объем свободной памяти. Одновременно в память может быть загружено любое число резидентных программ.
Резидентная часть обработчика.
Эта секция программы имеет две точки входа:
Перехват прерывания int09h(клавиатура). В результате нажатия или отпускания клавиши на клавиатуре запускается процедура new_09h.
Перехват мультиплексорного прерывания int2Fh. В результате перехвата мультиплексорного прерывания запускается процедура new_2fh.
Обработчик прерывания от клавиатуры.
После запуска процедуры new_09h сохраним используемые регистры. Затем получим скэн-код последней нажатой клавиши. В противном случае восстановим регистры и передадим управление следующему по цепочке обработчику клавиатуры (скорее всего это будет BIOS-овский обработчик «int09h»).
in al,60h
cmp al,80h
ja exit
Затем запишем этот скэн-код в буфер, считаем байт флагов клавиатуры из области данных BIOS и также занесём в буфер.
Наш буфер имеет объём 32 байта, поэтому после каждого шестнадцатого нажатия необходимо сохранять буфер в рабочем файле. Для подсчёта нажатий введена переменная-счётчик sch.
Увеличим счётчик на 2, затем проверим полон ли буфер, сравнив счётчик с 32. Если буфер не полон, сохраним использовавшиеся регистры и передадим управление следующему по цепочке обработчику клавиатуры. Если буфер забит, передадим управление процедуре fil.
Эта процедура откроет наш рабочий файл, установит указатель в конец и допишет столько байт из буфера начиная сначала, сколько укажет ей переменная-счётчик.
mov ah,40h
mov cl,sch
mov dx,offset bufer
int 21h
Это сделано для того, чтобы при удалении программы из памяти в файл были записаны все скэн-коды включая команду на удаление. Этот случай рассмотрим ниже. После того как данные будут сохранены, восстановим использовавшиеся регистры и передадим управление следующему по цепочке обработчику клавиатуры.
Обработчик мультиплексорного прерывания
Процедура new_2fh перехватит прерывание 2Fh, и если прерывание вызвано вместе с функцией F1h, то в зависимости от подфункции значение которой находится в AL выполнит следующие действия:
Если подфункция находящаяся в AL=00h (код наличия в памяти нашего обработчика), то наш обработчик возвратит в AL=FFh и выйдет из прерывания.
cmp al,00h
je inst
…
inst: mov al,0ffh
iret
Если подфункция находящаяся в AL=01h (команда на удаление из памяти обработчика), то сохраним используемые регистры, вызовем процедуру fil (работа этой процедуры была описана выше), а затем освободим блоки памяти занятые нашим обработчиком, восстановим старые векторы 09h и 2Fh. Восстановим использовавшиеся регистры и выйдем из прерывания.
Если мультиплексорное прерывание было вызвано с другой функцией либо с нашей функцией но с другими подфункциями, то обработчик передаст управление следующему по цепочке обработчику мультиплексорного прерывания.
cmp ah,0f1h
jne out_2fh
cmp al,00h
je inst
cmp al,01h
je off
jmp short out_2fh
inst: mov al,0ffh
iret
out_2fh:
3.3. Листинг программы
text segment 'code'
assume cs:text,ds:text
org 256
main proc
jmp init
; Поля данных резидентной секции
old_2fh dd 0 ; Ячейка для сохранения системного вектора 2Fh
old_09h dd 0 ; Ячейка для сохранения системного вектора 09h
bufer db 34 dup(?) ; Буфер для скэн-кодов и флагов клавиатуры
sch db 0 ; Счётчик нажатий клавиш
filename db 's_code&f.txt',0 ; Константа содержащая имя файла с которым работает программа
; Обработчик от клавиатуры
new_09h proc
; Сохраним используемые регистры
push ax
push bx
push cx
push dx
push ds
push cs ; Настроим DS на наш сегмент для простоты программирования
pop ds
in al,60h ; Получим скэн-код клавиши
cmp al,80h ; Проверим, является ли скэн-код кодом нажатия
ja exit ; Нет – на выход
mov bh,0 ; 0BH
mov bl,sch ; Текущее значения счётчика в BL
mov [bufer+bx],al ; Запишем в буфер скэн-код клавиши
inc bl ; Увеличим смещение буфера
push es ; Сохраним регистр ES
mov ax,40h ; Настроим ES на начало области данных BIOS
mov es,ax
mov al,es:[17h] ; Занесём байт флагов клавиатуры в AL
pop es ; Восстановим ES
mov [bufer+bx],al ; Запишем байт флагов в буфер
inc bl ; Увеличим смещение на 1
add sch,2 ; Счётчик нажатий +2
cmp sch,32 ; Пора скидывать буфер в файл?
je go ; Да – на процедуру записи в файл
jmp exit ; Нет – на выход
go: call fil ; Вызов процедуры записи в файл
; Восстановим использовавшиеся регистры
exit: pop ds
pop dx
pop cx
pop bx
pop ax
jmp cs:old_09h ; Передадим управление системному обработчику “int09h”
new_09h endp ; Конец процедуры обработчика от клавиатуры
; Процедура записи в файл скэн-кодов и флагов клавиатуры
fil proc
push cs ; Настроим DS на наш сегмент
pop ds
mov ah,3dh ; Функция открытия файла
mov al,1 ; для записи
mov dx,offset filename ; DS:DX ASCIIZ имени файла
int 21h
mov bx,ax ; Дескриптор в ВХ
xor cx,cx ; Отчистим СХ
xor dx,dx ; и DX
mov ax,4202h ; Функция установки указателя в конец файла
int 21h
mov ah,40h ; Функция записи в файл
mov cl,sch ; CL количество байт
mov dx,offset bufer ; DS:DX адрес буфера
int 21h
mov ah,3eh ; Функция закрытия файла
int 21h
mov sch,0 ; Обнулим счётчик
ret ; Выход из процедуры
fil endp ; Конец процедуры записи в файл
; Обработчик мультиплексорного прерывания
new_2fh proc
cmp ah,0f1h ; Проверим номер функции мультиплексорного прерывания
jne out_2fh ; Не наша – на выход
cmp al,00h ; Подфункция проверки на повторную установку?
je inst ; Да, сообщим о невозможности повторной установки
cmp al,01h ; Подфункция выгрузки?
je off ; Да – на выгрузку
jmp short out_2fh ; Неизвестная подфункция, на выход
inst: mov al,0ffh ; Программа уже установлена
iret ; Выход из прерывания
out_2fh:
jmp cs:old_2fh ; Переход в следующий по цепочке обработчик прерывания 2Fh
; Выгрузим программу из памяти, предварительно восстановив все перехваченные ею векторы
; Сохраним используемые регистры
off: push ds
push es
push dx
push ax
push bx
push cx
call fil ; Вызов процедуры записи в файл содержимого буфера
; Восстановим использовавшиеся регистры
pop cx
pop bx
pop ax
; Восстановим вектор 09h
mov ax,2509h ; Функция установки вектора
lds dx,cs:old_09h ; Заполним DS:DX
int 21h
; Восстановим вектор 2fh
mov ax,252fh ; Функция установки вектора
lds dx,cs:old_2fh ; Заполним DS:DX
int 21h
; Получим из PSP адрес собственного окружения и выгрузим его
mov es,cs:2ch ; ES окружение
mov ah,49h ; Функция освобождения блока памяти
int 21h
; Выгрузим теперь саму программу
push cs ; Загрузим в ES содержимое CS, т.е. сегментный адрес PSP
pop es
mov ah,49h ; Функция освобождения блока памяти
int 21h
; Восстановим использовавшиеся регистры
pop dx
pop es
pop ds
iret ; Возврат в вызвавшую программу
new_2fh endp ; Конец процедуры обработки прерывания 2Fh
end_res=$ ; Смещение конца резидентной части программы
main endp
tail db 'off' ; Ожидаемый хвост команды
flag db 0 ; Флаг требования выгрузки
tabl db '0123456789' ; Таблица для перевода BCD кода в ASCII
time db 25 dup(?) ; Ячейка для сохранения текущей даты и времени
; Процедура создания файла
div_f proc
mov ah,3ch ; Функция создания файла
mov cx,0 ; Без атрибутов
lea dx,filename ; DS:DX ASCIIZ имени файла
int 21h
mov bx,ax ; Дескриптор в ВХ
mov ah,40h ; Функция записи в файл
mov cx,buflen ; CХ количество байт
lea dx,buf ; DS:DX адрес строки
int 21h
mov ah,3eh ; Функция закрытия файла
int 21h
ret ; Выход из процедуры
div_f endp ; Конец процедуры создания файла
; Процедура открытия файла и записи в него текущей даты и времени
div2_f proc
mov [time],0ah ; Запись в переменную time маркеров
mov [time+1],0dh ; перехода на следующую строку
mov ah,3dh ; Функция открытия файла
mov al,1 ; для записи
mov dx,offset filename ; DS:DX ASCIIZ имени файла
int 21h
mov bx,ax ; Дескриптор в ВХ
push bx ; Сохраним дескриптор
xor cx,cx ; Отчистим СХ
xor dx,dx ; и DX
mov ax,4202h ; Функция установки указателя в конец файла
int 21h
mov ah,02h ; Функция чтения времени из «постоянных» «CMOS» часов реального времени
int 1ah ; Прерывание ввода – вывода для времени
mov bx,offset tabl ; DS:DX адрес таблицы
mov si,2 ; Установим смещение для переменной time
mov ax,cx ; Часы и минуты сохраним в AX
mov cx,12 ; Установим счётчик сдвига
next: push ax ; Сохраним AX
shr ax,cl ; Сдвинем AX на CL
and al,0fh ; Получим номер ячейки в таблице прибавив маску
xlat ; Получим ASCII код числа
mov [time+si],al ; Занесём его в переменную time
inc si ; Увеличим на 1 смещение
cmp si,4 ; Смещение = 4 ?
je ras ; Да, переход на метку ras
vw: sub> cl,4 ; Нет, уменьшим CL на 4
pop ax ; Восстановим AX
cmp cl,-4 ; Сравним CL с -4
jne next ; Не равно – выполним ещё раз
jmp ent1 ; Равно – переход на ent1
ras: mov [time+si],':' ; Запишем в переменную time – «:»
inc si ; Увеличим на 1 смещение
jmp vw ; Перейдём на метку vw
ent1: mov [time+si],' ' ; Запишем в переменную time – « »
inc si ; Увеличим на 1 смещение
mov ah,04h ; Функция чтения даты из «постоянных» «CMOS» часов реального времени
int 1ah ; Прерывание ввода – вывода для времени
mov ax,dx ; Дату сохраним в AX
mov cx,12 ; Установим счётчик сдвига
next1: push ax ; Сохраним AX
shr ax,cl ; Сдвинем AX на CL
and al,0fh ; Получим номер ячейки в таблице прибавив маску
xlat ; Получим ASCII код числа
mov [time+si],al ; Занесём его в переменную time
inc si ; Увеличим на 1 смещение
cmp si,10 ; Смещение = 10 ?
je ras1 ; Да, переход на метку ras1
vw1: sub> cl,4 ; Нет, уменьшим CL на 4
pop ax ; Восстановим AX
cmp cl,-4 ; Сравним CL с -4
jne next1 ; Не равно – выполним ещё раз
jmp ent2 ; Равно – переход на ent2
ras1: mov [time+si],'.' ; Запишем в переменную time – «.»
inc si ; Увеличим на 1 смещение
jmp vw1 ; Перейдём на метку vw1
ent2: mov [time+si],0ah ; Запись в переменную time маркеров
mov [time+si+1],0dh ; перехода на следующую строку
pop bx ; Восстановим дескриптор
mov ah,40h ; Функция записи в файл
mov cx,20 ; CХ количество байт
mov dx,offset time ; DS:DX адрес строки
int 21h
mov ah,3eh ; Функция закрытия файла
int 21h
ret ; Выход из процедуры
div2_f endp ; Конец процедуры подготовки файла
; Процедура инициализации
init proc
mov cl,es:80h ; Получим длину хвоста PSP
cmp cl,0 ; Длина хвоста = 0 ?
je live ; Да программа запущена без параметров
xor ch,ch ; Теперь CX=CL=длина хвоста
mov di,81h ; DS:SI хвост в PSP
lea si,tail ; DS:SI поле tail
mov al,' ' ; Уберём пробелы из начала хвоста
repe scasb ; Сканируем хвост, пока пробелы
dec di ; DI первый символ после пробелов
mov cx,3 ; Ожидаемая длина параметра
repe cmpsb ; Сравниваем введённый хвост с ожидаемым
jne live ; Введена неизвестная команда
inc flag ; Введено «off», установим флаг запроса на выгрузку
; Проверим, не установлена ли уже данная программа
live: mov ah,0f1h ; Установим нашу функцию
mov al,0 ; и подфункцию на наличие нашей программы в оперативной памяти
int 2fh
cmp al,0ffh ; Программа установлена?
je installed ; Да, при наличии запроса на выгрузку её можно выгрузить
; Сохраним вектор 2fh
mov ax,352fh ; Функция получения вектора 2fh
int 21h
mov word ptr cs:old_2fh,bx ; Сохраним смещение системного обработчика
mov word ptr cs:old_2fh+2,es ; Сохраним сегмент системного обработчика
; Заполним вектор 2fh
mov ax,252fh ; Функция установления вектора прерывания 2fh
mov dx,offset new_2fh ; Смещение нашего обработчика
int 21h
; Сохраним вектор 09h
mov ax,3509h ; Функция получения вектора 09h
int 21h
mov word ptr cs:old_09h,bx ; Сохраним смещение системного обработчика
mov word ptr cs:old_09h+2,es ; Сохраним сегмент системного обработчика
; Заполним вектор 09h
mov ax,2509h ; Функция установления вектора прерывания 09h
mov dx,offset new_09h ; Смещение нашего обработчика
int 21h
mov ah,4eh ; Функция поиска файла
lea dx,filename ; DS:DX ASCIIZ имени файла
int 21h
cmp ax,12h ; Файл не найден?
je creat ; Да, создадим файл
call div2_f ; Нет, вызов процедуры открытия файла и записи в него текущей даты и времени
jmp by ; Переход на метку by
creat: call div_f ; Вызов процедуры создания файла
; Выведем на экран информационное сообщение
by: mov ah,09h ; Функция вывода на экран
lea dx,mes ; DS:DX адрес строки
int 21h
mov ax,3100h ; Функция «завершиться и остаться резидентным»
mov dx,(end_res-main+10fh)/16 ; Размер в параграфах
int 21h
installed:
cmp flag,1 ; Запрос на выгрузку установлен?
je unins ; Да, на выгрузку
; Выведем на экран информационное сообщение
mov ah,09h ; Функция вывода на экран
lea dx,mes1 ; DS:DX адрес строки
int 21h
; Выведем предупреждающий звуковой сигнал
mov cx,5 ; Количество гудков
mov ah,02h ; Функция вывода на экран
l: mov dl,07h ; ASCII код зуммера
int 21h
loop l ; Повторим CX раз
mov ax,4c01h ; Функция завершения с кодом возврата
int 21h
unins:
; Перешлём в первую (резидентную) копию программы запрос на выгрузку
mov ax,0f101h ; Наша функция с подфункцией выгрузки
int 2fh ; Мультиплексное прерывание
; Выведем на экран информационное сообщение
mov ah,09h ; Функция вывода на экран
lea dx,mes2 ; DS:DX адрес строки
int 21h
mov ax,4c00h ; Функция завершения программы
int 21h
buf db 'Skencode&Klav_flag file',0ah,0dh
buflen equ $-buf
mes db 'Program installed$'
mes1 db 'Program already installed$'
mes2 db 'Program is DIE$'
init endp
text ends
end main
3.4. Рекомендации по улучшению
Главным недостатком этой программы является неудобное визуальное восприятие записей в файле. Т.е. мы видим не ASCII-код который образовался в результате нажатия клавиши, а так называемый скэн-код (номер клавиши) и состояние байта флагов клавиатуры, в котором он находился при этом нажатии. При необходимости можно написать процедуру в нашем обработчике либо в виде отдельной программы, которая анализировала бы байт флагов и в зависимости от этого подставляла ASCII-код соответствующий скэн-коду нажатой клавиши.
Вторым недостатком нашей программы является не всегда удобный механизм выгрузки программы из оперативной памяти. Можно предусмотреть выгрузку нашей программы специальной не стандартной комбинацией клавиш.
Третий существенный недостаток программы состоит в том, что наш обработчик не реагирует на сочетание клавиш Clrl+Alt+Del. Так как наш обработчик перехватывает прерывания от клавиатуры раньше чем системный обработчик “int09h”, то было бы целесообразно при этом сочетании сбрасывать содержимое буфера в файл, а затем передавать управление системному обработчику.
Можно предусмотреть запись в файл autoexec.bat либо config.sys строки с путём к нашему файлу, при запуске программы с параметром вводимым с командной строки.
Можно предусмотреть коррекцию размеров буфера, а также задавать имя рабочего файла с помощью всё тех же параметров вводимых с командной строки.
В зависимости от того в каких целях применяется данный обработчик, можно запретить нажатие какой либо клавиши, комбинации клавиш или последовательности.
Данная программа является шаблоном для резидентных обработчиков прерываний, в частности обработчиков прерываний от клавиатуры, и является огромным полем для творчества.
4. Список используемой литературы
П.И.Рудаков, К.Г.Финогенов «Программируем на языке ассемблера IBM PC», Обнинск 1997г.
Зубков С.В. «Assembler для DOS, Windows и UNIX», Москва 2000г.
Богумирский Б.С. «Руководство пользователя ПЭВМ», Санкт–Петербург 1994г.