Системное программное обеспечение персональных ЭВМ

         

Доступ к регистрам


2.1. Доступ к регистрам

1). В любой программе, разрабатываемой в среде Турбо-Си по умолчанию предопределены имена: _AX, _AL, _AH, _BX, _BL, _BH, _CX, _CL, _CH, _DX, _DL, _DH, _ES, _SS, _CS, _DS, _SI, _DI, _BP, _SP. Эти переменные (т.наз. псевдорегистры) типа int и char могут быть использованы для доступа к регистрам микропроцессора.

2). При обращениях к прерываниям передача входных и выходных параметров происходит через регистры. Для этих целей используются следующие описания данных, находящиеся в файле DOS.H (там же находятся и описания всех рассматриваемых далее функций и макросов).

Объединение REGS, используемое для задания содержимого регистров общего назначения:



Доступ к оперативной памяти


2.2. Доступ к оперативной памяти

В Си-программе программист имеет возможность прочитать и записать данные по абсолютному адресу памяти, а также определить физические адреса памяти, по которым располагаются переменные его программы.

Для чтения данных из памяти испольэуются функции:



Порты ввода-вывода


2.3. Порты ввода-вывода

Для чтения данных из порта испольэуются функции:



Генерация программных прерываний


3.1. Генерация программных прерываний

Для этих целей в Турбо-Си имеется целый ряд функций.

Функция генерации программного прерывания:



Программы обработки прерываний


3.2. Программы обработки прерываний

На языке Си мы можем писать и собственные программы обработки прерываний. Такая программа является функцией в программе пользователя и должна иметь описание типа:



Перехват прерываний


3.3. Перехват прерываний

Пусть функция обработки прерывания у нас уже разработана, теперь следует обеспечить ее вызов. Вспомним, что механизм прерываний ПЭВМ работает следующим образом. При поступлении прерывания с номером NN в стеке запоминаются регистр флагов и регистры CS:IP, а из оперативной памяти по адресу NN*4 выбирается четырехбайтный адрес, по которому передается управление. Этот адрес называется вектором прерывания, а первые 1024 байт оперативной памяти - таблицей векторов прерываний. Вектор хранится в памяти в следующем порядке: младший байт смещения - старший байт смещения - младший байт сегмента - старший байт сегмента. Таким образом, если мы хотим, чтобы по прерыванию NN получала управление наша interrupt-функция, мы должны записать ее адрес на место вектора NN. Технику этой записи мы рассмотрим здесь же, но чуть ниже, а прежде обсудим некоторые проблемы, которые могут при этом возникнуть.

До нашего вмешательства по адресу вектора NN находился адрес системной (например, из состава BIOS) программы обработки прерывания NN. После того как мы запишем на его место адрес своей программы обработки прерывания, по прерыванию NN управление будет передаваться нашей interrupt-функции (отсюда и выражение - "перехват прерывания"). Но, возможно, те действия, которые выполнял системный обработчик прерывания NN были не лишними, а может быть, и жизненно необходимыми для функционирования системы. Чтобы не дублировать эти действия в своем обработчике прерывания (тем более, что мы не всегда можем иметь о них исчерпывающую информацию), необходимо прежде, чем записывать свой адрес на место вектора, сохранить где-то тот адрес, который там был записан (адрес системного обработчика). Первым (после сохранения регистров) действием нашего обработчика должна быть передача управления по этому адресу, то есть вызов системного обработчика прерывания. Такой подход в некоторых источниках называется "дополнением прерывания". Кстати, в этом случае мы можем не сбрасывать контроллер прерываний, так как эта операция выполняется системным обработчиком.
Когда программа, включающая в себя пользовательскую обработку прерывания, заканчивается, она должна восстановить значение перехваченного вектора, то есть, системную обработку прерывания.
При программировании на языке Ассемблера возникает трудность, связанная с вызовом системного обработчика. Возврат из системного обработчика производится командой IRET, следовательно, вызывать его просто командой CALL нельзя (для IRET в стеке должно быть три слова, а CALL записывает в стек только два). В некоторых источниках почему-то рекомендуется сохранять старый вектор также в таблице векторов на каком-либо свободном ее месте и вызывать старый обработчик командой INT. Проще, однако, перед выполнением команды CALL занести в стек содержимое регистра флагов (PUSHF), что и обеспечивает Турбо-Си для вызовов функций, имеющих описатель interrupt.
Рассмотренный подход дополнения прерываний можно легко распространить на случай, когда несколько одновременно находящихся в ОЗУ программ используют одно и то же прерывание. Этот случай иллюстрируется Рисунок 3.1.



Маскирование аппаратных прерываний


3.4. Маскирование аппаратных прерываний

Выше мы использовали команду микропроцессора CLI (функцию Турбо-Си disable) для запрета всех аппаратных прерываний. Но аппаратные прерывания можно запрещать и выборочно. Для этого можно воспользоваться портом 0x21, в который записывается байт - маска аппаратных прерываний. Единица в разряде этого байта соответствует запрету соответствующего аппаратного прерывания, 0 - разрешению прерывания. Соответствие разрядов прерываниям приводится ниже (здесь и далее при поразрядном анализе байтов или слов номер 0 имеет самый младший бит):

0- прерывание таймера;
1- прерывание клавиатуры;
2- каскадирование прерываний (только AT);
3- прерывание асинхронного порта COM2;
4- прерывание асинхронного порта COM3;
5- прерывание контроллера жестких дисков (только XT);
6- прерывание контроллера гибких дисков;
7- прерывание принтера.

Приведенный ниже программный пример иллюстрирует выборочное маскирование прерываний. Функция Турбо-Си clock возвращает количество прерываний таймера, поступивших с начала выполнения нашей программы. Первые 10 значений, выводимые на экран, будут последовательно возрастать, показывая нам, что прерывания от таймера поступают. Затем в порт 0x21 записывается код, содержащий единицу в младшем разряде. Следующие 10 значений возвращаемые функцией clock будут неизменны: прерывания от таймера не поступают. Наконец, мы восстанавливаем значение, ранее находившееся в порту 0x21, и видим, что прерывания от таймера поступают вновь.



Основное распределение памяти


4.1. Основное распределение памяти

Как известно, адресное пространство микропроцессора 8086 имеет размер 1 Мбайт. Это адресное пространство распределяется таким образом, как показано на Рисунок 4.1 (младшие адреса - внизу).



Загрузка системы


4.2. Загрузка системы

При включении питания микропроцессор 8086 начинает работу с передачи управления по адресу FFFF:0000 (этот адрес заносится в регистры CS:IP) - это аппаратная особенность микропроцессора. Этот адрес принадлежит ПЗУ BIOS, 5 байт по этому адресу содержат команду перехода на начало программы POST в ПЗУ (Power On Self Test - самопроверка при включении питания). Программа POST проверяет состав оборудования и формирует список оборудования в области памяти BIOS, выполняет тест ОЗУ (как правило, прохождение этого теста трассируется на экране терминала) и прочего оборудования ПЭВМ и инициализирует таблицу векторов прерываний в части прерываний, обслуживаемых BIOS. Также программа ищет в памяти расширения ПЗУ (как это делала наша программа 4.2) и вызывает их программы инициализации.

Затем BIOS начинает процедуру начальной загрузки. Начальный загрузчик опрашивает первое устройство гибких дисков, проверяя на нем наличие диска для начальной загрузки. Если диска там нет, программа обращается к ПЗУ, связанным с другими устройствами, которые могут содержать диски для начальной загрузки. Когда устройство загрузки найдено, BIOS читает с него блок начальной загрузки и передает на него управление. Блок начальной загрузки, размещенный на дорожке 0, секторе 1, стороны 0 первой доступной дискеты (или жесткого диска) обычно считывается в память по адресу 07C0:0000 и содержит загрузчик операционной системы и блок параметров, из которого загрузчик получает информацию о конфигурации системного диска. Загрузчик через таблицу векторов использует сервисные средства BIOS для загрузки в память остальной части системы. Загрузчик прежде всего устанавливает, что первые два файла в каталоге - IO.SYS и MSDOS.SYS (IBMBIO.COM и IBMDOS. COM для операционной системы PC DOS). Если это так, то оба файла считываются в оперативную память.

Файл IO.SYS состоит из модуля резидентных драйверов (BIOS DOS), модуля инициализации BIOS.DOS и модуля системной инициализации. В ходе инициализации BIOS DOS формируется список резидентных драйверов устройств в соответствии со списком оборудования, полученным при работе POST и изменяются некоторые векторы прерываний BIOS (т.е.
BIOS DOS перехватывает обработку этих векторов у BIOS ПЗУ). После отработки модулей инициализации IO.SYS занимаемая ими память освобождается, и управление получает программа инициализации ядра DOS. Эта программа устанавливает векторы прерываний DOS и выполняет инициализацию резидентных драйверов. В ходе этой инициализации определяется максимальный размер сектора, используемого драйверами, и в соответствии с этим размером в памяти организуется буфер секторов. Затем выполняется обработка файла CONFIG.SYS. В соответствии с параметрами, заданными в CONFIG.SYS (или по умолчанию - если CONFIG.SYS отсутствует), формируются системные таблицы и загружаются устанавливаемые драйверы устройств. Наконец, открываются системные файлы (CON, PRN, AUX), загружается в память командный процессор COMMAND.COM, и управление передается его секции инициализации.
Имя командного процессора задается оператором SHELL в CONFIG.SYS, по умолчанию это COMMAND.COM. COMMAND.COM состоит из трех частей:
резидентная часть, содержащая обработчики прерываний 0 x22 (адрес завершения), 0x23 (реакция на Ctrl-Break), 0x24 (критическая ошибка); часть инициализации располагается в памяти вслед за резидентной частью COMMAND.COM, она обеспечивает выполнение файла AUTOEXEC.BAT и уничтожается (освобождает память) после выполнения своих функций; нерезидентная часть, содержащая интерпретатор внутренних команд DOS и загрузчик внешних команд. Эта часть располагается в старших адресах памяти и может частично или полностью перекрываться загружаемыми программами. При обращении к нерезидентной части командного процессора ее сохранность в ОЗУ проверяется по контрольной сумме и при необходимости она повторно загружается в память.
Таким образом, после окончания процесса загрузки область памяти, названная на Рисунок 4.1 "DOS и транзитные программы" распределяется так, как показано на Рисунок 4.2.



Тип ПЭВМ


5.1. Тип ПЭВМ

В конце ПЗУ BIOS по адресу FF00:0FFE записан байт типа ПЭВМ. Специфицированы следующие 4 значения этого байта для машин фирмы IBM: 0xFF - IBM PC; 0xFE - XT; 0xFD - PCjr; 0xFC - AT. Для ПЭВМ других производителей все наши эксперименты давали один из этих кодов в соответствии с классом компьютера, однако, стопроцентной гарантии этого дать нельзя.

Кроме того, 8 байт ПЗУ начиная с адреса 0xFF00:0x0FF5, содержат дату выпуска данной версии BIOS в символьном виде, например: 01/01/84. Эта информация может быть полезна только для пользователей продукции IBM, в условиях же калейдоскопа фирм-производителей пользу из этих данных извлечь трудно (мы встречали даже ПЭВМ, в которых дата выпуска BIOS отсутствовала вообще). Приведенная ниже программа извлекает из ПЗУ тип ПЭВМ и дату выпуска BIOS (пример 5.2.).



Состав оборудования


5.2. Состав оборудования

Опеределять состав оборудования следует только после того, как мы определили тип ПЭВМ. Это обусловлено тем, что способы получения информации о составе оборудования различны для XT и для AT. Рассмотрим сначала машины класса XT.

Состав оборудования XT (и PC) определяется положением переключателей на специальных колодках. Состояние этих переключателей может быть прочитано из программы обращением к порту 0x60. Но необходимости в этом нет - эту операцию выполняет BIOS при инициализации системы. На основании этой информации BIOS формирует так называемый список оборудования - 2-байтное слово по адресу 0040:0010. Прочитать это слово можно либо обратившись по указанному адресу, либо обратившись к BIOS через прерывание 0x11. Назначения разрядов списка оборудования следующие:

0- установлен в 1, если есть НГМД (см.разряды 6, 7);
1- установлен в 1, если есть сопроцессор;
2,3- число 16-Кбайтных блоков ОЗУ на системной плате;
4,5- код видеоадаптера: 11 - MDA, 10 - CGA, 80 колонок, 01 - CGA, 40 колонок, 00 - другой;
6,7- число НГМД-1 (если в разряде 0 единица);
8- 0, если установлен канал ПДП;
9,10,11- число последовательных портов RS-232;
12- 1, если установлен джойстик;
13- 1, если установлен последовательный принтер;
14,15- число параллельных принтеров.

Для XT и AT биты 8 и 13 всегда будут нулевыми.

Ниже приведен пример получения и интерпретации списка оборудования.



Определение объема оперативной


5.3. Определение объема оперативной памяти

Методика определения объема памяти - такая же, как и определения списка оборудования. Объем ОЗУ (в Кбайтах) находится в области памяти BIOS по адресу 0040:0013 (2-байтное слово) и может быть получен при помощи прерывания 0x12, как это показано в следующем примере.



Генерация звука


6.1. Генерация звука

Если динамик ПЭВМ включен и управляется от таймера, то высота генерируемого звука определяется частотой импульсов канала 2 и зависит от коэффициента деления, записанного в счетчике канала. Включение-выключение динамика управляется двумя разрядами в однобайтном регистре контроллера программируемого периферийного интерфейса, доступ к которому - через порт 0x61. Следует иметь в виду, что этот регистр используется также и для других целей, так что при его программировании следует вначале прочитать его содержимое, изменить требуемые разряды, а затем записать в порт 0x61 новое значение. Для управления динамиком используются такие разряды регистра:

0- единица в этом разряде устанавливает управление динамиком от таймера (возможно и прямое управление из программы, но мы его не рассматриваем);
1- включение/выключение (1/0) динамика.

Из приведенной выше методики программирования таймера очевидно, что динамик может воспроизводить только чистые (однотонные) звуки. Для создания полифонического звучания программа должна быстро переключать (перепрограммировать) таймер с одного тона на другой. Это достаточно громоздкая операция, которая занимает к тому же весь ресурс времени центрального процессора, поэтому мы здесь ограничимся только чистыми звуками.

Для нот первой октавы (включая полутона) ряд коэффициентов деления следующий:

912 - 861 - 813 - 767 - 724 - 678 - 645 - 609 - 574 - 542 - 512 - 483

Для перехода на октаву ниже следует умножить члены этого ряда на 2, на октаву выше - на 0,5.

Следующая программа воспроизводит 7 основных нот первой октавы (пример 6.1).

Длительность звучания каждой ноты и паузы между нотами здесь задается задержкой (delay). При выполнении этих задержек процессор только ожидает истечения заданного интервала, хотя он мог бы в это время выполнять какую-либо полезную работу. Как это организовать - мы увидим позже, рассматривая работу в реальном времени.



Системная служба времени


6.2. Системная служба времени

Импульсы, поступающие с выхода канала 0 таймера, вызывают прерывание 8. Обработчик этого прерывания в BIOS подсчитывает количество таких импульсов в 4-байтной области памяти (два 2- байтных слова). Этот счетчик, находящийся в области памяти BIOS по адресу 0040:006C, таким образом, хранит количество тиков таймера, прошедших от полуночи (0 в счетчике соответствует полночи). При запуске системы BIOS запрашивает у оператора время дня, переводит его в количество тиков и записывает по указанному адресу. Затем в процессе работы это число модифицируется обработчиком прерывания 8. То обстоятельство, что обработчик прерывания 8 в BIOS обеспечивает работу службы времени следует учитывать при перехвате прерывания 8 и при перепрограммировании канала 0 таймера.

Доступ к счетчику времени поддерживается прерыванием 0x1A. При обращении к этому прерыванию со значением 0 в регистре AH мы получаем в CX старшую, а в DX - младшую части счетчика. При обращении со значением 1 в AH мы задаем счетчик в регистрах CX, DX, и это значение записывается в память BIOS.

DOS поддерживает службу времени функциями 0x2C (чтение времени) и 0x2D (установка времени). Для представления времени в этих функциях используются регистры: CH (часы), CL (минуты), DH (секунды), DL (сотые доли секунды).

Программа примера 6.2 иллюстрирует чтение системного времени тремя способами.



Работа в реальном времени


6.3. Работа в реальном времени

В целом ряде приложений необходима привязка действий программы к определенным моментам времени или к временным интервалам. Наиболее простой подход заключается в циклическом опросе системного счетчика времени (по сути, такой подход применялся в примере 6.1 для управления длительностью звучания нот). Если же требуется заполнить паузы ожидания какой-либо полезной работой, приходится прибегать к расширению прерывания таймера. Для этого случая создается дополнение к прерыванию таймера, которое подсчитывает тики таймера параллельно с BIOS и по истечении заданного интервала либо сама выполняет требуемые действия, либо устанавливает какой-то флаг, по которому эти действия выполнит программа переднего плана. Характерным примером такой задачи является создание фонового музыкального сопровождения - музыка играет в то время, как программа производит вычисления. В приведенном примере 6.5 программа исполняет мелодию, закодированную в массиве MUS, где каждая нота описывается двумя числами: ко- эффициентом деления основной частоты таймера и длительностью звучания (в тиках).

При иницировании музыки переменные N и NM устанавливают- ся в начальные значения и подменяется вектор прерывания 8. "Полезная" работа программы переднего плана заключается в чтении кода нажатой клавиши, получении и выводе на экран те- кущего времени (это дает нам возможность убедиться в том, что работа системной службы времени не нарушена). При нажа- тии клавиши Esc программа и музыка завершаются. При поступ- лении очередного прерывания 8 управление получает функция newtime. Она прежде всего вызывает системный обработчик пре- рывания 8, а затем уменьшает счетчик тиков NM. Если счетчик тиков исчерпан, то отключается звук, программный цикл обес- печивает короткую паузу между нотами, а затем выбирается ко- эффициент деления для очередной ноты, который используется для программирования канала 2 таймера, а ее длительность ус- танавливается в счетчик NM.



Прерывание от клавиатуры и скан-коды


7.1. Прерывание от клавиатуры и скан-коды

Работа клавиатуры организована на базе собственного микропроцессора. При нажатии или отпускании любой клавиши генерируется код (скан-код), который записывается в собственную память (аппаратный буфер) клавиатуры. При этом в центральный процессор выдается прерывание 9, сигнализирующее о появлении очередного скан-кода. Программа обработки этого прерывания может теперь прочитать код из клавиатуры. Если она этого не сделает, то сканкоды будут накапливаться в аппаратном буфере и могут быть прочитаны позднее. Чтение скан-кода производится из однобайтного порта 0x60. При выполнении только чтения скан-код не удаляется из аппаратного буфера, и при следующем обращении к порту 0x60 будет прочитан тот же скан-код. Для того, чтобы удалить скан-код из аппаратного буфера, необходимо послать в клавиатуру сигнал подтверждения: на короткое время выставить единицу в старшем разряде однобайтного порта 0 x61 (не разрушая при этом остальных разрядов этого порта). Каждая клавиша имеет собственный уникальный скан-код, в том числе и на 101-клавишной клавиатуре AT, где некоторые клавиши дублируются - например, левая и правая клавиши Shift имеют разные скан-коды. Распределение значений скан-кодов примерно соответствует расположению клавиш на панели клавиатуры. Скан-коды не совпадают с кодировкой внутримашинного представления символов, для которой применяется код ASCII. Преобразование скан-кодов в коды ASCII происходит программными путем. В некоторых источниках имеются расплывчатые формулировки, которые можно трактовать таким образом, что некоторые клавиши или комбинации клавиш (PrintScrn, Ctrl +Break, etc) не имеют скан-кодов - это ни в коем случае не так! Каждая клавиша имеет скан-код, который считывается в компьютер вышеописанным образом, а уж программа обработки этого кода может распорядиться им специальным образом.

Выше упоминалось, что скан-код генерируется и при нажатии, и при отпускании клавиши. Для 84-клавишной клавиатуры XT скан-код отпускания совпадает со скан-кодом нажатия, но содержит дополнительную единицу в старшем разряде.
В источниках можно встретить информацию о том, что клавиатура AT генерирует двухбайтный скан-код при отпускании клавиши, в котором первый байт - 0xF0, а второй совпадает со скан-кодом нажатия. Это справедливо для 84-клавишной клавиатуры AT, но в настоящее время все AT комплектуется 101-клавишной клавиатурой, в которой код отпускания формируется по тем же прави- лам, что и в XT. Правда, в 101-клавишной клавиатуре некоторые дополнительные клавиши имеют скан-коды из 2 и более байт (и каждый байт сопровождается прерыванием 9).
Программный пример 7.1 позволяет определить скан-коды клавиш. Эта программа перехватывает прерывание 9. Обратите внимание на то, что обработчик прерывания представляет собой не дополнение к системному обработчику, а полностью его подменяет. При поступлении прерывания 9 обработчик читает скан-код из порта 0x60 и запоминает его в массиве SC, а затем посылает в клавиатуру сигнал подтверждения и сбрасывает контроллер прерываний. При поступлении скан-кода 1 (это клавиша Esc) обработчик взводит флаг окончания, по которому главная программа восстанавливает вектор и выводит на экран накопленные скан-коды.

Коды ASCII и работа BIOS


7.2. Коды ASCII и работа BIOS

Обработчиком прерывания 9 в BIOS скан-коды превращаются в коды ASCII. Имеется два типа ASCII-кодов: однобайтные и расширенные. Однобайтные коды - коды алфавитно-цифровых символов, символов псевдографики, некоторых управляющих символов (последние по-разному интерпретируются разными периферийными устройствами). Расширенные ASCII-коды - двухбайтные, первый байт всегда 0, второй содержит код. Такие коды связаны с клавишами, которые не имеют литерного отображения: клавиши функциональной клавиатуры, управления курсором и т.п. Обработчик прерывания 9 BIOS вырабатывает требуемые ASCII-коды и записывает в буфер клавиатуры. Далее программа, выполняющая ввод, обращается к BIOS или к DOS, и средства системы выбирают символы из буфера и передают программе.

Буфер клавиатуры находится в области данных BIOS. Этот буфер занимает память с адресами от 0040:001 по 0040:003C. Буфер организован как циклическая очередь, то есть при за- полнении указанной области памяти запись продолжается с ее начала. Два слова в области данных BIOS содержат адреса (смещения относительно начала области данных BIOS) начала и конца ("головы" и "хвоста") очереди. Адреса этих слов - 0040:001A и 0040:001C соответственно. Каждый код представлен в буфере BIOS двумя байтами. Для расширенных кодов ACSII первый байт содержит 0, а второй байт - ASCII-код. Для одно- байтных кодов первый байт содержит ASCII-код, а второй байт - скан-код клавиши, породившей этот ASCII-код. В буфере клавиатуры BIOS размещаются 15 слов, содержащих коды введенных клавиш, 16-е слово зарезервировано для размещения в нем признака конца 0x0D1C. Добавление нового кода в буфер состоит в записи кода по адресу "хвоста" и модификации указателя "хвоста". Удаление - в выборке кода по адресу "головы" и модификации указателя "головы" (модификация указателей должна учитывать циклическую природу очереди). Для очистка буфера достаточно приравнять указатель "хвоста" указателю "головы".

Программа примера 7.3 иллюстрирует работу буфера клавиатуры. При всяком изменении указателей очереди на экране отображается содержимое буфера и положение указателей в нем. Перехват прерывания 9 производится для распознавания управляющих клавиш программы. Клавиша "r" устанавливает признак чтения, по которому из очереди выбирается один код. Клавиша Esc устанавливает признак завершения программы.



Комбинация клавиш Ctrl+Break


7.4. Комбинация клавиш Ctrl+Break

Если нажата эта комбинация клавиш, то при первом же системном вызове она будет обработана. Обработка заключается обычно в немедленном завершении текущей программы. Обратите внимание на то, что комбинация обрабатывается не немедленно, а при вызове DOS. Начиная с самого нижнего уровня, порядок обработки этой комбинации следующий. Комбинация Ctrl+Break распознается BIOS при вводе. BIOS в этом случае вызывает прерывание 0x1B. Исходный обработчик этого прерывания состоит из одной команды IRET. DOS переустанавливает этот вектор на другой обработчик, который взводит признак нажатия Ctrl+Break в области данных BIOS (40:71). Функции DOS проверяют этот флаг и если он взведен (и если это разрешено статусом обработки) вызывают прерывание 0x23, а системный обработчик этого прерывания завершает программу.

В следующем примере дважды повторяются похожие действия: в программном цикле часть экрана заполняется символами '*'. Но в первом таком цикле нет системных вызовов. Поэтому если во время выполнения этого цикла будет нажата комбинация Ctrl+Break, прерывания программы не произойдет. Во втором цикле есть вызов функции DOS 0x0B, поэтому в этом цикле программу можно прервать.



Драйвер ANSI.SYS и переопределения клавиатуры


7.5. Драйвер ANSI.SYS и переопределения клавиатуры

В состав DOS входит драйвер ANSI.SYS, обеспечивающий расширенные средства управления консолью. Чтобы иметь доступ к средствам, предоставляемым этим драйвером, в файл CONFIG.SYS должна быть включена команда: DEVICE=ANSI.SYS Функции драйвера ANSI.SYS вызываются путем вывода в стандартный вывод специально форматированных последовательностей символов. Эти последовательности начинаются символом с кодом 27 (0x1B, 8-ричный код -33), отсюда их название - Esc-последовательности. Второй символ Esc-последовательности "[" - код 91 (0x5B). Последующие символы варьируются. Для вызова ANSI-функций программа может использовать функции символьного вывода DOS или соответствующие средства языков высокого уровня, которые выдают символы на консоль. Большинство ANSI-функций управляют выводом на терминал и будут нами рассмотрены в соответствующем месте. Применительно к клавиатуре драйвер ANSI.SYS обеспечивает переназначение клавиатуры, присваивая заданной клавише новое значение ASCII -кода или целой последовательности кодов. Формат Esc-последовательности для переопределения клавиши следующий: <Esc>[<исходный код>;<заменитель>p, где "[", ";" и "p" - символы, кодируемые как показано; <Esc> - символ с кодом 27; <исходный код> - десятичный цифровой ASCII-код переназначаемой клавиши, для клавиш, имеющих расширенные ASCII-коды представляется как: 0;<код>; <заменитель> - это может быть другой код или коды, заданные либо в виде десятичных чисел, либо в виде строковых (взятых в кавычки) констант. Составляющие заменителя разделяются символами ; (точка с запятой). Для восстановления исходного значения клавиши следует выдать последовательность: <Esc>[<исходный код>;<исходный код>p Довольно громоздкий способ кодирования переопределений, возможно, станет более понятным при анализе следующего примера.



Порты принтера


8.1. Порты принтера

DOS может работать с тремя параллельными принтерами, именуемыми LPT1, LPT2, LPT3. Каждый принтер имеет по три порта: порт вывода (базовый порт), порт состояния и порт управления. Адреса портов строго не фиксированы. В области данных BIOS по адресам 0040:0008, 0040:000A, 0040:000C содержатся адреса базовых портов для LPT1, LPT2, LPT3 соответственно. Адрес порта состояния - на 1 больше базового, порта управления - еще на 1 больше.

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

0- 2 - не используются, обычно установлены в 1;
3- ошибка принтера - нет/есть (0/1);
4- принтер подключен/не подключен (1/0);
5- бумага есть/нет (0/1);
6- принтер выводит очередной символ/готов (0/1);
7- принтер занят/свободен (0/1).

При возможных расхождениях в интерпретации этого байта для разных принтеров наиболее информативен, по-видимому, бит 3, его установка в 1 говорит о готовности принтера.

Программа примера 8.1 предлагает проверить байт состояния при некоторых, наиболее вероятных состояниях принтера. Для сравнения программа выдает байт стандарта Epson.



Прерывание BIOS


8.2. Прерывание BIOS

В BIOS принтер обслуживается прерыванием 0x17, имеющим функции:

0- вывод символа;
1- инициализация;
2- чтение состояния.

Все три функции возвращают состояние принтера в регистре AH.

Программные примеры 8.3 и 8.4 аналогичны по выполняемым действиям двум первым, но используют прерывание 0x17.



Функции DOS


8.3. Функции DOS

В DOS имеется функция 5, которая интегрирует в себе операции по управлению, выводу и анализу состояния. Те же действия программируются при помощи функции DOS следующим образом (пример 8.5):



Управление спецификациями печати


8.4. Управление спецификациями печати

Выше мы уже встретились с управляющими символами (24,10). Некоторые коды или последовательности кодов интерпретируются принтером не как коды символов, подлежащих отображению на бумаге, а как управляющие коды. Они используются для выполнения принтером специальных действий и управления спецификациями печати. Наиболее интересный из этих кодов - код 0x1B (27) или Esc. Появление этого кода интерпретируется принтером как начало целой управляющей последовательности. (Esc-последовательности принтера в отличие от клавиатуры и терминала обрабатываются не драйвером ANSI, а аппаратурой принтера). Следующий за Esc код задает тип действия, далее могут следовать еще несколько байт, количество которых зависит от действия. В примере 8.6 продемонстрированы некоторые режимы печати, устанавливаемые при помощи управляющих кодов и Esc-последовательностей. (Этот и следующие примеры не исчерпывают возможностей управления принтером при помощи управляющих кодов и Esc-последовательностей. Для получения полного представления о них следует обратиться к техническому описанию принтера.) Обратите внимание на последовательность 27, 64, названную в комментариях инициализацией. Не следует путать эту инициализацию с инициализацией порта, о которой шла речь выше. Esc-последовательность инициализации обеспечивает установку всех спецификаций печати в те значения, которые они имеют по умолчанию. Для выдачи управляющих последовательностей (как и печатаемых символов) можно использовать средства BIOS, DOS, а также любые средства, имеющиеся в применяемом языке программирования. Мы в этом и следующих примерах применяем функцию Турбо-Си putc.



Тип видеоадаптера


9.1. Тип видеоадаптера

В абсолютном большинстве применений ПЭВМ комплектуются видеоадаптером одного из четырех основных типов:

MDA (Monochrome Display Adapter - монохромный дисплейный адаптер) - текстовый режим: 80 колонок x 25 строк, разрешающая способность 720 x 348 точек, матрица символа 7 x 9, черно-белый; CGA (Color Graphics Adapter - цветной графический адаптер) - текстовый режим: 80 колонок x 25 строк, матрица символа 8 x 8, 16 цветов; графический режим: 640 x 200 точек - монохромный или 320 x 200 точек - 4 цвета из 16-цветовой палитры; EGA (Enhanched Graphics Adapter - улучшенный графический адаптер) - текстовый режим: 80 колонок x 25 строк, матрица символа 8 x 14, 16 цветов из 64-цветовой палитры; графический режим: 640 x 350 точек, 16 цветов из 64-цветовой палитры; VGA (Video Graphics Array - видеографический массив) - текстовый режим: 80 колонок x 25 строк, матрица символа 8 x 14, 16 цветов из 4096-цветовой палитры; графический режим: 640 x 480 точек при 16 цветах из 4096 и 320 x 200 при 256 цветах (имеются модели с разрешающей способностью 800 x 600 и 1024 x 768 точек).

В главе, посвященной анализу оборудования, мы посетовали на то, что из списка оборудования BIOS нельзя извлечь исчерпывающую информацию о типе адаптера, отличном от MDA и CGA. Как же получить эту информацию?

Видеоадаптер обслуживается прерыванием BIOS 0x10. Начиная с модели EGA обработчик этого прерывания располагается не в основном ПЗУ BIOS, а в расширении ПЗУ по адресу C000:0000. Это прерывание и в исходном своем варианте имело много функций, а для новых типов адаптеров список его функций расширяется. Эти новые функции и используются для определения типа адаптера, как показано в примере 9.1. В дальнейшем в этой главе, если речь идет просто о функции имеется в виду функция прерывания 0x10.



Режимы видеоадаптера и область данных BIOS


9.2. Режимы видеоадаптера и область данных BIOS

Видеоадаптер может функционировать в одном из нескольких режимов, отличающихся возможностями текст/графика и разрешающей способностью. Полные таблицы всех возможных режимов имеются в любом справочнике, с режимами адаптера EGA читатель может ознакомиться по нашим программным примерам 9.2 и 9.9.

Режимы переключаются функцией 0, при этом номер (код) режима задается в регистре AL. При рассмотрении примера 9.2 обратите внимание на то, что весь цикл перебора режимов повторяется в ней дважды - при второй итерации внешнего цикла задаются те же коды режимов, но с единицей в старшем бите регистра AL. Эта единица определяет сохранение содержимого экрана при переключении режима, на втором этапе выполнения программы будет сохраняться заполнение экрана.



Видеопамять, управление цветом


9.3. Видеопамять, управление цветом

В текстовом режиме видеопамять для CGA, EGA, VGA начинается с адреса B800:0000 (для MDA - B000:0000). Видеопамять физически расположена на плате адаптера, но ее адреса входят в общее адресное пространство ПЭВМ и доступны программисту для чтения и для записи. Каждое знакоместо экрана в видеопамяти представлено двумя байтами, первый из которых содержит ASCIIZ-код символа, а второй - цветовой атрибут в формате:



Управление курсором, вывод на терминал


9.4. Управление курсором, вывод на терминал

BIOS предоставляет следующие средства управления курсором: функция 3 - чтение координат курсора, функция 2 - установка курсора по заданным координатам. При обращениях к этим функциям в BH задается номер страницы, а в DL, DH получаются (или задаются) координаты - номер строки и колонки соответственно (нумерация начинается с 0). Позиционирование курсора иллюстрируется первой частью примера 9.5.

Курсор отображается на экране в виде одной или нескольких горизонтальных линий на матрице символа. Для того, чтобы задать форму курсора нужно задать номер S1 строки матрицы, с которой начинается курсор, и номер S2 - строки, которой он заканчивается. Если S2 < S1, то курсор состоит из двух частей: от S1 до 13 включительно и от 0 до S2 включительно. Управление формой обеспечивается функцией 1, начальная строка задается в CH, конечная - в CL. Если бит 5 CH при этом установлен в 1, то курсор становится невидимым (другой способ сделать курсор невидимым - установить его на строку 25 при помощи функции 2). Вторая часть примера 9.5 демонстрирует все возможные формы курсора. Обратите внимание на то, что при некоторых значениях номеров строк s1, s2 происходит скачкообразное изменение формы. Функция фактически отрабатывает только 8 значений номера строки, чтобы обеспечить совместимость с CGA, имеющим размер матрицы по вертикали 8.



Средства драйвера ANSI.SYS


9.5. Средства драйвера ANSI.SYS

Драйвер ANSI.SYS, рассмотренный нами в главе, посвященной вводу, имеет также средства управления выводом на консоль. Ниже приведены основные Esc-последовательности, применяемые для этих целей (их применение иллюстрируется примером 9.7).

<Esc>[<x>;<y>H - установить курсор в позицию (<x>,<y>), нумерация начинается с 1; <Esc>[<n>A - сдвинуть курсор вверх на <n> позиций; <Esc>[<n>B - сдвинуть курсор вниз на <n> позиций; <Esc>[<n>C - сдвинуть курсор вправо на <n> позиций; <Esc>[<n>D - сдвинуть курсор влево на <n> позиций; <Esc>[s - запомнить координаты курсора; <Esc>[u - восстановить ранее запомненные координаты курсора; <Esc>[2J - очистить экран; <Esc>[K - удалить часть строки от курсора до конца строки; <Esc>[<a1>;<a2>;...<an>m - функции экрана. Значения <ai> от 30 до 37 задают цвет вывода - от черного до белого, значения от 40 до 47 - цвет фона, значения от 0 до 5 определяют специальные режимы отображения; <Esc>[=<n>h - установить видеорежим <n>.



Знакогенератор


9.6. Знакогенератор

В ПЗУ адаптера имеется таблица генератора знаков (в EGA таких таблиц четыре - две для матрицы 8 x 14 и две для 8 x 8). Однако, пользователь может сформировать собственные образы букв и загрузить их в знакогенератор. Образы составляются на матрице выбранного размера, образ одной буквы имеет размер 14 или 8 байт, каждый байт описывает одну строку образа (первый байт - верхнюю). Для работы со закогенератором используется функция 0x11.

Подфункция 0 (AL=0) загружает шрифт в знакогенератор. Она позволяет за один вызов перезагрузить как всю таблицу символов, так и какую-то ее часть. При обращении к ней регистры ES:BP содержат адрес загружаемой таблицы, CX - число символов, описываемых этой таблицей, DX - код первого символа в таблице, BH - число байт в образе (8 или 14). В примере 9.8 сформированная нами таблица содержит перевернутые образы четырех следующих подряд букв - TUVW. При загрузке этой таблицы в знакогенератор в тексте на экране эти буквы перевернутся.



Гpафика


9.7. Гpафика

Мы уделяем гpафике довольно мало внимания, так как для получения эффективных pезультатов в этой области необходимо использовать либо уpовень поpтов ввода-вывода, либо сpедства языков пpогpаммиpования, поддеpживаемые соответствующими дpайвеpами систем пpогpаммиpования. И то, и дpугое лежит за пpеделами оговоpенного нами подхода.

В MS DOS нет сpедств (функций), поддеpживающих pаботу с гpафикой.

В BIOS pабота с гpафикой поддеpживается функциями пpеpывания 0x10, обеспечивающими установку pежима, упpавление палитpами, вывод символов. Стpого гpафических функций у пpеpывания 0x10 только две. Эти функции обеспечивают точечную гpафику - доступ к гpафической точке с кооpдинатами (x,y), где x - номеp столбца, y - номеp стpоки pазвеpтки (нумеpация начинается с 0). Функция 0xD возвpащает в AL цвет точки экpана, pасположенной на стpанице, заданной в BH, по кооpдинатам, заданным в (CX, DX). Функция 0xC записывает точку цвета AL на стpаницу BH по кооpдинатам (CX, DX). Если стаpший бит в AL пpи записи точки установлен в 1, то значение заданного цвета складывается по модулю 2 с пpежним значением цвета этой точки. Точно также pаботает стаpший бит атpибута цвета пpи использовании в гpафическом pежиме функций вывода символов.

Пpогpамма пpимеpа 9.9 иллюстpиpует возможности гpафических pежимов EGA. Она устанавливает гpафический pежим и в каждом pежиме pазмечает экpан сеткой с шагом в 10 точек (каждая десятая линия сетки выводится кpасным цветом). Линии сетки стpоятся по точкам с использованием функции 0xD (для того, чтобы веpтикальные белые линии не пеpекpывали гоpизонтальные кpасные, пpедваpительно пpовеpяется цвет каждой точки функцией 0xC). Затем пpи помощи функции 9 выводится стpока текста, содеpжащая инфоpмацию о pазpешающей способности экpана в этом pежиме. Такая же стpока выводится ниже, но со сложением цветов букв и сетки. На втоpой видеостpанице (EGA в гpафическом pежиме может иметь две стpаницы) стpоится и закpашивается (по точкам) окpужность с pадиусом 40 точек, и пpогpамма несколько pаз пеpеключает стpаницы. В гpафических pежимах пеpеключение видеостpаниц является весьма эффективным способом мгновенной смены изобpажения на экpане.



Физический дисковый адрес


10.1. Физический дисковый адрес

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

Дискета представляет собой круглую пластину, покрытую с двух сторон магнитным материалом. Информация записывается раздельно на две стороны дискеты. В дисководе к поверхности дискеты прижимаются две (по одной с каждой стороны) головки чтения/записи. Пара головок может перемещаться вдоль радиуса дискеты, занимая на нем ряд фиксированных положений. При вращении дискеты точки ее поверхности, которые могут находиться в контакте с головками, образуют концентрические окружности, именуемые дорожками. Дорожки делятся на секторы. Поскольку обмен информацией с дисками происходит через пря- мой доступ к памяти, сектор - это наименьший объем (блок) информации, передаваемый за одну операцию чтения/записи. Объем сектора - 512 байт.

Жесткий диск представляет собой два или более дисков, посаженных на одну ось. Все головки чтения/записи здесь, как и для дискеты образуют жесткий блок, то есть перемещаются все вместе. Для жесткого диска вполне органичным является понятие цилиндра - совокупности всех дорожек, расположенных на разных поверхностях и равноудаленных от оси вращения. С точки зрения адресации понятия "дорожка" и "цилиндр" являются синонимами.

Сектор является минимальной адресуемой единицей при обращениях к внешней памяти. Адрес сектора на внешней памяти состоит из трех составляющих:

номер дорожки (нумерация дорожек начинается с 0); номер головки (нумерация головок начинается с 0); номер сектора на дорожке (нумерация секторов начинается с 1).

При записи на диск больших объемов информации мы заинтересованы в том, чтобы свести к минимуму затраты по переключению на следующий сектор диска. Отсюда вопрос: какой сектор считать следующим при последовательной записи (чтении) ? Принят такой порядок его определения. Следующим считается сектор, расположенный следующим на той же дорожке, под той же головкой (при записи сектора головки за счет вращения диска установятся над следующим сектором, так что затраты времени на переключения практически нулевые). При заполнении всей дорожки следующим принято считать первый сектор дорожки, расположенный на том же цилиндре под следующей головкой (этот сектор будет находиться под головками, а электрическое переключение на другую головку - процесс быстрый). И только при заполнении всего цилиндра меняется номер дорожки в адресе (этот процесс требует механического перемещения головок и, следовательно, гораздо больших затрат времени).



Cредства чтения секторов BIOS и DOS.


10.2. Cредства чтения секторов BIOS и DOS.

В BIOS работа с дисками поддерживается прерыванием 0x13. Это весьма "богатое" прерывание, имеющее много функций, определяемых содержимым регистра AH. Ниже приведена краткая сводка функций этого прерывания (номера функций 16-ричные):

0- сброс дисковой подсистемы;
1- состояние дисковой подсистемы;
2- чтение секторов;
3- запись секторов;
4- контроль секторов;
5,6,7- форматирование дорожки;
8- параметры накопителя;
9- инициализация таблиц BIOS для жесткого диска;
A- длинное чтение (жесткий диск);
B- длинная запись (жесткий диск);
C- поиск цилиндра (жесткий диск);
D- альтернативный сброс (жесткий диск);
E- чтение буфера секторов (жесткий диск);
F- запись буфера секторов (жесткий диск);
10- проверка готовности (жесткий диск);
11- перекалибровка дисковода (жесткий диск);
12,13,14- диагностика контроллера (жесткий диск);
15- тип диска;
16- состояние замены (гибкий диск);
17- установка типа;
18- установка типа носителя;
19- парковка головок;
1A,1B,1C- ESDI жесткий диск.

Для целей нашего пособия наиболее интересны функции 2 и 3 - чтение и запись. Формат обращения к этим функция преры- вания 0x13 (содержимое регистров) следующий:

AH- номер функции;
AL- количество секторов, которое нужно прочитать/записать; CX - номер дорожки и сектора (см.ниже); DH - номер головки; DL - физический номер дисковода (0 - дисковод A, 1 - B, 0x80 - 1-й жесткий диск, 0x81 - 2-й жесткий диск);
ES:BX- адрес области оперативной памяти, с которой происходит обмен.

После выполнения прерывания 0x13 флаг переноса устанавливается в 0, если операция прошла без ошибок или в 1 при наличии ошибок, в последнем случае регистр AH содержит код ошибки.

Номер дорожки и сектора задается в регистре CX в следующем упакованном формате:



Логическая структура диска


10.3. Логическая структура диска

Некоторые области диска содержат системную информацию, используемую DOS при работе с данным диском. К таким областям относятся:

Главная Загрузочная Запись (MBR - Master Boot Record) - только для жесткого диска; Загрузочный Сектор логического диска (Boot-сектор); Таблица Размещения Файлов (FAT - File Allocation Table); Корневой Каталог (Root Directiry).

10.3.1. MBR занимает самый первый сектор жесткого диска (дорожка 0, головка 0, сектор 1). Причины введения MBR в логическую структуру диска следующие. Формат некоторых системных данных и обращений к прерываниям 0x25, 0x26 в ранних версиях DOS не предусматривал возможности работы с жестким диском объемом более 32 Мбайт. Начиная с версии DOS 3.30, это ограничение обходится путем разбиения жесткого диска на два или более логических дисков, объем каждого из которых менее 32 Мбайт. И хотя уже в версии 4.0 снято ограничение на объем логического диска, возможность разбиения диска остается, так как обеспечивает целый ряд дополнительных удобств (например, разделение внешней памяти между пользователями).

Формат MBR следующий (в программе примера 10.2 он описан структурой struct MBR):

первые 466 (0x1BE) байт занимает программа начальной загрузки; далее следует таблица разделов, состоящая из четырех элементов; последние два байта - признак конца таблицы - код 0xAA55.

Программа начальной загрузки, содержащаяся в MBR, выполняет поиск по таблице активного раздела (логического диска, с которого должна происходить загрузка DOS), чтение в память Boot-сектора этого раздела и передачу управления на него.

Элемент таблицы разделов описан в программе примера 10.2 структурой struct Part. Добавим некоторые комментарии к этому описанию.

Поле ActFlag принимает значение 0x80 для активного раздела или 0 - для неактивного.

В физических адресах начала и конца раздела дорожка и сектор задаются в формате регистра CX прерывания 0x13. Раздел, как правило, начинается и заканчивается на границе цилиндра. Если первый сектор цилиндра занимает MBR (или ее продолжение в расширенном разделе DOS), то остальные сектора этой дорожки не используются, и раздел начинается с сектора 1, головки 1 этой дорожки.
Неиспользуемые сектора называются скрытыми.

Поле SysCode для MS-DOS может принимать значения: 1 - логический диск объема менее 32 Мбайт, 12-битная FAT; 4 - логический диск объема менее 32 Мбайт, 16-битная FAT; 6 - логический диск объема более 32 Мбайт; 5 - расширенный раздел DOS.

Последнее значение SysCode означает, что сектор, задаваемый адресом начала раздела в свою очередь содержит MBR (без программы загрузки, но с таблицей разделов по смещению 446), в этой таблице в свою очередь может содержаться описатель расширенного раздела и т.д. Системная утилита FDISK, производящая разбиение физического диска на логические, использует только два элемента в каждой таблице разделов, позволяя создать один первичный (соответствующий логическому диску) и один расширенный раздел DOS.

Поле RelSect содержит номер начального сектора (логический адрес) относительно начала раздела внешнего по отношению к данному.
Программист не может прочитать MBR средствами DOS. Для прерывания 0x25 задается логический адрес - номер сектора внутри данного логического диска, а сама MBR не принадлежит никакому логическому диску.
Пример 10.2 демонстрирует разбиение логического диска. Начальный адрес для чтения задается : 0,0,1. При помощи прерывания 0x13 программа считывает сектор по заданному адресу, далее происходит поэлементный анализ таблицы разделов - пока не встретится признак конца таблицы или раздел нулевого размера. Значения полей элемента таблицы выводятся на экран. Манипуляции, описываемые макросами TRK и SECT, опеспечивают распаковку номера дорожки и сектора. Если поле SysCode содержит признак расширенного раздела, то устанавливается новый дисковый адрес, считывается новый сектор и анализируется новая таблица.

Средства работы с файлами DOS


10.4. Средства работы с файлами DOS

10.4.1. Два метода ввода-вывода

В предыдущем примере мы поставили себе искусственное ограничение - использовать для работы с файлом только средства BIOS. В реальной работе программист, конечно же не связан такими ограничениями, в его распоряжении имеется богатый набор средств работы с файлами, каталогами и дисками, обеспечиваемых DOS.

В DOS имеются два совершенно независимых метода работы с файлами: метод Управляющих Блоков Файлов (FCB - File Control Block) и метод файловых дескрипторов (Handle - переводится также как файловые описатель, файловое число, файловый индекс). Первый метод остался "по наследству" от старых версий DOS и далее - от CP/M. Второй метод, впервые реализованный в DOS 2.0, является предпочтительным в новых разработках, поэтому описание начнем с него.

При использовании метода файловых дескрипторов программист в операциях, связанных с открытием файла, задает ASCIIZ -строку, содержащую имя файла (если файл находится не в текущем каталоге, строка содержит и полный путь). При успешном открытии файла функция возвращает программисту файловый дескриптор (целое число) который является идентификатором открытого файла во всех других операциях с файлом. Метод файловых дескрипторов отличает простота использования, возможность работы с файлами, находящимися не в текущем каталоге, единообразие при работе с дисковыми файлами и файлами - символьными устройствами. Первые 5 значений дескрипторов связаны со всегда открытыми системными файлами-устройствами:

0- стандартный файл ввода (обычно - клавиатура);
1- стандартный файл вывода (обычно - экран);
2- стандартный файл вывода сообщений об ошибках (всегда - экран);
3- стандартный файл AUX (асинхронный порт);
4- стандартный файл печати (1-й параллельный принтер).

Используя эти файловые дескрипторы, программист может обращаться к указанным устройствам, как к открытым дисковым файлам.

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



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


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

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

Формат PSP описывается структурой struct PSP программного примера 11.1. Некоторые поля PSP "остались в наследство" от ранних версий DOS и даже от операционной системы CP/M и в современных версиях DOS не используются, хотя и исправно формируются операционной системой. Это прежде всего поле ret _op, используемое для возможного завершения программы по команде RET 0, а также поле old_call_dos, содержащее команду вызова диспетчера функций DOS. Обращение к этому полю в программе может использоваться вместо команды INT 21h, но в современных версиях DOS для этих целей лучше обращаться к полю new_call_dos. Поле end_of_mem содержит сегментный адрес конца доступной памяти в системе. В три поля: term_ptr, ctrlbrk_ptr, criterr_ptr DOS при загрузке программы копирует содержимое векторов прерываний: 0x22, 0x23, 0x24, представляющее собой адреса обработчиков: завершения программы, комбинации клавиш Ctrl+Break, критической ошибки - соответственно. Предполагается, что программа может свободно перенаправить эти векторы на собственные обработчики соответствующих ситуаций, но от забот по восстановлению векторов программа избавляется, так как при ее завершении DOS сама восстанавливает векторы из соответствующих полей PSP завершаемой программы. Для аналогичных целей предназначено и поле stack_ptr - в нем сохраняется (а при завершении - из него восстанавливается) адрес стека, использовавшегося до вызова программы.
Поле, именуемое father_psp, содержит сегментный адрес PSP родителя - программы, запустившей данную программу, обычно родителем является COMMAND.COM.

При загрузке программы DOS, кроме программного сегмента, создает для нее еще и сегмент окружения (environment). Сегмент окружения содержит ASCIIZ-строки, задающие значения некоторых глобальных переменных, эти значения могут устанавливаться командой DOS SET, они доступны командным файлам и - через PSP - программам. Набор строк окружения заканчивается пустой ASCIIZ-строкой (нулем). В DOS 3.0 и выше за ним следует еще 2-байтное число строк вызова (обычно 1) и далее - строка (или строки) вызова программы. Обычно в первую (до строк вызова) часть порождаемой программы копируется содержимое окружения программы-родителя. Программа имеет доступ к своему сегменту окружения через поле env_seg PSP, содержащее сегментный адрес окружения.

Поле JFT (Job File Table - Таблица Файлов Задачи) представляет собой массив из 20 однобайтных элементов. При открытии программой файла DOS формирует для него блок-описатель в системной таблице файлов и помещает ссылку на него (его номер) в свободный элемент JFT. Дескриптор файла, возвращаемый программе DOS при открытии файла, является номером элемента в JFT. При запуске программы первые пять элементов создаваемой для нее JFT содержат ссылки на системные файлы, остальные свободны - содержат код 0xFF. В описаниях DOS часто можно встретить упоминание об ограничении на число одновременно открытых в программе файлов, при этом обычно называется число 20 (включая 5 системных файлов). Первым объяснением этого ограничения является размер JFT. Ограничение это, однако, не является непреодолимым. В самом PSP зарезервированы пути для его обхода: при обработке JFT DOS использует не прямое обращение к полю JFT PSP, а косвенное - через поле JFT_ptr, а в качастве ограничителя размера JFT - не константу 20, а значение поля JFT_size PSP. Таким образом, программа может в другом месте выделить память для размещения JFT большего размера и, изменив поля JFT_ptr, JFT_size в своем PSP, обеспечить работу с новой таблицей файлов.


DOS, начиная с версии 3.30, поддерживает эти операции функцией 0x67, которая выделяет память для новой JFT (требуемый размер JFT задается в BX), копирует в выделенную область старую JFT и исправляет поля PSP. Следует иметь в виду, что даже если в CONFIG.SYS значение параметра FILES больше 20, JFT в PSP формируется все равно 20-байтной, и программа сама должна позаботиться о ее расширении. Действия по увеличению размера JFT являются необходимыми, но не всегда достаточными для снятия ограничения на число открытых файлов - возможно, для этого еще потребуется расширить системную таблицу файлов (см. главу 13). Непреодолимым ограничением представляется размер элемента JFT - 1 байт, который позволяет представить число от 0 до 254 (255 - признак свободного элемента). Трудно, впрочем, представить себе программу, в которой необходимо иметь одновременно открытыми 250 файлов.

Вернемся к описанию формата PSP. Еще 36 бит, следующих за полями, описанными struct PSP, отводятся для двух блоков FCB, которые DOS строит для файлов, имена которых, возможно, являются параметрами программы.

Остальные 128 байт PSP - неформатированная его часть. При вызове программы в ней, начиная со второго ее байта, располагается остаток командной строки - часть строки вызова после имени программы, то есть, параметры, переданные программе при вызове. Эта строка не закрывается кодом 0, поэтому первый байт неформатированной области содержит число символов в остатке командной строки. Неформатированная область PSP после загрузки программы назначается DOS в качестве исходной DTA загруженной программы.
Программа может получить доступ к своему PSP либо из начального содержимого регистров DS или ES, либо при помощи функций DOS 0x51 или 0x62 (обе функции возвращают в BX сегментный адрес PSP). По некоторым источникам функция 0x51 (и родственная ей 0x50) в версиях DOS до 3.0 работала не во всех ситуациях надежно, функция же 0x62 является документированной. Мы в наших примерах используем обе эти функции.
После всего, сказанного выше, программа примера 11.1 не нуждается в комментариях.Эта программа извлекает из своего PSP всю или почти всю полезную информацию. Читатель может усовершенствовать ее, добавив в struct PSP описание полей блоков FCB. Иллюстрация обращения к остатку командной строки есть в примере 11.5.

COM- и EXE-файлы


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

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

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

EXE-программа может иметь любой размер и состоять из нескольких сегментов. Содержимое сегментных регистров меняется в ходе выполнения - в них, как правило, заносятся адреса логических сегментов программы. Поскольку во время трансляции и компоновки не известно, какие абсолютные значения будут иметь эти адреса при размещении программы в памяти, в коде программы эти адреса представляются в виде смещений сегментов относительно начала программы с тем, чтобы при загрузке выполнить настройку обращений к началам сегментов на конкретные адреса памяти. Именно в связи с необходимостью настройки EXE-файл имеет более сложный формат. В начале файла расположен заголовок, который состоит из форматированной части и таблицы перемещений, далее следует сам образ программы. Форматированная часть заголовка EXE-файла описана структурой struct EXEH программы примера 11.3.

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

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


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

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

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



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


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

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

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

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



Блоки упpавления памятью


12.1. Блоки упpавления памятью

После загpузки опеpационной системы оставшаяся часть памяти свободна и pаспpеделяется DOS пpикладным пpогpаммам. Память pаспpеделяется блоками. Размеp блока может быть пpоизвольным, но обязательно кpатен pазмеpу паpагpафа (16 байт). Вообще пpи pаспpеделении памяти объем памяти измеpяется в паpагpафах, и все блоки памяти выpавниваются по гpанице паpагpафа. Блоки создаются и уничтожаются динамически, по меpе поступления запpосов на их выделение / освобождение. Каждый блок памяти пpедваpяется Упpавляющим Блоком Памяти (MCB - Memory Control Block). MCB имеет фиксиpованный pазмеp 1 паpагpаф и фоpмат, описываемый следующей стpуктуpой:



Функции pаспpеделения памяти DOS


12.2. Функции pаспpеделения памяти DOS

При обычной работе MS DOS сама занимается распределением памяти и предоставляет пользователю три функции выделения/освобождения памяти:

функция 0x48 - выделение блока памяти (на входе: BX - требуемый размер блока в параграфах, на выходе: AX - сегментный адрес выделенного блока; если памяти для удовлетворения запроса не хватает, BX содержит размер наибольшего свободного блока); функция 0x49 - освобождение блока (на входе ES - сегментный адрес освобождаемого блока); функция 0x4A - изменение размера ранее выделенного блока (на входе ES - сегментный адрес, BX - требуемый размер).

Для всех функций ненулевое состояние флага CY является признаком ошибки.

Следующий программный пример иллюстрирует работу системы по выделению/освобождению памяти.

Функция init производит сканирование цепочки MCB и определяет адрес последнего блока памяти freetop. При принятой в системе по умолчанию дисциплине распределения памяти старшие адреса памяти представляют собой большой свободный блок, и все последующие выделения памяти будут вестись за счет этого свободного блока.

Функция memmap перебирает цепочку MCB, начиная с адреса freetop и выдает карту распределения памяти от freetop до конца памяти.

Функции memget, memfree, memnew обеспечивают обращения к функциям DOS 0x48, 0x49, 0x4A соответственно.

При шагах 1-5 программы происходит выделение нескольких блоков памяти по 64 параграфа каждый. По изменению карты памяти при этих шагах можно видеть, что новый блок памяти выделяется в начале свободного большого блока. Для оставшейся части большого блока система строит новый MCB, в котором этот блок помечается как свободный.

На шаге 5 изменяется (уменьшается) размер блока a2. По карте памяти мы увидим, что блок a2 будет разбит на два блока: первый - занятый блок требуемого размера, а освободившаяся часть блока образует новый, свободный блок.

На шаге 6 выдается запрос на увеличение блока a2 до размера большего 64 параграфов. Этот запрос не может быть удовлетворен, так как размер блока может быть увеличен только в том случае, если за ним следует свободный блок достаточного размера. В нашем случае блок a2 будет увеличен до возможного размера (64 параграфов), и будет индицирована ошибка.

Из карты, выдающейся на шагах 7, 8 видно, что при освобождении блока, в его MCB появляется отметка "свободен", при освобождении возможно появление фрагментации памяти.

При поступлении запроса на блок большего размера (шаг 9), этот блок выделяется за счет последнего свободного блока, как и на шагах 1 - 5.

При поступлении запроса на блок меньшего размера (шаг 10), этот блок выделяется за счет свободного блока внутри цепочки (в нашем случае - за счет блока a1. При этом, если затребованный размер меньше размера свободного блока, выделяется затребованный объем памяти, а остаток образует свободный блок.