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


15. Резидентные программы

15.1. Проблемы разработки резидентных программ

Резидентными называются программы, постоянно находящиеся в оперативной памяти ЭВМ. В специальной литературе за ними закрепилось также название TSR-программы - по наименованию функции DOS "Завершить и оставить резидентной" (Terminate and Stay Resident). Находясь постоянно в оперативной памяти, TSR-программа, однако, большую часть времени является неактивной, компьютер выполняет другие - системные и пользовательские - программы. Активизация TSR-программы происходит по прерыванию (аппаратному или программному), и после выполнения TSR-программой требуемых от нее действий управление вновь возвращается к прерванной (фоновой) программе, а резидентная становится неактивной до следующего ее вызова. Очень часто активизация TSR-программы иницируется нажатием закрепленной за ней ("горячей") клавиши или комбинации клавиш, примером таких программ могут быть программы копирования экрана или резидентные словари и справочники. Из всего класса TSR-программ иногда выделяют подкласс, называемый резидентными обработчиками прерываний (ISR - Interrupt Service Resident) - программы, применяющиеся обычно для обслуживания внешних устройств и активизирующиеся по программному прерыванию подобно программам BIOS (например, драйвер мыши MOUSE. COM). ISR-программы, как правило, проще программ "горячей клавиши", так как моменты обращения к ним более предсказуемы.

Наш подход к рассмотрению TSR-программ будет несколько отличаться от принятого в большинстве справочных и учебных пособий. Поскольку TSR-программа постоянно находится в памяти, она должна занимать в ней как можно меньше места, поэтому средством ее реализации бывает, как правило, язык Ассемблера, и всегда значительное внимание уделяется вопросам минимизации объема. Нас же TSR-программы будут интересовать в несколько ином аспекте. Известно, что MS DOS - система однопрограммная. Но возможность функционирования в системе резидентных программ уже является шагом к организации работы DOS в многопрограммном режиме: в памяти одновременно сосуществуют фоновая программа и TSR-программа, которые активизируются поочередно. Нас будут интересовать в первую очередь проблемы корректного взаимодействия TSR-программы с фоновой программой и с DOS. Большинство TSR-программ, приводимых в литературе, "паразитируют" на фоновой программе, используя в той или иной степени ее ресурсы. В этом нет ничего предосудительного, особенно, если в каждом таком случае использование ресурса фоновой программы обосновано и гарантируется его сохранность. Мы же рассмотрим самый общий случай, когда фоновая и TSR-программа имеют раздельные ресурсы. При таком подходе проблемы построения TSR-программы можно разбить на следующие группы:

Решение всех этих проблем иллюстрируется программным примером 15.1.


/*==                  ПРИМЕР 15.1                       ==*/
/*============== Резидентная программа ===================*/
/* Модель памяти - small.
/* При компиляции установить: Options -> Compiler ->
   Code generation -> Test stack overflow -> Off !!! */
#include <dos.h>
#include <stdio.h>
#include <string.h>
#define byte unsigned char
#define word unsigned int
/* адрес в виде - сегмент:смещение */
typedef struct { word segm,offs; } addr;
/* Описания функций */
byte check_tsr(void);  /* проверка наличия TSR-программы в памяти */
void get_context(void); /* запоминание своего контекста */
void set_vectors(void);     /* установка своих векторов */
void restore_vectors(void);  /* восстановление векторов */
void far *get_vector(int n);          /* чтение вектора */
void set_vector(int n, void far *v); /* установка вект. */
void act_tsr(void);        /* активизация TSR-программы */
void self_kill(void);     /* самовыгрузка TSR-программы */
void tsr_exec(void);  /* прикладная часть TSR-программы */
/* описания новых обработчиков прерываний */
void interrupt new_8();
void interrupt new_9();
void interrupt new_13();
void interrupt new_28();
void interrupt new_2F();
void interrupt new_24();
/* номера обрабатываемых прерываний */
int int_nums[] = { 8, 9, 0x13, 0x28, 0x2f };
void interrupt (* new_v[])() = {
                 /* адреса новых обработчиков */
                 new_8, new_9, new_13, new_28, new_2F };
/* области сохранения старых векторов */
void interrupt (* old_v[5])();
addr a_2F;          /* адрес старого обработчика INT 2F */
/* Флаги */
byte far *dos_flag;  /* адрес флажка занятости DOS */
byte tsr_run = 0;              /* "TSR выполняется" (1) */
byte hot_key = 0;        /* "горячая клавиша нажата"(1) */
byte disk_op = 0;            /* "дисковая операция" (1) */
byte key_byte;     /* байт символа/состояния клавиатуры */
word cflag;                /* регистр флагов процессора */
/* набор команд перехода на обработчики прерываний */
struct far_jmp {
  byte jmp;                              /* код команды */
  void interrupt (* int_h)();                  /* адрес */
  } far *jmpar;
word jmp_segm;      /* сегм.адрес блока команд перехода */
/* переменные контекста */
byte old_ctrl_break; /* состояние обработки Ctrl-Break прерванной программы */
addr old_stack; /* сегмент и указатель стека прерванной программы */
addr tsr_stack;                /* то же - TSR-программы */
word old_pid;               /* PID прерванной программы */
word tsr_pid;                  /* то же - TSR-программы */
addr old_dta;        /* адрес DTA  прерванной программы */
addr tsr_dta;        /* то же - TSR-программы */
void interrupt (* old_24)(); /* адрес обработчика критической ошибки 
                        прерванной программы */
/* общие переменные */
word sizeprogram = 17000/16;  /* размер программы,
                             определенный опытным путем */
word hot_scan = 2;    /* scan-код горячей клавиши - "1" */
word hot_status =8;         /* маска спец.клавиши - Alt */
char tsr_mark[] = "TSR-программа 15_1 загружена"; /* опознавательная строка символов */
char far *mark;
byte com_func; /* номер коммуникационной функции прерывания 2F */
word far *ast;                         /* адрес в стеке */
word mcb_seg;                        /* адрес блока MCB */
byte screen[160];        /* для сохранения части экрана */
union REGS rr;
struct SREGS sr;
int i, n;
/*-------------------------------------------------------*/
/* ====            Пусковая программа                ====*/
main(int argn, char *argc[]) {
  /* проверка - не является ли программа уже резидентной */
  /* check_tsr возвращает 0, если уже резидентна */
  /* check_tsr присваивает значение переменной com_func */
  mark=(char far *)tsr_mark;
  if (!check_tsr()) {
    /*--- программа уже резидентна --*/
    /* с каким параметром вызвана программа ? */
    if (argn>1) {
      if (!strcmp(argc[1],"/Q")) {
        /* /Q - обращение к коммуникации для выгрузки */
        rr.h.ah=com_func; rr.h.al=0x30;
        /* коммуникационное прерывание */
        int86x(0x2f,&rr,&rr,&sr);
        printf("TSR-программа 15_1 выгружена\n");
        }
      else  /* неправильный вызов */
        printf("15_1 - инсталляция\n15_1 /Q - выгрузка\n");
      }
    else {  /* параметров нет - попытка повторной инсталляции */
      printf("!!! УЖЕ- %s -УЖЕ !!!\n",tsr_mark);
      printf("Функция прерывания 2Fh - %02Xh\n",com_func);
      }
    }
  else {
    /*--- программа не резидентна --*/
    get_context(); /* запоминание своего контекста */
    /* освобождение своего блока окружения */
    sr.es=peek(tsr_pid,0x2c);
    rr.h.ah=0x49; intdosx(&rr,&rr,&sr);
    set_vectors(); /* установка своих векторов */
    /* получение адреса флажка занятости DOS */
    rr.h.ah=0x34;
    intdosx(&rr,&rr,&sr);
    dos_flag=(byte far *)MK_FP(sr.es,rr.x.bx);
    /* завершение программы с оставлением ее резидентной */
    printf("%s\n",tsr_mark);
    rr.h.ah=0x31; rr.h.al=0;
    rr.x.dx=sizeprogram;
    intdos(&rr,&rr);
    }
}
/*---------------------------------------------*/
/*==== Обработчик прерывания от клавиатуры ====*/
void interrupt new_9() {
  /* чтение scan-кода и проверка его на совпадение с кодом
     горячей клавиши; проверка состояния переключателей */
  if (inportb(0x60)==hot_scan) {
    key_byte=peekb(0x40,0x17);
    if ((key_byte&hot_status) == hot_status) {
      /* посылка подтверждения в клавиатуру */
      key_byte=inportb(0x61);
      outportb(0x61,key_byte|0x80);
      outportb(0x61,key_byte);
      /* сброс контроллера прерываний */
      outportb(0x20,0x20);
      if (!tsr_run)  /* блок.реентерабельного вызова */
        hot_key=1;    /* установка флага горячей клавиши */
      }
    else (*old_v[1])();  /* системный обработчик */
    }
  else (*old_v[1])();  /* системный обработчик */
}
/*------------------------------------------*/
/*==== Обработчик прерывания от таймера ====*/
void interrupt new_8() {
  (*old_v[0])();  /* cистемный обработчик */
  if (hot_key && !(*dos_flag))
    /* если нажата горячая клавиша и не установлен флажок
       занятости DOS... */
    if (!disk_op) {
      /* ...и не выплоняется дисковая операция */
      hot_key=0;   /* сброс флага горячей клавиши */
      act_tsr();    /* активизация */
      }
}
/*--------------------------------------------------*/
/*==== Обработчик прерывания обращения к дискам ====*/
void interrupt new_13(bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl)
word bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl;  {
  disk_op++;      /* флаг дисковой операции */
  (*old_v[2])();  /* системный обработчик int 13 */
  /* возврат регистров, установленных сист.обработчиком */
  ax=_AX;  cx=_CX;  dx=_DX;
  /* обращение к int 2F, которая записывает регистр флагов 
        в static-переменную cflags */
  _AX=0x10;  new_2F();
  fl=cflag;
  --disk_op;  /* сброс флага дисковых операций */
}
/*-------------------------------------*/
/*==== Обработчик прерывания DOSOK ====*/
void interrupt new_28() {
  (*old_v[3])();  /* Системный обработчик */
  if (hot_key && *dos_flag) {
    /* если нажата горячая клавиша и установлен флажок занятости */
    hot_key=0;   /* сброс флага горячей клавиши */
    act_tsr();    /* активизация */
    }
}
/*-------------------------------------------------------*/
/*==== Обработчик прерывания 24 - критические ошибки ====*/
void interrupt new_24 (bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl)
word bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl;  {
  ax=0;
}
/*------------------------------------------------*/
/*====             Чтение вектора             ====*/
void far *get_vector(int in) {
  rr.h.ah=0x35; rr.h.al=in; intdosx(&rr,&rr,&sr);
  return(MK_FP(sr.es,rr.x.bx));
}
/*------------------------------------------------*/
/*====             Запись вектора             ====*/
void set_vector(int in, void far *v) {
  rr.h.ah=0x25; rr.h.al=in; sr.ds=FP_SEG(v);
  rr.x.dx=FP_OFF(v); intdosx(&rr,&rr,&sr);
  }
/*------------------------------------------------*/
/*====      Перехват векторов прерываний      ====*/
void set_vectors(void) {
  /* выделение памяти для команд перехода */
  rr.h.ah=0x48; rr.x.bx=2;
  intdos(&rr,&rr); jmp_segm=rr.x.ax;
  jmpar=(struct far_jmp far *)MK_FP(jmp_segm,0);
  for (i=0;i<5;i++) {
    /* получение старых векторов */
    old_v[i]=get_vector(int_nums[i]);
    /* запись кодов команд FAR JMP */
    jmpar[i].jmp=0xea;
    /* запись адресов наших обработчиков */
    jmpar[i].int_h=new_v[i];
    /* установка вектора на соответствующий jmp */
    set_vector(int_nums[i],(void far *)(jmpar+i));
    }
  /* адрес мультиплексного прерывания запоминается */
  a_2F.segm=FP_SEG(old_v[4]); a_2F.offs=FP_OFF(old_v[4]);
}
/*-----------------------------------------------*/
/*====  Восстановление векторов прерываний  ====*/
void restore_vectors(void) {
  for (i=n=0;i<5;i++) {
    /* если вектор наш - восстановить его */
    if ( get_vector(int_nums[i])==
           (void far *)(jmpar+i))
      set_vector(int_nums[i],old_v[i]);
    else {  /* если нет - запись адреса старого
      обработчика в команду перехода */
      jmpar[i].int_h=old_v[i];
      n++;
      }
    }
  /* если не все векторы восстановлены - блок команд
     перехода помечается принадлежащим DOS */
  if (n) poke(jmp_segm-1,1,8);
}
/*-------------------------------------------------------*/
/*====          Запоминание своего контекста         ====*/
void get_context(void) {
  /* сохранение своего сегмента стека */
  tsr_stack.segm=_SS; tsr_stack.offs=_SP-100;
  /* сохранение адреса своей DTA */
  rr.h.ah=0x2f;
  intdosx(&rr,&rr,&sr);
  tsr_dta.segm=sr.es; tsr_dta.offs=rr.x.bx;
  /* сохранение своего PID */
  rr.h.ah=0x62;
  intdos(&rr,&rr);
  tsr_pid=rr.x.bx;
}
/*-------------------------------------------------------*/
/*==== Переключение контекстов и активизация TSR-программы ====*/
void act_tsr(void) {
  tsr_run=1;  /* установка флага "TSR работает" */
  /*= изменение контекста при активизации TSR-программы =*/
  /* переключение на стек TSR-программы */
  disable();
  old_stack.offs=_SP;  old_stack.segm=_SS;
  _SP=tsr_stack.offs;  _SS=tsr_stack.segm;
  enable();
  /* подключение к вектору критических ситуаций */
  old_24=get_vector(0x24); set_vector(0x24,new_24);
  /* переключение статуса обработки Ctrl-Break */
  rr.h.ah=0x33; rr.h.al=0; intdos(&rr,&rr);
  old_ctrl_break=rr.h.dl;
  rr.h.ah=0x33; rr.h.al=1; rr.h.dl=0; intdos(&rr,&rr);
  /* переключение на DTA TSR-программы */
  rr.h.ah=0x2f; intdosx(&rr,&rr,&sr);
  old_dta.segm=sr.es; old_dta.offs=rr.x.bx;
  rr.h.ah=0x1e; sr.ds=tsr_dta.segm; rr.x.dx=tsr_dta.offs;
  intdosx(&rr,&rr,&sr);
  /* переключение на PID TSR-программы */
  rr.h.ah=0x62; intdos(&rr,&rr); old_pid=rr.x.bx;
  rr.h.ah=0x50; rr.x.bx=tsr_pid; intdos(&rr,&rr);
  /*= выполнение "прикладной части" =*/
  tsr_exec();
  /*= восстановление контекста прерванной программы =*/
  /* восстановление PID */
  rr.h.ah=0x50; rr.x.bx=old_pid; intdos(&rr,&rr);
  /* восстановление DTA */
  rr.h.ah=0x1e; sr.ds=old_dta.segm; rr.x.dx=old_dta.offs;
  intdosx(&rr,&rr,&sr);
  /* восстановление Ctrl-Break */
  rr.h.ah=0x33; rr.h.al=1; rr.h.dl=old_ctrl_break;
  intdos(&rr,&rr);
  /* восстановление обработчика критических ситуаций */
  set_vector(0x24,old_24);
  /* восстановление стека */
  disable();
  _SP=old_stack.offs;  _SS=old_stack.segm;
  enable();
  tsr_run=0;
}
/*------------------------------------------*/
/*==== "Прикладная" часть TSR-программы ====*/
void tsr_exec(void) {
  char *s; int i;
  /* сохранение экрана и вывод сообщения */
  for (i=0, s=tsr_mark; *s; ) {
    screen[i]=peekb(0xb800,i);
    pokeb(0xb800,i++,*s++);
    screen[i]=peekb(0xb800,i);
    pokeb(0xb800,i++,0x40);
    }
  screen[i]=0;
  /* ожидание нажатия клавиши */
  do {
    rr.h.ah=1; int86(0x16,&rr,&rr);
    } while (rr.x.flags&0x0040);
  rr.h.ah=0; int86(0x16,&rr,&rr);
  /* восстановление экрана */
  for(i=0;screen[i];i++) pokeb(0xb800,i,screen[i]);
  }
/*---------------------------------------------------*/
/*==== Обработчик прерывания 0x2F (коммуникации) ====*/
void interrupt new_2F(bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl)
 word bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,fl; {
  if ((ax>>8)==com_func) {  /* подфункции */
    switch (ax&0xff) {
      case 0: /* проверка возможности установки */
        ax|=0xff; /* запрет установки */
        es=FP_SEG(mark); bx=FP_OFF(mark);
        break;
      case 0x10: /* программный вызов TSR */
        act_tsr(); ax=0; break;
      case 0x20: /* получение регистра флагов */
        cflag=fl; break;
      case 0x30: /* выгрузка */
        self_kill(); ax=0; break;
      default: ax=0xffff;
      }
    }
  else {
    /* возврат из нашего обработчика в старый обработчик */
    for (ast=(word far *)&bp;
           ast<=(word far *)&flags;  ast++)
      *(ast-3)=*ast;
    cx=a_2F.offs; bx=a_2F.segm;
    _SP-=6;
    }
}
/*-------------------------------------------------------*/
/*==== Поиск свободных функций 2F
                            или функции, занятой 15_1 ===*/
byte check_tsr(void) {
 byte a, b;
 char far *s1;
 char *s2;
  com_func=0;
  for (a=0xff; a>=0xc0; a--) {
    rr.h.ah=a; rr.h.al=0;
    int86x(0x2f,&rr,&rr,&sr);
    b=rr.h.al;
    /* запоминание первого свободного номера функции */
    if (!(b+com_func)) com_func=a;
    if (b==0xff) /* функция занята */ {
      s1=(MK_FP(sr.es,rr.x.bx));
      for (s2=tsr_mark; *s2; s1++,s2++)
        if (*s1!=*s2) break;
      if (*s2) continue;
      /* занята нами */
      com_func=a;
      return (0);
      }
    }
  return (1);
  }
/*-----------------------------------------------*/
/*==== Операции по уничтожению TSR-программы ====*/
void self_kill(void)  {
  /* восстановление системных векторов прерываний */
  restore_vectors();
  /* определение начала цепочки MCB */
  rr.h.ah=0x52;  intdosx(&rr,&rr,&sr);
  mcb_seg=peek(sr.es,rr.x.bx-2);
  while (peekb(mcb_seg,0)==0x4d) {
    /* выборка из MCB признака последний/нет */
    if (peek(mcb_seg,1)==tsr_pid) {
    /* выборка из MCB PID программы-хозяина, если он
      совпадает с нашим, блок памяти освобождается
      rr.h.ah=0x49; sr.es=mcb_seg+1;
      intdosx(&rr,&rr,&sr);*/
      poke(mcb_seg,1,0);
      }
    /* переход к следующему MCB в цепочке */
    mcb_seg+=peek(mcb_seg,3)+1;
    }
}

15.2. Инициализация программы

TSR-программа запускается так же, как и любая другая программа. Но те действия, для которых она предназначена, программа должна выполнять не при запуске, а по прерыванию или по нажатию "горячей клавиши". Таким образом, действия, выполняемые при запуске программы должны только обеспечить условия для ее оставления резидентной и ее последующего функционирования в качестве резидентной. В нашем примере эти действия организуются функцией main, которая получает управление при запуске программы. Прежде всего программа должна проверить, не присутствует ли уже ее копия в памяти. Такую проверку выполняет функция check_tsr, которая будет нами подробно рассмотрена при обсуждении программных коммуникаций. В нашей программе предусмотрены два варианта запуска: запуск без параметров - для инициализации программы и запуск с параметром "/Q" - для выгрузки из памяти ранее загруженной копии программы. Функция main анализирует параметр и сопоставляет способ запуска с результатом проверки, произведенной функцией check_tsr. Если при запуске без параметров обнаруживается, что копия программы уже есть в памяти, то инициализация не производится. Если программа запущена с параметром, то для выгрузки TSR-программы происходит обращение к прерыванию 0x2F, которое также будет рассмотрено при обсуждении программных коммуникаций.
Если нет препятствий для инициализации программы, происходит обращение к функции get_context (рессматривается при обсуждении переключения контекстов), затем программа освобождает свой блок окружения, устанавливает свои векторы обрабатываемых прерываний (функция set_vectors - рассматривается при обсуждении обработки прерываний) и получает адрес флажка занятости DOS. Функция DOS 0x34 возвращает в регистрах ES:BX адрес переменной DOS, которая называется флажком занятости (ее использование рассматривается при обсуждении условий активизации).
Для завершения программы используется функция DOS 0x31 - "Завершить и оставить резидентной". Этой функции в регистре AL передается код завершения (как при завершении по функции 0x4C), а в DX - размер в параграфах той части программы, которая остается резидентной в памяти. При программировании на языке Ассемблера для программиста не представляет труда определение этого размера. При программировании на языке высокого уровня точно определить требуемый минимальный размер практически невозможно. Мы взяли размер, равный размеру EXE-файла, этот размер мог бы быть уменьшен, это уменьшение обя- зательно необходимо учитывать при формировании указателя стека (см.функцию get_context), окончательное значение размера подбирается методом проб и ошибок.
Другой способ оставить программу резидентной - прерывание 0x27, но функция DOS 0x31 предпочтительнее, она - более позднее средство, полностью перекрывающее возможности прерывания 0x27.

15.3. Условия активизации и обработка прерываний

Два аспекта, вынесенные в заголовок этого раздела, настолько связаны между собой, что не могут рассматриваться порознь.
Мы определили, что TSR-программа должна активизироваться по нажатию "горячей клавиши". Следовательно, первым прерыванием, которое должна обрабатывать программа является прерывание 9 - от клавиатуры. Обработчик прерывания 9 в нашей программе - функция new_9. По этому прерыванию программа должна читать скан-код нажатой клавиши и состояние клавиш-переключателей и сравнивать со своей "горячей комбинацией" (в нашей программе "горячей" является комбинация Alt+1, определяемая начальными значениями переменных hot_scan и hot_status). Если опознана "горячая комбинация" то код клавиши удаляется из буфера клавиатуры, в противном случае вызывается системный обработчик прерывания 9. Но может оказаться, что немедленную активизацию TSR-программы производить нельзя. Это обусловлено тем, что функции DOS нереентерабельны. Если мы прервали выполнение функции DOS и активизируем TSR-программу, которая, в свою очередь, будет обра-щаться к функциям DOS, то это может привести к катастрофическим последствиям. Поэтому лучше всего не активизировать TSR-программу сразу же после распознавания "горячей комбинации", а отложить активизацию до тех пор, пока не будут выполнены условия, делающие активизацию возможной, а в обработчике прерывания от клавиатуры только установить признак (hot_key) того, что "горячая клавиша" была нажата. Флаг tsr_run, также анализируемый в обработчике этого прерывания, блокирует реентерабельный вызов TSR-программы: его значение 1 сигнализирует о том, что TSR-программа уже активизирована.
Проверку этих условий удобно производить по таймеру. Следовательно, второе прерывание, которое должна обрабатывать наша программа - 8 (или 0x1C) - от таймера. Обработчик прерывания 8 в нашей программе - функция new_8. При каждом прерывании от таймера проверяется прежде всего флаг hot_key. Если он взведен, т.е. "горячая клавиша" была нажата, то проверяется флажок занятости DOS, который устанавливается DOS в 1 при выполнении любой функции DOS (адрес флажка занятости мы определили при инициализации программы). Еще одно условие инициализации - нулевое значение флага disk_op, о котором мы расскажем ниже. Если выполнены все условия инициализации, то из функции 8 вызывается функция act_tsr, а флаг "горячей клавиши" сбрасывается.
Но полностью отказываясь от активизации TSR-программы в моменты выполнения любых функций DOS, мы можем отложить эту активизацию на весьма неопределенный срок. Например, при отсутствии выполняемых программ командный процессор находится в состоянии ожидания ввода с клавиатуры, при этом выполняется функция DOS 0x0A, следовательно, флажок занятости взведен. Имеется, однако, способ избежать такой блокировки активизации. Основным фактором нереентерабельности функций DOS является то, что разные функции DOS используют для своей работы один и тот же стек, поэтому вложенный вызов функции DOS портит стек вызывающей функции. Оказывается однако, что в DOS имеется два стека - один используется функциями с номерами до 0x0C включительно, а второй - функциями с большими номерами. Если DOS выполняет функцию с номером не выше 0x0C, то TSR-программа может активизироваться, но при своем выполнении она не должна обращаться к функциям с меньшими номерами (что, впрочем, не составляет больших проблем). Индикатором такого состояния DOS является прерывание 0x28, выдаваемое DOS, когда DOS находится в состоянии ожидания ввода. Следовательно, еще одно обрабатываемое нами прерывание - 0x28 (обработчик - new_28). Обработчик этого прерывания вызывает функцию act_tsr при установленном флаге "горячей клавиши" даже при установленном флажке занятости DOS.
Еще одно прерывание, которое необходимо перехватывать нашей программе - 0x13 (обработчик new_13) - прерывание BIOS, обеспечивающее дисковые операции. Дисковые операции не следует прерывать хотя бы потому, что некоторые из них требуют временной синхронизации. Поэтому основная задача нашего обработчика new_13 - установить флаг disk_op, который блокирует активизацию TSR-программы на время выполнения дисковых операций. Для выполнения операции наш обработчик обращается к прежнему (системному) обработчику этого прерывания и обеспечивает возврат тех значений регистров AX, CX, DX и флагов, которые устанавливает системный обработчик (для формирования флагов приходится обращаться к функции new_2F).
И еще одно прерывание, которое обрабатывает наша программа - 0x2F, будет рассмотрено при обсуждении программных коммуникаций.
Эти пять векторов прерываний перехватываются TSR-программой при ее инициализации. При активизации TSR-программы она также должна перехватить вектор 0x24 - обработчика критических ошибок, в нашей программе он обеспечивает игнорирование ошибок во всех случаях.
Для получения и установки векторов прерываний служат у нас функции get_vector и set_vector, использующие функции DOS 0x35 и 0x25 соответственно. Представляет, однако, интерес организация передачи управления нашим обработчикам прерываний (функция set_vectors). Выделяется отдельный блок оперативной памяти размером 2 параграфа, в котором размещаются 5 команд "дальнего" перехода на наши функции обработки пяти перехватываемых при инициализации прерываний. В программе команда перехода описывается структурой struct far_jmp, в поле jmp которой заносится код команды JMP - 0xEA, а в поле int_h - адрес функции, на которую производится передача управления. В таблицу векторов записываются адреса сформированных команд перехода. Адреса прежних обработчиков прерываний запоминаются в массиве old_v. Для прерывания 0x2F сегментная часть и смещение адреса прежнего обработчика также запоминаются в дополнительной переменной a_2F. Почему же мы сразу не записали в таблицу векторов адреса наших обработчиков? Это сделано для предупреждения возможной аварии при выгрузке нашей программы (эта проблема была поставлена еще в главе 3). Наша программа включила свои обработчики прерываний в цепочки. Если наша программа будет удаляться из памяти и восстановит запомненные прежние векторы, то она этим исключит из цепочки обработчики тех программ, которые, возможно, подключились к цепочкам после нашей программы.
Восстановление векторов выполняется функцией restore_vectors. Эта функция читает из таблицы векторов вектора всех перехватываемых нашей программой прерываний и сравнивает их с адресами наших обработчиков. Если для какого-либо прерывания вектор содержит адрес нашего обработчика, то это значит, что наш обработчик является последним в цепочке обработки этого прерывания, в этом случае мы можем спокойно восстанавливать сохраненный при инициализации прежний вектор в таблице. Если же вектор содержит не наш адрес, это означает, что после нас к цепочке подключилась другая программа (возможно даже не одна), в этом случае мы не меняем таблицу векторов, но в команде перехода меняем адрес перехода на адрес прежнего обработчика. Если хоть один вектор прерывания не удалось восстановить в таблице векторов, то блок памяти, содержащий команды перехода, должен быть оставлен в памяти после удаления из нее нашей программы. Для этого в поле "владельца" 'этого блока, которое до сих пор содержало PID нашей программы, мы записываем код 8 - признак принадлежности этого блока DOS, что защитит его от удаления.

15.4. Переключение контекста

Под контекстом мы понимаем все системные переменные, указатели и т.п., значения которых определяют выполняемую (активную) в данный момент программу. Фактически контекст содержит описание ресурсов, выделенных данной программе, так как в его составе - стек программы, таблица файлов задачи, дисковая передаточная область.
При инициализации функция main обращается к функции get_context, которая запоминает переменные контекста.
Запоминается содержимое стековых регистров программы (содержимое регистра SP запоминается с уменьшением на 200, так как часть стека используется при инициализации TSR-программы; в случае, если размер оставляемой в памяти части TSR-программы уменьшается, следует перевычислить значение SP в соответствии с устанавливаемым размером). Если TSR-программа не будет при активизации переключаться на свой стек, она будет использовать стек фоновой программы; даже при аккуратной работе с этим стеком (TSR-программа не должна в нем ничего "забывать" или наоборот - выбирать лишнее) это может привести к аварии, если размер стека фоновой программы будет невелик.
Запоминается адрес дисковой передаточной области, назначенной программе. Это, как правило, не обязательно и необходимо только в том случае, если TSR-программа использует функции, работающие с DTA.
Запоминается адрес PSP программы. Не все TSR-программы это делают, но это совершенно необходимо, если TSR-программа использует функции файлового ввода-вывода метода дескрипторов DOS. Поскольку PSP содержит ссылку на таблицу файлов задания, переключение на свой PSP дает TSR-программе возможность работать с собственной таблицей файлов. Использование таблицы файлов фоновой программы в большинстве случаев рисковано, и оно совершенно невозможно в тех случаях, когда TSR -программе необходимо сохранять свои файлы открытыми между свими активизациями.
Когда выполнены условия активизации TSR-программы, происходит обращение к функции act_tsr. Эта функция устанавливает флаг активности tsr_run и обеспечивает переключение контекста с фоновой программы на TSR-программу. В операции по переключению контекста входят: сохранение значений регистров SS, SP, указывающих на стек фоновой программы, и установка собственных значений этих регистров, запомненных в get_context (на время изменения значений в стековых регистрах запрещаются прерывания); перехват вектора прерывания 0x24 - обработки критических ситуаций (прежний вектор сохраняется); получение и сохранение статуса обработки Ctrl+Break, установленного в фоновой программе, и отключение этой обработки (можно также перехватывать вектор прерывания 0x23, устанавливая собственный обработчик Ctrl+Break); сохранение адреса DTA фоновой программы и установка адреса своей DTA; запоминание PID текущей программы и установка собственного PID (с этого момента система "знает", что активной стала наша программа).
После переключения контекстов вызывается функция tsr_exec, выполняющая "прикладные" действия - то, для чего и разрабатывалась TSR-программа. После выполнения прикладной части происходит обратное переключение контекстов - восстановление запомненных адресов, переменных и т.д. фоновой программы, заканчивается функция act_tsr сбросом флага активности TSR-программы.
Прикладная часть нашей TSR-программы (функция tsr_exec) имеет только демонстрационное назначение: она выводит в левый верхний угол экрана сообщение о работе TSR-программы, которое снимается по нажатию любой клавиши. Однако, даже в такой простой функции следует обратить внимание на два важ- ных момента. Во-первых, переключение контекстов как бы продолжается в прикладной части. Мы имеем в виду сохранение образа той части экрана, которая будет перекрыта сообщением TSR-программы и восстановление этого образа перед возвратом из функции. В более общем случае следует, возможно, сохранять и восстанавливать и номер видеорежима, позицию курсора, номер текущей видеостраницы и т.д. Во-вторых, обратите внимание на то, что для вывода информации мы используем здесь прямую запись в видеопамять, а для ввода (ожидание нажатия клавиши) - прерывание BIOS. Это неслучайно: ведь по условиям применения прерывания 0x28 нам запрещено использование функций консольного ввода-вывода DOS.

15.5. Программные коммуникации

Под программными коммуникациями мы понимаем обращения к TSR-программе из других программ. Для чего может понадобиться такое обращение? Во-первых, если речь идет об ISR-программах, то их активизация может происходить только по программному обращению. Во-вторых, при инициализации TSR-программы необходимо проверять, нет ли уже копии этой программы в памяти. Следовательно, запускаемая программа должна обратиться к копии программы уже резидентной в памяти, а резидентная копия должна ответить на это обращение, подтвердив свое присутствие. Наконец, в-третьих, при запуске, например, нашей программы с параметром "/Q", она должна передать резидентной копии команду на самоуничтожение. Список соображений, по которым требуется обеспечение программных коммуникаций можно было бы продолжить.
Для программных коммуникаций можно использовать прерывания, закрепив для этих целей один из свободных векторов. В DOS 3.0 и далее для этих целей введено специальное прерывание 0x2F, именуемое мультиплексным. Каждый процесс (т.е. каждая TSR-программа) закрепляет за собой какую-либо функцию этого прерывания. Функции с номерами от 0 до 0xBF закреплены за DOS (известно, что функция 1 используется программой фоновой печати PRINT, 0xB7 - резидентной утилитой APPEND, 0x11 - внутренними вызовами DOS и т.д.), функции с номерами от 0xC0 до oxFF - для пользовательских программ. Поскольку одновременно несколько пользовательских резидентных программ могут находиться в памяти и использовать мультиплекское прерывание для своих коммуникаций, необходимо обеспечить разделение между ними функций, оставленных пользователям. По спецификациям прерывания 0x2F подфункция 0 (AL=0) любой функции должна обеспечивать индикацию занятости функции. При обращении к подфункции 0 любой функции прерывания 0x2F на выходе мы должны получать в регистре AL код 0 - если функция свободна и может быть захвачена пользовательской программой, или код 0xFF - если функция занята. Каждая программа, подключающаяся к прерыванию 0x2F должна обеспечить такое выполнение подфункции 0 для занимаемой ею функции, остальные подфункции можно назначать и использовать по своему выбору.
В нашем примере обработчиком прерывания 0x2F является функция new_2F. Номер функции прерывания 0x2F, которую занимает наша TSR-программа является переменной, значение которой устанавливается в процессе инициализации. Обработчик прерывания прежде всего анализирует содержимое регистра AH, сравнивая его с номером занятой нашей программой функции. Если они совпадают, то далее выбирается номер подфункции из регистра AL, и выполняются действия, определенные для данной подфункции. Для нашего обработчика определены 4 подфункции.

Если функция, с которой вызвано прерывание 0x2F, нами не опознана, необходимо обратиться к прежнему обработчику этого прерывания. Обратите внимание на то, как оформлено это обращение. Трудности, возникающие при выполнении такого обращения, характерны для перехвата всех программных прерываний. Заключаются они в том, что программные прерывания получают входные параметры в регистрах, в регистрах же возвращают и выходные параметры. Поэтому перед обращением к прежнему обработчику необходимо позаботиться о восстановлении всех регистров, а при возврате из нашего обработчика - обеспечить передачу содержимого регистров, сформированного прежним обработчиком. Применительно к прерыванию 0x2F трудности усугубляются тем, что выполняемым через него внутренним вызовам DOS (функция 0x11) параметры передаются не только через регистры, но и через стек, так что надо позаботиться о том, чтобы и содержимое стека при вызове старого обработчика было идентично тому, которое имелось при выдаче команды INT 2Fh. При программировании на языка Ассемблера это легко обеспечивается передачей управления на старый обработчик командой JMP, а не CALL. Здесь же нам приходится производить некоторые манипуляции со стеком, иллюстрируемые рис.15.1.


Рис.15.1 Манипуляции со стеком
при обращении к прежнему обработчику

При получении управления нашим обработчиком стек имеет вид, показанный на рис.15.1 а. Мы расширяем стек, смещая 12 последних записанных в нем слов (содержимое регистров перед INT 2Fh) на 3 слова влево (рис.15.1 б). Затем на места хранения регистров IP и CS мы записываем соответственно сегмент и смещение адреса прежнего обработчика (на рис.15.1 в - OF и SG, в программе - a_2F.offs и a_2F.segm). Указатель стека корректируется таким образом, чтобы он указывал на новую вершину стека (рис.15.1 г). Теперь при возврате из нашего обработчика из стека восстановятся все регистры от BP до AX, а в регистры CS:IP запишется адрес прежнего обработчика (SG: OFF), при этом в стеке останется адрес возврата в программу выдавшую прерывание 0x2F.
После того, как мы рассмотрели обработчик прерывания 0x2F, нам должно быть ясно, как происходит определение наличия резидентной копии в памяти. Функция check_tsr, выполняющая эту проверку, перебирает номера функций от 0xFF в сторону уменьшения до 0xC0, каждый раз обращаясь к прерыванию 0x2F для выполнения подфункции 0 текущей функции. Если прерывание возвращает в регистре AL 0, номер функции запоминается в глобальной переменной com_func для возможного занятия этой функции в дальнейшем. Если функция возвращает отличное от 0 значение, то область памяти, на которую указывает адрес в регистрах ES:BX, сравнивается с контрольной строкой "TSR-программа 15_1 загружена". Совпадение их является признаком того, что резидентная копия уже имеется в памяти. Для этого случая com_func принимает значение номера функции мультиплексного прерывания, занятого резидентной копией. При отсутствии резидентной копии номер первой свободной функции, запомненный в com_func, в дальнейшем используется для программных коммуникаций.

15.6. Завершение программы

Не все TSR-программы предусматривают свою выгрузку, то есть удаление из памяти, в этом случае удалить программу можно только перезагрузив систему. Но если такая возможность предусмотрена, то самоуничтожение TSR-программы сводится к восстановлению перехваченных векторов прерываний и освобождению занимаемой памяти. В нашем примере это обеспечивается функцией self_kill. Для восстановления векторов выполняется обращение к функции restore_vectors, уже нами рассмотренной, а освобождение памяти сводится к просмотру цепочки MCB. В тех блоках памяти, в которых идентификатор "владельца" совпадает с PID TSR-программы, этот идентификатор заменяется на нулевой, т.е. свободный. Заметим, что блок памяти, содержащий команды перехода на обработчики прерываний, может не быть освобожден, если в функции restore_vectors идентификатор его "владельца" был изменен.


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