КаталогИндекс раздела
НазадОглавлениеВперед


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. (При программировании на языке Ассемблера программист должен сам заботиться о сохранении и восстановлении регистров).
Поскольку для обычных (не interrupt) функций Си через стек передаются значения параметров, в функции обработки прерывания могут быть описаны параметры, как это показано в нашем примере, и программист может работать с регистрами как с параметрами, переданными его функции. Порядок сохранения регистров в стеке - всегда такой как показано в примере. Более того, в отличие от обычных функций Си параметры interrupt-функции являются также и выходными. Поскольку содержимое регистров перед возвратом восстанавливается из стека, любое изменение параметра будет произведено в стеке, и при возврате восстановится измененное содержимое регистра. Если же в программе обработки прерываний не требуется обработки содержимого регистров, она может быть описана как функция без параметров.
И еще одно требование к функции обработки прерывания. Если она обрабатывает аппаратное прерывание, то такую функцию необходимо заканчивать оператором сброса контроллера прерываний:

     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. После этого программа восстанавливает вектор и выводит на экран значение счетчика. Ее обработчик прерывания вызывает старый обработчик, а в дополнение к этому подсчитивает количество прерываний.
Номер прерывания задан в программе переменной intr_num, макрос VECT_ADDR определяет физический адрес вектора прерывания с заданным номером. Счетчик прерываний - переменная intr_count. Новый обработчик прерываний new_handler описан в программе как было рассказано выше. Интересно определение переменной old_ handler, служащей для размещения в ней старого вектора - она определена как указатель на функцию, имеющую тип void interrupt. Переменные segm и offs служат для сохранения адресных частей старого вектора.
Получение старого вектора состоит в чтении из памяти двух слов. По адресу вектора считывается смещение, а из следующих двух байт - сегмент. Запись нового вектора состоит в записи по тем же адресам двух слов: первое слово получается как смещение обработчика new_handler, а второе - как его сегмент. Обратите внимание на то, что операторам записи предшествует вызов функции Турбо-Си disable, а после записи вызывается функция enable. В программе на языке Ассемблера вместо этих вызовов должны стоять команды CLI и STI соответственно. На время записи в таблицу векторов прерывания должны быть запрещены - неизвестно что произойдет, если прерывание поступит в тот момент, когда мы уже изменили смещение, но еще не изменили сегмент. Аналогично происходит восстановление вектора - в таблицу записываются смещение и сегмент, считанные из нее в начале программы. Первый оператор обработчика new_handler - обращение по адресу, запомненному в old_ handler, второй - модификация счетчика.
(Обратите внимание на такую особенность этой программы. Если изготовить ее .EXE-файл и запустить на выполнение вне среды Турбо-Си, из командной строки DOS, то после ее завершения символы, соответствующие клавишам, нажатым при ее выполнении, появятся в командной строке. Это явится подтверждением того, что во время ее выполнения системный обработчик прерывания 9 функционировал нормально: он принял поступившие коды и занес их в буфер ввода, откуда они потом были извлечены командным процессором. При запуске из среды Турбо-Си такого эффекта мы не получим, так как Турбо-среда очищает буфер ввода при завершении программы.)

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.
Установка нового вектора производится при помощи функции 0x25. По спецификациям этой функции в регистр AL должен быть занесен номер прерывания, вектор которого мы устанавливаем, в регистре DS - сегментная часть вектора, а в регистр DX - смещение. Обратите внимание на то, что здесь при записи вектора мы не запрещаем прерывания - эти действия функция 0x25 выполняет сама. Восстановление вектора производится также при помощи функции 0x35.

В этой программе у нас нет необходимости вычислять адрес, по которому расположен вектор прерывания, поэтому макрос 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 - прерывание принтера.
Приведенный ниже программный пример иллюстрирует выборочное маскирование прерываний. Функция Турбо-Си clock возвращает количество прерываний таймера, поступивших с начала выполнения нашей программы. Первые 10 значений, выводимые на экран, будут последовательно возрастать, показывая нам, что прерывания от таймера поступают. Затем в порт 0x21 записывается код, содержащий единицу в младшем разряде. Следующие 10 значений возвращаемые функцией clock будут неизменны: прерывания от таймера не поступают. Наконец, мы восстанавливаем значение, ранее находившееся в порту 0x21, и видим, что прерывания от таймера поступают вновь.

/*=                  ПРИМЕР 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 - зарезервированы.


КаталогИндекс раздела
НазадОглавлениеВперед