Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лаба 2 / КНИГА_АСМ.docx
Скачиваний:
2
Добавлен:
09.02.2024
Размер:
160.52 Кб
Скачать
    1. 3.4. Перехват прерываний и создание резидентных программ

Вначале вспомним, что такое прерывания и как устроена система прерываний нашего процессора.

Под прерыванием понимается некое событие, заставляющее процессор прервать выполнение текущей программы и перейти на подпрограмму обработки этого события. Прерываемая программа обычно называется фоновой программой, а подпрограмма обработки – обработчиком. После того, как обработчик заканчивает свою работу, управление возвращается фоновой программе, в ту точку (той команде фоновой программы), на которой она была прервана.

При любом прерывании выполняется следующая последовательность действий:

  • Процессор автоматически сохраняет в стеке адрес возврата, то есть адрес той команды фоновой программы, на которую мы должны будем вернуться из обработчика. Для этого процессор заталкивает в стек содержимое трех регистров: 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

; Если программа уже установлена в памяти, выдаем сообщение об

; этом и завершаем программу, ничего не оставляя в памяти.

Соседние файлы в папке Лаба 2