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 ). Форматированная зона выглядит следующим образом:
Таблица расположения сегментов программы начинается сразу после форматированной области и состоит из четырехбайтовых значений в формате <смещение:сегмент>. Область файла после таблицы расположения сегментов выравнивается на границу параграфа с помощью байта-заполнителя. Дальше начинается сама программа. Приведем описание заголовка файла и таблицы расположения сегментов, которые вы можете использовать для доступа к отдельным полям указанных структур: 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 и описание назначения всех его полей приведены ниже:
Для обращения к полям 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 . Перед вызовом прерывания вы должны загрузить регистры процессора следующим образом:
После возврата из прерывания флаг переноса CF устанавливается в 0, если ошибок не было, и в 1 - при обнаружении ошибок. Если произошла ошибка, ее код записывается в регистр AX:
Функция 4Bh прерывания INT 21h имеет несколько
подфункций:
Опишем эти подфункции более подробно. Загрузка и выполнение программыДля функции 0 регистры DS:DX должны указывать на
полный путь запускаемой программы в формате ASCIIZ
(т. е. на текстовую строку, закрытую двоичным
нулем). Блок параметров EPB (Exec Parameter Block ) в этом
случае имеет следующий формат:
Запущенной программе доступны все файлы, открытые родительской программой. Если родительская программа сама формирует среду для дочерней программы, она должна подготовить новую среду на границе параграфа и поместить значение сегментного адреса в поле 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 имеет следующий формат:
Формат блока EPB для подфункции 2:
Загрузка программного оверлеяПодфункция 3 используется для загрузки программных оверлеев . Оверлей загружается в адресное пространство родительской программы, поэтому MS-DOS не заказывает дополнительной памяти и не строит PSP . Формат EPB для этой подфункции:
Следующий фрагмент программы загружает программу 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, описанную ниже:
Запуск программ из программ, составленных на языке СПользователи языка С имеют в своем распоряжении несколько возможностей запустить программу. Самый простой способ - использовать функцию 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:
Ниже мы привели исходный текст программы 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 . О таком способе будет подробно рассказано в главе, посвященной резидентным программам. |