Шаг 18.
Оптимизация с помощью ассемблера.
Использование прерываний. Написание процедур обработки прерываний

    На этом шаге мы рассмотрим написание процедур обработки прерываний.

    Вдобавок к вызовам функций DOS и BIOS вы можете писать свои собственные процедуры обработки прерываний. Для упрощения процесса в Borland C++ используется ключевое слово interrupt, преобразующее функцию типа void в обработчик прерывания. В программах на C и C++ можно написать функцию - обработчик прерывания следующим образом:

void interrupt AnyName(unsigned bp, unsigned di, unsigned si, unsigned ds,
 unsigned es, unsigned dx, unsigned cx, unsigned bx, unsigned ax, 
 unsigned ip, unsigned cs, unsigned flags)
{    // ... операторы C или C++  
}            

    Беззнаковые параметры обеспечивают доступ к регистровым переменным, сохраненным в стеке. В случае возникновения аппаратных прерываний эти значения останутся нетронутыми, поскольку они отражают состояние прерванной программы. В действительности для большей части обработчиков аппаратных прерываний, скорее всего, не придется объявлять ни одного параметра. Тем не менее, в программных прерываниях информация обычно передается в регистрах, и эту информацию можно получить с помощью вышеперечисленных параметров. Вы можете включить только те параметры, которые вам необходимы, но вы должны их включать именно в указанном порядке. Так, если вашей программе необходим регистр ax, в объявлении следует указать все параметры с bp по ax.

   Компилятор добавляет немного своего собственного кода к interrupt -функциям. В скомпилированном виде такие функции начинаются с преамбулы:

push ax                   ; Сохранить регистры 
push bx
push cx
push dx
push es
push ds
push si
push di
push bp
mov bp, DGROUP             ;Задать bp равным адресу сегмента данных
mov ds, bp                 ;Инициализировать ds сегментом данных программы
mov bp, sp                 ;Адресовать локальные данные через bp

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

    Операторы, выполняющиеся в функции, завершаются инструкциями, осуществляющими очистку перед возвратом из обработчика прерывания:

pop bp             ;Восстановить сохраненные регистры
pop di
pop si
pop es
pop dx
pop cx
pop bx
pop ax
iert                  ;Вернуться к выполнению прерванной программы

    После восстановления регистровых значений, сохраненных ранее в стеке, функция выполняет инструкцию iret, которая возвращает управление либо прерванной программе, либо, в случае программного прерывания, в место выполнения инструкции int. При этом также восстанавливаются регистры и флаги, запомненные в стеке во время возникновения сигнала или инструкции прерывания.

    В листинге 1 демонстрируется написание и установка функций - обработчиков прерываний, написанных с помощью встраиваемых инструкций asm. Вы должны скомпилировать и запустить эту программу из подсказки DOS. Программа устанавливает свой обработчик вектора прерывания PC 0x1c, который обычно не выполняет никаких действий, но тем не менее регулярно вызывается функцией BIOS, которую, в свою очередь, вызывает внутренний системный таймер. Вызываемый с частотой примерно 17.2 раза в секунду, вектор прерывания 0x1с играет роль удобного "крючка" с подвешенными действиями, которые необходимо выполнять регулярно, пока программа продолжается, даже не подозревая о прерываниях. В MULTIP прерывание запрограммировано отображать время в правом верхнем углу текстового дисплея, пока вы вводите текстовую строку. Нажмите <Enter> для завершения программы и удаления обработчика прерывания из памяти.


    Замечание. Если у вас монохромный адаптер дисплея, замените 0xb800 в строке на 0xb000.

    Листинг 1. Multip.cpp (Демонстрация процедуры обработки прерывания)

#include <stdio.h>
#include <dos.h>
#include <string.h>
#define DISPSEG 0xb800  //0xb000 для монохромного дисплея
// Глобальная переменная для первоначального вектора прерывания 0x1c
void interrupt (far *oldVector) (...);
// Прототипы функций
void InitInterrupt(void);
void DoWhateverYouWant(void);
void DeinitInterrupt(void);
void interrupt ShowTime(...);

main()
{
 setcbrk(0); // Предотвращение завершения программы с "Ctrl+C"
 InitInterrupt();
 DoWhateverYouWant();
 DeinitInterrupt();
 return 0;
}

//Перенаправляет вектор прерывания на нашу процедуру обработки
void InitInterrupt(void)
{ oldVector = getvect(0x1c); //Save current vector
  setvect(0x1c, ShowTime);   // Set vector to ShowTime()
}

//ShowTime() запускается вместе с примером функции
void DoWhateverYouWant(void)
{ int i, done = 0;
  char s[128];
 while (!done)
{  puts("\n\nEnter a strimg (Enter to quit):");
    gets(s);
    done = (strlen(s) == 0);
    if (!done)
     { for (i=0; i<40; i++)  puts(s); }
 }  
}

// Восстанавливает первоначальный вектор перед завершением программы
void DeinitInterrupt(void)
{ setvect(0x1c, oldVector);
}

//Обработчик  прерывания 0x1c. Отображает время на дисплее.
void interrupt ShowTime(...)
{ 
asm {
xor ax, ax                     // ax<- 0000
mov ds, ax                     // ds<- 0000
mov ax, [0x046d]               // Взять значение таймера / 256
mov bx, DISPSEG                // bx = адрес отображения
mov word ptr [0x009a], 0x0f07c // Отобразить '|'
mov bh, 0x70                   // Атрибут = обратный
push ax                        // Запомнить значение таймера
xchg ah, al                    //ah = значение таймера mod 256
aam                            //коррекция ASCII-умножения
or ax, 0x3030                  // Преобразовать в ASCII
mov bl, ah                     //Переслать цифру в bl
mov [0x0096], bx               //Отобразить первую цифру часов
mov bl , al                    //Переслать цифру в bl
mov [0x0098], bx               //Отобразить вторую цифру часов
pop ax                         // Восстановить значение таймера
mov cx, 0x0f06                 //Вычислить ax / 4.26
mul ch                         //ax <- ax*15
shr ax, cl                     //ax <- ax / 64
aam                            //коррекция ASCII-умножения
or ax, 0x3030                  // Преобразовать в ASCII
mov bl, ah                     // Переслать цифру в bl
mov [0x009C], bx               //Отобразить первую цифру минут
mov bl , al                    //Переслать цифру в bl
mov [0x009e], bx               //Отобразить вторую цифру минут
}
}
Текст программы можно посмотреть здесь

    Перед записью адреса пользовательской процедуры обработки прерывания в таблицу векторов прерывания PC следует всегда запоминать старый вектор для его восстановления в последствии. Для этого можно объявить переменную, подобную следующей:

void interrupt (far *oldVector) (...);

    В этой строке объявляется переменная oldVector в качестве дальнего указателя на interrupt-функцию, имеющую тип void и переменное число аргументов, которые представляются в C++ многоточием в скобках, в ANSI C - пустой парой скобок. Для сохранения вектора прерывания следует присвоить переменной oldVector значение, возвращаемое функцией getvect(). Затем вызовите функцию setvect() для перенаправления вектора прерывание на вашу процедуру. Еще один вызов функции setvect() восстанавливает старый вектор прерывания перед завершением программы.

    В функции void DoWhateverYouWant(void) запрашивается строка, после чего она отображается несколько раз в цикле for. Этот код нужен в программе для того, чтобы выполнились хоть какие-то действия, в то время как обработчик прерывания выполняется в фоновом режиме, - простая, но эффективная форма параллельного выполнения с помощью прерывания таймера. Запустите программу и подождите несколько минут перед вводом строки. Несмотря на то, что управление находится у функции DoWhateverYouWant(), программный обработчик прерывания автоматически обновляет отображаемое время.

    В функции - обработчике прерывания ShowTime() единственный встраиваемый оператор asm для расшифровки текущего времени, перевода двоично-десятичного ( BCD) результата в ASCII и записи цифр непосредственно в видеобуфер дисплея. Комментарии в листинге поясняют назначение каждой инструкции.

    Трудно представить себе функцию, более системно зависимую, чем ShowTime(), которая вряд ли будет работать корректно на всех компьютерах, кроме стопроцентно совместимых с IBM PC с обычным или монохромным графическим дисплеем. Эта функция определенно не станет работать с нестандартной аппаратной частью.

    Но такова природа ассемблера. Программы, написанные на ассемблере, тесно привязаны к своей машине. Однако, несмотря на это ограничение, если пользоваться ассемблером разумно, можно оптимизировать программы так, чтобы они работали настолько быстро, насколько это возможно, занимая при этом минимальное количество пространства.

    На следующем шаге мы рассмотрим ещё один пример.




Предыдущий шаг Содержание Следующий шаг