39768

СИСТЕМНОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ЭИС

Книга

Информатика, кибернетика и программирование

Требования высокой эффективности СП вызывают необходимость использования специальных языков, к которым принадлежат машинно-ориентированные языки типа Ассемблера, и языки высокого уровня с развитыми формами представления внутренней, прежде всего, адресной информации СП, например, Си или PL/M

Русский

2015-01-16

1.24 MB

8 чел.

Министерство образования и науки Российской Федерации

КУБАНСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНОЛОГИЧЕСКИЙ

УНИВЕРСИТЕТ

Кафедра вычислительной техники и АСУ

СИСТЕМНОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ЭИС

Учебно-методическое пособие для студентов всех

форм обучения специальности 080801

Краснодар

Издательство КубГТУ

2009


УДК 681.32

Составитель: канд. техн. наук, доцент А.Г. Мурлин

 

Системное программное обеспечение. Учебно-методическое пособие для студентов всех форм обучения специальности 080801 – Прикладная инфооматика / Кубан. гос. технол. ун-т. Сост. А.Г. Мурлин. Краснодар, 2009, 161 с.

Учебно-методическое пособие составлено в соответствии с рабочей программой курса "Системное программное обеспечение ЭИС" для студентов специальности 080801.

Ил. 14.Табл. 16. Библиогр.: 7 назв.

Рецензенты:

зав. каф. ВТ и АСУ, д-р техн. наук, проф. В.И. Ключко (КубГТУ),

зав. каф. СА и ОИ, д. э. наук, проф. Т.П.Барановская (КубГАУ)


Содержание

[1]
Проблематика системного программирования и подбор средств для решения задач

[1.1] Классификация программ информационных и управляющих систем

[1.2] Функции управляющих программ информационных систем

[1.3] Требования к программам информационных и управляющих систем

[1.4] Формализация структурного синтеза программ

[2]
Структура программы на языке ассемблера. Ассемблирование программ. Отладка с использованием Turbo Debugger

[2.1] Структура программы на языке ассемблера

[2.2] Ассемблирование программ

[2.3] Отладка программ с использованием Turbo Debugger

[2.4] Форматы исполняемых файлов

[3]
Разработка программ обработки прерываний

[4]
Использование системных средств обслуживания дисков для защиты программных продуктов от копирования и несанкционированного использования

[5] Разработка резидентных программ обработки прерываний

[5.1] Основные понятия

[5.2] Использование средств BIOS в обработчиках аппаратных прерываний

[5.3] Использование средств DOS в обработчиках аппаратных прерываний

[5.4] Асинхронная активизация резидентных программ командами с клавиатуры

[5.5] Работа с файлами в резидентном обработчике аппаратных прерываний

[5.6] Выгрузка из памяти резидентных программ

[5.7] Свопинг области текущих данных DOS

[6]
Драйверы устройств. Общая структура и взаимодействие с DOS

[6.1] Директивы ассемблера

[6.2] Основная процедура

[6.3] Заголовок драйвера

[6.4] Рабочая область драйвера

[6.5] Процедура стратегии

[6.6] Процедура прерывания

[6.7] Локальные процедуры

[6.8] Обработка команд DOS

[6.9] Слово состояния заголовка запроса

[6.10] Процедура выхода при ошибке

[6.11] Процедура общего выхода

[6.12] Секция конца программы

[6.13] Подготовка драйвера устройства к работе

[7]
Особенности процессоров 80386 и выше. Использование расширенных регистров при разработке команд

[7.1] Особенности 32-разрядных микропроцессоров

[7.2] Расширенный набор команд 32-разрядного процессора

[8]
Защищенный режим работы процессора

[8.1] Перевод микропроцессора в защищенный режим

[8.2] Сегментная организация памяти в защищённом режиме

[9] Работа с расширенной памятью

[9.1] Использование дескрипторов памяти

[9.2] Формат шлюзов таблицы IDT

[9.3] Пример использования расширенной памяти

[10]
Переключение задач в защищённом режиме

[10.1] Понятие задачи

[10.2] Организация мультизадачного режима

[11]
Защита задач по привилегиям

[11.1] Уровни привилегий задачи

[11.2] Построение многоуровневой системы

[12]
Обработка аппаратных прерываний в защищенном режиме

[12.1] Аппаратные прерывания в защищённом режиме

[12.2] Пример обработки аппаратного прерывания

[13] Режим виртуального МП 8086

[14]
Виртуальные драйверы Windows 95/98

[14.1] Виртуальные драйверы и виртуальные машины Windows

[14.2] Структура виртуального драйвера

[15]
Заключение

[16]
Список использованных источников


Введение

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

От эффективности и надёжности работы отдельных программных модулей операционной системы зависит работа всего вычислительного комплекса. Следовательно, при разработке программ системного назначения, большое внимание уделяется как выбору эффективных алгоритмов решения поставленной задачи, так и подбору инструментальных средств, позволяющих получать эффективный и быстродействующий исполняемый код. Зачастую этими инструментами являются машинно-ориентированные языки программирования типа ассемблера, а также язык системного программирования Си. Эти языки позволяют напрямую общаться с аппаратурой вычислительного комплекса, что очень удобно при разработке управляющих программ системного назначения.

Данное учебно-методическое пособие помогает изучить возможности применения ассемблера при разработке программ системного назначения. Рассмотрены возможности программирования как защищённого, так и режима реального адреса.

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

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

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

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

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

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

Требования высокой эффективности СП вызывают необходимость использования специальных языков, к которым принадлежат машинно-ориентированные языки типа Ассемблера, и языки высокого уровня с развитыми формами представления внутренней, прежде всего, адресной информации СП, например, Си или PL/M. Эффективность выходного кода компиляторов с языков программирования высокого уровня определяется качеством генераторов машинного кода, которое далеко не всегда приближается к качеству программирования на языке Ассемблера высококвалифицированным специалистом.

 

  1.  Функции управляющих программ информационных систем

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

Хозяйственная деятельность человека опирается на численные модели и текстовые описания предметной области, включающие:

  •  познавательные модели для представления фундаментальных связей в экономических, естественных и технических предметных областях;
  •  нормативные модели, которые обеспечивают рациональное управление хозяйственной, технической и экономической деятельности;
  •  модели проектирования и планирования во всех видах деятельности;
  •  модели, основанные на фактах и отображающие экономическое и техническое состояние производства на конкретном хозяйственном и техническом объекте.

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

Целесообразность использования языка Ассемблера обусловлена сложностью построения эффективных машинно-ориентированных генераторов объектных кодов в случае специализации использования ресурсов процессора, а также сложность генерации эффективных специфических комплексов команд. Условия предельных требований к скорости обработки в программах управления и требование приемлемого времени решения в больших информационных системах стимулируют поиск новых, более эффективных методов решения и связанных с этим работы по усовершенствованию процессорных структур и архитектур. Эти работы в большей степени ориентированны на общее повышение уровня параллелизма в ВК и в меньшей — на специализированные устройства повышенной эффективности.

  1.  Требования к программам информационных и управляющих систем

Требования к программам информационных и управляющих систем можно сформулировать с позиций функционального и организационного определения целей. Базовые функции таких систем – это:

  •  поиск информации по разным типам поисковых образов;
  •  выполнение эквивалентных и адекватных преобразований для приведения образов к сопоставимой форме;
  •  анализ и распознавание образов по входным данным;
  •  анализ ситуаций в системе в ретроспективном и перспективном плане;
  •  формирование планов и проектов действий в рамках информационной или управляющей системы.

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

  •  быстрое выполнение сопоставлений оперативных данных с эталонами;
  •  быстрое и целенаправленное продвижение по таблицам эталонов при поиске решений;
  •  использование прагматически эффективных кодов для быстрого выполнения операций поиска;
  •  простота и гибкость перестройки для новых условий и аппаратуры применения;
  •  использование групповых и кооперируемых операций процессов.

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

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

  1.  Формализация структурного синтеза программ

Основу формализации процесса составления алгоритма решения задач создает методика сопоставления формализованных задач с возможностями ресурсов ВК, доступных пользователю, путем поиска аналогий и декомпозиции задач. Ресурсы доступные программисту можно систематизировать по уровням языковых конструкций, функциональных и процедурных решений и объектно-ориентированных конструкций. Функциональные, процедурные и объектно-ориентированные ресурсы оформляются как библиотеки языков программирования, библиотеки специализированных ОС, специализированные библиотеки пользователей и интегрированные пакеты программ. При проектировании программ целесообразно движение в направлении от задач и ресурсов более высокого уровня до более низкого уровня. Поэтому один из путей формализации построения алгоритмов и программ – определение проекций задачи на базовые ресурсы текущего уровня с постепенным понижением уровня ресурсов. Другой способ связан с эквивалентными преобразованиями аналитических связей к виду формул прямого вычисления целевых характеристик.

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

Детализация данных, выполняемая, когда первоначально спроектированные форматы и типы данных или объектов не реализованы в рамках оборудования или выбранного языка программирования целевой ЭВМ.

Декомпозиция связей и действий, выполняемая при наличии используемых типов и форматов данных и отсутствии необходимых действий для обработки этих данных для получения полезных целевых результатов.

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

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

Спецификация задачи должна включать описание цели решения задач в сфере исследований, включая цель проектирования объектов с предварительно определенными свойствами, а также цель исследования поведения объектов в координатном пространстве сферы исследований с определением их статистических и динамических свойств. В случае использования языка Ассемблера в спецификацию решения задач приходится дополнительно включать и описания используемых элементов машинной архитектуры.

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

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

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

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

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

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

Для разработки эффективных системных программ языки программирования должны учитывать особенности аппаратуры ВК. В настоящее время часто используют соединение языка Си с Ассемблером, именно они использовались для создания ОС UNIX и других системных программ.

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

  1.  
    Структура программы на языке ассемблера. Ассемблирование программ. Отладка с использованием Turbo Debugger

  1.  Структура программы на языке ассемблера

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

В примере 2.1 представлен простой заголовок, характерный для большинства примеров программ. Необязательная строка %TITLE описывает назначение программы, и при распечатке ее в Turbo Assembler текст, находящийся в кавычках, будет располагаться в начале каждой страницы исходного текста. Директива IDEAL переводит Turbo Assembler в режим Ideal. Если программа написана на Microsoft Macro Assembler (MASM), директиву IDEAL нужно опустить.

Пример 2.1 Типичный заголовок ассемблерной программы

%TITLE "Тестовый заголовок --Не ассемблируйте" IDEAL

MODEL small 

STACK 256

Далее следует директива MODEL выбирающая одну из нескольких моделей памяти (табл. 2.1), многие, из которых используются при написании комбинированных ассемблерных программ с языками Си или Pascal. Наиболее удачным выбором при программировании на языке ассемблера является small (малая) модель памяти. Малая модель памяти выделяет 64 Кбайт – под исполняемый код и 64 Кбайт под данные и позволяет создавать программы размером до 128 Кбайт, что практически является пределом эффективности. Директива STACK резервирует пространство для стека программы – области памяти, в которой содержатся два типа данных; промежуточные значения, записываемые подпрограммами или передаваемые им, также адреса, на которые подпрограммы возвращают управление. Значение, следующее за директивой STACK, определяет количество байтов (256), которое будет зарезервировано Turbo Assembler под сегмент стека. В основном программы использует небольшой стек, и даже самые большие из них редко требуют более 8 Кбайт.

Таблица 2.1 Модели памяти

Название

Код

Данные

Описание

 tiny

near

near

Код, данные и стек содержатся в одном сегменте 64 Кбайт. Используется только для COM-программ

 small

near

near

Код и данные содержатся в различных сегментах, размером до 64 Кбайт, Используется для небольших и средних EXE-программ

Продолжение табл. 2.1

 medium

far

near

Неограниченный размер кода. Под данные отводится один сегмент 64 Кбайт. Используется для больших программ, с небольшим объемом данных

 compact

near

far

Размер кода ограничен 64 Кбайтами. Размер данных - неограничен. Используется для малых и средних по размеру программ с большим количеством переменных

 large

far

far

Размер кода и данных не ограничен. Используется для больших программ. Размер переменной не должен превышать 64 Кбайт

 huge

far

far

Размер кода и данных не ограничен. Аналогичен модели large. (Введен для совместимости с языками высокого уровня)

 tchuge

far

far

Совпадает с большой моделью памяти, но по другому использует регистры. Используется при программировании на языках Turbo С И Borland C++

 tpascal

near

far

Поддерживает ранние версии Turbo Pascal

 flat

near

near

Используется в среде OS/2; в остальном аналогичен малой модели памяти

После заголовка программы следуют описания констант и переменных. В языке ассемблера константы часто называют макроопределениями, использующими директиву EQU, которая связывает значение с идентификатором. Исключительно для числовых значений, помимо директивы EQU, можно применять знак равенства (=).

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

Count EQU 10

Element EQU 5

Size = Count * Element

MyBoat EQU "Gypsy Venus"

Size = 0

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

  •  После описания имени константы с помощью директивы EQU нельзя изменять её значение. Переопределение этой константы в равенстве не допускается (например, изменение значения Count в примере).
  •  Это ограничение не распространяется на имена-идентификаторы, описанные с помощью знака равенства (=), их значения можно свободно изменять. Можно осуществлять это в любом месте программы, а не только в секции равенств.
  •  EQU может описывать все типы равенств, включая числа, выражения и символьные строки. Знак равенства может описывать только числовые равенства, состоящие из чисел 10 или 0Fh либо выражений Count*Size или Address+2.
  •  Идентификаторы макроопределений равенств не являются переменными ни они, ни их значения не содержатся в сегменте данных программы. Команды ассемблера не могут изменить значения идентификатора в макроопределениях, независимо от того, были они описаны с помощью директивы EQU или знака =.
  •  Хотя располагать макроопределения можно в любом месте программы, но для лучшей видимости предпочтительнее размещать их в начале текста. Макроопределения, расположенные глубоко в тексте программы, могут стать источником трудно находимых ошибок.
  •  Выражения, описанные с помощью EQU, вычисляются, когда соответствующее имя-идентификатор используется программой. Выражения, описанные через знак равенства (=), вычисляются непосредственно в месте определения. Ассемблер сохраняет текст EQU-выражения, а для выражения, описанного с помощью знака равенства, – только его значение. 

Сегмент данных программы обычно располагается между макроопределениями и командами программы. Можно определять сегмент данных в произвольном месте программы или иметь несколько сегментов данных в разных частях программного текста. Несмотря на эти возможности, ассемблерная программа будет проще для понимания и модификации, если будете определять переменные между макроопределениями и исполняемым кодом.

Сегмент данных программы должен начинаться с директивы DATASEG. Она дает указание ассемблеру разместить в памяти переменные, указанные в сегменте данных программы, который может достигать 64 Кбайт для малой модели памяти. Сегмент данных может содержать два типа переменных: инициализированные и неинициализированные. При исполнении программы инициализированные переменные имеют определенные значения, которые определены в тексте программы, и содержатся в файле программного кода на диске. Эти переменные автоматически загружаются в память и доступны для чтения при исполнении программы. Неинициализированные переменные аналогичны инициализированным, за исключением того, что они не занимают пространства в исполняемом файле и, следовательно, имеют неопределенные значения при исполнении программы. Вследствие этого, определение большой неинициализированной переменной для резервирования в памяти большого буфера, заполняемого из дискового файла, не приводит к увеличению размера исполняемого файла программы.

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

DATASEG

numRows DB 25

numColums DB 80

videoBase DW 0B00h 

Первой идет директива DATASEG, информирующая ассемблер о необходимости выделения пространства в памяти под сегмент данных программы. Затем определены три переменные: nuntRows, nurnColumns и videoBase. DB (определить байт) и DW (определить слово) – две чаще всего, используемые директивы, которые применяются для выделения пространства под соответствующие переменные программы. В отличие от языков высокого уровня, в которых расположение переменных в памяти обычно не является важным, в языке ассемблера необходимо выделять пространство в памяти под переменные и, в случае инициализированных переменных, приписывать значения этому пространству.

Имена переменных (numRows, numColumns и videoBase) являются метками, которые указывают на выделяемые в памяти области - в данном случае, на пространство, резервируемое под значения переменных. Программа может обращаться к этому пространству, используя метку как указатель на соответствующее значение в памяти. В ассемблерных программах метки преобразуются в адреса памяти, по которым содержатся переменные, что позволяет вам обращаться к памяти по именам, а не по численным адресам. Переменные располагаются внутри сегмента данных непосредственно одна за другой. К примеру, кажется, что определения

DATASEG 

aTOm DB "ABCDEFGHIJKLH"

nTOz DB "NOPQRSTUVWXYZ'

создают две символьные строки с метками aTOm и nTOz. Однако в памяти символы от А до Z хранятся последовательно, создавая одну строку букв алфавита. Метка nTOz просто указывает на середину строки, при этом в памяти не создаются два отдельных фрагмента.

DB имеет специальную возможность определять многобайтовые значения от одного до необходимого вам количества байтов. Строка состоит из отдельных ASCII-символов, каждый из которых занимает один байт, следовательно, директива объявляется в ассемблере простым инструментом для определения строк символов, которые представляют собой просто записанный последовательно в памяти набор ASCII-символов. Вы можете использовать DB определения отдельных символов и байтов, разделяя их запятыми:

DATASEG

perfectTen DB 1. 2, 3, 4, 5,6, 7, 8, 9, 10

theTime DB 9, 0 ; т.е. 9:00

theDate DB 12, 15, 98 ; т.е. 12/15/1998

Кроме того, можно комбинировать символьные и байтовые значения, создавая двухстроковые переменные с ASCII-символами возврата каретки (13) и конца строки (10). Следующий пример показывает, что символьные строки могут заключаться как в одинарные, так и в двойные кавычки:

combo DB 'Line #1, 13, 10, "Line #2"

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

После сегмента данных располагается тело программы, известное под названием кодового сегмента – раздела памяти, который содержит исполняемый код ассемблерной программы. Внутри этой области можно выделить четыре колонки текста: метки, мнемонику, операнды и комментарии. Количество пробелов между колонками в тексте программы произвольное. Чаще всего колонки выравнивают с помощью одно – или двукратного нажатия клавиши табуляции в редакторе. Рассмотрим сегменты данных и кода в примере 2.2. Он содержит основные элементы кодового сегмента языка ассемблера. В сегменте данных для дальнейшего использования определяется однобайтовая переменная с именем exCode со значением 0.

Пример 2.2

Метка

Мнемоника

Операнд

Комментарий

DATASEG

exCode 

DB

0

; Байтовая переменная

CODESEG

Start:

mov

ax, @data

;Пересылка в DS адреса

mov

ds, ах

;сегмента данных

jmp

Exit

; Переход на метку Ехit

mov

cx, 10

;Эта строка пропускается!

Exit:

mov

ah, 04Ch

;Функция DOS: выход из

;программы

mov

al, exCodel

;Возвращает код выхода

int

21h

;Вызов DOS. Останов программы

END

Start

;Конец программы/Точка входа

После директивы CODESEG представлено несколько строк, разделенных на метки, мнемонику, операнды и колонки комментариев. В первой колонке находятся две метки – Start: и Exit:. Метки помечают места в программе, на которые могут ссылаться другие команды и директивы. Для строк без меток эта колонка не заполняется. В кодовом сегменте метка всегда заканчивается двоеточием (:), в сегменте данных двоеточие не используется (например, в метке exCode).

Во второй колонке содержатся мнемоники. Под каждой мнемонической формулировкой в этой колонке скрывается одна машинная команда: mov – для Move (пересылка данных), jmp – для Jump (безусловный переход), int –для Interrupt (прерывание). Некоторые мнемоники запоминаются легко: dec – для Decrement (декремент), shl – для Shift Left (сдвиг влево), ror – для Rotate Right (циклический сдвиг вправо). В некоторых редких случаях мнемокод полностью совпадает с командой: out – для Out (вывод байта или слова в порт), push – для Push (занесение слова в стек), pop – для Pop (извлечение слова из стека).

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

Четвертая и последняя колонка являются необязательными, и если включаются в программу, то должны начинаться с точки с запятой (;). Turbo Assembler игнорирует все символы от точки с запятой до конца строки, предоставляя вам место для размещения короткого комментария, описывающего выполняемые в данной строке действия.

Многие программисты начинают свои программы многострочным пояснительным комментарием:

;Назначение: Предсказание выигрышных номеров лотереи

;Система: IBM PC/TurboAssembler Ideal Mode 

;Автор: Алексей Хакер

В режиме MASM существует другой тип комментариев, но, к сожалению, он не поддерживается в режиме Ideal. В режиме MASM вы можете начинать большие комментарии с директивы COMMENT, за которой следует символ, называемый разделителем комментария, и непосредственно текст комментария, заканчивающийся аналогичным разделителем. Для того чтобы использовать такой комментарий в режиме Ideal, необходимо на время переключиться в режим MASM:

MASM

COMMENT /* Комментарий, который может располагаться на нескольких строках и который можно легко переформатировать с помощь” команд используемого вами редактора */

IDEAL

После перехода в режим MASM многостроковый комментарий начинается с директивы COMMENT, косая черта используется в качестве разделительного символа. Завершается комментарий также косой чертой. (Звездочка используется для лучшего выделения комментария в тексте, а также для обозначения сходства с комментариями на языке С.) И, наконец, директива Ideal возвращает Turbo Assembler в режим Ideal. Пустые строки после MASM н перед Ideal позволяют форматировать весь блок комментария, используя соответствующие возможности редактора, и упрощают редактирование длинных строк в тексте программы.

Последней частью программы на языке ассемблера является заключение – одиночная строка, информирующая Turbo Assembler о достижении конца программы. В заключении используется единственная директива END.

END Start ;конец программы/точка входа

Директива END помечает конец текста исходной программы. Ассемблер игнорирует любой текст ниже этой строки и здесь можно разместить дополнительные комментарии. Справа от директивы END вы должны определить метку, с которой вы хотите начать выполнение программы. Обычно эта метка совпадает с меткой, указывающей на первую команду, следующую за директивой CODESEG. Вы можете начинать, исполнение программы с любой точки, хотя для этого вряд ли существуют особые причины.

Следующим шагом является ассемблирование текста программы для получения исполняемого файла на диске. Рассмотрим эту процедуру на примере.

Программа 2.1. FF.ASM

%TITLE "Посылка на принтер команды прогона строки "

 IDEAL 

 MODEL small 

 STACK 256

; -- Макроопределения

ASCllcr EQU 13 ; ASCII-символ возврата каретки

ASCllff EQU 12 ;ASCII-код прогона страницы

CODESEG

Start: mov ax, @data ;Установить DS адрес

mov ds, ах ;сегмента данных

mov dl, ASCllcr ; Переслать код возврата каретки в dl

mov ah, 05h ;Функция DOS: Вывод на принтер

int 21h ; ; Вызов DOS - возврат каретки

mov dl, ASCllff ;Переслать код прогона строки в dl  mov ah, 05h ;Функция DOS: Вывод на принтер

int 21h  ;Вызов DOS - прогон страницы

Exit:: mov ах, 04C00h ;Функция DOS: выход из программы int 21h  ;Вызов DOS. Останов прогона

END Start ;конец программы/точка входа

  1.  Ассемблирование программ

Для получения исполняемого кода необходимо выполнить следующие команды:

tasm ff tlink ff 

Команда tasm запускает Turbo Assembler, который считывает файл FF.ASM и, если программа введена правильно, создает файл FF.OBJ, содержащий объектный код еще не готовый, для непосредственного исполнения. Если обнаружились ошибки, исправьте их и повторите те же действия. Команда tlink запускает Turbo Linker, который считывает файл FF.OBJ и создает исполняемый файл FF.EXE.

Для преобразования ассемблерной программы из текстовой формы в исполняемую программу требуется выполнение двух шагов – ассемблирования и компоновки. Turbo Assembler никогда сразу не создает готовую к исполнению программу, а генерирует вместо этого промежуточный файл, содержащий ассемблерную программу в виде так называемого объектного кода. Перед запуском программы на исполнение необходимо обработать этот объектный код с помощью компоновщика, создав исполняемый ЕХЕ-файл. Turbo Linker (так же как и другие компоновщики) может объединять множество файлов объектного кода для получения одной исполняемой программы. Эта возможность позволяет разделить большую программу на небольшие фрагменты, ассемблировать их, создавая, отдельные объектные файлы, а затем скомпоновать с помощью одной команды. Отдельные фрагменты, или модули, могут иметь общие данные и вызывать подпрограммы, описанные в других модулях.

В файле объектного кода содержатся машинные команды, полученные на основе трансляции ассемблерного текста. Кроме этого, в объектном коде содержится текстовая информация, используемая другими модулями, плюс необязательная информация для Turbo Debugger. Нет необходимости разбираться во всем, что находится в объектном файле. Просто помните, что Turbo Assembler создает такой файл с расширением OBJ и никогда не генерирует окончательный исполняемый файл. Это делает исключительно Turbo Linker. Вообще, объектные файлы Turbo Assembler соответствуют стандарту OBJ, и их можно компоновать, используя, другие компоновщики с объектными файлами, полученными в языках других производителей (например, в Microsoft С). Можно компоновать объектные файлы Turbo Assembler с объектными файлами из других Turbo-языков. Для этих целей всегда используйте Turbo Linker.

В процессе ассемблирования или компоновки можно выбирать различные характеристики выполнения процесса, которые задаются опциями в командной строке Turbo Assembler и Turbo Linker. Для вывода списка опций командной строки Turbo Assembler наберите tasm и нажмите <Enter>. Для вывода списка опций командной строки Turbo Linker наберите tlink и нажмите <Enter>.

Опции описываются одной или несколькими буквами, за некоторыми из которых может следовать дополнительная информация. Для задания опции наберите дефис и соответствующую букву между командой tasm или tlink и именем программы, которую ассемблируете либо компонуете. Например, для ассемблирования программы 2.1 и получения файла с листингом используйте команду tasm -l ff .

Опции разрешается набирать как прописными, так и строчными буквами. Вместо дефиса можно использовать косую черту. Опция -1 информирует Turbo Assembler о необходимости, помимо ассемблирования программы, создать файл с листингом FF.LST на диске.

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

tasm /h 

tasm - lff 

tasm /lff

tasm -zi ff

tasm -l -iC:\INCLUDES ff

Вместо ассемблирования первая команда просто выводит список опций командной строки. Для печати его, используйте команду tasm /h > prn. Вторая команда создает файл листинга с перекрестными номерами строк(#10, #25 и т.п.) в конце. Третья команда, делая то же самое, и просто показывает, что при определении опций можно использовать косую черту вместо короткой черты. Четвертая команда добавляет в FF.OBJ информацию для Turbo Debugger. Последняя команда создает файл листинга и определяет путь включаемых (дополнительно используемых) файлов. Включаемые файлы – это отдельные текстовые файлы, которые по вашему желанию Turbo Assembler вставляет в программу. Программа 2.1 не использует включаемые файлы, следовательно, эта команда не приведет к каким-либо практическим результатам. Turbo Linker также имеет различные опции командной строки, определяемые аналогичным образом, за исключением того, что некоторые ранние версии TLINK требуют использования опций с косой чертой (/m), а не дефиса (-т). Некоторые версии компоновщиков допускают использование, как косой черты, так и дефиса, но при наборе нескольких опций с косой чертой в одной строке они должны разделяться пробелами. Ниже приводится несколько примеров применения опций в Turbo Linker:

tlink -v ff 

tlink /v ff

tlink -m -l ff tlink /m /l ff tlink -x ff

tlink /x ff

Опции -v или /v в первых двух строках подготавливают FF.EXE да использования его с Turbo Debugger. Следующие строки определяют две опции, которые приводят к созданию расширенного файла отображений (FF.MAP) и добавляют в этот файл вспомогательную информацию о номерах строк (/l). Опции /x и -х запрещают Turbo Linker создавать файл отображений, что приводит к небольшой экономии дискового пространства и времени компоновки. Применяйте эти команды, если нет необходимости в файле отображений, который показывает организацию памяти программы и обычно используется отладчиками либо как часть программной документации.

Опции Tasm:

/a Алфавитное упорядочение сегмента

/b Не имеет никакого эффекта.

/c Перекрестная ссылка в распечатке файла.

/d Определяет символ.

/e Генерирует команды эмулятора с плавающей точкой.

/h,/? Отображает экран справки.

/i Устанавливает путь для включаемого файла.

/j Определяет директиву запуска ассемблера.

/kh Максимальное число позволенных символов

ks Максимальный размер пространства под строку Турбо Ассемблера.

/l Генерирует файл распечатки.

/la Показывает код интерфейса высокого уровня в распечатке файла.

/m Позволить многократным проходам решать вперед ссылки

/ml Обрабатывает все символы как чувствительные к регистру.

/mu Преобразовывает символы в верхний регистр

/mx Делает общие(public) и внешние(external) символы(глобальные) чувствительные к регистру .

/mv Установить максимальное значение длины для символов

/n Подавляет таблицу символов в распечатке файла.

/o Вывод tlink-совместимые объекты с допускаемой оверлейной поддержкой.

/oi Вывод совместимых с IBM компоновщиком объектов IBM линейно - выполнимый компоновщик.

/op Вывод Pharlap совместимые объекты

/os Вывод tlink-совместимые объекты без оверлейных программ.

/p Проверяет на чистоту код защищенного режима.

/q Удаляет все записи из .OBJ, в которых нет необходимости для компоновки.

/r Генерирует действительные с плавающей запятой команды

/s Последовательное упорядочение сегмента. (Значение по умолчанию)

/t Подавляет сообщения об успешной трансляции.

/v Опция совместимость.

/w Генерация предупреждающих сообщений

/x Включает неправильные условные выражения в распечатку.

x/z Отображает исходные линии наряду с сообщениями об ошибках.

/zd Допускает информацию номера строки в .OBJ

/zi Допускает информацию отладки в объектном файле.

Между ошибками и предупреждениями существует важное отличие:

  •  Ошибки являются критичными для программы. Даже если будет создан объектный код, он не будет компоноваться и исполняться, т.е. TLINK не скомпонует его в исполняемый файл.
  •  Предупреждения не являются критичными. Результирующий объектный код, вероятно, будет компоноваться, но исполняться он может некорректно.

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

tasm ff>err.txt (ошибки сохраняются в файле err.txt)

tasm ff>prn (ошибки распечатываются на принтере)

Если не используется символ перенаправления вывода (>) и имя файла, то сообщение выводится на экран. Если ошибки помещаются на дисплее, можно распечатать копию экрана одновременным нажатием клавиш Shift и PrtScr.

  1.  Отладка программ с использованием Turbo Debugger

Обнаружить логические ошибки не так просто как синтаксические. Turbo Assembler умеет ассем6лнровать синтаксически правильные программы, но не понимает, что, собственно, эта программа должна делать. Часто программы работают не так, как, по вашему мнению, должны были бы работать. В такой ситуации помогает Turbo Debugger – программа, специально разработанная для упрощения поиска и исправления логических ошибок.

Подобно всем отладчикам, Turbo Debugger работает в режиме супервизора, беря на себя управление программой и позволяя пошагового исполнять код программы и проверять значения переменных в памяти. Можно потребовать, чтобы Turbo Debugger исполнял программу до некоторой точки или до появления определенной ситуации. Можно изменять значения в памяти, временно вставлять новые команды, а также менять значения регистров и флагов. Кроме того, можно использовать Turbo Debugger для программ в машинных кода, что иногда бывает полезно, если количество команд не слишком велико.

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

Если система снабжена процессорами 80386, 80486 или Pentium, то можно пользоваться преимуществами специального режима в Turbo Debugger. Однако, несмотря на это, Turbo Debugger является мощной программой, позволяющей с помощью своих команд отлаживать, программы на любом PC. Если система работает на 80386 или старшем CPU, вставьте в файл CONFIG.SYS, расположенный в корневом каталоге, следующую команду, определяющую правильный путь к файлу драйвера TDH386.SYS:

DEVISE=\TDEBUG\TDH386.3YS 

Это позволяет Turbo Debugger использовать специальные отладочные регистры, доступные только в процессоре 8038б. С помощью этих регистров Turbo Debugger может останавливать программу при изменении любого байта в указанной области памяти или даже если эти байты просто просматриваются отлаживаемой программой. Кроме того, можно запустить программу в виртуальной памяти, моделируя исполнение программы как автономного (самостоятельного) приложения DOS. Без процессора 80386 программа обязательно использует общую с отладчиком память. В результате некоторые ошибки, в особенности те, которые зависят от положения программы в памяти, могут не проявляться под управлением отладчика, а затем снова возникать при обычном исполнении программы. Эту хитрую ситуацию бывает весьма сложно зафиксировать. С инсталлированным драйвером можно использовать версию Тurbo Debugger для виртуальной памяти TD386.EXE вместо стандартной версии ТD.ЕХЕ.

Ассемблируйте и скомпонуйте программу с опциями, которые добавляют отладочную информацию в OBJ и ЕХЕ файлы. В результате Turbo Debugger получит информацию о программных символах, расположении переменных, организации сегментов и т.д. Для подготовки программы к отладке введите следующие команды:

tasm /zi ff

tlink /v ff

Без этих опций Turbo Debugger все равно загрузит вашу программу, однако будет показывать только дизассемблированные. Машинные коды. С опциями командной строки отладка может показывать метки, структуры переменных, строки исходной программы и другую информацию. После ассемблирования и компоновки с опциями /zi и /v убедитесь, что на диске находятся файлы FF.ASM и FF.EXE, затем загрузите программу в отладчик с помощью команды

td ff

Если установлен драйвер устройства TDH386.SYS и система работает на процессоре 80386, то можно использовать версию Turbo Debugger для виртуальной памяти, вызвав его командой

td386 ff

Через некоторое время на экране появится Turbo Debugger с исходным текстом программы. Можно только просматривать этот текст, используя клавиши управления курсором, для исправления ошибок необходимо выйти из Turbo Debugger и воспользоваться текстовым редактором. Для получения другого представления программы нажмите <Alt+V+C>, выбирающую команду View-CPU-Window (просмотр окна центрального процессора). В CPU-окне содержится в сокращенном виде исходный текст программы, машинные коды, значения регистров и флагов, дамп байтов памяти. Между этим и предыдущим окном существует принципиальное различие. В окне исходного кода (просмотра модуля) показывается копия текста программы. В CPU-окне вы находитесь в памяти, видите действительные значения расположенных там байтов. Для передвижения выделенного курсора по командам используйте клавиши <Up> и <Down>. Для передвижения между секциями окон нажимайте клавишу <Tab>. Для изменения вида окна выберите команду Mixed (<Alt+F10>). Она имеет три установки: No, Yes, Both, которые позволяют изменить тип просмотра программы:

No показывает дизассемблированные машинные коды, аналогичные командам языка ассемблера.

Yes показывает исходный текст программы вместе с дизассемблированным машинным кодом.

Both устанавливается по умолчанию.

Turbo Debugger может по-разному исполнять код. Рассмотрим это на примере.

Для запуска программы нажмите <F9>. Выполните эту команду и исследуйте состояние памяти, регистров и флагов по завершении программы.

После исполнения программы нажмите <Ctrl+F2> для возврата в начальное состояние. При этом программа снова загрузится с диска и Turbo Debugger восстановит свои исходные опции.

Дважды нажмите <F6> для возврата в окно исходного кода.

Нажмите <Alt+V+R> для выбора команды просмотра регистров. Окно регистров показывает значения регистров и флагов процессора вашего компьютера. Оно полезно для исследования результатов выполнения различных машинных команд.

Маленькая стрелка слева от первой команды программы mov ax, @data в окне исходного текста показывает, что она является следующей исполняемой командой. Для выполнения этой команды нажмите <F8>. Стрелка перейдет на следующую команду и изменится значение регистра ax в окне регистров. Эта команда “занесла” значения в регистр – назовем ее успешной. Передвижение по отдельным командам с помощью клавиши позволяет исполнять программу постепенно, выполняя за одно нажатие одну команду, и останавливаться для просмотра результатов работы каждой команды.

Снова нажмите <F8> для выполнения следующей команды – mov ds, ax. Значение в регистре ds стало таким же, как и значение в регистре ax. Команда mov переместила значение ax в ds.

Понажимайте <F6>, пока курсор снова не появиться в окне исходного кода, которое при этом закроет окно регистров.

Передвиньте курсор вниз на строку mov dl, ASCIIff – при этом три команды, расположенные выше, будут отмечены стрелками. Нажмите <F4> и программа выполниться от текущей команды до курсора. Используйте этот прием для выполнения небольших фрагментов программы, если нет необходимости останавливаться после каждой команды.

Несколько раз нажмите <F6>, пока снова не появиться окно регистров. Затем дважды нажмите <F8>, выполнив две следующие команды. При этом наблюдайте за значением регистра ds – оно должно частично измениться.

Теперь стрелка указывает на команду Int 21h. Эта команда вызывает функцию DOS, посылая символ на принтер. Для выполнения команды нажмите <F8>.

Нет необходимости исполнять эту программу дальше, так как оставшиеся команды просто возвращают управление DOS или, в нашем случае, – Turbo Debugger. Нажмите <Alt+X> для завершения работы с отладчиком.

  1.  Форматы исполняемых файлов

В DOS существуют два типа исполняемых файлов, один заканчивается расширением COM, другой – расширением EXE. Рассмотрим шаблоны, содержащие самое необходимое для разработки COM- и EXE-программ. На примере программы 2.2 показана оболочка для COM-программ. Пример программы 2.3 содержит соответствующую EXE-оболочку.

Программа 2.2. COMSHELL.ASM

1: %TITLE “Оболочка для COM-файлов”

2:

3: IDEAL

4:

5: MODEL tiny

6:

7: ;---- Вставьте здесь директивы INCLUDE “filename”

8:

9: ;---- Вставьте здесь макроопределения EQU и =

10:

11: DATASEG

12:

13: ;---- Если программа будет прервана по ошибке, запишите

14: ; соответствующий код ошибки exCode и выполните

15: ; команду JMP Exit

16:

17: exCode DB 0

18:

19: ;---- Здесь опишите другие переменные с помощью DB, DW и т.п.

20:

21: CODESEG

22:

23: ORG 100h ; Стандартный адрес начала COM-программы

24:

25: Start:

26:

27: ;---- Здесь располагается программа, вызовы подпрограмм и т.п.

28:

29: Exit:

30: mov ah, 04Ch ; Функция DOS : выход из программы

31: mov al, [exCode] ; Возврат значения кода выхода

32: int 21h ; Вызов DOS. Останов программы

33:

34: END Start ; Конец программы / точка выхода

Программа 2.3. EXESHELL.ASM

1: %TITLE “Оболочка для EXE-файлов”

2:

3: IDEAL

4:

5: MODEL small

6: STACK 256

7:

8: ;---- Вставьте здесь директивы INCLUDE “filename”

9:

10: ;---- Вставьте здесь макроопределения EQU и =

11:

12: DATASEG

13:

14: ;---- Если программа будет прервана по ошибке, запишите

14: ; соответствующий код ошибки exCode и выполните

15: ; команду JMP Exit

16: ; Чтобы это можно было сделать из подмодуля, опишите метку

17: ; Exit с помощью директивы EXTRN

18:

19: exCode DB 0

20:

21: ;---- Здесь опишите другие переменные с помощью DB, DW и т.п.

22:

23: ;---- Здесь опишите все переменные типа EXTRN

24:

25: CODESEG

26:

27: ;---- Здесь определите все подпрограммы типа EXTRN

28:

29: Start:

30: mov ax, @data ; Установка в DS адреса

31: mov ds, ax ; сегмента данных

32: mov es, ax ; Установка es=ds

33:

34: ;---- Здесь располагается программа, вызовы подпрограмм и т.п.

35:

36: Exit:

37: mov ah, 04Ch ; Функция DOS : выход из программы

38: mov al, [exCode] ; Возврат значения кода выхода

39: int 21h ; Вызов DOS. Останов программы

40:

41: END Start ; Конец программы / точка выхода

Программа 2.2 представляет правильный формат файла для разработки СОМ-программ в режиме Ideal. Строка 5 выбирает сверхмалую модель памяти (tiny), которая объединяет переменные программы, код и стек в одном б4 Кбайт сегменте памяти. Вследствие этого СОМ-программа всегда занимает в памяти 64 Кбайт, независимо от ее размера на диске. Этот факт является одной из причин предпочтительного использования ЕХЕ-программ. Хотя ЕХЕ-файл может занимать больше дискового пространства (так как в него включена дополнительная информация об организации программы), небольшие ЕХЕ-программы занимают при исполнении в памяти значительно меньше места, чем эквивалентные им СОМ-программы.

Строка 23 показывает другую характеристику СОМ-программы. Директива ORG указывает компилятору Turbo Assembler, что первая команда программы должна загружаться по адресу 100h относительно начала кодового сегмента программы. Сегодня уже нет разумных причин для использования этого устаревшего формата файла.

Для ассемблирования СОМ-программ используются другие, отличные от описанных выше, команды. Необходимо указать Turbo Linker опцию /t, которая определяет сверхмалую модель памяти. Для практики ассемблируйте и скомпонуйте программу 2.2 с помощью следующих команд:

tasm comshell 

tlink /t comshell 

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

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

Написание программ в ЕХЕ-формате требует несколько большей работы, чем при создании СОМ-программ, однако результат обычно оправдывает усилия. ЕХЕ-формат занимает в памяти только место, необходимое непосредственно для исполнения программы, оставляя больше свободного пространства для записи данных, создания больших массивов, а также для других ЕХЕ-программ в многозадачных операционных системах. Написание программ в ЕХЕ-формате позволяет им более эффективно использовать память.

При использовании малой модели памяти переменные, стек и машинный код содержатся в различных сегментах памяти, размером до 128 Кбайт, поэтому разработка ЕХЕ-программ требует дополнительных усилий. В программе 2.3 размер стека определяется директивой STACK (строка 6). Размер сегмента данных определяется суммарным размером переменных программы. Размер кодового сегмента зависит от количества команд в программе.

Поскольку в ЕХЕ-программах переменные хранятся отдельно от кода программы, прежде всего, необходима инициализация сегмента данных ds. Это выполняется строками 30-31 в программе 2.3 путем записи в регистр ax встроенного символа @data (строка 30), т.е. записи в регистр ах значения сегмента данных, и затем пересылки значения регистра ах в ds (строка 31). Это выполняется за два шага, так как в соответствии с синтаксисом ассемблера нельзя переслать значение вида @data непосредственно в сегментный регистр, запись значений в который допускается только через другие регистры общего назначения типа ax.

Как показывают строки 37-39, ЕХЕ-программы заканчиваются аналогично СОМ-программам. Их назначение состоит в передаче управления программе COMMAND.COM с использованием специальной функции DOS.

  1.  
    Разработка программ обработки прерываний

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

Двойное слово, в котором находится адрес начала программы обработки прерывания, называется вектором прерывания. Всего допустимо иметь 256 различных номеров прерываний. Адрес вектора каждого прерывания находится по его номеру, умноженному на 4, так как для хранения двойного слова адреса требуется 4 байта. Векторы прерывания занимают первые 256*4=1024 байта памяти. Для определения адреса подпрограммы обработки прерывания умножьте номер прерывания на 4. В первых двух байтах полученного адреса размещено значение IP, а в следующих двух значение CS подпрограммы обработки данного прерывания. Если написать свою подпрограмму обработки прерываний и с помощью функций, описанных ниже, записать ее адрес в вектор прерывания, то при вызове данного прерывания его обработка будет проходить по новой подпрограмме. Старый вектор прерывания обычно запоминается, чтобы восстановить его значение после завершения программы. Каждая подпрограмма обработки прерывания завершается командой IRET, которая похожа на команду RET, но дополнительно восстанавливает из стека регистр флагов. Каждая подпрограмма обработки прерывания должна сохранять значения всех используемых ею регистров процессора аналогично обычной подпрограмме.

Прерывания могут быть вложенными, т.е. одно прерывание может вызывать другое внутри себя. Например, с каждым отсчетом таймера происходит аппаратное прерывание по вектору 08h. Однако оно, в свою очередь, всегда вызывает прерывание 1Ch. Это прерывание предназначено для программиста. Изначально система делает его указывающим на адрес в ПЗУ, по которому расположена лишь команда возврата из прерывания IRET. Программист может присоединиться к этому вектору с тем, чтобы процессор по каждому тику таймера (18,2 тика в сек.) выполнял некоторую дополнительную работу.

MS DOS предоставляет функции 35h и 25h прерывания 21h для чтения и установки вектора прерывания. Функция 35h выполняет чтение адреса подпрограммы обработки прерывания.

При вызове:

AH=35h

AL – номер прерывания

Возвращает:

ES:BX – указатель на подпрограмму прерывания, BX содержит смещение, ES сегмент подпрограммы.

mov ah, 35h

mov al, 21h

int 21h

; в результате bx = 0206h, es=0AAEh

Функция 25h устанавливает новый вектор прерывания.

При вызове:

AH=25h

AL – номер прерывания

DS:DX – указатель на программу обработки прерывания.

mov ax, cod ; cod - имя сегмента программы обработки

mov ds, ax ; прерывания

lea dx, name ;name – имя подпрограммы обработки прерывания

mov ah, 25h ; номер функции

mov al, 08h ; номер прерывания

int 21h

Пример 6.1. Написать программу, выводящую в текущее положение курсора символ “@”, следующий символ “@” выводить в позицию левее или правее, выше или ниже текущего положения, вывод осуществлять непрерывно с некоторой задержкой. Направление вывода определяется, нажатием клавиш “8, 2, 6, 4” на клавиатуре, нажатием клавиши “0” работа программы завершается, клавишей “5” меняет цвет выводимого символа с желтого на зеленый и наоборот, нажатие остальных клавиш игнорируется.

Текст программы:

data segment

direct db 1 ; направление перемещения

exit db 0 ; признак окончания работы (если не 0)

old_cs dw ? ; для хранения “старого” вектора прерываний

old_ip dw ? ; с номером 1Ch

pos1 dw 3840 ; позиция для вывода символа (вначале левый

; нижний угол)

sum db “@” ; символ, выводимый на экран

atr db 14 ; атрибут символа (желтый)

atr2 db 10 ; атрибут символа (зеленый)

data ends

code segment

assume cs:code, ds:data

new_1c proc far ; подпрограмма обработки прерываний 1Ch

 push ax

push bx

push cx

push dx

push ds

mov ax, data

 mov ds, ax

push es ; проверка буфера клавиатуры

mov ax, 40h

mov es, ax ; es на сегмент области данных BIOS

mov ax, es:[1Ch] ; в ax указатель “хвоста” буфера клавиатуры

mov bx, es:[1Ah] ; в bx указатель “головы”

cmp bx, ax ; буфер пуст ?

jne n5 ; нет – на n5

pop es ; да – вернуться в основную программу

jmp back

n5: mov al, es:[bx] ; извлечь символ из буфера

mov es:[1Ch], bx ; указатель “хвоста” на “голову”

pop es

cmp al, 30h ; нажата ли клавиша “0” ?

jnz n1 ; нет – идем дальше

mov exit, 1 ; да – устанавливаем признак конца

jmp back ; работы основной программы и выход

n1: cmp al, 35h ; клавиша “5” ?

jne n6 ; нет – идем дальше

mov dl, atr ; да – обмениваем значения атрибутов

 mov dh, atr2

mov atr, dh

 mov atr2, dl

jmp back ; выход в основную программу

n6: cmp al, 34h ; влево

 jz n2

 cmp al, 36h ; вправо

 jz n3

 cmp al, 38h ; вверх

 jz n4

 cmp al, 32h ; вниз

jnz back ; неиспользуемая клавиша - выход

 mov direct, 4

jmp back

n2: mov direct, 2

jmp back

n3: mov direct, 3

jmp back

n4: mov direct, 1

back: pop ds

pop dx

pop cx

pop bx

pop ax

iret

new_1c endp

 

cls proc near ; подпрограмма очистки экрана

push cx

push ax

push si

xor si, si

mov ah, 7 ; атрибут

mov al,’ ’ ; символ, которым заполняется экран

mov cx, 2000 ; кол-во слов видеопамяти под весь экран

c: mov es:[si], ax ; занести символ и атрибут в видеопамять

add si, 2 ; перейти к следующему слову видеопамяти

loop c

pop bx

pop ax

pop cx

ret

cls endp

delay proc near ; подпрограмма задержки

push cx

mov cx, 80000

d: nop

loop d

pop cx

ret

delay endp

out_sym proc near ;подпрограмма вывода символа с заданным атрибутом

push ax

push bx

mov al, sym ; в al - символ

mov ah, atr ; в ah - атрибут

mov bx, pos1 ; в bx – позиция видеоОЗУ для вывода символа

call delay ; задержка перед выводом символа

mov es:[bx], ax ; помещение символа и атрибута в видеоОЗУ

pop bx

pop ax

ret

out_sym endp

start: mov ax, data ; основная программа

mov ds, ax ; чтение вектора прерывания

mov ah, 35 ; функция получения вектора

mov al, 1Ch ; номер вектора

int 21h ; сегмент – в es, смещение в bx

mov old_ip, bx 

mov old_cs, es ; установка вектора прерывания

push ds

mov dx, offset new_1c

mov ax, seg new_1c

mov ds, ax

mov ah, 25h

mov al, 1Ch

int 21h

pop ds

mov ax,0b800h ; в es- сегментный адрес

mov es, ax ; видеоОЗУ

call cls

l1: cmp exit, 0 ; проверка признака завершения программы

jn quit ; если не “0” – конец программы

cmp direct, 1 ; вверх

jz l2

cmp direct, 2 ; влево

jz l3

cmp direct, 3 ; вправо

jz l4

mov ax, pos1 ; вниз

add ax, 160 ; считаем новую позицию для вывода

cmp ax,3999 ; правее правой нижней границы экрана ?

jg next ; да – возвращаемся в цикл

mov pos1,ax ; нет – записываем в pos1 новую позицию

call out_sym ; и выводим символ в этой позиции

jmp l1 ; назад в бесконечный цикл

l2: mov ax, pos1

sub ax, 160

jl next

mov pos1, ax

call out_sym

jmp l1

l3: mov ax, pos1

sub ax2, 160

jl next

mov pos1, ax

call out_sym

jmp l1

l4: mov ax, pos1

add ax, 2

cmp ax, 3999

jg next

mov pos1, ax

call out_sym

next: jmp l1

quit: call cls

push ds

mov dx, old_ip ; подготовка к восстановлению

mov ax, old_cs

mov ds, ax ; подготовка к восстановлению

mov ah, 25h ; функция установки вектора

mov al, 1Ch ; номер вектора

int 21h

pop ds

mov ax, 4C00h

int 21h

code ends

end start

  1.  
    Использование системных средств обслуживания дисков для защиты программных продуктов от копирования и несанкционированного использования

Наиболее разнообразны программные средства защиты информации от несанкционированного доступа. К ним относятся программы шифрации данных по задаваемому пользователем ключу, администраторы дисков, позволяющие ограничить доступ пользователей к отдельным логическим дискам,. методы установки программного продукта с дистрибутивных дискет, позволяющие выполнить установку не более заданного числа раз, запуск защищаемых программ посредством некопируемых ключевых дискет, специальные защитные программные оболочки, куда погружаются защищаемые программы и многие другие. Привязка к местоположению на диске. Если требуется исключить копирование программы с жесткого диска на другой жесткий диск, ее можно привязать к номеру кластера или сектора, с которого начинается файл программы на диске. Привязка осуществляется следующим образом. Специально подготовленная установочная программа открывает файл с рабочей программой и по таблице открытых файлов находит начальный номер кластера. Это число, являющееся своеобразным ключом, записывается установочной программой в определенное место файла рабочей программы (естественно, в поле данных). Рабочая же программа после запуска, прежде всего, выполняет ту же операцию – определяет свой начальный адрес, а затем сравнивает его с ключом. Если числа совпадают, программа приступает к выполнению своей содержательной части; если не совпадают – аварийно завершается. При копировании программы на другой диск (или даже на тот же самый) она окажется расположенной в другом месте и номер кластера, записанный установочной программой, уже не будет соответствовать реальному адресу файла. В то же время с помощью установочной дискеты программу нетрудно установить на любом диске.

Для программы типа .СОМ легко определить местонахождение в файле программы на диске место, зарезервированное для ключа. Проще всего было бы расположить ключ в самом начале программы, однако так поступить нельзя, так как программа типа .СОМ обязана начинаться с выполнимой строки, а не с данных. Если, однако, начать программу таким образом:

text segmentcode

 assume cs:text, ds:text

 org 100h

myproc proc

jmp short entry

cluster dw 0

entry:

то в образе программы на диске двухбайтовая команда jmp short entry займет байты 0 и 1, а поле кластер попадет в бай ты 2 и 3 от начала файла (место, зарезервированное под РSP директивой ORG 100h, в файле будет отсутствовать; это поле будет "развернуто" только в процессе загрузки программы в память). Таким образом, открыв файл и определив из соответствующего ему блока SFT номер начального кластера, надо записать этот номер в файл в байты 2. и 3.

Сложнее дело обстоит в более общем случае, когда требуется защитить от копирования программу типа .ЕХЕ. Дело в том, что файл типа .ЕХЕ начинается не с текста программы, а с заголовка, в котором хранятся важные для системного загрузчика характеристики программы. Размер заголовка (который указывается в самом заголовке) зависит от сложности программы. Таким образом, для того, чтобы найти в файле с программой ее поля данных, надо сначала прочитать первый блок за головка и определить его общий размер. Второе затруднение может возникнуть, если сегмент данных не является в про грамме первым сегментом. В этом случае, чтобы попасть в сегмент данных, надо определить размеры сегментов, расположенных перед ним. Проще, конечно, расположить сегмент данных в начале программы.

В таблице 6.1, приведен формат заголовка файла загрузочного модуля типа .ЕХЕ в системе MS-DOS.

Для определения размера заголовка и, соответственно, истинного начала программы в файле .ЕХЕ (на диске, не в памяти!) следует прочитать первые 512 байтов файла и содержимое слова со смещением 08h умножить на 10h. Получится размер заголовка в байтах. Далее надо прочитать из файла весь заголовок и начало программы, после чего можно записать в обусловленную ячейку в начале программы найденный заранее ключ и перенести начало программы назад. Однако при неизвестной заранее длине заголовка возникнут проблемы с чтением из файла, – какой длины буфер отводить в программе? Это затруднение можно разрешить следующим образом. Прочитав первые 512 байтов заголовка в буфер размером 512 байтов, и найдя размер заголовка в параграфах, делим это число на 512/1б=32 и получаем размер заголовка в виде числа блоков по 512 байтов (размер заголовка всегда кратен 512). Если теперь мы последовательно прочитаем в тот же буфер полученное число блоков по 512 байтов, в буфере окажутся первые 512 байтов программы.

Таблица 6.1 Заголовок файла загрузочного модуля типа .ЕХЕ

Смещение

Число байтов

Описание

00h

02h

04h

06h

08h

0Ah

0Ch

0Eh

10h

12h

14h

16h

18h

1Ah

1Ch

2

2

2

2

2

2

2

2

2

2

2

2

2

2

Признак файла типа .ЕХЕ

Размер программы, включая заголовок по модулю 512

Размер программы, включая заголовок в блоках по 512 байтов

Число элементов таблицы настройки

Размер заголовка в параграфах

Число параграфов, требуемых программе дополнительно к ее образу на диске (при наличии в конце программы сегментов с неинициализированными данными)

Максимальное число параграфов, требуемых программе дополнительно к ее образу на диске (по умолчанию FFFFh)

Смещение сегмента стека от начала программы в параграфах

Содержимое регистра SP при входе в программу

Контрольная сумма

Содержимое регистра IP при входе в программу

Смещение сегмента команд от начала программы в параграфах

Относительный адрес первого элемента настройки

Число перекрытий (0 для резидентной части программы)

Таблица настройки переменной длины

Как известно, DOS выделяет место под файлы целыми кластерами, в результате чего за логическим концом файла практически всегда имеется свободное пространство (до конца кластера). При копировании файла на другой диск реально переносятся только бай ты, соответствующие самому файлу, так как число копируемых байтов определяется логической длиной файла; Байты последнего кластера файла, находящиеся за логическими пределами файла, не копируются. Если в них записать ключ, то при копировании ключ исчезнет, Методика работы с программой не отличается от уже описанной, После записи рабочей программы на жесткий диск она устанавливается с помощью специальной установочной программы (хранящейся на дискете). Установочная программа открывает файл с рабочей программой, перемещает указатель файла на его конец и записывает ключ.

  1.  Разработка резидентных программ обработки прерываний

  1.  Основные понятия

При разработке реальных резидентных программ и обработчиков аппаратных прерываний возникает целый ряд проблем. Некоторые из этих проблем относятся только к резидентным обработчикам или вообще к резидентным программам; другие свойственны как резидентным, так и транзитным обработчикам аппаратных прерываний. На рисунке 8.1 изображена топологическая карта, где левая окружность обозначает обработчики аппаратных прерываний, а правая – резидентные программы. Тогда фигура в центре, образованная пересечением этих окружностей, относится к резидентным обработчикам, полумесяц слева – к транзитным обработчикам, а полумесяц справа – к резидентным программам, не обрабатывающим аппаратные прерывания и, следовательно, активизируемым синхронно. Если обработчик аппаратного прерывания обслуживает не стандартную для компьютера аппаратуру (например, измерительную или управляющую), он должен представлять собой законченную программу, включающую в себя все программные элементы, необходимые для обеспечения работоспособности обслуживаемого устройства. Если, однако, прикладной обработчик прерываний пишется для стандартной аппаратуры (клавиатуры, таймера, КМОП-микросхемы, дисков и проч.), целесообразно в программу прикладного обработчика включить лишь необходимые дополнения к системному алгоритму обслуживания устройства. В этом случае прикладной обработчик надо сцепить с системным, организовав, в зависимости от конкретных требований, прикладную обработку либо до, либо после системной (а иногда и до, и после). Для обработчиков аппаратных прерываний характерен асинхронный способ активизации: аппаратное прерывание может возникнуть в любой момент времени, и состояние программы, в частности, содержимое регистров на момент прерывания никогда не бывает известно. Процессор, выполняя процедуру прерывания, сохраняет только регистры CS:IP и флаги; все остальные регистры, если они используются в программе обработчика, необходимо при входе в обработчик сохранить, а перед выходом восстановить. Обычно сохранение осуществляется в стеке. Если обработчик является транзитным, т.е. включен в качестве составного элемента в текущую транзитную программу, используется, естественно, ее стек, общий для всей программы и для обработчика. Предвидя эту ситуацию, необходимо зарезервировать в стеке достаточно места, что нетрудно сделать на этапе разработки программы. Сложности возрастают, если обработчик является резидентным.

В силу асинхронного способа активизации обработчик аппаратного прерывания может получить управление в тот момент, когда в основной программе выполняется запрошенная им функция DOS или BIOS. И DOS, и BIOS состоят из нереентерабельных программ; нельзя, прервав выполнение какой-то функции DOS, вызвать ту же или другую функцию – это неминуемо приведет к разрушению системы. Поэтому без принятия специальных мер в обработчике аппаратного прерывания недопустимо обращаться к функциям DOS или BIOS. Отмеченная проблема является важнейшей из всех перечисленных.

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

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

Даже если преодолеть трудности, возникающие из-за нереентерабельности DOS, в резидентом обработчике аппаратных прерываний нельзя обращаться к услугам файловой системы. Это связано с тем, что при открытии файла DOS использует для связи с системной областью файлов (SFT) таблицу файлов задания, расположенную в PSP текущей программы. Поскольку при переходе на программу обработчика текущей для системы остается прерванная программа, файлы, открываемые в обработчике, будут использовать ее PSP (или, например, PSP программы COMMAND.COM, если в памяти нет запущенных транзитных программ). Ясно, что завершение текущей транзитной программы и запуск вместо нее другой разрушит цепочку связи с SFT и лишит обработчик возможности работать с нужным ему файлом. Таким образом, в обработчике, обращающемся к файловым функциям DOS, необходимо выполнять переключение текущей программы, для чего в DOS предусмотрены соответствующие средства.

Многие резидентные программы (электронные справочники, базы данных, калькуляторы, блокноты и проч.) должны активизироваться нажатием некоторой клавиши или комбинации клавиш. Обычно такую клавишу называют "горячей". Таким образом, программа сама по себе не имеющая отношения к аппаратным прерываниям, должна, тем не менее, перехватывать прерывания от клавиатуры и вылавливать из потока нажимаемых клавиш свои горячие клавиши, чтобы активизироваться или, наоборот, "свернуться" и перейти в пассивное состояние. Такого рода программы обычно широко используют системные средства для чтения файлов, вывода на экран и ввода с клавиатуры; всеми этими средствами нельзя воспользоваться, находясь "внутри" программы обработки аппаратного прерывания от клавиатуры.

Резидентные обработчики аппаратных прерываний обычно представляют собой относительно короткие программы, в которых не предусматривается возможность их аварийного завершения вводом с клавиатуры команд <Ctrl>/C или <Ctrl>/<Break>. Однако такая команда может быть введена случайно в том момент времени, когда фактически выполняется программа обработчика, что может привести к нарушению работы системы. Схожая ситуация возникает и при возникновении в обработчике критической ошибки. Поэтому в резидентных обработчиках обычно предусматривают перехват векторов 23h, 24h и 1Вh и собственные программы (часто просто программы-заглушки) обработки этих программных прерываний.

В любой резидентной программе должны предусматриваться средства проверки на повторную установку. Такую проверку можно осуществить разными методами, однако наиболее распространенным является использование мультиплексного прерывания 2Fh.

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

  1.  Использование средств BIOS в обработчиках аппаратных прерываний 

И DOS, и BIOS являются нереентерабельными программами, однако их нереентерабельность вызывается разными причинами и проявляется по-разному. Рассмотрим, например, прерывание 13h BIOS, закрепленное за программами управления дисками. Программный запрос на выполнение функции 02h прерывания 13h должен привести к чтению в программу указанной группы секторов жесткого или гибкого дисков. Этот запрос реализуется программами BIOS с помощью целого ряда команд, направляемых в контроллер диска: инициализации, поиска требуемой дорожки, чтения состояния контроллера, собственно чтения. При этом многие команды выполняются путем многократного обращения к контроллеру: сначала в него посылается код команды, затем – требуемые для выполнения этой команды параметры. Например, команда чтения данных требует посылки, кроме кода команды, еще 8 параметров (номеров накопителя, цилиндра, головки, сектора и т.д.).

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

В то же время прерывание дисковой функции BIOS само по себе не страшно. Если в обработчике при этом вызываются какие-то другие прерывания BIOS, например int 10h для управления экраном или int 16h для ввода с клавиатуры, нарушения работы не произойдет. Таким образом, прерывания BIOS нереентерабельны только по отношению к самим себе – нельзя, прервав выполнение int 13h, вызвать в обработчике то же прерывание int 13h или, прервав работу с экраном на уровне BIOS через прерывание 10h, вызвать в обработчике функцию того же прерывания int 10h.

Итак, вызов функций BIOS возможен в обработчике только в те моменты времени, когда прерываемая программа не выполняет функции того прерывания, к которому мы хотим обратиться в обработчике. Но как программа обработчика узнает, не выполняется ли сейчас интересующее нас прерывание? Для этого в состав резидентного обработчика включаются секции перехвата всех прерываний BIOS, которые предполагается использовать в обработчике. Входные адреса этих секций заносятся в соответствующие векторы прерываний, а сами программы перехватчиков строятся так, что часть своей работы они выполняют перед системным обработчиком данного прерывания, а часть – после. Перехватчик прерывания BIOS перед передачей управления BIOS устанавливает флаг "занятости" данного прерывания, а после возврата из BIOS сбрасывает этот флаг. Основная же программа обработчика, перед тем, как вызывать функции BIOS, анализирует состояние этого флага и обращается к BIOS только в том случае, если флаг сброшен, т.е. данное прерывание BIOS "свободно". Программа перехватчика выглядит следующим образом:

capt_13h ргос ;Процедура перехватчика int 13h

flag_13h db 0 ;Флаг занятости прерывания 13h

old_13h dd 0 ;Ячейка для хранения исходного

new_13h: ;Точка входа в перехватчик

inc CS: flag ;Текущая программа вызвала int

;13h, установим флаг занятости

pushf ;Вызовем системный обработчик

call CS:old_13h ;прерывания 13h с возвратом

dec CS:flag ;Системная обработка int 13h

;закончилась, сбросим флаг занятости

iret ;Назад в текущую программу

capt_13h endp ;Конец процедуры перехватчика int

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

  1.  Использование средств DOS в обработчиках аппаратных прерываний 

Функции DOS, как и прерывания BIOS, нереентерабельны, однако нереентерабельность DOS носит иной характер. Среди системных областей DOS имеется весьма важная структура, которая называется областью текущих данных (Swappable Data Агеа (SDA)). Это довольно большая область объемом около 2 Кбайт, адрес которой можно получить с помощью недокументированной функции DOS 5D06h:

mov AX, 5D06h

int 21h

Функция возвращает в регистрах DS:SI адрес SDA, а в регистре СХ ее размер. Наиболее интересные для прикладного программиста поля SDA представлены в таблице 8.1.

Флаг критической ошибки ErrorMode устанавливается DOS, если зафиксировано состояние критической ошибки, которое может возникнуть по разным причинам, в большинстве своем связанным с дисковыми операциями: попытка записи на защищенный или отсутствующий диск, на диске не найден требуемый сектор, ошибка в контрольной сумме данных в секторе и т.д. Обнаружив такую ситуацию, DOS устанавливает флаг ErrorMode, записывая в этот байт SDA 1, и выполняет команду int 24h. В этом векторе (содержимое которого, дублируется в PSP каждой запускаемой задачи, см. табл. 8.1) хранится адрес системного обработчика критической ошибки, который выводит на экран аварийное сообщение и вводит ответную команду пользователя.

Флаг занятости DOS, обычно называемый флагом InDOS ("внутри DOS"), устанавливается диспетчером DOS сразу после анализа им номера вызванной функции DOS, и сбрасывается перед возвратом из DOS в прикладную программу. Таким образом, значение этого флага, равное 1, говорит от том, что выполняется программа DOS. Функции DOS в прикладной программе можно вызывать, только флаг InDOS сброшен.

Сегментный адрес текущего PSP (смещение 10h), является важнейшей системной переменной. В этом поле записывается адрес PSP (идентификатор ID) той программы, которую DOS считает текущей. Именно эта программа будет завершена, если выдать запрос на выполнение функции 4Ch (независимо от того, какая программа выдала этот запрос). Далее, при создании или открытии файлов используется таблица файлов задания JFT текущей программы, опять же независимо от того, какая программа реально выдала запрос на работу с файлом. Понятие текущей программы приобретает особую важность при разработке обработчиков аппаратных прерываний. Действительно, при вызове обработчика изменяются только значения CS:IP, текущей же остается прерванная программа. Поэтому, например, дескрипторы файлов, открытых в обработчике, находятся не в PSP обработчика, а в PSP прерванной программы, и при завершении этой программы работа обработчика (если он, будучи резидентным, остался в памяти) будет нарушена.

Таблица 8.1 Некоторые поля области текущих данных

Смещение

Число

байтов

Назначение

00h

01h

02h

03h

04h

06h

07h

08h

0Ch

10h

12h

14h

16h

17h

30h

31h

32h

34h

36h

264h

2D0h

336h

480h

600h

1

1

1

1

2

1

1

4

4

2

2

2

1

1

1

1

2

2

1

4

4

330

384

384

Флаг критической ошибки (флаг ErrorMode)

Флаг занятости DOS (флаг InDOS)

Дисковод, на котором зафиксирована последняя

критическая ошибка, или FFh

Устройство, на котором зафиксирована последняя ошибка

Код расширенной ошибки для последней ошибки

Предлагаемое действие для исправления последней ошибки

Класс последней ошибки

Содержимое ES:DI при последней ошибке

Адрес текущей области дисковой передачи 0ТА

Сегментный адрес текущего PSP (ID текущей программы)

Ячейка для хранения SP при вызове int 23h

Код возврата завершившегося процесса

Текущий дисковод

Расширенный флаг BREAK

День месяца

Месяц

Год-1980

Номер дня от 01.01.1960

День недели (0=воскресенье, 1=понедельник,...)

Прошлый кадр стека

Позапрошлый кадр стека

Вспомогательный стек (для функций 0lh...0Ch при

наличии критической ошибки)

Дисковый стек (для функций 00h, 0Dh...6Ch)

Стек ввода-вывода (для функций 0lh...0Ch)

Адрес текущей области дисковой передачи (Disk Transfer Агеа (DTA)) важен в тех случаях, когда в обработчике аппаратного прерывания вызываются функции DOS, использующие эту область. Раньше, когда для работы с файлами использовались блоки управления файлами (FCB), с помощью DTA осуществлялись все файловые операции; в настоящее время эта область нужна только при выполнении функций поиска файлов.

В области текущих данных находятся и все три системных стека – стек ввода-вывода, который используется DOS при выполнении функций ввода-вывода из диапазона 0lh...0Ch, дисковый стек для всех остальных функций DOS и вспомогательный стек, на который DOS переключается в том случае, если в процессе обработки критической ошибки возникла необходимость обратиться к функциям ввода-вывода (главным образом через терминал, т.е. к функциям ввода с клавиатуры и вывода на экран). Наличие внутренних стеков повышает надежность работы DOS, так как устраняется возможность переполнения стека прикладной программы при выполнении запросов к DOS, которые в своем большинстве активно используют стек для вызова системных подпрограмм, сохранения текущих значений и передачи параметров.

Диспетчер DOS, получив управление в результате выполнения команды int 21h, совершает целый ряд операций, из которых, прежде всего надо отметить следующие:

  •  сохранение регистров задачи на стеке задачи; инкремент флага InDOS, который при первом (не вложенном) вызове DOS становится равен 1;
  •  переносит прошлый кадр стека (SS:SP) в ячейку для позапрошлого кадра, что в данном случае не имеет особого значения;
  •  сохраняет в ячейке для прошлого кадра стека SS:SP прерванной задачи;
  •  заносит в SS:SP кадр одного из системных стеков в соответствии с вызванной функцией);
  •  вызывает по номеру функции требуемую программу DOS. После завершения вызванной функции диспетчер выполняет описанные шаги в обратном порядке: восстанавливает кадр стека задачи и кадр прошлого стека, декрементирует флаг InDOS, который опять становится равен 0, восстанавливает из стека задачи ее регистры и выполняет команду iret возврата в прерванную задачу.

Нереентерабельность DOS связана с тем, что при выполнении большинства функций используется один и тот же системный стек. Если выполнение какой-либо функции DOS будет прервано аппаратным прерыванием, и в обработчике прерывания будет вызвана функция DOS из той же группы, т.е. использующая тот же стек, то диспетчер загрузит в SS:SP в точности те же значения, что и при первом вызове. В результате вложенный вызов будет затирать те данные, которые были сохранены в стеке первым вызовом. Однако при вызове функции из другой группы ничего страшного не произойдет, так как функции ввода-вывода и "дисковые" работают на разных системных стеках. При этом наличие в SDA двух ячеек для хранения не только прошлого, ни и позапрошлого кадров стека; позволяет правильно обрабатывать один вложенный вызов DOS. Эта возможность широко используется во "всплывающих" программах, активизируемых с клавиатуры.

Фактический адрес флага занятости DOS можно получить с помощью документированной функции DOS 34h. Она возвращает двухсловный адрес флага InDOS в регистрах ES:BX. Получив и сохранив этот адрес на этапе инициализации обработчика прерывания, в самом обработчике перед вызовом функций DOS следует выполнить проверку флага занятости. Будем считать, что адрес флага сохранен в ячейке in-dos:

in_dos dd 0 ; Адрес флага InDOS

task_req db 0 ; Флаг требования запуска task

...

dos_busy: les bx, cs:int_dos ; Получили адрес флага InDOS

 cmp es:[bx], 0 ; DOS свободна?

 jne wait_dos ;Нет, придется ждать

 call task ;4а, можно вызывать функции DOS

iret ;Завершим обработчик

 wait_dos inc cs:task_req ;Установим флаг требования запуска

 iret ;Завершим обработчик

В этом фрагменте предполагается, что процедура task как раз и содержит вызовы DOS. Если флаг InDOS сброшен, вызывается эта процедура и после ее завершения завершается и весь обработчик. Если же флаг InDOS установлен, обработчик устанавливает флаг task_req, который говорит о том, что обработчик из-за занятости DOS "недовыполнил" свои функции, и процедура task требует своего запуска. Управление возвращается в прерванную задачу (фактически – в прерванную функцию DOS) и DOS имеет возможность завершить свою работу.

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

in_dos dd

task_req db old_08h dd

0

0

0

;Адрес флага InDOS

;Флаг требования запуска task

;Ячейка для хранения исходного

;содержимого вектора 08h

new_08h: pushf

call

cmp

jne 1es

cmp

jne dec

...

CS:0ld_08h CS:task_req, l out_08h BX,CS:in_dos ES:[BX],0

out_08h CS:task_req

;Передадим упр-ние в сист. Обработчик прерываний от таймера ;Процедура task требует запуска?

;Нет, можно завершить обработку

;Да, снова выясним,

;свободна ли 00S?

;Нет, придется опять ждать

;Да, сбросим флаг требования запуска

са11

out_08h:

iret

task

;и вызовем task 

;Завершим обработчик

Проверка занятости DOS по состоянию флага InDOS необходима, но не достаточна в тех ситуациях, когда возможно возникновение критической ошибки. Дело в том, что, обнаружив критическую ошибку, DOS выполняет декремент флага InDOS и инкремент флага критической ошибки ErrorMode, находящегося, как и флаг InDOS, в области текущих данных. После этого DOS с помощью прерывания int 24h вызывает обработчик критической ошибки, который выводит на экран запрос, соответствующий сложившейся ситуации, например,

Not ready reading drive А 

Abort, Retry, Fai1?

и ждет указаний пользователя. И вывод на экран, и ввод с клавиатуры осуществляются с помощью функций DOS из диапазона 0lh...0Ch, причем они вызываются "изнутри" той функции DOS, в процессе выполнения которой возникла критическая ошибка. Диспетчер DOS перед переключением стека проверяет состояние флага критической ошибки и если этот флаг установлен, делает текущим не стек ввода-вывода, как обычно, а вспомогательный стек. В результате такой вложенный вызов DOS не приводит к разрушению системы. Таким образом, в обработчике аппаратного прерывания перед вызовом какой-либо функции DOS следует анализировать состояние не только флага InDOS, но и флага ErrorMode. Если хотя бы один из них не равен 0, вызывать DOS нельзя.

В версиях DOS отсутствуют документированные средства для определения адреса флага критической ошибки. Известно, что в DOS начиная с версии 3.10, он расположен в первом байте области текущих данных перед флагом InDOS, и для получения его адреса можно воспользоваться недокументированной функцией 5D06h. Поскольку сама эта функция не является реентерабельной, определение адреса следует выполнить на этапе инициализации обработчика (вместе с определением адреса флага InDOS).

С учетом сказанного, приведенный выше фрагмент проверки занятости DOS будет выглядеть следующим образом (предполагается, что адрес флага критической ошибки помещен в ячейку crit_егг):

in_dos dd 0 ;Адрес флага InDOS

crit_err dd 0 ;Адрес флага ErrorMode

task_req db 0 ;Флаг требования запуска task

...

dos_busy: les bx: cs:in_dos ;Получили адрес флага InDOS

 lds si, cs:crit_err ;Получили адрес флага ErrorMode

 cmp es:[bx],0 ;Флаг InDOS сброшен?

jne wait_dos ;Нет, придется ждать

cmp ds:[si], 0 ;Флаг ErrorHode сброшен?

jne wait_dos ;Нет, придется ждать

call task ;Да, оба флага сброшены,

; можно вызывать функции DOS

iret ; Завершим обработчик

wait_dos inc cs:task_req ;Установим флаг требований запуска

 iret ; Завершим обработчик

Аналогичную коррекцию следует ввести и в процедуру обработки прерываний от таймера.

Описанная методика годится далеко не для всех программ. Если текущая программа ждет ввода с клавиатуры, она не вы ходит из соответствующей функции DOS и флаг занятости DOS непрерывно установлен. То же получается, если текущей программы вообще нет, так как в этом случае активным является командный процессор COMMAND.COM, который ждет ввода с клавиатуры очередной команды пользователя (функцией DOS 0Аh). В такой ситуации наш обработчик никогда не сможет вызвать требуемую функцию DOS. Для преодоления этого затруднения в DOS включено специальное прерывание int 28h, вызываемое функциями ввода с клавиатуры. Системный обработчик прерывания 28h содержит единственную команду iret, поэтому вызов int 28h при выполнении функции ввода-вывода никак не нарушает ход программы. Однако прикладная программа может поместить в вектор 28h адрес своей процедуры обработки этого прерывания. Поскольку прерывание 28h возникает только при выполнении функций, использующих стек ввода-вывода, в обработчике прерывания 28h допустим вызов любых функций DOS из диапазона 0Dh...6Ch. Надо только иметь в виду, что при входе в обработчик 28h все регистры заполнены системными значениями, и для выполнения прикладных функций их надо соответствующим образом инициализировать.

Таким образом, прикладной обработчик аппаратных прерываний, в котором вызываются функции DOS, должен включать, помимо активизатора по прерываниям от таймера (через вектор 08h еще и активизатор по прерыванию 28h со схожим алгоритмом)

in_dos dd 0 ;Адрес флага InDOS 

crit_err dd 0 ;Адрес Флага ErrorHode

task_req db 0 ;Флаг требования запуска task

old_28h dd 0 ;Ячейка для хранения исходного

... ;содержимого вектора 28h

new_28h: cmp cs:task_req, 1

jne out_28h

les bx, cs:in_dos

lds si, cs:crit_err

cmp ds:[si], 0 ;Флаг ErrorMode сброшен?

jne 0ut_28h ;Нет, придется ждать

cmp es:[bx], 1 ;Флаг InDOS не больше 1?

jam out_28h ;Больше, вложенный вызов DOS,

;вызов DOS запрещен

dec cs:task_req ;Сбросим флаг требования запуска

call task ;и вызовем task

out_28h: jmp cs:old_28h ;Завершим обработчик

Прикладной обработчик прерывания 28h (включенный в состав обработчика аппаратного прерывания) прежде всего, проверяет, установлен ли флаг запроса запуска задачи. Если этот флаг сброшен, надо просто вернуться в прерванную функцию DOS для чего в простейшем случае можно выполнить команду Iгеt. Если, однако, в системе имеется несколько обработчиков прерывания 28h, такое завершение лишит эти обработчики возможности фиксировать прерывание 28h. Поэтому наш обработчик лучше завершить командой jmp CS:old _28h передачи управления на тот обработчик, адрес которого мы вытеснили из вектора 28h.

Если флаг запроса запуска задачи установлен, сначала проверяется состояние флага критической ошибки, и, если он сброшен, то и состояние флага занятости DOS. Этот флаг дол жен быть равен 1 (ведь сейчас выполняется функция ввода с клавиатуры). Если значение флага 1, это свидетельствует о том, что "внутри" прерывания 28h уже вызвана функция DOS, и вызывать функции DOS, увеличивая тем самым уровень вложенности DOS, нельзя. Бывают ситуации (например, при использовании программы Norton Commander), когда в обработчике int 28h флаг InDOS оказывается равен не 1, а 0, Проверка значения флага на условие >1 учитывает и эту возможность.

Наконец, после успешного прохождения всех проверок вызывается процедура task с вызовами DOS, после завершения, которой управление передается предыдущему обработчику int 28h .

Вызов int 28h выполняется функцией DOS на стеке ввода-вывода. Поэтому в прикладном обработчике прерывания 28h можно вызывать только функции "дисковой" группы. Кроме того, недопустим вызов файловых функций с указанием стандартных дескрипторов клавиатуры или экрана (0...2). Каким же образом в таком обработчике можно выполнить ввод-вывод, через терминал? Для этого следует "открыть" терминал, как файл, и полученный от DOS дескриптор использовать для операций ввода-вывода:

fname db 'СОN', 0 ;Имя консоли в формате файловых функций

handle dw 0 ;Ячейка для дескриптора консоли ...

mov ah, 30h ;Функция открытия файла

mov al, 2 ;Режим ввода и вывода

mov dx, offset fname ;Адрес имени файла

 int 21h

mov handle, ax ;Сохраним дескриптор консоли

;Введём сообщение с клавиатуры

mov ah, 3fh ;Файловая функция ввода

mov bx, handle ;Дескриптор консоли

mov cx, 80 ;Предельное число вводимых символов

mov dx, offset buf_in ;Адрес буфера

;Выведем сообщение

 mov ah, 40h ;Файловая функция вывода

 mov bx, handle ;Дескриптор консоли

 mov cx, mes_len ;Длина сообщения

 mov dx, offset mes ;Адрес сообщения

 int 21h

Другой способ организовать ввод-вывод через терминал в обработчике прерывания 28h - воспользоваться функциями BIOS.

  1.  Асинхронная активизация резидентных программ командами с клавиатуры

Для управления резидентной программой командами, подаваемыми с клавиатуры, в частности, для активизации программы, в ее состав необходимо включить обработчик прерываний от клавиатуры (прерывание 09h). Выполнять резидентную программу, находясь "внутри" такого обработчика, можно только в том случае, если в программе не используются вызовы DOS или BIOS. Поэтому обычно в обработчике прерывания от клавиатуры лишь выполняется анализ введенной команды и установка флага запроса запуска задачи, если команда соответствует запросу на активизацию. Сама же активизация задачи осуществляется в обработчике прерывания 28h, как это было описано в предыдущем разделе. Структура секции запуска задачи (для случая активизации нажатием одной клавиши "серый плюс") может выглядеть следующим образом:

old_28h dd 0 ;Старое содержимое вектора 28h

old_09h dd 0 ;Старое содержимое вектора 09h

task_req db 0 ;флаг запроса запуска задачи

 ...

new_09h proc

push ax ;Сохраним АХ 

in al, 60h ;Введем скэн-код нажатой клавиши

cmp al, 4Eh ;Нажат серый плюс7

je plus ;Да, на установку флага

pop ax ;Восстановим АХ

jmp cs:old_09h ;В обработчик прерывания 09Ь

plus: inc cs:task_req ;Установим флаг запроса запуска

pop ax ;Восстановим АХ

jmp cs:old_09h ;В обработчик прерывания 09Ь

new_09h endp

new_28h proc

cmp cs:task_req, 1;Задача требует актиеизации2

je pop_up ;Да

jmp cs:old_28h ;Нет, в обработчик прерывания 28h

pop_up:

;Проверка флагов критической ошибки

;Сброс флага запроса запуска задачи

;Запуск задачи

iret

  1.  Работа с файлами в резидентном обработчике аппаратных прерываний 

Работа с файлами в резидентном обработчике аппаратных прерываний осложняется тем, что при переходе в обработчик текущей остается прерванная программа, и дескрипторы открываемых файлов создаются системой в таблице файлов задания ее PSP. Для того чтобы дескрипторы файлов, открываемых в обработчике, полностью принадлежали самому обработчику, необходимо при входе в обработчик объявить его для DOS текущей программой. Для манипуляций с идентификаторами программ (т.с. сегментными адресами, их PSP) в DOS предусмотрены функции 51h (Получить PSP) и 50h. (Установить PSP). Обе функции не используют системные стеки и являются реентерабельными, поэтому их можно безопасно вызывать в обработчиках аппаратных прерываний. Структура обработчика, работающего с файлами, выглядит следующим образом:

entry: ;Сохранение регистров прерванной программы

...

;Сделаем обработчик текущей программой

 mov ah, 51h ;Функция получения текущего PSP

 int 21h ;BX=ID текущей (т.е. прерванной) программы

 push bx ;Сохраним ID текущей программы

 mov ah, 50h ;Функция установки текущего PSP

 mov bx, cs ;В .COM-программе CS=ID программы

 int 21h

;Теперь текущей считается программа обработчика. При открытии файлов

;дескрипторы будут создаваться в JFT обработчика

; ...

;Работа с файлами обработчика (естественно, если DOS свободна )

; ...

;Сделаем прерванную программу текущей

 pop bx ;Восстановим ID прерванной программы

 mov ah, 50h ;Функция установки текущей PSP

 int 21h

;Восстановим регистры прерванной программы

iret

В тех случаях, когда в обработчике вызываются файловые функции поиска файлов 4Eh и 4Fh, использующие область дисковой передачи DTA, переключения ID программы недостаточно. Действительно, при загрузке программы DOS записывает в SDA не только адрес PSP этой программы, т.е, ее ID, но и двухсловный адрес текущей DTA. Поэтому в обработчике следует при входе сохранить адрес текущей DTA и установить в качестве текущей DTA обработчика, а при выходе восстановить DTA прерванной программы. Для получения и установки дисковой области передачи предусмотрены функции 2Fh и 1Аh; они не являются реентерабельными, поэтому перед их использованием следует убедиться, что DOS свободна.

;Переключение DTA перед использованием файловых функций

mov AH,2Fh ;Функция получения текущей DTA 

int 21h ;ES:BX= адрес DTA

push CS ;Настроим DS на PSP программы

рор DS ;(если это не сделано ранее )

push ES ;Сохраним сегментный адрес DTA

push BX ;Сохраним смещение DTA

mov ah, 1Ah ;Функция установки текущей DTA

mov DX, 80h ;Пусть DTA обработчика будет на своем

int 21h ; исходном месте – начиная со смещения 80h в PSP

;Теперь можно вызывать файловые функции, использующие DTA

...

;Переключение DTA перед завершением обработчика

 mov ah, 1Ah ;Функция установки текущей DTA

 pop dx ;Восстановим сегментный адрес DTA

 pop ds ;Восстановим смещение DTA

int 21h

Как известно, DTA программы по умолчанию находится в ее PSP, начиная со смещения 80h (то же поле используется и для копирования параметров командной строки). Функция установки DTA 1Ah требует загрузки двухсловного адреса DTA в регистры DS:DX. При вызове обработчика прерывания, выполненного в формате .СОМ, в CS находится сегментный адрес PSP. Командами DS также настраивается на сегментный адрес PSP. Теперь достаточно занести в DX смещение 80h и можно вызывать функцию установки DTA.

При использовании в резидентном обработчике функций DOS не исключено возникновение ошибок. Код ошибки возвращается системой в регистре АХ, а расширенная информация об ошибке записывается в предназначенные для этого поля SDA. Функция DOS 59h позволяет получить из SDА расширенную информацию об ошибке и программно проанализировать ее. Если прерывание, активизировавшее наш обработчик, возникло после выполнения в прерванной программе функции DOS, завершившейся с ошибкой, и, кроме того, ошибка возникла в самом обработчике, то информация об ошибке прерванной программы в SDA будет затерта новой информацией, поступившей в SDА результате ошибки в обработчике. Если прерванная программа (после возвращения в нее) вызовет функцию DOS 59h для получения расширенной информации об ошибке, она получит информацию, относящуюся к ошибке в обработчике. Поэтому в резидентном обработчике аппаратных прерываний следует перед вызовом функций DOS получить из SDA расширенную информацию об ошибке (с помощью функции DOS 59h), а перед выходом из обработчика – восстановить ее с помощью функции 5D, подфункции 0Ah.

Функция 59h возвращает расширенную информацию об ошибке в следующих регистрах:

АХ=расширенный код ошибки ;

ВН=класс ошибки ;

ВL= рекомендуемое действие

СН=местоположение ошибки

ES:DI=содержимое этих регистров для последней ошибки

  1.  Выгрузка из памяти резидентных программ 

При разработке резидентных программ обычно предусматривают средства выгрузки их из памяти, если отпала необходимость в их использовании. Следует заметить, что в DOS вообще отсутствуют средства выгрузки резидентных программ. Единственный предусмотренный для этого механизм – перезагрузка компьютера. Практически большинство резидентных программных продуктов имеют встроенные средства выгрузки. Обычно выгрузка резидентной программы осуществляется соответствующей командой, подаваемой с клавиатуры и воспринимаемой резидентной программой. Для этого резидентная программа должна перехватывать прерывания, поступающие с клавиатуры, и "вылавливать" команды выгрузки. Другой, более простой способ заключается в запуске некоторой программы, которая с помощью, например, мультиплексного прерывания 2Fh передает резидентной программе команду выгрузки. Чаще всего в качестве "выгружающей" используют саму резидентную программу, точнее, ее вторую копию, которая, если ее – запустить в определенном режиме, не только не пытается остаться в памяти, но, наоборот, выгружает из памяти свою первую копию.

Выгрузку резидентной программы из памяти можно осуществить разными способами. Наиболее простой - освободить блоки памяти, занимаемые программой (собственно программой и ее окружением) с помощью функции DOS 49h. Другой, более сложный – использовать в выгружающей программе функцию завершении 4Ch, заставив ее завершить не саму выгружающую, а резидентную программу, да еще после этого вернуть управление в выгружающую. В любом случае перед освобождением памяти необходимо восстановить все векторы прерываний, перехваченные резидентной программой. Следует подчеркнуть, что восстановление векторов представляет в общем случае значительную и иногда даже неразрешимую проблему. Во-первых, старое содержимое вектора, которое хранится где-то в полях данных резидентной программы, невозможно извлечь "снаружи", из другой программы, так как нет никаких способов определить, где именно его спрятала резидентная программа в процессе инициализации. Поэтому выгрузку резидентной про граммы легче осуществить из нее самой, чем из другой про граммы. Во-вторых, даже если выгрузку осуществляет сама резидентная программа, она может правильно восстановить старое содержимое вектора лишь в том случае, если этот вектор не был позже перехвачен другой резидентной программой. Если же это произошло, в таблице векторов находится уже адрес не выгружаемой, а следующей резидентной программы, и если восстановить старое содержимое вектора, эта следующая программа "повиснет", лишившись средств своего запуска. Поэтому надежно можно выгрузить только последнюю из загруженных резидентных программ.

Для удаления из памяти резидентной программы, надо в какой-то программе вызвать прерывание 2Fh с функцией, присвоенной этой программе, и подфункцией 01h. Проще всего создать для этого специальную "выгружающую" программу. Очевидно, однако, что такая методика выгрузки резидентной программы довольно неуклюжа. Для каждой резидентной программы нам придется создавать свою выгружающую программу. Гораздо изящнее использовать в качестве выгружающей саму резидентную программу. Пусть, например, наша резидентная программа имеет имя DUMP.COM. Предусмотрим в секции инициализации программы механизм анализа командной строки так, чтобы команда DUMP загружала эту программу в память, оставляя ее резидентной, а команда DUMP OFF "дезактивировала" программу и выгружала ее из памяти.

Как известно, если программа запускается с клавиатуры с указанием каких-то параметров (имен файлов, ключей, определяющих режим работы программы и проч.), то DOS, загрузив программу в память, помещает все символы, введенные после имени программы (так называемый хвост команды) в префикс программного сегмента программы, начиная с относительного адреса 80h. Хвост команды помещается в PSP во вполне определенном формате. В байт по адресу 80h DOS заносит число символов в хвосте команды (включая пробел, разделяющий на командной строке саму команду и ее хвост). Далее (начиная с байта по адресу 81h) следуют все символы, введенные с клавиатуры до нажатия клавиши <Enter>. Завершается хвост кодом возврата каретки (13).

Таким образом, если программа с именем DUMP была вызвана командой dump off , то в PSP будет записана следующая информация: 4,' off',13.

Программа может проанализировать наличие и содержимое параметров ее запуска в PSP и, обнаружив там слово 'off’, вызвать предусмотренную заранее функцию выгрузки мультиплексного прерывания 2Fh, например, функцию 0lh. Естественно, в обработчике прерывания 2Fh в транзитной программе должна быть предусмотрена процедура выгрузки программы из памяти при получении "своей" функции с подфункцией 0lh.

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

Как известно, функция DOS 4Сh всегда вызывается в конце программы, чтобы завершить ее выполнение и передать управление системе (конкретно - командному процессору COMMAND.COM). Однако откуда DOS узнает, какую именно программу (какой процесс) следует завершить, и куда именно, передать управление? Эта информация хранится в PSP выполняемой программы. В слове со смещением 16h от начала PSP записан идентификатор (т.е. сегментный адрес PSP) родительского процесса, а в двухсловной ячейке со смещением 0Аh конкретный адрес возврата в него (рис. 3). В то же время в области текущих данных SDA в слове со смещением 10h хранится идентификатор текущего процесса. Если компьютер не выполняет никакую программу, текущим является COMMAND.COM, и его ID записан в SDА. При запуске с клавиатуры любой программы (в том числе и той, которой предстоит сделаться резидентной), DOS записывает в SDА ее ID, а в PSP запущенной программы - ID родительского процесса и адрес возврата в него, чем и обеспечивается возможность завершения запущенной программы и возврата в систему. Для всех программ, запускаемых с клавиатуры (т.е. с помощью командного процессора) родительским процессом является COMMAND.COM.

Функция завершения 4Сh, выполнив все предназначенные ей действия по закрытию файлов, восстановлению векторов 22h, 23h и 24h и освобождению памяти, занимаемой текущим процессом, переносит в SDA содержимое слова PSP со смещением 16h, назначая родительский (по отношению к данному) процесс текущим, и передает управление по адресу возврата, записан ному в PSP.

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

1) занести в SDA ID завершаемой программы, чтобы объявить ее текущей, и чтобы функция 4С11 завершала именно ее;

2) занести в слово со смещением 16h в PSP завершаемой программы ID выгружающей программы, чтобы после завершения резидентной программы текущей снова стала выгружающая;

3) занести в двухсловную ячейку со смещением 0Ah в PSP завершаемой программы требуемый адрес возврата в выгружающую программу;

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

  1.  Свопинг области текущих данных DOS

Нереентерабельность DOS связана с использованием ею в процессе выполнения системных функций области текущих данных SDA, где находятся стеки DOS, а также ячейки для хранения целого ряда характерных адресов и других величин, в частности, кадров стеков. Наиболее радикальным методом придания DOS свойств реентерабельности (при вызовах DOS в обработчиках аппаратных прерываний) является сохранение в прикладном обработчике перед вызовом каких-либо системных функций всей области текущих данных, и восстановление исходного содержимого SDA после завершения работы с DOS. Как уже отмечалось, адрес SDA можно получить с помощью недокументированной функции 5Dh, которая также возвращает и размер SDA. Такого рода процедура часто называется свопингом, что и дало название области текущих данных (Swappale Data Area). Перед выполнением свопинга целесообразно по-прежнему проверить состояние флага InDOS, так как если этот флаг окажется сброшен, в свопинге нет необходимости.

  1.  
    Драйверы устройств. Общая структура и взаимодействие с DOS

  1.  Директивы ассемблера

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

Листинг 9.1. Директивы ассемблера в программе простого драйвера устройства помещают программы в сегмент cseg.

Столбец 1

Столбец 2

Столбец 3

Столбец 4

cseg

segment

para

public ‘code’

simple

proc

far

assume

cs:cseg,es:cseg,ds:cseg

метки

команды

параметры

комментарий

  1.  Основная процедура

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

  1.  Заголовок драйвера

Листинг 9.2 Заголовок драйвера, дающий Dos информацию о параметрах устройства в типе драйвера.

next_dev dd -1 ; Других драйверов нет

attribute dw 8000h ; Символьное устройство

strategy dw dev_strategy ; Адрес первого вызова из Dos

interrupt dw dev_int ; Адрес второго вызова из Dos 

dev_name db 'Simple$ ' ; Имя драйвера

Заголовок драйвера представляет собой таблицу данных для DOS. Когда DOS загружает драйвер, она должна определить его тип, который и указывается в заголовке драйвера.

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

Двойное слово next_dev указывает DOS, имеется ли за драйвером другой драйвер. Если это так, сегментный адрес и смещение этого драйвера помещается в next_dev. Именно таким образом DOS следит за драйверами и связывает их в цепочку. Если же следующего драйвера нет, то вместо сегмента и смещения в эту переменную записывается –1, и DOS узнает, что драйвер только один. Адреса записываются в next_dev в следующем порядке: сначала смещение, затем сегментный адрес. Такое использование next_dev дает DOS возможность помещать в один файл несколько драйверов устройств. Это экономит время, поскольку DOS вместо открытия и чтения нескольких файлов работает только с одним. Кроме того, DOS использует next_dev для связывания драйверов цепочку устройств. Новые драйверы вставляются в начало цепочки после драйвера NUL:. При запросе на доступ к устройству DOS находит нужный драйвер в цепочке устройств. Таким образом, если за простым драйвером следует другой драйвер, то сегментный адрес и смещение этого драйвера необходимо поместить в поле next_dev. Это указывает DOS, где искать следующий драйвер. Переменная next_dev последнего драйвера содержит –1.

Слово атрибутов устройства с меткой attribute состоит из 16 бит и указывает DOS тип драйвера. В табл. 9.1 показано содержимое этой переменной для наиболее популярных типов драйвера. Как видно из табл. 1, слово attribute может описывать много типов устройств. В нашем примере слово attribute содержит 8000h. При записи этого числа в двоичном коде бит 15 содержит 1, что говорит о символьном типе управляемого этим драйвером устройства. Остальные биты содержат нули, указывая тем самым на отсутствие соответствующих атрибутов.

Таблица 9.1 Биты атрибутов в заголовке драйвера

Номер бита

Признак установления бита

0

Стандартное устройство ввода

1

Стандартное устройство вывода

2

Пустое устройство

3

Часы

4

Специальное устройство

5

Зарезервирован (должен содержать 0)

6

Обобщенный IOCTL

7

IOCTL-ЗАПРОСЫ

8-10

Зарезервированы (должны содержать 0)

11

Устройство поддерживает ОТКРЫТИЕ/ ЗАКРЫТИЕ/ СМЕННЫЙ НОСИТЕЛЬ

12

Зарезервирован (должен содержать 0)

13

Не поддерживаемый фирмой IBM формат

14

IOCTL

15

Символьное устройство (сод. 0 в случае блочного)

Процедура стратегии является набором команд, обеспечивающих начальную настройку драйвера устройства, а процедура прерывания использует информацию из процедуры стратегии для выполнения требуемых действий. DOS дает команды драйверу устройства в 2 этапа: сначала вызывается процедура стратегии, а затем- процедура прерывания. Обозначения strategy и interrupt в листинге 2.2 являются именами адресов двух программ драйвера, используемых DOS. Эти 16-ти разрядные слова содержат адреса двух процедур: dev_strategy и dev_int. Сначала DOS передает управление драйверу по адресу, определенному в strategy, а во второй раз DOS передаст управление по адресу, определенному в interrupt.

Символьному устройству в драйвере должно быть присвоено имя. Символьным устройствам в драйверах присваиваются имена, а дисковым – буквы. Через это имя программа может вызывать драйвер устройства. В листинге 9.2 поле dev_name содержит имя SIMPLE$, которое DOS считает именем устройства. Имя устройства должно соответствовать двум требованиям DOS. Во-первых, оно не должно быть NUL, поскольку DOS не разрешает замену драйвера NUL:. Во-вторых, длина имени не должна превышать восьми символов. Если символов в имени меньше, оно должно быть дополнено нужным количеством пробелов.

  1.  
    1.  Рабочая область драйвера

Как видно из листинга 9.3, эта часть драйвера устройства содержит определение локальных переменных для процедур. Две переменные нужны процедуре прерывания простого драйвера, еще одну переменную для хранения выводимого на экран сообщения требует процедура initial, которая будет рассмотрена ниже. Хотя все эти три переменные будут использоваться только соответствующими процедурами, мы определяем их в одном месте, чтобы потом проще было их найти.

Листинг 9.3. Локальные переменные для простого драйвера устройства.

rh_ofs dw ? ; смещение заголовка запроса

rh_seg dw ? ; сегмент заголовка запроса

msg1 db 07h 

 db 'Простой драйвер устройства!'

 db 0dh,0ah,07h,'$'

Эти три переменные находятся в памяти сразу за заголовком драйвера. Переменные rh_ofs и rh_seg будут использоваться для хранения информации, передаваемой от DOS драйверу. Важность этих переменных будет показана ниже. Содержимое переменной msg1 при работе простого драйвера будет выводиться на экран. Строка байтов, которой дано имя msg1, состоит из шестнадцатеричных кодов и текста, заключенного в апострофы. Коды позволяют управлять курсором на экране или использовать специальные функции ПК. В данном случае код 07h вызывает звуковой сигнал перед выдачей сообщения на экран. Коды 0dh и 0аh приводят к возврату каретки и переводу строки после выдачи сообщения, чтобы последующие сообщения не выводились поверх нашего. После этого ПК снова издает звуковой сигнал. Программа, запрашивающая какие-то действия, выполняемые драйвером, сначала передает управление DOS. На основе запроса программы DOS строит заголовок запроса. Затем управление передается процедуре стратегии, которая сохраняет содержимое регистров ES и BX в переменных rh_seg и rh_ofs. И, наконец, процедура прерывания с помощью этих двух переменных считывает заголовок запроса для выполнения команды.

  1.  Процедура стратегии

В процедуре стратегии содержится первая часть исполняемых команд программы. Эти команды выполняются при первом вызове драйвера устройства операционной системой DOS. Имя процедуры ("стратегия") обусловлено тем, что DOS использует ее для планирования всех запросов к драйверу (функции инициализации). В нашем случае единственной задачей процедуры стратегии является сохранение адреса заголовка запроса в переменных rh_seg и rh_ofs для того, чтобы драйвер смог найти команду и данные для ее выполнения. После того, как адрес сохранен, управление возвращается DOS, что позволяет ей продолжать работу и снова вызывать драйвер. Теперь драйвер может с помощью rh_seg и rh_ofs получить доступ к команде для ее выполнения. Это осуществляется процедурой прерывания.

Команда mov используется для сохранения содержимого регистров ES и BX. Обратите внимание, что в командах содержится указание ассемблеру вести отсчет адресов относительно сегмента cs вместо сегмента ds по умолчанию. Замена сегмента объясняется тем, что после передачи управления драйверу значение в регистре DS неверно. Если не выполнить замену сегмента, адреса двух переменных определялись бы относительно некоего неизвестного значения в DS. По завершению работы процедуры стратегии управление возвращается DOS.

Листинг 9.4 Процедура стратегии

dev_strategy: ; Первый вызов из DOS

 mov cs:rh_seg,es ; Сохранить сегментный адрес

; заголовка запроса

 mov cs:rh_ofs,bx ; сохранить смещение заголовка запроса ret 

  1.  Процедура прерывания

Процедура прерывания определяет, какую команду должен выполнить драйвер устройства, и обрабатывает код этой команды. Когда DOS вызывает процедуру прерывания, управление передается на фрагмент программы с меткой dev_int. Первое, что выполняет процедура прерывания – это сохранение состояния регистров микропроцессора путем их помещения в стек командами push. Теперь эти регистры можно использовать.

Листинг 2.5. Процедура прерывания

dev int: ;Второй вызов из DOS

 c1d ;Сохранить состояние процессора

 push ds ; при входе

 push es

push ax

push bx

push cx

push dx

push di

push si

; Осуществить переход по команде, переданной в заголовке запроса.

 mov al, es:[bx]+2 ;Получить код команд

 cmp al, 0 ;В Al содержится 0?

 jnz exit3 ;Нет - выход по ошибке

 rol al,1 ;Получить смещение в таблице

 lea di, cmdtab ;Получить адрес таблицы команд

 mov ah,0 ;Очистить старший байт

 add di,ax ;Прибавить смещение

 jmp word ptr[di] ;Косвенный переход

; Таблица команд

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

cmdtab label byte

 dw init ;Инициализация

В конце работы драйвера, перед выходом, первоначальные состояния регистров должны быть восстановлены. Отметим, что стек DOS не очень большой, в него можно поместить всего около 20 элементов. Хотя чаще всего для сохранения состояния регистров такая емкость стека более чем достаточна, лучше следить за тем, сколько байтов помещается в стек.

Вторая часть процедуры прерывания предназначена для выяснения, какую команду должен выполнить драйвер. Код соответствующей команды передается драйверу в составе заголовка запроса, где он являет вторым элементом. Примерами команд или запросов являются чтение, запись, инициализация. Простой драйвер может выполнять только одну команду — команду инициализации, код которой 0.

Для обработки каждой команды драйвер должен иметь соответствующую процедуру. Поскольку каждой команде ставится в соответствие уникальный код, можно построить таблицу для всех команд, каждый элемент которой будет содержать адрес процедуры для выполнения определенной команды. При этом первый элемент будет содержать адрес процедуры для обработки команды с кодом 0, второй — для обработки команды с кодом 1 и так далее. Например, для десяти возможных значений кода команды следует построить таблицу из десяти элементов, каждый элемент которой содержит адрес процедуры, вызываемой при соответствующем значении кода команды.

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

Первая команда обращается к заголовку запроса через регистры ES и ВХ. Двойка прибавляется для обращения к третьему байту, содержащему код команды. Поскольку код команды является байтовой величиной, команда mov помещает его в младшую часть регистра АХ — регистр AL. Следующие две команды характерны только для простого драйвера и не встретятся вам в других драйверах устройств. Дело в том, что простой драйвер может выполнять только команду инициализации. Содержащийся теперь в AL код команды сравнивается с 0, являющимся кодом команды ИНИЦИАЛИЗАЦИЯ. Если он отличен от 0, совершается переход на метку exit3. С команды rol начинается фрагмент поиска процедуры инициализации. Таблица с адресами процедур состоит из 16-разрядных элементов, а код команды является байтовой величиной. Это вызывает некоторые осложнения. Если непосредственно использовать код команды для выбора адреса, индексирование таблицы будет выполняться по байтам. В результате мы получим только половину двухбайтового адреса. Следовательно, для правильного индексирования надо превратить байтовое значение кода команды в двухбайтовое, т. е. значение команды 0 должно дать первый 16-разрядный адрес, значение 1 — второй 16-разрядный адрес и т. д. Это достигается умножением кода команды на 2 командой сдвига влево rol. Следующая команда служит для получения адреса таблицы команд (cmdtab). Команда lea считывает адрес таблицы переходов для определения процедуры, на которую драйвер должен передать управление. Затем команда add добавляет преобразованный код команды к адресу таблицы команд. Таким образом, выполняется индексация. Индексный регистр DI теперь содержит адрес процедуры обработки команды с кодом 0. Помещение 0 в регистр АН командой mov является мерой предосторожности, поскольку однобайтовые коды команд не используют регистр АН. Наконец, косвенный переход через регистр DI передает управление процедуре инициализации, которая и выполняет команду.

  1.  Локальные процедуры

Локальные процедуры помогают драйверу устройства выполнять его функции. В данном случае понадобится только одна процедура – процедура инициализации драйвера init.

Листинг 9.6 Локальная процедура

initial proc near

lea dx,msg1 ;Сообщение при инициализации

 mov ah,9

int 21h ;Вызов DOS

ret ;Возврат

initial endp

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

  1.  Обработка команд DOS

Секция драйвера обработки команд DOS содержит процедуры для каждой из команд. В нашем случае мы обрабатываем только команду с кодом 0. Приведенная в листинге 9.7 процедура init начинает свою работу с вызова рассмотренной выше процедуры initial.

Листинг 9.7 Обработка команды DOS

;Команда = 0 ИНИЦИАЛИЗАЦИЯ

init: call initial ;Вывод сообщения

lea ax,exit ;Получить адрес конца (смещение)

mov es:[bx]+0eh,ax ;Сохранить смещение

push cs ;Получить адрес конца (сегмент)

pop ax

mov es:[bx]+10h, ax ;Сохранить в поле для адреса разрыва

jmp exit2

Затем устанавливается адрес точки разрыва, который указывает DOS, где кончается драйвер.

Таблица 9.2 Слово состояния

Название

Биты

Описание

ОШИБКА

15

Устанавливается драйвером, показывает наличие ошибки (см. табл. 2.3)

ВЫПОЛНЕНО

8

Должен устанавливаться драйвером при выходе

ЗАНЯТО

9

При необходимости устанавливается драйвером для предотвращения следующих операций

Код ошибки

0—7

Стандартный код ошибки DOS (см. табл. 2.3)

10—14

Зарезервированы

Каждая команда потребует включения в драйвер программного фрагмента для реализации соответствующей функции. Например, для передачи данных на устройство DOS передает драйверу команду записи (с кодом 8).

  1.  Слово состояния заголовка запроса

Заголовок запроса, который DOS посылает драйверу, возвращается обратно к DOS, имея в своем составе слово состояния, показывающее результат выполнения затребованной команды. Слово состояния включает биты, которые устанавливаются в зависимости от определенных условий. В табл. 2.2 перечислены эти условия и соответствующие им биты. В случае ошибки ее код помещается в биты 0—7. В табл. 9.3 приведены коды стандартных ошибок.

Таблица 9.3 Стандартные коды ошибок для драйверов.

Код

Описание кода ошибки

Код

Описание кода ошибки

0

Нарушение защиты от записи

8

Сектор не найден

1

Неизвестное устройство

9

В принтере нет бумаги

2

Устройство не готово

A

Ошибка записи

3

Неизвестная команда

B

Ошибка чтения

4

Ошибка CRC

C

Общий отказ

5

Неправильная длина структуры запроса накопителя

D

Зарезервирован (DOS 3+)

6

Ошибка поиска

E

Зарезервирован (DOS 3+)

7

Неизвестный носитель

F

Неверная смена диска (DOS 3+)

Комбинация битов слова состояния может отражать любое состояние. Например, если запрос успешно выполнен, устанавливается бит ВЫПОЛНЕНО. Если же запрос выполнен, но возникла ошибка, устанавливаются и бит ВЫПОЛНЕНО, и бит ОШИБКА. Кроме того, возвращается код ошибки, по которому DOS (и пользователь) может узнать, что послужило причиной ошибки. Обратите внимание, что программа драйвера должна уметь распознавать ошибки и устанавливать соответствующие им биты в слове состояния. В случае вызова простого драйвера возможны лишь две ситуации и два соответствующих им кода состояния. Первая ситуация возникает, когда DOS передает драйверу команду инициализации (код 0) при его начальной загрузке. Вторая ситуация возникает, если DOS вызывает простой драйвер командой, отличной от нуля. Это может произойти в разных случаях. Например, пусть вы хотите скопировать содержимое какого-нибудь файла в SIMPLE$ (DOS считает это устройством) с помощью следующей команды: Copy simple.asm simple$. Для выполнения этой команды DOS вызовет драйвер командой вывода, или записи, с номером 8 (одной из шестнадцати возможных команд для драйверов). Это приведет к ошибке, так как наш драйвер не может выполнять никакие команды, кроме команды инициализации.

  1.  Процедура выхода при ошибке

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

Write protect error writing drive A

Abort, Retry, Ignore?

Если в процессе работы простого драйвера возникает ошибка, управление передается на ехitЗ. Как показано в листинге 2.5, к переходу exit3 ведет отличие кода команды от 0. Фрагмент обработки ошибок программы простого драйвера приведен в листинге 9.8.

Листинг 9.8 Выход при ошибке

; Установить флажок ВЫПОЛНЕНО,

; флажок ОШИБКА и код ошибки НЕИЗВЕСТНАЯ КОМАНДА

exit3: mov es: word ptr 3 [bx], 8103h

jmp exit1 ;Восстановить среду

Как показано в листинге 9.8, регистры ES и ВХ адресуют заголовок запроса с четвертого байта (относительный байт 3) начинается стояния. Это 16-разрядное слово возвращается в DOS и показывает итог работы драйвера. Если происходит переход на exit3, устанавливаются биты в поле состояния, которые показывают, что произошла ошибка, и указывают ее тип. Число 8103h в листинге 3.9 получено следующим образом: установлены бит ВЫПОЛНЕНО (бит 8), ОШИБКА (бит 15), означающий наличие ошибки, и биты кода ошибки образующие число 3 (установлены биты 0 и 1). Код ошибки 3 говорит DOS о том, что драйверу была подана неизвестная команда. Благодаря возвращаемому в заголовке запроса состоянию DOS информацию о выполнении драйвером команды.

  1.  Процедура общего выхода

Перед выходом необходимо восстановить состояние регистров процессора, которое было до начала работы драйвера. В качестве первого шага при восстановлении среды необходимо установить для DOS состояние завершения работы драйвера, поскольку DOS должна знать, как закончились операции драйвера. Если программа при ошибке переходит на exit3: в данной процедуре устанавливать состояние не нужно, поскольку оно уже установлено.

Листинг 9.9 Общий выход

;Общие выходы через три точки

;exit2 устанавливается бит ВЫПОЛНЕНО и сбрасывается бит ОШИБКА

;exit1 восстанавливается ES:BX вызывающей программы

;exit0 восстанавливается состояние машины и выход

exit2: ;Установить бит ВЫПОЛНЕНО

;и сбросить бит ОШИБКА

 mov es: word ptr 3[bx],0100h

exit1: mov bx, cs: rh ofs ;Восстановить заголовок запроса

 mov es, cs: rh seg ; в ES: BX

exit0: pop si

pop di

pop dx

pop cx

pop bx

pop ax

pop es

pop ds

ret

exit:

Первая команда по метке exit2 в листинге 2.9 устанавливает в слове заголовка запроса бит ВЫПОЛНЕНО (0l00h). Второй шаг при восстановлении среды заключается в восстановлении значений регистров ES и BX. В процедуре стратегии значения этих регистров были сохранены в переменных rh_seg и rh_ofs. Третий шаг восстановления среды – восстановление из стека значений всех регистров с помощью команды pop. Наконец, команда ret возвращает управление DOS.

  1.  Секция конца программы

Необходимо указать конец процедуры simple, а также конец сегмента cseg с помощью директив endp и ends. Директива end имеет в качестве операнда метку begin.

Листинг 9.10 Конец программы

simple endp

cseg ends

end begin

  1.  Подготовка драйвера устройства к работе

Исходный текст необходимо ассемблировать, скомпоновать и перевести в .СОМ-формат. Результатом работы утилиты ТLINK является файл в .ЕХЕ-формате, содержащий информацию о перемещении и не всегда пригодный для использования. Перед этим необходимо преобразовать драйвер в .СОМ-формат утилитой EXE2BIN(или какой-либо другой). Это важно, поскольку DOS не ожидает встретить в файле драйвера устройства никакой информации о перемещении. Затем переименовать его, указав расширение .SYS. Чтобы дать возможность DOS использовать простой драйвер, вы должны поместить в файл CONFIG.SYS команду DEVICE= и указать имя драйвера.

  1.  
    Особенности процессоров 80386 и выше. Использование расширенных регистров при разработке команд 

  1.  Особенности 32-разрядных микропроцессоров

С появлением 32-разрядных процессоров 80386 фирмы Intel, программисты значительно расширили спектр своих возможностей. Эти процессоры могут работать в трех режимах: реальном, защищенном и виртуального процессора 8086.

В реальном режиме достигается полная совместимость с программным обеспечением, разработанным для процессора 8086. Однако в этом случае выполняемая программа может использовать только адресное пространство памяти размером в 1Мбайт. В защищенном режиме используются полные возможности 32-разрядного процессора – обеспечивается непосредственный доступ к 4 Гбайт физического адресного пространства и многозадачный режим. Реализовать многозадачность для программ, разработанных для процессора 8086, можно с помощью режима виртуального процессора 8086. В этом случае операционная система защищенного режима позволяет создать несколько задач, каждая из которых будет выполняться как бы отдельным процессором 8086.

Процессор 486 содержит около 40 программно-адресуемых регистров (не считая регистров сопроцессора), из которых десять являются 16-разрядными, а остальные – 32-разрядными. Регистры принято объединять в семь групп: регистры общего назначения (или регистры данных), регистры-указатели, сегментные регистры, управляющие регистры, регистры системных адресов, отладочные регистры и регистры тестирования. Кроме того, в отдельную группу выделяют счетчик команд и регистр флагов.

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

Для сохранения совместимости с ранними моделями процессоров допускается обращение к младшим половинам всех регистров, которые имеют те же мнемонические обозначения, что и в МП 8086 (AX, ВХ, СХ, DX, SI, DI, ВР и SP). Естественно, сохранена возможность работы с младшими (AL, BL, CL и DL) и старшими (АН, ВН, СН и DH) половинками регистров МП 8086. Однако старшие половины 32-разрядных регистров МП 486 не имеют мнемонических обозначений и непосредственно недоступны. Для того, чтобы прочитать, например, содержимое старшей половины регистра ЕАХ (биты 31... 16) придется сдвинуть вес содержимое ЕАХ на 16 бит вправо (в регистр АХ) и прочитать затем содержимое АХ. Все регистры общего назначения и указатели программист может использовать по своему усмотрению для временного хранения адресов и данных размером от байта до двойного слова. Так, например, возможно использование следующих команд:

mov ЕАХ, 0FFFFFFFFh ; Работа с двойным словом (32 бит)

mov BX, 0FFFFh ; Работа со словом (16 бит)

mov CL, 0FFh ; Работа с байтом (В бит)

Все сегментные регистры, как и в процессоре 8086, являются 16-разрядными. В их состав включено еще два регистра - FS и GS, которые могут использоваться для хранения сегментных адресов двух дополнительных сегментов данных. Таким образом, при работе в реальном режиме из программы можно обеспечить доступ одновременно к четырем сегментам данных, а не к двум, как при использовании МП 8086.

Регистр указателя команд также является 32-разрядным, и обычно при описании процессора его называют EIP. Младшие шестнадцать разрядов этого регистра соответствуют регистру IP процессора 8086.

Регистр флагов процессора 486 принято называть EFLAGS. Дополнительно к шести флагам состояния (CF, PF, AF, ZF, SF и OF) и трем флагам управления состоянием процессора (TF, IF и DF), назначение которых было описано в статье 3, он включает три новых флага NT, RF и VM и двухбайтовое поле IOPL.

Новые флаги NT, RF и VM и поле IOPL используются процессором только в защищенном режиме.

Двухразрядное поле привилегий ввода-вывода IOPL (Input/Output Privilege Level) указывает на максимальное значение уровня текущего приоритета (от 0 до 3), при котором команды ввода-вывода выполняются без генерации исключительной ситуации.

Флаг вложенной задачи NT (Nested Task) показывает, является ли текущая задача вложенной в выполнение другой задачи. В этом случае NT=1. Он устанавливается автоматически при переключении задач. Значение NT проверяется командой iret для определения способа возврата в вызвавшую задачу.

Управляющий флаг рестарта RF (Resumption Rag) используется совместно с отладочными регистрами. Если RF=1, то ошибки, возникшие во время отладки при исполнении команды, итерируются до выполнения следующей команды.

Управляющий флаг виртуального режима VM (Virtual Mode) используется для перевода МП 486 из защищенного режима в режим виртуального процессора 8086. В этом случае процессор 486 функционирует как быстродействующий микропроцессор 8086, но реализует механизмы защиты памяти, страничной адресации и ряд других возможностей.

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

Регистр CR0 представляет собой слово состояния системы. Для управления режимом работы процессора 486 и указания его состояния используются следующие шесть битов:

Бит страничного преобразования РE (Paging Enable). Если этот бит установлен, то страничное преобразование разрешено, если сброшен, то запрещено.

Бит типа сопроцессора ЕТ (Processor Extension Type) в МП 80286 -80386 указывает на тип подключенного сопроцессора. Если ЕТ=1, то 80387, если ЕТ=0, то 80287. В МП 486 бит ЕТ всегда установлен.

Бит переключения задачи TS (Task Switched). Этот бит автоматически устанавливается процессором при каждом переключении задачи. Бит может быть очищен командой clts, которую можно использоваться только на нулевом уровне привилегий.

Бит эмуляции сопроцессора ЕМ (Emulate Coprocessor). Если ЕМ=1, то обработка команд сопроцессора производится программно. При выполнении этих команд или команды wait возбуждается исключение отсутствия сопроцессора.

Бит присутствия арифметического сопроцессора МР (Math Coprocessor). Операционная система устанавливает МР=1, если сопроцессор присутствует. Этот бит управляет работой команды wait, используемой для синхронизации работы программы и сопроцессора.

Бит разрешения защиты РЕ (Protection Enable). При РЕ=1 процессор работает в защищенном режиме, при РЕ=0 – в реальном. РЕ может быть установлен при загрузке регистра CR0 командами Lmsw или mov CR0, а сброшен только командой mov CR0.

Регистр CR1 зарезервирован фирмой Intel для последующих моделей процессоров.

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

Регистры системных адресов используются в защищенном режиме работы процессора 486. Они задают расположение системных таблиц, служащих для организации сегментной адресации в защищенном режиме. В состав процессора 486 входят четыре регистра системных адресов:

GDTR (Global Descriptor Table Register) – регистр таблицы глобальных дескрипторов, в который с помощью специальной структуры данных - псевдодескриптора, загружаются характеристики таблицы глобальных дескрипторов (линейный базовый адрес и граница).

LDTR (Local Descriptor Table Register) – регистр таблицы локальных дескрипторов, в который загружается селектор сегмента таблицы, локальных дескрипторов.

IDTR (Interrupt Descriptor Table Register) – регистр таблицы дескрипторов прерываний, в который с помощью псевдодескриптора загружаются характеристики сегмента таблицы дескрипторов прерываний.

TR (Task Register) – регистр состояния задачи, в который загружается селектор сегмента состояния задачи.

Отладочные регистры предназначены для отладки программ с использованием аппаратных возможностей процессора 486. Все эти регистры являются 32-разрядными и содержат следующую информацию:

  •  регистры DR0, DR1, DR2, DR3 (Debug Register)- линейные адреса точек останова 0, 1, 2 и 3 соответственно;
  •  регистры DR4 и DR5 зарезервированы фирмой Intel для следующих версий;
  •  регистр состояния точки останова DR6 содержит информацию, позволяющую отладчику определить, какое из условий отладки удовлетворено.
  •  регистр управления отладкой DR7 задает вид доступа к памяти, связанный с каждой контрольной точкой.

Два из регистров тестирования, TR6 и TR7 используются для проверки буфера трансляции адресов процессора, а остальные 3 (TR3, TR4 и TR5) – для контроля правильности работа внутренней кэш-памяти процессора.

Рассмотрим на простых примерах работу с регистрами МП 486. Пример 10.1 показывает принципы работы с 32-разрадными регистрами. В регистр ЕАХ заносится 8-битовое шестнадцатеричное число, выполняется операция сложения, а результат записывается в память. Заметим, что для сохранения результата нам требуется 8 байтов, поэтому для описания поля sum следует использовать директиву dd.

Пример 1.1. Программа сложения 32-разрядных операндов.

.386

text segment 'code' use16

assume CS: text, DS: data

begin:

mov AX,data

mov DS,AX

;Основной фрагмент программы

mov EAX,12345678h ;Загрузим операнд в 32-раэрядный EАХ

add ЕАХ,87654321h ;Выполним сложение

 mov dword ptr sum, EАХ ;Запишем результат в поле sum

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

mov AX,4COOh

int 21h

text ends

data segment

sum dd 0

data ends

end begin

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

В строках 4 и 6 листинга используется команда засылки операнда в аккумулятор (B8h). Однако в строке 6 наличие перед кодом этой команды префикса замены размера операнда (код 66h) определяет, что длина операнда равна 32 бита, и, следовательно, используется регистр ЕАХ. Префикс замены размера операнда вставляется в объектный модуль транслятором автоматически, если в программе указано мнемоническое обозначение 32-разрядного регистра, например, ЕАХ.

Вторая программа, текст которой приведен в примере 10.2, показывает, как можно использовать 32-разрядный регистр-счетчик ЕСХ. Для того, чтобы организовать достаточно длительную программную задержку, в МП 8086 приходится использовать относительно сложную конструкцию с вложенными циклами. В МП 486 достаточно указать команде loop, чтобы она использовала 32-разрядный счетчик ЕСХ. В этом случае число повторов может достигать 0FFFFFFFFh.

Пример 1.2. Реализация задержки с помощью 32-разрядного регистра.

.386

text segment 'code' use16

assume CS:text,DS:data

begin: mov AX,data

mov DS,AX

;Основной фрагмент программы

mov CX, 3 ;Число повторов внешнего цикла

asdf: push CX ;Начало внешнего цикла

mov АН,40h ;Вывод сообщения

mov DX,offset message

mov CX, 3

int 21h

mov ЕАХ, 0F0000000h ;3acылкa числа шагов внутреннего

;цикла в регистр ЕСХ

qwer: db 67h ;Задание размера операнда для команды

 loop

loop qwer

pop CX

loop asdf ;Завершение внешнего цикла

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

mov AX,4C00h

int 21h

text ends

data segment

message db '<>'

data ends

stk segment stack 'stack'

db 256 dup ('^')

stk ends

end begin

Обратите внимание, что перед обращением к команде loop в текст программы включено определение байта данных - db 67h. Этим способом обеспечивается установка префикса замены размера адреса (код 67h) для следующей команды, которая теперь будет работать с регистром ЕСХ. Если префикс замены размера операнда отсутствует, команда loop работает с регистром CX. Здесь транслятор не может определить, операнд какого размера имеется в виду, и префикс приходится устанавливать в явном виде.

  1.  Расширенный набор команд 32-разрядного процессора

В систему команд процессора 486 включен ряд команд, выполнение которых не поддерживается процессорами 8086 - 80286. Команды общего назначения:

bound – проверка индекса массива относительно границ массива.

bsf/bsr – команды сканирования битов.

bt/btc/btr/bts – команды выполнения битовых операций.

enter – создание кадра стека для параметров процедур языка высокого уровня.

imul reg,imm – умножение операнда со знаком на непосредственное значение.

ins/outs – ввод/вывод из порта в строку.

j(cc) – команды условного перехода, допускающие 32–битовое смещение.

leave – выход из процедуры языка высокого уровня и восстановление регистров, записанных в стек командой enter.

Iss/lfs/igs – команды загрузки сегментных регистров.

mov DRx,reg; reg,DRx

mov CRn,reg; reg,CRn

mov TRx,reg; reg,TRx. – команды обмена данными со специальными регистрами. В качестве источника или приемника могут быть использованы регистры CR0...CR3, DR0...DR7, TR3...TR5.

movsx/movzx – знаковое/беззнаковое расширение до размера приемника и пересылка.

push imm – запись в стек непосредственного операнда размером байт, слово или двойное слово (например push 0FFFFFFFFh).

pusha – запись в стек всех 16–разрядных регистров общего назначения (AX, BX, CX, DX, SP, ВР, SI, DI).

рора – извлечение из стека всех 16–разрядных регистров общего назначения (АХ, BX, CX, DX, SP, ВР, SI, DI).

pushad – запись в стек всех 32–разрядных регистров общего назначения (ЕАХ, ЕВХ, ЕСХ, EDX, ESP, ЕВР, ESI, EDI).

popad – извлечение из стека всех 32–разрядных регистров общего назначения (ЕАХ, ЕВХ, ЕСХ, EDX, ESP, ЕВР, ESI, EDI).

rcr/rcl/ror/rol reg/mem,imm – циклический сдвиг на непосредственно заданное значение.

sar/sal/shr/shi reg/mem,imm – арифметический сдвиг на непосредственно заданное значение.

shrd/shid – установка байта по условию.

Команды защищенного режима:

arpl – корректировка поля RPL (уровня запрашиваемого приоритета) селектора.

clts – сброс флага переключения задач в регистре CR0.

lar – загрузка байта разрешения доступа.

lgdt – загрузка регистра таблицы глобальных дескрипторов.

lidt – загрузка регистра таблицы дескрипторов прерываний.

lldt – загрузка регистра таблицы локальных дескрипторов.

lmsw – загрузка слова состояния.

lsl – загрузка границы сегмента.

ltr – загрузка регистра задачи.

sgdt – сохранение регистра таблицы глобальных дескрипторов.

sidt – сохранение регистра таблицы дескрипторов прерываний.

sldt – сохранение регистра таблицы локальных дескрипторов.

smsw – сохранение слова состояния.

ssl – сохранение границы сегмента.

str – сохранение регистра задачи.

verr – проверка доступности сегмента для чтения на текущем уровне привилегия.

verw – проверка доступности сегмента для записи на текущем уровне привилегии.

Примеры использования некоторых из перечисленных выше команд.

Пример 10.3. Использование команд процессора 486.

.386

text segment 'code' use16

assume CS:text, DS:data

begin: mov AX,data

mov DS,AX

;основной фрагмент пограммы

mov ЕАХ,0FF000000h

mov EBX,64h

mov CL, 0Ah

shld EBX,EAX,CL ;Сдвиг содержимого ЕВХ влево на 0Ah бит и ;объединение его содержимого с содержимым ЕАХ. Результат будет в ЕВХ;

bsf AX,EBX ;Проверка разрядов в регистре EBX,

;начиная с младшего, и

;запись номера первого слева разряда, в котором находится 1, в регистр АХ

bsr DX, EBX ;Проверка разрядов в регистре ЕВХ, начиная со ;старшего, и запись номера первого справа разряда, в котором находится 1, в ;регистр DX 

mov dword ptr summ, EBX ; Сохранение суммы завершение

;программы

mov AX,4C00h

int 21h

text ends

data segment

sum dd 0

data ends

end begin

В программе выполняется умножение содержимого регистра ЕВХ на два в десятой степени. Для этого используется команда сдвига влево на 10 разрядов (0Ah). С помощью этой же команды обеспечивается прибавление к полученному результату числа 3FCh. Слагаемое предварительно записывается в регистр ЕАХ, содержимое которого при выполнении команды побитно перемещается в регистр ЕВХ, начиная со старшего разряда. Поскольку выполняется сдвиг на 10 разрядов, в регистре ЕАХ находится число 0FF000000h.

Следующие две команды вычисляют номера разрядов, в которых находится старший и младший отличные от нуля биты полученного числа. Старший бит определяется командой сканирования, начиная со старшего разряда – bsr, и помещается в регистр DX. Для определения младшего используется команда bsl.

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

  1.  
    Защищенный режим работы процессора

  1.  Перевод микропроцессора в защищенный режим

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

  •  увеличение адресуемого пространства до 16 Мбайт для МП 286 и до 4 Гбайт для МП 386 и 486;
  •  возможность работать в виртуальном адресном пространстве, превышающем максимально возможный объем физической памяти. Для МП 286 виртуальное пространство составляет 1 Гбайт, а для МП 386 и 486 – 64 Гбайт. Правда, для реализации виртуального режима необходимы, помимо дисков большой емкости, еще и соответствующая операционная система, которая хранит все сегменты выполняемых программ в большом дисковом пространстве, автоматически загружая в оперативную память те или иные сегменты по мере необходимости;
  •  организация многозадачного режима с параллельным выполнением нескольких программ (процессов). Многозадачный режим организует многозадачная операционная система, однако, микропроцессор предоставляет необходимый для этого режима мощный и надежный механизм защиты задач друг от друга с помощью четырехуровневой системы привилегий;
  •  страничная организация памяти, повышающая уровень защиты задач друг от друга и эффективность их выполнения.

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

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

Перед запуском программ защищенного режима следует выгрузить драйверы обслуживания расширенной памяти (типа ЕММ386.ЕХЕ) и оболочку Windows.

Пример 11.1. Переход в защищенный режим и обратно.

;В защищенном режиме вывод фиксированных символов на экран

.386Р ;(1)Разрешение трансляции всех, в том числе

; привилегированных команд МП 386 и 486

;Структура для описания дескрипторов сегментов

 descr struct ;(2)

limit dw 0 ;(3)

base_l dw 0 ;(4)

base_m db 0 ;(5)

attr_1 db 0 ;(6)

attr_2 db 0 ;(7)

base_h db 0 ;(8)

descr ends ;(9)

data segment ;(10)

;Таблица глобальных дескрипторов GDT

gdt_null descr <0,0,0,0,0,0> ;(11)

gdt_data descr <data_size-1,0,0,92h,0,0> ;(12)

gdt_code descr <data_size-1,0,0,98h,0,0> ;(13)

gdt_stack descr <255,0,0,92h,0,0> ;(14)

gdt_screen descr <4095,8000h,0bh,92h,0,0> ;(15)

gdt_size =$-gdt_null ;(16)

; Поля данных программы

pdescr dp 0 ;(17)

real_sp dw 1 ;(18)

sym db 1 ;(19)

attr db 1Eh ;(20)

mes db 27, '[31; 42m Вернулись в реальный режим! ' ,27,'[0m$' ;(21)

data_size=$-gdt_null ;(22)

data ends ;(23)

text segment 'code' use16 ;(24)Начало сегмента команд. Будем

;работать в 16-разрядиом режиме

assume CS: text, DS:data ;(25)

main proc ;(26)

mov AX,data ;(27)Инициализация реального

mov DS.AX ;(28)режима

;Вычислим 32-битовый линейный адрес сегмента данных и загрузим его

;в дескриптор сегмента данных в таблице GDT. В регистре АХ уже

;находится сегментный адрес. Умножим его на 16 сдвигом влево на 4

;бита с размещением результата в регистрах DL:AX

mov DL,0 ;(29) Очистим DL

shld DX,AX,4 ; (30) Сдвинем биты 12... 15 АХ в DL

shl AX, 4 ;(31)Сдвинем влево АХ на 4 бита

;Теперь a DL:AX 32-битовый линейный адрес сегмента данных

 mov BX,offset gdt_data ;(32) В ВХ адрес дескриптора

mov [ВХ].base_1,АХ ;(33) Загрузим младшую часть базы

mov [BX] .base_m,DL ;(34) Загрузим среднюю часть базы

;Вычислим 32-битовый линейный адрес сегмента команд и загрузим его

;в дескриптор сегмента команд в таблице глобальных дескрипторов

 mov AX,CS ;(35)АХ-адрес сегмента команд

mov DL, 0 ;(36)Та же процедура умножения

shld DX,AX,4 ;(37)сегментного адреса на 16

shl AX, 4 ;(38)сдвигом влево на 4 бита

mov BX,offset gdt_code ;(39)ВХ=адрес дескриптора

mov [ВХ].basе_1,АХ ;(40)3агруэка младшей

mov [ВХ].basе_m,DL ;(41)и средней частей базы

;Аналогично для адреса сегмента стека

mov AX,SS ;(42)

mov DL,0 ;(43)Та же процедура умножения

shld DX,AX,4 ;(44) сегментного адреса на 16

shl AX, 4 ;(45)сдвигом влево на 4 бита

mov ВХ,offset gdt_stack ;(46)ВХ=адрес дескриптора

mov [ВХ].base_l,AX ;(47)Загрузка младшей

mov [ВХ] .base_m,DL ;(48)и средней частей базы

;Подготовим псевдодескриптор pdescr и загрузим регистр GDTR

mov BX, offset gdt_data ;(49)Адрес GDT

mov АХ,[ВХ].base_1 ;(50)Получим и занесем в pdescr

mov word ptr pdescr+2,AX ;(51)базу, биты О...15

mov DL,[BX].base_m ;(52)Получим и занесем в pdescr 

mov byte ptr pdescr+4,DL ;(53)базу, биты 16...23

mov word ptr pdescr,gdt_size-l ;(54)Граница GDT

lgdt pword ptr pdescr ;(55) Загрузим регистр GDTR

; Подготовимся к возврату из защищенного режима в реальный

mov AX, 40h ;(5б)Настроим ES на область

mov ES,AX ;(57)данных BIOS 

mov word ptr ES:[67h],offset return ;(58)Смещение точки возврата

mov BS:[69h],CS ;(59)Сегмент точки возврата

;Подготовимся к переходу в защищенный режим

cli ;(60)Запрет аппаратных прерываний

mov AL,8Fh ;(61)Запрет NMI (80h) и выборка

;байта состояния отключения 0Fh 

out 70h,AL ;(62)Порт КМОП-микросхемы

jmp $+2 ;(63)Задержка

mov AL,0Ah ;(64)Установим режим восстановле-

out 71h,AL ; (б5) ния после сброса процессора

;Переходим в защищенный режим

smsw AX ;(66) Получим слово состояния машины

or АХ, 1 ; (67) Установим бит РЕ

lmsw АХ ; (68) Запишем назад слово состояния

;Теперь процессор работает в защищённом режиме

;Загружаем в CS:IP селектор:смещение точки continue 

;и заодно очищаем очередь команд

db 0EAh ;(69) Код команды far jmp

dw offset continue ;(70)Смещение

dw 16 ;(71)Селектор сегмента команд

continue: ; (72) ;Делаем адресуемыми данные

mov AX, 8 ;(73)Селектор сегмента данных

mov DS,AX ;(74) ;Делаем адресуемым стек

mov АХ,24 ;(75) Селектор сегмента стека

mov SS,AX ;(76)

;Инициализируем BS и выводим символы

mov AX,32 ;(77)Селектор сегмента видеобуфера

mov ES,AX ;(78)

mov BX,800 ;(79)Начальное смещение на экране

mov СХ,640 ;(80)Число выводимым символов

mov AX,word ptr sym ;(81) Начальный символ с атрибутом

screen:

mov ES:[BX],AX ;(82)Вывод в видеобуфер

add ВХ,2 ;(83)Сместимся в видеобуфере

inc АХ ;(84)Следующий символ

loop screen ;(85)Цикл вывода на экран

;Вернемся в реальный режим

mov real_sp,SP ;(86)Сохраним SP

mov AL,0FEh ;(87)Команда сброса процессора

out 64h,AL ;(88)в порт 64h

hit ;(89)Останов процессора до окончания сброса

;Теперь процессор снова работает в реальном режиме

;Восстановим операционную среду реального режима

return:

mov AX,data ;(90)Восстановим адресуемость

mov DS,AX ;(91) данных

mov SP,real_sp ;(92)Восстановим

mov АХ, stk ;(93)адресуемость

mov ss,АХ ;(94)стека

;Разрешим аппаратные и немаскируемые прерывания

sti ;(95)Разрешение прерываний

mov AL,0 ;(96)Сброс бита 7 в порте CMOS-

out 70h,AL ;(97)разрешение NMI

;Проверим выполнение функций DOS после возврата в реальный режим

mov AH,09h ;(98)Функция вывода на экран строки

mov DX,offset mes ;(99)Адрес строки

int 2lh ;(100)Вызов DOS

mov AX,4C00h ;(101)Завершим программу обычным

int 2lh ;(102)образом

main endp ;(103)Конец главной процедуру

code_size=$-main ;(104)Размер сегмента команд

text ends ;(105)Конец сегмента команд

stk segment stack 'stack' ;(106)Начало сегмента стека

db 256 dup ('^') ;(107)

stk ends ;(108)Конец сегмента стека

end main ;(109)Конец программы

  1.  Сегментная организация памяти в защищённом режиме

Микропроцессоры 80386 и 486 отличаются от предыдущих расширенным набором команд, часть которых относится к привилегированным. Для того чтобы разрешить транслятору обрабатывать эти команды, в текст программы необходимо включить директиву ассемблера .386Р.

Программа начинается с описания структуры дескриптора сегмента. В защищенном режиме для каждого сегмента программы (т.е. для сегментов команд, данных и стека) в программе должен быть определен дескриптор - 8-байтовое поле, в котором в определенном формате записываются базовый адрес сегмента, его длина и некоторые другие характеристики (рис. 11.1).

База

31…24

Атрибуты

2

Атрибуты

1

База сегмента

23…0

Граница сегмента

15…0

Рисунок 11.1 – Формат дескриптора сегмента

Теперь для обращения к требуемому сегменту программист заносит в сегментный регистр не сегментный адрес, а так называемый селектор (рис. 11.2), в состав которого входит номер (индекс) соответствующего сегменту дескриптора. Процессор по этому номеру находит нужный дескриптор, извлекает из него базовый адрес сегмента и, прибавляя к нему указанное в конкретной команде смещение (относительный адрес), формирует адрес ячейки памяти. Индекс дескриптора (0, 1, 2 и т.д.) записывается в селектор, начиная с бита 3, что эквивалентно умножению его на 8. Таким образом, можно считать, что селекторы последовательных дескрипторов представляют собой числа 0, 8, 16, 24 и т.д. (см. к предложениям 11...15).

15  3

2 0

Индекс дескриптора

Рисунок 11.2 – Селектор дескриптора

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

Граница (limit) сегмента представляет собой номер последнего байта сегмента. Так, для сегмента размером 375 байт граница равна 374. Поле границы состоит из 20 бит и разбито на две части. Как видно из рис. 11.1, младшие 16 бит границы занимают байты 0 и 1 дескриптора, а старшие 4 бита входят в байт атрибутов 2, занимая в нем биты 0...3. Получается, что размер сегмента ограничен величиной 1 Мбайт. На самом деле это не так. Граница может указываться либо в байтах (и тогда, действительно, максимальный размер сегмента равен 1 Мбайт), либо в блоках по 4 Кбайт (и тогда размер сегмента может достигать 4 Гбайт). В каких единицах задается граница, определяет самый старший бит байта 7 (атрибуты 2). Этот бит называется битом дробности (гранулярности). Если он равен 0, граница указывается в байтах; если 1 - в блоках по 4 Кбайт.

База сегмента (32 бита) определяет начальный линейный адрес сегмента в адресном пространстве процессора. Линейным называется адрес, выраженный не в виде комбинации сегмент:смещение, а просто номером байта в адресном пространстве. Казалось бы, линейный адрес – это просто другое название физического адреса. Для нашего примера это так и есть, в нем линейные адреса совпадают с физическими. Если, однако, в процессоре включен блок страничной организация памяти, то процедура преобразования адресов усложняется. Отдельные блоки размером 4 Кбайт (страницы) линейного адресного пространства могут произвольным образом отображаться на физические адреса, в частности и так, что большие линейные адреса отображаются на начало физической памяти и наоборот. Страничная адресация осуществляется аппаратно (хотя для ее включения требуются определенные программные усилия) и действует независимо от сегментной организации программы. Поэтому во всех программных структурах защищенного режима фигурируют не физические, а линейные адреса. Если страничная адресация выключена, эти линейные адреса совпадают с физическими, если включена - могут и не совпадать.

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

Поскольку в дескриптор записывается 32-битовый линейный базовый адрес (номер байта), сегмент в защищенном режиме может начинаться на любом байте, а не только на границе параграфа, и располагаться в любом месте адресного пространства 4 Гбайт.

Поле базы, как и поле границы, разбито на 2 части: биты 0...23 занимают байты 2, 3 и 4 дескриптора, а биты 24...31 - байт 7. Для удобства программного обращения в структуре descr база описывается тремя полями: младшим словом (base_l) и двумя байтами: средним (base_m) и старшим (base_h).

В байте атрибутов 1 задастся рад характеристик сегмента. Не вдаваясь пока в подробности этих характеристик, укажем, что в примере 2.1 используются сегменты двух типов: сегмент команд, для которого байт аttr_l должен иметь значение 98h, и сегмент данных (или стека) с кодом 92h.

Некоторые дополнительные характеристики сегмента указываются в старшем полубайте байта attr_2 (в частности, тип дробности). Для всех наших сегментов значение этого полубайта равно 0.

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

Помимо единственной таблицы глобальных дескрипторов (она часто называется GDT от Global Descriptor Table) в памяти может находиться множество таблиц локальных дескрипторов (LDT от Local Descriptor Table). Разница между ними в том, что сегменты, описываемые глобальными дескрипторами, доступны всем задачам, выполняемым процессором, а к сегментам, описываемым локальными дескрипторами, может обращаться только та задача, в которой эта дескрипторы описаны.

Поля дескрипторов для наглядности заполнены конкретными данными явным образом, хотя объявление структуры descr с нулями во всех полях позволяет описать дескрипторы несколько короче, например:

gdt_null descr <>;Селектор 0 – обязательный нулевой дескриптор

gdt_data descr <data_size-l,,,92h> ;Селектор 8 – сегмент данных

В дескрипторе gdt_data, описывающем сегмент данных программы, заполняется поле границы сегмента (фактическое значение размера сегмента data_size будет вычислено транслятором), а также байт атрибутов 1. Код 92h говорит о том, что это сегмент данных с разрешением записи и чтения. База сегмента, т.е. физический адрес его начала, в явной форме в программе отсутствует, поэтому ее придется программно вычислить и занести в дескриптор уже на этапе выполнения.

Дескриптор gdt_code сегмента команд заполняется схожим образом. Код атрибута 98h обозначает, что это исполняемый сегмент, к которому, между прочим, запрещено обращение с целью чтения или записи. Таким образом, сегменты команд в защищенном режиме нельзя модифицировать по ходу выполнения программы.

Дескриптор gdt_stack сегмента стека имеет, как и любой сегмент данных, код атрибута 92h, что разрешает его чтение и запись, и явным образом заданную границу – 255 байт, что соответствует размеру стека. Базовый адрес сегмента стека так же придется вычислить на этапе выполнения программы.

Последний дескриптор gdt_screen описывает страницу 0 видеобуфера. Размер видеостраницы, как известно, составляет 4096 байт, поэтому в поле границы указано число 4095. Базовый физический адрес страницы известен, он равен B8000h. Младшие 16 бит базы (число 8000h) заполняют слово base_l дескриптора, биты 16...19 (число 0Bh) – байт base_m. Биты 20...31 базового адреса равны 0, поскольку видеобуфер размещается в первом мегабайте адресного пространства.

Перед переходом в защищенный режим процессору надо будет сообщить физический адрес таблицы глобальных дескрипторов и её размер (точнее, границу). Размер GDT определяется на этапе трансляции в предложении 16.

Сегмент команд text начинается, как и всегда, оператором segment, в котором указывается тип использования use 16. Этот описатель объявляет, что в данном сегменте будут по умолчанию использоваться 16-битовые адреса и операнды. Наша программа будет запускаться под управлением DOS, которая работает в реальном режиме с 16-битовыми адресами и операндами. Указание описателя use 16 не запрещает использовать в программе 32-битовые регистры.

Фактически вся программа примера 11.1, кроме ее завершающих строк, а также фрагмента, выполняемого в защищенном режиме, посвящена подготовке перехода в защищенный режим. Прежде всего, надо завершить формирование дескрипторов сегментов программы, в которых остались незаполненными базовые адреса сегментов. Базовые (32-битовые) адреса определяются путем умножения значений сегментных адресов на 16. После обычной инициализации сегментного регистра DS (предложения 27-28), которая позволит нам обращаться к полям данных программы (в реальном режиме!) выполняется очистка регистра DL (предложение 29) и сдвиг старших 4 битов регистра АХ в регистр DX (фактически в 4 младших бита DL). Эта операция выполняется командой shld (shift left double, двойной сдвиг влево), входящей в систему команд процессоров, начиная с 80386. Команда сдвигает влево содержимое первого операнда (в нашем случае DX) на указанное константой (или содержимым регистра CL) число бит, причем младшие биты первого операнда заполняются старшими битами второго. Второй операнд, однако, не изменяется. Командой shl (предложение 31) содержимое регистра АХ также сдвигается влево на 4 бита. При этом старшие 4 бита теряются, однако они уже находятся в регистре DL. Следующими тремя командами (предложения 32...34) содержимое АХ отправляется в поле base_l дескриптора gdt_data, а содержимое DL - в поле base_m.

Аналогично вычисляются 32-битовые адреса сегментов команд и стека, помещаемые в дескрипторы gdt_code и gdt_stack.

Следующий этап подготовки к переходу в защищенный режим – загрузка в регистр процессора ODTR (Global Descriptor Table Register, регистр таблицы глобальных дескрипторов) информации о таблице глобальных дескрипторов. Эта информация включает в себя линейный базовый адрес таблицы и ее границу и размещается в 6 байтах поля данных, называемого иногда псевдодескриптором. Для загрузки GDTR предусмотрена специальная привилегированная команда lgdt (load global descriptor table, загрузка таблицы глобальных дескрипторов), которая требует указания в качестве операнда имени псевдодескриптора.

Заполнение псевдодескриптора упрощается вследствие того, что таблица глобальных дескрипторов расположена в начале сегмента данных, и ее базовый адрес совпадает с базовым адресом всего сегмента, который уже был вычислен и помещен в дескриптор gdt_data. В предложениях 49... 53 компоненты базового адреса переносятся из дескриптора в требуемые поля pdescr, а в предложении 54 заполняется поле границы. Команда lgdt загружает регистр GDTR и сообщает процессору о местонахождении и размере GDT.

Программу запускаем под управлением DOS, и естественно завершить ее также обычным образом, чтобы не нарушить работоспособность системы. Но в защищенном режиме запрещены любые обращения к функциям DOS или BIOS. Причина этого совершенно очевидна – и DOS, и BIOS являются программами реального режима, в которых широко используется сегментная адресация реального режима, т.е. загрузка в сегментные регистры сегментных адресов. В защищенном же режиме в сегментные регистры загружаются не сегментные адреса, а селекторы. Кроме того, обращение к функциям DOS и BIOS осуществляется с помощью команд программного прерывания int с определенными номерами, а в защищенном режиме эти команды приведут к совершенно иным результатам. Таким образом, программу, работающую в защищенном режиме, нельзя завершить средствами DOS. Сначала ее надо вернуть в реальный режим.

Возврат в реальный режим можно осуществить сбросом процессора. Действия процессора после сброса определяются одной из ячеек КМОП-микросхемы – байтом состояния отключения, располагаемым по адресу Fh. В частности, если в этом байте записан код Ah, после сброса управление немедленно передается по адресу, который извлекается из двухсловной ячейки 40h:67h, расположенной в области данных BIOS. Таким образом, для подготовки возврата в реальный режим необходимо в ячейку 40h:67h записать адрес возврата, а в байт Fh КМОП-микросхемы занести код Ah. В предложениях 56...59 полный адрес точки возврата return заносится по адресу 40h:67h. Точка возврата может располагаться в любом месте программы

Еще одна важная операция, которую необходимо выполнить перед переходом в защищенный режим, заключается в запрете всех аппаратных прерываний. В защищенном режиме процессор выполняет процедуру прерывания не так, как в реальном. При поступлении сигнала прерывания процессор не обращается к таблице векторов прерываний в первом килобайте памяти, как в реальном режиме, а извлекает адрес программы обработки прерывания из таблицы дескрипторов прерываний, построенной схоже с таблицей глобальных дескрипторов и располагаемой в программе пользователя (или в операционной системе). В примере 11.1 такой таблицы нет, и на время работы программы прерывания придется запретить. Запрет всех аппаратных прерываний осуществляется командой cli (предложение 60). Однако желательно запретить еще и немаскируемые прерывания, которые поступают в процессор по отдельной линии (вход NMI микропроцессора) и не управляются битом IF регистра флагов. Немаскируемые прерывания обычно используются для обработки таких катастрофических событий, как сбой питания, ошибка памяти или ошибка четности на магистрали; в реальном режиме для них зарезервирован вектор 02h. Для запрета немаскируемых прерываний не предусмотрено никаких команд, однако, это можно сделать окольным способом, установив старший бит в адресном порте 70h КМОП-микросхемы.

В предложениях 61-62 в порт 70h засылается код 8Fh, который выбирает для записи байт Fh КМОП – микросхемы и одновременно запрещает немаскируемые прерывания установкой старшего бита. После небольшой задержки, необходимой для срабатывания микросхемы, в порт данных 71h посылается код Ah, определяющий режим восстановления (переход по адресу, извлекаемому из области данных BIOS).

В предложениях 66...68 осуществляется перевод процессора в защищенный режим. В примере использованы команды smsw (store machine status word, запись слова состояния машины) и lmsw (load machine status word, загрузка слова состояния машины), которые можно использовать и в МП 286. Переход в защищенный режим осуществляется установкой в 1 бита 0 слова состояния машины. Поскольку остальные биты этого слова нам могут быть не известны, сначала мы читаем в регистр АХ слово состояния машины, затем устанавливаем в нем бит 0 и, наконец, записываем модифицированное слово состояния назад в процессор. Все последующие команды выполняются уже в защищенном режиме.

Хотя защищенный режим установлен, однако действия по настройке системы еще не закончены. Действительно, во всех используемых в программе сегментных регистрах хранятся не селекторы дескрипторов сегментов, а базовые сегментные адреса, не имеющие смысла в защищенном режиме. Между прочим, отсюда можно сделать вывод, что после перехода в защищенный режим программа не должна работать, так как в регистре CS пока еще нет селектора сегмента команд, и процессор не может обращаться к этому сегменту. В действительности это не совсем так.

В процессоре для каждого из сегментных регистров имеется так называемый теневой регистр дескриптора, который имеет формат дескриптора. Теневые регистры недоступны программисту; они автоматически загружаются процессором из таблицы дескрипторов каждый раз, когда процессор загружает соответствующий сегментный регистр. Таким образом, в защищенном режиме программист имеет дело с селекторами, т.е. номерами дескрипторов, а процессор – с самими дескрипторами, хранящимися в теневых регистрах. Именно содержимое теневого регистра (в первую очередь, линейный адрес сегмента) определяет область памяти, к которой обращается процессор при выполнении конкретной команды.

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

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

Загрузить селекторы в сегментные регистры DS, SS и ES не представляет труда (предложения 73...78). Но как загрузить селектор в программно недоступный регистр CS? Для этого можно воспользоваться искусственно сконструированной командой дальнего перехода, которая, как известно, приводит к смене содержимого и IP, и CS. Предложения 69... 71 демонстрируют эту методику. В реальном режиме необходимо было бы поместить во второе слово адреса в сегментный адрес сегмента команд, в защищенном же записываем в него селектор этого сегмента (число 16).

Команда дальнего перехода, помимо загрузки в CS селектора, выполняет еще одну функцию – она очищает очередь команд в блоке предвыборки команд процессора. Как известно, в современных процессорах с целью повышения скорости выполнения программы используется конвейерная обработка команд программы, позволяющая совместить во времени фазы их обработки. Одновременно с выполнением текущей (первой) команды осуществляется выборка операндов следующей (второй), дешифрация третьей и выборка из памяти четвертой команды. Таким образом, в момент перехода в защищенный режим уже могут быть расшифрованы несколько следующих команд и выбраны из памяти их операнды. Однако эти действия выполнялись, очевидно, по правилам реального, а не защищенного режима, что может привести к нарушениям в работе программы. Команда перехода очищает очередь предвыборки, заставляя процессор заполнить ее заново уже в защищенном режиме.

Следующий фрагмент примера (предложения 77...85) является чисто иллюстративным. В нем инициализируется (по правилам защищенного режима!) сегментный регистр ES и в видеобуфер экрана выводится некоторое количество цветных символов, чем подтверждается правильное функционирование программы в защищенном режиме.

Чтобы не нарушить работоспособность DOS, процессор следует вернуть в реальный режим, после чего можно будет завершить программу обычным образом. Сброс процессора выполняется засылкой команды FEh в порт 64h контроллера клавиатуры (предложения 87-88). Эта команда возбуждает сигнал на одном из выводов контроллера клавиатуры, который приводит к появлению сигнала сброса на выводе RESET микропроцессора. Перед выполнением сброса текущее содержимое SP сохраняется в ячейке rеal_sp, чтобы после перехода в реальный режим можно было восстановить состояние стека.

После сброса процессор начинает работать в реальном режиме, причем управление передается программам BIOS. BIOS анализирует содержимое байта состояния отключения (Fh) КМОП - микросхемы и, поскольку мы записали туда код Ah, осуществляет передачу управления по адресу, хранящемуся в ячейке 40h:67h области данных BIOS. В нашем случае переход осуществляется на метку return.

Команда hlt (halt, останов) позволяет организовать ожидание сброса процессора, который выполняется не мгновенно. Вместо команды hlt можно было использовать бесконечный цикл: stop: jmp stop

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

Передача управления на метку return осуществляется программами BIOS, которые, естественно, используют регистры процессора. В частности, регистры SS:SP и DS указывают на поля данных BIOS, и их следует инициализировать заново, что и выполняется в предложениях 90...94. Заметим, что сохранение и восстановление указателя стека SP не является обязательным, во всяком случае, в нашем примере, где работа программы до перехода в защищенный режим и после возврата из него протекает независимо. С таким же успехом можно после возврата в реальный режим инициализировать SP заново, выполнив команду mov SP,256 (в предположении, что стек имеет размер 256 байт).

Для восстановления работоспособности системы следует разрешить маскируемые (предложение 95) и немаскируемые прерывания (предложения 96-97), после чего программа может продолжаться уже в реальном режиме. В рассматриваемом примере для проверки работоспособности системы на экран выводится некоторый текст с помощью функции DOS 09h. Для наглядности в него включены Esc-последовательности смены цвета символов, поэтому программу следует выполнить при установленном драйвере ANSI.SYS.

Программа завершается обычным образом функцией DOS 4Ch. Нормальное завершение программы и переход в DOS тоже в какой-то мере свидетельствует о ее правильности.

  1.  Работа с расширенной памятью

  1.  Использование дескрипторов памяти

Дескрипторы могут быть трех типов: дескрипторы памяти, системные дескрипторы и шлюзы. Форматы дескрипторов памяти и системных практически совпадают, формат же шлюза несколько отличается.

При выделении программе сегментов большого следует иметь в виду особенность вычисления процессором значения границы. Если G=1, истинная граница сегмента определяется следующим образом:

Граница сегмента = граница в дескрипторе * 4К + 4095

Таким образом, сегмент всегда простирается до конца последнего 4К-байтного блока. Пусть, например, базовый адрес сегмента равен 0, граница тоже равна 0, и бит дробности установлен. Тогда истинная граница сегмента будет равна 0*4К+4095=4095, т.е. сегмент будет занимать диапазон адресов 0...4095 и иметь размер ровно 4 Кбайт. Если установить значение границы, равное 1, сегмент будет иметь размер ровно 8 Кбайт и т.д.

Атрибуты сегмента занимают два байта дескриптора.

Бит A (Accessed, было обращение) устанавливается процессором в тот момент, когда в какой-либо сегментный регистр загружается селектор данного сегмента. Анализируя биты обращения различных сегментов, программа (в частности, операционная система защищенного режима) может судить о том, было ли обращение к данному сегменту после того, как программа сбросила бит А.

Тип сегмента занимает 3 бита (иногда бит А включают в поле типа, и тогда тип занимает 4 бит) и может иметь 8 значений, перечисленных в табл. 12.1.

Как видно из табл. 12.1, тип определяет правила доступа к сегменту. Указав для некоторого сегмента данных тип 0, можно защитить его от модификации. При потолке записи в такой сегмент возникнет исключение общей защиты. Сегменту команд обычно присваивается тип 4, и тогда его можно только исполнять. Таким образом, в защищенном режиме не предусмотрено составление самомодифицирующихся программ.

Таблица 12.1 – Значения поля типа дескриптора сегмента памяти

Тип

Характеристики сегмента

0

Разрешено только чтение (сегмент данных)

1

Разрешены чтение и запись (сегмент данных)

2

Расширение вниз, разрешено только чтение (сегмент стека)

3

Расширение вниз, разрешены чтение и запись (сегмент стека)

4

Разрешено только исполнение (сегмент команд)

5

Разрешены исполнение и чтение (сегмент команд)

6

Разрешено только исполнение (подчиненный сегмент)

7

Разрешены исполнение и чтение (подчиненный сегмент)

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

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

Бит 4 байта атрибутов 1 является идентификатором сегмента. Если он равен 1, дескриптор описывает сегмент памяти. Значение этого бита 0 характеризует дескриптор системного сегмента.

Поле DPL (Descriptor Privilege Level, уровень привилегий дескриптора) служит для защиты программ друг от друга. Уровень привилегий может принимать значения от 0 (максимальные привилегии) до 3 (минимальные). Программам операционной системы обычно назначается уровень 0, прикладным программам - уровень 3, в результате чего исключается возможность некорректным программам разрушить операционную систему. В наших примерах операционная система защищенного режима отсутствует, в некоторой степени программа сама выполняет функции операционной системы, поэтому всем сегментам наших программ назначается наивысший (нулевой) уровень привилегий, что открывает доступ ко всем средствам защищенного режима.

Бит Р говорит о присутствии сегмента в памяти. В основном он используется в тех случаях, когда общий размер программы (или программ в случае многозадачного режима) превышает объем наличной памяти, и часть сегментов программ хранится на диске. Тогда операционная система защищенного режима с помощью этого бита определяет, находится ли требуемый сегмент в памяти, и при необходимости загружает его с диска. Перед выгрузкой ненужного сегмента на диск бит Р сбрасывается. Младшая половина байта атрибутов 2 занята старшими битами границы сегмента. Бит AVL (от Available, доступный) не используется и не анализируется процессором и предназначен для использования прикладными программами.

Бит D (Default, умолчание) определяет действующий по умолчанию размер для операндов и адресов. Он изменяет характеристики сегментов двух типов: исполняемых и стека. Если бит D сегмента команд равен 0, в сегменте по умолчанию используются 16-битовые адреса и операнды, если 1 - 32-битовые.

Атрибут сегмента, действующий по умолчанию, можно изменить на противоположный с помощью префиксов замены размера операнда (66h) и замены размера адреса (67h). Таким образом, для сегмента с D=0 префикс 66h перед некоторой командой заставляет ее рассматривать свои операнды, как 32-битовые, а для сегмента с D=1 тот же префикс 66h, наоборот, сделает операнды 16-битовыми. В некоторых случаях транслятор сам включает в объектный модуль необходимые префиксы, в других случаях их приходится вводить в программу "вручную".

Поскольку наши программы работают в реальном режиме под управлением MS-DOS, естественно устанавливать D=0. Это, однако, не препятствует использованию в программе 32-битовых регистров (ЕАХ, ЕСХ и т.д.). Если транслятор встречается с командой, в качестве операнда которой используется 32-разрядный регистр, он включает в код команды префикс замены размера операнда 66h. Поэтому команды с явным обращением к 32-битовым операндам транслируются правильно. В то же время при использовании команды iret в защищенном режиме префикс 66h приходится включать в программу в явном виде, чтобы процессор, выполняя эту команду, снял со стека не три слова, как обычно, а 6 слов (три двойных слова). Без префикса команда iret будет выполняться неправильно.

Префикс замены размера адреса 67h необходимо указывать, например, перед командой loop, если в качестве счетчика цикла используется не СХ, а ЕСХ. При отсутствии префикса команда loop (в сегменте, где по умолчанию используется 16-битовая адресация) будет выполнять цикл СХ, а не ЕСХ раз.

Сегменты стека, адресуемые через регистр SS, с помощью бита D определяют, какой регистр использовать в качестве указателя стека в командах push и pop. Если D=0, используется регистр SP, если D=l, то ESP.

Сегменты команд, предназначенные целиком для использования в защищенном режиме, транслируются с описателем размера адресов и операндов use32, а описывающие их дескрипторы должны иметь бит D=1.

  1.  Формат шлюзов таблицы IDT

Рассмотрим теперь формат дескрипторов, из которых строится таблица прерываний IDT, и которые носят название шлюзов. В таблицу дескрипторов прерываний могут входить шлюзы трех типов; ловушек, прерываний и задач. В рассмотренных ранее примерах таблица дескрипторов прерываний состояла из шлюзов прерываний и ловушек, описывающих обработчики аппаратных прерываний и исключений. Формат шлюза изображен на рис. 12.1.

Если основной частью содержимого сегмента памяти был его линейный адрес, то в шлюзе вместо линейного адреса указывается полный трехсловный адрес обработчика, действующий в защищенном режиме и состоящий из селектора и смещения. Смещение имеет 32 бита и занимает в дескрипторе два поля - байты 0-1 и 6-7. Селектор имеет 16 бит и занимает байты 2-3. Байт 4 в шлюзах ловушек, прерываний и задач не используется.

Рисунок 12.1 – Формат шлюзов, входящих в IDT

Байт атрибутов имеет такую же структуру, как и в дескрипторах памяти и включает тип, идентификатор дескриптора (бит 4), уровень привилегий дескриптора DPL и бит присутствия Р. Тип дескриптора может принимать 16 различных значений (табл. 12.2), хотя, как уже отмечалось, в IDT допустимо описывать только шлюзы задач, прерываний и ловушек.

Таблица 12.2 – Значения поля типа для системных дескрипторов и шлюзов

Тип

Назначение дескриптора

0

Не определено

1

Свободный сегмент состояния задачи (TSS) 80286

2

LDT

3

Занятый сегмент состояния задачи (TSS) 80286

4

Шлюз вызова 80286

5

Шлюз задачи

6

Шлюз прерываний 80286

7

Шлюз ловушки 80286

8

Не определено

9

Свободный сегмент состояния задачи (TSS) 80386 к 486

Ah

Не определено

Bh

Занятый сегмент состояния задачи (TSS) 80386 и 486

Ch

Шлюз вызова 80386 и 486

Dh

Не определено

Eh

Шлюз прерываний 80386 и 486

Fh

Шлюз ловушки 80386 и 486

  1.  Пример использования расширенной памяти 

В примере 12.1 в расширенной памяти создается сегмент размером 2 Мбайт, который заполняется натуральным рядом чисел (512К четырехбайтовых чисел) с визуальным контролем заполнения.

Пример 12.1 – Работа с расширенной памятью


IDEAL

P386

model small

stack 100h

Macro debug

push ax

push bx

push cx

push dx

push ax

and ax,0f000h

shr ax,12

mov bx,offset tblhex

xlat

mov [si],al

pop ax

push ax

and ax,0f00h

shr ax,8

inc si

xlat

mov [si],al

pop ax

push ax

and ax,0f0h

shr ax,4

inc si

xlat

mov [si],al

pop ax

push ax

and ax,0fh

inc si

xlat

mov [si],al

pop ax

pop dx

pop cx

pop bx

pop ax

endm

struc descr

limit dw 0

base_l dw 0

base_m db 0

attr_1 db 0

attr_2 db 0

base_h db 0

ends descr

struc trap

offs_l dw 0

sel dw 16

rsrv db 0

attr db 8fh

offs_h dw 0

ends trap

DataSeg

gdt_null descr <0,0,0,0,0,0>

gdt_data descr <data_size-1,0,0,92h,0,0>

gdt_code descr <code_size-1,0,0,98h,0,0>

gdt_stack descr <255,0,0,92h,0,0>

gdt_screen descr <4095,8000h,0bh,92h,0,0>

gdt_himem descr <511,0,10h,92h,80h,0>

gdt_size =$-gdt_null

idt trap 10 dup (<dummy_exc>)

trap <exc_0a>

trap <exc_0b>

trap <exc_0c>

trap <exc_0d>

trap <exc_0e>

trap 17 dup (<dummy_exc>)

idt_size = $ - idt

pdescr dp 0

mes db 10,13,' Real mode',"$",10,13,0

TblHex db '0123456789ABCDEF'

string db '**** **** **** **** ****'

len = $ -string

number db "???? ????"

Home_sel dw home

dw 10h

data_size=$-gdt_null

ends

CodeSeg

assume CS: @Code, DS:@data;

sttt equ $

proc dummy_exc

pop eax

pop eax

mov si,offset string+5

debug

mov ax,1111h

jmp [Dword ptr Home_sel]

endp

proc exc_0a

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0ah

jmp [Dword ptr Home_sel]

endp

proc exc_0b

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0bh

jmp [Dword ptr Home_sel]

endp

proc exc_0c

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0ch

jmp [Dword ptr Home_sel]

endp

proc exc_0d

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0dh

jmp [Dword ptr Home_sel]

endp

proc exc_0e

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0eh

jmp [Dword ptr Home_sel]

endp

Start:

xor eax,eax

mov ax,@data

mov DS,ax

shl eax, 4

mov ebp,eax

mov BX,offset gdt_data

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

Xor eax,eax

mov ax,cs

shl eax, 4

mov BX,offset gdt_code

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

Xor eax,eax

mov ax,ss

shl eax, 4

mov BX,offset gdt_stack

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

mov [Dword Ptr Pdescr+2],ebp

mov [word Ptr Pdescr],gdt_size-1

lgdt [pword ptr Pdescr]

cli

mov al,8fh

out 70h,al

jmp $+2

mov al,0ah

out 71h,al

mov ax,40h

mov es,ax

mov [word ptr es:67h],offset return

mov [word ptr es:69h],cs

mov [word Ptr Pdescr],idt_size-1

xor eax,eax

mov ax,offset idt

add eax,ebp

mov [Dword Ptr Pdescr+2],ebp

lidt [pword ptr Pdescr]

mov al,0d1h

out 64h,al

mov al,0dfh

out 60h,al

mov eax,CR0

or eax,1

mov CR0,eax

db 0eah

dw offset continue,16

continue:

mov ax,8

mov ds,ax

mov ax,24

mov ss,ax

mov ax,32

mov es,ax

mov ax,40

mov gs,ax

mov eax,0

mov ebx,0

mov ecx,80000h

fill:

mov [dword ptr gs:ebx],eax

push eax

push cx

mov si,offset number+5

debug

shr eax,16

mov si,offset number

debug

mov si,offset number

mov cx,9

mov ah,43h

mov di,1040

scrh:

lodsb

stosw

loop scrh

pop cx

pop eax

add ebx,4

inc eax

db 67h

loop fill_1

jmp go

fill_1:

jmp fill

go:

mov ebx,0

mov eax,[dword ptr gs:ebx]

mov si,offset string+15

debug

shr eax,16

mov si,offset string+10

debug

mov ebx,2ffffch

mov eax,[dword ptr gs:ebx]

mov si,offset string+25

debug

shr eax,16

mov si,offset string+20

debug

mov ax,0ffffh

home:

mov si , offset string

debug

mov si,offset string

mov cx,len

mov ah,74h

mov di,1600

scr:

lodsb

stosw

loop scr

mov al,0d1h

out 64h,al

mov al,0ddh

out 60h,al

mov al,0feh

out 64h,al

hlt

return:

mov ax,@data

mov ds,ax

mov ax,@stack

mov ss,ax

mov sp,100h

sti

mov al,0

out 70h,al

mov ah,09h

mov dx,offset mes

int 21h

mov AX,4C00h

int 21h

ends

code_size=$-sttt

end Start

end


Желая образовать, дополнительный сегмент объемом 2 Мбайт в расширенной памяти, необходимо предусмотреть описывающий его дескриптор в таблице глобальных дескрипторов:

gdt_himem descr <511, 0,10h, 92h, 80h, > ; Селектор 40

Будучи расположен в таблице GDT на шестом месте, этот дескриптор получает индекс 5, что соответствует селектору 40.

Линейный адрес первого байта расширенной памяти равен 100000h. Из этого адреса младшие 16 бит (0000h) записываются в поле base_l, следующие 8 бит, содержащие 10h, - в поле base_m, и старшие 8 бит (00h) - в поле base_h.

Поскольку размер сегмента превышает 1 Мбайт, его границу следует указывать в блоках по 4 Кбайт. Поэтому мы устанавливаем в 1 бит дробности G в байте атрибутов 2, который, таким образом, принимает значение 80h. Размер сегмента (2 Мбайт) составляет 512 блоков по 4 Кбайт, поэтому значение границы составляет 511. Байт атрибутов 1, как и для других сегментов памяти, принимает значение 92h (для чтения и записи, присутствует).

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

Перед переходом в защищенный режим (или после перехода в него) следует открыть линию А20, т.е. адресную линию, на которой устанавливается единичный уровень сигнала, если происходит обращение к мегабайтам адресного пространства с номерами 1, 3, 5 и т.д. (первый мегабайт имеет номер 0). В реальном режиме линия А20 заблокирована, и если значение адреса выходит за пределы FFFFFh, выполняется его циклическое оборачивание (линейный адрес 100000h превращается в 00000h, адрес 100001h – в 00001h и т.д.). Открытие (разблокирование) линии А20 выключает механизм циклического оборачивания адреса, что позволяет адресоваться к расширенной памяти. Управление блокированием линии А20 осуществляется через порт 64h, куда сначала следует послать команду D1h управления линией А20, а затем - код открытия (DFh). Эти действия выполняются в предложениях 34...37.

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

Заполнение расширенной памяти начинается с предложения 40. В свободный сегментный регистр GS заносится селектор сегмента в расширенной памяти и инициализируются регистры ЕАХ, EBX и ЕСХ (число шагов в цикле превышает 64 К, поэтому требуется 32-битовый регистр ЕСХ). В предложении 53 число-заполнитель отправляется в расширенную память. Для динамического контроля хода заполнения памяти в каждом шаге цикла очередное число выводится на экран. Далее выполняется смещение индекса в указателе, инкремент числа-заполнителя и возврат в начало цикла.

Для того чтобы команда loop использовала в своей работе регистр ЕСХ, а не СХ (вспомним, что мы установили для сегмента команд D=0 и назначили тем самым по умолчанию 16-битовые адреса и операнды), перед ней в программу включен код префикса замены размера адреса. Другая сложность возникла из-за того, что в цикле оказалось больше 128 байтов, а команда loop может осуществлять только короткие переходы в диапазоне +127...-128 байтов. Поэтому командой loop выполняется переход не на начало цикла, а на вспомогательную строку fill_l, откуда командой безусловного перехода управление может быть передано уже в любую точку программы.

Команда безусловного перехода jmp следующее предложение после завершения цикла.

В программе предусмотрено контрольное чтение расширенной памяти и вывод в диагностическую строку string прочитанных чисел.

Перед переходом в реальный режим следует закрыть линию А20, для чего в порт 64h посылается команда D1h управления линией А20, а затем – код закрытия линии (DDh).

  1.  
    Переключение задач в защищённом режиме

  1.  Понятие задачи

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

Под задачей понимают выполняемую на компьютере программу или ее фрагмент, имеющий относительно самостоятельное значение. В частности, задачей можно назвать всю сколь угодно сложную программу, и тогда мультизадачность обозначает параллельное выполнение нескольких, возможно не связанных друг с другом программ. Однако задачей может быть и достаточно самостоятельная часть программы. Например, обработчики аппаратных прерываний уместно назвать задачами: для них как раз характерно выполнение параллельно и независимо от основной части программы. Вообще ничто не мешает включить в состав одной программы несколько самостоятельных участков, переключение между которыми может осуществляться периодически или по каким-то событиям: нажатиям определенных клавиш, прерываниям от диска и т.д. Эти фрагменты могут образовывать отдельные сегменты команд, но могут входить в состав единого программного сегмента в виде процедур.

  1.  Организация мультизадачного режима

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

  •  сегмент состояния задачи (Task State Segment, TSS);
  •  дескриптор сегмента состояния задачи;
  •  регистр задачи (Task Register, TR);
  •  дескриптор шлюза задачи (Task gate).

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

В отличие от обычных сегментов данных, TSS описывается не дескриптором сегмента памяти, а системным дескриптором, который к тому же может находиться только в таблице глобальных дескрипторов. Системные дескрипторы имеют тот же формат, что и дескрипторы памяти, отличаясь только кодом типа. Для процессоров 386 и 486 дескрипторы TSS имеют код 9. На рис. 13.1 приведен для формат дескриптора TSS для процессора 486.

Рисунок 13.1 – Формат системного дескриптора, описывающего TSS для МП 386 и 486

В зависимости от геометрического места расположения дескриптора TSS в таблице дескрипторов, ему соответствует тот или иной селектор. Селектор TSS текущей (активной) задачи должен быть загружен в регистр задачи TR. Для исходной, главной задачи эта загрузка осуществляется программно с помощью предназначенной для этого команды ltr (load task register, загрузка регистра задач); при переключении на новую задачу программа передает процессору селектор нового TSS и перезагрузку регистра TR осуществляет процессор в ходе переключения задач.

Переключение на новую задачу осуществляется командой дальнего вызова call dword ptr или, в некоторых случаях, дальнего перехода jmp dword ptr. В качестве аргумента этих команд указывается двухсловное поле, в первом слове которого записывается селектор требуемого TSS. Существует и другой способ переключения – не через селектор TSS, а через шлюз задачи (дескриптор шлюза с кодом типа, равным 5). В этом случае селектор требуемого TSS указывается в поле для селектора в шлюзе задачи. Переключение через шлюз задачи имеет то преимущество, что его можно выполнить по аппаратному прерыванию, так как, в отличие от дескриптора сегмента состояния задачи TSS, шлюз задачи можно расположить в таблице дескрипторов прерываний.

В процессе переключения на новую задачу процессор сохраняет контекст текущей задачи в ее TSS и загружает новый контекст (включая селектор и относительный адрес точки входа в задачу) из TSS новой задачи.

Задача, на которую осуществляется переключение, должна в этом случае завершаться командой iret, которая обрабатывается процессором совсем не так, как iret обычного обработчика прерывания. В случае переключения задач процессор по команде iret выполняет обратное перемещение контекстов - контекст завершающейся задачи (на момент ее завершения) сохраняется в се TSS, а в регистры процессора из TSS исходной задачи загружается сохраненный там контекст, соответствующий моменту переключения на вторую задачу. Таким образом, TSS можно рассматривать, как функциональный аналог стека, который тоже используется для сохранения содержимого регистров и другой информации в обработчиках прерываний и подпрограммах. Однако сохранение в стеке и восстановление из него выполняются с помощью последовательностей команд процессора, а сохранение и восстановление контекстов с помощью TSS осуществляется процессором аппаратно в процессе переключения задач.

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

Рисунок 13.2 – Формат сегмента состояния задачи для МП 386 и 486

Сегмент состояния задачи имеет размер минимум 104 байт. В самом начале TSS предусмотрено 16-битовое поле связи, используемое при переключении задач. Для исходной, главной задачи (task_0) его содержимое не имеет значения. При переключении на новую задачу (task_l) процессор заносит в поле связи TSS новой задачи селектор TSS исходной задачи, чем создается связь между новой и старой задачами. Если новая задача, в свою очередь, переключается на следующую задачу (task_2), в поле связи TSS этой следующей задачи процессор заносит селектор TSS предыдущей задачи и т.д. В результате создается связный список вложенных задач (рис. 13.3).

Рисунок 13.3 – Цепочка связанных TSS

Команда iret, которой должна завершаться каждая вложенная задача, выполняет обратное переключение задач. В процессе этой операции процессор извлекает из поля связи TSS последней вложенной задачи селектор TSS предыдущей по порядку вложенности задачи и передаст ей управление, восстановив перед этим из TSS этой задачи ее контекст.

По адресам 04h, 0Сh и 14h относительно базы TSS располагаются кадры стеков уровней привилегий 0, 1 и 2. Содержимое этих полей загружается в регистры SS и ESP, если при переключении задачи происходит смена уровня привилегий. Задачи примеров будут работать на одном (нулевом) уровне привилегий; в этом случае поля кадров стеков не используются, а регистры SS и ESP инициализируются содержимым ячеек TSS с адресами 50h и 38h.

Регистр управления CR3 (поле TSS с адресом 1Ch) содержит базовый физический адрес каталога страниц и используется, если включено страничное преобразование. Наличие в TSS поля для регистра CR3 позволяет иметь для каждой задачи свой каталог страниц и, соответственно, свои таблицы отображения виртуальных страниц программы на физические адреса памяти. В примерах страничное преобразование выключено, и регистр CR3 не используется.

Двухсловное поле TSS с адресом 20h предназначено для хранения значения указателя команд ЕIP. В TSS исходной задачи это поле при переключении на новую задачу заполняется процессором адресом команды, следующей за командой переключения, т.е. адресом возврата. В TSS задачи, на которую планируется осуществить переключение, поле для EIP должно быть заполнено программно смещением точки входа в задачу. При этом следует иметь в виду, что при возврате в старую задачу командой iret процессор записывает в поле для EIP завершившейся задачи в качестве "адреса возврата" адрес команды, следующей iret, т.е. адрес уже за пределами задачи. Если планируется повторное переключение на эту задачу, перед каждым переключением необходимо восстанавливать в TSS этой задачи адрес се точки входа.

Сохранение в TSS исходной задачи текущего содержимого регистра флагов EFLAGS (по адресу 24h) позволяет осуществлять переключение в любой точке задачи без потери ее работоспособности.

Участок TSS с адресами 28h...47h отведен для хранения содержимого регистров общего назначения. При переключении с исходной задачи на новую задачу процессор сохраняет в этих полях TSS исходной задачи текущее содержимое регистров, а при обратном переключении командой iret восстанавливает регистры из этих полей TSS исходной задачи, обеспечивая правильное продолжение ее выполнения. Что же касается TSS новой задачи, то, заполнив заранее поля регистров в TSS новой задачи, можно передать ей исходные параметры.

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

Если новая задача использует таблицу локальных дескрипторов, ее селектор следует занести в TSS по адресу 60h.

Бит 0 слова по адресу 64h используется для отладки переключаемых задач. Если в TSS новой задачи установлен этот бит, то сразу же после переключения генерируется исключение отладки с номером 1. Остальные биты слова по адресу 64h должны быть равны 0.

Слово с адресом 66h содержит смещение битовой карты ввода-вывода, которая, при ее наличии, располагается в TSS по последующим адресам и используется для защиты портов компьютера от несанкционированного доступа. Каждый бит этой карты соответствует одному порту (вся карта, таким образом, может достигать 64 Кбит, или 8 Кбайт). Если бит, закрепленный за некоторым портом, равен 0, задача любого уровня привилегий может обращаться к этому порту. Если бит равен 1, то при обращении к порту задачей с недостаточно высоким уровнем привилегий генерируется исключение общей защиты.

При переключение на новую задачу команда iret должна инициировать довольно сложную процедуру обратного переключения через селектор TSS, хранящийся в поле связи TSS текущей задачи. Однако в обработчиках прерываний и исключений та же команда iret выполняется иначе: она просто снимает со стека три 32-битовых слова (EFLAGS, CS и EIP) и загружает их в соответствующие регистры, обеспечивая возврат из обработчика в прерванную задачу.

Режим выполнения команды iret определяется состоянием специального флага NT (Nested Task, вложенная задача), расположенного в бите 14 регистра флагов EFLAGS. Команда iret анализирует состояние флага NT и, если он сброшен, осуществляет обычный возврат из программы обработки прерывания (через стек); если же флаг NT установлен, команда iret инициирует обратное переключение задач через селектор в TSS.

После загрузки компьютера флаг NT находится в установленном состоянии. Однако любое аппаратное прерывание или исключение сбрасывает этот флаг, в результате чего команда iret, завершающая обработчик, выполняется в "облегченном" варианте. То же происходит при выполнении процессором команды программного прерывания int. Поскольку команда iret восстанавливает исходное состояние регистра флагов, после завершения обработчика флаг NT снова оказывается установленным (если, конечно, он не был явным образом сброшен выполняемой программой).

При выполнении процедуры переключения на новую задачу через шлюз задачи или непосредственно через TSS, процессор сохраняет в TSS текущей задачи слово флагов и устанавливает в регистре флагов бит NT (независимо от того, был ли он перед этим сброшен или установлен). Команда iret, завершающая задачу, обнаруживает NT=1 и, вместо осуществления возврата через стек, инициирует механизм обратного переключения задач.

Если вложенная задача, в свою очередь, выполняет переключение на следующую задачу, текущее слово флагов с установленным битом NT сохраняется в TSS текущей задачи. После завершения новой задачи это слово будет возвращено в регистр флагов и, таким образом, задача будет продолжаться с NT=1, что обеспечит её правильное завершение.

Выделим строки заполнения расширенной памяти в отдельную процедуру и оформим ее в качестве задачи. Выполним в главной программе переключение на эту задачу и возврат из нее.

Пример 13.1 – Техника переключения задач


IDEAL

P386

model small

stack 100h

Macro debug

push ax

push bx

push cx

push dx

push ax

and ax,0f000h

shr ax,12

mov bx,offset tblhex

xlat

mov [si],al

pop ax

push ax

and ax,0f00h

shr ax,8

inc si

xlat

mov [si],al

pop ax

push ax

and ax,0f0h

shr ax,4

inc si

xlat

mov [si],al

pop ax

push ax

and ax,0fh

inc si

xlat

mov [si],al

pop ax

pop dx

pop cx

pop bx

pop ax

endm

struc descr

limit dw 0

base_l dw 0

base_m db 0

attr_1 db 0

attr_2 db 0

base_h db 0

ends descr

struc trap

offs_l dw 0

sel dw 16

rsrv db 0

attr db 8fh

offs_h dw 0

ends trap

DataSeg

gdt_null descr <0,0,0,0,0,0>

gdt_data descr <data_size-1,0,0,92h,0,0>

gdt_code descr <code_size-1,0,0,98h,0,0>

gdt_stack descr <255,0,0,92h,0,0>

gdt_screen descr <4095,8000h,0bh,92h,0,0>

gdt_himem descr <511,0,10h,92h,80h,0>

gdt_tss_0 descr <103,0,0,89h,0,0>

gdt_tss_1 descr <103,0,0,89h,0,0>

gdt_size =$-gdt_null

idt trap 10 dup (<dummy_exc>)

trap <exc_0a>

trap <exc_0b>

trap <exc_0c>

trap <exc_0d>

trap <exc_0e>

trap 17 dup (<dummy_exc>)

idt_size = $ - idt

pdescr dp 0

real_sp dw 0

real_ss dw 0

mes db 10,13,' Real mode',"$",10,13,0

TblHex db '0123456789ABCDEF'

string db '**** **** **** **** **** ****'

len = $ -string

number db "???? ????"

Home_sel dw home

dw 10h

tss_0 db 104 dup(0)

tss_1 db 104 dup(0)

task1_offs dw 0

task1_sel dw 56

data_size=$-gdt_null

ends

CodeSeg

assume CS: @Code, DS:@data;

sttt equ $

proc dummy_exc

pop eax

pop eax

mov si,offset string+5

debug

mov ax,1111h

jmp [Dword ptr Home_sel]

endp

proc exc_0a

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0ah

jmp [Dword ptr Home_sel]

endp

proc exc_0b

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0bh

jmp [Dword ptr Home_sel]

endp

proc exc_0c

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0ch

jmp [Dword ptr Home_sel]

endp

proc exc_0d

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0dh

jmp [Dword ptr Home_sel]

endp

proc exc_0e

pop eax

pop eax

mov si,offset string+5

debug

mov ax,0eh

jmp [Dword ptr Home_sel]

endp

Start:

xor eax,eax

mov ax,@data

mov DS,ax

shl eax, 4

mov ebp,eax

mov BX,offset gdt_data

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

Xor eax,eax

mov ax,cs

shl eax, 4

mov BX,offset gdt_code

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

Xor eax,eax

mov ax,ss

shl eax, 4

mov BX,offset gdt_stack

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

mov eax,ebp

add ax,offset tss_0

mov BX,offset gdt_tss_0

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

mov eax,ebp

add ax,offset tss_1

mov BX,offset gdt_tss_1

mov [(descr ptr bx).base_l],ax

rol eax,16

mov [(descr ptr bx).base_m],al

mov [Dword Ptr Pdescr+2],ebp

mov [word Ptr Pdescr],gdt_size-1

lgdt [pword ptr Pdescr]

mov [word ptr tss_1+4ch],16

mov [word ptr tss_1+20h],offset highmem

mov [word ptr tss_1+50h],24

mov [word ptr tss_1+38h],128

mov [word ptr tss_1+54h],8

mov [word ptr tss_1+48h],32

cli

mov al,8fh

out 70h,al

jmp $+2

mov al,0ah

out 71h,al

mov ax,40h

mov es,ax

mov [word ptr es:67h],offset return

mov [word ptr es:69h],cs

mov [word Ptr Pdescr],idt_size-1

xor eax,eax

mov ax,offset idt

add eax,ebp

mov [Dword Ptr Pdescr+2],ebp

lidt [pword ptr Pdescr]

mov al,0d1h

out 64h,al

mov al,0dfh

out 60h,al

mov eax,CR0

or eax,1

mov CR0,eax

db 0eah

dw offset continue,16

continue:

mov ax,8

mov ds,ax

mov ax,24

mov ss,ax

mov ax,32

mov es,ax

mov ax,40

mov gs,ax

mov ax,48

ltr ax

call [dword ptr task1_offs]

mov [word ptr tss_1+20h],offset highmem

call [dword ptr task1_offs]

mov ax,0ffffh

home:

mov si , offset string

debug

mov si,offset string

mov cx,len

mov ah,74h

mov di,1600

scr:

lodsb

stosw

loop scr

mov al,0d1h

out 64h,al

mov al,0ddh

out 60h,al

mov al,0feh

out 64h,al

hlt

return:

mov ax,@data

mov ds,ax

mov ax,@stack

mov ss,ax

mov sp,100h

sti

mov al,0

out 70h,al

mov ah,09h

mov dx,offset mes

int 21h

mov AX,4C00h

int 21h

proc highmem

mov ax,40

mov gs,ax

xor eax,eax

xor ebx,ebx

mov ecx,80000h

fill:

mov [dword ptr gs:ebx],eax

push eax

push cx

mov si,offset number+5

debug

shr eax,16

mov si,offset number

debug

mov si,offset number

mov cx,9

mov ah,43h

mov di,1040

scrh:

lodsb

stosw

loop scrh

pop cx

pop eax

add ebx,4

inc eax

db 67h

loop fill_1

jmp go

fill_1:

jmp fill

go:

iret

endp

ends

code_size=$-sttt

end Start

end


Таблица глобальных дескрипторов дополнена двумя новыми дескрипторами gdt_tss_0 и gdt_tss_l. Это дескрипторы сегментов состояния задач TSS. В нашем примере используются TSS минимального размера – по 104 байта, поэтому в поле границы дескрипторов TSS указано число 103. Атрибут 1 дескрипторов имеет значение 89h: присутствующий, уровень привилегий DPL=0, свободный TSS МП 486 (рис.13.4).

Рисунок 13.4 – Расшифровка значения атрибута для дескриптора TSS

Вспомним, что дескриптор TSS может быть четырех типов: свободный TSS МП 286, занятый TSS МП 286, свободный TSS МП 386/486 и занятый TSS МП 386/486. Описывая сегменты состояния задач в таблице дескрипторов, с помощью кода типа указывают, что они свободны. Как только селектор дескриптора TSS будет загружен в регистр задачи TR, процессор изменяет код атрибута дескриптора этого TSS, объявляя его занятым. При этом TSS исходной задачи остается занятым до ее завершения, даже если выполняются переключения на другие задачи. TSS вложенной задачи помечается процессором, как занятый, как только произойдет переключение на эту задачу, а после ее завершения и возврата в исходную задачу TSS вложенной задачи снова освобождается. Попытка переключения на задачу, TSS который занят, приводит к исключению нарушения общей защиты. Тем самым предотвращается повторный запуск активной задачи.

В сегменте данных главной задачи зарезервировано место для двух сегментов состояния задач. Там же предусмотрено двухсловное поле адреса для команды call dword ptr переключения на вложенную задачу. Поскольку переключение осуществляется через TSS задачи, в качестве сегментного адреса задачи указывается селектор ее TSS (в данном случае 56). Слово со смещением при переключении задач игнорируется, хотя должно присутствовать в программе согласно формату команды call.

Перед переключением на задачу 1 следует инициализировать некоторые поля TSS_1 (TSS_0 будет заполнен процессором при первом возврате из вложенной задачи в исходную). В данном примере инициализация TSS_1 выполняется еще в реальном режиме, хотя эту операцию можно было бы перенести в защищенный режим. Поля для CS и IP заполняются селектором сегмента команд задачи 1 (в данном случае это общий для обеих задач сегмент с селектором 16) и смещением highmem точки входа в задачу 1. Для стека задачи 1 произвольно выделена область, начиная с середины нашего сегмента стека. Поскольку задача 1 будет использовать общий сегмент данных, и обращаться к видеобуферу, ей передаются селекторы этих сегментов.

Далее следуют уже рассмотренные ранее операции запрета прерываний, подготовки адреса возврата в реальный режим, загрузки регистра IDTR, открытия линии А20 и перехода в защищенный режим. После перехода в защищенный режим и выполнения обычных процедур инициализации сегментных регистров в регистр задачи TR загружается селектор TSS исходной задачи. Переключение на вложенную задачу можно будет выполнить и без этого, однако загрузка TR обеспечит возврат из вложенной задачи в исходную.

Наконец, командой дальнего косвенного вызова осуществляется переключение на задачу 1, в качестве которой выступает процедура highnicm, размещенная в самом конце сегмента команд.

Задача 1 заполняет расширенную память последовательными числами, выводя их одновременно на экран, что позволяет наглядно контролировать ее работу. Завершающая команда iret этой задачи осуществляет обратное переключение, передавая управление на очередную команду задачи 0, следующую за командой дальнего вызова. Здесь продемонстрирована техника повторного переключения на задачу 1: в поле TSS для указателя команд восстанавливается начальный адрес процедуры highmem и снова выполняется команда дальнего вызова. Продолжение исходной задачи не отличается от предыдущего примера.

Отладочная макрокоманда debug, используемая в программе, дает возможность детально изучить процесс переключения задач и участие в этом процессе различных системных структур. Можно выполнить анализ следующих объектов.

1. Содержимое области связи в TSS_1 до переключения на задачу 1 и после этого переключения. До переключения эта область пуста, а после переключения в ней должен быть записан селектор TSS_0 (48=30h)

2. Значение кода атрибута в дескрипторах сегментов состояния задач. До активизации задачи код должен быть равен 89h, после активизации - 8Bh, поскольку сегмент становится занятым.

3. Состояние регистра флагов и, в частности, флага NT, до переключения на вложенную задачу, "внутри нее" и после возврата в исходную задачу. Вывести на экран содержимое регистра флагов (нас будет интересовать только его младшее слово) можно следующим образом:

pushf  ;Отправим флаги в стек

pop AX ;Извлечем флаги из стека в АХ

mov SI,offset string+10 ;Преобразуем в символьную

debug  ;форму и выведем на экран

Этот эксперимент может оказаться не очень наглядным, поскольку, как уже отмечалось, в исходном состоянии компьютера флаг NT установлен. Естественно, он останется установленным и после переключения. Для того, чтобы детально изучить роль флага NT, следует сбросить его перед переключением на вложенную задачу. Это можно осуществить командами

mov АХ, 0 ; Обнулим АХ

push АХ ;Отправим 0 в стек

popf ;Перенесем его в регистр флагов

(то, что сбрасываются и остальные флаги, роли не играет). Теперь анализ регистра флагов окажется более поучительным: флаг NT будет сброшен при выполнении задачи 0, но установлен при выполнении задачи 1. После возврата в задачу 0 флаг снова сбросится, так как именно такое состояние регистра флагов будет храниться в TSS_0.

Чтобы проанализировать процесс сохранения и восстановления содержимого регистра флагов, можно после очистки регистра флагов принудительно установить в нем какой-либо флаг, например CF (что можно выполнить командой stc) и вывести на экран, кроме слова флагов, еще и содержимое ячеек TSS_0 и TSS_1 со смещением 24h.