Основные компоненты операционной системы
Дистрибутив операционной системы MS-DOS состоит, в зависимости от версии, из одной или нескольких дискет. На них расположены файлы собственно операционной системы IO.SYS, MSDOS.SYS, командный процессор COMMAND.COM, файлы внешних команд операционной системы (FORMAT, FDISK и т.п.), драйверы и другие файлы.
Файл IO.SYS содержит расширение базовой системы ввода/вывода и является интерфейсом между операционной системой и BIOS. Расширение используется операционной системой для взаимодействия с аппаратурой компьютера и BIOS.
Файл MSDOS.SYS является в некотором смысле набором программ обработки прерываний, в частности прерывания INT21H. Это тело операционной системы.
Командный процессор COMMAND.COM предназначен для организации диалога с оператором. Он анализирует вводимые оператором команды и организует их выполнение. Так называемые внутренние команды (DIR, COPY и т.д.) обрабатываются именно командным процессором. Программист имеет возможность написать свой собственный командный процессор и подключить его вместо стандартного. Новый командный процессор должен выполнять все функции, которые раньше выполнял стандартный COMMAND.COM.
Драйверы (обычно это файлы, имеющие расширение имени .SYS) представляют собой программы, обслуживающие различную аппаратуру. Эти программы имеют специальный формат и будут подробно описаны в книге. Применение драйверов легко решает проблемы использования новой аппаратуры - достаточно написать для нового устройства драйвер и подключить его к операционной системе. Прикладные программы взаимодействуют с устройствами через драйвер, поэтому они не будут меняться при изменениях в аппаратуре. Например, новое дисковое устройство может иметь другое количество дорожек и секторов, другие управляющие команды. Все это учитывается драйвером, а прикладная программа будет работать с новым диском как и раньше, используя прерывания MS-DOS.
Файлы внешних команд операционной системы содержат программы-утилиты для выполнения разнообразных операций, таких как форматирование дисков, сортировка файлов, печати текстов и других.
Немного об утилитах, предназначенных для подготовки дисков и дискет.
Файлы операционной системы выделяются своим особым расположением на диске (кроме COMMAND.COM) - эти файлы должны находиться в специально отведенном для них месте. Если вам нужно подготовить дискету как системную (т.е. такую, с которой можно загружать операционную систему), для переноса файлов операционной системы следует использовать специальные утилиты.
Самый простой способ подготовки системной дискеты - использовать команду FORMAT с опцией /S, например:
FORMAT A: /S
В этом случае после форматирования на дискету будут перенесены файлы операционной системы. При использовании команды текущим должен быть корневой каталог системного диска, например, диска С:.
Если вы собираетесь обновить версию операционной системы (например, вместо MS-DOS 3.30 установить MS-DOS 4.01), не обязательно заново переформатировать весь диск. Загрузив новую версию с дискеты, для переноса новых системных файлов используйте команду SYS:
SYS C:
Файл COMMAND.COM скопируйте обычным способом.
Если вам надо сделать дискету системной, а ее форматирование нежелательно (дискета содержит важную информацию), воспользуйтесь программой Norton Disk Doctor или аналогичной. Программа освободит место в начале диска для операционной системы, переписав располагавшиеся там данные на свободное место в конце дискеты, затем запишет системные файлы и даже скопирует файл COMMAND.COM.
Утилита FDISK предназначена для подготовки к работе жесткого диска. Она разбивает диск на участки, называемые разделами.
На одном физическом диске могут быть разделы, принадлежащие разным операционным системам. Один из разделов - активный, операционная система загружается из активного раздела.
Для MS-DOS утилита FDISK позволяет создать первичный и вторичный разделы. В первичном разделе располагается системный диск C:, с которого выполняется загрузка операционной системы, вторичный раздел может быть разделен на логические диски (D:, E:, F: и т.д.). Диски, располагающиеся во вторичном разделе, не могут быть системными.
Заметим, что только MS-DOS версии 4.01, 5.0 и Compaq DOS 3. 31 позволяют создавать логические диски размером более 32 мегабайт. Это связано с тем, что другие версии DOS используют 16-разрядную адресацию секторов диска, что недостаточно для дисков больших размеров.
Очень часто вместо утилиты FDISK для подготовки жесткого диска используются диск-менеджеры. Это такие программы, как Advanced Disk Manager, Speed Stor и т.д. Используя свои собственные форматы разделов и таблиц разделов (и свои драйверы дисковых устройств), диск-менеджеры предоставляют такие дополнительные возможности, как защита логического диска от записи или организация парольной защиты данных на диске, создание логических дисков размером более 32 мегабайт.
Однако не всегда применение диск-менеджеров может привести к желаемому результату. Защита от несанкционированного доступа часто легко преодолевается, мощные ситемы защиты сильно привязаны к конкретной версии операционной системы (например, WatchDog, очень мощная система защиты, требует только DOS версии 3.20).
Кроме того, драйверы, используемые диск-менеджерами могут замедлить работу дисковой подсистемы компьютера.
Некоторые программы, особенно защищенные от копирования, отказываются работать на диске, подготовленном не утилитой FDISK. Причины этого мы увидим, когда будем обсуждать проблемы защиты программ от несанкционированного копирования.
Перейдем к процедуре начальной загрузки операционной системы.
Особенности обработки аппаратных прерываний
Аппаратные прерывания вырабатываются устройствами компьютера, когда возникает необходимость их обслуживания. Например, по прерыванию таймера соответствующий обработчик прерывания увеличивает содержимое ячеек памяти, используемых для хранения времени. В отличие от программных прерываний, вызываемых запланировано самой прикладной программой, аппаратные прерывания всегда происходят асинхронно по отношению к выполняющимся программам. Кроме того, может возникнуть одновременно сразу несколько прерываний!
Для того, чтобы система "не растерялась", решая какое прерывание обслуживать в первую очередь, существует специальная схема приоритетов. Каждому прерыванию назначается свой уникальный приоритет. Если происходит одновременно несколько прерываний, то система отдает предпочтение самому высокоприоритетному, откладывая на время обработку остальных прерываний.
Система приоритетов реализована на двух микросхемах Intel 8259 (для машин класса XT - на одной такой микросхеме). Каждая микросхема обслуживает до восьми приоритетов. Микросхемы можно объединять (каскадировать) для увеличения количества уровней приоритетов в системе.
Уровни приоритетов обозначаются сокращенно IRQ0 - IRQ15 (для машин класса XT существуют только уровни IRQ0 - IRQ7).
Для машин XT приоритеты линейно зависели от номера уровня прерывания. IRQ0 соответствовало самому высокому приоритету, за ним шли IRQ1, IRQ2, IRQ3 и так далее. Уровень IRQ2 в машинах класса XT был зарезервирован для дальнейшего расширения системы и, начиная с машин класса AT, IRQ2 стал использоваться для каскадирования контроллеров прерывания 8259. Добавленные приоритетные уровни IRQ8 - IRQ15 в этих машинах располагаются по приоритету между IRQ1 и IRQ3.
Приведем таблицу аппаратных прерываний, расположенных в порядке приоритета:
Номер | Описание |
8 | IRQ0 - прерывание интервального таймера, возникает 18,2 раза в секунду. |
9 | IRQ1 - прерывание от клавиатуры. Генерируется при нажатии и при отжатии клавиши. Используется для чтения данных с клавиатуры. |
A | IRQ2 - используется для каскадирования аппаратных прерываний в машинах класса AT. |
70 | IRQ8 - прерывание от часов реального времени. |
71 | IRQ9 - прерывание от контроллера EGA. |
72 | IRQ10 - зарезервировано. |
73 | IRQ11 - зарезервировано. |
74 | IRQ12 - зарезервировано. |
75 | IRQ13 - прерывание от математического сопроцессора. |
76 | IRQ14 - прерывание от контроллера жесткого диска. |
77 | IRQ15 - зарезервировано. |
B | IRQ3 - прерывание асинхронного порта COM2. |
C | IRQ4 - прерывание асинхронного порта COM1. |
D | IRQ5 - прерывание от контроллера жесткого диска для XT. |
E | IRQ6 - прерывание генерируется контроллером флоппи-диска после завершения операции. |
F | IRQ7 - прерывание принтера. Генерируется принтером, когда он готов к выполнению очередной операции. Многие адаптеры принтера не используют это прерывание. |
#include <stdio.h> #include <stdlib.h> #include <conio.h>
void main(void);
void main(void) {
outp(0x21,0); printf("\nПрерывания от флоппи-диска разрешены.\n");
exit(0); }
Заметьте, что мы только что замаскировали прерывание именно от флоппи-диска, все остальные устройства продолжали нормально работать. Если бы мы выдали машинную команду CLI, то отключились бы все аппаратные прерывания. Это привело бы, например, к тому, что клавиатура была бы заблокирована.
Еще одно замечание, касающееся обработки аппаратных прерываний. Если вы полностью заменяете стандартный обработчик аппаратного прерывания, не забудьте в конце программы выдать байт 20h в порт с адресом 20h (A0h для второго контроллера 8259). Эти действия необходимы для очистки регистра обслуживания прерывания ISR. При этом разрешается обработка прерываний с более низким приоритетом чем то, которое только что обрабатывалось.
Если вы обрабатываете прерывание 1Ch, то добавка в конце программы не нужна, так как это прерывание является расширением другого прерывания (прерывания таймера).
Перед тем, как завершить изучение прерываний, зададимся вопросом - можно ли замаскировать немаскируемое прерывание? Оказывается можно!
Конечно, если сигнал прерывания пришел на вход немаскируемого прерывания процессора, ничего сделать нельзя - прерывание произойдет неизбежно. Но в компьютерах XT и AT предусмотрены схемы, блокирующие вход немаскируемого прерывания процессора NMI.
Для XT маскированием немаскируемого прерывания управляет порт с адресом 0A0h. Если записать в него 0, немаскируемое прерывание будет запрещено, если 80h - разрешено.
Аналогично для AT маскированием немаскируемого прерывания управляет бит 7 порта 70h. Запись байта 0ADh в порт 70h запретит немаскируемое прерывание, а байта 2Dh - разрешит прохождение прерывания.
Заметим, что мы не запрещаем немаскируемое прерывание "внутри" процессора - это невозможно по определению, мы "не пускаем" сигнал прерывания на вход NMI.
Особенности отладки драйверов
Драйверы достаточно сложны для отладки. Это связано прежде всего с тем, что Вы не сможете использовать такие отладчики, как CodeView. На этапе инициализации драйвера (при выполнении команды инициализации) загрузка операционной системы еще не завершена, и воспользоваться обычным отладчиком невозможно.
Прикладная программа также не вызывает драйвер напрямую, а делает это через прерывания DOS. Отладчик CodeView не позволит Вам трассировать прерывание 21h, даже если Вы и сможете это сделать при помощи другого отладчика (например, отладчик Advanced Fullscreen Debugger фирмы IBM позволяет трассировать операционную систему), Вам придется очень долго "добираться" до программы прерывания Вашего драйвера.
Малейшие ошибки в программе инициализации могут привести к невозможности завершения загрузки операционной системы. В этом случае Вам придется загрузиться с дискеты и удалить строку, описывающую драйвер из файла CONFIG.SYS, затем повторить загрузку с диска.
Можно использовать специально подготовленную системную дискету, записать на нее отлаживаемый драйвер и загрузить операционную систему с дискеты. Если произойдет зависание системы, загрузите DOS с жесткого диска.
Можно порекомендовать следующую методику отладки драйвера.
Программа стратегии обычно очень проста и проблем не вызывает.
Для отладки программы инициализации можно подготовить специальные процедуры, отображающие на экране содержимое наиболее важных переменных и областей памяти. Такие же процедуры можно использовать и для отладки других частей драйвера.
В качестве примера приведем текст процедуры, выводящей на экран содержимое всех регистров процессора. После вывода программа ожидает нажатия любой клавиши.
Текст этой процедуры следует поместить в ту часть драйвера, которая останется резидентной, тогда Вы сможете вызывать ее не только при инициализации, но и при выполнении других команд.
;========================================== ; Процедура выводит на экран содержимое ; всех регистров и ожидает нажатия на ; любую клавишу. ; После возвращения из процедуры ; все регистры восстанавливаются.
ntrace proc near
; Сохраняем в стеке регистры, ; содержимое которых будет изменяться
pushf push ax push bx push cx push dx push ds push bp
push cs pop ds
mov bp,sp
; Выводим сообщение об останове
mov dx,offset cs:trace_msg @@out_str
; Выводим содержимое всех регистров
mov ax,cs ; cs call Print_word @@out_ch ':' mov ax,[bp]+14 ; ip call Print_word
@@out_ch 13,10,13,10,'A','X','=' mov ax,[bp]+10 call Print_word
@@out_ch ' ','B','X','=' mov ax,[bp]+8 call Print_word
@@out_ch ' ','C','X','=' mov ax,[bp]+6 call Print_word
@@out_ch ' ','D','X','=' mov ax,[bp]+4 call Print_word
@@out_ch ' ','S','P','=' mov ax,bp add ax,16 call Print_word
@@out_ch ' ','B','P','=' mov ax,[bp] call Print_word
@@out_ch ' ','S','I','=' mov ax,si call Print_word
@@out_ch ' ','D','I','=' mov ax,di call Print_word
@@out_ch 13,10,'D','S','=' mov ax,[bp]+2 call Print_word
@@out_ch ' ','E','S','=' mov ax,es call Print_word
@@out_ch ' ','S','S','=' mov ax,ss call Print_word
@@out_ch ' ','F','=' mov ax,[bp]+12 call Print_word
lea dx,cs:hit_msg @@out_str
; Ожидаем нажатия на любую клавишу
mov ax,0 int 16h
; Восстанавливаем содержимое регистров
pop bp pop ds pop dx pop cx pop bx pop ax popf
ret
trace_msg db 13,10,'>---- BREAK ----> At address ','$' hit_msg db 13,10,'Hit any key...','$'
ntrace endp
;========================================== ; Процедура выводит на экран содержимое AX
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
end
Для вывода строки на экран в этой процедуре используется макро @@out_str и @@out_ch, которые определены в файле sysp.inc:
@@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
@@out_str MACRO mov ah,9 int 21h ENDM
Другой метод - построить макет драйвера в виде COM-программы. Используя свой любимый отладчик, Вы сможете проверить работу большинства входящих в драйвер модулей.
Если у Вас есть отладчик Advanced Fullscreen Debugger, можно использовать его способность оставаться резидентным и вызываться по нажатию комбинации клавиш CTRL+ESC.
В интересующее Вас место драйвера поместите вызов прерывания 16h, ожидающий ввода с клавиатуры, например:
push ax mov ax,0 int 16h pop ax
Можно сохранить в стеке и регистр флагов, если его изменение нежелательно.
После того, как драйвер повиснет на ожидании ввода, активизируйте отладчик, нажав комбинацию клавиш CTRL+ESC. Вы окажетесь в теле обработчика прерывания 16h. Выполняя программу по шагам, довольно скоро Вы достигнете выхода из этого обработчика - команды IRET. После выполнения команды IRET управление будет передано команде, следующей за командой int16h. Эта команда (в приведенном примере - pop ax) принадлежит Вашему драйверу!
Используя известное теперь значение адреса заголовка запроса (регистры ES:BX), можно определить, какая команда выполняется драйвером, и просмотреть сам запрос.
Выполнив все необходимые отладочные действия, запустите программу на выполнение без отладки. В нужный момент времени Вы снова сможете вызвать отладчик тем же способом.
Теоретически возможно создать такой отладчик, который сам оформлен в виде драйвера, и подключить его в файле CONFIG.SYS до проверяемого драйвера. Тогда весь процесс загрузки и инициализации будет производиться под контролем этого драйвера-отладчика.
Программа-драйвер использует системный стек, имеющий довольно небольшой размер. Поэтому при необходимости организуйте свой стек в области памяти, принадлежащей драйверу.
Очень важно, чтобы драйвер перед началом работы сохранил содержимое всех регистров, включая регистр флагов, а перед возвратом управления операционной системе восстановил старое содержимое регистров.
Критичные участки драйвера должны выполняться с замаскированными прерываниями.
Особенности резидентных программ
TSR-программы имеют некоторые особенности, отличающие их от "нормальных" программ.
Им не разрешается использовать DOS-прерывания, когда вздумается. Это связано с тем, что DOS проектировалась как однозадачная операционная система, поэтому модули DOS не обладают свойством реентерабельности (повторной входимости). Что это означает на практике?
Допустим, Ваша программа записывает длинный файл на диск. Во время записи вы (возможно, случайно) нажали клавишу, активизирующую TSR-программу записи содержимого экрана в файл.
Теперь доступа к диску требуют две программы - прикладная, записывающая длинный файл, и Ваша TSR-программа. Запись файла из прикладной программы приостановится, далее произойдет запись копии экрана в файл, после чего возобновится запись файла из прикладной программы. Все было бы хорошо, если бы прикладная программа и TSR-программа не использовали одни и те же внутренние области данных DOS для работы с диском. При запуске TSR-программа безвозвратно испортит текущее состояние служебных областей данных, которые прикладная программа использовала при записи на диск.
И таких примеров можно привести много. BIOS также далеко не весь реентерабельный. TSR-программа может смело использовать разве лишь прерывание 16h для работы с клавиатурой, которое реентерабельно. Для вывода на экран лучше всего использовать непосредственную запись символов в видеопамять дисплейного адаптера.
Не стоит пользоваться многими функциями библиотеки QuickC, так как они могут вызывать прерывания DOS. Например, функция malloc() вызывает прерывание DOS для определения размера свободной памяти в системе.
Могут возникнуть трудности с использованием арифметических действий с числами в формате плавающей запятой, так как функция _dos_keep() при завершении программы восстанавливает прерывания, использовавшиеся для эмуляции арифметики с плавающей запятой и для работы с арифметическим сопроцессором.
Некоторые из перечисленных проблем (те, что связаны с использованием прерываний DOS) можно решить с помощью недокументированного прерывания INT 28h.
Это прерывание вызывается DOS при ожидании ввода с клавиатуры. В этот момент вы можете использовать любое прерывание DOS, кроме функций от 00h до 0Сh прерывания INT 21h. Утилита спулинга печати PRINT.COM использует это прерывание.
Можно рекомендовать следующий способ использования прерывания 28h. Обработчик прерывания 9 отслеживает нажатие клавиши, которая должна активизировать TSR-программу. Обнаружив эту клавишу (или комбинацию клавиш), обработчик прерывания 9 устанавливает флаг запроса на активизацию TSR-программы и завершает свою работу обычным способом.
Ваша TSR-программа должна создать свой обработчик прерывания 28h и сцепить его со стандартным. Каждый раз, когда DOS ожидает ввода с клавиатуры (в этот момент DOS не использует сама свои прерывания), вызывается прерывание 28h. Ваш обработчик проверяет флаг активизации, устанавливаемый обработчиком прерывания 9, и если флаг установлен, TSR-программа активизируется и может пользоваться услугами DOS, в частности, файловой системой.
Разумеется, после выполнения всех необходимых действий TSR-программа должна сбросить флаг активизации.
Можно также вместе с прерыванием 28 использовать аппаратное прерывание таймера с номером 8. В этом случае надо проверять не только флаг активизации, но и так называемый флаг критической секции DOS. Это байт, адрес которого возвращает недокументированная функция 34h прерывания DOS 21h в регистрах ES:BX. Если этот байт равен 0, то DOS не использует свои прерывания и наступил подходящий момент для активизации TSR-программы.
TSR-программа может вступить в конфликт с другими TSR-программами или прикладным обеспечением, если будет использовать занятые ими номера прерываний. Известен случай, когда драйвер клавиатуры и экрана (SDRIVER) вызывал зависания программных продуктов фирмы Microsoft из-за того, что эти продукты выдавали 16h прерывание с содержимым регистра AX равным 5500h, 5501h и т.д. Обработчик 16h-го прерывания программы SDRIVER при этом зацикливался из-за ошибки в программе (или точнее, из-за плохой реакции на такого рода номера функций прерывания 16h).
Очень важно не допустить случайного повторного запуска TSR-программы, так как повторное переназначение векторов прерываний почти наверняка приведет систему к краху.
Для проверки наличия TSR-программы в памяти обычно используют прерывание мультиплексора 2Fh, специально предназначенное для организации элементов мультизадачности в DOS. Это прерывание используется спулером печати PRINT.COM (об этом мы будем говорить при описании работы с принтером).
TSR-программа может переназначить это прерывание на себя и сделать так, чтобы оно "откликалось" на какой-либо код функции, зарезервированный для прикладных программ. Можно использовать коды C0h...FFh. При запуске TSR-программа вызывает прерывание 2Fh с выбранным кодом и проверяет ответ (передаваемый, например, в регистре AX). Если прерывание отвечает тем кодом, который задан в TSR-программе, это означает, что копия программы уже есть в памяти и повторная установка недопустима.
Приведенные ниже примеры проиллюстрируют все эти особенности.
Перезагрузка операционной системы.
Вызов прикладной программой прерывания INT 19h приведет к перезагрузке операционной системы.
Получение системной информации.
Функция 30h возвращает в регистре AX номер версии DOS. Например, для версии MS-DOS 5.00 содержимое регистра AH равно 00, регистра AL - 05.
Дополнительно через регистр BH функция возвращает программе серийный номер фирмы-производителя ОЕМ (IBM - 00, DEC - 16h, 0FFh - Microsoft и т.п.), а в регистрах BL:CX после вызова функции находится серийный номер пользователя.
Эта информация может применяться для анализа возможности использования таких средств операционной системы, которые поддерживаются не всеми версиями DOS, или для настройки программы на конкретный серийный номер пользователя.
Функции 2Ah и 2Ch позволяют программе узнать системную дату и время.
Есть функции, возвращающие текущий диск и текущий каталог. Номера этих функций - 19h и 47h.
Функция 2Fh позволяет программе узнать адрес текущей области DTA (Disk Transfer Area). Эта область используется, например, при поиске файлов в каталоге.
Важная информация находится в блоке PSP (Programm Segment Prefix). Этот блок располагается в памяти непосредственно перед выполняющейся программой. В нем находятся, в частности, параметры, передаваемые программе при запуске. Функция 62h возвращает адрес текущего блока PSP.
Кратко перечислим некоторые другие функции для получения системной информации:
35h | получить значение вектора прерывания с заданным номером; |
4Dh | узнать код завершения процесса; |
59h | получить расширенный код ошибки; |
54h | узнать, используется ли проверка при записи на диск; |
33h | узнать, используется ли проверка на CTRL-BREAK. |
- Получить положение курсора.
Программа может узнать в любое время, где расположен курсор. Это может потребоваться ей, например, для того, чтобы переместить курсор в следующую позицию (вправо, вверх, вниз, на 10 символов левее текущего положения и т.д.).
- Получить положение светового пера.
Световое перо используется относительно редко, однако если оно есть, то функция 04h позволит вам работать с этим устройством.
- Получить состояние дисковой системы.
Эта функция позволяет проверить результат выполнения предыдущей операции. Если операция завершилась аварийно, при помощи этой функции можно определить код ошибки.
В настоящее время можно найти
В настоящее время можно найти уже довольно много книг, посвященных операционной системе MS-DOS. Большинство из них, однако, ограничиваются описанием MS-DOS на уровне пользователя или, в крайнем случае, на уровне прикладного программиста, не затрагивая деталей и тонкостей работы самой операционной системы. Этот подход, безусловно, правомерен и оправдан - пользователей ПЭВМ гораздо больше, чем системных программистов, а более глубокая информация предоставляется в руководствах, поставляемых фирмами и специальных заказных пособиях.
И все же потребность в, казалось бы, специальной информации у нас огромна. Наш программист часто оказывается в очень сложной ситуации: не имея доступа к зарубежной оригинальной литературе, он вынужден разрабатывать специальное программное обеспечение, написание которого требует глубокого знания операционной системы MS-DOS и аппаратных особенностей IBM PC. Можно с уверенностью сказать, что в условиях информационного голода, отсутствия фирменного технического обслуживания и необходимости обеспечения работы самой экзотической аппаратуры каждый прикладной программист вынужден быть немножко системным программистом, знать и уметь больше, чем его западный коллега.
Как известно, спрос рождает предложение, и уже сейчас появились книги под названием "Системное программирование в MS-DOS", в которых описываются, как правило, прерывания MS-DOS и приводятся примеры работы с ними. Такой подход нам кажется полезным только на начальном этапе изучения программирования в MS-DOS.
При написании настоящей книги авторы предполагали, что с прерываниями DOS и BIOS вы уже знакомы достаточно хорошо, либо способны разобраться самостоятельно - для этого существуют как специальные справочные системы типа TechHelp (которая сейчас имеется и на русском языке) или NortonGuide, так и контекстные справочники, являющиеся элементом интегрированных сред программирования (например, QuickAdvisor корпорации Microsoft или справочная система Thelp фирмы Borland International).
Авторы настоящего руководства стремились прежде всего осветить те вопросы, которые практически невозможно найти нигде, кроме как в документации, поставляемой фирмами. В соответствии с этим общеизвестные вещи изложены более кратко. В книге описана, например, структура управляющих блоков MS-DOS, показано, как написать собственный драйвер устройства или правильно работающую резидентную программу. Книга рассчитана на хорошего прикладного программиста, имеющего в своем распоряжении широко распространенную справочную информацию по MS-DOS, который, однако, пришел к необходимости еще более углубить свои знания.
Предполагается, что у вас имеется в распоряжении компьютер, на котором вы в процессе изучения книги можете опробовать приводимые нами примеры и проводить свои собственные исследования.
Все программы транслировались в среде Microsoft Quick C версий 2.01 и 2.5. На прилагаемой дискете находятся исходные тексты программ и некоторые утилиты, описанные в настоящем руководстве. Примеры составлены так, чтобы вы могли без значительных переделок использовать их в своих разработках.
Для тех, кто уже исчерпал документированные особенности MS-DOS, приводятся сведения о наиболее полезных недокументированных прерываниях и управляющих блоках MS-DOS. Изучение недокументированных прерываний и структур данных позволит вам глубже понять внутреннее устройство операционной системы, извлечь такую информацию о состоянии системы, которую трудно, если вообще возможно, получить "законным" способом.
Объем информации настолько огромен, что наше руководство разделено на несколько частей.
Первый том серии содержит сведения об операционной системе MS-DOS, прерываниях, драйверах, резидентных программах, файловой системе MS-DOS. Основное внимание при этом уделяется не таким общеизвестным вещам, как, например, запись/чтение файлов (хотя об этом тоже будет рассказано), а скорее описанию того, как MS-DOS выполняет эти операции, какие внутренние структуры данных она при этом использует и каким образом происходит взаимодействие DOS и прикладной программы пользователя.
На основе анализа управляющих блоков DOS вы сможете проанализировать состояние операционной системы, определить конфигурацию логических дисковых устройств, получить доступ к загружаемым (устанавливаемым) драйверам, в том числе к резидентным драйверам операционной системы. Вы научитесь переопределять прерывания DOS, составлять "выскакивающие" ("POP-UP") программы, которые, оставаясь резидентными в памяти после запуска, "оживают" при нажатии на клавишу, определенную заранее. Знание внутренней "кухни" файловой системы позволит вам при необходимости выполнять все файловые операции самостоятельно, без помощи DOS, пользуясь только прерыванием BIOS INT 13H для непосредственной работы с диском (такая работа в обход DOS может потребоваться, например, для организации защиты данных от несанкционированного доступа).
Второй том посвящен в большей степени аппаратному обеспечению компьютера. Здесь описаны клавиатура, мышь, таймер, контроллеры прерываний и прямого доступа к памяти, принтер, расширенная и дополнительная память. Приведены сведения о внутреннем устройстве клавиатуры, портах, средствах DOS и BIOS для работы с клавиатурой - т.е. все, что необходимо для полного использования возможностей клавиатуры. Для мыши приводится большое число примеров программ, которые вы можете использовать в своих разработках.
Подробно описаны порты принтера, средства DOS и BIOS для работы с принтером, а также система принтерного спулинга (печать в фоновом режиме). Рассматриваются вопросы русификации принтеров и управления принтером, поддерживающим протокол фирмы Epson.
Третий том - это описание дисплейных адаптеров. Рассмотрены адаптеры CGA, EGA и VGA. Описаны видеорежимы, средства DOS и BIOS для управления адаптерами и вывода текстовой и графической информации, способы загрузки знакогенератора адаптера EGA, а также описываются основные возможности графической библиотеки Microsoft Quick C 2.5.
Приведены рекомендации по отладке программ с использованием этих средств, по отладке программ специального типа (например, драйверов), а также основные приемы, используемые для защиты программ от отладки.
Как пользоваться книгой?
Можно просто читать ее, изучая приводимые примеры программ и тут же проверяя их работу на компьютере. Все сведения излагаются последовательно, поэтому вы сразу можете садиться за компьютер и начинать работать. Если вас интересует что-то конкретное, например, драйверы, вы можете начать сразу с соответствующей главы.
При необходимости книгами серии можно пользоваться как справочниками: все наиболее полезные таблицы вынесены в приложения.
Прилагаемая к книге дискета содержит не только исходные тексты примеров программ, но и готовые библиотеки объектных модулей для всех моделей памяти, а также справочную базу данных по этим модулям. Базу данных можно подключить к справочной базе интегрированной среды Quick C или использовать отдельно при помощи утилиты Microsoft Quick Help QH.EXE.
Все необходимые сведения об использовании содержимого дискеты приведены в файле README.DOC.
Каков начальный уровень знаний, необходимых для работы с серией? Предполагается, что вы свободно владеете языками ассемблера и Си (хотя в некоторых случаях приводятся необходимые пояснения), умеете пользоваться стандартными прерываниями BIOS и DOS, знакомы в целом с архитектурой компьютера и имеете некоторый опыт составления программ.
Первый раздел книги напомнит вам о составе операционной системы, процессе ее загрузки и об общей схеме работы. Раздел также содержит обзор прерываний DOS и BIOS, сведения о механизме обработки ошибок. Если вы владеете этим материалом, можете пропустить первый раздел.
Для работы вы можете использовать любой совместимый с IBM PC/XT/AT компьютер с любым дисплейным адаптером. Однако при изучении дисплейных адаптеров EGA и VGA вам будет нужен соответственно адаптер EGA или VGA. Желательно, чтобы компьютер был оснащен жестким диском (на машине, например, ЕС-1840, вам будет очень трудно работать). Наличие жесткого диска обязательно для изучения глав, посвященных файловой системе. Для изучения мыши вам следует приобрести это устройство.
Все программы, приведенные в книге, подготовлены для Microsoft Quick C или Microsoft C 6.0. Не исключено, что вы сможете использовать Turbo-C фирмы Borland, если приведете программы в соответствие со стандартами Turbo-C.
Программы, составленные на языке Ассемблера, транслировались при помощи программы Quick Assembler, входящей в состав интегрированной среды Quick C 2.01. Возможно также использование ассемблера MASM версии 5.0 или более поздней версии, программы Turbo-Assembler фирмы Borland с учетом приведенных выше замечаний.
Еще одно замечание, перед тем, как вы начнете изучение книги по системному программированию.
Системное программирование - это, исходя из названия, программирование системных задач, создание операционных систем или отдельных компонент операционных систем, таких, например, как драйверы внешних устройств.
В отличие от прикладного программиста, системный программист должен в одинаковой степени владеть и программным, и аппаратным обеспечением компьютера. Если прикладной программист, как правило, не работает напрямую с аппаратурой, пользуясь сервисом операционной системы, то одна из основных задач системного программиста - организация обслуживания устройств ввода/вывода.
Поэтому книга содержит в себе две равноценные по объему части, посвященные операционной системе и аппаратному обеспечению компьютера.
Однако при создании драйвера устройства вам следует ориентироваться прежде всего на фирменное техническое описание устройства - только там приводятся все технические подробности, без учета которых Ваш драйвер не будет правильно работать.
Очень осторожно следует использовать недокументированные средства операционной системы, может получиться так, что в другой версии операционной системы Ваша отлаженная программа будет делать совсем не то, для чего она предназначена.
Префикс программного сегмента
Теперь займемся вплотную префиксом программного сегмента PSP. Формат PSP уже был описан ранее, для удобства приведем его еще раз вместе со структурой из файла sysp.h:
(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 | неформатированная область параметров, заполняется при запуске программы из командной строки |
#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()
Программы могут получить из PSP такую информацию, как параметры командной строки при запуске, размер доступной памяти, найти сегмент области переменных среды и т.д.
Как программе узнать адрес своего PSP? Очень просто сделать это для программ, написанных на языке ассемблера: при запуске программы этот адрес передается ей через регистры DS и ES. То есть этот адрес равен DS:0000 или ES:0000 (для COM-программ на PSP указывают также регистры CS и SS).
Для программ, составленных на языке Си, доступна глобальная переменная _psp типа unsigned. Эта переменная содержит сегментный адрес PSP.
В качестве примера приведем текст программы на языке ассемблера, которая выводит на экран передаваемые ей через PSP параметры запуска:
.MODEL tiny DOSSEG
.STACK 100h
.DATA
parm_msg DB "Укажите параметры", 13, 10, "$"
.CODE .STARTUP
mov cl,ds:80h ; количество символов ; в командной строке cmp cl,0 je ask_parm ; нет параметров - просим ; задать параметры
mov si,81h ; со смещением 81h ; начинается область ; параметров cld
get_parm:
lods BYTE PTR es:[si] ; загружаем в al ; очередной ; символ строки ; параметров
mov ah,2 ; выводим его на экран mov dl,al int 21h
loop get_parm jmp end_progr
ask_parm:
mov ah, 9h mov dx, OFFSET parm_msg int 21h
end_progr: .EXIT 0
END
Приведенная ниже программа, составленная на языке Си, определяет адрес своего PSP, затем показывает содержимое некоторых полей из PSP:
#include <stdio.h> #include <stdlib.h> #include <dos.h> #include "sysp.h"
void main(void);
void main(void) {
PSP far *psp_ptr;
psp_ptr = FP_MAKE(_psp,0); // Конструируем указатель // на PSP printf("PSP расположено по адресу: %Fp\n" "Доступно памяти, байт: %ld\n" "PSP родительской программы: %Fp\n" "\n", psp_ptr, (long)(psp_ptr->mem_top)*16L, FP_MAKE(psp_ptr->parn_psp,0)); exit(0); }
Используя поле parn_psp, можно определить адрес PSP родительской программы, то есть программы, запустившей Вашу программу.
Немного о назначении полей term_adr, cbrk_adr, crit_err.
Поле term_adr содержит значение, полученное из таблицы векторов прерываний для вектора 22h. Это адрес программы, которая получает управление, когда текущая программа завершает свою работу. Это может быть, например, COMMAND.COM. Программа может создать свою собственную подпрограмму, которая будет получать управление при завершении работы основной программы. Она может записать свой собственный адрес в вектор 22h, затем запустить другую программу. В таком случае в запущенной программе это поле в ее PSP будет содержать адрес родительской программы. Когда основная программа завершает свою работу, DOS восстанавливает адрес программы завершения в векторе 22h из поля term_adr PSP.
Поле cbrk_adr содержит адрес программы обработки прерывания по нажатию Ctrl-Break из вектора 23h таблицы векторов прерываний. Так как программа может устанавливать свою собственную программу обработки прерывания по Ctrl-Break, DOS при завершении работы программы восстанавливает оригинальное значение из поля cbrk_adr.
Аналогично поле crit_err предназначено для восстановления содержимого вектора 24h - адреса обработчика критических ошибок.
Способы переназначения векторов будут приведены в разделе, посвященном прерываниям.
Конечно, программы, составленные на языке Си, не обязательно должны использовать PSP для доступа к параметрам командной строки и переменным среды. Для этого есть параметры функции main и набор функций типа getenv, putenv и т.п., предназначенных для работы со средой. Но ведь PSP содержит и другую информацию!
Пример драйвера блочного устройства
Приведем пример драйвера "электронного" диска, расположенного в основной (не расширенной или дополнительной) памяти компьютера. Этот драйвер предназначен, разумеется, не для замены поставляющегося стандартно RAMDRIVE.SYS, однако на его примере можно увидеть, как устроены драйверы блочных устройств.
И если когда-нибудь Вам потребуется использовать диски ЭВМ серии ЕС в качестве винчестера персонального компьютера, то разобравшись в том, как работает приведенный ниже драйвер, Вы сможете самостоятельно приспособить его для такой задачи.
; ; Драйвер электронного диска, ; использует основную память компьютера ; .MODEL tiny .CODE ; Драйвер состоит из одного ; сегмента кода
org 0 ; Эта строка может отсутствовать
include sysp.inc
ram PROC far
;=======================================================
; Заголовок драйвера
dd 0ffffffffh ;адрес следующего драйвера dw 2000h ;байт атрибутов dw dev_strategy ;адрес процедуры стратегии dw dev_interrupt ;адрес процедуры прерывания db 1 db 7 dup(?)
; Блок BPB для электронного диска
bpb equ $
dw 512 ; количество байтов в секторе db 1 ; количество секторов в кластере dw 1 ; количество зарезервированных секторов db 2 ; количество копий FAT dw 64 ; макс. количество файлов в корневом каталоге dw 360 ; общее количество секторов db 0fch ; описатель среды носителя данных dw 2 ; количество секторов на одну копию FAT
bpb_ptr dw bpb ; указатель на блок BPB
; Область локальных переменных драйвера
total dw ? ; количество секторов verify db 0 ; флаг проверки при записи start_sec dw 0 ; номер начального сектора vdisk_ptr dw 0 ; сегмент начала участка памяти, ; в котором расположен диск
user_dta dw ? ; адрес области передачи данных dw ?
; Образец записи BOOT для инициализации ; первого сектора диска
boot_rec equ $
db 3 dup(0) db 'MSDOS4.0' dw 512 db 1 dw 1 db 2 dw 64 dw 360 db 0fch dw 2
;========================================================
; Программа стратегии
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
; Выход из драйвера, если функция не поддерживается
ioctl_in: nondestruct_in: input_status: clear_input: output_status: clear_output: ioctl_out: Removable_media: Device_open: Device_close:
or es:word ptr [bx]+3,8103h jmp quit
;=======================================================
; Построение блока BPB
make_bpb:
push es push bx
mov cs:WORD PTR start_sec,0 mov cs:WORD PTR total,1 call calc_adr
push cs pop es
lea di,bpb add si,11 mov cx,13 rep movsb
pop bx pop es
lea dx,bpb mov es:18[bx],dx mov es:20[bx],cs
jmp quit
check_media:
; Проверка смены носителя данных. ; Носитель не менялся.
mov es:BYTE PTR 14[bx],1 jmp quit
; Обработчик команды вывода данных
output_verify:
; Для вывода с проверкой устанавливаем флаг проверки
mov cs:BYTE PTR verify,1
output_data:
call in_save mov ax,es:WORD PTR 20[bx] mov cs:start_sec,ax
mov ax,es:WORD PTR 18[bx] mov cs:total,ax
call sector_write
mov es,cs:req_seg mov bx,cs:req_off
cmp cs:BYTE PTR verify,0 jz no_verify
mov cs:BYTE PTR verify,0 jmp input_data
no_verify:
jmp quit
;=======================================================
; Обработчик команды ввода данных
input_data:
call in_save mov ax,es:WORD PTR 20[bx] mov cs:start_sec,ax
mov ax,es:WORD PTR 18[bx] mov cs:total,ax
call sector_read
mov es,cs:req_seg mov bx,cs:req_off
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,'¦ *RAM/DISK* (C)Frolov A., 1990 ¦' db 13,10,'++' db 13,10,0
;========================================================
; Сохранение адреса буфера и значения счетчика ; из области запроса в области локальных данных
in_save proc near
mov ax,es:WORD PTR 14[bx] mov cs:user_dta,ax
mov ax,es:WORD PTR 16[bx] mov cs:user_dta+2,ax
mov ax,es:WORD PTR 18[bx] xor ah,ah mov cs:total,ax
ret
in_save endp
; Процедура пересчитывает адрес сектора ; в адрес соответствующего этому сектору ; блока памяти. В регистре DS возвращается ; сегментный адрес этого блока, ; в CX - общее количество байт во всех секторах. ; Количество секторов задается в total, ; номер начального сектора - в start_sec
calc_adr proc near
mov ax,cs:start_sec mov cx,20h mul cx
mov dx,cs:vdisk_ptr add dx,ax mov ds,dx
xor si,si mov ax,cs:total mov cx,512 mul cx
or ax,ax jnz move_it
mov ax,0ffffh
move_it:
xchg cx,ax ret
calc_adr endp
; Чтение сектора из памяти виртуального диска
sector_read proc near
call calc_adr mov es,cs:user_dta+2 mov di,cs:user_dta
mov ax,di add ax,cx jnc read_copy mov ax,0ffffh sub ax,di mov cx,ax read_copy: rep movsb ret
sector_read endp
; Запись сектора в память виртуального диска
sector_write proc near
call calc_adr push ds pop es mov di,si mov ds,cs:user_dta+2 mov si,cs:user_dta
mov ax,si add ax,cx jnc write_copy mov ax,0ffffh sub ax,si mov cx,ax write_copy: rep movsb ret
sector_write endp
;========================================================
E_O_P: ;Метка конца программы
;========================================================
initialize:
push cs pop dx
lea ax,cs:vdisk ; начало памяти, в которой ; расположен диск mov cl,4 ror ax,cl add dx,ax mov cs:vdisk_ptr,dx
mov ax,2d00h ; размер памяти, отведенной ; для диска add dx,ax
; Записываем в область запроса адрес за ; концом области памяти, отведенной диску
mov es:word ptr [bx]+14,0 mov es:word ptr [bx]+16,dx
; Количество поддерживаемых логических дисков - 1
mov es:word ptr [bx]+13,1
; Возвращаем адрес построенного BPB
lea dx,bpb_ptr mov es:word ptr [bx]+18,dx mov es:word ptr [bx]+20,cs
; Инициализируем BOOT-сектор
mov es,cs:vdisk_ptr xor di,di lea si,boot_rec mov cx,24 rep movsb
; Обнуляем два сектора для FAT
mov cs:WORD PTR start_sec,1 mov cs:WORD PTR total,2 call calc_adr
push ds pop es mov di,si xor al,al rep stosb
; Подготавливаем первую копию FAT
mov ds:BYTE PTR [si],0fch mov ds:BYTE PTR 1[si],0ffh mov ds:BYTE PTR 2[si],0ffh
; Подготавливаем вторую копию FAT
push ds push si
mov cs:WORD PTR start_sec,3 mov cs:WORD PTR total,2 call calc_adr
push ds pop es mov di,si
pop si pop ds
rep movsb
; Записываем нули в сектора корневого каталога
mov cs:WORD PTR start_sec,5 mov cs:WORD PTR total,4 call calc_adr
xor al,al push ds pop es xor di,di rep stosb
; Выводим сообщение
mov ax,cs mov ds,ax mov si,offset hello call dpc
jmp quit
; Здесь начинается область данных, в которой ; расположен электронный диск. Эта область ; выровнена на границу параграфа.
ALIGN 16
vdisk equ $ ram ENDP END ram
Пример драйвера символьного устройства
Приведем пример драйвера символьного устройства, который Вы можете взять в качестве прототипа своей разработки. Этот драйвер выполняет следующие действия:
принимает и анализирует строку параметров из команды "DEVICE=" файла CONFIG.SYS, преобразует параметры из символьной формы в двоичную и проверяет их на корректность;
если параметры заданы неправильно, в процессе инициализации выводится сообщение, и драйвер не подключается к операционной системе;
драйвер переназначает одно прерывание, номер которого задается в строке параметров;
обработчик переназначенного прерывания моделирует выполнение функций ввода, вывода и выполняет обработку неправильной функции;
демонстрируется использование функций IOCTL и ввода/вывода, ввод данных драйвер производит с клавиатуры, вывод осуществляет на экран дисплея.
Приведем полный текст драйвера:
Примеры резидентных программ
Приведем несколько примеров TSR-программ.
Первая программа перехватывает прерывание 9 (аппаратное прерывание клавиатуры). Запустив эту программу из приглашения DOS, вы сможете убедиться в том, что прерывание от клавиатуры возникает не только тогда, когда вы нажимаете на клавишу, но и когда ее отпускаете.
#include <dos.h> #include <stdio.h> #include <stdlib.h>
// Выключаем проверку стека и указателей #pragma check_stack( off ) #pragma check_pointer( off )
// Макро для подачи звукового сигнала #define BEEP() _asm { \ _asm xor bx, bx \ _asm mov ax, 0E07h \ _asm int 10h \ }
// Указатель на старую функцию обработки // 9-го прерывания void (_interrupt _far *oldkey)( void );
// Объявление новой функции обработки // 9-го прерывания void _interrupt _far newkey( void );
void main(void); void main(void) {
unsigned size; // Размер резидентной части // TSR-программы
char _far *newstack; // Указатель на новый стек, // который будет использовать // TSR-программа
char _far *tsrbottm; // Указатель на конец // TSR-программы, используется // для определения размера // резидентной части
// Записываем адрес стека TSR-программы _asm mov WORD PTR newstack[0], sp _asm mov WORD PTR newstack[2], ss
FP_SEG(tsrbottm) = _psp; // Указатель конца FP_OFF(tsrbottm) = 0; // программы устанавливаем // на начало PSP
// Вычисляем размер программы в параграфах // Добавляем 1 параграф на случай // некратной параграфу длины size = ((newstack - tsrbottm) >> 4) + 1;
// Встраиваем свой обработчик прерывания 9, // запоминаем старый вектор прерывания 9 oldkey = _dos_getvect(0x9); _dos_setvect(0x9, newkey);
// Завершаем программу и остаемся в памяти _dos_keep(0, size); }
// Новый обработчик клавиатурного прерывания void _interrupt _far newkey() { BEEP(); // Выдаем звуковой сигнал
// Вызываем стандартный обработчик прерывания 9 _chain_intr( oldkey ); }
Следующая программа GRAB демонстрирует использование функций DOS в TSR-программах. Она содержит все элементы, необходимые "безопасным" резидентным программам.
Программа предназначена для копирования содержимого видеобуфера в файл. Запись в файл активизируется при нажатии комбинации клавиш Ctrl+PrtSc. После каждой записи имя файла изменяется.
В самом начале своей работы программа проверяет наличие своей копии в памяти, так как повторное переназначение векторов прерываний приведет систему к краху. Некоторые тонкости, связанные с программированием клавиатуры и видеоадаптера, с получением адреса видеобуфера, используются в программе без объяснения. Вся необходимая информация будет приведена позже, в главах, посвященных программированию клавиатуры и видеоадаптеров.
Итак, текст программы:
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include "sysp.h"
// Выключаем проверку стека и указателей #pragma check_stack( off ) #pragma check_pointer( off )
// Макро для подачи звукового сигнала #define BEEP() _asm { \ _asm xor bx, bx \ _asm mov ax, 0E07h \ _asm int 10h \ } // Указатели на старые обработчики прерываний void (_interrupt _far *old8)(void); // Таймер void (_interrupt _far *old9)(void); // Клавиатура void (_interrupt _far *old28)(void); // Занятость DOS void (_interrupt _far *old2f)(void); // Мультиплексор
// Новые обработчики прерываний void _interrupt _far new8(void); void _interrupt _far new9(void); void _interrupt _far new28(void); void _interrupt _far new2f(unsigned _es, unsigned _ds, unsigned _di, unsigned _si, unsigned _bp, unsigned _sp, unsigned _bx, unsigned _dx, unsigned _cx, unsigned _ax, unsigned _ip, unsigned _cs, unsigned flags);
int iniflag; // Флаг запроса на вывод экрана в файл int outflag; // Флаг начала вывода в файл int name_counter; // Номер текущего выводимого файла char _far *crit; // Адрес флага критической секции DOS
// ======================================= void main(void); void main(void) { union REGS inregs, outregs; struct SREGS segregs;
unsigned size; // Размер резидентной части // TSR-программы
// Вызываем прерывание мультиплексора с AX = FF00 // Если программа GRAB уже запускалась, то новый // обработчик прерывания мультиплексора вернет // в регистре AX значение 00FF. // Таким способом мы избегаем повторного изменения // содержимого векторной таблицы прерываний. inregs.x.ax = 0xff00; int86(0x2f, &inregs, &outregs);
if(outregs.x.ax == 0x00ff) { printf("\nПрограмма GRAB уже загружена\n"); hello(); exit(-1); }
// Выдаем инструкцию по работе с программой GRAB hello();
// Вычисляем размер программы в параграфах // Добавляем 1 параграф на случай // некратной параграфу длины size = (12000 >> 4) + 1;
// Устанавливаем начальные значения флагов outflag=iniflag=0;
// Сбрасываем счетчик файлов. Первый файл будет // иметь имя GRAB0.DOC. В дальнейшем этот счетчик // будет увеличивать свое значение на 1. name_counter=0;
// Получаем указатель на флаг критической секции DOS. // Когда этот флаг равен 0, TSR-программа может // пользоваться функциями DOS inregs.h.ah = 0x34; intdosx( &inregs, &outregs, &segregs ); crit=(char _far *)FP_MAKE(segregs.es,outregs.x.bx);
// Устанавливаем собственные обработчики прерываний. old9 = _dos_getvect(0x9); _dos_setvect(0x9, new9);
old8 = _dos_getvect(0x8); _dos_setvect(0x8, new8);
old28 = _dos_getvect(0x28); _dos_setvect(0x28, new28);
old2f = _dos_getvect(0x2f); _dos_setvect(0x2f, new2f);
// Завершаем программу и остаемся в памяти _dos_keep(0, size); }
// ======================================= // Новый обработчик прерывания мультиплексора. // Используется для предохранения программы // от повторного встраивания в систему как резидентной.
void _interrupt _far new2f(unsigned _es, unsigned _ds, unsigned _di, unsigned _si, unsigned _bp, unsigned _sp, unsigned _bx, unsigned _dx, unsigned _cx, unsigned _ax, unsigned _ip, unsigned _cs, unsigned flags) { // Если прерывание вызвано с содержимым // регистра AX, равным FF00, возвращаем // в регистре AX значение 00FF, // в противном случае передаем управление // старому обработчику прерывания
if(_ax != 0xff00) _chain_intr(old2f); else _ax = 0x00ff; }
// ======================================= // Новый обработчик аппаратного прерывания таймера
void _interrupt _far new8(void) {
// Вызываем старый обработчик (*old8)();
// Если была нажата комбинация клавиш Ctrl+PrtSc // (iniflag при этом устанавливается в 1 // новым обработчиком прерывания 9) и // если запись в файл уже не началась, // то при значении флага критической секции // DOS, равном 0, выводим содержимое экрана // в файл
if((iniflag != 0) && (outflag == 0) && *crit == 0) {
outflag=1; // Устанавливаем флаг начала вывода _enable(); // Разрешаем прерывания
write_buf(); // Записываем содержимое // буфера экрана в файл
outflag=0; // Сбрасываем флаги в исходное iniflag=0; // состояние } }
// ======================================= // Новый обработчик прерывания 28h, которое вызывает // DOS, если она ожидает ввода от клавиатуры. // В этот момент TSR-программа может пользоваться // функциями DOS.
void _interrupt _far new28(void) {
// Если была нажата комбинация клавиш Ctrl+PrtSc // (iniflag при этом устанавливается в 1 // новым обработчиком прерывания 9) и // если уже не началась запись в файл, // то выводим содержимое экрана в файл
if((iniflag != 0) && (outflag == 0)) {
outflag=1; // Устанавливаем флаг начала вывода _enable(); // Разрешаем прерывания
write_buf(); // Записываем содержимое видеобуфера // в файл
outflag=0; // Сбрасываем флаги в исходное iniflag=0; // состояние }
// Передаем управление старому обработчику // прерывания 28 _chain_intr(old28); }
// ======================================= // Новый обработчик клавиатурного прерывания. // Он фиксирует нажатие комбинации клавиш Ctrl+PrtSc // и устанавливает флаг iniflag, который сигнализирует // о необходимости выбрать подходящий момент и // записать содержимое видеобуфера в файл
void _interrupt _far new9(void) {
// Если SCAN-код равен 0x37 (клавиша PrtSc), // нажата клавиша Ctrl (бит 4 байта состояния // клавиатуры, находящийся в области данных // BIOS по адресу 0040:0017 установлен в 1) // и если не установлен флаг iniflag, // то устанавливаем флаг iniflag в 1.
if((inp(0x60) == 0x37) && (iniflag == 0) && (*(char _far *)FP_MAKE(0x40,0x17) & 4) != 0) {
// Выдаем звуковой сигнал BEEP(); BEEP(); BEEP();
_disable(); // Запрещаем прерывания
// Разблокируем клавиатуру // и разрешим прерывания _asm { in al,61h mov ah,al or al,80h out 61h,al xchg ah,al out 61h,al
mov al,20h out 20h,al }
// Устанавливаем флаг запроса // на запись содержимого видеобуфера // в файл iniflag = 1; _enable(); // Разрешаем прерывания }
// Если нажали не Ctrl+PrtSc, то // передаем управление старому // обработчику прерывания 9 else _chain_intr(old9);
}
// ======================================= // Функция возвращает номер // текущего видеорежима
int get_vmode(void) { char _far *ptr; ptr = FP_MAKE(0x40,0x49); // Указатель на байт // текущего видеорежима return(*ptr); }
// ======================================= // Функция возвращает сегментный адрес // видеобуфера. Учитывается содержимое // регистров смещения адреса видеобуфера.
int get_vbuf(int vmode) { unsigned vbase; unsigned adr_6845; unsigned high; unsigned low; unsigned offs;
// В зависимости от видеорежима базовый адрес // видеобуфера может быть 0xb000 или 0xb800 vbase = (vmode == 7) ? 0xb000 : 0xb800;
// получаем адрес порта видеоконтроллера 6845 adr_6845 = *(unsigned _far *)(FP_MAKE(0x40,0x63));
// Считываем содержимое регистров 12 и 13 // видеоконтроллера outp(adr_6845,0xc); high = inp(adr_6845+1);
outp(adr_6845,0xd); low = inp(adr_6845+1);
offs = ((high << 8) + low) >> 4;
// Добавляем к базовому адресу видеобуфера // смещение, взятое из регистров видеоконтроллера vbase += offs;
return(vbase); }
// ======================================= // Функция возвращает количество символов в строке // для текущего видеорежима
int get_column(void) { return(*(int _far *)(FP_MAKE(0x40,0x4a))); }
// ======================================= // Функция возвращает количество строк // для текущего видеорежима
int get_row(void) { unsigned char ega_info; ega_info = *(unsigned char _far *)(FP_MAKE(0x40,0x87));
// Если нет EGA, то используется 25 строк, // если EGA присутствует, считываем число // строк. Это число находится в области данных // BIOS по адресу 0040:0084.
if(ega_info == 0 ( (ega_info & 8) != 0) ) { return(25); } else { return(*(unsigned char _far *) (FP_MAKE(0x40,0x84)) + 1); } }
// ======================================= // Функция записи содержимого видеобуфера в // файл
int write_buf(void) {
// Видеопамять состоит из байтов символов и байтов // атрибутов. Нам нужны байты символов chr. typedef struct _VIDEOBUF_ { unsigned char chr; unsigned char attr; } VIDEOBUF;
VIDEOBUF _far *vbuf; int i, j, k, max_col, max_row; FILE *out_file; char fname[20],ext[8];
i=get_vmode(); // Получаем номер текущего // видеорежима
// Для графического режима ничего не записываем if(i > 3 && i != 7) return(-1);
// Устанавливаем указатель vbuf на видеобуфер vbuf=(VIDEOBUF _far *)FP_MAKE(get_vbuf(i),0);
// Определяем размеры экрана max_col = get_column(); max_row = get_row();
// Формируем имя файла для записи образа экрана itoa(name_counter++,ext,10); strcpy(fname,"!grab"); strcat(fname,ext); strcat(fname,".doc");
out_file=fopen(fname,"wb+");
// Записываем содержимое видеобуфера в файл for(i=0; i<max_row; i++) { for(j=0; j<max_col; j++) {
fputc(vbuf->chr,out_file); vbuf++;
}
// В конце каждой строки добавляем // символы перевода строки и // возврата каретки fputc(0xd,out_file); fputc(0xa,out_file); } fclose(out_file); return(0); }
// ======================================= // Функция выводит на экран инструкцию по // использованию программы GRAB
int hello(void) { printf("\nУтилита копирования содержимого" "\nэкрана в файл GRAB<n>.DOC" "\nCopyright (C)Frolov A.,1990" "\n" "\nДля копирования нажмите Ctrl+PrtSc" "\n"); }
Приведем пример TSR-программы, написанной на языке ассемблера. Эта программа переназначает прерывание 13h, которое используется для работы с дисками. Она позволяет организовать защиту диска от записи. При первом запуске программа включает защиту, при втором выключает, потом опять включает и так далее. В качестве флага - признака включения или выключения защиты, используется компонента смещения вектора прерывания F0h, зарезервированного для интерпретатора BASIC.
.MODEL tiny .CODE .STARTUP
jmp begin
old_int13h_off dw 0 ; Адрес старого обработчика old_int13h_seg dw 0 ; прерывания 13h
old_int2Fh_off dw 0 ; Адрес старого обработчика old_int2Fh_seg dw 0 ; прерывания 2Fh
; Новый обработчик прерывания 2Fh нужен ; для проверки наличия программы в памяти ; при ее запуске для предохранения ; от повторного запуска
new_int2Fh proc far cmp ax,0FF00h jz installed
jmp dword ptr cs:old_int2Fh_off
; Если код функции 0FF00h, то возвращаем ; в регистре AX значение 00FFh. Это признак ; того, что программа уже загружена в память
installed: mov ax,00FFh iret
new_int2Fh endp
; Новый обработчик прерывания 13h. Для команд записи ; на жесткий диск выполняет проверку содержимого ; компоненты смещения вектора прерывания FFh. ; Эта ячейка служит для триггерного переключения ; режима работы прерывания 13h - включения/выключения ; защиты записи.
new_int13h proc far cmp ah,3 ; запись сектора je protect cmp ah,5 ; форматирование трека je protect jmp dword ptr cs:old_int13h_off
old_int13h: pop es pop bx pop ax
jmp dword ptr cs:old_int13h_off
protect: push ax push bx push es
; Проверяем значение триггерного флага защиты xor ax,ax mov es,ax mov bx,0F0h*4 mov ax,WORD PTR es:[bx] cmp ax,0FFFFh
jne old_int13h
; Для флоппи-дисков защиту не включаем
cmp dl,0 je old_int13h cmp dl,1 je old_int13h
pop es pop bx pop ax
; Имитируем ошибку записи при попытке ; записать данные на защищенный от записи диск
mov ah,3 stc ret 2
new_int13h endp
;============================== ; Точка входа в программу
begin proc far
; Проверяем, не загружена ли уже программа ; в память
mov ax,0FF00h int 2Fh
cmp ax,00FFh jne first_start
jmp invert_protect_flag
; Первоначальный запуск программы
first_start:
; Устанавливаем триггерный флаг защиты записи ; в состояние, соответствующее включенной защите
xor ax,ax mov es,ax mov bx,0F0h*4 mov WORD PTR es:[bx],0FFFFh
; Запоминаем адрес старого обработчика прерывания 13h
mov ax,3513h int 21h mov cs:old_int13h_off,bx mov cs:old_int13h_seg,es
; Запоминаем адрес старого обработчика прерывания 2Fh
mov ax,352Fh int 21h mov cs:old_int2Fh_off,bx mov cs:old_int2Fh_seg,es
push cs pop ds
; Выводим сообщение о включении защиты
mov dx,offset msg_on mov ah,9 int 21h
; Устанавливаем новые обработчики прерываний 13h и 2Fh
mov dx,OFFSET new_int13h mov ax,2513h int 21h
mov dx,OFFSET new_int2Fh mov ax,252Fh int 21h
; Завершаем программу и оставляем резидентно ; в памяти часть программы, содержащую новые ; обработчики прерываний
mov dx,OFFSET begin int 27h
; Если это не первый запуск программы, ; инвертируем содержимое триггерного флага защиты
invert_protect_flag:
xor ax,ax mov es,ax mov bx,0F0h*4
mov ax,WORD PTR es:[bx] not ax mov WORD PTR es:[bx],ax mov cx,ax
cmp cx,0FFFFh je prot_on
; Выводим сообщение о выключении защиты
mov dx,OFFSET msg_off jmp short send_msg prot_on: ; Выводим сообщение о включении защиты mov dx,OFFSET msg_on send_msg: mov ah,9 push cs pop ds int 21h
.EXIT begin endp
msg_on db 'Защита диска включена$' msg_off db 'Защита диска ВЫКЛЮЧЕНА$' end
Этим примером мы завершим обзор TSR-программ. В следующей главе будет описан другой вид резидентных программ - драйверы. Использование драйвера - более предпочтительный, чем TSR-программы способ организовать обслуживание нестандартной аппаратуры.
Процесс загрузки драйверов
Системный файл DOS IO.SYS содержит некоторые драйверы устройств, составляющие базовую систему ввода/вывода. Эти драйверы появляются в памяти при загрузке операционной системы и связаны в цепочку через поля next в своих заголовках. Такие драйверы являются резидентными драйверами операционной системы.
Резидентные драйверы могут быть заменены драйверами пользователя, кроме того, пользователь может добавить в список драйверов новые.
Для подключения драйвера пользователя к операционной системе файл CONFIG.SYS должен содержать команду:
DEVICE=<путь_файла_драйвера>_<параметры>.
Например:
DEVICE=c:\dos\smartdrv.sys 120
В этом примере подключается драйвер smartdrv.sys, который находится в каталоге dos на диске C:. В качестве параметра инициализации драйверу передается число 120. (Параметры считываются драйвером один раз в процессе инициализации драйвера. Об этом мы будем говорить подробно в разделе, посвященном инициализации драйвера).
В списке драйверов драйверы пользователя находятся перед резидентными. В этом можно убедиться, посмотрев на результаты работы программы DRI.COM, описанной ранее:
Device Drivers Information V1.00 Copyright (C)Frolov A.,1990
Address Attr Device Name ------- ---- ----------- 02C1:0048 8004 NUL 112F:0000 8800 RBUSDRIV 10E4:0000 0800 ------> Block Device, Number of Units: 0001 0D86:0000 C800 SMARTAAR 0CC7:0000 A000 XMSXXXX0 0BA5:0000 6842 ------> Block Device, Number of Units: 0003 0070:016E 8013 CON 0070:0180 8000 AUX 0070:0192 A040 PRN 0070:01A4 8008 CLOCK$ 0070:01B6 0842 ------> Block Device, Number of Units: 0003 0070:01CA 8000 COM1 0070:01DC A040 LPT1 0070:01EE A040 LPT2 0070:0200 A040 LPT3 0070:0212 8000 COM2 0070:0224 8000 COM3
Эта программа выдает весь список драйверов с самого его начала. Для каждого драйвера выводится адрес драйвера в памяти, слово атрибутов драйвера и имя устройства (либо количество обслуживаемых блочным драйвером устройств).
Первым в этом списке всегда идет драйвер с именем устройства NUL. Это нуль-устройство, используемое для тестовых целей. Драйвер псевдоустройства NUL не может быть переназначен, он всегда расположен непосредственно за векторной таблицей связи DOS.
Дальше идут драйверы, описанные в файле CONFIG.SYS следующим образом:
device=sstor.sys device=e:\C600\BIN\himem.sys device=e:\c600\bin\smartdrv.sys 530 device=e:\c600\bin\ramdrive.sys 732 512 64 /e device=e:\vega\rbusdrv.sys 1 80 378 379 37a 37a
Начиная с драйвера консоли CON, идут резидентные драйверы, имеющие сегментный адрес 0070. Это драйвер последовательного канала связи AUX, драйвер устройства печати PRN, драйвер часов CLOCK$, драйверы последовательных каналов COM1, COM2, COM3 и драйверы устройств печати LPT1, LPT2, LPT3.
Если Ваш драйвер должен заменить стандартный, в поле имени заголовка драйвера укажите имя устройства заглавными буквами (например, LPT1). Система разместит Ваш драйвер в цепочке драйверов до стандартного с именем LPT1.
Когда операционная система загружает драйвер, она создает заголовок запроса и помещает в него команду инициализации. Затем вызывается программа стратегии драйвера, которой передается адрес подготовленного заголовка запроса. После этого вызывается программа прерывания драйвера. Эта программа просматривает заголовок запроса, определяет, что пришла команда инициализации, и начинает ее обрабатывать.
Инициализирующая часть выполняется только один раз при загрузке драйвера. Могут выполняться такие действия, как считывание параметров инициализации из файла CONFIG.SYS, выдача инициализирующих команд на обслуживаемое устройство ввода/вывода, инициализация внутренних областей данных и т.д. Перед завершением своей работы инициализирующая часть драйвера записывает в заголовок запроса размер резидентной части драйвера. Сама инициализирующая часть больше не будет нужна, поэтому эта часть драйвера не оставляется при инициализации резидентной в памяти.
Таким образом, количество памяти, отводимое драйверу при загрузке, операционная система определяет, исходя из той информации, которую сам драйвер передает операционной системе при инициализации, а не пользуется длиной файла драйвера, как это можно было бы предположить.
Более подробно процесс инициализации будет рассмотрен при описании команды инициализации драйвера.
Процесс загрузки операционной системы
При включении питания компьютера управление передается базовой системе ввода/вывода, BIOS.Она выполняет проверку аппаратных узлов компьютера, формирует начальную часть таблицы векторов прерываний, инициализирует устройства и начинает процесс загрузки операционной системы.
Загрузка начинается с того, что BIOS делает попытку прочитать самый первый сектор дискеты, вставленной в дисковод А: (на загрузочной дискете этот сектор содержит загрузчик операционной системы). Если в дисковод вставлена системная дискета, с нее считывается загрузчик и ему передается управление.
Если дискета не системная, т.е. не содержит загрузочной записи, на экран выдается сообщение с просьбой заменить дискету.
Если же дискеты в дисководе А: вообще нет, то BIOS читает основную загрузочную запись диска С: (Master Boot Record). Обычно это самый первый сектор на диске. Управление передается загрузчику, который находится в этом секторе. Загрузчик анализирует содержимое таблицы разделов (она также находится в этом секторе), выбирает активный раздел и читает загрузочную запись этого раздела. Загрузочная запись активного раздела (Boot Record) аналогична загрузочной записи, находящейся в первом секторе системной дискеты.
Загрузочная запись активного раздела считывает с диска файлы IO.SYS и MSDOS.SYS (именно в этом порядке). Затем считываются и загружаются резидентные драйверы. Начинается формирование связанного списка драйверов устройств. Анализируется содержимое файла CONFIG.SYS, загружаются описанные в этом файле драйверы. Сначала загружаются драйверы, описанные параметром DEVICE, затем (только в MS-DOS версии 4.х и 5.0) резидентные программы, указанные операторами INSTALL. После этого считывается командный процессор и ему передается управление.
Командный процессор состоит из трех частей - резидентной, инициализирующей и транзитной. Первой загружается резидентная часть. Она обрабатывает прерывания INT 22H, INT 23H, INT 24H, управляет загрузкой транзитной части. Эта часть командного процессора обрабатывает ошибки MS-DOS и выдает запрос пользователю о действиях при обнаружении ошибок.
Инициализирующая часть используется только в процессе загрузки операционной системы. Она определяет начальный адрес, по которому будет загружаться пользовательская программа и инициализирует выполнение файла AUTOEXEC.BAT.
Транзитная часть командного процессора располагается в старших адресах памяти. В этой части находятся обработчики внутренних команд MS-DOS и интерпретатор командных файлов с расширением имени .BAT. Транзитная часть выдает системное приглашение (например, А:\> ), ожидает ввода команды оператора с клавиатуры или из пакетного файла и организует их выполнение.
После загрузки командного процессора и выполнения начальных процедур, перечисленных в файле AUTOEXEC.BAT, подготовка системы к работе завершается.
Процесс загрузки программ в память
Загрузка COM- и EXE-программ происходит по-разному, однако есть некоторые действия, которые операционная система выполняет в обоих случаях одинаково.
Определяется наименьший сегментный адрес свободного участка памяти для загрузки программы (обычно DOS загружает программу в младшие адреса памяти, если при редактировании не указана загрузка в старшие адреса).
Создаются два блока памяти (и, следовательно, два блока MCB, описанные ранее) - блок памяти для переменных среды и блок памяти для PSP и программы.
Для DOS версии 3.х и старше в блок памяти переменных среды помещается путь файла программы.
Заполняются поля префикса сегмента программы PSP в соответствии с характеристиками программы (количество памяти, доступное программе, адрес сегмента блока памяти, содержащего переменные среды и т.д.)
Устанавливается адрес области Disk Transfer Area (DTA) на вторую половину PSP (PSP:0080).
Анализируются параметры запуска программы на предмет наличия в первых двух параметрах идентификаторов дисковых устройств. По результатам анализа устанавливается содержимое регистра AX при входе в программу. Если первый или второй параметры не содержат правильного идентификатора дискового устройства, то соответственно в регистры AL и AH записывается значение FF.
А дальше действия системы по загрузке программ форматов COM и EXE будут различаться.
Для COM-программ, которые представляют собой двоичный образ односегментной программы, выполняется чтение файла программы с диска и запись его в память по адресу PSP:0100. Вообще говоря, программы типа COM могут состоять из нескольких сегментов, но в этом случае они должны сами управлять содержимым сегментных регистров, используя в качестве базового адреса адрес PSP.
После загрузки файла операционная система для COM-программ выполняет следующие действия:
сегментные регистры CS, DS, ES, SS устанавливаются на начало PSP;
регистр SP устанавливается на конец сегмента PSP;
вся область памяти после PSP распределяется программе;
в стек записывается слово 0000;
указатель команд IP устанавливается на 100h (начало программы) с помощью команды JMP по адресу PSP:100.
Загрузка EXE-программ происходит значительно сложнее, так как связана с настройкой сегментных адресов:
Считывается во внутренний буфер DOS форматированная часть заголовка файла.
Определяется размер загрузочного модуля по формуле:
size=((file_size*512)-(hdr_size*16)-part_pag
Определяется смещение начала загрузочного модуля в EXE-файле как hdr_size*16.
Вычисляется сегментный адрес для загрузки START_SEG, обычно используется значение PSP+10h.
Загрузочный модуль считывается в память по адресу START_SEG:0000.
Сканируются элементы таблицы перемещений, располагающейся в EXE-файле со смещением relt_off.
Для каждого элемента таблицы:
1. Считывается содержимое элемента таблицы как два двухбайтных слова (OFF,SEG).
2. Вычисляется сегментный адрес ссылки перемещения
REL_SEG = (START_SEG + SEG)
3. Выбирается слово по адресу REL_SEG:OFF, к этому слову прибавляется значение START_SEG, затем сумма записывается обратно по тому же адресу.
Заказывается память для программы, исходя из значений min_mem и max_mem.
Инициализируются регистры, и программа запускается на выполнение.
При инициализации регистры ES и DS устанавливаются на PSP, регистр AX устанавливается так же, как и для COM-программ, в сегментный регистр стека SS записывается значение START_SEG + ss_reg, а в SP записывается sp_reg.
Для запуска программы в CS записывается START_SEG+cs_reg, а в IP - ip_reg. Такая запись невозможна напрямую, поэтому операционная система сначала записывает в свой стек значение для CS, затем значение для IP и после этого выполняет команду дальнего возврата RETF (команда возврата из дальней процедуры).
- Проверить состояние устройства вывода.
Эти команды проверяют состояние устройства ввода и устройства вывода соответственно (только символьные устройства).
Для устройства ввода бит занятости слова состояния (бит 9) сбрасывается в ноль, если буфер устройства не пуст и в нем есть готовые для чтения символы. Этот бит устанавливается драйвером в единицу, когда буфер пуст, и последующая команда чтения приведет к ожиданию ввода от устройства, например, к ожиданию нажатия на клавишу.
Для устройства вывода бит занятости в слове состояния (бит 9) сбрасывается в ноль, если нет текущих ожидающих готовности устройства запросов на вывод и последующая команда вывода может быть немедленно выполнена. Бит устанавливается в 1, если предыдущий запрос на вывод еще не обработан.
Для команд проверки состояния запрос состоит только из заголовка, область переменного формата отсутствует.
- Проверка секторов.
Функция проверяет сектора на правильность циклической контрольной суммы, CRC (Cyclic Redundancy Check); записи содержимого секторов в память не происходит.
Работа с дисплейным адаптером.
Прерывание INT10h выполняет все многочисленные операции по обслуживанию дисплейного адаптера.
При вызове прерывания INT 10h, как и при вызове многих других прерываний, регистр AH содержит номер функции, которую требуется выполнить. Остальные регистры при вызове прерывания содержат операнды.
Программирование дисплейного адаптера - сложная задача. Функции, выполняемые прерыванием INT 10h обширны, полностью они будут описаны во втором томе книги. Приведем краткий обзор функций прерывания INT 10h.
Работа с драйвером символьного устройства
// Данная прорамма использует прерывание 80h, // которое устанавливается демонстрационным // драйвером. Для правильной установки файл // CONFIG.SYS должен содержать, например, // такую строку для подключения драйвера: // // device=e:\sysprg\devdrv.sys 1 80 378 379 37a 37a // // Число 80 означает номер используемого прерывания.
#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], ch; int io_handle; unsigned count;
// Открываем устройство с именем DEVDRIVR
if( (io_handle = open("DEVDRIVR", O_RDWR)) == - 1 ) {
// Если открыть устройство не удалось, выводим // код ошибки
printf("Ошибка при открытии устройства %d",errno); return errno; }
// Читаем 8 байт из устройства в буфер buf
printf("\nВведите 8 символов с клавиатуры\n");
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; }
// Вводим строку IOCTL из устройства
printf("\nВведите строку IOCTL (8 символов): ");
inregs.h.ah = 0x44; inregs.h.al = 2; inregs.x.bx = io_handle; inregs.x.dx = (unsigned)buf; inregs.x.cx = 8; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим код ошибки
printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); } buf[8]=0; printf("\n___ Введена строка IOCTL: %s ___",buf);
// Выводим строку IOCTL на устройства из buf
printf("\nВыведена строка IOCTL: ");
inregs.h.ah = 0x44; inregs.h.al = 3; inregs.x.bx = io_handle; inregs.x.dx = (unsigned)buf; inregs.x.cx = 8; intdos( &inregs, &outregs ); if(outregs.x.cflag == 1) {
// При ошибке выводим код ошибки
printf("IOCTL error %x\n",&outregs.x.ax); exit(-1); }
printf("\n\n\nПроверяем вызов прерывания." "\n" "\nНажмите любую клавишу...\n\n"); getch();
printf("\nКоманда записи:\n");
inregs.h.ah = 0x0; /* WRITE */ inregs.h.bh = 0x777; inregs.h.bl = 0x13; int86( 0x80, &inregs, &outregs );
printf("\nКоманда чтения:\n");
inregs.h.ah = 0x1; /* READ */ inregs.h.bh = 0x776; int86( 0x80, &inregs, &outregs ); ch=outregs.h.bl;
printf("Полученное значение: %x\n",ch);
printf("\nНеизвестная команда:\n");
inregs.h.ah = 0x2; /* ??? */ int86( 0x80, &inregs, &outregs );
// Закрываем устройство close(io_handle); exit(0); }
Работа с файловой системой.
DOS предоставляет программам обширный сервис для работы с файлами и дисками. Практически все файловые операции можно выполнять с помощью специально предназначенных для этого функций, только в некоторых случаях (защита данных от копирования, например) приходится использовать прямое обращение к диску.
С помощью файловых функций DOS можно создавать и удалять файлы и каталоги, открывать и закрывать файлы, создавать временные файлы. Ввод/вывод может быть буферизован, можно получить как последовательный доступ к содержимому файла, так и произвольный.
Мы не будем приводить сейчас конкретные примеры или номера функций, отложим это до книги 3, посвященной файловой системе.
Работа с системными часами.
Функции прерывания INT1Ah обслуживают часы, имеющиеся в каждом компьютере. С их помощью вы можете установить время и дату, опросить текущее состояние часов. Вы можете работать с часами реального времени, которые имеются на машинах класса не ниже AT.
Для AT можно установить на заданное время "будильник" - в нужный момент будет вызвано прерывание "будильника" с номером 4Ah. Обработчик прерывания INT 4Ah может подать звуковой сигнал или вывести на экран предупреждающее сообщение.
- Сброс буфера устройства вывода
Эти функции заставляют драйвер сбросить все текущие запросы на ввод/вывод.
Сброс буфера входного устройства используется, например, для удаления всех символов из буфера клавиатуры. После выполнения очистки буферов драйвер выставляет слово состояния и возвращает управление операционной системе.
Запрос состоит только из заголовка.
- Сброс дисковой системы.
Эта функция выполняет установку в исходное состояние всей дисковой системы или выбранного дискового устройства. Используется обычно перед началом работы с устройством.
Символьный ввод/вывод.
Эти функции применяются для работы со всеми символьными устройствами, такими как консоль, принтер, последовательный порт, и называются функциями стандартного ввода/вывода.
Ввод/вывод программы, использующей стандартные функции, может быть переназначен.
Приведем обзор основных функций стандартного символьного ввода/вывод в виде таблицы:
Код | Назначение | Описание |
01h | Ввод с клавиатуры | Выполняется ввод символа со стандартного ввода и эхо-вывод символа на стандартное устройство вывода. Выполняется проверка на нажатие комбинации клавиш CTRL/C и CTRL-BREAK |
06h | Ввод с клавиатуры | Ввод символа со стандартного ввода без ожидания и вывод его на устройство стандартного вывода. Комбинации CTRL/C и CTRL-BREAK не проверяются. |
07h | Прямой ввод | Ввод символа со стандартного с клавиатуры устройства ввода. Комбинации клавиш CTRL/C и CTRL-BREAK не проверяются. |
08h | Ввод с клавиатуры | Аналогично функции 07h, но проверяются комбинации клавиш CTRL/C и CTRL-BREAK. |
02h | Отобразить символ | Отображаемый символ посылается на стандартное устройство вывода. |
09h | Отобразить строку | На стандартное устройство вывода символов посылается строка, закрытая символом '$'. |
03h | Ввод из последовательного порта | Вводится символ из последовательного порта |
04h | Вывод в последовательный порт | Выводится символ на последовательный порт |
05h | Вывод на принтер | Выводится символ на принтер. |
Из таблицы видно, что для ввода с клавиатуры можно использовать несколько функций. Ввод без эхо-вывода удобен для такой информации, как пароли. Если логика работы программы не допускает прерывания по нажатию комбинаций клавиш CTRL-C или CTRL-BREAK, нужно использовать функции, которые не проверяют эти комбинации.
Для вывода строки символов можно использовать функцию 09h, но выводимая строка не может содержать символ '$', так как этот символ используется в качестве признака конца строки.
Система обработки ошибок.
Система обработки ошибок DOS проста и удобна. Для кодирования ошибок как правило используется флаг переноса (CARRY, CF). Если после обращения к прерыванию DOS флаг переноса установлен в 1, произошла ошибка. Для того чтобы проанализировать ошибку и предпринять какие-то действия, можно вызвать соответствующую функцию DOS, которая вернет уточняющую информацию об ошибке и предоставит соответствующие рекомендации (разумеется, лишь в виде кодов, находящихся в регистрах процессора).
Если произошла критическая ошибка ввода/вывода (например, прочитать дискету невозможно), вызывается стандартная процедура DOS, выводящая на экран запрос о дальнейших действиях. Пользовательская программа может подключить вместо системной свою программу обработки критических ошибок. Подробнее об обработке ошибок будет сказано в разделе 1.6.
Система связи с драйверами устройств.
Эта система скрыта от прикладных программ - программы не могут обращаться непосредственно к драйверам устройств ввода/вывода. Программа вызывает DOS, а DOS обращается при необходимости к драйверам.
Возможно, что запрет на непосредственный вызов драйверов введен для обеспечения совместимости с будущими версиями операционной системы, в которых механизм вызова драйверов может измениться. Однако, используя сведения, приведенные в этой книге, вы сможете обойти этот запрет и обратиться непосредственно к драйверу. При этом вам придется использовать некоторые недокументированные прерывания DOS, что само по себе нежелательно из-за возможной потери совместимости с другими версиями операционной системы.
Для управления состоянием устройства ввода/вывода или состоянием драйвера используется специальная функция 44h прерывания DOS 21h. Эта функция предназначена для обмена управляющей информацией между прикладной программой и драйвером.
Система управления памятью.
Эта подсистема DOS используется для распределения памяти запускаемым программам.
DOS управляет памятью с помощью блоков MCB (Memory Control Block). Память разбивается на блоки; каждому блоку предшествует MCB, в котором записаны характеристики блока памяти. Для каждой вновь запускаемой программы DOS создает определенное количество блоков MCB. При освобождении памяти или при выполнении запросов на получение дополнительной памяти DOS также использует блоки MCB, проверяя при этом правильность их содержимого.
Все блоки MCB располагаются друг за другом. Адрес первого блока хранится в векторной таблице связи, CVT, о которой мы будем говорить в главе 2. Там же будет описан формат блока управления памятью.
Прикладная программа может заказать для себя дополнительные блоки памяти. Для этого она обращается к системе управления памятью, используя функции прерывания 21h DOS.
Система управления программами.
При запуске программы DOS выполняет несколько операций. Сначала она обращается к системе управления памятью, чтобы подготовить блоки памяти для запускаемой программы. С помощью файловой системы файл, содержащий программу, загружается в память, после чего программа (это относится только к файлам типа .exe) настраивается на конкретный физический адрес. Только после этого программе передается управление.
Как известно, в MS-DOS существуют два формата выполняемых программ - .com и .exe. Способы запуска этих программ сильно различаются. Система управления программами автоматически распознает их и загружает в память по-разному. Мы еще вернемся к описанию различий между этими типами программ.
Другая задача, решаемая ситемой управления программами - запуск программ из программ и загрузка "программных перекрытий" - оверлеев. Если не все модули большого программного комплекса нужны одновременно, вы можете разбить комплекс на несколько частей. Это могут быть либо несколько отдельных программ, либо несколько оверлейных модулей. Каждый из этих способов имеет свои преимущества и недостатки; оба они пригодны для экономии памяти.
И наконец, последняя функция системы управления программами - работа с резидентными программами. Если вам надо, чтобы после завершения своей работы программа осталась резидентной в памяти, вы, как и в случае завершения обычной программы, обращаетесь к системе управления программами через соответствующую функцию прерывания DOS 21h.
Системный сервис для машин класса AT.
Прерывание INT15h использовалось в компьютерах IBM PC и IBM PC Jr для управления кассетным накопителем на магнитной ленте (функции 0-3). Для машин класса AT и более высокого класса прерывание INT 15h имеет и другое назначение. С его помощью обслуживается расширенная клавиатура, выполняется программная задержка, задаваемая в микросекундах, обслуживается расширенная память. Кроме того, одна из функций прерывания INT 15h переводит процессор 80286 или 80386 в защищенный режим. Заметим, что вернуть процессор обратно в реальный режим можно только сигналом начального сброса. Это же относится и к арифметическому сопроцессору 80287.
Функция C0h прерывания INT 15h выдает дополнительные сведения о конфигурации аппаратных средств компьютера.
Для PS/2 назначение некоторых функций этого прерывания другое по сравнению с машиной AT.
На этом мы завершим описание предоставляемых BIOS функций и перейдем к обзору функций DOS.
Служба времени.
Компьютер обычно оборудуется системными часами. Это могут быть КМОП-часы с питанием от аккумулятора, содержимое которых не сбрасывается при выключении питания компьютера, или таймер, регулярно вырабатывающий прерывания. В любом случае операционная система ведет подсчет времени и хранит текущие показания часов и дату.
Программа может опросить часы, обратившись к DOS с запросом через одну из функций прерывания 21h, или установить новое состояние часов.
Операционная система содержит драйвер устройства CLOCK$. Прикладная программа может обратиться к этому устройству для чтения показания часов или для установки часов. В книге 2 первого тома приведен пример программы для работы с устройством CLOCK$.
Программа может также использовать прерывания таймера для регулярного выполнения каких-либо функций.
Список управляющих блоков устройств
Поле dev_cb векторной таблицы связи содержит FAR-адрес цепочки блоков управления устройствами DOS (DOS Device Control Block) - DDCB. Блок DDCB строится операционной системой для каждого дискового устройства и содержит информацию о характеристиках этого устройства и указатель на заголовок драйвера, обслуживающего данное устройство.
Этот блок может быть использован программами, которые выполняют доступ к диску на уровне секторов. Подробнее назначение и использование полей блока DDCB будет описано в разделе, посвященном файловой системе, так как эта информация требуется в основном для организации работы с диском на низком уровне.
Приведем формат блока DDCB для DOS версий 2.х и 3.х:
(0) 1 | drv_num | номер устройства (0 соответствует устройству А:, 1 - В: и т.д.) |
(+1) 1 | drv_numd | дополнительный номер устройства внутри драйвера |
(+2) 2 | sec_size | размер сектора в байтах |
(+4) 1 | clu_size | число, на единицу меньшее количества секторов в кластере |
(+5) 1 | clu_base | если содержимое этого поля не равно нулю, то для получения общего числа секторов в кластере надо возвести 2 в степень clu_base и получившееся число прибавить к clu_size |
(+6) 2 | boot_siz | количество зарезервированных секторов (boot-сектора, начало корневого каталога) |
(+8) 1 | fat_num | количество копий FAT |
(+9) 2 | max_dir | максимальное число дескрипторов файлов в корневом каталоге (т.е. максимальное число файлов, которое может содержать корневой каталог на этом устройстве) |
(+11) 2 | data_sec | номер первого сектора данных на диске (номер сектора, соответствующего кластеру номер 2) |
(+13) 2 | hi_clust | максимальное количество кластеров (равно увеличенному на 1 количеству кластеров данных) |
(+15) 1 | fat_size | количество секторов, занимаемых одной копией FAT |
(+16) 2 | root_sec | номер первого сектора корневого каталога |
(+18) 4 | drv_addr | FAR-адрес заголовка драйвера, обслуживающего данное устройство |
(+22) 1 | media | байт описания среды носителя данных |
(+23) 1 | acc_flag | флаг доступа, 0 означает, что к устройству был доступ |
(+24) 4 | next | адрес следующего блока DDCB, для последнего блока в поле смещения находится число FFFF |
--------------- только для DOS 2.x ----------------- | ||
(+28) 2 | dir_clu | номер начального кластера текущего каталога (0 для корневого каталога) |
(+30) 64 | dir_path | строка в формате ASCIIZ, содержащая путь к текущему каталогу |
----- DOS 3.х ------ | ||
(+28) 2 | reserv1 | зарезервировано, обычно равно 0 |
(+30) 2 | built | число FFFF в этом поле означает, что блок DDCB был построен |
Для DOS версии 4. х формат этого блока другой. Кроме того, изменилась его длина:
(0) 1 | drv_num | номер устройства (0 соответствует устройству А:, 1 - В: и т.д.) |
(+1) 1 | drv_numd | дополнительный номер устройства внутри драйвера |
(+2) 2 | sec_size | размер сектора в байтах |
(+4) 1 | clu_size | число, на единицу меньшее количества секторов в кластере |
(+5) 1 | clu_base | если содержимое этого поля не равно нулю, то для получения общего числа секторов в кластере надо возвести 2 в степень clu_base и получившееся число прибавить к clu_size |
(+6) 2 | boot_siz | количество зарезервированных секторов (boot-сектора, начало корневого каталога) |
(+8) 1 | fat_num | количество копий FAT |
(+9) 2 | max_dir | максимальное число дескрипторов файлов в корневом каталоге (т.е. максимальное число файлов, которое может содержать корневой каталог на этом устройстве) |
(+11) 2 | data_sec | номер первого сектора данных на диске (номер сектора, соответствующего кластеру номер 2) |
(+13) 2 | hi_clust | максимальное количество кластеров (равно увеличенному на 1 количеству кластеров данных) |
(+15) 1 | fat_size | количество секторов, занимаемых одной копией FAT |
(+16) 1 | reserv1 | зарезервироано |
(+17) 2 | root_sec | номер первого сектора корневого каталога |
(+19) 4 | drv_addr | FAR-адрес заголовка драйвера, обслуживающего данное устройство |
(+23) 1 | media | байт описания среды носителя данных |
(+24) 1 | acc_flag | флаг доступа, 0 означает, что к устройству был доступ |
(+25) 4 | next | адрес следующего блока DDCB, для последнего блока в поле смещения находится число FFFF |
(+29) 2 | reserv2 | зарезервироано |
(+31) 2 | built | число FFFF в этом поле означает, что блок DDCB был построен |
/* Блок управления устройством DOS */
#pragma pack(1)
typedef struct _DDCB_ { unsigned char drv_num; unsigned char drv_numd; unsigned sec_size; unsigned char clu_size; unsigned char clu_base; unsigned boot_siz; unsigned char fat_num; unsigned max_dir; unsigned data_sec; unsigned hi_clust; unsigned char fat_size; char reserv1; unsigned root_sec; void far *drv_addr; unsigned char media; unsigned char acc_flag; struct _DDCB_ far *next; unsigned reserv2; unsigned built; } DDCB;
#pragma pack()
Еще раз уместно заметить, что формат этого блока не описан в документации по MS-DOS, поэтому он может отличаться в различных версиях операционных систем.
Приведем тексты программ для получения адресов первого и последующих блоков DDCB:
/** *.Name get_fddcb * *.Title Получить адрес первого DDCB * *.Descr Функция возвращает адрес первого блока DDCB * *.Params DDCB far *get_fddcb(CVT far *cvt) * * cvt - адрес векторной таблицы связи * *.Return Указатель на первый блок DDCB **/
#include <stdlib.h> #include <stdio.h> #include "sysp.h"
DDCB far *get_fddcb(CVT far *cvt) {
DDCB far * ddcb; ddcb = cvt->dev_cb; return(ddcb); }
/** *.Name get_nddcb * *.Title Получить адрес следующего DDCB * *.Descr Функция возвращает адрес следующего блока DDCB * или 0, если это последний блок в цепочке * *.Params DDCB far *get_nddcb(DDCB far *ddcb) * * ddcb - адрес предыдущего DDCB * *.Return Указатель на следующий блок DDCB * или 0, если это последний блок в цепочке **/
#include <dos.h> #include "sysp.h"
DDCB far *get_nddcb(DDCB far *ddcb) {
DDCB far *ddcb_n;
ddcb_n = ddcb->next; if(FP_OFF(ddcb_n) == 0xffff) return((DDCB far *)0); return(ddcb_n); }
С помощью приведенной ниже программы можно просмотреть содержимое всех блоков DDCB. Так как при большом количестве дисков выводится очень много информации, следует использовать средство переназначения стандартного устройства вывода DOS:
show_ddc > drives.lst
Эта программа проверена для версии MS/DOS 4.01.
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include "sysp.h"
void main(void);
void main(void) { CVT far *cvt; DDCB far *ddcb;
printf("\nБлоки управления дисковыми устройствами (DDCB)" "\nCopyright (C)Frolov A., 1990\n" "\n");
cvt=get_mcvt(); ddcb=get_fddcb(cvt);
for(;;) { if(ddcb == (DDCB far *)0) break; printf("Адрес DDCB: %Fp\n" "Номер устройства: %d\n" "Дополнительный номер: %d\n" "Размер сектора: %d\n" "Размер кластера в секторах: %d\n" "База размера кластера: %d\n" "Зарезервировано секторов: %d\n" "Число копий FAT: %d\n" "Макс. файлов в корневом каталоге : %d\n" "Первый кластер данных: %d\n" "Всего кластеров: %d\n" "Размер FAT в секторах: %d\n" "Первый сектор корневого каталога: %d\n" "Поле reserv1: %01X\n" "Адрес драйвера: %Fp\n" "Байт описателя среды носителя: %01X\n" "Флаг доступа: %01X\n" "Адрес следующего DDCB: %Fp\n" "Поле reserv2: %04X\n" "Блок построен: %04X\n" "-------------------------------------\n\n", ddcb, ddcb->drv_num, ddcb->drv_numd, ddcb->sec_size, ddcb->clu_size, ddcb->clu_base, ddcb->boot_siz, ddcb->fat_num, ddcb->max_dir, ddcb->data_sec, ddcb->hi_clust, ddcb->fat_size, ddcb->root_sec, ddcb->reserv1, ddcb->drv_addr, ddcb->media, ddcb->acc_flag, ddcb->next, ddcb->reserv2, ddcb->built); ddcb=get_nddcb(ddcb); } exit(0); }
Приведенный выше способ получения доступа к блокам DDCB больше всего подходит для просмотра блоков управления всеми дисковыми устройствами. Если вам требуется получить DDCB для какого-нибудь конкретного устройства, можно воспользоваться недокументированной функцией 32H прерывания INT 21H (со всеми ограничениями, связанными с использованием недокументированных возможностей).
Функция 32H получает в регистре DL номер устройства (0 - текущий диск, 1 - А: и т.д.) и возвращает в регистровой паре DS:BX адрес соответствующего DDCB. Если номер устройства был задан неправильно, регистр AL после выполнения функции будет содержать значение FF.
Если требуется получить адрес DDCB флоппи-диска, необходимо установить диск в приемный карман дисковода.
Приведем текст программы, возвращающей указатель на DDCB диска с заданным номером:
/** *.Name get_ddcb * *.Title Получить адрес DDCB заданного диска * *.Descr Функция возвращает адрес блока управления * устройством DOS DDCB * *.Params DDCB far *get_ddcb(int device_number) * * device_number - номер диска, для которого * требуется получить DDCB * Номер задается так: * 0 - текущий диск, 1 - В и т.д. * *.Return Указатель на DDCB заданного диска **/
#include <dos.h> #include <stdio.h> #include "sysp.h"
DDCB far *get_ddcb(unsigned char device_number) {
union REGS inregs, outregs; struct SREGS segregs;
inregs.h.ah = 0x32; inregs.h.al = 0; inregs.h.dl = device_number; intdosx( &inregs, &outregs, &segregs ); if(outregs.h.al == 0xff) return(DDCB far *)0;
return((DDCB far*)FP_MAKE(segregs.ds,outregs.x.bx)); }
Программа, приведенная ниже, выводит адреса всех DDCB. Можете запустить ее (она есть на дискете, прилагающейся к книге) и посмотреть, что получится. Не забудьте вставить флоппи-диски во все дисководы.
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include "sysp.h"
void main(void);
void main(void) { DDCB far *ddcb; unsigned char dr;
for(dr=1;;dr++) { ddcb=get_ddcb(dr); if(ddcb == (DDCB far *)0) break; printf("%Fp\n",ddcb); } exit(0); }
Список загружаемых драйверов устройств
Все драйверы, резидентные или подключенные к операционной системе во время обработки файла CONFIG.SYS, связаны в список. Первый драйвер - это так называемый NUL-драйвер - располагается всегда непосредственно после векторной таблицы связи. Подробно о драйверах будет рассказано во второй книге, а сейчас мы кратко опишем, как проследить цепочку загруженных драйверов и получить некоторые сведения об этих драйверах.
Драйвер - это программа, которая занимает не более одного сегмента (64 килобайта) и имеет в самом начале специальный заголовок. Заголовок драйвера имеет следующий формат:
(0) 4 | next | указатель на заголовок следующего драйвера. Если смещение адреса следующего драйвера равно FFFF, это последний драйвер в цепочке |
(+4) 2 | attrib | атрибуты драйвера |
(+6) 2 | strateg | смещение программы стратегии драйвера |
(+8) 2 | interrupt | смещение программы обработки прерывания для драйвера |
(+10) 8 | dev_name | имя устройства для символьных устройств или количество обслуживаемых устройств для блочных устройств. |
Мы приведем программу, которая сканирует список драйверов и выводит на стандартное устройство вывода адрес драйвера, его атрибуты, имя устройства для символьных устройств и количество обслуживаемых устройств для блочных драйверов.
Эта программа написана на языке ассемблера.
; Программа выводит информацию о загруженных драйверах
; ; Mакроопределение печатает символы на экране ; @@out_ch MACRO c1,c2,c3,c4,c5 mov ah,02h IRP chr,<c1,c2,c3,c4,c5> IFB <chr> EXITM ENDIF mov dl,chr int 21h ENDM ENDM
.MODEL tiny DOSSEG
.STACK 100h
.DATA
msg DB 13,10,"Device Drivers Information V1.00", 13, 10 DB "Copyright (C)Frolov A.,1990",13,10,13,10 DB "Address Attr Device Name",13,10 DB "------- ---- -----------",13,10 DB "$"
bl_msg DB "------> Block Device, Number of Units: ","$"
.CODE .STARTUP
mov ah, 9h ; Выводим заголовок mov dx, OFFSET msg int 21h
mov ah,52h ; Получаем адрес первого int 21h ; драйвера в цепочке add bx,22h ; es:bx - адрес первого драйвера
dr_loop: call show_driver_info ; выводим параметры ; драйвера
cmp WORD PTR es:[bx+2], 0ffffh ; последний ? jz end_of_driver_list cmp WORD PTR es:[bx], 0ffffh jz end_of_driver_list
mov ax,es:[bx] ; получаем адрес следующего mov cx,es:[bx+2] ; драйвера mov bx,ax mov es,cx
jmp dr_loop
end_of_driver_list:
.EXIT 0
show_driver_info proc near ;es:bx - адрес драйвера
push es push bx
mov ax,es ; выводим адрес драйвера call Print_word @@out_ch ':' mov ax,bx call Print_word @@out_ch ' ',' '
mov ax,es:[bx+4] ; выводим атрибут драйвера call Print_word @@out_ch ' ',' '
test WORD PTR es:[bx+4],8000h ; проверяем, это ; символьный jz is_block ; драйвер или ; блочный
mov cx,8 ; для символьного выводим mov si,bx ; имя драйвера pr_name: mov al,BYTE PTR es:[si+10] @@out_ch al inc si loop pr_name jmp nxt
is_block:
mov ah, 9h ; для блочного драйвера mov dx, OFFSET bl_msg ; выводим количество int 21h ; логических устройств, mov al,BYTE PTR es:[bx+10] ; которые ; обслуживает mov ah,0 ; этот драйвер call Print_word
nxt:
@@out_ch 13,10
pop bx pop es
ret show_driver_info endp
; Вывод на экран содержимого регистра AX
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 ;
END
Если запустить эту программу, она выведет на экран сведения о всех загруженных драйверах:
Device Drivers Information V1.00 Copyright (C)Frolov A.,1990
Address Attr Device Name ------- ---- ----------- 02C1:0048 8004 NUL 112F:0000 8800 RBUSDRIV 10E4:0000 0800 ------> Block Device, Number of Units: 0001 0D86:0000 C800 SMARTAAR 0CC7:0000 A000 XMSXXXX0 0BA5:0000 6842 ------> Block Device, Number of Units: 0003 0070:016E 8013 CON 0070:0180 8000 AUX 0070:0192 A040 PRN 0070:01A4 8008 CLOCK$ 0070:01B6 0842 ------> Block Device, Number of Units: 0003 0070:01CA 8000 COM1 0070:01DC A040 LPT1 0070:01EE A040 LPT2 0070:0200 A040 LPT3 0070:0212 8000 COM2 0070:0224 8000 COM3
К этой картинке мы еще вернемся при обсуждении процесса загрузки драйверов.
Структура загружаемого драйвера
Драйвер - это еще одна разновидность программ в дополнение к уже изученным нами программам формата COM и EXE. Иногда говорят, что драйверы - это разновидность COM-программ, но это не так. Скорее способ получения загрузочного модуля драйвера похож на способ получения программы в формате COM. Есть еще одно сходство драйверов и программ в формате COM (которое как раз и появляется из-за одинакового способа их получения) - загрузочные модули этих программ являются точным отображением исходного текста на языке ассемблера без добавления каких-либо управляющих блоков в начало файла, как это происходит в программах формата EXE. (К сожалению, драйвер должен быть написан на языке ассемблера. Авторам этой книги не известны способы составления драйверов на других языках программирования).
Но, оказывается, управляющий блок в самом начале модуля драйвера имеется. Это так называемый заголовок драйвера. Только в отличие от программ формата EXE, этот заголовок создается не редактором связи, а самим программистом и должен быть помещен в самое начало исходного текста программы-драйвера.
При загрузке драйвера в память заголовок драйвера тоже помещается в оперативную память, и в нем операционная система производит некоторые изменения, о которых мы еще будем говорить.
Таким образом, можно говорить и о сходстве драйвера с программами в формате EXE, так как в начале загрузочного модуля драйвера имеется управляющий блок. Только этот управляющий блок в отличие от заголовка EXE-файла является принадлежностью самой программы и загружается вместе с ней в память. Заголовок EXE-программы используется при загрузке EXE-программы, но после загрузки операционная система убирает его из памяти.
Не стоит пытаться запускать драйвер как программу в формате COM, так как управление будет передано в область памяти, содержащую заголовок драйвера, а там нет правильных машинных команд. Поэтому обычно файлы драйверов имеют расширения имени, отличные от COM или EXE. Чаще всего используются расширения SYS, DRV, иногда BIN. На самом деле расширение имени можно задавать любое, так как при описании драйвера в файле CONFIG.SYS указывается его полное имя.
Для драйвера никогда не создается префикс программного сегмента PSP. В начале исходного текста программы-драйвера не ставится директива ORG 100H, как это делается для COM-программы, так как не надо резервировать место для PSP.
Что же представляет из себя загрузочный модуль драйвера?
Как уже было сказано, в начале модуля находится заголовок драйвера. Мы уже немного говорили о нем при описании векторной таблицы связи операционной системы. Приведем формат заголовка:
(0) 4 | next | указатель на заголовок следующего драйвера. Если смещение адреса следующего драйвера равно FFFF, это последний драйвер в цепочке |
(+4) 2 | attrib | атрибуты драйвера |
(+6) 2 | strateg | смещение программы стратегии драйвера |
(+8) 2 | interrupt | смещение программы обработки прерывания для драйвера |
(+10) 8 | dev_name | имя устройства для символьных устройств или количество обслуживаемых устройств для блочных устройств. |
Программист, когда он составляет программу-драйвер, заносит в это поле либо 0FFFFh:0FFFFh, если исходный текст содержит только один драйвер, либо адрес следующего драйвера (в виде дальней ссылки на метку заголовка следующего драйвера). Если исходный текст содержит несколько драйверов, то в заголовке последнего в поле next должно находиться значение 0FFFFh:0FFFFh.
При загрузке драйверов в память операционная система изменит содержимое поля next в заголовках драйверов для того, чтобы это поле указывало на заголовок следующего драйвера в цепочке. (Изменит в памяти, а не в файле драйвера!)
Обычно исходный текст программы содержит один драйвер, и поле next задается следующим образом:
next DD 0FFFFFFFFh
Следующее поле в заголовке драйвера - поле атрибутов драйвера atrib.
Это поле описывает устройство, обслуживаемое данным драйвером. Каждый бит слова отвечает за ту или иную особенность устройства. Прежде чем мы детально рассмотрим назначение всех битов этого слова, заметим, что бит 15 (самый старший бит) указывает, является ли это устройство символьным или блочным.
Следует специально отметить, что все драйверы можно разделить на две группы - драйверы символьных устройств и драйверы блочных устройств. Первые обслуживают устройства посимвольного ввода/вывода, такие как принтеры, клавиатура, контроллеры последовательной передачи данных RS232C и т.д., вторые ориентированы на ввод/вывод блоками - это диски.
Как правило, все нестандартные устройства, начиная от цифрового вольтметра до роботов и систем автоматизации производственных процессов, обслуживаются символьными драйверами. Хотя этот тип драйверов ориентирован на передачу данных посимвольно, для быстродействующих устройств ввода/вывода можно организовать буферизацию (средствами операционной системы).
Блочные драйверы могут Вам потребоваться в основном для обслуживания своих нестандартных дисковых устройств. Например, можно использовать более плотную запись информации на дискетах для повышения их емкости, можно через аппаратуру связи персонального компьютера и ЭВМ серии ЕС создавать псевдо-винчестеры на дисках ЕС (такие "винчестеры" будут восприниматься DOS как обычные стандартные диски). С помощью блочных драйверов можно организовать защиту информации на дисках от несанкционированного доступа, если Ваш драйвер будет шифровать записываемую на диск информацию и предоставлять ее по предъявлению пароля.
Мы рассмотрим оба типа драйверов, каждый раз будем отмечать особенности символьных и блочных устройств.
Приведем назначение отдельных битов слова атрибутов символьного драйвера:
Бит | Назначение |
0 | 1 - драйвер обслуживает стандартное устройство ввода; 0 - этот драйвер не обслуживает стандартное устройство ввода |
1 | 1 - драйвер обслуживает стандартное устройство вывода; 0 - драйвер не обслуживает стандартное устройство вывода |
2 | 1 - это драйвер стандартного устройства NUL; 0 - драйвер не обслуживает устройство NUL |
3 | 1 - драйвер обслуживает часы |
4 | Зарезервировано, бит должен быть равен 0 |
5 | Зарезервировано, бит должен быть равен 0 |
6 | 1 - разрешено использование драйвером функций GENERIC IOCTL (для версий DOS, более поздних, чем 3.2); 0 - функции GENERIC IOCTL не поддерживаются |
7-10 | Эти биты зарезервированы и должны быть равны 0 |
11 | 1 - поддерживаются функции открытия/закрытия устройства (OPEN/CLOSE) для символьных устройств; 0 - функции OPEN/CLOSE для символьных устройств не поддерживаются |
12 | Зарезервировано, бит должен быть равен 0 |
13 | 1 - для символьных устройств поддерживается функция вывода до получения состояния занятости устройства; 0 - функция вывода до состояния занятости не поддерживается |
14 | 1 - поддерживаются функции IOCTL; 0 - функции IOCTL не поддерживаются |
15 | 1 - символьное устройство; 0 - блочное устройство |
Для драйверов блочных устройств формат слова атрибутов другой:
Бит | Назначение |
0 | Зарезервировано, бит должен быть равен 0 |
1 | 1 - драйвер поддерживает 32-битовую адресацию сектора (для версий DOS, начиная с 4.00 и более поздних); если установлен этот бит, поле номера сектора всех запросов является двойным словом, добавляемым в конец заголовка запроса, старое поле номера сектора должно содержать -1); 0 - используется 16-битовая адресация сектора |
2-5 | Эти биты зарезервированы и должны быть равны 0 |
6 | 1 - поддерживаются логические устройства (используется блочными драйверами для управления "виртуальными" флоппи-дисками, создаваемые драйвером DRIVER.SYS в DOS версии 3.2 и более поздних версиях); 0 - логические устройства для блочных драйверов не поддерживаются; |
7-10 | Эти биты зарезервированы и должны быть равны 0 |
11 | 1 - единица в этом бите означает, что драйвер поддерживает функцию проверки замены носителя данных в устройстве (например, замены дискеты); используется для DOS версий 3.00 и более поздних; 0 - для блочных устройств функция проверки замены носителя данных не поддерживается |
12 | Зарезервировано, бит должен быть равен 0 |
13 | 1 - драйвер не использует стандартное IBM-устройство, и необходимо выдать запрос на построение блока параметров BIOSBIOS BPB; 0 - используется IBM-устройство |
14 | 1 - поддерживаются функции IOCTL; 0 - функции IOCTL не поддерживаются |
15 | 1 - символьное устройство; 0 - блочное устройство |
attrib DW 8000h
После слова атрибутов драйвера находятся два очень важных поля: смещение программы стратегии драйвера strateg и смещение программы обработки прерывания interrupt.
Эти две программы используются DOS для организации обращения к драйверу. Для обращения к драйверу DOS формирует в своей области данных запрос, состоящий из заголовка стандартного формата и переменной части запроса, длина и формат которой зависят от типа запроса. После этого DOS считывает из заголовка драйвера значение смещения программы стратегии и передает ей управление, записав в регистры ES:BX адрес заголовка запроса.
Задача программы стратегии - запомнить этот адрес внутри тела драйвера для дальнейшего использования или организовать очередь запросов обслуживания.
Сразу после вызова программы стратегии DOS вызывает программу обработки прерываний, определив ее адрес из поля interrupt заголовка драйвера.
Программа обработки прерывания извлекает только что записанный программой стратегии адрес заголовка запроса и выполняет ту функцию, номер которой записан в запросе. Номер функции находится в заголовке запроса.
Результаты выполнения функции программа прерывания записывает в специально отведенные поля заголовка запроса, и на этом процедура обращения DOS к драйверу завершается.
Формат заголовка запроса будет приведен ниже, а сейчас покажем, как в заголовке драйвера задаются смещения программ стратегии и прерывания:
strateg DW strateg_proc interrupt DW interrupt_proc
Последнее поле заголовка драйвера dev_name имеет различную интерпретацию для символьных и блочных устройств.
Для символьных устройств в этом поле должно располагаться выровненное по левому краю и дополненное до восьми символов пробелами имя устройства. Это имя будет использоваться для обращения к драйверу. Если Вы собираетесь заменить драйвер стандартного символьного устройства DOS на свой, Вы должны записать имя устройства заглавными буквами:
dev_name DB 'AUX '
Для блочных устройств первый байт поля dev_name содержит количество устройств, обслуживаемых данным драйвером, остальные семь байтов не используются:
dev_name DB 1 DB 7 dup(?)
Таким образом, мы выяснили, что драйвер содержит в самом начале заголовок, и где-то дальше должны располагаться программы стратегии и прерывания. (Не следует путать программу прерывания драйвера с программой обслуживания аппаратных или программных прерываний. Хотя программа прерывания драйвера немного похожа на обработчик программных прерываний, назначение этой программы и механизм ее использования совершенно другой).
Что еще может находиться в программе-драйвере?
Это могут быть области данных, используемые драйвером, и подпрограммы, вызываемые программами стратегии и прерывания. Иногда стандартные драйверы переназначают на себя некоторые вектора прерываний, и тогда они содержат в себе обработчики этих прерываний. В области памяти, отведенной операционной системой драйверу, может располагаться стек драйвера, если размер системного стека недостаточен.
На длину драйвера накладывается такое же ограничение, как и на длину COM-программ - 64 килобайта, то есть один сегмент.
Связь драйвера с операционной системой
Рассмотрим теперь более подробно механизм взаимодействия драйвера и операционной системы.
После загрузки драйвер становится как бы частью операционной системы. Все обращения к драйверу DOS выполняет с использованием заголовка драйвера. Для примера приведем вид заголовка символьного драйвера, выполняющего только простейшие функции:
next DD 0FFFFFFFFh attrib DW 8000h strateg DW strateg_proc interrupt DW interrupt_proc dev_name DB 'TESTDRV '
Это символьный драйвер (старший бит поля attrib равен 1), исходный текст содержит только один драйвер (поле next содержит значение 0FFFFFFFFh), имя устройства, которое нужно будет использовать при обращении к драйверу - TESTDRV. Имя устройства не должно совпадать с именем файла, содержащего символьный драйвер, иначе Вы не сможете обратиться к файлу, например, для его переименования - DOS будет работать не с файлом, а с устройством.
Как уже было сказано, перед обращением к драйверу DOS подготавливает заголовок запроса в своей области данных и вызывает программу стратегии, извлекая ее смещение из заголовка драйвера. Программа стратегии обычно очень проста, так как ее задача - запомнить адрес заголовка запроса в области памяти драйвера. Область для хранения адреса заголовка запроса может быть определена следующим образом:
req_off DW ? req_seg DW ?
Тогда программа стратегии должна записать содержимое регистра ES в поле req_seg, а регистра BX - в поле req_off:
strateg_proc: mov cs:req_off,bx mov cs:req_seg,es ret
Драйвер состоит из одного сегмента кодов, поэтому для адресации данных используется сегментный регистр CS.
Запрос операционной системы к драйверу соcтоит из заголовка, имеющего фиксированный формат и длину 13 байт, и переменной части, размер и формат которой зависит от выполняемой функции.
Приведем формат заголовка запроса:
(0) 1 | size | Длина запроса в байтах (длина заголовка запроса плюс длина переменной части запроса) |
(+1) 1 | unit | Номер устройства (используется для блочных устройств, указывает, с каким именно устройством, обслуживаемым драйвером, будет работать операционная система) |
(+2) 1 | cmd | Код команды, которую требуется выполнить (может иметь значение от 0 до 18h) |
(+3) 2 | status | Слово состояния устройства, заполняется драйвером перед возвратом управления операционной системе |
(+5) 8 | reserved | Зарезервировано |
После вызова программы стратегии DOS передает управление программе прерывания (без параметров). Задача программы прерывания - выполнить команду, код которой находится в поле cmd заголовка запроса. Если драйвер блочного устройства обслуживает несколько логических устройств, то в поле unit находится номер устройства, для которого необходимо выполнить команду.
В зависимости от выполняемой команды запрос может содержать другую информацию, необходимую для выполнения команды.
Как результаты выполнения команды возвращаются DOS?
Данные (или адреса данных), полученные драйвером от физического устройства ввода/вывода, помещаются в область переменной части запроса. Кроме того, драйвер должен установить слово соcтояния устройства status в заголовке запроса в соответствии с результатами выполнения команды.
Приведем формат слова состояния устройства:
Бит | Назначение |
0-7 | Код ошибки устройства (если команда выполнена с ошибкой и драйвер установил признак ошибки (бит 15) в единицу, в это поле он должен записать код ошибки). |
8 | Команда выполнена. Этот бит всегда устанавливается драйвером перед тем, как он возвращает управление операционной системе. |
9 | Занято. Этот бит устанавливается обработчиком команды, когда физическое устройство занято выполнением предыдущей операции и поэтому не может выполнить требуемую команду. Этот бит используется также для передачи такой информации, как "буфер клавиатуры не пуст", "среда носителя данных заменяемая" (в команде проверки возможности замены среды носителя данных). |
10-14 | Зарезервировано. |
15 | Признак ошибки. Устанавливается драйвером, когда он не может обработать запрос или произошла физическая либо логическая ошибка при обработке правильного запроса. Биты 0-7 при этом должны содержать код ошибки. |
Код | Описание |
0 | Нарушение защиты от записи. Была предпринята попытка записи информации на защищенное от записи устройство. |
1 | Неизвестное устройство. |
2 | Устройство не готово. |
3 | Неизвестная команда. Затребованная команда не поддерживается драйвером. |
4 | Ошибка CRC. При выполнении команды обнаружена ошибка циклического кода проверки. |
5 | Неправильная длина запроса. Поле длины в заголовке запроса содержит неверное значение. |
6 | Ошибка при поиске дорожки (дорожка не найдена). |
7 | Неизвестный носитель данных. |
8 | Сектор не найден. |
9 | Нет бумаги в принтере. |
0Ah | Ошибка записи. |
0Bh | Ошибка чтения. |
0Ch | Общая ошибка. |
0Dh | Зарезервировано. |
0Eh | Зарезервировано. |
0Fh | Неразрешенная замена диска (только для DOS версии 3.0 и более поздних версий). |
Общая схема действий программы прерывания драйвера такова:
получив управление от операционной системы, программа прерывания сохраняет содержимое всех регистров процессора и считывает номер команды из заголовка запроса;
при необходимости программа считывает дополнительную информацию из области запроса;
затребованная команда выполняется (если она поддерживается драйвером);
если драйвер считывает какие-либо данные от обслуживаемого физического устройства для передачи их DOS, то сами данные или их адреса программа прерывания записывает в область запроса;
программа прерывания устанавливает слово состояния устройства в соответствии с результатами выполнения команды (если драйвер не поддерживает затребованную команду, в слове состояния устройства устанавливаются биты 15 и в биты 0-7 записывается код ошибки 3 - неизвестная команда);
восстанавливается содержимое регистров процессора, и управление возвращается операционной системе с помощью команды возврата из дальней процедуры.
Приведем фрагмент исходного текста программы прерывания, который выполняет описанные выше функции:
interrupt_proc:
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
; Загружаем в регистр AL код команды из заголовка ; запроса и умножаем его на 2 для получения индекса ; в таблице адресов команд
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:
; Если функция не поддерживается драйвером, устанавливаем ; в единицу биты 15 (ошибка), 8 (выполнение команды ; завершено). В биты 0-7 записываем код ошибки 3 - ; неизвестная команда.
or es:word ptr [bx]+3,8103h jmp quit
;=======================================================
; Это пример обработчика команды:
Device_open:
; . . . . . . . . . . ; Некоторые действия для открытия устройства. ; . . . . . . . . . .
jmp quit
;=======================================================
;---выходим, модифицируя байт состояния status в заголовке ; запроса
quit:
or es:word ptr [bx]+3,100h ;устанавливаем бит 8 ;(выполнение команды ;завершено)
pop bp ;восстанавливаем регистры pop di pop si pop dx pop cx pop bx pop ax pop ds pop es ret
В следующем разделе мы подробно рассмотрим все команды, коды которых могут передаваться драйверу через заголовок запроса. Для каждой команды будет приведен формат области запроса.
Связь с драйверами устройств.
Мы уже обращали Ваше внимание на то, что программы не могут обращаться непосредственно к драйверам устройств ввода/вывода. Все обращения к драйверам имеют либо неявный характер (ввод/вывод с помощью функций прерывания INT21h), либо используют специальную функцию DOS с кодом 44h. Эта функция используется для обмена управляющей информацией между драйвером и программой.
Таблица файлов MS-DOS
DOS создает таблицу открытых файлов и помещает ее адрес в поле file_tab векторной таблицы связи. В этой таблице для каждого открытого файла хранится такая информация, как количество файловых чисел (file handle), связанных с данным файлом, режим открытия файла (чтение, запись и т.д.), слово информации об устройстве, указатель на заголовок драйвера, обслуживающего данное устройство, элемент дескриптора файла (дата, время, имя файла, номер начального кластера, распределенного файлу), номер последнего прочитанного кластера и т.д.
Эта информация может пригодиться при организации защиты программы от копирования путем ее привязки к номерам занимаемых программой кластеров. Заметьте, что получить информацию о начальном кластере файла довольно трудно - стандартные средства DOS не предоставляют такой возможности. Приходится работать с диском на уровне секторов, отслеживать FAT, читать напрямую каталог и т.д. Таблица файлов содержит этот номер в явном виде.
Строка файла CONFIG.SYS может содержать оператор FILES=xx. Этот оператор в конечном счете определяет размер таблицы файлов DOS (DFT). Каждая таблица DFT содержит указатель на следущую таблицу и количество управляющих блоков файлов DOS (DFCB). Сами блоки DFCB (по одному для каждого файла) расположены в конце таблицы DFT.
Формат этого блока различается для DOS 3.х и 4.х. Информация по некоторым полям отсутствует. И хотя эти поля отмечены как резервные, на самом деле они используются, но неизвестно как.
Приведем сначала формат таблицы файлов для DOS 3.х:
(0) 4 | next | указатель на следующую таблицу файлов |
(+4) 2 | file_count | количество файлов в этой таблице |
--- Дальше идут блоки DFCB в количестве file_count штук ---- | ||
(0) 2 | handl_num | количество файловых чисел, связанных с данным файлом (file handle) |
(+2) 1 | access_mode | режим доступа к файлу, заданный при открытии файла |
(+3) 2 | reserv1 | зарезервировано |
(+5) 2 | dev_info | информация IOCTL, полученная для устройства, на котором расположен этот файл (подробно формат и назначение этого поля будут рассмотрены в главе, посвященной драйверам) |
(+7) 4 | driver | указатель на драйвер, обслуживающий устройство, содержащее файл |
(+11) 2 | first_clu | номер первого кластера, распределенного файлу |
(+13) 2 | time | время последнего изменения файла в упакованном формате |
(+15) 2 | date | дата последнего изменения файла в упакованном формате |
(+17) 4 | fl_size | размер файла в байтах |
(+21) 4 | offset | текущее смещение внутри файла в байтах |
(+25) 2 | reserv2 | зарезервировано |
(+27) 2 | last_clu | номер только что прочитанного кластера |
(+29) 3 | reserv3 | зарезервировано |
(+32) 11 | filename | имя файла в формате FCB (имя выровнено на левую границу поля, дополнено пробелами до 8 символов, справа к нему прилегает 3 символа расширения без точки) |
(+43) 2 | reserv4 | зарезервировано |
(+45) 2 | ownr_psp | PSP программы, открывшей файл |
(+47) 2 | reserv5 | зарезервировано |
Операционная система MS-DOS версии 4. х отличается расположением поля last_clu, кроме того изменилась длина DFCB:
(0) 4 | next | указатель на следующую таблицу файлов |
(+4) 2 | file_count | количество файлов в этой таблице |
--- Дальше идут блоки DFCB в количестве file_count штук ---- | ||
(0) 2 | handl_num | количество файловых чисел, связанных с данным файлом (file handle) |
(+2) 1 | access_mode | режим доступа к файлу, заданный при открытии файла |
(+3) 2 | reserv1 | зарезервировано |
(+5) 2 | dev_info | информация IOCTL, полученная для устройства, на котором расположен этот файл (подробно формат и назначение этого поля будут расмотрены в главе, посвященной драйверам) |
(+7) 4 | driver | указатель на драйвер, обслуживающий устройство, содержащее файл |
(+11) 2 | first_clu | номер первого кластера, распределенного файлу |
(+13) 2 | time | время последнего изменения файла в упакованном формате |
(+15) 2 | date | дата последнего изменения файла в упакованном формате |
(+17) 4 | fl_size | размер файла в байтах |
(+21) 4 | offset | текущее смещение внутри файла в байтах |
(+25) 2 | reserv2 | зарезервировано |
(+27) 2 | reserv7 | зарезервировано |
(+29) 3 | reserv3 | зарезервировано |
(+32) 1 | reserv4 | зарезервировано |
(+33) 11 | filename | имя файла в формате FCB (имя выравнено на левую границу поля, дополнено пробелами до 8 символов, справа к нему прилегает 3 символа расширения без точки) |
(+44) 2 | reserv5 | зарезервировано |
(+46) 2 | ownr_psp | PSP программы, открывшей файл |
(+48) 2 | reserv6 | зарезервировано |
(+50) 2 | last_clu | номер только что прочитанного кластера |
(+52) 4 | reserv8 | зарезервировано |
Для версии MS/DOS 4.01 файл sysp.h содержит определение структур для работы с таблицами файлов:
typedef struct _DFCB_ { unsigned handl_num; unsigned char access_mode; unsigned reserv1; unsigned dev_info; void far *driver; unsigned first_clu; unsigned time; unsigned date; unsigned long fl_size; unsigned long offset; unsigned reserv2; unsigned reserv7; unsigned reserv3; char reserv4; char filename[11]; char reserv5[6]; unsigned ownr_psp; unsigned reserv6; unsigned last_clu; char reserv8[4]; } DFCB;
typedef struct _DFT_ { struct _DFT_ far *next; unsigned file_count; DFCB dfcb; } DFT;
Приведем текст программ, возвращающих указатели на первый и последующий элементы списка таблиц файлов DOS:
/** *.Name get_fdft * *.Title Получить адрес первой DTF * *.Descr Функция возвращает адрес первой таблицы файлов DOS * *.Params DTF far *get_fdtf(CVT far *cvt) * * cvt - адрес векторной таблицы связи * *.Return Указатель на первый блок DDCB **/
#include <stdlib.h> #include <stdio.h> #include "sysp.h"
DFT far *get_fdft(CVT far *cvt) {
DFT far * dft;
dft = cvt->file_tab;
return(dft);
}
/** *.Name get_ndft * *.Title Получить адрес следующей DTF * *.Descr Функция возвращает адрес следующей * таблицы файлов DOS или 0, если это последняя таблица * *.Params DFT far *get_ndft(DFT far *dft) * * dft - адрес предыдущей таблицы DFT * *.Return Указатель на следующую DFT или 0, если последняя **/ #include <dos.h> #include <stdlib.h> #include <stdio.h> #include "sysp.h"
DFT far *get_ndft(DFT far *dft) {
DFT far * dft_next;
dft_next = dft->next; if(FP_OFF(dft_next) == 0xffff) return((DFT far *)0);
return(dft_next);
}
Для подробной распечатки содержимого таблицы файлов можно использовать следующую программу, которая была проверена в MS-DOS версии 4.01:
#include <dos.h> #include <stdio.h> #include <stdlib.h> #include "sysp.h"
void main(void);
void main(void) {
CVT far *cvt; DFT far *dft; unsigned i,j,k; DFCB far *dfcb; FILE *list;
printf("Информация об открытых файлах DOS\n" "Copyright Frolov A. (C),1990\n");
// Открываем файл для вывода информации о файлах
list=fopen("!dfcb.lst","w+");
fprintf(list,"Информация об открытых файлах DOS\n" "Copyright Frolov A. (C),1990\n\n");
cvt=get_mcvt(); // Адрес векторной таблицы связи dft=get_fdft(cvt); // Адрес начала таблицы файлов
for(;;) { if(dft == (DDCB far *)0) break; // Конец таблицы
i=dft->file_count; fprintf(list,"Таблица файлов DFT: %Fp, в ней %d файлов\n" "===========================================\n", dft,i);
for(j=0;j<i;j++) { // Цикл по файловым // управляющим блокам
dfcb=(&(dft->dfcb))+j; // Адрес DFCB файла
fprintf(list,"\nDFCB файла: %Fp\n\n",dfcb);
fprintf(list,"Имя файла: "); for(k=0;k<11;k++) { fputc(dfcb->filename[k],list); }
fprintf(list,"\nКоличество file handles: %d\n" "Режим доступа: %d\n" "Поле reserv1: %04X\n" "Информация об устройстве: %04X\n" "Адрес драйвера: %Fp\n" "Начальный кластер: %d\n" "Время: %04X\n" "Дата: %04X\n" "Размер файла в байтах: %ld\n" "Текущее смещение в файле: %ld\n" "Поле reserv2: %04X\n" "Последний прочитанный кластер: %d\n" "Сегмент PSP владельца файла: %04X\n" "Поле reserv7: %d\n" "-------------------------------\n\n", dfcb->handl_num, dfcb->access_mode, dfcb->reserv1, dfcb->dev_info, dfcb->driver, dfcb->first_clu, dfcb->time, dfcb->date, dfcb->fl_size, dfcb->offset, dfcb->reserv2, dfcb->last_clu, dfcb->ownr_psp, dfcb->reserv7);
} dft=get_ndft(dft); } fclose(list); exit(0); }
Описание содержимого таблицы файлов будет записано в файл с именем "!dfcb.lst".
Таблица связи управляющих блоков MS-DOS
Операционная система MS-DOS, подобно операционным системам для больших ЭВМ серии ЕС, содержит векторную таблицу связи основных управляющих блоков. К сожалению, в руководстве по MS-DOS ничего не говорится об этой таблице. Мы попытаемся в некоторой степени восполнить этот пробел, так как изучение векторной таблицы связи позволит глубже осознать принципы работы операционной системы. Информация из векторной таблицы связи будет полезной для составления программ отображения распределения памяти, вывода списка загруженных драйверов, списка устройств прямого доступа и т.д.
Для получения адреса векторной таблицы связи можно воспользоваться недокументированной внутренней функцией 52h прерывания 21h. Для версий MS-DOS 2.х, 3.х, 4.00, 4.01 и 5.0 после вызова этой функции регистры ES:BX будут содержать искомый адрес. Так как описание этой функции отсутствует в руководстве по MS-DOS, в следующих версиях операционной системы возможно придется искать другой способ получения адреса векторной таблицы связи. Может также измениться формат этой таблицы.
Функции для получения адреса векторной таблицы связи:
;** ;.Name get_cvt ; ;.Title Получить адрес векторной таблицы связи ; ;.Descr Функция возвращает адрес векторной таблицы связи ; в регистрах ES:BX для DOS версий 2.х, 3.х, 4.00, ; 4.01 ; ;.Params Нет ; ;.Return ES - сегмент векторной таблицы связи, ; BX - смещение векторной таблицы связи ;** PUBLIC get_cvt .MODEL tiny
.CODE get_cvt proc near
mov ax,5200h int 21h ret
get_cvt endp end
/** *.Name get_cvt * *.Title Получить адрес векторной таблицы связи * *.Descr Функция возвращает адрес векторной таблицы связи * для DOS версий 2.х, 3.х, 4.00, 4.01 * *.Params Нет * *.Return Указатель на векторную таблицу связи **/
#include <dos.h> #include <stdio.h> #include "sysp.h"
void far *get_cvt(void) { union REGS inregs, outregs; struct SREGS segregs;
inregs.h.ah = 0x52; intdosx( &inregs, &outregs, &segregs );
return(FP_MAKE(segregs.es,outregs.x.bx)); }
Функция get_cvt возвращает адрес поля dev_cb. Для удобства работы с векторной таблицей связи определим тип CVT:
#pragma pack(1)
typedef struct _CVT_ { unsigned mcb_seg; void far *dev_cb; void far *file_tab; void far *clock_dr; void far *con_dr; unsigned max_btbl; void far *disk_buf; void far *drv_info; void far *fcb_tabl; unsigned fcb_size; unsigned char num_bdev; unsigned char lastdriv; } CVT;
#pragma pack()
Эта структура содержит описание полей векторной таблицы связи для MS-DOS версий 3.х, 4.х и 5.0.
Директива #pragma pack(1) предназначена для выравнивания полей структуры на границу байта. Эта директива необходима потому, что по умолчанию транслятор Microsoft выравнивает поля в структуре на границу 16-ти битового слова. Неправильное выравнивание может привести к тому, что поля структуры не будут располагаться в памяти последовательно.
Заметьте, что функция get_cvt возвращает указатель на поле dev_cb. Модифицируем эту функцию так, чтобы можно было использовать для обращения к полям векторной таблицы связи структуру _CVT_:
/** *.Name get_mcvt * *.Title Получить адрес векторной таблицы связи * *.Descr Функция возвращает адрес векторной таблицы связи * для DOS версий 2.х, 3.х, 4.00, 4.01 * *.Params Нет * *.Return Указатель на векторную таблицу связи **/
#include <dos.h> #include <stdio.h> #include "sysp.h"
CVT far *get_mcvt(void) { union REGS inregs, outregs; struct SREGS segregs; inregs.h.ah = 0x52; intdosx( &inregs, &outregs, &segregs ); return((CVT far*)FP_MAKE(segregs.es,outregs.x.bx-2)); }
Ниже будут подробно описаны отдельные поля векторной таблицы связи и приведены примеры использования информации из этой таблицы.
Таблица векторов прерываний
Для того чтобы связать адрес обработчика прерывания с номером прерывания, используется таблица векторов прерываний, занимающая первый килобайт оперативной памяти - адреса от 0000:0000 до 0000:03FF. Таблица состоит из 256 элементов - FAR-адресов обработчиков прерываний. Эти элементы называются векторами прерываний. В первом слове элемента таблицы записано смещение, а во втором - адрес сегмента обработчика прерывания.
Прерыванию с номером 0 соответствует адрес 0000:0000, прерыванию с номером 1 - 0000:0004 и т.д. Для программиста, использующего язык Си, таблицу можно описать следующим образом:
void (* interrupt_table[256])();
Инициализация таблицы происходит частично BIOSпосле тестирования аппаратуры и перед началом загрузки операционной системой, частично при загрузке DOS. DOS может переключить на себя некоторые прерывания BIOS.
Займемся теперь содержимым таблицы векторов прерываний. Приведем назначение некоторых наиболее важных векторов:
Номер | Описание |
0 | Ошибка деления. Вызывается автоматически после выполнения команд DIV или IDIV, если в результате деления происходит переполнение (например, при делении на 0). DOS обычно при обработке этого прерывания выводит сообщение об ошибке и останавливает выполнение программы. Для процессора 8086 при этом адрес возврата указывает на следующую после команды деления команду, а в процессоре 80286 - на первый байт команды, вызвавшей прерывание. |
1 | Прерывание пошагового режима. Вырабатывается после выполнения каждой машинной команды, если в слове флагов установлен бит пошаговой трассировки TF. Используется для отладки программ. Это прерывание не вырабатывается после выполнения команды MOV в сегментные регистры или после загрузки сегментных регистров командой POP. |
2 | Аппаратное немаскируемое прерывание. Это прерывание может использоваться по-разному в разных машинах. Обычно вырабатывается при ошибке четности в оперативной памяти и при запросе прерывания от сопроцессора. |
3 | Прерывание для трассировки. Это прерывание генерируется при выполнении однобайтовой машинной команды с кодом CCh и обычно используется отладчиками для установки точки прерывания. |
4 | Переполнение. Генерируется машинной командой INTO, если установлен флаг OF. Если флаг не установлен, то команда INTO выполняется как NOP. Это прерывание используется для обработки ошибок при выполнении арифметических операций. |
5 | Печать копии экрана. Генерируется при нажатии на клавиатуре клавиши PrtScr. Обычно используется для печати образа экрана. Для процессора 80286 генерируется при выполнении машинной команды BOUND, если проверяемое значение вышло за пределы заданного диапазона. |
6 | Неопределенный код операции или длина команды больше 10 байт (для процессора 80286). |
7 | Особый случай отсутствия математического сопроцессора (процессор 80286). |
8 | IRQ0 - прерывание интервального таймера, возникает 18,2 раза в секунду. |
9 | IRQ1 - прерывание от клавиатуры. Генерируется при нажатии и при отжатии клавиши. Используется для чтения данных от клавиатуры. |
A | IRQ2 - используется для каскадирования аппаратных прерываний в машинах класса AT. |
B | IRQ3 - прерывание асинхронного порта COM2. |
C | IRQ4 - прерывание асинхронного порта COM1. |
D | IRQ5 - прерывание от контроллера жесткого диска для XT. |
E | IRQ6 - прерывание генерируется контроллером флоппи-диска после завершения операции. |
F | IRQ7 - прерывание принтера. Генерируется принтером, когда он готов к выполнению очередной операции. Многие адаптеры принтера не используют это прерывание. |
10 | Обслуживание видеоадаптера. |
11 | Определение конфигурации устройств в системе. |
12 | Определение размера оперативной памяти в системе. |
13 | Обслуживание дисковой системы. |
14 | Последовательный ввод/вывод. |
15 | Расширенный сервис для AT-компьютеров. |
16 | Обслуживание клавиатуры. |
17 | Обслуживание принтера. |
18 | Запуск BASIC в ПЗУ, если он есть. |
19 | Загрузка операционной системы. |
1A | Обслуживание часов. |
1B | Обработчик прерывания Ctrl-Break. |
1C | Прерывание возникает 18.2 раза в секунду, вызывается программно обработчиком прерывания таймера. |
1D | Адрес видеотаблицы для контроллера видеоадаптера 6845. |
1E | Указатель на таблицу параметров дискеты. |
1F | Указатель на графическую таблицу для символов с кодами ASCII 128-255. |
20-5F | Используется DOS или зарезервировано для DOS. |
60-67 | Прерывания, зарезервированные для пользователя. |
68-6F | Не используются. |
70 | IRQ8 - прерывание от часов реального времени. |
71 | IRQ9 - прерывание от контроллера EGA. |
72 | IRQ10 - зарезервировано. |
73 | IRQ11 - зарезервировано. |
74 | IRQ12 - зарезервировано. |
75 | IRQ13 - прерывание от математического сопроцессора. |
76 | IRQ14 - прерывание от контроллера жесткого диска. |
77 | IRQ15 - зарезервировано. |
78 - 7F | Не используются. |
80-85 | Зарезервированы для BASIC. |
86-F0 | Используются интерпретатором BASIC. |
F1-FF | Не используются. |
IRQ0 - IRQ15 - это аппаратные прерывания, о них будет рассказано позже.
Управление памятью.
DOS управляет распределением памяти с помощью блоков управления памятью MCB (Memory Control Block). Вся память разбивается на блоки различного размера, которым предшествует блок MCB, содержащий характеристики данного блока памяти (например, его размер).
Программа может динамически получать и освобождать области памяти с помощью функций 48h и 49h соответственно. Кроме того, можно изменять размер блока, выделенного операционной системой программе. Это делает функция 4Ah.
Детально механизм управления памятью будет рассмотрен в главе 2 при описании векторной таблицы связи DOS.
Управление программами.
DOS предоставляет программам возможность организовать запуск других программ или загрузку и выполнение программных оверлеев. Для этого служит функция 4Bh.
Для завершения работы программа должна также использовать одну из специальных функций DOS. Функция 4Ch, завершая работу программы, позволяет передать операционной системе некоторое число, называемое кодом завершения программы. Это число может быть затем проанализировано в пакетном файле командой IF ERRORLEVEL. Если одна программа запускает другую, то первая может получить код завершения второй с помощью функции 4Dh.
Для того чтобы завершающаяся программа осталась в оперативной памяти (т.е. стала резидентной), она должна вызвать прерывание INT27h или воспользоваться функцией 31h.
Мы приведем различные примеры запуска программ из программ и научимся составлять резидентные программы.
- Установить активное логическое устройство
Эти команды обрабатываются только теми драйверами, у которых в слове атрибутов установлен бит 6 - поддержка логических устройств. Команды используются в DOS версии 3.2 и в более поздних версиях.
Команды обеспечивают метод опроса номера текущего активного логического устройства на физическом диске или установления активного логического устройства.
Приведем формат запроса:
(0) 13 | header | Заголовок запроса. |
(+13) 1 | unit | Код логического устройства, которое должно стать активным при использовании команды 24, или код активного устройства, помещаемый драйвером по команде 23. |
(+14) 1 | cmd | Код команды. |
(+15) 4 | status | Слово состояния. |
(+19) 4 | reserved | Зарезервировано. |
По команде 23 (получить активное логическое устройство) драйвер должен поместить идентификатор устройства в поле unit, для устройства А: помещается 1, для В: - 2 и т.д. Если драйвер управляет единственным устройством, он должен записать в поле unit ноль.
После обзора команд перейдем к описанию функции 44h прерывания 21h. Эта функция предназначена для управления вводом/выводом и обладает широкими возможностями.
- Установить характеристики курсора.
С помощью этой функции вы можете установить размер и форму курсора, сделать курсор мигающим или убрать его совсем.
- Установить положение курсора.
Эта функция позволяет управлять расположением курсора на экране, в частности, один из способов убрать курсор - расположить его за пределами экрана, например, на несуществующей 26 строке.
Векторная таблица связи MS-DOS
2.1.
2.2.
2.3.
2.4.
2.5.
2.6.
Вслед за областью данных BIOS в оперативной памяти IBMPC располагается область данных DOS. Здесь располагаются внутренние переменные и структуры DOS. Основные структуры данных организованы в виде дерева. Корнем является векторная таблица связи, которая содержит адреса всех остальных структур: список блоков управления памятью (MCB), список блоков управления устройствами DOS, таблицу файлов, дисковые буфера.
Векторная таблица связи содержит и другую полезную информацию, открывающую доступ практически ко всем внутренним структурам данных операционной системы. Можно, например, получить доступ ко всем резидентным и загружаемым драйверам операционной системы. Можно узнать, какие дисковые устройства установлены в системе и каковы их характеристики. Зная форматы управляющих блоков операционной системы, можно анализировать ошибочные ситуации, возникающие при отладке программного обеспечения, разрабатывать программы, отображающие внутреннее состояние системы и конфигурацию устройств.
Понимание внутренней структуры MS-DOS - едва ли не самое важное для профессионального системного программирования, поэтому это первое, на чем мы подробно остановимся. В последующих главах мы будем постоянно пользоваться этой информацией.
Многочисленные управляющие блоки, которые использует файловая система и BIOS при работе с дисковыми устройствами, будут описаны в книге 3, посвященной файловой системе.
Прикладная программа может пользоваться услугами DOS, вызывая прерывание INT 21H. Это прерывание имеет несколько десятков функций, которые и представляют собой интерфейс между DOS и прикладной программой. С помощью этих функций прикладная программа получает доступ к файловой системе, может обращаться к драйверам устройств, получать и устанавливать системные параметры, работать с дисплеем и клавиатурой и т.д.
Для полного использования возможностей прерываний DOS необходимы сведения о форматах различных управляющих блоков, используемых этими прерываниями.
Внутренняя организация MS-DOS
1.1.
1.2.
1.3.
1.4.
1.5.
1.6.
Ввод/вывод на консоль оператора.
Консоль оператора состоит из двух устройств - клавиатуры и дисплея. Эти два устройства обслуживаются одним драйвером - драйвером консоли CON. Т.е. можно считать, что в компьютере имеется устройство - консоль - с именем CON.
Операционная система обслуживает консоль с помощью функций прерывания 21h, обеспечивающих ввод и вывод символов на устройство CON. Для работы с физической клавиатурой и дисплейным адаптером этот драйвер использует прерывания BIOS.
- Выбрать активную страницу дисплейной памяти.
Компьютер хранит, как правило, не один отображаемый образ экрана, а несколько. Для этого видеопамять (память для хранения видеоизображения, находится на плате видеоконтроллера) разбивается на так называемые страницы. Отображается только активная страница видеопамяти.
Используя механизм страниц, программа может заранее подготовить изображение в неактивной странице, затем сделать подготовленную страницу активной. Изображение новой страницы мгновенно появится на экране.
Некоторые отладчики программ используют одну страницу видеопамяти для отлаживаемой программы, другую - для выдачи своих диагностических сообщений.
Вывод на принтер (параллельный порт).
BIOS содержит простейшую поддержку принтера - три функции прерывания INT17h. Это функция 01h - инициализация принтера, 02h - опрос состояния принтера и 00h - вывод символа на принтер.
Поскольку к персональному компьютеру можно подключить несколько последовательных портов, при обращении к принтеру следует указывать номер порта.
Вызов резидентной программы
Возможны два способа вызова резидентной программы: либо прикладная программа выдает прерывание, обрабатываемое TSR-программой, либо сама TSR-программа отслеживает нажатие клавиш оператором компьютера и, в случае нажатия определенной клавиши (или комбинации клавиш), запускает диалоговую часть резидентной программы.
Первый способ используется прикладными программами, работающими с нестандартной аппаратурой, второй - диалоговыми резидентными программами.
Самый простой способ организовать связь оператора с TSR-программой - это использовать прерывание по нажатию клавиши печати содержимого экрана PrtScr. Но при этом вы теряете возможность распечатки содержимого экрана. Впрочем, иногда вам и нужно, чтобы вместо печати содержимое экрана записывалось, например, в файл. Или вы можете составить свою собственную программу печати содержимого экрана (используя графическую печать или какие-либо особенности принтеров).
Если вы используете прерывание 9, вырабатываемое при каждом нажатии на клавишу для определения момента запуска диалога, не следует забывать об организации цепочки прерываний, с тем чтобы после анализа (или перед анализом) передать управление стандартному обработчику 9-го прерывания.
Подробно о работе с клавиатурой и о 9-м прерывании будет рассказано во втором томе книги.
- Задание видеорежима.
Эта функция обычно вызывается первой при начале работы с дисплейным адаптером или при необходимости изменить текущий режим адаптера. Что здесь имеется в виду?
Напомним, что дисплейный адаптер может работать либо в текстовом, либо в графическом режиме. На самом деле существует несколько текстовых и несколько графических режимов, различающихся количеством строк и столбцов, способом представления цвета и т.д.
В процессе инициализации BIOS задает начальный режим адаптера исходя из его типа. Если Вашей программе нужен другой режим, отличный от исходного, она должна использовать эту функцию. При этом необходимо учитывать, что дисплейные адаптеры могут поддерживать не все режимы.
- Закрыть устройство
Для того чтобы драйвер мог использовать эти команды, бит 11 в слове атрибутов устройства в заголовке драйвера должен быть установлен в 1.
Драйверы символьных устройств по этой команде могут посылать инициализирующие последовательности символов на устройства (например, на принтер могут посылаться команды установки типа шрифта, формата бумаги и т.д.) или устанавливать устройство в исходное состояние. Можно также обнаруживать попытки получения многократного доступа к устройству. В этом случае вторая команда открытия должна возвратить ошибку (если многократный доступ к устройству запрещен).
Драйвер блочного устройства с помощью этих команд может производить подсчет открытых файлов. Если содержимое счетчика открытых файлов для данного устройства равно 0, то открытых файлов на этом устройстве нет. Если драйвер теперь сбросит все буфера на диск, пользователь сможет заменить диск на другой. Однако если программы открывают файлы без их закрытия, то могут возникнуть ошибки при использовании этого метода.
Стандартные устройства CON, AUX, PRN всегда открыты.
Запрос для этих команд состоит только из заголовка.
- Запись строки.
Для машин класса AT и выше при наличии дисплейных адаптеров EGA или VGA эта функция позволяет вывести на экран произвольную строку символов заданной длины, с заданным атрибутом и в заданном месте экрана. Можно также задать номер дисплейной страницы.
Если вы не можете использовать эту функцию (Ваш компьютер - XT или дисплейный адаптер - CGA), единственный способ вывести на экран строку символов с помощью прерывания INT10h - вызывать в цикле функции 09h, 0Ah или 0Eh для вывода строки по одному символу.
Запуск программ из программ
Ваша программа может при необходимости запустить другую программу формата EXE или COM. Для ассемблерных программ существует функция 4Bh прерывания INT 21h, для программ, составленных на языке Си - разнообразные функции, входящие в состав стандартной библиотеки. Сначала рассмотрим запуск программ при помощи функции 4Bh прерывания INT 21h.
Содержимое регистров перед вызовом прерывания:
AH = 4BH AL - код подфункции (0, 1, 2, 3) DS:DX - указатель на путь к запускаемой программе ES:BX - указатель на блок параметров EPB
После возврата из прерывания флаг CF устанавливается в 0, если ошибок не было, и в 1 при обнаружении ошибок. Регистр AX в случае наличия ошибок содержит код ошибки:
1 | неверный код подфункции; |
2 | файл запускаемой программы не найден; |
3 | путь не найден; |
4 | слишком много открытых файлов; |
5 | нет доступа; |
8 | нет памяти для загрузки программы; |
10 | длина блока среды больше 32 килобайт; |
11 | плохой формат запускаемого EXE-файла. |
Функция 4Bh прерывания 21h имеет четыре подфункции с номерами от 0 до 3:
0 | загрузить и выполнить программу; |
1 | загрузить, но не выполнять программу (внутренняя подфункция для DOS 3.х); |
2 | загрузить, но не выполнять программу (внутренняя подфункция для DOS 2.х); |
3 | загрузить программу как оверлей (не создавать PSP). |
Для функции 0 регистры DS:DX должны указывать на полный путь запускаемой программы в формате ASCIIZ ( т.е. текстовая строка, закрытая двоичным нулем). Блок параметров EPB (Exec Parameter Block) в этом случае имеет следующий формат:
(0) 2 | seg_env | сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы |
(+2) 4 | cmd | FAR-адрес строки параметров для запускаемой программы. Эта строка должна иметь такой же формат, как и в PSP, т.е. вначале идет байт со значением, равным количеству символов в строке параметров, а затем - сама строка параметров |
(+6) 4 | fcb1 | адрес блока FCB, который будет помещен в PSP со смещением 5Ch (в PSP помещается блок, а не адрес!) |
(+10) 4 | fcb2 | адрес блока FCB, который будет помещен в PSP со смещением 6Ch. |
Запущенной программе доступны все файлы, открытые родительской программой.
Если родительская программа сама формирует среду для дочерней программы, она должна подготовить новую среду на границе параграфа и поместить значение сегментного адреса в поле seg_env блока EPB.
Приведем простую программу, которая запускает программу с именем PARM.COM из текущего каталога. Программу PARM.COM мы только что рассматривали, эта программа выводит на экран полученные ей в командной строке параметры.
.MODEL small DOSSEG
.STACK 100h
.DATA
path db "PARM.COM",0 command_line db 8,"Parm Str"
epb dw 0 cmd_off dw ? cmd_seg dw ? fcb1 dd ? fcb2 dd ?
.CODE .STARTUP
mov bx,OFFSET command_line ; адрес командной mov cmd_off,bx ; строки для блока EPB mov cmd_seg,ds
mov ax,ds mov es,ax
mov bx,OFFSET epb ; ES:BX указывают на EPB mov dx,OFFSET path ; DS:DX указывают на путь ; запускаемой программы
mov ax, 4B00h ; AH = 4Bh ; AL = 0 загрузить и выполнить int 21h
.EXIT 0
END
Эта программа использует модель памяти SMALL, и ее загрузочный модуль имеет формат EXE. При редактировании был указан стандартный для Quick C 2.01 размер памяти, требуемый для программы. Если попытаться использовать формат COM в модели TINY, то окажется, что вся память распределена COM-программе и для дочерней программы не осталось места.
Следующая программа освобождает всю неиспользуемую ей память, после чего на освободившееся место загружает программу PARM.COM:
.MODEL tiny DOSSEG
.STACK 100h
.DATA
path db "PARM.COM",0 command_line db 8,"Parm Str"
epb dw 0 cmd_off dw ? cmd_seg dw ? fcb1 dd ? fcb2 dd ?
.CODE .STARTUP ; ; Освобождаем лишнюю память за концом программы ; mov bx,OFFSET last ; смещение конца ; программы
mov cl,4 ; вычисляем длину в ; параграфах shr bx,cl
add bx,17 ; добавляем 1 параграф для ; выравнивания и 256 байт ; для стека
mov ah, 4Ah ; изменяем размер выделенного int 21h ; блока памяти
mov ax,bx ; установка нового значения shl ax,cl ; указателя стека dec ax mov sp,ax
mov bx,OFFSET command_line ; адрес командной mov cmd_off,bx ; строки для ; блока EPB mov cmd_seg,ds
mov ax,ds mov es,ax
mov bx,OFFSET epb ; ES: BX указывают на EPB mov dx,OFFSET path ; DS:DX указывают на путь ; запускаемой программы
mov ax, 4B00h ; AH = 4Bh ; AL = 0 загрузить и ; выполнить int 21h
.EXIT 0 last: db ? END
Для изменения размера выделенного программе блока памяти мы использовали функцию 4Ah прерывания 21h.
Подфункции 1 и 2 прерывания 4Bh используются DOS (это внутренние подфункции DOS). Мы приведем недокументированный формат блока EBP для этих функций.
Для подфункнкции 1:
(0) 2 | seg_env | сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы |
(+2) 4 | cmd | FAR-адрес строки параметров для запускаемой программы. |
(+6) 4 | fcb1 | адрес блока FCB, который будет помещен в PSP со смещением 5Ch |
(+10) 4 | fcb2 | адрес блока FCB, который будет помещен в PSP со смещением 6Ch. |
(+14) 4 | ss_sp | это поле будет содержать значение SS:SP после возврата |
(+18) 4 | entry_p | адрес точки входа в загруженную программу (CS:IP) |
(0) 2 | seg_env | сегментный адрес среды, которая создается родительской программой для запускаемой программы. Если в этом поле находится 0, то для запускаемой программы копируется среда родительской программы |
(+2) 4 | cmd | FAR-адрес строки параметров для запускаемой программы. |
(+6) 4 | fcb1 | адрес блока FCB, который будет помещен в PSP со смещением 5Ch |
(+10) 4 | fcb2 | адрес блока FCB, который будет помещен в PSP со смещением 6Ch. |
(0) 2 | seg_env | сегментный адрес, по которому загружается программа |
(+2) 4 | reloc | фактор перемещения, аналогичен элементу таблицы перемещений в заголовке EXE-файла |
Следующая демонстрационная программа загружает программу PARM.COM_как оверлей без передачи ей управления:
.MODEL small DOSSEG
.STACK 100h
.DATA
path db "PARM.COM",0
epb dw 0 reloc dd 0
.CODE .STARTUP
mov ax,ds mov es,ax
mov bx,SEG buff mov epb,bx
mov bx,OFFSET epb ; ES:BX указывают на EPB mov dx,OFFSET path ; DS:DX указывают на путь ; загружаемой программы
mov ax, 4B03h ; AH = 4Bh ; AL = 0 загрузить оверлей int 21h
.EXIT 0
buff: dd 100 dup(?)
END
Программа загружается в буфер buff.
Пользователи языка Си имеют в своем распоряжении три возможности запустить программу.
Самый простой способ - использовать функцию system(). Эта функция может выполнить любую команду DOS или любую программу, пакетный файл. Например:
system("FORMAT A:");
При использовании этой функции должен быть доступен COMMAND.COM. К сожалению, хотя system и возвращает код завершения, по нему нельзя сделать вывод о том, как была выполнена запускаемая программа. Если в качестве аргумента функции будет передано неправильное имя, на экране появится сообщение:
Bad command or file name
Код возврата в этом случае будет 0 - как будто все нормально!
Другие две возможности запустить программу - использовать функции spawn и exec. Функция spawn и ее разновидности запускают программу как дочерний процесс. Функция exec загружает новую программу как оверлей на место старой и передает ей управление без возврата. После завершения дочерней программе управление будет передано COMMAND.COM или программе, которая запустила родительскую программу.
Семейство функций spawn обеспечивает запуск дочерней программы с родительской или со специально сформированной средой. Кроме того, в файле process.h описаны параметры, которые можно передать функции spawn:
P_WAIT | выполнение родительской программы задерживается до завершения дочерней программы. |
P_NOWAIT | родительская программа продолжает выполнение сразу после запуска дочерней. Этот параметр имеет смысл только для операционных систем OS/2, UNIX, в которых поддерживается мультизадачность. |
P_OVERLAY | загружает программу как оверлей и передает ей управление. Этот режим соответствует функции exec в том смысле, что родительская программа не получит управления после завершения дочерней. |
В качестве примера использования функций запуска программы рассмотрим возможное решение проблемы создания HELP-системы для прикладной программы.
С помощью текстового редактора можно создать справочную базу данных в формате утилиты Microsoft HELPMAKE, затем, запуская в нужный момент диалоговую утилиту работы с базой данных Microsoft Quick Help QH.EXE, можно получить нужную справку.
Утилита QH использует базы данных, описанные в переменной среды HELPFILES. Мы будем использовать либо родительскую среду, где находится значение переменной HELPFILES по умолчанию, либо указывать новое значение для этой переменной.
Приведенная ниже программа используется для получения справки о функции стандартной библиотеки printf, поиск производится в HELP-базе QuickC:
#include <stdio.h> #include <conio.h> #include <process.h>
main() { int r;
// Получаем справку о функции printf, // справочная база данных расположена // в каталоге d:\qc2\bin
r = help("HELPFILES=d:\\qc2\\bin;","printf");
if( r == -1 ) printf( "Невозможно запустить процесс" ); else printf( "\nПроцесс завершен" ); exit(r); }
/** *.Name help * *.Title Получить справку по заданному контексту * *.Descr Функция получает в качестве параметров * переменную среды, указывающую на путь * к справочной базе данных и указатель * на строку контекста для поиска в базе. * Затем запускается как дочерний процесс * утилита Microsoft Quick Help QH.EXE, для * которой формируются среда и параметры. * *.Params int help(char *help_file, char *help_topic); * * help_file - переменная среды, указывающая * на путь к справочной базе * * help_topic - контекст для поиска в базе * * *.Return 0 при успешном запуске процесса * -1 не удалось запустить процесс **/
int help(char *help_file, char *help_topic) {
char *env[] = { "", NULL }; // Среда, которую // получит QH при запуске
if(*help_file != 0) { env[0] = help_file; // Формируем среду для QH
// Запускаем утилиту
return(spawnlpe(P_WAIT,"QH","QH", "-u",help_topic,NULL,env)); } else {
// Если переменная среды не задана, // используем родительскую среду
return(spawnlp(P_WAIT,"QH","QH", "-u",help_topic,NULL));
} }
Подробная информация об использовании утилит HELPMAKE и QH приводится в документации на Microsoft C 6.0.
Завершение работы программы
Старые версии DOS (до 2.0) требовали выполнения достаточно сложной процедуры для завершения программы. В начале работы программы необходимо было сохранить адрес PSP, затем, перед завершением работы поместить этот адрес в стек, поместить туда же слово 0000 и выполнить команду дальнего возврата. Управление при этом передается в начало PSP, где находится команда INT 20h.
Для версий DOS, начиная с 2.0, существуют более удобные способы - использование напрямую команды INT 20h или функции 0 прерывания 21h (CS при этом должен указывать на PSP, поэтому этот способ хорош для COM-программ), или функции 4Ch прерывания 21h в любое время и с любым содержимым регистров.
Последний способ рекомендуется для использования и имеет еще то преимущество, что позволяет передать родительской программе (например, COMMAND.COM) код завершения. Этот код доступен для анализа в пакетных файлах командой IF ERRORLEVEL.
Приведенные в книге примеры программ на языке ассемблера содержат директиву .EXIT. Эта директива завершает выполнение программы с помощью функции 4Ch и позволяет передать код завершения.
Если Ваша программа запустила дочернюю программу и та завершилась с передачей кода возврата, то родительская программа может определить этот код с помощью функции 4Dh прерывания 21h. Эта функция возвращает код в регистре AX.
Программа, написанная на языке Си, может завершаться с помощью return в функции main или с помощью exit в любом месте программы. При этом также возможна передача кода возврата.
Существуют еще способы завершения работы программы, при которых программа (или ее часть) остается резидентной в памяти. Это вызов прерывания INT 27H или функции 31h прерывания INT 21h. Об этом будет подробно рассказано в разделе, посвященном резидентным программам.