| Каталог | Индекс раздела |
| Назад | Оглавление | Вперед |
3. Прерывания
Мы выделяем рассмотрение этих средств языка Си в отдельный раздел, так как обсуждение этой темы выходит за рамки собственно языка Си и требует рассмотрения также и некоторых особенностей Ассемблерного программирования.
3.1. Генерация программных прерываний
Для этих целей в Турбо-Си имеется целый ряд функций.
Функция генерации программного прерывания:
int int86(int int_num, union REGS *inregs,
union REGS *outregs);
Функция выполняет прерывание с номером int_num, причем,
перед выдачей команды INT содержимое полей объединения inregs
копируется в регистры микропроцессора, а после возврата из
прерывания - содержимое регистров - в поля объединения
outregs.
Функция обращения к DOS:
int intdos(union REGS *inregs, union REGS *outregs);
Вызов этой функции эквивалентен вызову функции int86 со
значением параметра int_num = 0x21.
В приведенных выше функциях для передачи параметров используются только регистры общего назначения. Если для передачи параметров требуется использовать также и сегментные регистры, то можно воспользоваться функциями:
int int86x(int int_num, union REGS *inregs,
union REGS *outregs, struct SREGS *segregs);
int intdosx(union REGS *inregs, union REGS *outregs,
struct SREGS *segregs);
Здесь структура segregs, на которую указывает дополнительный
параметр, служит для задания и входных, и выходных значений
сегментных регистров.
Наконец, наиболее полный набор регистров передается прерыванию функцией: void intr(int int_num, struct REGPACK *regs); Структура regs также содержит и входные, и выходные значения. Но использование этой функции нами, к сожалению, ограничено ошибкой, имеющейся в системе программирования Турбо-Си 2.0. Ошибка заключается в том, что значение регистра BP, передаваемое в составе структуры regs, функция не отрабатывает. Поэтому в наших примерах мы в случаях, когда необходимо передать регистр BP, пользуемся функцией geninterrupt:
void geninterrupt(int int_num);
geninterrupt представляет собой функцию, основное содержание
которой составляет единственная команда Ассемблера INT int_num.
Значения регистров можно передать прерыванию через
псевдорегистры - _AX, _BX и т.д., из них же получить и результаты.
Но применение этой функции требует большой
осторожности: во-первых, при формировании входных регистров имеется
риск в процессе формирования второго и последующих
испортить содержимое регистров, сформированных ранее,
во-вторых, при выполнении прерывания может быть изменено содержимое
регистров - в первую очередь DS и ES, а также и BP,
DI, SI (поэтому мы всегда, применяя geninterrupt, сохраняем
их содержимое в статической памяти).
Еще несколько подобных функций мы опускаем, так как они не используются в программах нашего пособия.
3.2. Программы обработки прерываний
На языке Си мы можем писать и собственные программы обработки прерываний. Такая программа является функцией в программе пользователя и должна иметь описание типа:
void interrupt int_handler (int bp,int di,int si, int ds,
int es,int dx,int cx,int bx,int ax,
int ip,int cs,int flags);
Рассмотрим элементы описания подробнее. Имя функции (у
нас int_handler) может быть произвольным. Тип функции void
очевиден - программа обработки прерывания не может
возвращать значение. Описатель interrupt заставляет Си транслятор
генерировать некоторые дополнительные коды для этой функции.
Как известно, по команде INT в стек заносится содержимое регистра флагов и регистров CS:IP. Дополнительные коды, гене-
рируемые для interrupt-функции, обеспечивают сохранение в
стеке остальных регистров. При возврате управления из
interrupt-функции содержимое регистров восстанавливается, и
возврат выполняется командой IRET. (При программировании на
языке Ассемблера программист должен сам заботиться о
сохранении и восстановлении регистров).
outportb(0x20,0x20);.
3.3. Перехват прерываний
Пусть функция обработки прерывания у нас уже разработана,
теперь следует обеспечить ее вызов. Вспомним, что механизм
прерываний ПЭВМ работает следующим образом. При
поступлении прерывания с номером NN в стеке запоминаются регистр
флагов и регистры CS:IP, а из оперативной памяти по адресу
NN*4 выбирается четырехбайтный адрес, по которому передается
управление. Этот адрес называется вектором прерывания, а
первые 1024 байт оперативной памяти - таблицей векторов прерываний.
Вектор хранится в памяти в следующем порядке: младший
байт смещения - старший байт смещения - младший байт
сегмента - старший байт сегмента. Таким образом, если мы хотим,
чтобы по прерыванию NN получала управление наша
interrupt-функция, мы должны записать ее адрес на место вектора NN.
Технику этой записи мы рассмотрим здесь же, но чуть
ниже, а прежде обсудим некоторые проблемы, которые могут при
этом возникнуть.
До нашего вмешательства по адресу вектора NN находился
адрес системной (например, из состава BIOS) программы
обработки прерывания NN. После того как мы запишем на его место
адрес своей программы обработки прерывания, по прерыванию NN
управление будет передаваться нашей interrupt-функции (отсюда
и выражение - "перехват прерывания"). Но, возможно, те
действия, которые выполнял системный обработчик прерывания
NN были не лишними, а может быть, и жизненно необходимыми
для функционирования системы. Чтобы не дублировать эти действия
в своем обработчике прерывания (тем более, что мы не
всегда можем иметь о них исчерпывающую информацию),
необходимо прежде, чем записывать свой адрес на место вектора,
сохранить где-то тот адрес, который там был записан (адрес
системного обработчика). Первым (после сохранения регистров)
действием нашего обработчика должна быть передача управления
по этому адресу, то есть вызов системного обработчика прерывания.
Такой подход в некоторых источниках называется "дополнением
прерывания". Кстати, в этом случае мы можем не
сбрасывать контроллер прерываний, так как эта операция
выполняется системным обработчиком. Когда программа,
включающая в себя пользовательскую обработку прерывания,
заканчивается, она должна восстановить значение перехваченного
вектора, то есть, системную обработку прерывания.
При программировании на языке Ассемблера возникает трудность, связанная с вызовом системного обработчика. Возврат из системного обработчика производится командой IRET, следовательно, вызывать его просто командой CALL нельзя (для IRET в стеке должно быть три слова, а CALL записывает в стек только два). В некоторых источниках почему-то рекомендуется сохранять старый вектор также в таблице векторов на каком-либо свободном ее месте и вызывать старый обработчик командой INT. Проще, однако, перед выполнением команды CALL занести в стек содержимое регистра флагов (PUSHF), что и обеспечивает Турбо-Си для вызовов функций, имеющих описатель interrupt.
Рассмотренный подход дополнения прерываний можно легко распространить на случай, когда несколько одновременно находящихся в ОЗУ программ используют одно и то же прерывание. Этот случай иллюстрируется рис.3.1.
![]() Рис.3.1. Совместная обработка прерывания несколькими программами |
Пусть в исходном состоянии по адресу вектора NN был записан адрес системного обработчика sys_IH. Затем в память загружается пользовательская программа, которая перехватывает этот вектор и записывает на его место адрес собственного обработчика us1_IH. Вторая загружаемая в память программа перехватит прерывание у первой и запишет на место вектора адрес своего модуля us2_IH, и так далее. Если все пользовательские обработчики соблюдают правила дополнения, то при поступлении прерывания NN управление получит модуль us3_IH, который первым делом вызовет модуль us2_IH, тот вызовет us1_IH, а тот - sys_IH. Системный обработчик, выполнив свои функции, вернет управление в us1_IH, этот модуль отработав вернет управление в us2_IH, и так далее. Таким образом, возможность выполнить какие-то свои действия по прерыванию NN будет предоставлена всем программам. Здесь, однако, возникает трудноразрешимая проблема в связи с восстановлением векторов. Действительно, если в нашем примере первой завершится программа, чей модуль обработки прерывания us2_IH, то она восстановит тот вектор, который она перехватила, то есть us1_IH, исключив таким образом из цепочки us3_IH. Этот аспект более подробно рассматривается в главе, посвященной резидентным программам.
Теперь вернемся к вопросу о том, как подменить вектор прерывания. Рассмотрим здесь две техники разных уровней.
1). Поскольку адрес вектора прерывания нам известен, известны также споособы прямого чтения/записи памяти, можно воспользоваться ими для чтения и подмены вектора. Приведенная ниже программа иллюстрирует эту возможность.
/*= ПРИМЕР 3.1 =*/
/*=============== Перехват прерывания ===================*/
#include <dos.h>
#define VECT_ADDR(x) x*4 /* Вычисление адреса вектора */
int intr_num = 9; /* Номер прерывания */
int intr_count = 0; /* Счетчик прерываний */
void interrupt new_handler(); /* Описание нового обработчика
прерывания */
void interrupt (* old_handler)(); /* Переменная для
сохранения старого вектора */
unsigned int segm, offs; /* Сегмент и смещение из
старого вектора */
main() {
/* Получение старого вектора */
offs=peek(0,VECT_ADDR(intr_num));
segm=peek(0,VECT_ADDR(intr_num)+2);
old_handler=MK_FP(segm,offs);
/* Запись нового вектора */
disable();
poke(0,VECT_ADDR(intr_num),FP_OFF(new_handler));
poke(0,VECT_ADDR(intr_num)+2,FP_SEG(new_handler));
enable();
/* Ожидание 10-кратного срабатывания */
while (intr_count<10);
/* Восстановление старого вектора */
disable();
poke(0,VECT_ADDR(intr_num),offs);
poke(0,VECT_ADDR(intr_num)+2,segm);
enable();
/* Печать содержимого счетчика */
printf("intr_count=%d\n",intr_count);
}
/* Новый обработчик прерываний */
void interrupt new_handler() {
/* Вызов старого обработчика */
(*old_handler)();
/* Подсчет прерываний */
intr_count++;
}
Прежде всего - что делает эта программа. Она перехватывает
прерывание 9 (аппаратное прерывание, поступающее при
нажатии и при отпускании любой клавиши клавиатуры), а затем
ожидает, пока счетчик прерываний на достигнет числа 10. После
этого программа восстанавливает вектор и выводит на экран
значение счетчика. Ее обработчик прерывания вызывает старый
обработчик, а в дополнение к этому подсчитивает количество
прерываний.2). Функции DOS 0x35 и 0x25 обеспечивают чтение и запись вектора соответственно. Та же программа, использующая эти функции приведена ниже.
/*= ПРИМЕР 3.2 =*/
/*=============== Перехват прерывания ===================*/
#include <dos.h>
int intr_num = 9; /* Номер прерывания */
int intr_count = 0; /* Счетчик прерываний */
void interrupt new_handler(); /* Описание нового обработчика
прерывания */
void interrupt (* old_handler)(); /* Переменная для
сохранения старого вектора */
union REGS rr; /* Регистры общего назначения */
struct SREGS sr; /* Сегментные регистры */
void *readvect(int in);
void writevect(int in, void *h);
main() {
/* Получение старого вектора */
old_handler=readvect(intr_num);
/* Запись нового вектора */
writevect(intr_num,new_handler);
/* Ожидание 10-кратного срабатывания */
while (intr_count<10);
/* Восстановление старого вектора */
writevect(intr_num,old_handler);
/* Печать содержимого счетчика */
printf("intr_count=%d\n",intr_count);
}
/*==== Новый обработчик прерываний ====*/
void interrupt new_handler() {
(*old_handler)();
intr_count++;
}
void *readvect(int in) {
/*==== Получение старого вектора ====*/
rr.h.ah=0x35; /* AH - номер функции */
rr.h.al=in; /* AL - номер прерывания */
intdosx(&rr,&rr,&sr); /* Вызов DOS */
/* ES - сегментная часть вектора, BX - смещение вектора */
return (MK_FP(sr.es,rr.x.bx));
}
/*==== Запись нового вектора ====*/
void writevect(int in, void *h) {
rr.h.ah=0x25; /* AH - номер функции */
rr.h.al=in; /* AL - номер прерывания */
sr.ds=FP_SEG(h); /* DS - сегментная часть вектора */
rr.x.dx=FP_OFF(h); /* DX - смещение вектора */
intdosx(&rr,&rr,&sr); /* Вызов DOS */
}
Введенные здесь дополнительные переменные rr и sr служат
для передачи параметров функциям DOS. Чтение старого вектора
производится при помощи функции 0x35 (ее номер перед обращением
к DOS заносится в регистр AH). По спецификациям функции
0x35 в регистр AL должен быть занесен номер прерывания,
вектор которого читается. Функция возвращает в регистре ES
сегментную часть вектора, а в регистре BX - смещение, эти
значения наша программа запоминает в переменных segm и offs.В этой программе у нас нет необходимости вычислять
адрес, по которому расположен вектор прерывания, поэтому
макрос VECT_ ADDR здесь отсутствует. В прикладных задачах
использованию функций DOS для чтения/установки векторов
следует отдавать предпочтение еще и потому, что в новых версиях
DOS адреса векторов, возможно, не будут так легко доступны
пользователю.
В дальнейшем мы неоднократно будем использовать функции
readvect и writevect, приведенные в примере 3.2.
3.4. Маскирование аппаратных прерываний
Выше мы использовали команду микропроцессора CLI
(функцию Турбо-Си disable) для запрета всех аппаратных прерываний.
Но аппаратные прерывания можно запрещать и выборочно.
Для этого можно воспользоваться портом 0x21, в который записывается
байт - маска аппаратных прерываний. Единица в разряде
этого байта соответствует запрету соответствующего
аппаратного прерывания, 0 - разрешению прерывания.
Соответствие разрядов прерываниям приводится ниже (здесь и
далее при поразрядном анализе байтов или слов номер 0 имеет
самый младший бит):
| 0 | - прерывание таймера; |
| 1 | - прерывание клавиатуры; |
| 2 | - каскадирование прерываний (только AT); |
| 3 | - прерывание асинхронного порта COM2; |
| 4 | - прерывание асинхронного порта COM3; |
| 5 | - прерывание контроллера жестких дисков (только XT); |
| 6 | - прерывание контроллера гибких дисков; |
| 7 | - прерывание принтера. |
/*= ПРИМЕР 3.3 =*/
/*======== Маскирование аппаратных прерываний ============*/
#include <dos.h>
#include <time.h>
main() {
unsigned char mask; /* Исходная маска прерываний */
int i;
/* Индикация исходного состояния */
for (i=0; i<10; i++) {
printf("%3d ",clock()); delay(100);
}
printf(" маска=%02x\n",(mask=inportb(0x21)));
/* Запрет прерываний от таймера */
outportb(0x21,mask|0x01);
/* Индикация нового состояния */
for (i=0;i<10;i++) {
printf("%3d ",clock()); delay(100);
}
printf(" маска=%02x\n",inportb(0x21));
/* Восстановление старой маски */
outportb(0x21,mask);
/* Индикация восстановленного состояния */
for (i=0;i<10;i++) {
printf("%3d ",clock()); delay(100);
}
printf(" маска=%02x\n",inportb(0x21));
}
Из текста программы видно, что порт 0x21 доступен и для
записи, и для чтения. Таким образом, для корректного запрета
выбранного прерывания необходимо прочитать текущее состояние
маски прерываний, и занести в него запрещающую единицу при
помощи операции "логическое ИЛИ".
Для AT разряд 2 используется для каскадирования второго контроллера прерываний. Доступ к маске второго контроллера - через порт 0xA1. Назначения разрядов второй маски следующие:
| 0 | - прерывание часов реального времени; |
| 1 | - прерывание контроллера EGA; |
| 5 | - прерывание математического сопроцессора; |
| 6 | - прерывание контроллера жестких дисков; |
| 2,3,4,7 | - зарезервированы. |
| Каталог | Индекс раздела |
| Назад | Оглавление | Вперед |