Реализация обработчика прерывания клавиатуры в окружении DOS

Резидентный перехватчик аппаратного прерывания клавиатуры для ДОС.

Cover Image

Сегодня мы рассмотрим, как написать простейший перехватчик прерывания под ДОС. Зачем это надо в наше время? Ответ прост - для того, чтобы лучше разобраться как работает машина на низком уровне, общего развития и расширения кругозора, ну и некоторым для написания курсачей. Возможно этот материал будет полезен желающим попробовать написать свою ОС - почему бы и нет?

Интересно? Мы начинаем!

Термины и определения

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

Программное прерывание – прерывание, при котором обработчик вызывается не по внешнему событию, а напрямую из основной программы. Таким образом, можно реализовать набор часто используемых подпрограмм (так работает DOS (int 21h)).

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

Вектор прерывания – адрес ячейки памяти в которой находится адрес точки входа в обработчик прерывания (аналог указателей в Си).

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

Постановка задачи

С терминами разобрались – поехали разбираться с тем, что нам предстоит сделать. А будем делать мы резидентный перехватчик аппаратного прерывания клавиатуры для ДОС. Алгоритмы обработчика могут быть разными – это и будет «домашним заданием» и полем для экспериментов. На них останавливаться не будем – нам важны сами принципы написания такого драйвера. Об этом и продолжим.

Что нужно иметь ввиду: т.к. речь идет про ДОС, нужно понимать, что это однозадачная система (что нам на руку), т.е. никакой другой процесс (поток выполнения) не может поменять значения регистров, которые мы будем использовать в нашей программе, а это очень круто! Сразу естественный вопрос: если система однозадачная – каким образом обеспечить выполнение нашей программы в фоне (именно так «обязаны» работать драйверы)? Ответ прост – пишем резидент! Для начала определимся с планом действий, т.е. что нам нужно сделать – это определит структуру нашей программы.

Итак:

  1. Запомнить системный (досовский) вектор прерывания клавиатуры;
  2. Установить этот вектор на свой обработчик;
  3. Реализовать обработчик (сам алгоритм);
  4. Остаться работать в фоне и запускать обработчик по возникновению прерывания от клавиатуры.

Структура программы на ассемблере

Уточню сразу: все нижеизложенное будет касаться ассемблеров TASM и MASM (сам использую TASM). У других ассемблеров синтаксис отличается и надо читать документацию. Для начала рассмотрим шапку программы. Выглядит она следующим образом:

.model tiny ; модель памяти
.data ; Здесь описываем переменные, битовые поля и т.д. и т.п. 
.code ; Сегмент кода – здесь будет находиться код
.startup ; Точка входа в программу
end ; директива ассемблера, конец исходника с программой

Здесь нас интересует модель памяти. Дело в том, что ДОС поддерживает несколько моделей памяти: tiny, small, medium, large, huge и flat (винда). Модель памяти – способ организации сегментации памяти: сколько каких сегментов используется и какие устанавливаются между ними взаимоотношения. Нас интересует tiny – когда все сегменты (кода, данных, стека и т.п.) имеют один и тот же сгментный адрес. После ключевого слова .data описываем переменные – тут у нас будет адрес системного обработчика, текстовые сообщения и переменные, используемые в алгоритме обработчика. Переменные записываются следующим образом:

Msg db ‘А у нас сегодня кошка родила вчера котят’,10,13,’$’
MyFuckingSeg dw 0
ApocalypseCountdown dw 666

Что это такое? Для начала, разберемся с «типами данных». Тут использованы два типа: db – байт и dw – слово. Как несложно догадаться, в слове у нас 16 бит, а в байте 8. Первая запись, с текстовой строкой – массив байт. Т.е. по адресу, по которому он будет находиться, будет шутливое предложение про кошку, затем два байта – 10 и 13 (ASCII коды символов «перенос строки» и «возврат каретки») и знак доллара. Знак доллара нам нужен для функции 09h программного прерывания ДОС (int 21h) – это метка конца текстовой строки. Во второй строке резервируется слово в памяти для хранения сегмента (обычное число). Ну и третья строка – обычная переменная, с инициализированым значением – типа счетчик. С основами разобрались. Теперь рассмотрим функции программного прерывания ДОС (int 21h), которые нам пригодятся для реализации задуманного. Поехали (тут должна быть картинка с Гагариным).

INT 21h функция 09h

Вывод текстовой строки.

ВХОД: AH09h DX – смещение в сегменте данных (в нашем случае совпадает с сегментом кода) текстовой строки, завершающейся символом $. Пример:

.model tiny 
.code 
.startup 

mov ah, 09h 
mov dx, offset Msg 
int 21h 

mov ax, 4C00h ; выход из программы
int 21h 

Msg db ‘per aspera ad astra’,10,13,’$’ 

еnd 

Все, теперь открываем пиво (чай, молоко, кумыс – по вкусу) – мы написали свой первый «хеллоуворлд». Откладываем кумыс в сторону, идем дальше.

INT 21h функция 02h

Функция выводит один символ на экран. Можно использовать для вывода служебных кодов ASCII.

ВХОД: AH02h DL – выводимый символ

INT 21h функция 4Сh

Тут мы просто выходим в ДОС и сообщаем ДОСу код завершения (применяется в BAT-файлах). Аналогично с return в Си.

ВХОД: AH4Ch AL – код завершения программы

INT 21h функция 35h

После вызова этой функции в регистрах ES и BX окажутся, соответственно, сегмент и смещение обработчика прерывания. Это нам пригодится для корректной передачи управления в ДОС после выполнения нашего алгоритма.

ВХОД: AH35h AL – номер прерывания ВЫХОД: ES – сегмент обработчика прерывания BX – смещение обработчика прерывания

INT 21h функция 25h

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

ВХОД: AH25h AL – номер прерывания DS – сегмент обработчика прерывания DX – смещение обработчика прерывания

INT 27h

Завершиться, но остаться резидентным

ВХОД: DX – адрес первого байта за резидентным участком программы ВНИМАНИЕ, АХТУНГ, АЛЯРМ! Не 21, а 27! Да-да, именно 27. Эта функция завершает программу, оставляя резидентную часть (обработчик прерывания) в памяти. Т.е. она, по сути, информирует ДОС, что эта область памяти занята и писать туда ничего нельзя, дабы не затереть обработчик.

Теперь перейдем к коду

Напишем скелет нашего резидента.

1.      .model tiny
2.      .code
3.      .startup
4.     
5.      jmp Init
6.     
7.      Msg db 'Resident module installed!',10,13,'$'
8.      Old_09h dw 0,0
9.     
10.     New09h:
11.     mov ah, 02h
12.     mov dl, 'A'
13.     int 21h
14.     jmp dword ptr cs:Old_09h
15.     
16.     Init:
17.     mov ah, 09h
18.     mov dx, offset Msg
19.     int 21h
20.     mov ax, 3509h
21.     int 21h
22.     mov Old_09h, bx
23.     mov Old_09h+2, es
24.     mov ax, 2509h
25.     lea dx, New09h
26.     int 21h
27.     lea dx, Init
28.     int 27h
29.     
30.     end

С первыми тремя строками ясно – это шапка программы.

В пятой строке управление передается на код с меткой Init. Важно отметить, что в коде обработчик прерывания в коде располагается раньше модуля инициализации резидента – это обусловлено работой программного прерывания int 27h, в противном случае ДОС затрет обработчик, что приведёт к отстрелам ног пулеметными очередями.

С 10-й по 14-ю строку – собственно сам обработчик. Мы просто выводим на экран букву А (см. выше) и перепрыгиваем на системный обработчик прерывания 09h, чтоб не нарушать работу ДОСа и других программ, вызываемых из него.

Самое интересное тут происходит в строках 17-28, это инициализация резидента. Сначала мы выводим приветственное сообщение (17-19), затем сохраняем значение вектора системного прерывания 09h (20-23), устанавливаем свой обработчик (24-26) и выходим, оставляя резидентную часть программы в памяти (27-28).

Кульбит с адресом системного обработчика прерывания 09h (22-23) обусловлен устройством памяти у Intel. Дело в том, что Intel использует организацию памяти «младшим-вперед» - это значит, что физически первым передается младший байт (little-endian). Т.к. мы передаем указатель в виде СЕГМЕНТ:СМЕЩЕНИЕ, то нам надо это учитывать и передавать адрес в том виде, как это принято.

Подробнее c порядком байтов можно познакомиться тут - Википедия, да-да, именно там

Ну вот и все – мы написали наш первый резидент под ДОС. Можно вновь доставать кумыс и насладиться им в полной мере – мы это заслужили.

Реализацию алгоритма отдаю на откуп читателю, намекнув лишь, что при возникновении прерывания скан-код нажатой клавиши будет находится в порту по адресу 60h (читать – in ax, 60h).

Так же, при нажатии и отпускании клавиши возвращается 2 сканкода, т.е. прерывание будет вызвано дважды. Наш резидент, описанный в примере выведет две буквы А вместо одной.

Ну вот и все, как говорится, Enjoy!

Ссылки на софт

Полезно будет почитать

  • В.П. Коцубинский, В.В. Одиноков – "Программирование на Ассемблере"
  • Калашников О.А. – "Ассемблер? Это просто! Учимся программировать"
  • Нортон П. –"Язык ассемблера для IBM PC"
  • Абель П. – "Ассемблер и программирование для IBM PC"

За сим откланяюсь.

Приветы слать сюда: l0calhost [мухтар] mail.ru, 2:5020/570.77@fidonet