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

Одна из важнейших систем MS-DOS - система управления программами . Она отвечает за выполнение процедуры запуска и завершения программ, в том числе имеющих оверлейную структуру.

Теперь, когда мы знаем структуру памяти на момент завершения загрузки операционной системы, можно посмотреть, а что же происходит дальше, когда пользователь запускает какую-нибудь программу.

3.1. Форматы программных файлов

В среде MS-DOS пользователь может запустить два типа программ (если не считать пакетных файлов, которые, вообще говоря, не являются программами, состоящими из машинных кодов). Файлы, содержащие программы этих двух типов, имеют расширение имени .com и .exe (мы будем называть их, соответственно, com-программы и exe-программы).

Указанные программные файлы имеют различный формат и загружаются по-разному, однако, когда загрузка завершена, структура распределенной для них памяти выглядит совершенно одинаково.

Программы, которые хранятся в файлах с расширением имени .com - это двоичный образ программы, состоящий из кода и данных. Образно говоря, com-файл содержит программу в "чистом" виде. Такая программа (как и exe-программа) может загружаться в любое место памяти. MS-DOS выполняет ее привязку к физическим адресам при загрузке с помощью установки сегментных регистров. Существенным ограничением com-программы является то, что она не может занимать больше одного сегмента (соответственно, стандартный com-файл не может иметь размер, превосходящий 64 Кбайта).

Программа второго типа (exe-программа) может иметь любой размер. В самом начале файла программы содержится заголовок (у файла com-программы заголовка нет). Этот заголовок используется операционной системой в процессе загрузки программы в память для правильной установки сегментных регистров. Заголовок exe-файла нужен только при загрузке; когда программа загружена и готова к работе, самого заголовка уже нет в памяти.

Заголовок EXE-файла

Заголовок exe-файла состоит из форматированной зоны и таблицы расположения сегментов (Relocation Table ). Форматированная зона выглядит следующим образом:

Смещение, байт Размер, байт Имя поля Описание
0 2 signature Два байта 'MZ' (4Dh, 5Ah), идентифицирующие exe-файл
2 2 part_pag Размер последней страницы программы в байтах (страница содержит 512 байт)
4 2 file_size Размер программы в страницах по 512 байт
6 2 rel_item Количество элементов в таблице расположения сегментов
8 2 hdr_size Размер заголовка файла в параграфах (длина параграфа - 16 байт)
10 2 min_mem Минимальное количество памяти в параграфах, которое нужно зарезервировать после области загруженной программы
12 2 max_mem Максимальное количество памяти в параграфах, которое нужно зарезервировать после области загруженной программы
14 2 ss_reg Начальное значение для загрузки сегментного регистра SS
16 2 sp_reg Начальное значение для загрузки регистра SP
18 2 chk_summ Контрольная сумма всех слов в файле
20 2 ip_reg Значение, которое будет загружено в регистр IP при запуске программы
22 2 cs_reg Смещение от начала программы для установки сегментного регистра кода CS
24 2 relt_off Смещение таблицы расположения сегментов программы от начала exe-файла
26 2 overlay Номер оверлея, равен 0 для основного модуля программы

Таблица расположения сегментов программы начинается сразу после форматированной области и состоит из четырехбайтовых значений в формате <смещение:сегмент>.

Область файла после таблицы расположения сегментов выравнивается на границу параграфа с помощью байта-заполнителя. Дальше начинается сама программа.

Приведем описание заголовка файла и таблицы расположения сегментов, которые вы можете использовать для доступа к отдельным полям указанных структур:

typedef struct 
{
  unsigned signature;
  unsigned part_pag;
  unsigned file_size;
  unsigned rel_item;
  unsigned hdr_size;
  unsigned min_mem;
  unsigned max_mem;
  unsigned ss_reg;
  unsigned sp_reg;
  unsigned chk_summ;
  unsigned ip_reg;
  unsigned cs_reg;
  unsigned relt_off;
  unsigned overlay;
} EXE_HDR;

typedef struct 
{
  unsigned offset;
  unsigned segment;
} RELOC_TAB;

Программа EXELIST

Для демонстрации приемов работы с заголовком exe-файла приведем исходный текст программы EXELIST (листинг 3.1).

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

Листинг 3.1. Файл exelist\ exelist.cpp

#include <stdio.h>
#include <stdlib.h>

typedef struct
{
  unsigned signature;
  unsigned part_pag;
  unsigned file_size;
  unsigned rel_item;
  unsigned hdr_size;
  unsigned min_mem;
  unsigned max_mem;
  unsigned ss_reg;
  unsigned sp_reg;
  unsigned chk_summ;
  unsigned ip_reg;
  unsigned cs_reg;
  unsigned relt_off;
  unsigned overlay;
} EXE_HDR;

typedef struct 
{
  unsigned offset;
  unsigned segment;
} RELOC_TAB;

void main(int, char *[]);
int get_exeh(EXE_HDR *exeh, RELOC_TAB **rtb,
             FILE *exe_file);
int listhdr(char *path);

void main(int argc, char *argv[])
{
  printf("Просмотр заголовка exe-файла\n"
    "(C) Фролов А.В., 1995\n\n");

  if(argc != 2)
  {
    printf("  Укажите путь к exe-файлу в качестве"
           "параметра\n" );
    return;
  }

  if(listhdr(argv[1]) != 0)
  {
    printf("Ошибка в формате файла или нет такого"
      "файла\n");
    return;
  }
}

// -------------------------------------------
// listhdr
// Отображение заголовка exe-файла
// -------------------------------------------

int listhdr(char *path)
{
  EXE_HDR     header;
  RELOC_TAB   *reloc;
  FILE        *inpfile;
  int         i;

  if((inpfile = fopen(path,"rb")) == 0)
    return(-1);

  if(get_exeh(&header,&reloc,inpfile) != 0)
  {
    fclose(inpfile);
    return(-1);
  }
  printf("Магическое число:                   %04X\n"
	 "Длина последней страницы файла:      %d\n"
	 "Количество страниц в файле:          %d\n"
	 "Кол. элементов табл. перемещений:    %d\n"
	 "Размер заголовка в параграфах:       %d\n"
	 "Минимальная память для программы:    %04X\n"
	 "Максимальная память для программы:   %04X\n"
	 "Значение адреса стека SS:SP:         %04X:%04X\n"
	 "Контрольная сумма:                   %04X\n"
	 "Значения для регистров CS:IP:        %04X:%04X\n"
	 "Смещение табл. перемещений:          %02X\n"
	 "Номер оверлея:                       %d\n",
	 header.signature, header.part_pag,
	 header.file_size, header.rel_item,
	 header.hdr_size,  header.min_mem,
	 header.max_mem,   header.ss_reg,
	 header.sp_reg,    header.chk_summ,
	 header.cs_reg,    header.ip_reg,
	 header.relt_off,  header.overlay);

  if(reloc != 0)
  {
    printf("\nСодержимое таблицы перемещений:\n\n");

      for(i=0; i < header.rel_item; i++)
      {
        printf("%04X:%04X\n",
          (reloc+i)->segment, (reloc+i)->offset);
      }
      free(reloc);
   }

   fclose(inpfile);
   return(0);
}

// -------------------------------------------
// get_exeh
// Прочитать заголовок exe-файла
//
// Функция читает заголовок exe-файла в структуру
// типа exe_HDR, заказывает память для таблицы
// размещений сегментов и считывает таблицу
// в эту область. Адрес заказанной области
// помещается по адресу, указанному через
// параметр rtb.
// Если таблица размещений отсутствует, память для
// нее не заказывается.
//
// Параметры:
//   exeh -  указатель на структуру, которая
//           должна быть заполнена информацией
//           из заголовка exe-файла
//
//   rtb  -  указатель на слово памяти, в котором
//           хранится указатель на таблицу
//           размещений сегментов программы
//
//   exe_file - указатель на открытый exe-файл
//
// Возвращаемое значение:
//   0 при успешном считывании заголовка;
//  -1 в случае неправильного формата заголовка
// -------------------------------------------

int get_exeh(EXE_HDR *exeh, RELOC_TAB **rtb,
             FILE *exe_file)
{
  int i, j, k;

  // Считываем форматированную часть заголовка
  for(i=0; i < sizeof(EXE_HDR); i++)
  {
    *(((char*)exeh) + i) = fgetc(exe_file);
    if(feof(exe_file)) break;
  }

  // Убеждаемся, что это EXE-файл
  if(exeh->signature != 0x5a4d) return(-1);

  // Eсли есть таблица перемещений,
  // заказываем для нее память
  if((i = exeh->rel_item) != 0)
  {
    *rtb =
      (RELOC_TAB*)malloc(i * sizeof(RELOC_TAB) + 16);

    // Считываем таблицу перемещений
    for(k=0; k<i; k++)
    {
      for(j=0; j < sizeof(RELOC_TAB); j++)
      {
        *((char*)(*rtb)+j+k*sizeof(RELOC_TAB)) =
           fgetc(exe_file);
        if(feof(exe_file)) break;
      }
    }
  }
  else
    *rtb = (RELOC_TAB *)NULL;
  return(0);
}

3.2. Процесс загрузки программ в память

Загрузка com- и exe-программ происходит по-разному, однако есть некоторые действия, которые операционная система выполняет в обоих случаях одинаково:

А дальше действия системы по загрузке com- и exe-программ будут различаться.

Для com-программ, которые представляют собой двоичный образ односегментной программы, выполняется чтение файла программы с диска и запись его в память по адресу PSP :0100h.

Размер обычных com-программ, как мы уже говорили, не превышает 64 Кбайт, так как они состоят только из одного сегмента. Но, строго говоря, com-программы могут состоять и из нескольких сегментов. В этом случае они должны сами управлять содержимым сегментных регистров, используя в качестве базового адрес PSP .

Загрузка COM-программы

В процессе загрузки com-программы операционная система выполняет следующие действия:

Загрузка EXE-программы

Загрузка exe-программы происходит значительно сложнее, так как связана с настройкой сегментных адресов:

size = ((file_size * 512) - (hdr_size * 16) - part_pag
START_OFF = hdr_size * 16;
START_SEG = <сегментный адрес PSP > + 10h;

- считывается содержимое элемента таблицы как два двухбайтных слова (OFF, SEG);

- вычисляется сегментный адрес ссылки перемещения по формуле:

REL_SEG = (START_SEG + SEG)

- выбирается слово по адресу REL_SEG:OFF, затем к этому слову прибавляется значение START_SEG, после чего сумма записывается обратно по тому же адресу

При инициализации регистры ES и DS устанавливаются на начало PSP , регистр AX устанавливается так же, как и для com-программ, в сегментный регистр стека SS записывается значение START_SEG + ss_reg, а в регистр SP записывается значение sp_reg.

Для передачи управления программе в сегментный регистр CS записывается значение START_SEG + cs_reg, а в регистр IP - значение ip_reg. Такая запись невозможна напрямую, поэтому операционная система сначала записывает в свой стек значение для CS, затем значение для IP и после этого выполняет команду дальнего возврата RETF (команда возврата из дальней процедуры).

3.3. Префикс программного сегмента PSP

Префикс программного сегмента всегда создается при загрузке программы в память и имеет следующий формат:

Программы могут получить из PSP такую информацию, как параметры командной строки при запуске, размер доступной памяти. Зная адрес PSP, легко найти сегмент области переменных среды и получить другую полезную информацию.

Формат PSP

Формат блока PSP и описание назначения всех его полей приведены ниже:

Смещение, байт Размер, байт Имя поля Описание
0 2 int20h Двоичный код команды INT 20h (программы могут использовать эту команду для завершения своей работы)
2 2 mem_top Нижняя граница доступной памяти в системе в параграфах
4 1 reserv1 Зарезервировано
5 5 call_dsp Команда CALL межсегментного вызова диспетчера MS-DOS
10 4 term_adr Адрес завершения (Terminate Address)
14 4 cbrk_adr Адрес обработчика прерывания, который получает управление, если пользователь нажал комбинацию клавиш <Ctrl+Break>
18 4 crit_err Адрес обработчика критической ошибки
22 2 parn_psp Сегмент PSP программы, запустившей данную программу (программы-родителя)
24 20 file_tab Таблица открытых файлов; если в этом поле находятся байты 0FFH, то таблица не используется
44 2 env_seg Сегмент блока памяти, содержащего переменные среды
46 4 ss_sp Адрес стека программы SS:SP
50 2 max_open Максимальное количество открытых файлов
52 4 file_tba Адрес таблицы открытых файлов
56 24 reserv2 Зарезервировано
80 3 disp Диспетчер функций MS-DOS
83 9 reserv3 Зарезервировано
92 16 fcb1 Если первый аргумент командной строки содержит правильное имя файла, поле форматируется как стандартный блок FCB
108 20 fcb2 Заполняется для второго аргумента командной строки аналогично fcb1
128 1 p_size Количество значащих символов в неформатированной области параметров, либо буфер обмена с диском DTA , назначенный по умолчанию
129 127 parm Неформатированная область параметров, заполняется при запуске программы из командной строки

Для обращения к полям PSP мы определим тип данных PSP:

typedef struct
{
  unsigned char int20h[2];
  unsigned mem_top;
  unsigned char reserv1;
  unsigned char call_dsp[5];
  void far *term_adr;
  void far *cbrk_adr;
  void far *crit_err;
  unsigned parn_psp;
  unsigned char file_tab[20];
  unsigned env_seg;
  void far *ss_sp;
  unsigned max_open;
  void far *file_tba;
  unsigned char reserv2[24];
  unsigned char disp[3];
  unsigned char reserv3[9];
  unsigned char fcb1[16];
  unsigned char fcb2[20];
  unsigned char p_size;
  unsigned char parm[127];
} PSP ;

Используя поле parn_psp, можно определить адрес PSP родительской программы, то есть программы, запустившей вашу программу.

Поле term_adr содержит значение, полученное из таблицы векторов прерываний для прерывания INT 22h. Это адрес программы, которая получает управление, когда текущая программа завершает свою работу. Такой программой может быть, например, программа command.com .

Ваша программа может определить функцию, которая будет получать управление при завершении ее работы. Для этого она должна записать свой собственный адрес в ячейку таблицы векторов прерываний, соответствующую прерыванию INT 22h, а затем запустить другую программу. Поле term_adr блока PSP запущенной программы будет содержать адрес родительской программы. Когда основная программа завершит свою работу, MS-DOS восстановит адрес программы завершения в векторе прерывания INT 22h из поля term_adr блока PSP.

Поле cbrk_adr содержит адрес программы обработки прерывания, которое возникает, когда пользователь нажимает комбинацию клавиш <Ctrl+Break>. При запуске программы этот адрес переписывается из ячейки таблицы векторов прерываний, соответствующей прерыванию INT 23h .

Программа может устанавливать свою собственную функцию обработки прерывания по комбинации клавиш <Ctrl+Break>. Поэтому при завершении работы программы MS-DOS восстанавливает оригинальное значение из поля cbrk_adr.

Поле crit_err предназначено для восстановления содержимого вектора прерывания INT 24h (адреса обработчика критических ошибок).

Способы переназначения векторов будут приведены в главе, посвященной прерываниям .

Конечно, программы, составленные на языке С, не обязательно должны использовать PSP для доступа к параметрам командной строки и переменным среды. Для этого есть параметры функции main и набор функций типа getenv, putenv и т. п., предназначенных для работы со средой. Но блок PSP содержит и другую информацию, доступ к которой с помощью стандартных функций невозможен.

Определение адреса PSP

Как программе определить адрес своего PSP ?

Очень просто сделать это в программах, составленных на языке ассемблера: при запуске программы адрес PSP передается ей через регистры DS и ES. То есть этот адрес равен DS:0000 или ES:0000 (для com-программ на PSP указывают также регистры CS и SS).

Программам, составленным на языке С, доступна глобальная переменная _psp типа unsigned int. Эта переменная содержит сегментный адрес PSP .

Программа PARM

В качестве примера приведем текст программы PARM (листинг 3.2), составленной на языке ассемблера, которая выводит на экран передаваемые ей через PSP параметры запуска.

Листинг 3.2. Файл parm\ parm.asm

  .MODEL  tiny
  DOSSEG
  .DATA

parm_msg  DB "Укажите параметры", 13, 10, "$"

  .CODE
  .STARTUP

  mov    cl,ds:80h ; количество символов
                   ;   в командной строке
  cmp    cl,0
  je     ask_parm  ; нет параметров - просим
                   ;   указать параметры

  mov    si,81h    ; со смещением 81h
                   ; начинается область параметров
  cld

get_parm:

; Загружаем в al очередной символ строки параметров
  lods   BYTE PTR es:[si] 

  mov    ah,2      ; выводим его на экран
  mov    dl,al
  int    21h

  loop get_parm
  jmp  end_progr

ask_parm:

  mov     ah, 9h
  mov     dx, OFFSET parm_msg
  int     21h

end_progr:
  
  .EXIT   0
  END

Для трансляции и редактирования программы PARM вы можете использовать пакетный файл, приведенный в листинге 3.3.

Листинг 3.3. Файл parm\mk.bat

tasm parm
tlink parm /t

Программа PSPLIST

Программа PSPLIST (листинг 3.4), составленная на языке С, определяет адрес своего блока PSP , затем показывает содержимое некоторых полей PSP.

Листинг 3.4. Файл psplist\psplist.cpp

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

typedef struct
{
  unsigned char int20h[2];
  unsigned mem_top;
  unsigned char reserv1;
  unsigned char call_dsp[5];
  void far *term_adr;
  void far *cbrk_adr;
  void far *crit_err;
  unsigned parn_psp;
  unsigned char file_tab[20];
  unsigned env_seg;
  void far *ss_sp;
  unsigned max_open;
  void far *file_tba;
  unsigned char reserv2[24];
  unsigned char disp[3];
  unsigned char reserv3[9];
  unsigned char fcb1[16];
  unsigned char fcb2[20];
  unsigned char p_size;
  unsigned char parm[127];
} PSP ;

void main(void)
{
  PSP  far *psp_ptr;

  // Конструируем указатель на PSP 
  psp_ptr = (PSP  far *)MK_FP (_psp, 0);

  printf("PSP  расположен по адресу:    %Fp\n"
         "Доступно памяти, байт:       %ld\n"
         "PSP  родительской программы:  %Fp\n"
         "\n", psp_ptr,
         (long)(psp_ptr->mem_top)*16L,
         MK_FP (psp_ptr->parn_psp, 0));
}

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

Ваша программа может при необходимости запустить другую exe- или com-программу.

Программа, составленная на языке ассемблера, запускает другую программу с помощью функции 4Bh прерывания INT 21h . Для выполнения той же задачи из программ, составленных на языке С, следует использовать разнообразные функции, входящие в состав стандартной библиотеки системы разработки.

Сначала рассмотрим процедуру запуска программы при помощи функции 4Bh прерывания INT 21h .

Перед вызовом прерывания вы должны загрузить регистры процессора следующим образом:

Регистр Содержимое
AH 4Bh
AL Код подфункции (0, 1, 2, 3, 5)
DS:DX Указатель на текстовую строку в формате ASCIIZ , содержащую путь к запускаемой программе
ES:BX Указатель на блок параметров EPB

После возврата из прерывания флаг переноса CF устанавливается в 0, если ошибок не было, и в 1 - при обнаружении ошибок. Если произошла ошибка, ее код записывается в регистр AX:

Код ошибки Описание
1 Неверный код подфункции
2 Не найден файл запускаемой программы
3 Указанный путь не найден
4 Слишком много открытых файлов
5 Нет доступа
8 Нет памяти для загрузки программы
10 Длина блока среды больше 32 Кбайт
11 Неправильный формат запускаемого exe-файла

Функция 4Bh прерывания INT 21h имеет несколько подфункций:

Код Описание
0 Загрузить и выполнить программу
1 Загрузить, но не выполнять программу
2 Загрузить, но не выполнять программу (недокументированная подфункция)
3 Загрузить программу как оверлей (не создавая при этом блок PSP )
5 Подготовить программу для выполнения

Опишем эти подфункции более подробно.

Загрузка и выполнение программы

Для функции 0 регистры DS:DX должны указывать на полный путь запускаемой программы в формате ASCIIZ (т. е. на текстовую строку, закрытую двоичным нулем). Блок параметров EPB (Exec Parameter Block ) в этом случае имеет следующий формат:

Смещение, байт Размер, байт Имя поля Описание
0 2 seg_env Сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы
2 4 cmd Дальний адрес строки параметров для запускаемой программы. Эта строка должна иметь такой же формат, как и в PSP , т. е. в начале строки находится байт со значением, равным количеству символов в строке параметров, а затем - сама строка параметров
6 4 fcb1 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 5Ch
10 4 fcb2 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 6Ch

Запущенной программе доступны все файлы, открытые родительской программой.

Если родительская программа сама формирует среду для дочерней программы, она должна подготовить новую среду на границе параграфа и поместить значение сегментного адреса в поле seg_env блока EPB .

Для примера приведем исходный текст простой программы SPARM (листинг 3.5), которая запускает программу с именем parm.com из текущего каталога. Программу parm.com мы уже рассматривали (листинг 3.2). Эта программа выводит на экран параметры, полученные ей при запуске через командную строку.

Листинг 3.5. Файл sparm\sparm.asm

  .MODEL  tiny
  DOSSEG
  .DATA

path         db "PARM.COM",0
command_line db 11,"Parm1 Parm2"
epb          dw 0
cmd_off      dw ?
cmd_seg      dw ?
fcb1         dd ?
fcb2         dd ?

  .CODE
  .STARTUP
;
; Освобождаем лишнюю память за концом программы
;
  mov     bx, OFFSET last ; смещение конца программы

  mov     cl,4    ; вычисляем длину программы в параграфах
  shr     bx,cl

  add     bx,17   ; добавляем 1 параграф для
                  ; выравнивания и 256 байт для стека

  mov     ah, 4Ah ; изменяем размер выделенного
  int     21h     ; блока памяти

  mov     ax,bx   ; устанавливаем новое значение
  shl     ax,cl   ;  для указателя стека
  dec     ax
  mov     sp,ax

  mov     bx,OFFSET command_line ; адрес командной
  mov     cmd_off,bx             ; строки для блока EPB 
  mov     cmd_seg,ds

  mov     ax,ds
  mov     es,ax

  mov     bx, OFFSET epb  ; ES:BX указывают на EPB 
  mov     dx, OFFSET path ; DS:DX указывают на путь
                          ; к файлу запускаемой программы

  mov     ax, 4B00h      ; AH = 4Bh
                         ; AL = 0 загрузить и выполнить
  int     21h
  .EXIT   0

last:   db ?
END

Программа SPARM освобождает всю неиспользуемую ей память, после чего на освободившееся место загружает программу parm.com. Такая процедура необходима потому, что MS-DOS выделяет всю имеющуюся память в распоряжение запускаемой com-программы. Поэтому при попытке запустить программу без предварительного освобождения части памяти функция 4Bh вернет код ошибки 8 (нет памяти для загрузки программы).

Для изменения размера блока памяти, выделенного программе, мы использовали функцию 4Ah прерывания INT 21h .

Загрузка программы без выполнения

Подфункции 1 и 2 прерывания INT 4Bh используются операционной системой MS-DOS для собственных нужд (это внутренние подфункции MS-DOS). Они также необходимы для создания программ-отладчиков, таких как, например, debug.com или td.exe. Мы приведем недокументированный формат блока EBP для этих функций.

Для подфункции 1 блок EBP имеет следующий формат:

Смещение, байт Размер, байт Имя поля Описание
0 2 seg_env Сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы
2 4 cmd Дальний адрес строки параметров для запускаемой программы
6 4 fcb1 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 5Ch
10 4 fcb2 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 6Ch
14 4 ss_sp В это поле после завершения работы запускаемой программы будет записано содержимое регистров SS:SP
18 4 entry_p Адрес точки входа в загруженную программу, который нужно записать в регистры CS:IP при запуске программы

Формат блока EPB для подфункции 2:

Смещение, байт Размер, байт Имя поля Описание
0 2 seg_env Сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы
2 4 cmd Дальний адрес строки параметров для запускаемой программы
6 4 fcb1 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 5Ch
10 4 fcb2 Адрес блока FCB . Этот адрес будет записан в блок PSP со смещением 6Ch

Загрузка программного оверлея

Подфункция 3 используется для загрузки программных оверлеев . Оверлей загружается в адресное пространство родительской программы, поэтому MS-DOS не заказывает дополнительной памяти и не строит PSP . Формат EPB для этой подфункции:

Смещение, байт Размер, байт Имя поля Описание
0 2 seg_env Сегментный адрес, по которому загружается программа
2 4 reloc Фактор перемещения. Для exe-программ обычно содержит то же значение, что и поле seg_env, для com-программ в этом поле находится значение 0

Следующий фрагмент программы загружает программу parm.com как оверлей без передачи ей управления (программа загружается в буфер buff):

  .DATA
path    db "PARM.COM",0
epb     dw 0
reloc   dd 0
  .CODE
  .STARTUP

  mov     ax,ds
  mov     es,ax
  mov     bx,SEG buff
  mov     epb,bx

  mov     bx,OFFSET epb  ; ES:BX указывают на EPB 
  mov     dx,OFFSET path ; DS:DX указывают на путь
                         ; загружаемой программы

  mov     ax, 4B03h ; AH = 4Bh
                    ; AL = 0 загрузить оверлей
  int     21h
;
; Работа с загруженной программой
........
;
  .EXIT   0
buff:   dd 100 dup(?)
  END

Подготовка программы для выполнения

Подфункция 5 используется для загрузки и предварительной подготовки программы к выполнению . Она впервые появилась в MS-DOS версии 5.0. Вы можете использовать ее вместо недокументированных подфункций 1 и 2.

Если программа, запущенная с помощью подфункции 5, попытается определить версию MS-DOS, ей будет предоставлен не истинный номер версии, а определенный с учетом действия драйвера setver.

Напомним, что с помощью драйвера setver MS-DOS может "обмануть" программу, сообщив ей, что работает MS-DOS, например, версии 3.31 или любой другой версии, указанной пользователем. Такая возможность требуется в тех случаях, когда программа была рассчитана на конкретную версию MS-DOS, но, тем не менее, способна работать и в новой версии.

Для подфункции 5 указатель, расположенный в регистрах DS:DX, должен указывать на структуру EXECSTATE, описанную ниже:

Смещение, байт Размер, байт Имя поля Описание
0 2 Reserved Зарезервировано
2 2 Flags Тип программы: 0 - com-программа, 1 - exe-программа, 2 - оверлей
4 4 ProgName Указатель на текстовую строку ASCIIZ , содержащую имя программы
8 2 PSP Сегмент блока PSP новой программы
10 4 StartAddr Стартовый адрес CS:IP новой программы
14 4 ProgSize Общий размер программы с учетом размера блока PSP

Запуск программ из программ, составленных на языке С

Пользователи языка С имеют в своем распоряжении несколько возможностей запустить программу.

Самый простой способ - использовать функцию system . Эта функция может выполнить любую команду MS-DOS или любую программу, а также пакетный файл. Например:

system("FORMAT A:");

При использовании этой функции должен быть доступен файл command.com .

К сожалению, хотя функция system и возвращает код завершения, по нему нельзя сделать вывод о том, как была выполнена запускаемая программа. Если в качестве аргумента функции будет передано имя несуществующей программы, на экране появится сообщение:

Bad command or file name

Код возврата в этом случае будет 0 - как будто все хорошо!

Другая возможности запустить программу - использовать функции spawn и exec.

Функция spawn и ее разновидности запускают программу как дочерний процесс. Функция exec загружает новую программу как оверлей на место старой и передает ей управление без возврата. После завершения дочерней программы управление будет передано программе command.com или программе, которая запустила родительскую программу.

Семейство функций spawn обеспечивает запуск дочерней программы, передавая ей родительскую или с специально сформированную среду. Кроме того, в файле process.h описаны параметры, которые можно передать функции spawn:

Параметр Описание
P_WAIT Выполнение родительской программы задерживается до завершения дочерней программы
P_NOWAIT Родительская программа продолжает выполнение сразу после запуска дочерней программы. Этот параметр имеет смысл только для мультизадачных операционных систем
P_OVERLAY Загружает программу как оверлей и передает ей управление. Этот режим соответствует функции exec в том смысле, что родительская программа не получит управления после завершения дочерней

Ниже мы привели исходный текст программы SPARM1 (листинг 3.6), которая запускает программу parm.com с помощью функции spawnlpe. Эта функция входит в стандартную библиотеку Borland C++ и позволяет не только запустить программу, но и сформировать для нее среду, а также передать параметры.

Листинг 3.6. Файл sparm1\sparm1.cpp

#include <stdio.h>
#include <conio.h>
#include <process.h>

int main(void)
{
  char *env[] = { "PARMVAR=d:\\VARS", NULL };
  int rc;

  rc = spawnlpe(P_WAIT ,"parm","parm",
    "Parm1", "Parm2", NULL, env);

  if(rc == -1)
    printf("Невозможно запустить процесс");
  else
    printf("\nПроцесс завершен");
  return rc;
}

3.5. Завершение работы программы

Старые версии MS-DOS (до 2.0) требовали выполнения достаточно сложной процедуры для завершения программы. В начале работы программы было нужно сохранить адрес PSP , затем перед завершением работы поместить этот адрес в стек, записать туда же слово 0000h и выполнить команду дальнего возврата. Управление при этом передается в начало PSP, где находится команда INT 20h .

Для версий MS-DOS, начиная с 2.0, существуют более удобные способы.

С помощью прерывания INT 20h или функции 0 прерывания INT 21h обычно завершают свою работу com-программы. Учтите, что перед завершением работы программы регистр CS должен указывать на PSP .

Более удобна функция 4Ch прерывания INT 21h которую можно использовать с любым содержимым регистров.

Последний способ рекомендуется для повсеместного использования. Он позволяет передать родительской программе (например, программе command.com ) код завершения. Этот код доступен для анализа в пакетных файлах командой if errorlevel.

Примеры программ на языке ассемблера, приведенные в нашей книге, содержат директиву .EXIT . Эта директива завершает выполнение программы с помощью функции 4Ch и позволяет передать код завершения.

Если ваша программа запустила дочернюю программу и та завершилась, передав код возврата, то родительская программа может определить этот код с помощью функции 4Dh прерывания INT 21h . Эта функция возвращает код в регистре AX.

Программа, написанная на языке С, может завершаться с помощью оператора return в функции main или с помощью функции exit в любом месте программы. При этом также возможна передача кода возврата.

Существуют еще способы завершения работы программы, при которых программа (или ее часть) остается резидентной в памяти. Это вызов прерывания INT 27h или функции 31h прерывания INT 21h . О таком способе будет подробно рассказано в главе, посвященной резидентным программам.