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


11. Управление программами

11.1. Программный сегмент и программный идентификатор

Для программы, вызванной на выполнение, DOS выделяет блок памяти, называемый программным сегментом. Программный сегмент всегда начинается на границе параграфа. В начале программного сегмента DOS строит PSP (Program Segment Prefix - Префикс Программного Сегмента), который занимает 256 байт. (Для построения PSP в DOS имеются функции 0x26, 0x55, но это чисто внутренние функции DOS, мы оставим их без внимания). Директива ORG 100h, которой часто начинаются Ассемблерные программы, как раз учитывает смещение начала программы относительно начала программного сегмента, равное длине PSP. При передаче программе управления сегментный адрес PSP находится в регистрах DS и ES.

Формат PSP описывается структурой struct PSP программного примера 11.1. Некоторые поля PSP "остались в наследство" от ранних версий DOS и даже от операционной системы CP/M и в современных версиях DOS не используются, хотя и исправно формируются операционной системой. Это прежде всего поле ret _op, используемое для возможного завершения программы по команде RET 0, а также поле old_call_dos, содержащее команду вызова диспетчера функций DOS. Обращение к этому полю в программе может использоваться вместо команды INT 21h, но в современных версиях DOS для этих целей лучше обращаться к полю new_call_dos. Поле end_of_mem содержит сегментный адрес конца доступной памяти в системе. В три поля: term_ptr, ctrlbrk_ptr, criterr_ptr DOS при загрузке программы копирует содержимое векторов прерываний: 0x22, 0x23, 0x24, представляющее собой адреса обработчиков: завершения программы, комбинации клавиш Ctrl+Break, критической ошибки - соответственно. Предполагается, что программа может свободно перенаправить эти векторы на собственные обработчики соответствующих ситуаций, но от забот по восстановлению векторов программа избавляется, так как при ее завершении DOS сама восстанавливает векторы из соответствующих полей PSP завершаемой программы. Для аналогичных целей предназначено и поле stack_ptr - в нем сохраняется (а при завершении - из него восстанавливается) адрес стека, использовавшегося до вызова программы. Поле, именуемое father_psp, содержит сегментный адрес PSP родителя - программы, запустившей данную программу, обычно родителем является COMMAND.COM.
При загрузке программы DOS, кроме программного сегмента, создает для нее еще и сегмент окружения (environment). Сегмент окружения содержит ASCIIZ-строки, задающие значения некоторых глобальных переменных, эти значения могут устанавливаться командой DOS SET, они доступны командным файлам и - через PSP - программам. Набор строк окружения заканчивается пустой ASCIIZ-строкой (нулем). В DOS 3.0 и выше за ним следует еще 2-байтное число строк вызова (обычно 1) и далее - строка (или строки) вызова программы. Обычно в первую (до строк вызова) часть порождаемой программы копируется содержимое окружения программы-родителя. Программа имеет доступ к своему сегменту окружения через поле env_seg PSP, содержащее сегментный адрес окружения.
Поле JFT (Job File Table - Таблица Файлов Задачи) представляет собой массив из 20 однобайтных элементов. При открытии программой файла DOS формирует для него блок-описатель в системной таблице файлов и помещает ссылку на него (его номер) в свободный элемент JFT. Дескриптор файла, возвращаемый программе DOS при открытии файла, является номером элемента в JFT. При запуске программы первые пять элементов создаваемой для нее JFT содержат ссылки на системные файлы, остальные свободны - содержат код 0xFF. В описаниях DOS часто можно встретить упоминание об ограничении на число одновременно открытых в программе файлов, при этом обычно называется число 20 (включая 5 системных файлов). Первым объяснением этого ограничения является размер JFT. Ограничение это, однако, не является непреодолимым. В самом PSP зарезервированы пути для его обхода: при обработке JFT DOS использует не прямое обращение к полю JFT PSP, а косвенное - через поле JFT_ptr, а в качастве ограничителя размера JFT - не константу 20, а значение поля JFT_size PSP. Таким образом, программа может в другом месте выделить память для размещения JFT большего размера и, изменив поля JFT_ptr, JFT_size в своем PSP, обеспечить работу с новой таблицей файлов. DOS, начиная с версии 3.30, поддерживает эти операции функцией 0x67, которая выделяет память для новой JFT (требуемый размер JFT задается в BX), копирует в выделенную область старую JFT и исправляет поля PSP. Следует иметь в виду, что даже если в CONFIG.SYS значение параметра FILES больше 20, JFT в PSP формируется все равно 20-байтной, и программа сама должна позаботиться о ее расширении. Действия по увеличению размера JFT являются необходимыми, но не всегда достаточными для снятия ограничения на число открытых файлов - возможно, для этого еще потребуется расширить системную таблицу файлов (см. главу 13). Непреодолимым ограничением представляется размер элемента JFT - 1 байт, который позволяет представить число от 0 до 254 (255 - признак свободного элемента). Трудно, впрочем, представить себе программу, в которой необходимо иметь одновременно открытыми 250 файлов.
Вернемся к описанию формата PSP. Еще 36 бит, следующих за полями, описанными struct PSP, отводятся для двух блоков FCB, которые DOS строит для файлов, имена которых, возможно, являются параметрами программы.
Остальные 128 байт PSP - неформатированная его часть. При вызове программы в ней, начиная со второго ее байта, располагается остаток командной строки - часть строки вызова после имени программы, то есть, параметры, переданные программе при вызове. Эта строка не закрывается кодом 0, поэтому первый байт неформатированной области содержит число символов в остатке командной строки. Неформатированная область PSP после загрузки программы назначается DOS в качестве исходной DTA загруженной программы.

Программа может получить доступ к своему PSP либо из начального содержимого регистров DS или ES, либо при помощи функций DOS 0x51 или 0x62 (обе функции возвращают в BX сегментный адрес PSP). По некоторым источникам функция 0x51 (и родственная ей 0x50) в версиях DOS до 3.0 работала не во всех ситуациях надежно, функция же 0x62 является документированной. Мы в наших примерах используем обе эти функции.

После всего, сказанного выше, программа примера 11.1 не нуждается в комментариях. Эта программа извлекает из своего PSP всю или почти всю полезную информацию. Читатель может усовершенствовать ее, добавив в struct PSP описание полей блоков FCB. Иллюстрация обращения к остатку командной строки есть в примере 11.5.


/*==                      ПРИМЕР 11.1                   ==*/
/*=============== Распечатка собственного PSP ============*/
#include <dos.h>
#define byte unsigned char
#define word unsigned int
struct psp {
  byte ret_op[2];       /* команда INT 20h */
  word end_of_mem;      /* вершина доступной памяти */
  byte reserved1;
  byte old_call_dos[5]; /* старый вызов DOS */
  void *term_ptr;       /* адрес завершения */
  void *ctrlbrk_ptr;    /* адрес обработчика Ctrl+Break */
  void *criterr_ptr;    /* адрес обработчика крит.ошибок */
  word father_psp;      /* PID родителя */
  byte JFT[20];         /* таблица файлов программы */
  word env_seg;         /* адрес окружения */
  void *stack_ptr;      /* адрес стека */
  word JFT_size;        /* размер таблицы файлов */
  byte *JFT_ptr;        /* адрес таблицы файлов */
  byte reserved2[24];
  byte new_call_dos[3]; /* новый вызов DOS */
  } *mypsp;
word mypid;             /* сегм.адрес PSP */
int dos, i, l;
char *s;
union REGS rr;
main() {
  clrscr();
  /* определение версии DOS */
  rr.h.ah=0x30; intdos(&rr,&rr); dos=rr.h.al;
  /* получение адреса своего PSP */
  rr.h.ah=0x62; intdos(&rr,&rr); mypid=rr.x.bx;
  /* распечатка PSP */
  printf("***** PID=%04X *****\n",mypid);
  mypsp=(struct psp *)MK_FP(mypid,0);
  printf ("Команды:  завершение - int 20h ---> %02X %02X\n",
     mypsp->ret_op[0],mypsp->ret_op[1]);
  printf ("          старый вызов DOS -------> ");
  for (i=0;i<5;printf("%02X ",mypsp->old_call_dos[i++]));
  printf ("\n          новый вызов DOS --------> ");
  for(i=0;i<3;printf("%02X ",mypsp->new_call_dos[i++]));
  printf
    ("\n\nАдреса: конец памяти -------------> %04X:0000\n",
    mypsp->end_of_mem);
  printf("        обработчик завершения ----> %Fp\n",
    mypsp->term_ptr);
  printf("        обработчик Ctrl+Break ----> %Fp\n",
    mypsp->ctrlbrk_ptr);
  printf("        обработчик критич.ошибки -> %Fp\n",
    mypsp->criterr_ptr);
  printf("        стек ---------------------> %Fp\n",
    mypsp->stack_ptr);
  printf("\nРодитель: ------------------------> %04X  ",
    mypsp->father_psp);
  pr_file_tab();   /* таблица файлов */
  printf("\nОкружение DOS --------------------> %04X\n",
    mypsp->env_seg);
  s=(char *)MK_FP(mypsp->env_seg,0);
  while(l=strlen(s)) { printf("      %s\n",s); s+=l+1; }
  if (dos>2) {
    /* для DOS 3.0 и дальше можно получить строку вызова */
    s++; l=*((int *)s);
    printf("Строки вызова ----------------------> %d\n",l);
    s+=2;
   for(i=0; i<l; i++) { printf("%s\n",s); s+=strlen(s)+1; }
  }
  getch();
  /* увеличение размера таблицы файлов */
  rr.h.ah=0x67;   /* функция 67 */
  rr.x.bx=30;     /* новый размер - 30 */
  intdos(&rr,&rr);
  if (rr.x.cflag) printf("Ошибка функции 67h\n");
  else pr_file_tab();
  getch();
}
/*==== распечатка таблицы файлов ====*/
pr_file_tab() {
  s=mypsp->JFT_ptr;
  printf
    ("\n\nТаблица файлов: -------------------> %Fp (%d) ",
    s,mypsp->JFT_size);
  if (s==(byte *)mypsp+0x18) printf(" -  в этом же PSP");
  printf("\n");
  for (i=0; ++i<=mypsp->JFT_size; printf("%d ",*(s++)));
  printf("\n");
}

Абревиатура PSP представляется весьма удачной, так как она созвучна со словом "паспорт". PSP - тот блок, из которого можно почерпнуть всю доступную информацию о загруженной в память программе, и во всех операциях DOS по управлению программами, данными и памятью программа представляется прежде всего адресом (сегментным) своего PSP. Этот адрес в системе часто именуется PID (Program Identifier - Программный Идентификатор). Где-то в DOS имеется ячейка памяти, в которой система хранит PID активной в текущий момент программы. Выше говорилось, что функции 0x51, 0x62 позволяют программе получить сегментный адрес своего PSP - это не совсем так. Обе эти функции возвращают PID активной программы. Поскольку MS DOS - система по своему назначению однопрограммная, понятия "свой" и "активный", как правило, идентичны, но для резидентных программ они могут и не совпадать (см. главу 14). Хранящийся в системе PID активной программы можно изменить при помощи функции DOS 0x50 (новый PID задатся в BX).

Программа примера 11.2 выполняет поиск адреса или адресов, по которым DOS хранит PID активной в текущий момент программы. Обращением к функции 0x52 программа получает сегментный адрес конца области памяти, занимаемой DOS (объяснение этой операции отложим до главы 13). Далее программа получает свой PID (функция 0x62) и последовательным перебором адресов от начала сегмента 0x60 и до конца памяти DOS ищет в памяти слово, совпадающее с полученным PID. Если такое слово отыскалось, то программа должна убедиться, что совпадение не случайно. Для этого она с помощью функции 0x50 устанавливает новый PID (на 1 больший прежнего), вновь считывает слово по тому же адресу и обязательно (!) восстанавливает прежний PID. Если при изменении при помощи функции 0x50 активного PID соответственно изменилось и содержимое памяти по найденному адресу, то найденный адрес запоминается в массивах mseg, moff, и поиск продолжается. Продолжение поиска обусловлено предположением о том, что DOS, возможно, хранит PID активной программы не в одном месте (по источникам, в версиях DOS 2.x таких мест было два), однако, хотя в нашей программе и зарезервированы 10-элементные массивы, мы в наших экспериментах в DOS 3.30 и 5.0 всегда получали единственный адрес.
При поиске мы изменяли PID при помощи функции DOS, а читали его прямым обращением к памяти. Контрольные операции - изменение PID прямой записью в память и чтение его функцией DOS всегда подтверждают правильность поиска.


/*==                     ПРИМЕР 11.2                    ==*/
/*============ Определение места PID в системе ===========*/
#include <dos.h>
#define word unsigned int
word mypid;       /* PID программы */
word dosend;      /* конец памяти DOS*/
word seg,off;     /* текущий адрес */
word vpid;        /* прочитанный PID */
word              /* массивы для запоминания адресов */
  mseg[10], moff[10];
int nmem;         /* число адресов */
int i;
union REGS rr;
struct SREGS sr;
main() {
  /* получение адреса конца памяти DOS */
  rr.h.ah=0x52; intdosx(&rr,&rr,&sr);
  dosend=peek(sr.es,rr.x.bx-2);
  /* Получение своего PSP (PID )*/
  rr.h.ah=0x62; intdos(&rr,&rr);  mypid=rr.x.bx;
  printf("PID программы  = %04X\n",mypid);
  /* Поиск PID в системе */
  nmem=0;
  /* поиск происходит начиная от сегмента 60 до конца
     памяти, занимаемой DOS */
  for(seg=0x60;seg<dosend;seg++)
    for(off=0;off<16;off++)
      if (peek(seg,off)==mypid) { /* PID найден */
        /* изменяется текущий PID */
        rr.h.ah=0x50; rr.x.bx=mypid+1; intdos(&rr,&rr);
        /* повторная выборка из памяти */
        vpid=peek(seg,off);
        /* PID восстанавливается */
        rr.h.ah=0x50; rr.x.bx=mypid; intdos(&rr,&rr);
        if (vpid==mypid+1) {
          /* адрес слова, хранящего PID, запоминается */
          mseg[nmem]=seg; moff[nmem++]=off;
          }
        }
  /* Контрольные операции */
  /* По найденным адресам записывается другой PID */
  for(i=0; i<nmem; i++) poke(mseg[i],moff[i],mypid+1);
  /* проверяется, изменился ли PID, хранящийся в системе */
  rr.h.ah=0x62; intdos(&rr,&rr); vpid=rr.x.bx;
  /* Восстанавливается содержимое */
  for(i=0; i<nmem; i++) poke(mseg[i],moff[i],mypid);
  /* Печатаемое значение должно совпадать с mypid+1 */
  printf("Измененный PID = %04X\n",vpid);
  /* Распечатка адресов хранения PID в DOS */
  for (i=0;i<nmem; i++)
    printf("Адрес хранения PID = %04X:%04X\n",
      mseg[i],moff[i]);
  printf("Нажмите любую клавишу...\n"); getch();
}

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

11.2. COM- и EXE-файлы

Известно, что программные файлы в MS DOS бывают двух видов - файлы с расширениями COM и EXE.

COM-файл - это обязательно программа размером не более 64 Кбайт. Все логические сегменты этой программы (код, данные, стек) помещаются в одном физическом сегменте памяти. COM-файл содержит двоичный код-образ программы, абсолютно идентичный тому образу, который программа будет иметь в оперативной памяти. При загрузке COM-программы система размещает ее сразу вслед за ее PSP (поэтому COM-программа должна начинаться директивой ORG 100h), во все сегментные регистры записывает адрес PSP, и содержимое этих регистров не меняется в ходе выполнения программы.

EXE-программа может иметь любой размер и состоять из нескольких сегментов. Содержимое сегментных регистров меняется в ходе выполнения - в них, как правило, заносятся адреса логических сегментов программы. Поскольку во время трансляции и компоновки не известно, какие абсолютные значения будут иметь эти адреса при размещении программы в памяти, в коде программы эти адреса представляются в виде смещений сегментов относительно начала программы с тем, чтобы при загрузке выполнить настройку обращений к началам сегментов на конкретные адреса памяти. Именно в связи с необходимостью настройки EXE-файл имеет более сложный формат. В начале файла расположен заголовок, который состоит из форматированной части и таблицы перемещений, далее следует сам образ программы. Форматированная часть заголовка EXE-файла описана структурой struct EXEH программы примера 11.3.
Первое слово заголовка содержит обязательно коды букв 'MZ' или 'ZM' - признак EXE-файла (при загрузке программы DOS отличает EXE- от COM-файла не по расширению, а именно по этому признаку). Поле HdrSize определяет размер заголовка (включая таблицу перемещений) в параграфах - смещение в EXE-файле начала программы. Поля PageCnt и LastPage - определяют размер файла программы. Поля ReloCnt и TabOff определяют количество элементов в таблице перемещений и смещение ее начала относительно начала файла. Поля заголовка MinMem и MaxMem формируются компоновщиками и содержат размер памяти, которую необходимо выделить программе дополнительно к образу, хранящемуся в файле. При загрузке программы система пытается выделить ей MaxMem памяти, если такого ресурса свободной памяти в системе нет - выделяется максимально возможный объем - но не меньше MinMem - иначе загрузка завершается аварийно. (Поскольку COM-файл не имеет заголовка, и система "не знает", сколько памяти ему потребуется, она выделяет такой программе всю свободную память). Поле ChkSum - контрольная сумма всех слов в EXE-файле. Поля ReloSS и ReloCS содержат сегментные смещения относительно начала программы сегментов стека и кода - для установки регистров SS, CS при запуске, поля ExeSP и ExeIP - значения, устанавливаемые при запуске в регистры SP, IP.

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


/*==                      ПРИМЕР 11.3                   ==*/
/*=============== Чтение заголовка EXE-файла =============*/
#include <dos.h>
#include <string.h>
#include <stdlib.h>
#define byte unsigned char
#define word unsigned int
/* Форматированная часть заголовка EXE-файла */
struct EXEH {
  byte ExeFlag[2]; /* 0x4D5a признак EXE-файла ('MZ') */
  word LastPag;    /* длина неполной последней страницы */
  word PageCnt;    /* длина образа в 512-байтовых стр. */
  word ReloCnt;    /* число элементов в табл.перемещения */
  word HdrSize;    /* длина заголовка в параграфах */
  word MinMem;     /* мин. памяти за концом прогр.*/
  word MaxMem;     /* макс.требуемой памяти */
  word ReloSS;     /* смещение сегмента стека */
  word ExeSP;      /* значение регистра SP при запуске */
  word ChkSum;     /* контрольная сумма */
  word ExeIP;      /* значение регистра IP при запуске */
  word ReloCS;     /* смещение кодового сегмента */
  word TabOff;     /* смещение 1-го эл-та перемещения */
  word Overlay;    /* номер оверлея */
  } exeh;
word psp;             /* сегм.адрес PSP */
char fname[80], *fn;  /* имя EXE-файла */
int exef;             /* дескриптор EXE-файла */
union REGS rr;
struct SREGS sr;
main() {
  clrscr();
  /* получение сегментного адреса PSP */
  rr.h.ah=0x51; intdos(&rr,&rr); psp=rr.x.bx;
  /* получение имени файла из окружения */
  for(fn=(char *)MK_FP(peek(psp,0x2C),0);*fn|*(fn+1);fn++);
  strcpy(fname,fn+4);
  if (strchr(fname,'.')==NULL) strcat(fname,".EXE");
  printf("Файл: %s\n",fname);
  /* открытие файла */
  rr.h.ah=0x3d; rr.h.al=0;
  sr.ds=FP_SEG(fname); rr.x.dx=FP_OFF(fname);
  intdosx(&rr,&rr,&sr);
  if (rr.x.cflag) {
    printf("Невозможно открыть файл\n"); exit(0);
    }
  else exef=rr.x.ax;
  /* чтение заголовка */
  rr.h.ah=0x3f; rr.x.bx=exef; rr.x.cx=sizeof(struct EXEH);
  sr.ds=FP_SEG(&exeh); rr.x.dx=FP_OFF(&exeh);
    intdosx(&rr,&rr,&sr);
  if (rr.x.cflag) { printf("Ошибка чтения\n"); exit(0); }
  printf("======= Заголовок файла %s =======\n",fname);
  printf("Подпись файла .EXE             = %02X%02X (%c%c)\n",
    exeh.ExeFlag[0],exeh.ExeFlag[1],
    exeh.ExeFlag[0],exeh.ExeFlag[1]);
  printf("Длина последней страницы       = %d\n",
    exeh.LastPag);
  printf("Длина модуля в страницах       = %d\n",
    exeh.PageCnt);
  printf("Число эл-тов в табл.перемещ.   = %d\n",
    exeh.ReloCnt);
  printf("Длина заголовка                = %d\n",
    exeh.HdrSize);
  printf("Минимум памяти                 = %04Xh\n",
    exeh.MinMem);
  printf("Максимум памяти                = %04Xh\n",
    exeh.MaxMem);
  printf("Смещение сегмента стека        = %04Xh\n",
    exeh.ReloSS);
  printf("Значение регистра SP           = %04Xh\n",
    exeh.ExeSP);
  printf("Контрольная сумма              = %d\n",
    exeh.ChkSum);
  printf("Значение регистра IP           = %04Xh\n",
    exeh.ExeIP);
  printf("Смещение кодового сегмента     = %04Xh\n",
    exeh.ReloCS);
  printf("Смещение 1-го элемента таблицы = %04Xh\n",
    exeh.TabOff);
  printf("Номер оверлея                  = %d\n",
    exeh.Overlay);
  getch();
  }

Таблица перемещений может располагаться сразу вслед за форматированной частью заголовка, тогда ее смещение - 0x1C, но конкретные компоновщики могут располагать перед таблицей еще некоторую собственную специфическую информацию. Таблица состоит из 4-байтных элементов, в каждом из которых записано расположение в коде программы слова, содержащего обращение к адресу логического сегмента и, следовательно, требующее настройки на абсолютный адрес (в примере 11.4 - struct ReloItem). При загрузке программы DOS определяет сегментный адрес ее начала (стартовый адрес) как PID + 0x10 и далее ко всем словам программного кода, которые адресуются таблицей перемещений, прибавляет этот адрес. Результаты этой работы DOS иллюстрируются примером 11.4. Эта программа, как и предыдущая, находит свой EXE-файл и считывает его заголовок. Зная адрес своего PSP, программа вычисляет стартовый сегментный адрес, затем находит в EXE-файле начало таблицы перемещений и поэлементно читает ее. Для каждого элемента таблицы перемещений программа находит слово, которое этот элемент адресует - в оперативной памяти и в EXE-файле - и выводит на экран оба варианта этого слова (для контроля выводятся также по 4 байта до и после этого слова). Во всех случаях разность между значениями этого слова в памяти и в файле будет равна стартовому сегментному адресу.


/*==                   ПРИМЕР 11.4                    ==*/
/*======== Обработка таблицы перемещений EXE-файла =====*/
#include <dos.h>
#include <string.h>
#include <stdlib.h>
#define byte unsigned char
#define word unsigned int
#define dword unsigned long
struct EXEH { /* заголовок EXE-файла */
  word ExeFlag, LastPag, PageCnt, ReloCnt, HdrSize, MinMem,
   MaxMem,ReloSS, ExeSP, ChkSum, ExeIP, ReloCS, TabOff,
   Overlay;  } exeh;
/* Элемент таблицы перемещений */
struct ReloItem {
  word offs;   /* смешение */
  word segm;   /* сегмент */
  } ri;
word psp,      /* сегм.адрес PSP */
     startseg, /* стартовый сегмент */
     reloseg,  /* сегмент модифицированного слова */
     d;        /* разность между памятью и файлом */
char fname[80],*fn; /* имя EXE-файла */
int exef;      /* дескриптор EXE-файла */
dword fileoff, /* адрес в файле начала модуля */
      itoff,   /* адрес в файле модиф.слова */
      seekoff; /* адрес в файле эл-та таблицы */
byte buff[10],         /* буфер в ОП */
     *s;
int i, ni;
union REGS rr;
struct SREGS sr;
main() {
  /* получение сегментного адреса PSP и открытие файла */
  rr.h.ah=0x51; intdos(&rr,&rr); psp=rr.x.bx;
  for(fn=(char *)MK_FP(peek(psp,0x2C),0);*fn|*(fn+1);fn++);
  strcpy(fname,fn+4);
  if (strchr(fname,'.')==NULL) strcat(fname,".EXE");
  rr.h.ah=0x3d; rr.h.al=0; sr.ds=FP_SEG(fname);
  rr.x.dx=FP_OFF(fname); intdosx(&rr,&rr,&sr);
  if (rr.x.cflag) {
    printf("Невозможно открыть файл %s\n",fname); exit(0);
    }
  else exef=rr.x.ax;
  /* чтение заголовка */
  dread(&exeh,sizeof(struct EXEH));
  startseg=psp+0x10;
  printf("\nСтартовый сегмент = %04Xh\n",startseg);
  printf("Число элементов   = %d\n\n",exeh.ReloCnt);
  fileoff=exeh.HdrSize*16;
  /* перебор таблицы перемещений */
  for (seekoff=exeh.TabOff,ni=0; ni<exeh.ReloCnt; ni++) {
    /* чтениe эл-та табл.перемещений */
    dseek(seekoff);
    dread(&ri,sizeof(struct ReloItem));
    printf("Элемент перемещения #%d : %04X:%04X\n",
      ni+1,ri.segm,ri.offs);
    /* выборка модифицированного адреса из программы в ОП */
    reloseg=startseg+ri.segm;
    s=(byte *)MK_FP(reloseg,ri.offs)-4;
    printf("      Память : %Fp -> ",s);
    d=*((word *)(s+4));
    prtmem(s);
    /* выборка немодифицированного адреса из EXE-файла */
    itoff=fileoff+ri.segm*16+ri.offs-4;
    dseek(itoff);__ _.dread(buff,10);
    printf("      Файл   : %9ld -> ",itoff);
    d-=*((word *)(buff+4));
    prtmem(buff);
    printf("                           Разность = %04X\n",d);
    seekoff+=sizeof(struct ReloItem);
    if (getch()==27) exit(0);
    }
  }
/*==== Чтение из файла ====*/
dread(void *addr,int len) {
  rr.h.ah=0x3f; rr.x.bx=exef; rr.x.cx=len;
  sr.ds=FP_SEG(addr); rr.x.dx=FP_OFF(addr);
  intdosx(&rr,&rr,&sr);
  if (rr.x.cflag) { printf("Ошибка чтения %s\n"); exit(0); }
}
/*==== Позиционирование в файле ====*/
dseek(dword off) {
  rr.h.ah=0x42; rr.x.bx=exef; rr.h.al=0; rr.x.cx=off>>16;
  rr.x.dx=off&0xffff; intdos(&rr,&rr);
  if (rr.x.cflag) {  printf("Ошибка позиционирования\n");
     exit(0); }
}
/*==== Дамп участка памяти ====*/
prtmem(byte *a) {
 int i;
  for (i=0; i<10; i++) {
    if ((i==4)||(i==6)) printf("  "); printf("%02X",*(a++));
    }
  printf("\n");
 }

Заголовок EXE-файла нужен только при загрузке программы в оперативную память - он не сохраняется после загрузки. Приведем итоговую схему загрузки EXE-программ:

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

"Старые" способы возврата из программы: функция DOS 0 и прерывание 0x20. Оба этих способа требуют, чтобы в регистре CS был сегментный адрес PSP. При этом восстанавливаются векторы прерываний 0x22, 0x23, 0x24, но не выполняется автоматическое закрытие открытых программой файлов. Завершение программы командой RET сводится к выполнению команды INT 20h, так как DOS при вызове программы записывает в стек нулевое слово, по RET это слово выбирается в IP, и управление передается по адресу PSP:0000.

В современных версиях DOS следует использовать для возврата функцию 0x4C. Эта функция возвращает управление родительской программе с закрытием всех файлов, восстановлением векторов и возможностью установить код завершения (задается в регистре AL). После окончания программы код ее завершения может быть проанализирован в командном файле (BAT-файле) по значению ERRORLEVEL или может быть получен программой, запустившей завершившуюся по функции 0x4D (возвращает код завершения в AL, в AH - способ завершения: 0 - нормальное, 1 - по Ctrl+Break, 2 - по критической ошибке, 3 - программа завершилась, но осталась резидентной).

11.4. Запуск программы из программы

Пользовательская программа может в ходе своего выполнения запустить другую программу из COM- или EXE-файла. Эта возможность обеспечивается функцией DOS 0x4B.
Подфункция 1 этой функции выполняет загрузку и запуск другой программы. Для загружаемой (порожденной) программы DOS выделяет память из своего ресурса, следовательно, программа-родитель должна обеспечить наличие свободной памяти в системе (в первую очередь это относится к COM-родителям, так как при их загрузке весь ресурс памяти DOS отдается им) средствами, рассмотренными в следующей главе. Для запуска программа-родитель должна сформировать строку вызова, содержащую имя файла вызываемой программы (с полным маршрутом, если файл находится не в текущем каталоге) и построить EPB (Exec Parameters Block - Блок Параметров Выполнения) - см. struct EPB в примере 11.5. EPB содержит сегментный адрес окружения, которое копируется в сегмент окружения создаваемый для порожденной программы (если этот адрес 0, то в него копируется окружение родителя) и адреса строки параметров и двух блоков FCB, помещаемых в PSP порожденной программы. При обращении к функции 0x4B в регистр AL записывается 1 - код подфункции, в регистры DS:DX - адрес строки вызова, а в ES:BX - адрес EPB.

Наш пример 11.5 состоит из двух программных файлов: 11_5.C и 11_5_A.C, каждому из которых должен соответствовать свой EXEфайл. Программа-родитель 11_5 находит свою строку вызова и заменяет в ней свое имя на имя порождаемой программы (предполагается, что обе программы будут размещены в одном каталоге). Далее родитель получает у DOS блок памяти, в который заносит формируемое для порождаемой программы окружение (признак '$' в конце текста окружения нужен только для программы-родителя как признак конца окружения, он не копируется в окружение). Родитель запрашивает у оператора строку вызова, формирует EPB и обращается к функции 0x4B. Это обращение вызывает загрузку порождаемой программы и передачу ей управления. Порожденная программа получает свой PID и сообщает оператору его и PID родителя, из PSP находит свой сегмент окружения и выводит на экран окружение и строку вызова, а также строку параметров из PSP. Перед завершением порожденная программа запрашивает у оператора код завершения и завершается по функции DOS 0x4C, передавая ей полученный код завершения. Программа-родитель, когда к ней возвращается управление, получает этот код завершения при помощи функции 0x4D.


/*==               ПРИМЕР 11.5 == Файл 11_5.C            ==*/
/*======== Программа, вызывающая другую программу ========*/
#include <dos.h>
#include <string.h>
#include <stdlib.h>
#define byte unsigned char
#define word unsigned int
struct EPB {  /* блок параметров EXEC */
  word env;             /* адрес окружения */
  char far *parm_str;   /* адрес строки параметров */
  byte far *fcb1;       /* адреса FCB */
  byte far *fcb2;
  } epb;
char call_string[128];  /* строка вызова */
char parm_string[128] = "*";  /* строка параметров,
                     1-ым байтом будет длина строки */
char env_str[]=
  "Специальное окружение\0   для иллюстрации\0\0$";
char father_file[] = "11_5.EXE";
char sun_file[] = "11_5_A.EXE";
word env;         /* адрес подготовленного окружения */
word pid;         /* свой PID */
union REGS rr;
struct SREGS sr;
char far *s, *s1;

main() {
  /* определение своего PID */
  rr.h.ah=0x62; intdos(&rr,&rr); pid=rr.x.bx;
  /* формирование строки вызова */
  for(s=(char *)MK_FP(peek(pid,0x2C),0); *s|*(s+1); s++);
  strcpy(call_string,s+4);
  if ((s=strstr(call_string,father_file))==NULL) exit(0);
  *s='\0';  strcat(call_string,sun_file);
  printf("\nПрограмма-родитель: PID = %04X\n",pid);
  printf("Вызов: >%s %s\n",call_string,parm_string+1);
  /* выделение памяти для нового окружения */
  rr.h.ah=0x48; rr.x.bx=4; intdos(&rr,&rr);
  if (rr.x.cflag)  printf("Ошибка 48h\n");
  else {  /* формирование нового окружения и EPB */
    epb.env=env=rr.x.ax; s=(char *)MK_FP(env,0);
    for(s1=env_str; *s1!='$'; *(s++)=*(s1++));
    /* формирование строки параметров */
    printf("Введите строку параметров >");
    gets(parm_string+1);
    epb.parm_str=parm_string;
    parm_string[0]=strlen(parm_string)-1;
    epb.fcb1=epb.fcb2=NULL;
    /* загрузить и выполнить */
    rr.h.ah=0x4b;   /* функция 4B */
    rr.h.al=0;      /* подфункция 0 */
    sr.ds=FP_SEG(call_string);   /* адр.строки вызова */
    rr.x.dx=FP_OFF(call_string);
    sr.es=FP_SEG(&epb);          /* адр.EPB */
    rr.x.bx=FP_OFF(&epb);
    intdosx(&rr,&rr,&sr);
    if (rr.x.cflag) printf("Ошибка EXEC - %d\n",rr.x.ax);
    else {  /* получение кода возврата */
      rr.h.ah=0x4d;     /* функция 4D */
      intdos(&rr,&rr);
      printf("Возврат из вызова - %d, с кодом - %02xh\n",
            rr.h.ah,rr.h.al);
      }
    /* освобождение памяти */
    rr.h.ah=0x49; sr.es=env; intdosx(&rr,&rr,&sr);
    if (rr.x.cflag) printf("Ошибка 49h\n");
    }
}
/*==                                  Файл 11_5_A.C                ==*/
/*======== Программа, вызываемая из другой программы ========*/

#include <dos.h>
#include <stddef.h>
main(int argn, char *argv[]) {
union REGS rr;
  int i;
  char *eee;
  printf("Hello, I`m 11_5_A!\n");
  printf("ПАРАМЕТРЫ:\n");
  for (i=0; i<argn; i++)
    printf(">>%s<<\n",argv[i]);
  printf("ОКРУЖЕНИЕ:\n");
  for (i=0; environ[i]!=NULL; i++)
    printf("%s\n",environ[i]);
  rr.h.ah=0x4c; rr.h.al=0x47;
  intdos(&rr,&rr);
} 


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