# 0 - Инициализация драйвера
Эта функция выполняется только один раз при загрузке драйвера и подключении его к операционной системе.
Функция инициализации должна поддерживаться любым драйвером, так как она сообщает операционной системе сведения, необходимые DOS для правильного подключения и использования драйвера.
Приведем формат запроса для команды инициализации:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | n_units | Количество устройств, обслуживаемых драйвером. Это поле заполняется только блочным драйвером. |
(+14) 4 | end_addr | Конечный FAR-адрес резидентной части кода драйвера. В это поле драйвер записывает адрес байта памяти, следующего за той частью кода драйвера, которая должна стать резидентной. |
(+18) 4 | parm | FAR-адрес строки параметров инициализации драйвера из файла CONFIG.SYS. Эта строка содержит все, что находится в строке файла после команды 'DEVICE=', она заканчивается символами перевода строки и возврата каретки 0Ah, 0Dh. При возврате драйвер блочного устройства должен записать в это поле адрес массива указателей на блоки параметров BIOSBIOS (BPB), по одному указателю на каждое устройство, обслуживаемое драйвером. |
(+22) 1 | drive | Номер устройства. Для версии DOS 3.0 и более поздних версий в это поле при загрузке драйвера операционная система заносит номер, назначенный устройству, обслуживаемому драйвером. Например, для устройства А: это 0, для B: - 1 и т.д. |
При инициализации драйвер символьного устройства сохраняет в своей внутренней области данных параметры инициализации, используя адрес parm. Если параметры содержат числовые величины, программа инициализации может произвести их перекодировку и сохранить значения в двоичном формате.
Затем драйвер может выполнить инициализацию обслуживаемого физического устройства ввода/вывода, инициализацию своих внутренних переменных, вывести на экран какие-либо сообщения либо даже запросить у оператора дополнительные данные - функция инициализации может пользоваться для организации диалога с оператором и других действий функциями прерывания 21h с номерами от 01h до 0Ch, 25h, 30h, 35h и функциями BIOS.
Кроме этого, драйвер должен заполнить поле end_addr адресом конца резидентной части драйвера. Так как программа инициализации выполняется только один раз, обычно ее располагают в конце драйвера и для экономии памяти не оставляют резидентной.
Драйверы блочных устройств дополнительно должны возвратить DOS количество обслуживаемых устройств (в поле n_units) и указатель на массив указателей на блоки BPB (в поле parm).
Количество устройств используется DOS для определения логических имен устройств. Например, если Ваш драйвер обслуживает три логических устройства, и на момент его загрузки в системе имеются устройства A:, B: и C:, то устройства, обслуживаемые Вашим драйвером, получат имена D:, E: и F:. Количество устройств необходимо указывать также и в заголовке драйвера, в первом байте поля имени устройства dev_name.
Для каждого логического устройства драйвер должен содержать так называемый блок параметров BIOS (BIOS Parameter Block) BPB.
Блок BPB содержится в загрузочном секторе диска и содержит информацию, необходимую BIOS для работы с диском. Приведем формат BPB:
(0) 2 | sect_siz | Количество байтов в одном секторе диска. |
(+2) 1 | clustsiz | Количество секторов в одном кластере. |
(+3) 2 | res_sect | Количество зарезервированных секторов. |
(+5) 1 | fat_cnt | Количество таблиц FAT. |
(+6) 2 | root_siz | Максимальное количество дескрипторов файлов, содержащихся в корневом каталоге диска. |
(+8) 2 | tot_sect | Общее количество секторов на носителе данных (в разделе DOS). |
(+10) 1 | media | Байт-описатель среды носителя данных. |
(+11) 2 | fat_size | Количество секторов, занимаемых одной копией FAT. |
Приведем фрагмент исходного текста драйвера, возвращающего при инициализации указатель на массив BPB:
lea dx,bpb_ptr mov es:[bx+18],dx mov es:[bx+20],cs . . . . . . . . . .
В этом примере предполагается, что ES:BX содержит адрес заголовка запроса.
Область данных может содержать, например, такое описание bpb_ptr:
bpb: DW 512 ; количество байтов на сектор DB 1 ; количество секторов на кластер DW 1 ; зарезервировано секторов DB 2 ; количество копий FAT DW 64 ; максимальное количество файлов ; в корневом каталоге DW 360 ; общее число секторов на диске DB 0FCh ; описатель среды DW 2 ; количество секторов в FAT ; bpb_ptr DW bpb ; таблица для трех одинаковых DW bpb ; логических устройств DW bpb
Если Ваш драйвер работает с несколькими разными по параметрам логическими устройствами, для каждого устройства необходимо подготовить свой BPB и занести его адрес в соответствующее порядковому номеру устройства место таблицы указателей на блоки BPB.
Какие действия должна выполнить функция инициализации, если выяснилось, что по тем или иным причинам установка драйвера невозможна? Например, ошибочно заданы параметры, не хватает оперативной памяти, других ресурсов или, наконец, отсутствует само обслуживаемое устройство.
Драйвер символьного устройства при этом может указать в качестве конечного адреса резидентной части программы адрес начала драйвера, т.е. адрес заголовка драйвера. Размер резидентной части при этом будет равен нулю.
Блочные драйверы дополнительно должны записать ноль в поле количества обслуживаемых логических устройств n_units.
В обоих случаях включения драйвера в состав операционной системы не произойдет.
Приведем пример простейшего драйвера символьного устройства, который обслуживает только команду инициализации. Фактически этот драйвер не работает ни с каким физическим устройством. Он просто стирает содержимое экрана при инициализации, выводит сообщение и ожидает нажатия оператором любой клавиши. После нажатия драйвер завершает свою работу без включения себя в состав DOS.
Исходный текст драйвера:
.MODEL tiny .CODE ; Драйвер состоит из одного ; сегмента кода
org 0 ; Эта строка может отсутствовать
include sysp.inc
;========================================================
simple PROC far ; Драйвер - это FAR-процедура
;========================================================
E_O_P: ;Метка конца программы, ;устанавливаем ее в начало ;драйвера, т.к. не надо ;оставлять драйвер резидентно ;в памяти
; Заголовок драйвера
dd 0ffffffffh ; адрес следующего драйвера dw 8000h ;байт атрибутов dw dev_strategy ;адрес процедуры стратегии dw dev_interrupt ;адрес процедуры прерывания db 'SIMPLE_D' ;имя устройство (дополненное ;пробелами)
;========================================================
; Программа стратегии
dev_strategy: mov cs:req_seg,es mov cs:req_off,bx ret
; Здесь запоминается адрес заголовка запроса
req_seg dw ? req_off dw ?
;========================================================
;Обработчик прерывания
dev_interrupt: push es ;сохраняем регистры push ds push ax push bx push cx push dx push si push di push bp
; Устанавливаем ES:BX на заголовок запроса
mov ax,cs:req_seg mov es,ax mov bx,cs:req_off
; Получаем код команды из заголовка запроса и умножаем ; его на два, чтобы использовать в качестве индекса ; таблицы адресов обработчиков команд
mov al,es:[bx]+2 shl al,1
sub ah,ah ; Обнуляем AH lea di,functions ; DI указывает на смещение ; таблицы add di,ax ; Добавляем смещение в таблице jmp word ptr [di] ; Переходим на адрес из таблицы
functions LABEL WORD ; Таблица функций
dw initialize dw check_media dw make_bpb dw ioctl_in dw input_data dw nondestruct_in dw input_status dw clear_input dw output_data dw output_verify dw output_status dw clear_output dw ioctl_out dw Device_open dw Device_close dw Removable_media
; Выход из драйвера, если функция не поддерживается
check_media: make_bpb: ioctl_in: nondestruct_in: input_status: clear_input: output_verify: output_status: clear_output: ioctl_out: Removable_media: Device_open: Device_close: output_data: input_data:
or es:word ptr [bx]+3,8103h jmp quit
;========================================================
quit: or es:word ptr [bx]+3,100h pop bp pop di pop si pop dx pop cx pop bx pop ax pop ds pop es ret
;========================================================
; Процедура выводит на экран строку ; символов в формате ASCIIZ
dpc proc near
push si
dpc_loop: cmp ds:byte ptr [si],0 jz end_dpc mov al,ds:byte ptr [si] @@ out_ch al inc si jmp dpc_loop
end_dpc: pop si ret
dpc endp
;========================================================
hello db 13,10,'++' db 13,10,'¦ *SIMPLE* (C)Frolov A., 1990 ¦' db 13,10,'++' db 13,10 db 13,10,'Hit any key...' db 13,10,0
;========================================================
initialize:
lea ax,E_O_P ;смещение конца программы в AX mov es:word ptr [bx]+14,ax ;помещаем его в заголовок mov es:word ptr [bx]+16,cs ;
; Стираем экран
mov dh,18h mov dl,80h
xor cx,cx mov bh,7 xor al,al mov ah,6 int 10h
; Устанавливаем курсор в левый верхний угол экрана
mov bh,0 xor dx,dx mov ah,2 int 10h
; Выводим сообщение
mov ax,cs mov ds,ax mov si,offset hello call dpc
; Ожидаем нажатия на любую клавишу
mov ax,0 int 16h
jmp quit
simple ENDP
END simple
В программе используется макро @@out_ch, описанное в файле sysp.inc и предназначенное для вывода символов на экран дисплея. Файл sysp.inc имеется на дискете, прилагающейся к книге. Приведем текст макро @@out_ch:
@@out_ch MACRO c1,c2,c3,c4,c5,c6,c7,c8,c9,c10 mov ah,02h IRP chr,<c1,c2,c3,c4,c5,c6,c7,c8,c9,c10> IFB <chr> EXITM ENDIF mov dl,chr int 21h ENDM ENDM
Текст этого драйвера транслировался при помощи ассемблера, входящего в состав QuickC 2.01. Полученный объектный модуль обрабатывался пакетным файлом:
link %1.obj; exe2bin %1.exe %1.sys
Вы можете также использовать макроассемблер MASM версии 5.0 или более поздней версии.
Нетрудно заметить, что процедура получения загрузочного модуля драйвера действительно похожа на процедуру получения COM-программы. Сообщение редактора об отсутствии сегмента стека следует проигнорировать, сегмента стека действительно нет и быть не может, так как драйвер состоит из единственного сегмента кода.
Для испытания этого и других драйверов запишите драйвер в корневой каталог системной дискеты (с которой можно загрузить операционную систему) и поместите в файл CONFIG.SYS, находящийся на этой дискете строку:
DEVICE=a:\simple.sys
Когда Ваш драйвер будет отлажен, его можно переписать на диск и подключить к файлу CONFIG.SYS, находящемуся на диске С:.
# 0Ah Проверить индекс на локальный/удаленный
Вызов:
Регистр | |
AH | 44h |
AL | 0Ah |
BX | Индекс (только для устройства, а не для файла) |
Возврат без ошибки:
Регистр | |
CF | 0 |
DX | Слово атрибутов устройства, если бит 15 установлен в 1, это удаленное устройство, в противном случае устройство является локальным |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
06h - BX содержит несуществующий индекс. |
Ошибка 01h может возникнуть, если не загружена команда SHARE.
Данная подфункция позволяет проверить, является ли открытое устройство или файл локальным или удаленным. Справедливы замечания к предыдущей подфункции о том, что работа программы не всегда должна зависеть от расположения файла или устройства.
# 0Bh Установка количества повторов при обращении к файлу
Вызов:
Регистр | |
AH | 44h |
AL | 0Bh |
CX | Продолжительность паузы (по умолчанию используется значение 1) |
DX | Число повторов (по умолчанию равно 3) до вызова программы обработки прерывания 24h - прерывания по критической ошибке ввода/вывода |
Возврат без ошибки:
Регистр | |
CF | 0 |
Возврат с ошибкой:
Регистр | |
CF | 1 |
AX | 01h - запрошена несуществующая функция; |
Код ошибки 01h появляется, если задан недопустимый номер подфункции или не загружена команда SHARE.
Операции, которые могут привести к возникновению конфликтных ситуаций, автоматически повторяются несколько раз до выдачи сообщения об ошибке. Можно задавать количество повторений операций и пауз между ними. Пауза формируется при выполнении пустого цикла, и ее длительность зависит от тактовой частоты процессора на машине:
mov cx,pause loop $
# 0Ch Переключение кодовых страниц
Вызов:
Регистр | ||
AH | 44h | |
AL | 0Ch | |
BX | Индекс для открытого устройства | |
CH | Код категории устройства: 00 - неизвестное устройство; 01 - устройства COM1, COM2 и т.д.; 03 - консоль CON; 05 - устройства печати LPT1, LPT2 и т.д. | |
CL | Код операции: 45h - установить число повторений операции; 4Ah - выбор кодовой страницы; 4Ch - начало подготовки кодовой страницы; 4Dh - конец подготовки кодовой страницы; 5Fh - установить устройство "дисплей"; 65h - получить число повторений операции; 6Ah - получить выбранную кодовую страницу; 6Bh - получить подготовленный список; 7Ah - получить параметры дисплея (ширина, длина и цвет) | |
DS:DX | Указатель на блок параметров. |
Возврат без ошибки:
Регистр | |
CF | 0 |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
06h - BX содержит несуществующий индекс. |
Для подготовки кодовой страницы сначала вызывают эту подфункцию с кодом операции CL=4Ch, затем должна идти серия вызовов подфункции 03h функции 44h прерывания INT21h - запись IOCTL на символьное устройство.
Формат записываемых данных зависит от типа устройства. Драйверы DISPLAY.SYS и PRINTER.SYS, входящие в состав дистрибутива DOS, получают эти данные из файлов с расширением имени .CPI, таких как EGA.CPI, LCD.CPI, 4201.CPI и т.д.
Блок параметров имеет различный формат для разных кодов операций:
CL = 45h | блок параметров состоит из слова, содержащего количество повторений; |
CL = 4Ah, 4Dh, 6Ah | блок параметров состоит из двух слов, первое слово - длина данных (0002), второе - идентификатор кодовой страницы; |
CL = 4Ch | в начале блока параметров расположены три слова - флаги (0000), длина остальной части блока параметров в байтах и количество кодовых страниц, за этими тремя словами следуют слова кодовых страниц. Количество слов кодовых страниц в зависимости от значения MAXFONTS при установке драйвера и может достигать 12; |
CL = 6Bh | первым располагается слово, содержащее длину остальной части блока в байтах, затем идет количество аппаратных кодовых страниц и слова для этих кодовых страниц, в конце расположено слово, содержащее количество подготовленных кодовых страниц и слова с подготовленными кодовыми страницами. Возвращаемый блок может иметь длину до 56 байтов. |
# 0Dh Общее управление вводом/выводом (GENERIC IOCTL)
Вызов:
Регистр | ||
AH | 44h | |
AL | 0Dh | |
BL | Номер дисковода (0 - текущий дисковод, 1 - дисковод А: и т.д.) | |
CH | Код категории устройства: 08h - дисковое устройство | |
CL | Операция:
40h - установить параметры устройства; 60h - получить параметры устройства; 41h - записать дорожку на логическом устройстве; 61h - прочитать дорожку на логическом устройстве; 42h - форматировать дорожку на логическом устройстве; 62h - проверить дорожку на логическом устройстве | |
DS:DX | Указатель на блок параметров. |
Возврат без ошибки:
Регистр | |
CF | 0 |
Возврат с ошибкой:
Регистр | |
CF | 1 |
AX | Код ошибки |
Общая схема использования этой подфункции:
Сохранить параметры устройства операцией 40h.
Установить нужные параметры.
Выполнить операцию ввода/вывода или форматирования.
Восстановить предыдущие параметры.
Формат блока параметров зависит от выполняемой операции:
CL = 40h/60h (получить/установить параметры устройства)
Смещение | Размер | Содержимое поля | |
(0) | 1 | Специальные функции | |
(+1) | 1 | Тип устройства, возвращаемый драйвером:
0 - 320/360 К флоппи-диск (5,25"); 1 - 1,2 М флоппи-диск (5,25"); 2 - 720 К флоппи-диск (3,5"); 3 - 8" флоппи-диск нормальной плотности; 4 - 8" флоппи-диск двойной плотности; 5 - жесткий диск; 6 - накопитель на магнитной ленте; 7 - 1,44 М флоппи-диск (3,5") | |
(+2) | 2 | Атрибуты устройства, возвращаемые драйвером. В этом поле используются только два младших бита. Бит 0 - признак заменяемости среды носителя данных (0 - заменяемая, 1 - не заменяемая), бит 1 - признак наличия аппаратного контроля замены дискеты (1 - контроль выполняется, 0 - контроль не выполняется). Остальные биты зарезервированы и должны содержать 0. | |
(+4) | 2 | Максимальное количество цилиндров на физическом устройстве. Это поле устанавливается драйвером. | |
(+6) | 1 | Тип среды носителя данных. Используется для устройств, поддерживающих несколько типов носителей данных, например, для флоппи-дисковода на 1.2М значение этого поля, равное 0, соответствует дискете на 1.2М, а 1 - 360К. | |
(+7) | 31 | BPB для устройства. Если бит 0 поля специальных функций сброшен, то в этом поле находится новый BPB для устройства. Если бит 0 установлен, драйвер устройства возвращает BPB для всех последующих запросов на построение BPB. | |
(+38) | ? | Таблица разметки дорожки, имеет переменную длину. |
Биты байта специальных функций имеют следующее значение:
Бит 0:
В операции 60h значение этого бита, равное 1, используется для извлечения текущего BPB, как если бы он был получен по команде драйвера с кодом 2 (построить BPB). Значение этого бита, равное 0, говорит о том, что надо извлечь BPB, используемый по умолчанию.
Для операции с кодом 40h значение бита, равное 1, используется для извлечения текущего BPB, значение 0 приводит к использованию BPB, подготовленного в данном блоке параметров.
Бит 1:
Значение этого бита, равное 1, - это указание игнорировать все поля в блоке параметров, кроме поля описания физической структуры дорожки на данном устройстве.
Бит 2:
Значение этого бита, равное 1, говорит о том, что все сектора на этой дорожке имеют одинаковый размер.
Таблица разметки дорожки начинается с двухбайтового слова, содержащего общее количество секторов на дорожке. Затем для каждого сектора в таблице находится по два двухбайтовых слова, содержащих номер сектора (1, 2 и т.д.) и размер сектора. То есть для каждого сектора в таблице содержится два слова.
Если в поле "специальные функции" бит 2 установлен в 1, размеры всех секторов должны быть одинаковыми.
CL = 41h/61h (записать/прочитать дорожку)
Смещение | Размер | Содержимое поля |
(0) | 1 | Специальные функции (это поле всегда содержит 0) |
(+1) | 2 | Номер головки |
(+3) | 2 | Номер дорожки |
(+5) | 2 | Номер начального сектора (нумерация секторов, в отличие от нумерации головок и дорожек начинается с 0) |
(+7) | 2 | Общее количество секторов на дорожке, уменьшенное на единицу |
(+9) | 4 | FAR-указатель на буфер обмена с диском, в который помещается считываемая информация или откуда берется записываемая информация |
Смещение | Размер | Содержимое поля |
(0) | 1 | Специальные функции. Для этой операции определен только бит 0. Перед вызовом команды значение бита, равное 0, требуется для форматирования дорожки. Если этот бит установлен в 1, то проверяется возможность использования заданного формата трека. Если после выполнения команды значение бита 0 равно 0, то поддерживается заданный формат трека и заполненную таблицу разметки дорожки можно использовать. Если значение бита 0 равно 1, то затребованный формат дорожки не поддерживается. |
(+1) | 2 | Номер головки для форматирования/проверки |
(+3) | 2 | Номер дорожки для форматирования/проверки |
# 0Eh Получение информации о логическом дисководе
Вызов:
Регистр | |
AH | 44h |
AL | 0Eh |
BL | Номер дисковода (0 - текущий дисковод, 1 - дисковод А: и т.д.) |
Возврат без ошибки:
Регистр | |
CF | 0 |
AL | Содержимое этого регистра равно 0, если данному дисководу соответствует только одно логическое устройство, или номеру текущего логического дисковода (1 - А:, 2 - В: и т.д.). |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
0Fh - неправильный идентификатор дисковода; |
# 0Fh Установка текущего логического дисковода
Вызов:
Регистр | |
AH | 44h |
AL | 0Fh |
BL | Номер дисковода, который должен стать текущим (0 - текущий дисковод, 1 - дисковод А: и т.д.) |
Возврат без ошибки:
Регистр | |
CF | 0 |
AL | Содержимое этого регистра равно 0, если данному дисководу соответствует только одно логическое устройство, или номеру логического дисковода, который будет использоваться в последующих операциях ввода/вывода (1-А:, 2-В: и т.д.). |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
0Fh - неправильный идентификатор дисковода; |
Последние две команды предназначены для работы с логическими дисководами. На одном физическом дисководе DOS может образовать несколько логических дисков. Эти команды могут использоваться программами, например, для выдачи сообщения о необходимости заменить дискету при переходе от одного логического устройства к другому, если эти устройства расположены на одном физическом накопителе.
# 00H Получить информацию об устройстве
Вызов:
Регистр | |
AH | 44h |
AL | 00h |
BX | Индекс устройства (handle). Значение, которое операционная система возвращает при открытии файла или устройства и которое она затем использует для доступа к открытому файлу или устройству. |
Возврат без ошибки:
Регистр | |
CF | 0 |
DX | Информация об устройстве |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
06h - BX содержит несуществующий или неоткрытый handle. |
Функция возвращает в регистре DX информацию об устройстве, которая имеет следующий формат (для устройства):
Бит | Значение | |
0 | Это устройство является стандартным устройством ввода. | |
1 | Стандартное устройство вывода. | |
2 | NUL-устройство. | |
3 | Часы. | |
4 | Специальное устройство. | |
5 | 1 - двоичный режим работы;
0 - режим ASCII. | |
6 | 0 - при чтении достигнут конец файла. | |
7 | 1 - это слово информации относится к устройству (данный handle относится к устройству);
0 - слово информации относится к файлу. | |
8-10 | Зарезервировано. | |
11 | 1 - Устройство поддерживает команды открытия/закрытия. | |
12 | Сетевое устройство (только для DOS версии 3.0 и более поздних версий). | |
13 | Устройство поддерживает вывод до состояния занятости. | |
14 | Устройство может обрабатывать управляющие строки IOCTL, посылаемые подфункциями 2, 3, 4, 5 функции 44h. Этот бит может быть только прочитан, его установка подфункцией 1 функции 44h не производится. | |
15 | Зарезервировано. |
Если при вызове этой подфункции регистр BX содержал индекс файла, формат получаемой в регистре DX информации будет следующий:
Бит | Значение | |
0-5 | Номер дисковода (0-А:, 1-В: и т.д.). | |
6 | 0 - Была запись в выходной файл. | |
7 | 1 - это слово информации относится к устройству (данный handle относится к устройству);
0 - слово информации относится к файлу. | |
8-11 | Зарезервировано. | |
12 | Сетевое устройство (только для DOS версии 3.0 и более поздних версий). | |
13-14 | Зарезервировано. | |
15 | 1 - Данный файл является удаленным при работе в сети (только для DOS версии 3.0 и более поздних версий). |
Особое внимание следует обратить на бит 5 слова информации об устройстве. Этот бит определяет режим обмена данными DOS и драйвера - двоичный или ASCII. В двоичном режиме управляющие символы CTRL-C, CTRL-P, CTRL-S, CTRL-Z интерпретируются как обычные данные.
# 1 - Проверка замены носителя данных
Эту команду DOS выдает драйверу, когда она хочет проверить, не произошла ли замена носителя данных, например, замена дискеты.
Вообще говоря, довольно трудно определить, заменил ли оператор дискету или там все еще стоит старая, с которой DOS начала работу. Наиболее достоверные результаты можно было бы получить от аппаратуры, если бы она следила за заменой дискет. Но когда аппаратура не отслеживает замены дискет, приходится анализировать метку диска или как-то иначе определять, поменялась дискета или нет. Метка диска - слабое подспорье в этом вопросе, так как обычно при форматировании дискет пользователь не задает метку. Однако MS-DOS 4.01 при форматировании автоматически записывает на дискету уникальный серийный номер, который можно использовать для проверки факта замены дискеты в приемном кармане дисковода.
Можно отслеживать время доступа к диску и считать, что для смены носителя нужно не менее двух секунд.
К чему может привести некорректный ответ DOS на вопрос о замене дискеты?
К очень тяжелым последствиям.
Возможны три варианта ответа на вопрос о замене дискеты:
Да, дискета заменена.
Нет, дискета все та же.
Неизвестно, произошла замена дискеты или нет.
Если DOS получила ответ, что дискета не заменена, она продолжает работу, которой занималась раньше.
Если пришел ответ, что носитель данных заменен, DOS выдает драйверу команду с номером 2. Это запрос драйверу на построение нового BPB. Все буфера, связанные с данным устройством, при этом очищаются, и, если они не были записаны на диск, может произойти потеря информации. Осуществляется чтение каталога и FAT.
Если пришел ответ, что неизвестно, сменили ли носитель данных или нет, действия DOS зависят от наличия в данный момент непустых дисковых буферов. Если непустых буферов нет, DOS считает, что носитель сменился, и ведет себя так, как это было описано раньше. Если есть непустые буфера, DOS записывает буфера на диск. При этом структура информации на новой дискете может оказаться полностью нарушенной.
Приведем формат запроса для команды проверки замены носителя:
(0) 13 | header | Заголовок запроса. | |
(+13) 1 | media | В этом поле драйверу передается байт-описатель среды носителя данных, с которым DOS работала раньше. | |
(+14) 1 | reply | В это поле драйвер должен поместить ответ о факте замены среды:
1 - диск не заменялся; 0 - неизвестно; -1 - диск был заменен. | |
(+15) 4 | vol_id | Указатель на предыдущую метку тома (если установлен бит 11 слова атрибута устройства и диск был заменен) |
Если драйвер поддерживает функцию проверки замены среды носителя данных (бит 11 слова атрибута установлен в 1) и оказалось, что произошла замена диска, драйвер должен вернуть в поле vol_id указатель на область памяти, содержащую предыдущую метку тома в формате ASCIIZ. Если метка тома не используется драйвером, а бит 11 слова атрибутов установлен, необходимо вернуть указатель на строку "NO_NAME", закрытую двоичным нулем.
Байт-описатель среды media классифицирует используемую среду носителя данных, но делает это неоднозначно. Мы приведем характерные для этого байта параметры дисков:
0FFh | 2 стороны, 8 секторов на дорожку; |
0FEh | 1 сторона, 8 секторов на дорожку; |
0FDh | 2 стороны, 8 секторов на дорожку; |
0FCh | 1 сторона, 9 секторов на дорожку; |
0F9h | 2 стороны, 15 секторов на дорожку; |
0F8h | жесткий диск; |
0FEh, 0FDh | используются восьмидюймовые дискеты. |
# 01H Установить информацию об устройстве
Вызов:
Регистр | |
AH | 44h |
AL | 01h |
BX | Индекс файла (handle) |
DH | 0 |
DL | Устанавливаемая информация (биты 0-7 слова информации об устройстве, описанного выше) |
Возврат без ошибки:
Регистр | |
CF | 0 |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
06h - BX содержит несуществующий handle. |
Эта подфункция позволяет устанавливать некоторые биты в слове информации об устройстве.
В зависимости от установки бита 5 (двоичный/ASCII) драйвер по-разному выполняет команды ввода-вывода.
В режиме ASCII обмен с драйвером символьного устройства выполняется по байтам (содержимое поля count в запросе всегда равно 1). Если Вам надо обслуживать быстродействующее устройство ввода/вывода, такой посимвольный обмен может оказаться слишком медленным. В этом случае Вы можете установить бит 5 в единицу и выполнять обмен в двоичном режиме. Разумеется, Вам придется самостоятельно отслеживать управляющие символы.
Ниже мы приведем пример программы, которая сначала устанавливает ASCII-режим для драйвера, описанного выше, выводит на него 8 символов, затем выполняет аналогичную операцию в двоичном режиме. Драйвер при каждом обращении к нему для ввода или вывода выдает сообщение на экран. Запустив программу (и не забыв подключить драйвер), Вы увидите, что в ASCII-режиме для записи или чтения восьми символов драйвер вызывается восемь раз, а в двоичном режиме - только один раз.
#include <io.h> #include <conio.h> #include <stdio.h> #include <fcntl.h> #include <sys\types.h> #include <sys\stat.h> #include <malloc.h> #include <errno.h> #include <dos.h>
int main(void);
union REGS inregs, outregs; struct SREGS segregs;
int main(void) {
char buf[100]; int io_handle; unsigned count;
// Открываем устройство с именем IODRIVER
if( (io_handle = open("IODRIVER", O_RDWR)) == - 1 ) {
// Если открыть не удалось, выводим // код ошибки
printf("Ошибка при открытии устройства %d",errno); return errno; }
// Читаем 8 байт из устройства в буфер buf
if( (count = read(io_handle, buf, 8)) == -1 ) {
// Если при чтении произошла ошибка, // выводим ее код
printf("Ошибка чтения %d",errno); return errno; }
// Закрываем прочитанную строку нулем // для последующего вывода функцией printf
buf[8]=0;
printf("\n___ Введена строка: %s ___",buf);
// Выводим только что прочитанные данные // обратно на то же устройство
if( (count = write(io_handle, buf, 8)) == -1 ) {
// Если при записи произошла ошибка, // выводим ее код
printf("Ошибка записи %d",errno); return errno; }
// Получаем информацию об устройстве
inregs.h.ah = 0x44; inregs.h.al = 0; inregs.x.bx = io_handle; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим ее код
printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); }
// Выводим слово информации об устройстве на экран
printf("\nDevice Information: %04X\n", outregs.x.dx);
// Устанавливаем в 1 бит 5 (переключаем драйвер // в двоичный режим обмена данными
inregs.x.dx = (outregs.x.dx | 0x0020) & 0x00ff;
// Устанавливаем слово информации об устройстве
inregs.h.ah = 0x44; inregs.h.al = 1; inregs.x.bx = io_handle; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим код ошибки
printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); }
// Выводим слово информации об устройстве на экран
printf("\nDevice Information: %04X\n", outregs.x.dx);
// Читаем 8 байт из устройства в буфер buf // Обмен теперь производится в двоичном режиме
if( (count = read(io_handle, buf, 8)) == -1 ) {
// Если при чтении произошла ошибка, // выводим ее код
printf("Ошибка чтения %d",errno); return errno; }
// Закрываем прочитанную строку нулем // для последующего вывода функцией printf
buf[8]=0;
printf("\n___ Введена строка: %s ___",buf);
// Выводим только что прочитанные данные // обратно на то же устройство
if( (count = write(io_handle, buf, 8)) == -1 ) {
// Если при записи произошла ошибка, // выводим ее код
printf("Ошибка записи %d",errno); return errno; }
// Закрываем устройство
close(io_handle);
exit(0); }
# 2 - Построить блок BPB
Эта команда имеет смысл только для блочных устройств и вызывается после предыдущей команды, если произошла смена носителя данных.
Драйвер должен возвратить адрес нового блока BPB.
Формат запроса для этой команды:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | media | В этом поле драйверу передается байт-описатель среды носителя данных, с которым DOS работала раньше. |
(+14) 4 | buf_adr | Адрес буфера обмена. Содержимое этого буфера при вызове драйвера зависит от установки бита 13 слова атрибутов устройства (IBM-формат). Если этот бит равен 0 (устройство формата IBM), буфер содержит первый сектор первой копии FAT. В противном случае указатель установлен на буфер свободного сектора. |
(+18) 4 | bpb_adr | Указатель на новый BPB, записывается в это поле драйвером. |
Операционная система MS-DOS версии 3.0 и более поздних версий включает поддержку аппаратных средств проверки смены носителя данных. Если драйвер обнаружил, что смена носителя данных произошла неправильно, в неподходящий момент времени, он может вернуть ошибку с кодом 15 (неразрешенная замена диска).
# 02H/03h Чтение/запись управляющей информации для символьных устройств
Вызов:
Регистр | |
AH | 44h |
AL | 02h/03h |
BX | Индекс (только для устройства, а не для файла) |
CX | Количество читаемых/записываемых байтов |
DS:DX | Указатель на буфер, в который надо прочитать управляющую информацию или из которого надо записать управляющую информацию |
Возврат без ошибки:
Регистр | |
CF | 0 |
AX | Количество действительно прочитанных/записанных байтов |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
06h - BX содержит несуществующий индекс (handle). |
Эти подфункции позволяют прикладной программе обмениваться произвольной управляющей информацией с драйвером устройства. Они инициируют вызов драйвера с командами 3 и 12 соответственно для операций чтения и записи управляющей информации.
Для использования этих подфункций драйвер должен поддерживать интерфейс IOCTL. Для проверки можно использвать подфункцию 0 (чтение информации устройства). Если бит 14 установлен, драйвер поддерживает IOCTL.
/03H - Чтение/запись секторов.
Выполняется чтение секторов в оперативную память компьютера или запись информации из памяти в сектора диска.
Сектор задается для выбранных устройства, дорожки и головки. Программа должна также задать количество читаемых/записываемых секторов.
# 04H/05h Чтение/запись управляющей информации для блочных устройств
Вызов:
Регистр | |
AH | 44h |
AL | 04h/05h |
BL | Номер дисковода (0 - текущий дисковод, 1 - дисковод А: и т.д.) |
CX | Количество читаемых/записываемых байтов |
DS:DX | Указатель на буфер, в который надо прочитать управляющую информацию или из которого надо записать управляющую информацию |
Возврат без ошибки:
Регистр | |
CF | 0 |
AX | Количество действительно прочитанных/записанных байтов |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
05h - доступ к дисководу запрещен. |
Для использования этих подфункций драйвер должен поддерживать интерфейс IOCTL. Для проверки можно использовать подфункцию 0 (чтение информации устройства). Если бит 14 установлен, драйвер поддерживает IOCTL.
# 5 - Неразрушающее чтение без ожидания
Эта команда предназначена для выборки одного байта из буфера символьного устройства. Для блочных устройств эта команда не используется.
Если в буфере символьного устройства есть байты, которые могут быть прочитаны командой чтения, драйвер возвращает следующий символ, который был бы прочитан.
Если в результате проверки выяснится, что в буфере есть символы, драйвер должен установить бит 9 слова состояния устройства (занятость) в 0, в противном случае - в 1.
Эта команда используется перед выдачей команды чтения для проверки, есть ли в буфере готовые для чтения данные. Такая проверка позволяет избежать длительного ожидания готовности данных при вводе. Кроме того, команда используется для проверки наличия в буфере клавиатуры символа CTRL-BREAK.
Формат запроса для команды 5:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | byte | В это поле драйвер записывает извлеченный из буфера байт, который будет считан по следующей команде ввода. |
# 06H/07h Получить состояние ввода/вывода
Вызов:
Регистр | |
AH | 44h |
AL | 06h/07h |
BX | Индекс (устройства или файла) |
Возврат без ошибки:
Регистр | ||
CF | 0 | |
AL | Для устройства:
00h - устройство не готово к приему/передаче; 0FFh - устройство готово к приему/передаче. Для файла: 00h - достигнут конец файла; 0FFh - конец файла не достигнут. |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
05h - доступ к дисководу запрещен; 06h - BX содержит несуществующий индекс; 0Dh - неправильные данные. |
Эти подфункции предназначены для проверки готовности устройства к вводу/выводу данных или для проверки на достижение конца файла.
, 07H - Прокрутка (скроллинг) окна вверх/вниз.
С помощью этих функций вы сможете переместить выбранную область окна на заданное число строк вверх или вниз. Освободившееся место будет заполняться пустыми строками. Одно из применений этой функции - полная очистка экрана.
# 08H Проверить возможность замены носителя данных для блочного устройства
Вызов:
Регистр | |
AH | 44h |
AL | 08h |
BL | Номер дисковода (0 - текущий дисковод, 1 - дисковод А: и т.д.) |
Возврат без ошибки:
Регистр | ||
CF | 0 | |
AX | 00h - замена носителя возможна;
01h - носитель данных несменный |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
0Fh - неправильный идентификатор дисковода |
Эта подфункция проверяет возможность замены носителя данных. Дискета - заменяемый носитель данных, а жесткий диск или электронный (RAM) диск - нет.
# 09H Получить информацию о том, является ли устройство локальным или удаленным (при работе в сети)
Вызов:
Регистр | |
AH | 44h |
AL | 09h |
BL | Номер дисковода (0 - текущий дисковод, 1 - дисковод А: и т.д.) |
Возврат без ошибки:
Регистр | |
CF | 0 |
DX | Биты атрибута устройства. Если блочное устройство является локальным (расположено на рабочей станции сети Microsoft Networks), бит 12 установлен в 0, остальные биты копируются из заголовка драйвера устройства. Если устройство является удаленным (находится на сервере), устанавливается в 1 только бит 12, остальные биты зарезервированы и равны нулю. |
Возврат с ошибкой:
Регистр | ||
CF | 1 | |
AX | 01h - запрошена несуществующая функция;
0Fh - неправильный идентификатор дисковода; |
Ошибка 01h может возникнуть, если не загружена команда SHARE.
Используя эту подфункцию, программы могут определить принадлежность дисковода локальной станции или серверу. Однако обычно работа программы не должна зависеть от расположения дисковода.
, 09H - Прочитать/записать символ и атрибут.
С помощью этих функций можно прочитать или записать символ и его атрибут. При записи символа можно задать число повторений. Функцию записи с повторением удобно использовать для заполнения области экрана каким-либо символом.
Запись/чтение символа начинается с текущего положения курсора.
, 11H, 12h - Обслуживание адаптера EGA.
Эти функции работают только при использовании дисплейных адаптеров EGA и VGA. Они позволяют устанавливать свою цветовую палитру, загружать знакогенератор (например, шрифтом для русских букв) и выполнять некоторые другие функции.
# 15 - Проверка сменяемости диска
Эта команда используется только для тех драйверов, в слове атрибутов которых бит 11 установлен в 1. Драйвер должен сообщить DOS, возможна ли замена носителя данных. Например, замена жесткого диска обычно невозможна, хотя есть накопители со сменными жесткими дисками.
Драйвер возвращает информацию о возможности замены носителя в бите 9 слова состояния устройства. Значение этого бита, равное 0, драйвер устанавливает, если возможна замена носителя данных, в противном случае устанавливается значение, равное 1.
Запрос состоит только из заголовка.
# 19 - Функции управления вводом/выводом (IOCTL)
Команда с кодом 19 предназначена для выполнения нескольких функций и поддерживается только теми драйверами, у которых в слове атрибутов устройства установлен в 1 бит поддержки IOCTL (бит 14). Эта команда поддерживается DOS версии 3.2 и более поздних версий.
Команда используется для выполнения ряда операций с дисками, доступных обычно лишь на уровне BIOS, например, форматирования, чтение/запись секторов по их абсолютному номеру и т.д. Имеется стандартный интерфейс для различных типов дисков, обеспечиваемых драйвером логических дисков DRIVER.SYS.
DOS имеет специальную функцию номер 44h прерывания 21h. Эта функция имеет множество подфункций и предназначена для поддержки IOCTL. Очень скоро мы займемся этой функцией DOS, а сейчас приведем формат запроса для команды с кодом 19:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | funct | Это поле содержит код функции команды общего IOCTL. |
(+14) 1 | subfunc | Код подфункции для функции funct. |
(+15) 2 | si_reg | Значение регистра SI при вызове функции 44h прерывания 21h. Эта функция DOS предназначена для управления вводом/выводом. |
(+17) 2 | di_reg | Значение, передаваемое при вызове функции 44h прерывания 21h через регистр DI. |
(+19) 4 | buf | Указатель на буфер данных, содержащий управляющую информацию для устройства или предназначенный для приема информации от устройства. |
Блоки управления памятью в MS-DOS
Первое поле векторной таблицы связи mcb_seg содержит сегментный адрес первого блока управления памятью (MCB). Блок MCB всегда начинается на границе параграфа, поэтому полный адрес первого блока будет равен mcb_seg:0.
Для лучшего понимания механизма управления памятью в MS-DOS вспомним карту распределения памяти:
Диапазон адресов | Содержимое |
0000:0000 | Векторы прерываний |
0000:0400 | Область данных BIOS |
0000:0500 | Область данных DOS |
xxxx:0000 | Область программ DOS (расширение BIOS, обработчики прерываний DOS, буфера, области данных, загружаемые драйверы устройств) |
xxxx:0000 | Резидентная порция COMMAND.COM |
xxxx:0000 | TSR-программы (остающиеся резидентными после запуска) |
xxxx:0000 | Выполняющиеся прикладные программы типа COM или EXE |
xxxx:0000 | Транзитная порция COMMAND.COM |
A000:0000 | Память EGA, используемая некоторыми видеорежимами |
B000:0000 | Память монохромного дисплейного адаптера |
B800:0000 | Память видеоадаптера CGA |
C800:0000 | Внешнее ПЗУ |
F600:0000 | ПЗУ интерпретатора BASIC |
FE00:0000 | ПЗУ BIOS |
Первый килобайт памяти занимает таблица векторов прерываний. Она содержит 256 4-х байтных элемента - дальние адреса обработчиков прерываний. Подробно формат и использование этой таблицы будет обсуждаться в главе, посвященной прерываниям.
Адреса 0000:0400 - 0000:04FF (или от 0040:0000 до 0050:0000) занимает область данных BIOS. Это внутренние переменные BIOS. К ним можно обращаться для получения различной информации, но необходимо только помнить, что формат этой области может быть различным для различных версий BIOS. Мы будем при необходимости ссылаться на отдельные поля этой области.
Начиная с адреса 0000:0500 (или с адреса 0050:0000, что одно и то же) следует область данных DOS. Здесь DOS хранит свои внутренние таблицы и переменные. Формат этой области (и ее размер) зависит от версии операционной системы.
Далее следует большая область памяти, используемая DOS. Здесь располагаются:
система ввода-вывода DOS (содержимое файла IO.SYS);
обработчики прерываний DOS, в частности, обработчик прерывания INT21H (эти обработчики входят в состав файла MSDOS.SYS);
внутренние буфера DOS и области данных;
загружаемые драйверы (описанные в файле CONFIG.SYS).
После драйверов располагается резидентная порция COMMAND.COM (командный интерпретатор). Она, в частности, обрабатывает прерывания INT 22H, INT 23H и INT 24H.
Следующая область памяти занимается программами, остающимися резидентными после запуска (TSR-программы).
После резидентных программ находится выполняющаяся в настоящий момент программа (COM или EXE). Она может занимать всю оставшуюся память до адреса A000:0000 или только часть этой памяти (для EXE-программ). Для EXE-программ можно при редактировании указать требуемый объем памяти.
Нижнюю часть 640-килобайтного адресного пространства (до адреса A000:0000) занимает транзитная часть COMMAND.COM. Она может перекрываться выполняющейся программой. Если программа перекроет транзитную часть COMMAND.COM, то после завершения выполнения программы эта часть командного интерпретатора будет загружена заново.
Область адресов от A000:0000 до C800:0000 используется дисплейными адаптерами. Каждый адаптер использует эту часть памяти по-своему.
Далее и до конца мегабайтной границы идет область ПЗУ. Там расположено ПЗУ BIOS, ПЗУ интерпретатора BASIC, расширение BIOS (например EGA BIOS - для дисплейных адаптеров EGA).
Диапазон адресов свыше мегабайта используется для машин класса не ниже AT. Это так называемая расширенная память (Extended Memory). Она обслуживается BIOS и используется операционной системой MS-DOS для организации "электронного" диска, кэш-памяти для дисков. Некоторые прикладные программы (например, отладчик Microsoft CodeView) хранят в этой области свои данные. Полностью управлять расширенной памятью способны операционные системы типа OS/2 и UNIX в защищенном режиме работы (процессоры Intel 80286, 80386, 80486), а также оболочка Microsoft Windows версий 3.0 и 3.1.
С расширенной памятью не следует путать дополнительную память (Expanded Memory). Эта память с помощью специальной аппаратуры и драйверов отображается в область адресного пространства, лежащую до границы 1 мегабайт. MS-DOS может использовать эту память аналогично расширенной памяти.
Зона памяти, начиная с области программ DOS и до видеопамяти дисплейных адаптеров, разбита на блоки. Перед каждым блоком находится блок управления памятью - Memory Control Block (MCB).
Сегментный адрес первого блока MCB находится в векторной таблице связи, в поле mcb_seg (смещение блока равно 0). Внутри блока MCB содержится длина описываемого данным MCB блока памяти. Следующий MCB начинается сразу за предыдущим. Таким образом, все блоки управления памятью связаны в список.
Блоки MCB бывают двух типов - M и Z. M-блоки ('middle') - это промежуточные блоки. Блок типа Z является последним блоком в списке и может быть только один.
Приведем формат блока MCB (описание этого блока отсутствует в документации по MS-DOS версии 4.01, поэтому его формат может измениться в последующих версиях операционной системы):
(0) 1 | type | тип блока MCB (M или Z) |
(+1) 2 | owner | параграф владельца блока (если 0, то блок описывает сам себя) |
(+3) 2 | size | число параграфов в этом блоке (один параграф имеет размер 16 байт) |
(+5) 11 | reserve | зарезервировано |
#pragma pack(1)
typedef struct _MCB_ { unsigned char type; unsigned owner; unsigned size; char reserve[11]; } MCB;
#pragma pack()
Существует несколько удобных программ для просмотра списка блоков MCB. Вот какую информацию выдает программа MI.COM из пакета PCSHELL при запуске с параметром /A:
Memory Info v5.8 Copyright 1989 Central Point Software, Inc. All rights reserved.
Conventional memory. Total: 640k Largest executable program: 485k
Type Paragraphs Bytes Owner ---- ---------- ----- ------------- Sys 0BA4-18C5h 53792 0008h < DOS > Free 18C7-18CFh 144 0000h < DOS > Env 18D1-18D2h 32 18D4h JYRKEYB Prog 18D4-1904h 784 18D4h JYRKEYB C:\DOS\JYRKEYB.COM C Prog 1906-1A69h 5696 1906h COMMAND Env 1A6B-1A7Dh 304 1906h COMMAND 1A7F-1A82h 64 1906h COMMAND Free 1A84-1A93h 256 0000h < DOS > Prog 1A95-1DD8h 13376 1A95h MOUSE Env 1DDA-1DEDh 320 1ED8h NS Prog 1DEF-1ED6h 3712 1DEFh SHELLB DOSSHELL Prog 1ED8-21EBh 12608 1ED8h NS f:\norton\NS.EXE Env 21ED-2200h 320 2202h NC Prog 2202-2527h 12896 2202h NC f:\norton\NC.EXESocha 2529-253Ch 320 253Eh COMMAND Prog 253E-26A1h 5696 253Eh COMMAND /a Env 26A3-26B5h 304 253Eh COMMAND Env 26B7-26CAh 320 26CCh MI Prog 26CC-9FFFh 485k 26CCh MI c:\dos\MI.COM /a
Программа сообщает размер оперативной памяти (640 К), максимальный размер выполняемой программы (485 К), затем выдает список блоков памяти с указанием типа, занимаемых параграфов памяти, размера в байтах и владельца блока памяти. Программа MI различает четыре типа блоков памяти:
системный (Sys), его владельцем является MS-DOS;
свободный (Free), обычно тоже принадлежит MS-DOS;
программный (Prog) - его занимает запущенная программа;
среда (Env) - содержит переменные среды MS-DOS.
Откуда MI берет информацию о типе блока памяти и имени программы? Системный блок распознается по занимаемым адресам, программный - по наличию правильного префикса программного сегмента (будет описан ниже). Блок переменных среды находится перед программным блоком и содержит кроме собственно переменных среды еще и полный путь файла запущенной программы. Если блок не системный, программный, или не является блоком среды и если в поле владельца этого блока записан ноль, программа отмечает такой блок памяти как свободный.
Видно, что для каждой запущенной программы создается два блока памяти - блок среды и программный блок. Среда формируется при загрузке операционной системы с помощью команд SET и содержит строки вида:
LIB=D:\C600\LIB
Строки хранятся в формате ASCIIZ, т.е. закрыты двоичным нулем. Вся таблица переменных среды также закрыта двоичным нулем. После переменных среды в блоке памяти, отведенном для среды, содержится путь программного файла в формате ASCIIZ.
Блок памяти типа Prog (программный) независимо от формата загрузочного модуля (COM или EXE) начинается с префикса программного сегмента PSP, за которым следует сама программа.
Таким образом, при загрузке для программы выделяются блоки памяти, располагающиеся в следующей последовательности:
MCB для блока памяти переменных среды;
блок памяти переменных среды;
MCB программного блока памяти;
префикс программного сегмента PSP;
программный модуль.
Приведем фрагмент дампа оперативной памяти, полученные при отладке программы MI.COM с помощью отладчика Microsoft CodeView. Дамп памяти начинается с адреса 6D47:0000.
Первая строка дампа (смещение 000-00F) - это MCB блока памяти переменных среды. Это не последний блок памяти, поэтому MCB имеет тип M. В поле владельца блока памяти находятся нули, следовательно, MCB принадлежит сам себе. Поле длины содержит значение 0014 - это количество параграфов в блоке памяти переменных среды.
Со смещением 010 начинается блок переменных среды. Из дампа видно, что строки определения переменных среды закрыты двоичным нулем. Таблица переменных среды также закрыта нулем (смещение 13B). Со смещением 13C записано слово 0001 (количество слов в последующей строке), после которого расположен полный путь для файла запущенной программы.
000 4D00 0014 0000 0000 0000 0000 0000 0000 M............... 010 434F 4D53 5045 433D 433A 5C43 4F4D 4D41 COMSPEC=C:\COMMA 020 4E44 2E43 4F4D 0054 4D50 3D67 3A5C 7465 ND.COM.TMP=g:\te 030 6D70 0050 4154 483D 673A 5C3B 643A 5C71 mp.PATH=g:\;d:\q 040 6332 5C62 696E 3B65 3A5C 6336 3030 5C62 c2\bin;e:\c600\b 050 696E 623B 653A 5C63 3630 305C 6269 6E3B inb;e:\c600\bin; 060 633A 5C64 6F73 3B63 3A5C 6172 633B 663A c:\dos;c:\arc;f: 070 5C6E 6F72 746F 6E3B 653A 5C77 6F72 643B \norton;e:\word; 080 004C 4942 3D65 3A5C 7163 325C 4C49 4200 .LIB=e:\qc2\LIB. 090 494E 434C 5544 453D 673A 5C69 6E63 6C75 INCLUDE=g:\inclu 0A0 6465 3B65 3A5C 7163 325C 494E 434C 5544 de;e:\qc2\INCLUD 0B0 453B 653A 5C63 746F 6F6C 735C 696E 636C E;e:\ctools\incl 0C0 7564 653B 0048 454C 5046 494C 4553 3D65 ude;.HELPFILES=e 0D0 3A5C 4336 3030 5C48 454C 505C 2A2E 484C :\C600\HELP\*.HL 0E0 503B 0049 4E49 543D 653A 5C43 3630 305C P;.INIT=e:\C600\ 0F0 494E 4954 0044 4D41 4B45 3D67 3A5C 646D INIT.DMAKE=g:\dm 100 616B 653B 0056 4547 413D 653A 5C76 6567 ake;.VEGA=e:\veg 110 613B 004E 4152 4348 454C 503D 663A 5C61 a;.NARCHELP=f:\a 120 7263 5C6E 6172 633B 0048 454C 5050 4154 rc\narc;.HELPPAT 130 483D 643A 5C68 656C 703B 0000 0100 433A H=d:\help;....C: 140 5C44 4F53 5C6D 692E 636F 6D00 6D00 7016 \DOS\mi.com.m.p.
Далее начинается MCB прогаммного блока памяти (смещение 150). Это последний блок памяти, поэтому MCB имеет тип Z. Непосредственно за MCB располагается префикс программного сегмента PSP (смещение 160). Размер PSP - 256 байт, его формат будет описан ниже.
И, наконец, со смещением 260 расположена сама программа MI.COM.
150 5A00 00A3 3216 0000 6D69 0000 0000 0000 Z...2...mi...... 160 CD20 00A0 009A F0FE 1DF0 3D09 B22E 340A . ........=...4. 170 B22E 850E B22E CD26 FFFF FFFF FFFF FFFF .......&........ 180 FFFF FFFF FFFF FFFF FFFF FFFF 486D 92B3 ............Hm.. 190 FD4B 1400 1800 5D6D FFFF FFFF 0000 0000 .K....]m........ 1A0 0000 0000 0000 0000 0000 0000 0000 0000 ................ 1B0 CD21 CB00 0000 0000 0000 0000 0020 2020 .!........... 1C0 2020 2020 2020 2020 0000 0000 0020 2020 ..... 1D0 2020 2020 2020 2020 0000 0000 0000 0000 ........ 1E0 0320 2F61 0038 0F00 2C09 0001 0000 4005 . /a.8..,.....@. 1F0 7808 0000 9000 0100 04B3 0D07 ED2F 14B3 x............/.. 200 7501 4A36 0000 0000 0000 4003 A308 3F00 u.J6......@...?. 210 FD4B FD4B 0100 2ABA 0000 0000 026F 5718 .K.K..*......oW. 220 3800 0500 3800 CF08 DD26 2D09 FD4B FD4B 8...8....&-..K.K 230 E408 0000 4200 A308 3F00 FD4B FD4B E408 ....B...?..K.K.. 240 1304 0000 5718 C000 0500 C000 CE02 CE03 ....W........... 250 5718 8400 0500 8400 CE02 CE03 973A 92B3 W............:.. 260 E9BB 000D 0A4D 656D 6F72 7920 496E 666F .....Memory Info 270 2076 352E 380D 0A43 6F70 7972 6967 6874 v5.8..Copyright 280 2031 3938 3920 4365 6E74 7261 6C20 506F 1989 Central Po 290 696E 7420 536F 6674 7761 7265 2C20 496E int Software, In 2A0 632E 2020 416C 6C20 7269 6768 7473 2072 c. All rights r 2B0 6573 6572 7665 642E 0D0A 0A00 4279 2047 eserved.....By G 2C0 5744 2030 312F 3036 2F38 391A 2D2D 2D2D WD 01/06/89.---- 2D0 5041 5443 4820 4152 4541 2D2D 2D2D 2D2D PATCH AREA------ 2E0 2D2D FF90 5601 4102 0000 0290 5D6D 5C6D --..V.A.....]m\m
Префикс программного сегмента всегда создается при загрузке программы COM или EXE в память и имеет одинаковый формат для COM и EXE файлов:
(0) 2 | int20h | двоичный код команды int 20h (программы могут использовать эту команду для завершения своей работы) |
(+2) 2 | mem_top | нижняя граница доступной памяти в системе в параграфах |
(+4) 1 | reserv1 | зарезервировано |
(+5) 5 | call_dsp | команда вызова FAR 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 | диспетчер функций DOS |
(+83) 9 | reserv3 | зарезервировано |
(+92) 16 | fcb1 | форматируется как стандартный FCB, если первый аргумент командной строки содержит правильное имя файла |
(+108) 20 | fcb2 | заполняется для второго аргумента командной строки аналогично fcb1 |
(+128) 1 | p_size | число значащих символов в неформатированной области параметров, либо буфер обмена с диском DTA, назначенный по умолчанию |
(+129) 127 | parm | неформатированная область параметров, заполняется при запуске программы из командной строки |
Для обращения к полям PSP файл sysp.h содержит следующее определение:
#pragma pack(1)
typedef struct _PSP_ { 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; #pragma pack()
Приведем программу, определяющую адрес первого блока MCB:
/** *.Name get_fmcb * *.Title Получить адрес первого MCB * *.Descr Функция возвращает адрес первого блока MCB * *.Params MCB far *get_fmcb(CVT far *cvt) * * cvt - адрес векторной таблицы связи * *.Return Указатель на первый блок MCB **/
#include "sysp.h"
MCB far *get_fmcb(CVT far *cvt) { return((MCB far *)FP_MAKE(cvt->mcb_seg,0)); }
В качестве аргумента функции get_fmcb необходимо передать указатель на векторную таблицу связи, полученный с помощью описанной ранее функции get_mcvt.
Для получения адреса следующего MCB можно использовать функцию get_nmcb:
/** *.Name get_nmcb * *.Title Получить адрес следующего MCB * *.Descr Функция возвращает адрес следующего блока MCB * или 0, если это последний блок. В качестве * параметра используется указатель на предыдущий * блок MCB * *.Params MCB far *get_fmcb(MCB far *mcb) * * mcb - адрес предыдущего MCB * *.Return Указатель на следующий блок MCB или 0, если * это последний MCB **/
#include <dos.h> #include "sysp.h"
MCB far *get_nmcb(MCB far *mcb) { unsigned seg, off;
if(mcb->type == 'M') { seg = FP_SEG(mcb) + mcb->size + 1; off = FP_OFF(mcb); return((MCB far *) FP_MAKE(seg,off)); } else return ((MCB far *)0); }
В завершение описания блоков управления памятью приведем программу, выводящую некоторую информацию об активных MCB. Эта программа определяет адрес векторной таблицы связи, получает из этой таблицы адрес первого MCB и сканирует весь список MCB.
#include <dos.h> #include <stdio.h> #include "sysp.h"
void main(void);
void main(void) { CVT far *cvt; MCB far *mcb; printf("\nБлоки управления памятью (MCB)" "\nCopyright (C)Frolov A., 1990\n" "\nАдрес MCB Тип Владелец Размер" "\n--------- --- -------- ------" "\n"); cvt=get_mcvt(); mcb=get_fmcb(cvt); for(;;) { if(mcb == (MCB far *)0) break; printf("%Fp %c %04X %04X\n", mcb, mcb->type, mcb->owner, mcb->size); mcb=get_nmcb(mcb); } exit(0); }
Что такое резидентная программа?
Обычно после завершения очередной выполняющейся программы DOS освобождает место в памяти, которое занимала программа, чтобы загрузить на это место новую. Однако есть способ оставить программу в памяти и после ее завершения. Следующая запускаемая программа при этом разместится в памяти после оставленной.
Остающиеся в памяти после завершения своей работы программы называются резидентными или TSR (Terminate and Stay Resident).
Для чего может понадобиться составление TSR-программ?
Существует несколько приложений, для которых подходят TSR-программы. Чаще всего TSR-программы используются для русификации импортных персональных компьютеров. Написано множество программ для загрузки русских шрифтов в память видеоадаптеров, для печати русских букв на принтере в графическом режиме, для русификации клавиатуры и т.п. Для всех этих программ характерно то, что они запускаются один раз при загрузке компьютера - их имена обычно включают в AUTOEXEC.BAT. Эти программы могут переключать на себя обработку прерываний, связанных с выводом на печать или с обращением к клавиатуре и/или выполнять разовые инициализирующие действия, такие как загрузка русских шрифтов в память видеоадаптера.
Другим примером использования TSR-программ могут служить программы резидентных калькуляторов, справочных баз данных типа Norton Guide или целых интегрированных систем наподобие Sidekick фирмы Borland. Такие программы тоже обычно запускаются через AUTOEXEC.BAT или при необходимости. Они перехватывают клавиатурные прерывания и отслеживают нажатие клавиш. Как только обнаруживается нажатие определенной заранее клавиши, поверх имеющегося на экране изображения выводится окно диалога резидентной программы.
Иногда TSR-программы используют для обслуживания нестандартной аппаратуры, особенно когда с этой аппаратурой должны работать по очереди несколько разных программ. В этом случае TSR-программа встраивает обработчик какого-либо прерывания, не занятого системой, и через этот обработчик все прикладные программы обращаются к нестандартной аппаратуре.
Аналогично работает резидентная часть системы управления базами данных ORACLE. Через прерывание, устанавливаемое при запуске ORACLE, прикладная программа, составленная на любом языке программирования, имеющем средство вызова прерывания, может пользоваться базой данных ORACLE.
Для обслуживания нестандартной аппаратуры больше подходят загружаемые драйверы устройств, о которых мы еще будем подробно говорить. Драйверы предоставляют намного более гибкие и богатые средства общения с устройствами, чем TSR-программы. Однако, если все, что вы собираетесь сделать, - это обработать несколько прерываний или обслужить какое-нибудь несложное устройство, то TSR-программы - это то, что вам нужно.
TSR-программы не могут свободно пользоваться прерываниями DOS из-за того, что программы DOS (и BIOS) не обладают свойством реентерабельности. Об этом мы будем говорить при обсуждении особенностей резидентных программ.
Драйвер символьного устройства
; Демонстрационный драйвер символьного устройства ; ; Copyright (C)Frolov A., 1990
.MODEL tiny .CODE ; Драйвер состоит из одного ; сегмента кода
org 0 ; Эта строка может отсутствовать
include sysp.inc
devdrv PROC far
; Заголовок драйвера
dd 0ffffffffh ;адрес следующего драйвера dw 0C800h ;байт атрибутов dw dev_strategy ;адрес процедуры стратегии dw dev_interrupt ;адрес процедуры прерывания db 'DEVDRIVR' ;имя устройства (дополненное ; пробелами)
;=================================================== ; Программа стратегии
dev_strategy: mov cs:req_seg,es mov cs:req_off,bx ret
; Здесь запоминается адрес заголовка запроса
req_seg dw ? req_off dw ?
;=================================================== ;Обработчик прерывания
dev_interrupt: push es ;сохраняем регистры push ds push ax push bx push cx push dx push si push di push bp
; Устанавливаем ES:BX на заголовок запроса
mov ax,cs:req_seg mov es,ax mov bx,cs:req_off
; Получаем код команды из заголовка запроса и умножаем ; его на два, чтобы использовать в качестве индекса ; таблицы адресов обработчиков команд
mov al,es:[bx]+2 shl al,1
sub ah,ah ; Обнуляем AH lea di,functions ; DI указывает на смещение ; таблицы add di,ax ; Добавляем смещение в таблице jmp word ptr [di] ; Переходим на адрес из таблицы
functions LABEL WORD ; Таблица функций
dw initialize dw check_media dw make_bpb dw ioctl_in dw input_data dw nondestruct_in dw input_status dw clear_input dw output_data dw output_verify dw output_status dw clear_output dw ioctl_out dw Device_open dw Device_close dw Removable_media dw reserved dw reserved dw reserved dw generic_ioctl
; Выход из драйвера, если функция не поддерживается
check_media: make_bpb: clear_input: output_verify: clear_output: Removable_media: reserved: generic_ioctl:
; Выводим сообщение о вызове ; неподдерживаемой драйвером команды
mov ax,cs mov ds,ax mov si,offset errmsg call dpc
; Ожидаем нажатия на любую клавишу
mov ax,0 int 16h
; Устанавливаем признак ошибки
or es:word ptr [bx]+3,8103h jmp quit
;==================================================nondestruct_in:
; Выводим сообщение о начале неразрушающего ввода
mov ax,cs mov ds,ax mov si,offset inpmsg_nd call dpc
; Вводим символ с клавиатуры и помещаем ; его в область запроса
mov ax, 0 int 16h mov BYTE PTR es:[bx]+0dh,al
jmp quit
;=================================================== input_status:
; Выводим сообщение о вызове команды ; проверки состояния ввода
mov ax,cs mov ds,ax mov si,offset statmsg_i call dpc
; Устанавливаем признак "Занято", т.к. ; последующая команда чтения приведет к ожиданию ; нажатия на клавишу (буферизация не используется)
or es:word ptr [bx]+3,0200h jmp quit
;=================================================== output_status:
; Выводим сообщение о вызове команды ; проверки состояния вывода
mov ax,cs mov ds,ax mov si,offset statmsg_o call dpc
; Бит занятости не устанавливаем, т.к. ; считаем, что консоль доступна для вывода
or es:word ptr [bx]+3,0000h jmp quit
;=================================================== ; Обработчик команды вывода данных
output_data:
; Записываем в регистр CL количество ; выводимых символов
mov cl,es:[bx]+18 push cx
; Выводим сообщение о начале вывода
mov ax,cs mov ds,ax mov si,offset outmsg call dpc
pop cx
; Загружаем в DS:SI адрес буфера данных
mov ax,es:[bx]+16 mov ds,ax mov si,es:[bx]+14
; Выводим на экран символы из буфера
out_loop: mov al,ds:byte ptr [si] @@out_ch al inc si loop out_loop
jmp quit
;=================================================== ; Обработчик команды ввода данных
input_data:
; Записываем в регистр CL количество ; вводимых символов
mov cl,es:[bx]+18 push cx
; Выводим сообщение о начале ввода
mov ax,cs mov ds,ax mov si,offset inpmsg call dpc
; Загружаем в DS:SI адрес буфера данных
pop cx mov ax,es:[bx]+16 mov ds,ax mov di,es:[bx]+14
; Вводим символы с клавиатуры и записываем в буфер
inp_loop: mov ax,0 int 16h mov ds:byte ptr [di],al @@out_ch al inc di loop inp_loop
jmp quit
;=================================================== ; Обработчик команды вывода данных IOCTL
ioctl_out:
; Записываем в регистр CL количество ; выводимых символов
mov cl,es:[bx]+18
; Загружаем в DS:SI адрес буфера данных
mov ax,es:[bx]+16 mov ds,ax mov si,es:[bx]+14
; Выводим на экран символы из буфера
ioctl_out_loop: mov al,ds:byte ptr [si] @@out_ch al inc si loop ioctl_out_loop
jmp quit
;=================================================== ; Обработчик команды ввода данных IOCTL
ioctl_in:
; Записываем в регистр CL количество ; вводимых символов
mov cl,es:[bx]+18
; Загружаем в DS:SI адрес буфера данных
mov ax,es:[bx]+16 mov ds,ax mov di,es:[bx]+14
; Вводим символы с клавиатуры и записываем в буфер
ioctl_inp_loop: mov ax,0 int 16h mov ds:byte ptr [di],al @@out_ch al inc di loop ioctl_inp_loop
jmp quit
;=================================================== Device_open:
; Выводим сообщение об открытии устройства
mov ax,cs mov ds,ax mov si,offset openmsg call dpc
jmp quit
;=================================================== Device_close:
; Выводим сообщение о закрытии устройства
mov ax,cs mov ds,ax mov si,offset closemsg call dpc
jmp quit
;=================================================== quit: or es:word ptr [bx]+3,100h pop bp pop di pop si pop dx pop cx pop bx pop ax pop ds pop es ret
;=================================================== parm_off dw ? ; Смещение строки параметров parm_seg dw ? ; Сегмент строки параметров
pc_type dw ? ; Область памяти для сохранения int_num dw ? ; значений параметров out_port dw ? inp_port dw ? ctrl_inp_port dw ? ctrl_out_port dw ?
;===================================================;** ; ;.Name INTERRUPT ;.Title Обработчик прерывания ;.Synopsis ;- ; int <NN> ; ; Вход: NN - Номер прерывания, заданный в файле ;CONFIG.SYS ; AH - Номер выполняемой функции: ; 0 - операция записи; ; 1 - операция чтения ; BH - Адрес (0...7Fh) ; BL - Данные для записи (0...FFh) ; Выход: BL - Прочитанные данные ;- ;.Description ;- ; Прерывание вызывается командой INT <NN> с ;параметрами: ; ; Вход: NN - Номер прерывания, заданный в файле ;CONFIG.SYS ; AH - Номер выполняемой функции: ; 0 - операция записи; ; 1 - операция чтения ; BH - Адрес (0...7Fh) ; BL - Данные для записи (0...FFh) ; Выход: BL - Прочитанные данные ;- ;.Returns ; BL - Прочитанные данные ;.Version 1.00 (c)Copyright Frolov A., 1990 ;- ;**
interrupt: push ax push cx push dx push bp push si push di push ds push es
cmp ah,0 ; команда записи jz int_write cmp ah,1 ; команда чтения jz int_read
; Обработка неизвестной команды
@@out_ch 13,10,'?','?','?',13,10
; Устанавливаем признак ошибки
mov ax,0ffffh jmp int_exit
int_write:
; Выводим сообщение о приходе прерывания, ; предназначенного для записи
@@out_ch 13,10,'W','R','I','T','E',13,10
mov ax,0 jmp int_exit
int_read:
; Выводим сообщение о приходе прерывания, ; предназначенного для чтения
@@out_ch 13,10,'R','E','A','D',13,10
; Имитация чтения, всегда возвращается значение 55h
mov bl,55h mov ax,0 jmp int_exit
int_exit: pop es pop ds pop di pop si pop bp pop dx pop cx pop ax
iret
;=================================================== ; Процедура выводит на экран строку ; символов в формате ASCIIZ
dpc proc near push si dpc_loop: cmp ds:byte ptr [si],0 jz end_dpc mov al,ds:byte ptr [si] @@out_ch al inc si jmp dpc_loop
end_dpc: pop si ret dpc endp
;=================================================== hello db 13,10,'++' db 13,10,'¦ *DEVDRV* (C)Frolov A., 1990 ¦' db 13,10,'++' db 13,10,0
outmsg DB 13,10,'___ Вывод на устройство DEVDRIVR ___',0 inpmsg DB 13,10,'___ Ввод с устройства DEVDRIVR ___',0
openmsg db 13,10,'___ Открываем DEVDRIVR ___',0 closemsg db 13,10,'___ Закрываем DEVDRIVR ___',0
inpmsg_nd DB 13,10 DB '___ ND-ввод с устройства DEVDRIVR ___',0 statmsg_i DB 13,10 DB '___ Чтение состояния ввода DEVDRIVR ___',0 statmsg_o DB 13,10 DB '___ Чтение состояния вывода DEVDRIVR ___',0
errmsg DB 13,10 DB '___ Команда не поддерживается DEVDRIVR ___',0
;=================================================== E_O_P: ;Метка конца программы
initialize:
lea ax,E_O_P ;смещение конца программы в AX mov es:word ptr [bx]+14,ax ;помещаем его в заголовок mov es:word ptr [bx]+16,cs ;
mov ax,es:word ptr [bx]+18 ;смещение строки ; параметров mov cs:parm_off,ax mov ax,es:word ptr [bx]+20 ;сегмент строки ; параметров mov cs:parm_seg,ax
; Стираем экран
mov dh,18h mov dl,80h
xor cx,cx mov bh,7 xor al,al mov ah,6 int 10h
; Устанавливаем курсор в левый верхний угол экрана
mov bh,0 xor dx,dx mov ah,2 int 10h
; Выводим сообщение
mov ax,cs mov ds,ax mov si,offset hello call dpc
; Раскодируем строку параметров и проверяем ; корректность заданных параметров
push cs pop ds
mov bx, cs:parm_off ; ES:BX - адрес строки ; параметров mov ax, cs:parm_seg mov es, ax
; Адрес начала области параметров
mov bp, OFFSET cs:pc_type
; Анализируем параметры
call parm jc parm_errors
; Устанавливаем вектор прерывания с номером, ; заданным в строке параметров
push cs pop ds mov dx,OFFSET cs:interrupt mov ax,cs:int_num mov ah,25h int 21h
jmp quit
parm_errors:
; Если параметры заданы с ошибкой, ; установку драйвера не производим.
mov ax,cs:req_seg ;ES:BX указывают на заголовок ; запроса mov es,ax mov bx,cs:req_off
lea ax,devdrv ;смещение начала драйвера mov es:word ptr [bx]+14,ax ;помещаем его в заголовок mov es:word ptr [bx]+16,cs ;
jmp quit
;=================================================== parm proc near
xor si, si ; индекс в строке параметров
next_chr: mov al, BYTE PTR es:[bx][si] cmp al, 0ah ; проверки на конец je parm_br ; строки параметров cmp al, 0dh je parm_br cmp al, 0h jz parm_br
; Копируем очередной байт строки параметров в буфер
mov BYTE PTR cs:parm_buffer[si], al inc si jmp next_chr
; Закрываем скопированную строку параметров нулем
parm_br: mov BYTE PTR cs:parm_buffer[si], 0
; Подготавливаем регистры для вызова программы ; анализа параметров и проверяем правильность ; заданных параметров
mov dx, OFFSET sep mov cl, 6 mov bx, OFFSET cs:parm_buffer
call get_parm jc err_msg
mov ax,cs:pc_type cmp ax,0 jz model_is_valid cmp ax,1 jz model_is_valid
jmp err_msg
model_is_valid:
; Если параметры заданы правильно, ; выводим их значения на экран
mov si, OFFSET msg1 call dpc mov ax,cs:pc_type call print_word
mov si, OFFSET msg2 call dpc mov ax,cs:int_num call print_word
mov si, OFFSET msg3 call dpc mov ax,cs:out_port call print_word
mov si, OFFSET msg4 call dpc mov ax,cs:inp_port call print_word
mov si, OFFSET msg41 call dpc mov ax,cs:ctrl_inp_port call print_word
mov si, OFFSET msg42 call dpc mov ax,cs:ctrl_out_port call print_word
@@out_ch 13,10,13,10 clc jmp end_of_parm err_msg:
; Если были ошибки в параметрах, выводим ; сообщение об ошибке, ; саму ошибочную строку параметров ; и ожидаем нажатия на любую клавишу. ; На выходе устанавливаем флаг CARRY
mov si, OFFSET msg5 call dpc
mov ds,cs:parm_seg mov si,cs:parm_off call dpline
mov ax,cs mov ds,ax mov si, OFFSET msg6 call dpc
mov ax,0 int 16h stc end_of_parm: ret
parm_buffer db 100 DUP (?)
sep db " ",0 msg1 db 13,10,"Personal Computer Type ........ ",0 msg2 db 13,10,"Used Interrupt Number ........ ",0 msg3 db 13,10,"Device Input Port ........ ",0 msg4 db 13,10,"Device Output Port ........ ",0 msg41 db 13,10,"Device Inp Control Port ........ ",0 msg42 db 13,10,"Device Out Control Port ........ ",0
msg5 db 13,10,"Driver Parameters Error!",13,10,0
msg6 db 13,10,13,10," Press any key...",13,10,0
parm endp
;** ; ;.Name get_parm ;.Title Разбор строки параметров. ;.Synopsis ; ; ds:bx - исходная строка, закрытая нулем ; ds:dx - строка из сепараторов, закрытая ; нулем ; ds:bp - буфер параметров ; cx - число параметров ; ;.Description ; Исходная строка ds:bx, разделенная ; сепараторами ds:dx, состоящая из cx ; параметров и начинающаяся с полного пути ; программы, разбирается на слова, параметры - ; шестнадцатеричные цифры. Двоичные слова, ; соответствующие параметрам, последовательно ; заносятся в буфер параметров ds:bp. ; ;.Returns В случае ошибки устанавливается флаг ; переноса. ; ;.Version 1.00 (c)Copyright Frolov G., 1990 ;**
get_parm proc near
push bx push cx push ax push si
xor ch, ch xor ax, ax mov si, ax
call strtoc jc parm_end parm_loop: mov ax, 22h call strtoc jc parm_end call hex_to_bin jc parm_end mov ds:[bp][si], ax inc si inc si loop parm_loop parm_end: pop si pop ax pop cx pop bx
ret
get_parm endp
;** ; ;.Name strtoc ;.Title Выделяет очередное слово из строки. ;.Synopsis ; ; 1) при первом обращении к процедуре: ; Вход: ; ax = 0 ; ds:bx - исходная строка, закрытая нулем ; ds:dx - строка из сепараторов, закрытая ; нулем ; Выход: ; ds:bx - подстрока до первого разделителя, ; закрытая нулем ; 2) при последующих обращениях ; Вход: ; ax != 0 ; ds:dx - строка из сепараторов, закрытая ; нулем ; Выход: ; ds:bx - подстрока до следующего ; разделителя, закрытая нулем ; ;.Description При первом вызове выделяет из строки первое ; слово до разделителя. При повторном вызове ; возвращает указатель на следующее слово в ; исходной строке. Если все слова из строки ; уже выделены, устанавливается флаг ; переноса. ; ;.Version 1.00 (c)Copyright Frolov G., 1990 ;**
strtoc proc near
push bp push di
mov space, 0
cmp ax, 0 jz first
mov bx, cs:ds1_off mov ax, cs:ds1_seg mov ds, ax
first: cmp BYTE PTR ds:[bx], 0 jz error
mov bp, bx str_begin: mov di, dx
compe: mov ah, BYTE PTR ds:[bp] cmp ah, 0 jz lab cmp BYTE PTR ds:[di], ah jne next mov BYTE PTR ds:[bp], 0 inc bp inc BYTE PTR cs:space jmp str_begin
lab: mov WORD PTR cs:ds1_off, bp mov ax, ds mov WORD PTR cs:ds1_seg, ax jmp end_proc
next: inc di cmp BYTE PTR ds:[di], 0 jnz compe cmp BYTE PTR cs:space, 0 jnz lab inc bp jmp str_begin
error: stc end_proc: pop di pop bp
ret
ds1_off dw ? ds1_seg dw ? space db ?
strtoc endp
;** ; ;.Name hex_to_bin ;.Title Преобразует hex строку в двоичное число. ;.Synopsis ; ; Вход: ; ds:bx - исходная строка, закрытая нулем ; Выход: ; ax - соответствующее строке число ; ;.Description Преобразует строку из ascii символов в hex ; форме в ее 16-битовый двоичный эквивалент. ; В случае переполнения или если строка ; содержит символы, не равные 0..9, A..F, a..f ; устанавливается флаг переноса. ; ;.Version 1.00 (c)Copyright Frolov G., 1990 ;** hex_to_bin PROC NEAR
push bp push si push dx
xor ax, ax mov bp, bx
begin: cmp BYTE PTR ds:[bp], 0h jz end_pro call hex jc h_error
mov si, 10h xor dx, dx mul si cmp dx, 0 jnz h_error
add ax, bx inc bp jmp begin
h_error: stc end_pro: pop dx pop si pop bp
ret hex_to_bin endp
hex proc near xor bh, bh mov bl, BYTE PTR ds:[bp]
cmp bl, '0' jb hex_error cmp bl, '9' ja next_big and bx, 0fh jmp ok
next_big: cmp bl, 'A' jb hex_error cmp bl, 'F' ja next_small sub bl, 37h jmp ok
next_small: cmp bl, 'a' jb hex_error cmp bl, 'f' ja hex_error sub bl, 57h jmp ok
hex_error: stc ok: ret
hex endp
;** ; ;.Name dec_to_bin ;.Title Преобразует dec строку в двоичное число. ;.Synopsis ; ; Вход: ; ds:bx - исходная строка, закрытая нулем ; Выход: ; ax - соответствующее строке число ; ;.Description Преобразует строку из ascii символов в dec ; форме в ее 16-битовый двоичный эквивалент. В ; случае переполнения или если строка содержит ; символы, не равные 0..9, устанавливается флаг ; переноса. ; ;.Version 1.00 (c)Copyright Frolov G., 1990 ;** dec_to_bin proc near
push bp push si push dx
xor ax, ax mov bp, bx
d_begin: cmp BYTE PTR ds:[bp], 0h jz d_end_pro
cmp BYTE PTR ds:[bp], '0' jb d_error cmp BYTE PTR ds:[bp], '9' ja d_error mov si, 10 xor dx, dx mul si cmp dx, 0 jnz d_error mov bl, BYTE PTR ds:[bp] and bx, 0fh add ax, bx inc bp jmp d_begin d_error: stc d_end_pro: pop dx pop si pop bp
ret
dec_to_bin endp
print_word proc near ;-------------------- push ax push bx push dx ; push ax mov cl,8 rol ax,cl call byte_to_hex mov bx,dx @@out_ch bh @@out_ch bl ; pop ax call byte_to_hex mov bx,dx @@out_ch bh @@out_ch bl ; pop dx pop bx pop ax ret print_word endp
byte_to_hex proc near ;-------------------- ; al - input byte ; dx - output hex ;-------------------- push ds push cx push bx ; lea bx,tabl mov dx,cs mov ds,dx ; push ax and al,0fh xlat mov dl,al ; pop ax mov cl,4 shr al,cl xlat mov dh,al ; pop bx pop cx pop ds ret ; tabl db '0123456789ABCDEF' byte_to_hex endp
;======================================================= dpline proc near push si dpline_loop: cmp ds:byte ptr [si],0dh jz end_dpline cmp ds:byte ptr [si],0ah jz end_dpline
mov al,ds:byte ptr [si] @@out_ch al inc si jmp dpline_loop
end_dpline: pop si ret dpline endp
devdrv ENDP
END devdrv
Для работы с этим драйвером и демонстрации его основных возможностей мы подготовили следующую программу:
Драйвер системных часов CLOCK$
Операционная система содержит в своем составе драйвер системных часов. Это драйвер символьного устройства, имя устройства - CLOCK$. Символ "$" используется для того, чтобы это имя не конфликтовало с именем файла CLOCK.
Характерный признак драйвера системных часов - бит 3 слова атрибутов устройства установлен в 1. Именно этот признак используется DOS для обнаружения драйвера часов, имя CLOCK$ может быть изменено в последующих версиях операционной системы.
Для опроса содержимого часов или для установки часов DOS соответственно читает или записывает шесть байтов информации следующего формата:
Смещение | Длина | Назначение |
(0) | 2 | Количество дней, прошедших после 1 января 1980 года |
(+2) | 1 | Счетчик минут |
(+3) | 1 | Счетчик часов |
(+4) | 1 | Счетчик сотых долей секунды |
(+5) | 1 | Счетчик секунд |
Мы приведем пример программы, которая открывает устройство CLOCK$, задает двоичный режим обмена информацией с ним и вводит 6 байтов, имеющих только что описанную структуру. Показания часов выводятся на экран.
#include <io.h> #include <conio.h> #include <stdio.h> #include <fcntl.h> #include <sys\types.h> #include <sys\stat.h> #include <malloc.h> #include <errno.h> #include <dos.h>
int main(void);
union REGS inregs, outregs; struct SREGS segregs;
int main(void) {
int io_handle; unsigned count;
struct { unsigned days; unsigned char min; unsigned char hours; unsigned char sec_per_100; unsigned char sec; } clock_buf;
// Открываем устройство с именем CLOCK$
if( (io_handle = open("CLOCK$", O_RDWR)) == - 1 ) {
// Если открыть устройство не удалось, выводим // код ошибки
printf("Ошибка при открытии устройства %d",errno); return errno; }
// Получаем информацию об устройстве
inregs.h.ah = 0x44; inregs.h.al = 0; inregs.x.bx = io_handle; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим ее код printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); }
// Устанавливаем в 1 бит 5 (переключаем драйвер // в двоичный режим обмена данными
inregs.x.dx = (outregs.x.dx | 0x0020) & 0x00ff;
// Устанавливаем слово информации об устройстве
inregs.h.ah = 0x44; inregs.h.al = 1; inregs.x.bx = io_handle; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим код ошибки
printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); }
// Выводим слово информации об устройстве на экран
printf("\nDevice Information: %04X\n", outregs.x.dx);
// Читаем 6 байт из устройства в буфер buf // Обмен производится в двоичном режиме
if((count = read(io_handle, &clock_buf, 6)) == -1) {
// Если при чтении произошла ошибка, // выводим ее код
printf("Ошибка чтения %d",errno); return errno; }
printf("\nПолучено от драйвера часов CLOCK$:" "\n" "\nПрошло дней после 01.01.80: %d" "\nМинуты: %d" "\nЧасы: %d" "\nСекунды: %d" "\nСотые доли секунды: %d" "\n", clock_buf.days, clock_buf.min, clock_buf.hours, clock_buf.sec, clock_buf.sec_per_100);
// Закрываем устройство close(io_handle); exit(0); }
Драйверы
6.1.
6.2.
6.3.
6.4.
6.5.
6.6.
6.7.
6.8.
6.9.
6.10.
Другие функции.
Существуют функции, позволяющие переопределять прерывания, работать с системными часами и календарем, функции для работы в сети и некоторые другие. Эти функции будут описаны позже в соответствующих разделах книги.
Другие функции прерывания INT13h.
Среди других функций прерывания INT 13h - форматирование дорожки, позиционирование головки на заданную дорожку диска, тестирование и предварительная установка диска, запуск диагностики контроллера и многое другое. Описание этих функций мы отложим до глав, посвященных файловой системе.
Другие подсистемы DOS.
DOS имеет набор драйверов для работы с печатающими устройствами и последовательными портами, которые обеспечивают прием/передачу символов и управление режимами работы устройств.
Кроме того, для печати текста в фоновом режиме в состав дистрибуции DOS входит программа PRINT.COM. Эта практически единственная "мультизадачная" утилита операционной системы является примером стандартной резидентной программы. Она позволяет выполнять параллельно с печатью текста другую работу (например, редактирование другого текста).
Специально следует отметить драйверы электронного диска и кэш-памяти. Драйвер электронного диска называется RAMDRIVE.SYS. Этот драйвер организует в расширенной или в дополнительной памяти компьютера быстрый псевдо-диск. Можно организовать электронный диск и в основной памяти, но основной памяти всегда мало!
Операционная система MS-DOS версий 4.01 и 5.0 имеет в своем составе систему управления дополнительной и расширенной памятью, которая реализуется драйвером HIMEM.SYS. Этот драйвер позволяет программам, составленным специальным образом, использовать расширенную или дополнительную память для хранения данных (но не для выполнения программ).
Если подключен драйвер HIMEM.SYS, то с помощью драйвера SMARTDRV.SYS можно создать кэш-память для жесткого диска. Особенно эффективно применение кэш-памяти для работы с базами данных, когда вам периодически требуется одна и та же информация. В следующий раз, когда информация потребуется, она будет доступна без обращения к диску.
Такое широко распространенное и ставшее стандартным для любого персонального компьютера устройство как мышь не поддерживается операционной системой. Для использования мыши следует подключить драйвер, который обычно поставляется вместе с этим устройством.
Другие устройства ввод/вывода (сканеры, плоттеры, стримеры и т.п.) также не поддерживаются DOS. Для этих устройств фирмы поставляют драйверы, которые надо подключить к операционной системе.
Другие поля векторной таблицы связи
Как мы только что увидели, все драйверы связаны в цепочку, которую нетрудно проследить. Для драйверов часов CLOCK$ и консоли CON векторная таблица связи содержит их актуальные адреса, соответственно в полях clock_dr и con_dr. Если вам надо получить адреса актуальных драйверов, то самый быстрый способ - взять эти адреса из векторной таблицы связи.
Поле lastdriv содержит значение команды LASTDRIVE в файле CONFIG.SYS. Его можно использовать для определения максимального количества дисковых устройств в системе. Количество действительно используемых блочных устройств находится в поле num_bdev.
Если CONFIG.SYS содержит команду FCBS=xx, то в поле fcb_tabl находится адрес таблицы FCB, а в поле fcb_size - размер этой таблицы.
В поле max_btbl находится максимальное число байтов, содержащихся в блоке блочного устройства.
Очень интересные сведния находятся в массиве дисковой информации, на который указывает поле drv_info векторной таблицы связи. Количество элементов этого массива равно количеству дисковых устройств в системе.
Для каждого диска в этом массиве можно найти текущий путь доступа в виде строки ASCIIZ, указатель на блок управления устройствами DOS DDCB, номер начального кластера текущего каталога (кстати, если к какому-либо каталогу ни разу не обращались, в этом поле находится -1).
Приведем формат элемента массива и соответствующую структуру в файле sysp.h:
(0) 64 | path | текущий путь доступа для диска |
(+64) 2 | reserv1 | зарезервировано |
(+66) 2 | reserv2 | зарезервировано |
(+68) 1 | reserv3 | зарезервировано |
(+69) 4 | ddcb | адрес соответствующего DDCB |
(+73) 2 | cdir_clu | первый кластер текущего каталога на диске. 0 соответствует корневому каталогу, -1 - если к диску еще не обращались |
(+75) 2 | reserv4 | зарезервировано |
(+77) 2 | reserv5 | зарезервировано |
(+79) 2 | reserv6 | зарезервировано |
---------------- для DOS 4.х ----------------- | ||
(+81) 7 | reserv7 | зарезервировано |
Приводимое ниже определение типа DINFO соответствует формату MS-DOS 4.х.
#pragma pack(1)
typedef struct _DINFO_ { char path[64]; unsigned reserv1; unsigned reserv2; unsigned char reserv3; DDCB far *ddcb; unsigned cdir_clu; unsigned reserv4; unsigned reserv5; unsigned reserv6; unsigned char reserv7[7]; } DINFO;
#pragma pack()
Как пример использования этой информации приведем программу, которая выводит содержимое массива на экран:
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include "sysp.h"
void main(void);
void main(void) {
CVT far *cvt; DINFO far *dinfo; unsigned i,j,k;
printf("Информация о дисковых устройствах\n" "Copyright Frolov A. (C),1990\n");
cvt=get_mcvt(); // Адрес векторной таблицы связи dinfo=cvt->drv_info; // Адрес таблицы дисковых // устройств i=cvt->num_bdev; // Количество дисковых устройств
for(j=0;j<i;j++) {
printf("Адрес: %Fp,путь: %Fs\n" "Первый кластер каталога: %d\n\n", dinfo, dinfo->path, dinfo->cdir_clu); dinfo = dinfo+1;
} exit(0); }
На этом мы завершаем изучение векторной таблицы связи MS-DOS. Еще раз уместно напомнить, что вся приведенная выше информация отсутствует в документации и может меняться (и действительно меняется!) от версии к версии. Если вы хотите использовать описанные выше средства, вам необходимо динамически определять версию DOS и использовать соответствующие форматы управляющих блоков.
Файловая система.
Файловая система является одной из важнейших подсистем DOS. Она используется как в процессе загрузки операционной системы, так и в процессе ее работы. Сама операционная система записана на системном диске в виде файлов (IO.SYS, MSDOS.SYS, COMMAND.COM, драйверы, внешние команды и т.д.). Все прикладные программы и вообще все программы и данные для них хранятся на дисках в виде файлов, поэтому можно сказать, что файловая система - ключевая подсистема DOS.
Для обращения к файловой системе прикладная программа должна использовать специально предназначенные для этого функции прерывания DOS. Эти функции выполняют все файловые операции - создание, удаление файлов и каталогов, буферизованная и не буферизованная запись или чтение, получение справочной информации о состоянии файловой системы и другие. Файловые операции будут кратко описаны в обзоре прерываний DOS, кроме того, файловой системе полностью посвящена третья книга первого тома.
Файловая система работает с дисками через драйверы. Драйверы, в свою очередь, пользуются сервисом BIOS.
На уровне BIOS выполняются элементарные операции с диском, такие как чтение/запись секторов, форматирование и т.п. Этот низкий уровень доступен и прикладной программе, но обычно она пользуется функциями прерывания DOS, выполняющими все необходимые действия по обслуживанию каталогов и таблицы размещения файлов (File Allocation Table - FAT). Программы защиты от несанкционированного доступа или копирования вынуждены обращаться к средствам более низкого уровня, вызывая прерывания BIOS, или даже работать с контроллером дисковода через порты ввода/вывода.
Используя подсистему DOS, отвечающую за связь с драйверами, прикладные программы могут также выполнять элементарные операции с диском, такие как форматирование.
Форматы программных файлов
Теперь, когда мы знаем структуру памяти на момент завершения загрузки операционной системы, можно посмотреть, а что же происходит дальше, когда оператор запускает какую-нибудь программу.
Оператор может запустить два типа программ (если не считать командных файлов, которые, вообще говоря, не являются программами, состоящими из машинных кодов) - программы, имеющие расширение имени .COM и .EXE. Эти файлы имеют различный формат и загружаются по-разному, однако, когда загрузка завершена, в памяти компьютера эти два типа программ выглядят совершенно одинаково.
COM-файл - это двоичный образ Вашей программы, состоящий из кода и данных. То есть это файл, содержащий программу в "чистом" виде. Такая программа (как и EXE-программа) может загружаться в любое место памяти. DOS выполняет ее привязку к физическим адресам при загрузке с помощью установки сегментных регистров. Существенным ограничением COM-программы является то, что она не может занимать больше одного сегмента (соответственно, файл .COM не может быть по длине больше 64К).
Программа в формате EXE может иметь любой размер. В самом начале файла программы содержится заголовок (у COM-файла заголовка нет). Этот заголовок используется операционной системой в процессе загрузки программы в память для правильной установки сегментных регистров. Заголовок 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 | смещение от начала файла таблицы расположения сегментов программы |
(+26) 2 | overlay | номер оверлея, равен 0 для основного модуля |
Таблица расположения сегментов программы начинается сразу после форматированной области и состоит из четырехбайтовых значений в формате "смещение:сегмент".
Область файла после таблицы расположения сегментов выравнивается на границу параграфа с помощью байта-заполнителя, и дальше начинается сама программа.
В файле sysp.h есть описание заголовка файла и таблицы расположения сегментов, которые вы можете использовать при обработке заголовка EXE-файла:
typedef struct _EXE_HDR_ { 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 _RELOC_TAB_ { unsigned offset; unsigned segment; } RELOC_TAB;
В качестве примера приведем программу, которая считывает форматированную часть заголовка EXE-файла, проверяет наличие в его первых двух байтах признака EXE-формата ('MZ'). Если признак имеется, программа выводит на экран расшифрованное содержимое заголовка и таблицу перемещений, если такая таблица присутствует. В качестве параметра программе надо при запуске передать имя исследуемого EXE-файла.
#include <stdio.h> #include <stdlib.h> #include "sysp.h"
void main(int, char *[]);
void main(int argc, char *argv[]) {
printf("Распечатка заголовка EXE-файла\n" "Copyright (C)Frolov A., 1990\n\n");
if( argc != 2 ) { printf( " Задайте путь EXE-файла в качестве" "параметра\n" ); exit(0); }
if( gethdr( argv[1]) != 0) { printf( "Ошибка в формате файла или нет такого" "файла\n" ); exit(0); } exit(0); }
int gethdr( 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); }
Приведенная выше программа для чтения заголовка EXE-файла пользуется функцией get-exeh:
/** *.Name get_exeh * *.Title Прочитать заголовок EXE-файла * *.Descr Функция читает заголовок EXE-файла в * структуру типа EXE_HDR, заказывает память * для таблицы размещений сегментов и считывает * таблицу в эту область. Адрес заказанной области * помещается по адресу, передаваемому в rtb. * Если таблица размещений отсутствует, память для * нее не заказывается. * *.Params int get_exeh(EXE_HDR *exeh,RELOC_TAB **rtb, * FILE *exe_file) * * exeh - указатель на структуру, которая * должна быть заполнена информацией * из заголовка EXE-файла * * rtb - указатель на указатель на таблицу * размещений сегментов программы * * exe_file - указатель на открытый EXE-файл * (до вызова функции нельзя обращаться * к этому файлу, т.к. считается, что * указатель текущего смещения * установлен на начало файла) * *.Return 0 при успешном считывании заголовка; * -1 в случае неправильного формата заголовка **/
#include <stdlib.h> #include <stdio.h> #include "sysp.h"
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);
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 *)0;
return(0);}
Функции управления устройствами ввода/вывода IOCTL
В этом разделе мы рассмотрим средства управления устройствами ввода/вывода IOCTL (сокращение от Input/Output Control). Использование IOCTL - обычный метод организации связи с устройствами или получения информации об открытых файлах.
Описания будут приводиться в порядке номеров подфункций функции 44h прерывания 21h.
Для версии DOS 2.1 и более поздних версий поддерживаются подфункции с номерами от 0 до 7, для версии 3.0 дополнительно могут использоваться подфункции 8, 0Bh, для 3.1 - 9 и 0Ah, для 3.2 и более поздних версий добавляются подфункции 0Dh, 0Eh, 0Fh. Версия DOS 3.3 и более поздние версии поддерживают также подфункцию 0Ch.
00h
01h
02h/03h
04h/05h
06h/07h
08h
09h
0Ah
0Bh
0Ch
0Dh
0Eh
0Fh
Функции загружаемого драйвера
Как уже было сказано, номер функции, которую должен выполнить драйвер, передается операционной системой через поле cmd заголовка запроса. Рассмотрим отдельные функции.
0 -
1 -
2 -
3 -
4 -
5 -
6 -
7 -
8 -
9 -
10 -
11 -
12 -
13 -
14 -
15 -
16 -
19 -
23 -
24 -
H, 0Dh - Записать/прочитать графическую точку.
Можно высветить точку заданного цвета в заданном месте экрана для выбранной дисплейной страницы или прочитать значение цвета любой точки на экране соответственно.
H - Прочитать текущий видеорежим.
Если Ваша программа изменяет видеорежим, она может сохранить старый видеорежим, получив его с помощью этой функции.
Кроме того, функция возвращает количество столбцов на экране и номер текущей активной дисплейной страницы.
H - Выбрать цветовую палитру.
Эта функция позволяет управлять цветом рамки вокруг изображения в текстовом режиме и цветом фона в графическом режиме (для адаптеров EGA, VGA).
H - Запись символа на экран в стиле TTY.
После записи символа на экран курсор продвигается на следующую позицию, при этом обрабатываются такие управляющие символы, как BEL (подача звукового сигнала), возврат на одну позицию, перевод строки, возврат к началу строки.
H - Записать символ.
Эта функция предназначена для записи символа в видеопамять без задания для него индивидуального значения атрибута. Используется текущий атрибут. Можно задавать кратность записи.
INT11h - Получить список оборудования.
Прежде чем пытаться работать с каким-либо устройством ввода/вывода, следует убедиться в том, что оно есть в составе оборудования компьютера. В процессе инициализации тестовые модули, находящиеся в BIOS, динамически определяют состав аппаратного обеспечения машины и записывают конфигурацию системы в специально отведенную для этого ячейку памяти.
Программа, вызывая прерывание INT 11h, получает в регистре AX содержимое этой ячейки. Каждый бит в слове конфигурации отвечает за соответствующее устройство.
Анализируя слово конфигурации, программа может узнать, входят ли в состав оборудования компьютера дискеты и если входят, то сколько дисководов имеется в наличии, присутствует ли арифметический сопроцессор, какой начальный режим дисплейного адаптера используется, сколько в системе принтеров, адаптеров последовательного интерфейса RS232, подключен ли игровой адаптер (джойстик)?
Обычно прикладная программа не работает сама с аппаратурой, а пользуется услугами операционной системы. При обращении к стандартной аппаратуре через операционную систему программа пользователя получит признак ошибки, если запрашиваемое устройство отсутствует. Программы, составленные на языке программирования Си при использовании библиотеки эмуляции арифметического сопроцессора, сами определяют, имеется сопроцессор или нет, и не пытаются пользоваться отсутствующим устройством. Операции сопроцессора эмулируются центральным процессором и программа просто работает медленнее.
Но если программа обращается непосредственно к портам ввода/вывода отсутствующего устройства, это может привести в лучшем случае к зависанию системы.
INT12h - Получить размер основной памяти.
Сказанное выше справедливо и по отношению к оперативной памяти. Для работы некоторых программ требуется достаточное количество памяти. Прерывание INT 12h возвращает в регистре AX количество имеющихся блоков памяти размером в один килобайт. Анализируя эту величину, программы могут при нехватке памяти либо вывести на экран соответствующее сообщение и отказаться от работы, либо изменить алгоритмы работы, организовав, например, "виртуальную" память на диске или просто записывая в файл промежуточные результаты.
Если Ваш компьютер оборудован расширенной памятью (адресное пространство этой памяти находится выше границы в 1 мегабайт), размер этой памяти в килобайтах можно узнать, вызвав прерывание INT 15h со значением регистра AX, равным 8800h.
Интерфейс между программным обеспечением и аппаратурой
Фирмы-разработчики аппаратного обеспечения постоянно совершенствуют внешние устройства и другие узлы персонального компьютера. Постоянно появляются новая периферийная аппаратура и новые модификации уже существующих устройств. Старые устройства наделяются новыми возможностями, новые делают такое, о чем раньше не приходилось и мечтать.
Этот процесс можно только приветствовать, однако что делать с уже разработанным программным обеспечением, рассчитанным на старую аппаратуру? Если разработка программного обеспечения была дорогостоящая, если по своим возможностям она еще не устарела, вряд ли целесообразно переделывать все заново только из-за того, что появился новый дисковод с большей емкостью носителя данных или новый принтер, обеспечивающий лучшее качество печати.
Интуитивно ясно, что должна существовать какая-то программная прослойка между аппаратным и программным обеспечением, выполняющая "согласующие" и "унифицирующие" действия. Эта прослойка работает напрямую с аппаратурой, а прикладное (да и системное) программное обеспечение имеет дело только с этой интерфейсной прослойкой.
В разных типах компьютеров и для разных типов операционных систем существуют различные способы устранения зависимости программного обеспечения от аппаратуры. По-видимому, одним из наиболее неудачных следует считать способ, примененный в операционной системе ОС ЕС для ЭВМ ряда ЕС. Для ЭВМ ряда ЕС любое периферийное устройство имеет одинаковый и достаточно сложный аппаратный интерфейс с каналами ЭВМ (канал - это устройство, обеспечивающее ввод/вывод информации в ЭВМ ряда ЕС). Для каждого типа устройства составляются канальные программы (программы, которые будут выполняться каналом). Прикладное программное обеспечение может пользоваться либо вводом/выводом на этом, крайне низком, канальном уровне, либо осуществлять ввод/вывод на уровне так называемых методов доступа. Библиотеки программ методов доступа содержат программы, работающие на канальном уровне. Для любого, даже очень простого нового устройства в этой системе необходим сложный аппаратный интерфейс с каналом. Составление программ своего метода доступа - крайне сложная задача, доступная лишь профессионалам высокого класса. О внесении изменений в существующие программы методов доступа говорить вообще вряд ли стоит, так как они очень сложны, а исходные тексты этих программ, как правило, отсутствуют.
Другой подход используется в операционных системах UNIX и аналогичных ей (XENIX, VENIX и т.д.).
В этих операционных системах для каждого устройства составляется своя программа обслуживания - драйвер. Эта программа выполняет все операции со своим устройством. Сама операционная система имеет дело только с драйвером, а прикладное программное обеспечение для выполнения ввода/вывода вызывает соответствующие модули операционной системы. Такая программная "буферизация" устраняет, с одной стороны, зависимость операционной системы от аппаратуры, с другой - зависимость прикладной программы от операционной системы и от аппаратуры.
Для каждого нового устройства (например, для нового видеоконтроллера) фирма-разработчик поставляет свой драйвер. Если Вы купили новое устройство, Вы просто подключаете новый драйвер к операционной системе и все Ваши любимые старые программы будут теперь работать с новым устройством.
Этот подход имеет тот недостаток, что Вы не сможете легко и свободно переносить Вашу операционную систему с одного типа компьютера на другой, даже совместимый с ним. Драйверы операционной системы UNIX очень сильно привязаны к аппаратуре. На компьютере, который совместим с исходным не полностью, эти драйверы работать не будут.
Обычно операционные системы поставляются с конкретным комплектом драйверов под определенный тип компьютера, и с этим приходится считаться.
Операционная система MS-DOS, работающая на компьютерах фирмы IBM или совместимых с ними, тоже использует механизм драйверов.
Однако драйверы MS-DOS не всегда обращаются напрямую к аппаратуре. Обычно они вызывают функции BIOS, и уже BIOS выполняет все действия по вводу/выводу. Конечно, BIOS содержит программы обслуживания только стандартных устройств ввода/вывода, нестандартные устройства обслуживаются драйверами напрямую.
Использование BIOS как дополнительного интерфейса между драйверами стандартных устройств и аппаратурой резко повышает "живучесть" MS-DOS на не вполне совместимых с IBM персональных компьютерах. И это действительно так - самая распространенная на сегодняшний день операционная система MS-DOS версии 3.30 работает на всех компьютерах, хоть сколько-нибудь совместимых с IBM PC.
Это возможно благодаря тому, что производители совместимых компьютеров учитывают в программах BIOS все аппаратные особенности, и DOS "не видит" отличий. А прикладная программа - тем более.
Почему же этот способ не используется в операционных системах UNIX или OS/2? Дело в том, что к сожалению, программы BIOS не являются реентерабельными. Это не имеет значения для однозадачной MS-DOS, а мультизадачные операционные системы вынуждены сами организовывать обслуживание аппаратуры реентерабельным способом. (Существуют еще проблемы разделения ресурсов между параллельно выполняющимися процессами, которые тоже не решаются в рамках BIOS).
Таким образом, независимость аппаратного и программного обеспечения в DOS обеспечивается, с одной стороны, BIOS для стандартных устройств, с другой стороны - драйверами.
Пользователи могут легко дополнять операционную систему своими драйверами, составленными для нестандартных устройств. Возможна также замена стандартных драйверов, замена или расширение функций BIOS.
В разделе, посвященном резидентным программам, мы уже рассказывали о том, как можно организовать обслуживание нестандартных устройств в DOS с помощью TSR-программ. Но это "незаконный" способ с точки зрения DOS, так как при этом DOS не сможет использовать свои стандартные средства ввода/вывода, ориентированные на связь с аппаратурой через драйверы. Поэтому наилучшим способом организации обслуживания аппаратуры является использование драйвера.
- IOCTL запись, 16 - Вывод, пока не занято
Все эти команды используются для чтения/записи информации из устройства или в устройство соответственно. Они имеют практически одинаковый формат запроса, поэтому мы их будем рассматривать вместе.
В операциях чтения или записи участвуют сектора для блочных устройств и байты для символьных устройств. Область запроса содержит указатель на буфер обмена, куда нужно поместить прочитанные данные или откуда взять данные для записи, поле количества записываемых/читаемых байт для символьных устройств или секторов для блочных.
Кроме того, драйвер должен вернуть количество действительно прочитанных или записанных байт/секторов.
Приведем формат запроса для этих команд:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | media | В этом поле драйверу передается байт-описатель среды носителя данных. |
(+14) 4 | buf_adr | Адрес буфера для передачи данных. |
(+18) 2 | count | Количество передаваемых байтов для символьных устройств или секторов для блочных устройств. |
(+20) 2 | sector | Номер начального сектора, если драйвер использует 16-битовую адресацию секторов или -1 для 32-битовой адресации. Это поле не используется символьными драйверами. |
(+22) 4 | vol_id | Указатель на метку тома в формате ASCIIZ. Возвращается блочным драйвером, если он выставляет ошибку 15 (неправильная смена диска). Это поле должно содержать ссылку на метку требуемого диска. |
(+26) 4 | sect32 | Номер начального сектора, если содержимое поля sector равно -1. Первым идет старшее слово номера сектора. Если обнаружена ошибка с номером 15, в это поле записывается указатель на метку тома. |
После выполнения операций чтения или записи драйвер обязательно должен записать в поле count количество действительно переданных байтов для символьных устройств или секторов для блочных. В случае ошибки также требуется запись правильного значения в поле count, недостаточно только установить признак и код ошибки в слове состояния драйвера.
Для команды 9 (запись с проверкой) драйвер должен после выполнения записи проверить записанные данные. Если с консоли введена команда DOS VERIFY ON, то для дисковых устройств DOS вместо команды записи 8 использует команду записи с проверкой 9.
Драйвер может возвратить количество переданных байтов меньше запрошенного, это само по себе не является ошибкой. Иногда может получиться так, что надо выполнить запись 64 Кбайтов. Такая операция может вызвать переход за границу сегмента в буфере передачи данных. В этом случае драйвер должен проигнорировать лишние байты. Например, надо записать 10000h байтов, распределенных по секторам из буфера с адресом ХХХХ:0001. В этом случае драйвер должен проигнорировать последние два байта. Они будут записаны в следующий раз.
IOCTL чтение/запись - это обмен с устройством управляющей информацией. Мы будем подробно говорить об этом ниже.
Команда с кодом 16 - "вывод, пока не занято", предназначена для работы с такими устройствами ввода/вывода, которые имеют свой собственный буфер, например, принтеры.
Приведем исходный текст программы драйвера, использующего команды ввода и вывода. Для ввода драйвер использует клавиатуру, вывод осуществляется на экран дисплея.
.MODEL tiny .CODE ; Драйвер состоит из одного ; сегмента кода
org 0 ; Эта строка может отсутствовать
include sysp.inc
;========================================================
iodrv PROC far ;драйвер - это FAR-процедура
;========================================================
; Заголовок драйвера
dd 0ffffffffh ;адрес следующего драйвера dw 8000h ;байт атрибутов dw dev_strategy ;адрес процедуры стратегии dw dev_interrupt ;адрес процедуры прерывания db 'IODRIVER' ;имя устройства (дополненное ; пробелами)
;========================================================
; Программа стратегии
dev_strategy: mov cs:req_seg,es mov cs:req_off,bx ret
; Здесь запоминается адрес заголовка запроса
req_seg dw ? req_off dw ?
;========================================================
;Обработчик прерывания
dev_interrupt: push es ;сохраняем регистры push ds push ax push bx push cx push dx push si push di push bp
; Устанавливаем ES:BX на заголовок запроса
mov ax,cs:req_seg mov es,ax mov bx,cs:req_off
; Получаем код команды из заголовка запроса и умножаем ; его на два, чтобы использовать в качестве индекса ; таблицы адресов обработчиков команд
mov al,es:[bx]+2 shl al,1
sub ah,ah ; Обнуляем AH lea di,functions ; DI указывает на смещение ; таблицы add di,ax ; Добавляем смещение в таблице jmp word ptr [di] ; Переходим на адрес из таблицы
functions LABEL WORD ; Таблица функций
dw initialize dw check_media dw make_bpb dw ioctl_in dw input_data dw nondestruct_in dw input_status dw clear_input dw output_data dw output_verify dw output_status dw clear_output dw ioctl_out dw Device_open dw Device_close dw Removable_media
; Выход из драйвера, если функция не поддерживается
check_media: make_bpb: ioctl_in: nondestruct_in: input_status: clear_input: output_verify: output_status: clear_output: ioctl_out: Removable_media: Device_open: Device_close:
or es:word ptr [bx]+3,8103h jmp quit
;========================================================
; Обработчик команды вывода данных
output_data:
; Записываем в регистр CL количество ; выводимых символов
mov cl,es:[bx]+18 push cx
; Выводим сообщение о начале вывода
mov ax,cs mov ds,ax mov si,offset outmsg call dpc
pop cx
; Загружаем в DS:SI адрес буфера данных
mov ax,es:[bx]+16 mov ds,ax mov si,es:[bx]+14
; Выводим на экран символы из буфера
out_loop: mov al,ds:byte ptr [si] @@out_ch al inc si loop out_loop
jmp quit
;========================================================
; Обработчик команды ввода данных
input_data:
; Записываем в регистр CL количество ; вводимых символов
mov cl,es:[bx]+18 push cx
; Выводим сообщение о начале ввода
mov ax,cs mov ds,ax mov si,offset inpmsg call dpc
; Загружаем в DS:SI адрес буфера данных
pop cx mov ax,es:[bx]+16 mov ds,ax mov di,es:[bx]+14
; Вводим символы с клавиатуры и записываем в буфер
inp_loop: mov ax,0 int 16h mov ds:byte ptr [di],al @@out_ch al inc di loop inp_loop
jmp quit
;========================================================
quit: or es:word ptr [bx]+3,100h pop bp pop di pop si pop dx pop cx pop bx pop ax pop ds pop es ret
;========================================================
; Процедура выводит на экран строку ; символов в формате ASCIIZ
dpc proc near push si dpc_loop: cmp ds:byte ptr [si],0 jz end_dpc mov al,ds:byte ptr [si] @@out_ch al inc si jmp dpc_loop
end_dpc: pop si ret dpc endp
;========================================================
hello db 13,10,'++' db 13,10,'¦ *IODRV* (C)Frolov A., 1990 ¦' db 13,10,'++' db 13,10,0
outmsg DB 13,10,'___ Вывод на устройство IODRIVER ___',0 inpmsg DB 13,10,'___ Ввод с устройства IODRIVER ___',0
;========================================================
E_O_P: ;Метка конца программы
initialize:
lea ax,E_O_P ;смещение конца программы в AX mov es:word ptr [bx]+14,ax ;помещаем его в заголовок mov es:word ptr [bx]+16,cs ;
; Стираем экран
mov dh,18h mov dl,80h
xor cx,cx mov bh,7 xor al,al mov ah,6 int 10h
; Устанавливаем курсор в левый верхний угол экрана
mov bh,0 xor dx,dx mov ah,2 int 10h
; Выводим сообщение
mov ax,cs mov ds,ax mov si,offset hello call dpc
jmp quit
iodrv ENDP
END iodrv
Для работы с этим драйвером можно использовать приводимую ниже программу, составленную на языке Си. Программа открывает устройство, вводит из него восемь символов, печатает введенные символы на экране и выводит их обратно на устройство:
#include <io.h> #include <conio.h> #include <stdio.h> #include <fcntl.h> #include <sys\types.h> #include <sys\stat.h> #include <malloc.h> #include <errno.h> #include <dos.h>
int main(void);
int main(void) {
char buf[100]; int io_handle; unsigned count;
// Открываем устройство с именем IODRIVER
if( (io_handle = open("IODRIVER", O_RDWR)) == - 1 ) {
// Если открыть не удалось, выводим // код ошибки
printf("Ошибка при открытии устройства %d",errno); return errno; }
// Читаем 8 байт из устройства в буфер buf
if( (count = read(io_handle, buf, 8)) == -1 ) {
// Если при чтении произошла ошибка, // выводим ее код
printf("Ошибка чтения %d",errno); return errno; }
// Закрываем прочитанную строку нулем // для последующего вывода функцией printf
buf[8]=0;
printf("\n___ Введена строка: %s ___",buf);
// Выводим только что прочитанные данные // обратно на то же устройство
if( (count = write(io_handle, buf, 8)) == -1 ) {
// Если при записи произошла ошибка, // выводим ее код
printf("Ошибка записи %d",errno); return errno; }
// Закрываем устройство close(io_handle); exit(0); }
Эта программа служит примером того, как можно организовать взаимодействие драйвера и прикладной программы, работающей с драйвером. Позже мы приведем пример более сложного драйвера символьного устройства.
Изменение таблицы векторов прерываний
Вашей программе может потребоваться организовать обработку некоторых прерываний. Для этого программа должна переназначить вектор на свой обработчик. Это можно сделать, изменив содержимое соответствующего элемента таблицы векторов прерываний.
Очень важно не забыть перед завершением работы восстановить содержимое измененных векторов в таблице прерываний, т.к. после завершения работы программы память, которая была ей распределена, считается свободной и может быть использована для загрузки другой программы. Если вы забыли восстановить вектор и пришло прерывание, то система может разрушиться - вектор теперь указывает на область, которая может содержать что угодно.
Поэтому последовательность действий для нерезидентных программ, желающих обрабатывать прерывания, должна быть такой:
прочитать содержимое элемента таблицы векторов прерываний для вектора с нужным вам номером;
запомнить это содержимое (адрес старого обработчика прерывания) в области данных программы;
установить новый адрес в таблице векторов прерываний так, чтобы он соответствовал началу Вашей программы обработки прерывания;
перед завершением работы программы прочитать из области данных адрес старого обработчика прерывания и записать его в таблицу векторов прерываний.
Кроме того, операция изменения вектора прерывания должна быть непрерывной в том смысле, что во время изменения не должно произойти прерывание с номером, для которого производится замена программы обработки. Если вы, например, запишете новое значение смещения, а сегментный адрес обновить не успеете, то по какому адресу будет передано управление в случае прерывания и что при этом произойдет? Об этом можно только догадываться.
Для облегчения работы по замене прерывания DOS предоставляет в Ваше распоряжение специальные функции для чтения элемента таблицы векторов прерывания и для записи в нее нового адреса. Если вы будете использовать эти функции, DOS гарантирует, что операция по замене вектора будет выполнена правильно. Вам не надо заботиться о непрерывности процесса замены вектора прерывания.
Для чтения вектора используйте функцию 35h прерывания 21h. Перед ее вызовом регистр AL должен содержать номер вектора в таблице. После выполнения функции в регистрах ES:BX будет искомый адрес обработчика прерывания.
Функция 25h прерывания 21h устанавливает для вектора с номером, находящимся в AL, обработчик прерывания DS:DX.
Разумеется, вы можете непосредственно обращаться к таблице векторов прерываний, но тогда при записи необходимо замаскировать прерывания командой CLI, не забыв разрешить их после записи командой STI.
Для пользователей языка Си библиотека Quick C содержит функции _dos_getvec(), _dos_setvect(). Первая функция получает адрес из таблицы векторов прерываний, вторая устанавливает новый адрес.
Если вам надо добавить какие-либо собственные действия к тем, что выполняет стандартный обработчик прерывания, то можно организовать цепочку прерываний.
Для организации цепочки прерываний вы записываете в векторную таблицу адрес своего обработчика, не забыв сохранить прежнее содержимое таблицы. Ваш обработчик получает управление по прерыванию, выполняет какие-либо действия, затем передает управление стандартному обработчику. Можно сделать и по-другому: ваш обработчик вызывает стандартный обработчик как подпрограмму, после возврата из стандартного обработчика выполняет дополнительные действия. То есть вы можете вставить дополнительную обработку как до вызова стандартного обработчика, так и после его вызова.
В библиотеке Quick C имеется функция для организации цепочки прерываний - _chain_intr().
Рассмотрим более подробно возможности библиотеки интегрированной среды Quick C, предназначенные для работы с прерываниями.
Модификатор interrupt (_interrupt для Quick C 2.5 и C 6.0) описывает функцию, которая является обработчиком прерывания. Такая функция завершается командой возврата из обработки прерывания IRET, и для нее автоматически генерируются команды сохранения регистров на входе и их восстановления при выходе из обработчика прерывания. Пример использования модификатора для описания функции обработки прерывания:
void interrupt far int_funct(void) { // Тело обработчика прерывания }
Функция обработки прерывания должна быть FAR-функцией, т.к. таблица векторов прерываний содержит полные адреса в виде сегмент:смещение.
Ключевое слово interrupt используется также для описания переменных, предназначенных для хранения векторов прерываний:
void (_interrupt _far *oldvect)(void);
Модификаторы _interrupt и _far для Quick C 2.5 и C 6.0 являются синонимами соответственно interrupt и far.
Какие требования предъявляются к программе обработки прерывания?
Если прерывания происходят часто, то их обработка может сильно замедлить работу прикладной программы. Поэтому обработчик прерывания должен быть короткой, быстро работающей программой, которая выполняет только самые необходимые действия. Например, считать очередной символ из порта принтера и поместить его в буфер, увеличить значение какого-либо глобального счетчика прерываний и т.п.
Для установки своего обработчика прерываний используйте функцию _dos_setvec. Эта функция имеет два параметра - номер прерывания и указатель на новую функцию обработки прерывания. Например:
_dos_setvect(0x16, my_key_intr);
В этом примере для клавиатурного прерывания с номером 16h устанавливается новый обработчик прерывания my_key_intr.
Если вам надо узнать адрес старого обработчика прерывания по его номеру, лучше всего воспользоваться функцией _dos_getvect, которая принимает в качесте параметра номер прерывания и возвращает указатель на соответствующий этому номеру в таблице векторов прерываний обработчик. Например:
old_vector = _dos_getvect(0x16);
Для организации цепочки прерываний используйте функцию _chain_intr. В качестве параметра эта функция принимает адрес старого обработчика прерываний.
Следующий простой пример иллюстрирует применение всех трех функций, предназначенных для работы с прерываниями. Эта программа встраивает собственный обработчик прерывания таймера, который будет вызываться примерно 18,2 раза в секунду. Встраиваемый обработчик прерывания считает тики таймера и, если значение счетчика кратно 20, на динамик компьютера выдается звуковой сигнал. В конце работы новая программа обработки прерывания таймера вызывает старый обработчик с помощью функции _chain_intr.
После установки нового обработчика прерывания таймера основная программа ждет нажатия на клавиатуре любой клавиши, затем она восстанавливает старое содержимое вектора прерывания.
#include <dos.h> #include <stdio.h> #include <stdlib.h>
// Выключаем проверку стека и указателей
#pragma check_stack( off ) #pragma check_pointer( off )
// Это макро используется для выдачи // сигнала на внутренний динамик // компьютера. Используется вывод // в формате TTY символа BELL (7) // через прерывание BIOS 10h
#define BEEP() _asm { \ _asm mov bx,0 \ _asm mov ax, 0E07h \ _asm int 10h \ }
void main(void);
// Объявление программы обработки прерывания
void _interrupt _far timer(void);
// Эта переменная предназначена для хранения // старого значения вектора прерывания // таймера. Она должна быть глобальной.
void (_interrupt _far *oldvect)(void);
// Переменная для подсчета тиков таймера
volatile long ticks;
void main(void) {
ticks=0L; // Сбрасываем счетчик тиков таймера
oldvect = _dos_getvect(0x1c); // Запоминаем адрес // старого обработчика // прерывания
_dos_setvect(0x1c, timer); // Устанавливаем свой // обработчик
printf("\nТаймер установлен. Нажмите любую" "клавишу...\n");
getch(); // Ожидаем нажатия на любую клавишу
_dos_setvect(0x1c,oldvect); // Восстанавливаем старый // обработчик прерывания // таймера exit(0);
}
// Функция обрабатывает прерывания таймера
void _interrupt _far timer(void) {
ticks++; // Увеличиваем счетчик тиков таймера
// Если значение счетчика тиков кратно 20, // выдаем сигнал на динамик компьютера if((ticks % 20) == 0) BEEP();
// Вызываем старый обработчик прерывания _chain_intr(oldvect);
}
Как программе стать резидентной?
У вас есть две возможности оставить свою программу резидентной в памяти - использовать прерывание INT27h или функцию 31h прерывания INT 21h.
Для использования прерывания 27h сегментный регистр CS должен указывать на PSP программы, а в регистре DX должно быть записано смещение последнего байта программы плюс один байт. Нетрудно заметить, что этот способ остаться резидентной больше всего подходит для программ в формате COM. Вы не сможете оставить резидентной программу длиннее 64 килобайт.
Другой, более удобный способ - использовать функцию 31h прерывания INT 21h. В регистре AL вы можете указать код завершения программы, регистр DX в этом случае должен содержать длину резидентной части программы в параграфах. Здесь уже нет ограничения 64 килобайта на длину программы. Использование этой функции - единственная возможность оставить резидентной программу длиннее 64 килобайт.
Но не стоит увлекаться длинными TSR-программами, так как обычно освободить память, занимаемую ставшей уже ненужной резидентной программой, можно только с помощью перезагрузки операционной системы.
Библиотека функций Quick C содержит специальную функцию для оставления программы резидентной в памяти. Эта функция использует прерывание INT 21h (функция 31h) и имеет имя _dos_keep(). Первый параметр функции - код завершения (то, что записывается в регистр AL), а второй - длина резидентной части программы в параграфах.
Нужно внимательно следить за размером оставляемой резидентной части программы, не забывать про области данных и буфера. Если вы укажите слишком малый объем памяти для TSR-программы, то прикладная программа, загруженная сразу за ней, может оказаться разрушенной.
Маскирование прерываний
Часто при выполнении критических участков программ, для того чтобы гарантировать выполнение определенной последовательности команд целиком, приходится запрещать прерывания. Это можно сделать командой CLI. Ее нужно поместить в начало критической последовательности команд, а в конце расположить команду STI, разрешающую процессору воспринимать прерывания. Команда CLI запрещает только маскируемые прерывания, немаскируемые всегда обрабатываются процессором.
Если вы используете запрет прерываний с помощью команды CLI, следите за тем, чтобы прерывания не отключались на длительный период времени, так как это может привести к нежелательным последствиям. Например, будут отставать часы.
Если вам надо запретить не все прерывания, а только некоторые, например, от клавиатуры, то для этого надо воспользоваться услугами контроллера прерываний. Подробно об этом немного ниже, сейчас отметим только, что выдачей в этот контроллер определенной управляющей информации можно замаскировать прерывания от отдельных устройств.
Механизм прерываний
Для обработки событий, происходящих асинхронно по отношению к выполнению программы, лучше всего подходит механизм прерываний. Прерывание можно рассматривать как некоторое особое событие в системе, требующее моментальной реакции. Например, хорошо спроектированные системы повышенной надежности используют прерывание по аварии в питающей сети для выполнения процедур записи содержимого регистров и оперативной памяти на магнитный носитель с тем, чтобы после восстановления питания можно было продолжить работу с того же места.
Кажется очевидным, что возможны самые разнообразные прерывания по самым различным причинам. Поэтому прерывание рассматривается не просто как таковое: с ним связывают число, называемое номером типа прерывания или просто номером прерывания. С каждым номером прерывания связывается то или иное событие. Система умеет распознавать, какое прерывание, с каким номером оно произошло, и запускает соответствующую этому номеру процедуру.
Программы могут сами вызывать прерывания с заданным номером. Для этого они используют команду INT. Это так называемые программные прерывания. Программные прерывания не являются асинхронными, так как вызываются из программы (а она-то знает, когда она вызывает прерывание!).
Программные прерывания удобно использовать для организации доступа к отдельным, общим для всех программ модулям. Например, программные модули операционной системы доступны прикладным программам именно через прерывания, и нет необходимости при вызове этих модулей знать их текущий адрес в памяти. Прикладные программы могут сами устанавливать свои обработчики прерываний для их последующего использования другими программами. Для этого встраиваемые обработчики прерываний должны быть резидентными в памяти. Мы научимся создавать свои программы обработки прерываний и будем говорить об этом при обсуждении резидентных программ.
Аппаратные прерывания вызываются физическими устройствами и приходят асинхронно. Эти прерывания информируют систему о событиях, связанных с работой устройств, например о том, что наконец-то завершилась печать символа на принтере и неплохо было бы выдать следующий символ, или о том, что требуемый сектор диска уже прочитан, его содержимое доступно программе.
Использование прерываний при работе с медленными внешними устройствами позволяют совместить ввод/вывод с обработкой данных в центральном процессоре и в результате повышает общую производительность системы.
Некоторые прерывания (первые пять в порядке номеров) зарезервированы для использования самим центральным процессором на случай каких-либо особых событий вроде попытки деления на ноль, переполнения и т.п.
Иногда желательно сделать систему нечувствительной ко всем или отдельным прерываниям. Для этого используют так называемое маскирование прерываний, о котором мы еще будем подробно говорить. Но некоторые прерывания замаскировать нельзя, это немаскируемые прерывания.
Заметим еще, что обработчики прерываний могут сами вызывать программные прерывания, например, для получения доступа к сервису BIOS или DOS (сервис BIOS также доступен через механизм программных прерываний).
Составление собственных программ обработки прерываний и замена стандартных обработчиков DOS и BIOS является ответственной и сложной работой. Необходимо учитывать все тонкости работы аппаратуры и взаимодействия программного и аппаратного обеспечения. При отладке возможно разрушение операционной системы с непредсказуемыми последствиями, поэтому надо очень внимательно следить за тем, что делает Ваша программа.
Обработка ошибок
Когда программа обращается к DOS для выполнения какой-либо операции, она должна вызвать соответствующее прерывание, загрузив перед вызовом прерывания все необходимые операнды в регистры процессора. Если выполнение операции невозможно по каким-то причинам (неправильные операнды, устройство неработоспособно, запрашиваемая операция не поддерживается текущей версией DOS и т.д.), то для большинства функций DOS устанавливается признак ошибки - флаг переноса CARRY. Для DOS версии 2.0 и более поздних версий регистр AX при этом содержит код ошибки.
Приведем коды ошибок, возвращаемые программе через регистр AX:
1 | Неправильный код функции |
2 | Файл не найден |
3 | Путь не найден |
4 | Слишком много открытых файлов |
5 | Доступ запрещен |
6 | Неправильный идентификатор файла |
7 | Разрушен блок управления памятью |
8 | Недостаточно памяти |
9 | Неправильный адрес блока памяти |
10 | Неправильная среда |
11 | Неправильный формат |
12 | Неправильный код доступа |
13 | Неправильные данные |
14 | Зарезервировано |
15 | Ошибка при указании дисковода |
16 | Невозможно удалить текущий каталог |
17 | Другое устройство |
18 | Больше нет подходящих файлов |
Для DOS версии 3.0 и более поздних версий обработка ошибок значительно расширена. Введена функция 59h прерывания INT 21h, предназначенная для получения дополнительной информации об ошибках.
При вызове этой функции регистр BX должен содержать индикатор уровня анализа ошибок, который должен быть равен 0. Кроме расширенного кода ошибки, возвращаемого в регистре AX, программа может получить класс ошибки (регистр BH), код предполагаемых действий (регистр BL), локализацию ошибки, т.е. место, где произошла ошибка (регистр CH).
К сожалению, эта функция разрушает содержимое регистров CL, DX, SI, DI, BP, DS, ES. Программа, использующая функцию 59h, должна позаботиться о сохранении содержимого этих регистров.
Расширенный код ошибки, возвращаемый в регистре AX, может принимать значения, указанные в приводимой ниже таблице. Коды от 1 до 18 эквивалентны представленным выше и второй раз не приводятся.
Расширенные коды ошибок:
19 | Запись на защищенный от записи диск |
20 | Задан неизвестный идентификатор устройства |
21 | Дисковод не готов |
22 | Неизвестная команда |
23 | Ошибка циклического кода проверки |
24 | Неправильная длина структуры запроса |
25 | Ошибка поиска |
26 | Неизвестен тип среды носителя данных |
27 | Сектор не найден |
28 | Кончилась бумага в принтере |
29 | Ошибка записи |
30 | Ошибка чтения |
31 | Общая ошибка |
32 | Нарушение разделения файла |
33 | Нарушение блокировки файла |
34 | Неправильная замена диска |
35 | FCB недоступен (слишком много блоков FCB) |
36 | Переполнился буфер разделения |
37 | Зарезервировано |
38 | Не завершена операция "Конец файла" |
39-49 | Зарезервировано |
50 | Сетевая функция не поддерживается |
51 | Удаленный компьютер "не слышит" |
52 | Дублирование имени в сети |
53 | Сетевое имя не найдено |
54 | Сеть занята |
55 | Сетевое устройство больше не существует |
56 | Превышен лимит команды сетевой BIOS |
57 | Ошибка в аппаратуре сетевого адаптера |
58 | Неправильный ответ из сети |
59 | Непредусмотренная ошибка сети |
60 | Несовместимый удаленный адаптер |
61 | Заполнена очередь печати |
62 | Для печатаемого файла недостаточно места |
63 | Печатающийся файл был удален |
64 | Сетевое имя было удалено |
65 | Доступ запрещен |
66 | Неправильный тип сетевого устройства |
67 | Сетевое имя не найдено |
68 | Превышен лимит сетевого имени |
69 | Превышен лимит сеанса сетевой BIOS |
70 | Временная пауза |
71 | Сетевой запрос отвергнут |
72 | Приостановлена печать или переадресация диска |
73-79 | Зарезервировано |
80 | Файл уже существует |
81 | Зарезервировано |
82 | Невозможно создать дескриптор в каталоге |
83 | Ошибка обработчика критических ошибок INT 24h |
84 | Слишком много переназначений |
85 | Двойное переназначение |
86 | Неправильный пароль |
87 | Неправильный параметр |
88 | Ошибка данных в сети |
89 | Нет такой функции в сети |
90 | Требуемый компонент системы не установлен |
1 | Недостаточно ресурсов: блоков FCB, памяти и т.д. |
2 | Временная ситуация |
3 | Нет прав доступа |
4 | Внутренняя ошибка DOS |
5 | Ошибка аппаратуры |
6 | Системная ошибка DOS (нет CONFIG.SYS и т.п.) |
7 | Ошибка в прикладной программе |
8 | Файл или объект не найден |
9 | Неправильный формат файла или объекта |
10 | Файл или объект заблокирован |
11 | Ошибка носителя данных |
12 | Файл или объект уже существует |
13 | Прочие ошибки |
Код предполагаемых действий, передаваемый через регистр BL, поможет Вашей программе правильно обработать ошибку и избежать зависания системы. Приведем таблицу кодов предполагаемых действий:
1 | Повторить операцию позже. Можно спросить пользователя, желает он повторить операцию или завершить работу программы. |
2 | Повторить предыдущую операцию после небольшой паузы. Если ошибка не исчезла, следует спросить пользователя, будет он ждать и дальше, или следует завершить работу программы. |
3 | Если пользователь вводил какие-то данные для DOS, следует попросить его ввести эти данные еще раз (например, пользователь мог указать неправильный идентификатор диска или путь доступа к файлу). |
4 | Аварийно завершить работу прикладной программы с выполнением всех обычных завершающих действий (закрытие файлов, сброс буферов на диск, освобождение блоков памяти и т.д.) |
5 | Немедленный выход из программы без выполнения завершающих действий. Система находится в непредсказуемом состоянии. |
6 | Следует игнорировать ошибку. |
7 | Повторить операцию после того, как пользователь выполнит требуемые действия (установит дискету и т.п.). |
1 | Локализация ошибки не может быть определена (система не знает, где произошла ошибка). |
2 | Ошибка произошла в блочном устройстве (диск или магнитная лента). |
3 | Ошибка связана с сетью. |
4 | Ошибка произошла в символьном устройстве, например, в принтере. |
5 | Ошибка связана с оперативной памятью. |
int 21h jc error
Программы, составленные на языке Си, обращаются к прерываниям DOS обычно с помощью таких функций, как intdos, int86, intdosx и т.д. Для передачи параметров используются структуры REGS, WORDREGS, BYTEREGS, SREGS. Они описаны в файле dos.h, для использования этих структур программа должна содержать строку:
include <dos.h>
Значение флага переноса записывается в переменную cflag, определенную в структуре WORDREGS. Эта структура входит в объединение REGS:
union REGS { struct WORDREGS x; struct BYTEREGS h; }
struct WORDREGS { unsigned int ax; unsigned int bx; unsigned int cx; unsigned int dx; unsigned int si; unsigned int di; unsigned int cflag; }
struct BYTEREGS { unsigned char al, ah; unsigned char bl, bh; unsigned char cl, ch; unsigned char dl, dh; }
Проверка переменной cflag может быть выполнена, например, таким образом:
union REGS inregs, outregs;
intdos(&inregs, &outregs); if( outregs.x.cflaf != 0 ) error();
Код ошибки при этом содержится в переменной outregs.x.ax.
Приведем пример программы, которая стирает каталог с именем DIR в текущем каталоге и, в случае ошибки, выводит расширенную информацию об ошибке, класс ошибки, код предполагаемых действий и код локализации ошибки:
#include <dos.h> #include <stdio.h>
union REGS inregs, outregs; struct SREGS segregs;
void main(void);
void main(void) {
char _far *dir_name = "DIR";
// Стираем каталог с именем DIR. Для этого вызываем // функцию 0x3A прерывания INT 21h.
inregs.h.ah = 0x3a; segregs.ds = FP_SEG(dir_name); inregs.x.dx = FP_OFF(dir_name); intdosx(&inregs, &outregs, &segregs);
// Если после выполнения прерывания установлен // флаг переноса, выводим сообщение об ошибке.
if(outregs.x.cflag != 0) { printf( "Ошибка при удалении каталога: %d", outregs.x.ax);
// Получаем расширенную информацию об ошибке // с помощью функции 0x59 прерывания INT 21h.
inregs.h.ah = 0x59; inregs.x.bx = 0;
// Сохраняем регистры в стеке, т.к. их содержимое // изменится
_asm { push ds push es push si push di }
intdosx(&inregs, &outregs, &segregs);
_asm { pop di pop si pop es pop ds }
// Выводим расширенную информацию об ошибке.
printf("\nРасширенный код ошибки: %d" "\nКласс ошибки: %d" "\nПредполагаемые действия: %d" "\nЛокализация ошибки: %d", outregs.x.ax, outregs.h.bh, outregs.h.bl, outregs.h.ch); } }
При составлении программ обработки ошибок следует учитывать, что для DOS версии 1.0 при некоторых ошибках функции DOS возвращают в регистре AX значение 0FFh. Начиная с версии DOS 2.0, при ошибке устанавливается флаг переноса, код ошибки записывается в регистр AX. Однако для более полной диагностики причины ошибки следует использовать функцию 59h прерывания INT 21h.
Если Ваша программа, составленная на языке Си, вызывает функции DOS неявным образом (через функции стандартной библиотеки транслятора, такие как fprintf, puts и т.д.), то можно воспользоваться средствами обработки ошибок, входящими в состав стандартной библиотеки.
Когда при обращении к функциям DOS средствами стандартной библиотеки транслятора Си возникает ошибка, то в глобальную переменную errno записывается код ошибки.
Возможны следующие коды ошибок (они описаны в файле errno.h и stdlib.h):
ECHILD | Нет порожденных процессов. Задача, не имеющая подзадач, выдала команду ожидания, или была выдана команда ожидания для подзадачи, имеющей признак NO-WAIT. |
EAGAIN | Больше нет процессов. Попытка создать новый процесс окончилась неудачно, т.к. либо больше нет резервов для создания процессов, либо недостаточно оперативной памяти, либо превышен максимальный уровень вложенности процессов. |
E2BIG | Слишком велик список аргументов. Либо размер списка аргументов превышает 128 байт, либо требуемый размер памяти для среды превышает 32К. |
EACCES | Доступ запрещен. Затребованный вид доступа к файлу запрещен или несовместим с установленными атрибутами файла (или каталога). Этот код ошибки передается при попытке чтения из неоткрытого файла, при попытке записи в файл, защищенный от записи, или при попытке открыть каталог как файл. |
EBADF | Плохой номер файла. Номер файла, использованный при вызове функции, имеет неверное значение или не относится к открытому файлу, или сделана попытка записи в открытый только для чтения файл или устройство. |
EDEADLOCK | Произошла блокировка ресурсов. Произведено 10 неудачных попыток заблокировать файл. Этот код ошибки используется только DOS версии 3.0 и более поздних версий. |
EDOM | Ошибка в аргументе математической функции. Аргумент математической функции вышел за пределы области определения этой функции. |
EEXIST | Файл уже существует. Сделана попытка создать файл с именем, которое уже используется существующим файлом. |
EINVAL | Неверный аргумент. Для одного из аргументов функции было задано неверное значение. |
EMFILE | Открыто слишком много файлов. Исчерпан запас номеров файлов , нельзя больше открыть ни одного файла. |
ENOENT | Нет такого файла или каталога. Запрошенный файл или каталог отсутствует или не может быть найден. |
ENOEXEC | Сделана попытка выполнить загрузочный файл, имеющий неправильный формат. |
ENOMEM | Недостаточно памяти. Эта ошибка появляется, когда недостаточно памяти для запуска процесса или для удовлетворения запроса программы на выделение блока памяти. |
ENOSPC | Нет свободного места на устройстве. На устройстве нет места для записи информации (например, переполнился диск). |
ERANGE | Слишком большой результат. Слишком большой по величине аргумент математической функции привел к частичной или полной потере значимости результата. |
EXDEV | Связь различных устройств. Сделана попытка переслать файл на другое устройство, используя функцию переименования. |
Из приведенного выше списка кодов ошибок видно, что средствами стандартной библиотеки транслятора обрабатываются не только ошибки, возникающие при обращении к функциям DOS, но и ошибки, появляющиеся при работе с математическим функциями.
Для диагностической выдачи сообщения об ошибке можно использовать функции perror и strerror. Первая функция выводит в stderr соответствующее сообщение об ошибке, вторая только формирует строку сообщения. Функции perror и strerror имеют операнд - указатель на строку. Эта строка добавляется в начало стандартного сообщения об ошибке. Если к стандартному сообщению ничего добавлять не надо, операнд должен иметь значение NULL.
Следует заметить, что значение переменной errno отражает последнюю ошибку. Успешный вызов функции не приводит к автоматическому сбросу переменной errno.
Поэтому функции perror и strerror необходимо вызывать сразу после того, как вызываемая функция возвратит признак ошибки.
Приведем пример программы, обрабатывающей ошибки с использованием переменной errno:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h>
void main(int argc, char *argv[]) {
FILE *stream;
// Открываем файл только для чтения
stream = fopen(argv[1], "r");
// Если произошла ошибка, выводим сообщение
if( (stream == NULL) (ferror(stream)) ) { perror("Не могу открыть файл"); exit(errno); }
// Пытаемся произвести запись в файл, который // открыт только для чтения. Это приведет к ошибке.
fprintf(stream, "Пишем в файл\n");
if( (stream == NULL) (ferror(stream)) ) {
// Выводим сообщение об ошибке двумя способами - // с помощью функции perror и strerror
perror("Запись в защищенный файл"); printf("Запись в защищенный файл: %s\n", strerror(errno));
exit(errno); } exit( 0 ); }
DOS имеет еще одно средство для обработки ошибок - обработчик критических ошибок (Critical Error Handler). Этот модуль вызывается DOS, когда она получает сообщение об ошибке от драйвера устройства.
Модуль выдает на экран хорошо известное вам сообщение:
Abort, Retry, Ignore?
Это сообщение обычно появляется тогда, когда вы забываете вставить дискету или начинаете печатать при отключенном принтере.
Прикладные программы могут подключать свой модуль обработки критических ошибок вместо стандартного. Мы научимся обрабатывать критические ошибки в книге 3. Там же будет приведен соответствующий пример.
Общая схема работы DOS
Для того чтобы правильно работать с системным программным и аппаратным обеспечением, нужно четко представлять себе механизм взаимодействия прикладной программы с компьютером. На рис. 1.1 показаны функциональные связи программы с программно-аппаратным обеспечением IBMPC.
Рис.1. Функциональные связи программы для MS-DOS с программно-аппаратным обеспечением ПЭВМ
Как правило, ядро DOS разделяют на несколько подсистем, каждая из которых отвечает за выполнение той или иной задачи. Как показано на рисунке, обычно выделяются следующие подсистемы:
файловая система;
система управления памятью;
система управления программами;
система связи с драйверами устройств;
система обработки ошибок;
службу времени;
систему ввода/вывода консоли оператора.
Эти подсистемы общаются с аппаратурой через BIOS, драйверы или напрямую. Прикладное программное обеспечение может вызывать подсистемы DOS, работать с BIOS или непосредственно с аппаратурой. Обратите, однако, внимание на то, что прикладные программы могут обращаться к драйверам только через соответствующую подсистему DOS.
Очевидно также, что чем выше уровень интерфейса прикладной программы и аппаратуры, тем меньше программа будет зависеть от особенностей аппаратуры.
Рассмотрим подсистемы DOS отдельно.
Обслуживание дисковой подсистемы.
Прерывание INT13h предназначено для обслуживания жестких и флоппи-дисков. Многочисленные функции прерывания INT 13h выполняют все операции по вводу/выводу на диски. Мы сделаем обзор только самых важных функций, остальные будут рассмотрены в книге 3, посвященной файловой системе.
Обслуживание клавиатуры.
Обработчик прерывания INT 16h выполняет несколько функций, связанных с обслуживанием клавиатуры. Мы не будем сейчас перечислять эти функции, они будут подробно описаны в главе, посвященной клавиатуре. С помощью функций обслуживания клавиатуры можно выполнить ввод кода нажатой клавиши как с ожиданием нажатия, так и без ожидания. В последнем случае функция сразу после вызова возвращает код нажатой клавиши или признак того, что никакая клавиша не нажималась.
Заметим, что символы, введенные с клавиатуры, помещаются в специальный клавиатурный буфер. Функция ввода символа без ожидания нажатия на клавишу проверяет состояние буфера - есть в нем символы, или нет. Если в буфере есть символы, первый помещенный в буфер символ возвращается программе. Этот символ затем может быть считан функцией ввода с ожиданием нажатия - фактически ожидания при этом не будет.
Для очистки буфера клавиатуры также можно использовать пару описанных выше функций: сначала программа проверяет пуст ли буфер, и, если он не пуст, считывает символ. Считанный символ никуда не помещается (теряется). После считывания символа программа опять проверяет содержимое буфера и так до тех пор, пока клавиатурный буфер не окажется пустым.
Для машин класса не ниже AT обработчик прерывания INT 16h выполняет и другие функции: установку задержки, запись символов в буфер клавиатуры, обслуживание расширенной клавиатуры.
Обслуживание последовательного порта связи
Функции прерывания INT 14h обслуживают порт последовательной передачи данных RS232. С помощью этих функций можно задавать формат и скорость передачи данных, определять состояние портов и, конечно, выполнять побайтную передачу данных.
Обзор прерываний BIOS
Как правило, любая программа работает с тем или иным устройством ввода/вывода. Программы для первых ЭВМ работали непосредственно с портами и регистрами этих устройств. Модули, выполняющие такие стандартные действия, как ввод данных с перфокарт или печать результатов вычислений, входили в состав пользовательских программ.
В начале своего развития микропроцессорные системы имели в своем составе программу, называемую "монитор". Эта программа обычно находилась в постоянном запоминающем устройстве и обслуживала устройства ввода/вывода: клавиатуру, дисплей, кассетный накопитель на магнитной ленте и др. Диалоговая часть монитора позволяла выполнять некоторые операторские функции: загрузку и запуск программы, отладку программы в пошаговом режиме, печать текстов, просмотр и редактирование содержимого памяти и т.п. Но самое главное - прикладные программы, составленные для этих систем, могли пользоваться модулями монитора для работы с периферийной аппаратурой и для выполнения других функций.
Программа уже не содержала все необходимые для ее работы модули, а пользовалась "стандартными" услугами программы-монитора. Такая организация программы не только уменьшила размер ее загрузочного модуля, но и позволила программистам сосредоточить свои усилия на решении основной задачи.
Как программа пользовалась услугами монитора? Механизм взаимодействия программы пользователя и монитора был реализован по-разному в разных системах. В худшем случае прикладная программа пользовалась известными абсолютными адресами модулей монитора, в лучшем - использовала специальные таблицы адресов программных модулей.
К сожалению, разные системы были несовместимы по составу модулей монитора и механизму их вызова, что сильно затрудняло, если не совершенно исключало их программную совместимость.
В первом массовом персональном компьютере IBM PC модули обслуживания стандартной периферии были записаны в постоянном запоминающем устройстве. Совокупность этих модулей (плюс программа начальной инициализации и тестирования) называется базовой системой ввода/вывода - Basic Input/Output System. Общепринятое сокращение - BIOS.
Выпускаемые различными фирмами компьютеры, совместимые с IBM PC, могут немного отличаться по типу периферийного оборудования, но для достижения совместимости с IBM PC модули BIOS нивелируют эти различия, предоставляя в распоряжение программы пользователя стандартный набор модулей для работы с устройствами ввода/вывода.
Не будет преувеличением сказать, что одна из причин такого невиданного успеха компьютера IBM PC на рынке персональных компьютеров - наличие хорошо продуманного стандартного интерфейса модулей BIOS и прикладных программ. Именно благодаря этому интерфейсу достигается почти стопроцентная совместимость по программному обеспечению компьютеров этого типа, выпускаемых разными фирмами.
В этом разделе книги мы расскажем о том, как прикладные программы, составленные для компьютера, совместимого с IBM PC, могут пользоваться модулями BIOS для работы со стандартной периферией, затем приведем краткий обзор основных модулей BIOS.
Напомним вам, что такое программные прерывания, так как именно они используются для вызова модулей BIOS.
В начале оперативной памяти персонального компьютера (в пределах первого килобайта) находится так называемая векторная таблица прерываний. Она состоит из 256 ячеек, хранящих адреса программ-обработчиков прерывания. Мы будем подробно изучать эту таблицу в главе 4, а сейчас вспомним машинную команду INT<n>.
По этой команде содержимое ячейки векторной таблицы прерываний с номером n помещается в адресные регистры процессора, причем в стеке запоминается текущее содержимое адресных регистров. Управление передается по адресу, записанному в ячейке таблицы. Программа-обработчик прерывания должна заканчиваться командой IRET, по которой из стека извлекается старое значение адресных регистров и управление передается обратно в программу, вызвавшую прерывание командой INT <n>.
Вообще говоря, процедура вызова и обработки программного прерывания похожа на процедуру вызова подпрограммы. Отличие заключается в том, что вызывающая программа "не знает" абсолютного адреса модуля обработки прерывания в памяти. Поэтому работа программ не зависит от адресов расположения модулей обработчиков прерывания.
Для вызова модуля BIOS программа использует команду INT <n> с соответствующим номером n. Программа передает параметры модулям BIOS через регистры процессора, результат работы модуля возвращается также в регистрах.
Не все номера прерываний n используются BIOS. Часть из них предназначена для аппаратных прерываний от устройств ввода/вывода, часть зарезервирована для DOS, часть - для программ пользователя.
Подробно прерывания BIOS будут изучаться во втором томе, так как описание этих прерываний трудно отделить от описания особенностей аппаратуры. Приводимый здесь обзор предназначен в основном для иллюстрации основных возможностей BIOS по обслуживанию периферии, поэтому в обзор вошли не все прерывания BIOS, а только самые важные. Остальные прерывания будут изучаться по мере необходимости при изложении соответствующего материала.
Обзор прерываний DOS
DOS предоставляет программе набор системных вызовов, реализованных с использованием механизма программных прерываний. Эти вызовы открывают прикладной программе доступ к системной информации, к системе консольного ввода/вывода, файловой системе, к подсистеме управления программами и памятью, позволяют организовать обращение к драйверам устройств ввода/вывода и т.д.
Все основные функции DOS вызываются с помощью прерывания INT 21h, однако DOS использует и другие прерывания:
INT 20h | завершение работы программы; |
INT 25h/26h | чтение/запись на диск с абсолютной адресацией секторов; |
INT 27h | завершение работы программы с оставлением ее резидентной в памяти; |
INT 28h | прерывание зарезервировано для DOS, может быть использовано для составления резидентных программ; |
INT 2Eh | выполнение команды DOS; |
INT 2Fh | прерывание мультиплексора, используется для спулера печати PRINT.COM. |
Функции прерывания INT 21h можно разделить на следующие группы:
получение системной информации;
символьный ввод/вывод;
работа с файловой системой;
управление программами;
управление памятью;
связь с драйверами устройств;
прочий системный сервис.
В данном обзоре мы кратко рассмотрим эти группы, делая акцент в основном на составе функций. Полностью информация об использовании функций прерывания DOS INT 21h (и других прерываний DOS) будет приводиться в соответствующих разделах книги.
Номер функции задается при вызове прерывания INT 21h в регистре AH.