Продавай мобильный контент со своего сайта

Учебник по основам языка Ассемблера

Содержание

  • 1. Введение
  • 2. Основы программирования
  • 2.1. Подготовка и отладка программы
  • 2.2. Представление данных
  • 2.3. Описание данных
  • 2.4. Структуры и записи
  • 2.5. Способы адресации
  • 2.6. Переходы
  • 2.7. Вызовы подпрограмм
  • 2.8. Макросредства ассемблера
  • 3. Команды и алгоритмы
  • 3.1. Организация приложений MS-DOS
  • 3.2. Циклы и условные переходы
  • 3.3. Обработка строк
  • 3.4. Использование подпрограмм
  • 3.5. Двоично-десятичные числа
  • 3.6. Программирование аппаратных средств
  • 4. Расширенные возможности современных микропроцессоров
  • 4.1. Архитектурные особенности
  • 4.2. Дополнительные режимы адресации
  • 4.3. Использование средств 32-разрядных процессоров в программировании
  • 4.4. Основы защищенного режима
  • Приложение. Система команд процессоров Intel


От автора

Введение

Микропроцессоры корпорации Intel и персональные компьютеры на их базе прошли не очень длинный во времени, но значительный по существу путь развития, на протяжении которого кардинально изменялись и возможности и даже сами принципы их архитектуры. В то же время, внос в микропроцессор принципиальные изменения, разработчики были вынуждены постоянно иметь в виду необходимость обеспечения совмести мости новых моделей со старыми, чтобы не отпугивать потенциального покупателя перспективой полной замены освоенного или разработанного им программного обеспечения. В результате современные микропроцессоры типа Pentium, обеспечивая такие возможности, как 32-битную адресацию почти неограниченных объемов памяти, многозадачный режим с одновременным выполнением нескольких программ, аппаратных средства защиты операционной системы и прикладных программ друг друга, богатый набор дополнительных эффективных команд и способе адресации, в то же время могут работать (и часто работают) в режиме первых микропроцессоров типа 8086, используя всего лишь 1 мегабайт оперативной памяти, 16-разрядные операнды (т. е. числа в диапазоне до 216 - 1 = 65535) и ограниченный состав команд. Поскольку программирование на языке ассемблера напрямую затрагивает аппаратные возможности микропроцессора, прежде всего следует выяснить, в какой степени программист может использовать новые возможности микропроцессоров в своих программах и какие проблемы программной несовместимости могут при этом возникнуть.
Первые персональные компьютеры корпорации IBM, появившиеся в 1981 г. и получившие название IBM PC, использовали в качестве центрального вычислительного узла 16-разрядный микропроцессор с 8-разрядной внешней шиной Intel 8088. В дальнейшем в персональных компьютерах стал использоваться и другой вариант микропроцессора, 8086, который отличался от 8088 тем, что являлся полностью 16-разрядным. С тех пор его имя стало нарицательным, и в программах, использующих только возможности процессоров 8088 или 8086, говорят, что они работают в режиме 86-го процессора.
В 1983 г. корпорацией Intel был предложен микропроцессор 80286, в котором был реализован принципиально новый режим работы, получивший название защищенного. Однако процессор 80286 мог работать и в режиме 86-го процессора, который стали называть реальным.
В дальнейшем на смену процессору 80286 пришли модели 80386, i486 и, наконец, различные варианты процессора Pentium. Все они могут работать и в реальном, и в защищенном режимах. Хотя каждая следующая модель была значительно совершеннее предыдущей (в частности, почти на два порядка возросла скорость работы процессора, начиная с модели 80386 процессор стал 32-разрядным, а в процессорах Pentium реализован даже 64-разрядный обмен данными с системной шиной), однако с точки зрения программиста все эти процессоры весьма схожи. Основным их качеством является наличие двух режимов работы - реального и защищенного. Строго говоря, в современных процессорах реализован еще и третий режим - виртуального 86-го процессора, или V86, однако в плане использования языка ассемблера этот режим не отличается от обычного режима 86-го процессора, мы его касаться не будем.
Реальный и защищенный режимы прежде всего принципиально различаются способом обращения к оперативной памяти компьютера. Метод адресации памяти, используемый в реальном режиме, позволяет адресовать память лишь в пределах 1 Мбайт; в защищенном режиме используется другой механизм (из-за чего, в частности, эти режимы и оказались полностью несовместимыми), позволяющий обращаться к памяти объемом до 4 Гбайт. Другое важное отличие защищенного режима заключается в аппаратной поддержке многозадачности с аппаратной же (т.е. реализованной в самом микропроцессоре) защитой задач друг от друга.
Реальный и защищенный режимы имеют прямое отношение к работе операционной системы, установленной на компьютере.
В настоящее время на персональных компьютерах типа IBM PC используются в основном два класса операционных систем (оба - разработки корпорации Microsoft): однозадачная текстовая система MS-DOS и многозадачная графическая система Windows. Операционная система MS-DOS является системой реального режима; другими словами, она использует только средства процессора 8086, даже если она установлена на компьютере с процессором Pentium. Система Windows - это система защищенного режима; она значительно более полно использует возможности современных процессоров, в частности, многозадачность и расширенное адресное пространство. Разумеется, система Windows не могла бы рабо-тать с процессором 8086, так как в нем не был реализован защищенный режим.
Соответственно двум типам операционных систем, и все программное обеспечение персональных компьютеров подразделяется на два класса: программы, предназначенные для работы под управлением MS-DOS (их часто называют приложениями DOS) и программы, предназначенные для системы Windows (приложения Windows). Естественно, приложения DOS могут работать только в реальном режиме, а приложения Windows - только в защищенном.
Таким образом, выражения "программирование в системе MS-DOS", "программирование в реальном режиме" и "программирование 86-го процессора" фактически являются синонимами. При этом следует подчеркнуть, что хотя процессор 8086, как микросхема, уже давно не используется, его архитектура и система команд целиком вошли в современные процессоры. Лишь относительно небольшое число команд современных процессоров специально предназначены для организации защищенного режима и распознаются процессором, только когда он работает в за щенном режиме. Поэтому изучение языка ассемблера целесообразно начинать с изучения архитектуры процессора 8086 или, точнее, того гипотетического процессора, который как бы объединяет часть архитектур средств современных процессоров, предназначенных для использования в реальном режиме, и соответствующих архитектуре процессора 8086. будем называть этот гипотетический процессор МП 86. Изучению архитектуры и программирования МП 86 посвящены первые три главы.
Деление программ на приложения DOS и приложения Windows исчерпывают вопроса о возможных типах программ. Дело в том, что ряд дополнительных средств, имеющихся в современных процессорах, вполне можно использовать и в реальном режиме (хотя сама операционная система MS-DOS, разработанная еще в эпоху процессора 8088, ими пользуется). К этим средствам относится расширенный состав команд процессоров и, главное, их 32-разрядная архитектура.
Современные процессоры (начиная с 80386), в отличие от своего предшественника 8086, являются 32-разрядными. Это дает возможность программисту использовать в программе 32-разядные операнды (т. е. числа в диапазоне до 232-1=4 294 967 295), что во многих случаях позволяет упростить алгоритм программы и повысить ее быстродействие. Программа, предполагающая работать с 32-разрядными операндами, должна иметь в своем составе одну из директив .386, .486 или .586, которые разрешают транс тору использовать дополнительные средства соответствующего процессе. Включение в программу этой директивы одновременно открывает доступ и к дополнительным командам и способам обращения к памяти, отсутствующим в процессоре 8086, что также расширяет возможности программирования. Эти средства описаны в главе, посвященной расширенным возможностям современных процессоров.
Особая ситуация возникает, если программист хочет использовать не только 32-битовые данные, но и 32-разрядные адреса ячеек памяти. Программы такого рода называются 32-разрядными приложениями (в отличие от 16-разрядных, в которых все адреса 16-разрядные, хотя дани могут иметь размер 32 бит). Для создания 32-разрядного приложения в состав программы необходимо включить (в соответствующем месте) oписатель use32. Однако следует иметь в виду, что MS-DOS не позволяет пускать 32-разрядные приложения, которые, таким образом, должны 6ыть приложениями Windows. В то же время приложения Windows обычно составляются не на языке ассемблера, а на одном из языков высокого уровня - Паскаль, С или C++, где тип создаваемой программы (16-разрядная или 32-разрядная) определяется настройками компилятора. Поэтому вопросы разработки 32-разрядных приложений на языке ассемблера не вошли в настоящую книгу.
Как известно, система Windows.-допускает запуск приложений DOS (программ реального режима), организуя для этого так называемый сеанс DOS, в котором программы фактически выполняются в режиме 86-го процессора. Кроме этого, в Windows предусмотрена возможность загрузки компьютера в режиме эмуляции DOS, когда компьютер работает под уп-равлением варианта DOS, встроенного в систему Windows. Наконец, ком-пьютер можно сконфигурировать так, чтобы он позволял загружать как Windows, так и "чистую" DOS (например, MS-DOS 6.22), без всякой Windows. Каким вариантом загрузки компьютера пользоваться при проработке настоящей книги - дело вкуса и привычек читателя. Приводимые в книге примеры в своем большинстве просты, не затрагивают каких-то специальных системных средств и будут работать в любом режиме. Последнее замечание не относится только к примеру программы защищенного режима, приводимому в гл. 4, а также к обработчику прерываний от мыши, рассматриваемому в гл. 3. Эти программы следует запускать на "чистой" DOS.


2. Основы программирования

    • 2.1. Подготовка и отладка программы
    • 2.2. Представление данных
    • 2.3. Описание данных
    • 2.4. Структуры и записи
    • 2.5. Способы адресации
    • 2.6. Переходы
    • 2.7. Вызовы подпрограмм
    • 2.8. Макросредства ассемблера

2.1. Подготовка и отладка программы
 Процесс подготовки и отладки программы на языке ассемблера включает этапы подготовки исходного текста, трансляции, компоновки и отладки.
Подготовка исходного текста программы выполняется с помощью любого текстового редактора, хотя бы редактора, встроенного в программу Norton Commander, или еще более удобного редактора Norton Editor. При использовании одного из более совершенных текстовых процессоров, вроде Microsoft Word, следует иметь в виду, что эти программы добавляют в выходной файл служебную информацию о формате (размер страниц, тип шрифта и др.), которая будет непонятна транслятору. Однако практически все текстовые редакторы и процессоры позволяют вывести в выходной файл "чистый текст", без каких-либо служебных символов. Именно таким режимом и надлежит воспользоваться в нашем случае.
В принципе для подготовки исходного текста можно воспользоваться любым редактором системы Windows, например, программой WordPad или Блокнотом. Однако в этом случае возникнут неприятности с русским шрифтом. Как известно, корпорация Microsoft приняла для своих русифицированных продуктов собственную кодировку русских символов, расходящуюся со стандартной, используемой в приложениях DOS. Если программу, использующую русский текст в качестве комментариев, или выводящую его на экран, подготовить в одном из редакторов Windows, то при ее просмотре и запуске в среде DOS вместо русского текста вы увидите бессмысленный набор символов. Поэтому программы, предназначенные для выполнения под управлением MS-DOS, лучше и подготавливать в среде DOS. Файл с исходным текстом должен иметь расширение .ASM.
Следующая операция состоит в трансляции исходного текста программы, т.е. в преобразовании строк исходного языка в коды машинных команд. Эта операция выполняется с помощью транслятора с языка ассемблера (т.е. с помощью программы ассемблера). Известные разработчики программного обеспечения - корпорации IBM, Borland, Microsoft и др. предлагают свои варианты трансляторов, несколько различающиеся своими возможностями и системой обозначений. Однако входной язык любого транслятора, включающий в себя мнемонику машинных команд и других операторов и правила написания предложений ассемблера, для всех ассемблеров одинаков, поэтому при подготовке и отладке примеров данной книги можно с равным успехом воспользоваться любой из указанных программ. Мы, как уже отмечалось, использовали программы пакета TASM 5.0 (фирменные названия этих программ - Turbo Assembler,
Turbo Link и Turbo Debugger, а имена соответствующих им файлов - TASM.EXE, TLINK.EXE и TD.EXE).
После трансляции образуются два файла - листинг трансляции и объектный файл с расширением OBJ. Листинг представляет собой текстовый файл, предназначенный для чтения в каком-либо редакторе, и содержит исходный текст оттранслированной программы вместе с машинными кодами команд. В случае обнаружения транслятором каких-либо ошибок, в листинг также включаются сообщения об этих ошибках.
Рассмотрим элементы листинга трансляции примера 1-1 из предыдущей главы. На рис. 2.1 приведен несколько сокращенный текст этого листинга, из которого удалены комментарии к отдельным предложениям.
айту. В этом случае следует использовать перетаскивание в обратном направлении. А именно, разыщите целевой файл, например, при помощи программы Проводник, затем перетащите его значок на карту узла — на изображение родительского документа.

Рис. 2.1. Листинг трансляции программы 1-1.

Рассматривая листинг, можно отметить ряд полезных моментов общего характера. Предложения программы с операторами assume, segment, ends, end, как уже отмечалось ранее, не транслируются в какие-либо машинные коды и не находят отражения в памяти. Они нужны лишь для передачи транслятору служебной информации о способе трансляции команд (assume), границах сегментов (segment и end) и строке, на которой следует завершить обработку исходного текста (end).
Каждому транслируемому предложению программы соответствует определенное смещение, причем задание смещений выполняется в каждом сегменте в отдельности. Первая команда mov AX,data имеет смещение от начала сегмента команд, равное нулю. Она занимает 3 байта, поэтому следующая команда начинается с байта 3 и имеет соответствующее смещение.
Транслятор не смог полностью определить код команды mov AX,data. В этой команде в регистр АХ засылается сегментный адрес сегмента data. Однако этот адрес станет известен лишь в процессе загрузки выполнимого файла программы в память. Поэтому в листинге на месте этого адреса стоят нули, помеченные буквой s, напоминающей о том, что здесь должен быть пока неизвестный сегментный адрес.
Еще одна помеченная команда с кодом ВА 0000 располагается в строке 8 листинга. В этой команде в регистр DX заносится смещение поля с именем msg, расположенное в сегменте данных (ключевое слово offset, указанное перед именем поля, говорит о том, что речь идет не о содержимом ячейки msg, а об ее смещении). Поле msg расположено в самом начале сегмента данных, и его смещение от начала сегмента равно 0, что и указано в коде команды. Почему же эта команда помечена буквой т, являющейся сокращением слова relocatable, переместимый?
Чтобы ответить на этот вопрос, нам придется рассмотреть, как сегменты программы размещаются в памяти. Как уже говорилось, любой сегмент может располагаться в памяти только с адреса, кратного 16, т.е. на границе 16-байтового блока памяти (параграфа). Конкретный адрес программы в памяти зависит от конфигурации компьютера, - какой размер занимает DOS, сколько загружено резидентных программ и драйверов, а также в каком режиме запускается программа - в отладчике или без него. Предположим, что сегментный адрес сегмента команд оказался равным 1306п (рис. 2.2, а). В нашей программе сегмент команд имеет размер 11h байт (что указано в строке 13 листинга), т.е. занимает целый параграф плюс один байт. Сегмент данных имеет размер 14h байт (строка 19 листинга) и тоже требует для своего размещения немного больше одного парафафа. Из-за того, что сегмент данных должен начаться на границе параграфа, ему будет назначен сегментный адрес 1308h и между сегментами образуется пустой промежуток размером 15 байт.
Потеря 15 байт из многомегабайтовой памяти, разумеется, не имеет никакого значения, однако в некоторых случаях, например, при компоновке единой профаммы из большого количества модулей с небольшими по размеру подпрофаммами, суммарная потеря памяти может оказаться значительной.

Рис. 2.2. Расположение сегментов программы в памяти при выравнивании по умолчанию (а) и на байт (б).

Для того, чтобы устранить потери памяти, можно сегмент данных объявить с выравниванием на байт:

data segment byte

Такое объявление даст возможность системе загрузить сегмент данных так, как показано на рис. 2.2, б. Сегмент данных частично перекрывает сегмент команд, начинаясь на границе его последнего параграфа (в нашем случае по адресу 1307h). Для того, чтобы данные не наложились на последние команды сегмента команд, они смещаются вниз так, что начинаются сразу же за сегментом команд. В нашем примере, где сегмент команд "выступает" за сегментный адрес 1307h всего на 1 байт, данные и надо сместить на этот 1 байт. В результате поле msg, с которого начинается сегмент данных, и которое в листинге имело смещение 0, получит смещение 1. Все остальные адреса в сегменте данных также сместятся на один байт вперед. В результате данные будут располагаться в физической памяти вплотную за командами, без всяких промежутков, однако все обращения в сегменте команд к данным должны быть скорректированы на величину перекрытия сегментов, в нашем случае - на 1 байт. Эта коррекция выполняется системой после загрузки программы в память, но еще до ее запуска. Адреса, которые могут потребовать описанной коррекции, и помечаются в листинге трансляции буквой "г".Из сказанного следует очень важный и несколько неожиданный вывод: коды команд программы в памяти могут не совпадать с кодами, показанными в листинге трансляции. Это обстоятельство необходимо учитывать при отладке программ с помощью интерактивного отладчика, который, естественно, показывает в точности то, что находится в памяти, и что не всегда соответствует листингу трансляции.
Вернемся к рассмотрению листинга трансляции. Данные, введенные нами в программу, также оттранслировались: вместо символов текста в загрузочный файл попадут коды ASCII этих символов. Так, буква "П" преобразовалась в код 8Fh, буква "р" в код ЕО и т. д. При выводе этих кодов на экран видеосистема компьютера преобразует их назад в изображения символов, записанных в исходном тексте программы.
Из листинга трансляции легко определить размер отдельных составляющих программы. В нашем случае длина сегмента команд составляет 11h = 17 байт, длина сегмента данных - 14h = 20 байт, а под стек отведено ровно столько, сколько мы запросили в программе - 100h = 256 байт. Размер же всей программы окажется больше суммы длин сегментов, во-первых, из-за пустых промежутков между сегментами (у нас на них уйдет 15 + 12 = 27 байт), и, во-вторых, за счет подсоединения к программе обязательного префикса программы, имеющего всегда размер 256 байт.
Как мы уже отмечали, в результате трансляции программы образуется два файла - с листингом и с объектным модулем программы.
Объектный файл является основным результатом работы транслятора и представляет собой текст программы, преобразованный в машинные коды. Хотя в этом файле уже присутствуют коды команд, он не может быть выполнен. Для того чтобы получить выполнимую программу, объектный файл необходимо скомпоновать.
Компоновка объектного файла выполняется с помощью программы компоновщика (редактора связей). Эта программа получила такое название потому, что ее основное назначение - подсоединение к файлу с основной программой файлов с подпрограммами и настройка связей между ними. Однако компоновать необходимо даже простейшие программы, не содержащие подпрограмм. Дело в том, что у компоновщика имеется и вторая функция - изменение формата объектного файла и преобразование его в выполнимый файл, который может быть загружен в оперативную память и выполнен. Файл с программой компоновщика обычно имеет имя LINK.EXE, хотя это может быть и не так. Например, компоновщик пакета TASM назван TLINK.EXE. В результате компоновки образуется загрузочный, или выполнимый файл с расширением .ЕХЕ.
Отладку и изучение работы готовой программы удобнее всего осуществлять с помощью интерактивного отладчика, который позволяет выполнять отлаживаемую программу по шагам или с точками останова, выводить на экран содержимое регистров и областей памяти, модифицировать (в известных пределах) загруженную в память программу, принудительно изменять содержимое регистров и выполнять другие действия, позволяющие в наглядной и удобной форме контролировать выполнение программы.
Рассмотрим вкратце основные приемы работы с "турбоотладчиком" TD.EXE из пакета TASM. Приступая к работе с отладчиком, следует убедиться, что в рабочем каталоге имеются и загрузочный (Р.ЕХЕ), и исходный (P.ASM) файлы, так как отладчик в своей работе использует оба эти файла. Для запуска отладчика следует ввести команду

td р

На экране появится кадр отладчика, в котором видны два окна - окно Module с исходным текстом отлаживаемой программы и окно Watches для наблюдения за ходом изменения заданных переменных в процессе выполнения программы (рис. 2.3). Окно Watches нам не понадобится, и его можно убрать, щелкнув мышью по маленькому квадратику в левом верхнем углу окна, или введя команду <Alt>+<F3>, предварительно сделав это окно активным. Переключение (по кругу) между окнами осуществляется клавишей <F6>.

Рис. 2.З. Начальный кадр отладчика с текстом отлаживаемой программы.

В процессе отладки программы на экран приходится выводить много дополнительных окон; они перекрываются и часто скрывают друг друга. Чтобы увидеть их все одновременно, размер окон приходится уменьшать, а сами окна перемещать по экрану. Режим изменения размеров и положения окна включается командой <Ctrl>+<F5>, после чего клавиши со стрелками перемещают окно по экрану, а те же клавиши при нажатой клавише <Shift> позволяют изменять его размер. Выход из режима настройки окна осуществляется нажатием клавиши <Enter>.
Начальное окно отладчика дает слишком мало информации для отладки программы. В нем можно выполнять программу по частям до местоположения курсора (клавиша <F4>) и команда за командой (клавиша <F8>); можно также с помощью окна Watches наблюдать изменения заданных полей данных. Однако для отладки программы на уровне языка ассемблера необходимо контролировать все регистры процессора, включая регистр флагов, а также, во многих случаях, поля данных вне программы (например, векторы прерываний или системные таблицы). Гораздо более информативным является "окно процессора", которое вызывается с помощью пункта Vicw>CPU верхнего меню или командой <Alt>+<V>+<C> (рис. 2.4).

Рис. 2.4. Окно процессора с внутренними окнами.

Окно процессора состоит, в свою очередь, из 5 внутренних окон для наблюдения текста программы на языке ассемблера и в машинных кодах, регистров процессора, флагов, стека и содержимого памяти. С помощью этих окон можно полностью контролировать ход выполнения отлаживаемой программы. Для того чтобы можно было работать с конкретным окном, например, прокручивать его содержимое, надо сделать его активным, щелкнув по нему мышью. Перейти из окна в окно можно также с помощью клавиатуры, нажимая клавишу Tab. Посмотрим, какие сведения можно извлечь из содержимого окна процессора.
Содержимое сегментных регистров DS и ES одинаково и составляет HF5h. Эта значит, что программа загружена в память, начиная с физического адреса 11F50, т.е. приблизительно с 70-го килобайта. Чем заняты первые 70 Кбайт памяти? Обычно компьютер конфигурируется так, что в обычной памяти размещается только малая часть DOS (около 16 Кбайт), драйверы обслуживания расширенной памяти и резидентная часть COMMAND.COM. Основная часть DOS, остальные драйверы и необходимые резидентные программы (например, русификатор) переносятся в расширенную память. В этом случае системные области в начале памяти занимают всего 20 - 25 Кбайт. Тем не менее наша программа начинается не с 25-го, а с 70-го килобайта. Произошло это из-за того, что программа запущена под управлением отладчика, который сначала загружается в память сам, и лишь затем загружает отлаживаемую программу. Но отсюда следует, что если бы мы запустили программу без отладчика, она попала бы на другое место в памяти, гораздо ближе к ее началу. В большинстве случаев это обстоятельство не имеет особого значения, так как любая программа должна одинаково успешно выполняться в любом месте памяти, однако необходимо отдавать себе отчет, что отладчик изменяет операционную среду программы (в частности, переносит ее на другое место в памяти). Строго говоря, программа под управлением отладчика выполняется не совсем так, как она выполнялась бы непосредственно в DOS.
Еще один пример "самодеятельности" отладчика можно увидеть в том же окне регистров процессора. Содержимое всех регистров общего назначения (АХ, ВХ, СХ, DX, SI, DI и ВР) равно 0. Отсюда можно сделать вывод, что DOS, загружая программу в память, очищает регистры процессора. Однако на самом деле это совсем не так! Регистры очищает не DOS, а отладчик. При обычном запуске программы исходное содержимое регистров практически непредсказуемо, и ни в коем случае нельзя рассчитывать, что в них будут нули. Иногда можно столкнуться и с более тонким влиянием отладчика на ход выполнения программы, вплоть до того, что некоторые виды программ, например, управляющие подключенной к компьютеру аппаратурой, в отладчике будут выполняться просто неверно.
Итак, после загрузки программы в память содержимое регистров DS и ES оказалось одинаковым. Это вполне естественно, если вспомнить, что перед выполнением оба регистра указывают на префикс программы (см, рис. 1.9). Вслед за префиксом располагается сегмент команд и поскольку префикс всегда занимает точно lOOh байт (т.е. 10h параграфов по 16 байт), то содержимое CS в нашем случае должно быть равно HF5h + 10h = 1205h. Так оно и есть (см. рис. 2.4).
В нашем примере программа должна начать выполняться с метки begin, поскольку именно эту метку мы указали в качестве операнда завершающей директивы end. Эта метка относится к самой первой команде сегмента команд и ее значение (или, что то же самое, смещение первой команды программы) должно быть равно 0. Поэтому исходное значение указателя команд, как это видно из рис. 2.4, тоже равно 0. В дальнейшем, по мере выполнения команд, значение IP будет возрастать. Выполним две первые команды программы, дважды нажав клавишу <F8>. Состояние программы после этой операции показано на рис. 2.5.

Рис. 2.5. Состояние программы после выполнения двух первых команд.

Видно, что указатель команд получил значение 5 и показывает на очередную (еще не выполнявшуюся) команду mov AH,09h, относительный адрес которой равен 5. Сегментный регистр DS получил значение 1207h, что должно соответствовать сегментному адресу сегмента данных. Вспомним, что сегмент команд у нас занимает 11h байт и требует в памяти 2 параграфа. Сегмент команд имеет сегментный адрес 1205h, следовательно, сегментный адрес сегмента данных должен быть равен 1207h, что мы и получили.
Обратим внимание на самую правую колонку в окне процессора, в которой индицируются состояния флагов процессора. Как уже говорилось, состояния флагов заново устанавливаются процессором после выполнения каждой команды, и по ним можно в определенной степени судить о результате команды. С самого начала у нас был установлен только флаг IF (i в окне отладчика), что свидетельствует о включенном механизме аппаратных прерываний; остальные флаги сброшены. После выполнения двух первых команд состояние регистра флагов не изменилось. Произошло это потому, что команда пересылки mov не изменяет состояния флагов. Поскольку в нашей программе нет никаких команд, кроме mov и hit, а команда hit тоже состояния флагов обычно не изменяет, то наблюдать с помощью нашего примера функционирование регистра флагов не удастся.
Рассмотрим теперь стек. Сегмент данных имеет у нас размер 14h байт, и под него в памяти надлежит выделить 2 параграфа. Это объясняет содержимое сегментного регистра стека SS - 1209п. Под стек отведено 256 байт, поэтому исходное положение SP (под дном стека) соответствует смещению l00h.
Наконец, стоит еще обратить внимание на нижнюю половину окна команд, заполненную странными командами add [bx+si],al. Таких команд, да еще в таком количестве, в нашей программе нет, их "придумал" отладчик, пытаясь деассемблировать промежуток между сегментом команд и сегментом данных, заполненный нулями. Код 0000h соответствует команде add [bx+si],al, которую и изобразил отладчик.
Таким образом, рассмотрев информацию, предоставленную отладчиком, мы подтвердили все предыдущие рассуждения о расположении в памяти сегментов программы и об инициализации регистров процессора при загрузке программы в память.
Обратимся теперь к окну дампа. При запуске отладчика в окно дампа выводится содержимое памяти, начиная с адреса DS:0000h, т.е. начало префикса программы (см. рис. 2.4 и 2.5). Для того, чтобы вывести на экран что-либо иное, надо воспользоваться командой <Alt>+<F10>, которая для каждого внутреннего окна процессора открывает дополнительное меню. Вид этого меню зависит от того, какое окне было активным в момент ввода команды. На рис. 2.6 показано дополнительное меню окна дампа.

Рис. 2.6. Дополнительное меню окна дампа памяти.

Чаще всего приходится пользоваться первым пунктом этого меню Goto, с помощью которого можно задать любой адрес (входящий или не входящий в сегменты программы), и получить дамп этого участка.. На рис. 2.7. изображено содержимое окна дампа после ввода начального адреса в виде DS:0 (тот же результат даст начальный адрес DS:msg, а так же и просто msg, так как по умолчанию сегментный адрес берется из DS). Как и следовало ожидать, по этому адресу расположено наше единственное данное - строка текста, выводимая программой на экран. Кстати, в окне дампа видно начало промежутка между сегментами (данных и стека), заполненного нулями.

Рис. 2.7. Дамп сегмента данных.

2.2. Представление данных
В языке ассемблера имеются средства записи целых и вещественных чисел, а также символьных строк и отдельных символов. Целые числа могут быть со знаком и без знака, а также записанными в двоично-десятичном формате. Для целых чисел и символов в составе команд микропроцессора и, соответственно, в языке ассемблера, есть средства обработки - анализа, сравнения, поиска и проч. Для вещественных чисел таких средств в самом микропроцессоре нет, они содержатся в арифметическом сопроцессоре. Поскольку программирование сопроцессора в настоящей книге не рассматривается, то и вещественными числами мы заниматься не будем.
Рассмотрим сначала целые числа без знака и со знаком. Числа без знака получили свое название потому, что среди этих чисел нет отрицательных. Это самый простой вид чисел: они представляют собой весь диапазон двоичных чисел, которые можно записать в байте, слове или двойном слове. Для байта числа без знака могут принимать значения от 00h (0) до FFh (255); для слова - от 0000h (0) до FFFFh (65535); для двойного слова - от 00000000h (0) до FFFFFFFFh (4294967295).
В огромном количестве приложений вычислительной техники для чисел нет понятия знака. Это справедливо, например, для адресов ячеек памяти, кодов ASCII символов, результатов измерений многих физических величин, кодов управления устройствами, подключаемыми к компьютеру. Для таких чисел естественно использовать весь диапазон чисел, записываемых в ячейку того или иного размера. Если, однако, мы хотим работать как с положительными, так и с отрицательными числами, нам придется половину чисел из их полного диапазона считать положительными, а другую половину - отрицательными. В результате диапазон изменения числа уменьшается в два раза. Кроме того, необходимо предусмотреть систему кодирования, чтобы положительные и отрицательные числа не перекрывались.
В вычислительной технике принято записывать отрицательные числа в так называемом дополнительном коде, который образуется из прямого путем замены всех двоичных нулей единицами и наоборот (обратный код) и прибавления к полученному числу единицы. Это справедливо как для байтовых (8-битовых) чисел, так и для чисел размером в слово или в двойное слово (рис. 2.8)

Рис. 2.8. Образование отрицательных чисел различного размера.

Такой способ образования отрицательных чисел удобен тем, что позволяет выполнять над ними арифметические операции по общим правилам с получением правильного результата. Так, сложение чисел +5 и -5 дает 0; в результате вычитания 3 из 5 получается 2; вычитание -3 из -5 дает -2 и т.д.
Анализируя алгоритм образования отрицательного числа, можно заметить, что для всех отрицательных чисел характерно наличие двоичной единицы в старшем бите. Положительные числа, наоборот, имеют в старшем бите 0. Это справедливо для чисел любого размера. Кроме того, из рис. 2.8 видно, что для преобразования отрицательного 8-битового числа в слово достаточно дополнить его слева восемью двоичными единицами. Легко сообразить, что для преобразования положительного 8-битового числа в слово его надо дополнить восемью двоичными нулями. То же справедливо и для преобразования слова со знаком в двойное слово со знаком, только добавить придется уже не 8, а 16 единиц или нулей. В системе команд МП 86 и, соответственно, в языке ассемблера, для этих операций предусмотрены специальные команды cbw и cwd.
Следует подчеркнуть, что знак числа условен. Одно и то же число, например, изображенное на рис. 2.8 8-битовое число FBh можно в одном контексте рассматривать, как отрицательное (-5), а в другом - как положительное, или, правильнее, число без знака (FBh=251). Знак числа является характеристикой не самого числа, а нашего представления о его смысле.
На рис. 2.9 представлена выборочная таблица 16-битовых чисел с указанием их машинного представления, а также значений без знака и со знаком. Из таблицы видно, что для чисел со знаком размером в слово диапазон положительных значений простирается от 0 до 32767, а диапазон отрицательных значений - от -1 до -32768.

Рис. 2.9. Представление 16-битовых чисел без знака и со знаком.

На рис. 2.10 представлена аналогичная таблица для 8-битовых чисел. Из таблицы видно, что для чисел со знаком размером в байт диапазон положительных значений простирается от 0 до 127, а диапазон отрицательных значений - от -1 до -128.

Рис. 2.10. Представление 8-битовых чисел без знака и со знаком.

Среди команд процессора, выполняющих ту или иную обработку чисел, можно выделить команды, безразличные к знаку числа (например, inc, add, test), команды, предназначенные для обработки чисел без знака (mul, div, ja, jb и др.), а также команды, специально рассчитанные на обработку чисел со знаком (imul, idiv, jg, jl и т.д.). Особенности использования этих команд будут описаны в следующей главе.
Рассмотрим теперь другой вид представления чисел - двоично-десятичный формат (binary-coded decimal , BCD), используемый в ряде прикладных областей. В таком формате выдают данные некоторые измерительные приборы; он же используется КМОП-часами реального времени компьютеров IBM PC для хранения информации о текущем времени. В МП 86 предусмотрен ряд команд для обработки таких чисел.
Двоично-десятичный формат существует в двух разновидностях: упакованный и распакованный. В первом случае в байте записывается двухразрядное десятичное число от 00 до 99. Каждая цифра числа занимает половину байта и хранится в двоичной форме. Из рис. 2.11 можно заметить, что для записи в байт десятичного числа в двоично-десятичном формате достаточно сопроводить записываемое десятичное число символом h.

Рис. 2.11. Упакованный двоично-десятичный формат.

В машинном слове или в 16-разрядном регистре можно хранить в двоично-десятичном формате четырехразрядные десятичные числа от 0000 до 9999 (рис.2.12).

Рис. 2.12. Запись десятичного числа 9604 в слове.

Распакованный формат отличается от упакованного тем, что в каждом байте записывается лишь одна десятичная цифра (по-прежнему в двоичной форме). В этом случае в слове можно записать десятичные числа от 00 до 99 (см. рис. 2.13)

Рис. 2.13. Запись десятичного числа 98 в распакованном виде.

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

2.3. Описание данных
Практически любая программа содержит в себе перечень данных, с которыми она работает. Это могут быть символьные строки, предназначенные для вывода на экран; числа, определяющие ход выполнения программы или участвующие в вычислениях; адреса подпрограмм, обработчиков прерываний или просто тех или иных полей программы; специальные коды, например, коды цвета выводимых на экран символов и т.д. Кроме данных, определяемых в тексте программы, в программу часто входят зарезервированные поля, предназначенные для заполнения по ходу выполнения программы, например, результатами вычислений или путем чтения из файла. Все эти данные и зарезервированные поля должны быть определены в составе сегмента данных программы (в принципе они могут быть определены, и часто определяются, не в сегменте данных, а в сегменте команд, но здесь мы не будем касаться этого вопроса).
Для определения данных используются, главным образом, три директивы ассемблера: db (define byte, определить байт) для записи байтов, dw (define word, определить слово) для записи слов и dd (define double, определить двойное слово) для записи двойных слов:

db 255
dw 6.5535
dd 100000000

Кроме перечисленных, имеются и другие директивы, например df (define fanvord, определить поле из 6 байт), dq (define quadword, определить четверное слово) или dt (define tcraword, определить 10-байтовую переменную), но они используются значительно реже.
Для того чтобы к данным можно было обращаться, они должны иметь имена. Имена данных могут включать латинские буквы, цифры (не в качестве первого знака имени) и некоторые специальные знаки, например, знаки подчеркивания (_), доллара ($) и коммерческого at (@). Длину имени некоторые ассемблеры ограничивают (например, ассемблер MASM - 31 символом), другие - нет, но в любом случае слишком длинные имена затрудняют чтение программы. С другой стороны, имена данных следует выбирать таким образом, чтобы они отражали назначение конкретного данного, например counter для счетчика или filename для имени файла:

counter dw 10000
filename db "a:\myfile.001'

Значения числовых данных можно записывать в различных системах счисления; чаще других используются десятичная и 16-ричная запись:

size dw 256 ;В ячейку size записывается
;десятичное число 256
setb7 db 80h ;В ячейку setb7 записывается
;16-ричное число 80h

Необходимо отметить неточность приведенных выше комментариев. В памяти компьютера могут храниться только двоичные коды. Если мы говорим, что в какой-то ячейке записано десятичное число 128, мы имеем в виду не физическое содержимое ячейки, а лишь форму представления этого числа в исходном тексте программы. В слове с именем size фактически будет записан двоичный код 0000000100000000, являющийся двоичным эквивалентом десятичного числа 256. Во втором случае в байте с именем setbit? будет записан двоичный эквивалент шестнадцатиричного числа 80h, который составляет 10000000 (т.е. байт с установленным битом 7, откуда и получила имя эта ячейка).
Для резервирования места под массивы используется оператор dup (duplicate, дублировать), который позволяет "размножить" байт, слово или двойное слово заданное число раз:

rawdata dw 300 dup (1) ;Резервируются 300 слов,
;заполненных числом 1
string db 80 dup ('^') ;Резервируются 80 байтов,
;заполненных знаком '^'

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

mov AX,size

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

data segment
0000h counter dw 10000
0002h pages db "Страница 1"
000Ch numbers db 0, 1, 2, 3, 4
0011h page_addr dw pages
data ends

Сегмент данных начинается с данного по имени counter, которое описано, как слово (2 байт) и содержит число 10000. Очевидно, что его смещение равно 0. Поскольку это данное занимает 2 байт, следующее за ним данное pages получило смещение 2. Данное pages описывает строку текста длиной 10 символов и занимает в памяти столько же байтов, поэтому следующее данное numbers получило относительный адрес 2 + 10 = 12 = Ch. В поле numbers записаны 5 байтовых чисел, поэтому последнее данное сегмента с именем page_addr размещается по адресу Ch + 5 = 11h.
Ассемблер, начиная трансляцию сегмента (в данном случае сегмента данных) начинает отсчет его относительных адресов. Этот отсчет ведется в специальной переменной транслятора (не программы!), которая называется счетчиком текущего адреса и имеет символическое обозначение знака доллара (S). По мере обработки полей данных, их символические имена сохраняются в создаваемой ассемблером таблице имен вместе с соответствующими им значениями счетчика текущего адреса. Другими словами, введенные нами символические имена получают значения, равные их смещениям. Таким образом, с точки зрения транслятора counter равно 0, pages - 2, numbers - Ch и т.д. Поэтому предложение

page_addr dw pages

трактуется ассемблером, как

page_addr dw 2

и приводит к записи в слово с относительным адресом 11h числа 2 (смещения строки pages).
Приведенные рассуждения приходится использовать при обращении к "внутренностям" объявленных данных. Пусть, например, мы хотим выводить на экран строки "Страница 2", "Страница 3", "Страница 4" и т.д. Можно, конечно, все эти строки описать в сегменте данных по отдельности, но это приведет к напрасному расходу памяти. Экономнее поступить по-другому: выводить на экран одну и ту же строку pages, но модифицировать в ней номер страницы. Модификацию номера можно выполнить с помощью, например, такой команды:

mov pages + 9, ' 2'

Здесь мы "вручную" определили смещение интересующего нас символа в строке, зная, что все данные размещаются ассемблером друг за другом в порядке их объявления в программе. При этом, какое бы значение не получило имя pages, выражение pages + 9 всегда будет соответствовать байту с номером страницы.
Таким же приемом можно воспользоваться при обращении к данному numbers, которое в сущности представляет собой небольшой массив из 5 чисел. Адрес первого числа в этом массиве равен просто numbers, адрес второго числа - numbers + 1, адрес третьего - numbers + 2 и т.д. Следующая команда прочитает последний элемент этого массива в регистр DL:

mov DL,numbers+4

Какой смысл имело объединение ряда чисел в массив numbers? Да никакого, если к этим числам мы все равно обращаемся по отдельности. Удобнее было объявить этот массив таким образом:

nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

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

mov SI,4 ;Индекс элемента в массиве
mov DL,numbers[SI] ;Обращение по адресу
;numbers + содержимое SI

Иногда желательно обращаться к элементам массива (обычно небольшого размера) то с помощью индексов, то по их именам. Для этого надо к описанию массива, как последовательности отдельных данных, добавить дополнительное символическое описание адреса начала массива с помощью директивы ассемблера label (метка):

numbers label byte
nmb0 db 0
nmbl db 1
nmb2 db 2
nmb3 db 3
nmb4 db 4

Метка numbers должна быть объявлена в данном случае с описателем byte, так как данные, следующие за этой меткой, описаны как байты и мы планируем работать с ними именно как с байтами. Если нам нужно иметь массив слов, то отдельные элементы массива следует объявить с помощью директивы dw, а метке numbers придать описатель word:

numbers label word
nmb0 dw 0
nmbl dw 1
nmb2 dw 2
nmb3 dw 3
nmb4 dw 4

В чем состоит различие двух последних описаний данных? Различие есть, и весьма существенное. Хотя в обоих случаях в память записывается натуральный ряд чисел от 0 до 4, однако в первом варианте под каждое число в памяти отводится один байт, а во втором - слово. Если мы в дальнейшем будем изменять значения элементов нашего массива, то в первом варианте каждому числу' можно будет задавать значения от 0 до 255, а во втором - от 0 до 65535.
Выбирая для данных способ их описания, необходимо иметь в виду, что ассемблер выполняет проверку размеров используемых данных и не пропускает команды, в которых делается попытка обратиться к байтам, как к словам, или к словам - как к байтам. Рассмотрим последний вариант описания массива numbers. Хотя под каждый элемент выделено целое слово, однако реальные числа невелики и вполне поместятся в байт. Может возникнуть искушение поработать с ними, как с байтами, перенеся предварительно в байтовые регистры:

mov AL,nmb0 ;Переносим nmb0 в AL
mov DL,nmbl ;Переносим nmb1 в AL
mov CL,nmb2 ;Переносим nmb2 в AL

Так делать нельзя. Транслятор сообщит о грубой ошибке - несоответствии типов, и не будет создавать объектный файл. Однако довольно часто возникает реальная потребность в операциях такого рода. Для таких случаев предусмотрен специальный атрибутивный оператор byte ptr (byte pointer, байтовый указатель), с помощью которого можно на время выполнения одной Команды изменить размер операнда:

mov AL,byte ptr nmb0
mov DL,byte ptr nmbl
mov CL,byte ptr nmb2

Эти команды транслятор рассматривает, как правильные.
Часто возникает необходимость выполнить обратную операцию - к паре байтов обратиться, как к слову. Для этого надо использовать оператор word ptr:

okey db 'OK'
…
mov AX,word ptr okey

Здесь оба байта из байтовой переменной okey переносятся в регистр АХ. При этом первый по порядку байт, т.е. байт с меньшим адресом, содержащий букву "О" (можно считать, что он является младшим в слове
"OK"), отправится в младшую половину АХ - регистр AL, а второй по порядку байт, с буквой "К", займет регистр АН.
До сих пор речь шла о данных, которые, в сущности, являлись переменными, в том смысле, что под них выделялась память и их можно было модифицировать. Язык ассемблера позволяет также использовать константы, которые являются символическими обозначениями чисел и могут использоваться всюду в тексте программы, как наглядные эквиваленты этих чисел:

maxsize = 0FFFFh
mov CX,maxsize mov CX,0FFFFh

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

position=80*2*12+40*2

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

mes db 'Ждите'
mes_len = $-mes

В этом примере константа mes_len получает значение длины строки mes (в данном случае 5 байт), которая вычисляется как разность значения счетчика текущего адреса после определения строки и ее начального адреса mes. Такой способ удобен тем, что при изменении содержимого строки достаточно перетранслировать программу, и та же константа mes_len автоматически получит новое значение.

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

meddata struc ;Структура с именем meddata
index dd 0 ; Номер карты
sex db 0 ;Пол
birth dw 0 ;Год рождения
datein db ' / / ' ;Дата поступления
dateout db ' / / ' ;Дата выписки
meddata ends ;Конец описания структуры

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

data segment
pat 1 meddata <1234567, 'м',1955, 1З/06/981, '15/06/98'>
pat2 meddata <1982234, 'м',1932, '18/06/98', '25/06/98 '>
pat3 meddata <4389012, 'ж',1966, '01/12/97', '15/12/97'>
pattemp meddata <>
data ends

Имена patl, pat2 и т.д. будут служить именами переменных, каждая из которых содержит полный комплект данных об одном пациенте. Угловые скобки ограничивают конкретные данные, поступающие в каждую структурную переменную. Для переменной с именем pattemp транслятор выделит в памяти 23 байт, поместив в нее в точности то, что было указано в описании структуры (нули и два символьные шаблона для даты):

0,0,0, ' / / ',' / / '

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

mov EAX,patl.index ;ЕАХ=1234567
mov SI,offset patl.datein ;31=смещение элемента patl.datein
mov DL,pat3.sex ;DL='ж'

Особенности использования в приложениях DOS 32-разрядных регистров (ЕАХ в первой строке приведенного фрагмента) будут описаны в гл. 4.
Адрес конкретной структурной переменной можно поместить в базовый или индексный регистр, и пользоваться им в конструкциях с косвенной адресацией:

mov BX,offset pat3 ;ВХ=смещение pat3
mov EAX,[BX].index ;EAX=4389012
mov [BX].sех='м' ;Программная инициализация

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

mov BX, off set pat2 ;ВХ=смещение pat2
add BX,sex ;ВХ=смещение pat2.sex
mov DL, [BX] ;DL='M'
mov SI,birth ;SI=5 (сомнительная команда)

Записи
Записи, как и структуры, представляют собой шаблоны, накладываемые на реальные данные с целью введения удобных мнемонических обозначений отдельных элементов данных. В отличие от структур, дающих имена байтам, словам, двойным словам или целым массивам, в записях определяются строки битов внутри байтов, слов или двойных слов.
Известно, что дата создания файла хранится в каталоге диска в виде 16-битового слова, в котором старшие 7 бит обозначают год (от 1980), следующие 4 бит - месяц и последние 5 бит - день (рис. 2.14).

Рис. 2.14. Формат записи даты в каталоге диска.

Эти данные удобно специфицировать с помощью записи filedate, определяемой в программе следующим образом:

fdate record year:7, month: 4, day:5

Ключевое слово record говорит о том, что имя fdate относится к записи, а мнемонические обозначения year, month и day являются произвольными именами отдельных битовых полей описываемого слона.
Включение в программу описания шаблона битовых полей позволяет отказаться от утомительного и чреватого ошибками определения "вручную" содержимого полного данного по задаваемым значениям его отдельных составляющих. Для приведенной выше записи объявления конкретных переменных будут выглядеть следующим образом:

filel fdate <5,6,7> ;7 июня 1985г.
file2 fdate <18,12,30> ;30 декабря 1998г.
file3 fdate <> ;"Пустая" (пока) переменная

Переменная filel будет определена, как число 0AC7h, file2 - как число 259Eh, а fileЗ - как число 0000h. При необходимости программного заполнения переменной типа fdate можно пользоваться именами ее составляющих, которые трактуются ассемблером, как индексы соответствующих битовых полей, отсчитываемые от младшего конца слова. Для приведенного примера day=0, month=5, a year=9. Однако в системе команд МП 86 практически нет средств работы с битовыми полями. Поэтому программное заполнение придется осуществлять с помощью команд сдвигов и логического сложения:

mov flle3,30 ;Помещаем день
mov AX,12 ;Месяц пока в АХ
mov CL,month ;Будем сдвигать на month бит
shl AX,CL ; Сдвинули месяц в АХ на 5 бит
or file3,AX ;Добавили биты месяца в file3
mov AX, 18 ;Год пока в АХ
mov CL,month ;Будем сдвигать на year бит
shl AX,CL ;Сдвинули год в АХ на 9 бит
or file3,AX ;Добавили биты года в file3

В итоге в переменной fileЗ окажется тот же код 259Eh, что и в переменной file2.

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

mov mem, AX

в качестве операндов используется обозначение ячейки памяти mem, a также обозначение регистра АХ. В то же время, для соответствующей машинной команды операндами являются содержимое ячейки памяти и содержимое регистра. Было бы правильнее говорить об операндах машинных команд и о параметрах, или аргументах команд языка ассемблера.
По отношению к командам ассемблера было бы правильнее использовать термин "параметры", оставив за термином "операнд" обозначение тех физических объектов, с которыми имеет дело процессор при выполнении машинной команды, однако обычно эти тонкости не принимают в расчет, и говоря об операндах команд языка, понимают в действительности операнды машинных команд.
В архитектуре современных 32-разрядных процессоров Intel предусмотрены довольно изощренные способы адресации; в МП 86 способов адресации меньше. В настоящем разделе будут описаны режимы адресации, используемые в МП 86.
В книгах, посвященных языку ассемблера, можно встретить разные подходы к описанию способов адресации: не только названия этих режимов, но даже и их количество могут различаться. Разумеется, способов адресации существует в точности столько, сколько их реализовано в процессоре; однако, режимы адресации можно объединять в группы по разным признакам, отчего и создается некоторая путаница, в том числе и в количестве имеющихся режимов. Мы будем придерживаться распространенной, но не единственно возможной терминологии.
Регистровая адресация. Операнд (байт или слово) находится в регистре. Этот способ адресации применим ко всем программно-адресуемым регистрам процессора.

inc СН ;Плюс 1 к содержимому СН
push DS ;DS сохраняется в стеке
xchg ВХ,ВР ;ВХ и ВР обмениваются содержимым
mov ES, АХ ;Содержимое АХ пересылается в ES

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

mov АН, 40h ;Число 40h загружается в АН
mov AL,'*' ;Код ASCII символа "*' загружается в AL
int 21h ;Команда прерывания с аргументом 21h
limit = 528 ;Число 528 получает обозначение limit
mov CX,limit ;Число, обозначенное limit, загружается в СХ

Команда mov, использованная в последнем предложении, имеет два операнда; первый операнд определяется с помощью регистровой адресации, второй - с помощью непосредственной.
Важным применением непосредственной адресации является пересылка относительных адресов (смещений). Чтобы указать, что речь идет об относительном адресе данной ячейки, а не об ее содержимом, используется описатель onset (смещение):

; Сегмент данных
mes db "Урок 1' ;Строка символов
;Сегмент команд
mov DX,offset mes ;Адрес строки засылается в DX

В приведенном примере относительный адрес строки mes, т.е. расстояние в байтах первого байта этой строки от начала сегмента, в котором она находится, заносится в регистр DX.
Прямая адресация памяти. Адресуется память; адрес ячейки памяти (слова или байта) указывается в команде (обычно в символической форме) и поступает в код команды:

;Сегмент данных
meml dw 0 ;Слово памяти содержит 0
mem2 db 230 ;Байт памяти содержит 230
;Сегмент команд
inc meml ;Содержимое слова meml увеличивается на 1
mov DX, meml ; Содержимое слова с именем menu загружается в DX
mov AL,mem2 ; Содержимое байта с именем mem2 загружается в АL

Сравнивая этот пример с предыдущим, мы видим, что указание в команде имени ячейки памяти обозначает, что операндом является содержимое этой ячейки; указание имени ячейки с описателем offset - что операндом является адрес ячейки.
Прямая адресация памяти на первой взгляд кажется простой и наглядной. Если мы хотим обратиться, например, к ячейке meml, мы просто указываем ее имя в программе. В действительности, однако, дело обстоит сложнее. Вспомним, что адрес любой ячейки состоит из двух компонентов: сегментного адреса и смещения. Обозначения meml и mem2 в предыдущем примере, очевидно, являются смещениями. Сегментные же адреса хранятся в сегментных регистрах. Однако сегментных регистров четыре: DS, ES, CS и SS. Каким образом процессор узнает, из какого регистра взять сегментный адрес, и как сообщить ему об этом в программе?
Процессор различает группу кодов, носящих название префиксов. Имеется несколько групп префиксов: повторения, размера адреса, размера операнда, замены сегмента. Здесь нас будут интересовать префиксы замены сегмента.
Команды процессора, обращающиеся к памяти, могут в качестве первого байта своего кода содержать префикс замены сегмента, с помощью которого процессор определяет, из какого сегментного регистра взять сегментный адрес. Для сегментного регистра ES код префикса составляет 26h, для SS - 361i, для CS - 2Eh. Если префикс отсутствует, сегментный адрес берется из регистра DS (хотя для него тоже предусмотрен свой префикс).
Если в начале программы с помощью директивы assume указано соответствие сегменту данных сегментного регистра DS

assume DS:data

то команды обращения к памяти транслируются без какого-либо префикса, а процессор при выполнении этих команд берет сегментный адрес из регистра DS.
Если в директиве assume указано соответствие сегмента данных регистру ES

assume ES:data

(в этом случае сегмент данных должен располагаться перед сегментом команд), то команды обращения к полям этого сегмента транслируются с добавлением префикса замены для сегмента ES. При этом предложения программы выглядят обычным образом; в них по-прежнему просто указываются имена полей данных, к которым производится обращение.
Однако в ряде случаев префикс замены сегмента должен указываться в программе в явной форме. Такая ситуация возникает, например, если данные расположены в сегменте команд, что типично для резидентных обработчиков прерываний. Для обращения к таким данным можно, конечно, использовать регистр DS, если предварительно настроить его на сегмент команд, но проще выполнить адресацию через регистр CS, который и так уже настроен должным образом. Если в сегменте комавд содержится поле данных с именем mem, то команда чтения из этого поля будет выглядеть следующим образом:

mov AX,CS:mem

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

mov AX,0B800h ;Сегментный адрес видеобуфера
mov ES,AX ;Отправим его в ES
mov byte ptr ES:0, ' ! ' ;Отправим символ на 1-е знакоместо экрана
mov byte ptr ES:2, ' ! ' ;Отправим символ на 2-е знакоместо экрана

Настроив регистр ES на сегментный адрес видеобуфера BS00h, мы пересылаем код знака "!" сначала по относительному адресу 0 (в самое начало видеобуфера, в байт со смещением 0), а затем на следующее знакоместо, имеющее смещение 2 (в нечетных байтах видеобуфера хранятся атрибуты символов, т.е. цвет символов и фона под ними). В обеих командах необходимо с помощью обозначения ES: указать сегментный регистр, который используется для адресации памяти. Встретившись с этим обозначением, транслятор включит в код команды префикс замены сегмента, в данном случае код 26h.
В приведенном примере мы снова столкнулись с использованием атрибутивного оператора byte ptr, который позволяет в явной форме задать размер операнда. Однако если раньше этот оператор использовался, чтобы извлечь байт из данного, объявленного, как слово, то здесь его назначение иное. Транслятор, обрабатывая команду

mov byte ptr ES:0, ' ! '

не имеет возможности определить размер операнда-приемника. Разумеется, видеобуфер, как и любая память, состоит из байтов, однако надо ли рассматривать эту память, как последовательность байтов или слов? Команда без явного задания размера операнда

mov ES:0, ' ! '

вызовет ошибку трансляции, так как ассемблер не сможет определить, надо ли транслировать это предложение, как команду пересылки в видеобуфер байта 21h, или как команду пересылки слова 0021h.
Между прочим, на первый взгляд может показаться, что в обсуждаемой команде достаточно ясно указан размер правого операнда, так как символ (в данном случае "!") всегда занимает один байт. Однако транслятор, встретив обозначение "!", сразу же преобразует его в код ASCII этого символа, т.е. в число 21h, и уже не знает, откуда это число произошло и какой размер оно имеет.
Стоит еще отметить, что указание в команде описателя word ptr

mov word ptr ES:0,'!'

не вызовет ошибки трансляции, но приведет к неприятным результатам. В этом случае в видеобуфер будет записано слово 002lh, которое заполнит байт 0 видеобуфера кодом 21h, а байт 1 кодом 00h. Однако атрибут 00h обозначает черный цвет на черном фоне, и символ на экране виден не будет (хотя и будет записан в видеобуфер).
При желании можно избавиться от необходимости вводить описатель размера операнда. Для этого надо пересылать не непосредственное данное, а содержимое регистра:

mov AL,'!' mov ES:0,AL

Здесь операндом-источником служит регистр AL, размер которого (1 байт) известен, и размер операнда-приемника определять не надо. Разумеется, команда

mov ES:0,AX

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

mov AX,0B800h ;Сегментный адрес
mov DS,AX ; видеобуфера в DS
mov byte ptr DS:0, ' ! ' ;Символ в видеобуфер

Любопытно, что хотя обозначение DS: здесь необходимо, транслятор не включит в код команды префикс замены сегмента, так как команда без префикса выполняет адресацию по умолчанию через DS.
Если, однако, по умолчанию выполняется адресация через DS, то нельзя ли опустить в последней команде обозначение сегментного регистра? Нельзя, так как обозначение DS: число указывает, что число является не непосредственным операндом, а адресом операнда. Команда (неправильная)

mov 6,10

должна была бы переслать число 10 в число 6, что, разумеется, лишено смысла и выполнено быть не может. Команда же

mov DS:6,10

пересылает число 10 по относительному адресу 6, что имеет смысл. Таким образом, обозначение сегментного регистра с двоеточием перед операндом говорит о том, что операнд является адресом. В дальнейшем мы еще столкнемся с этим важным правилом.
Мы рассмотрели три важнейших способа адресации: регистровую, непосредственную и прямое обращение к памяти. Все остальные режимы адресации относятся к группе косвенной адресации памяти, когда в определении адреса ячейки памяти участвует один или несколько регистров процессора. Рассмотрим последовательно эти режимы.
Регистровая косвенная (базовая и индексная). Адресуется память (байт или слово). Относительный адрес ячейки памяти находится в регистре, обозначение которого заключается в прямые скобки. В МП 86 косвенная адресация допустима только через регистры ВХ, ВР, SI и DI. При использовании регистров ВХ или ВР адресацию называют базовой, при использовании регистров SI или DI - индексной.
Преобразуем приведенный выше пример, чтобы продемонстрировать использование косвенной адресации через регистр.

mov AX,0B800h ;Сегментный адрес
mov ES,AX ; видеобуфера в ES
mov BX,2000 ;Смещение к середине экрана
mov byte ptr ES:[ВХ], ' ! ' ;Символ на экран

Настроив ES, мы засылаем в регистр ВХ требуемое смещение (для разнообразия к середине видеобуфера, который имеет объем точно 4000 байт), и в последней команде засылаем код в видеобуфер с помощью косвенной базовой адресации через пару регистров ES:BX с указанием замены сегмента (ES:).
Если косвенная адресация осуществляется через один из регистров ВХ, SI или DI, то подразумевается сегмент, адресуемый через DS, поэтому при адресации через этот регистр обозначение DS: можно опустить:

mov AX,0B800h ;Сегментный адрес
mov DS,AX ;видеобуфера в DS
mov BX,2000 ;Смещение к середине экрана
mov byte ptr [ВХ], ' ! ' ;Символ на экран

Кстати, этот фрагмент немного эффективнее предыдущего в смысле расходования памяти. Из-за отсутствия в коде последней команды префикса замены сегмента он занимает на 1 байт меньше места.
Регистры ВХ, SI и DI в данном применении совершенно равнозначны, и с одинаковым успехом можно воспользоваться любым из них:

mov D1,2000 ;Смещение к середине экрана
mov byte ptr [DI] , ' ! ' ;Символ на экран

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

mov BX,2000 ;Смещение к середине экрана
mov byte ptr ES: [BX] , ' ! ' ;Символ на экран

можно использовать одну

mov byte ptr ES:2000,'!' ;Выведем символ в середину экрана

Однако команда с базовой адресацией занимает меньше места в памяти (так как в нее не входит адрес ячейки) и выполняется быстрее команды с прямой адресацией (из-за того, что команда короче, процессору требуется меньше времени на ее считывание из памяти). Поэтому базовая адресация эффективна в тех случаях, когда по заданному адресу приходится обращаться многократно, особенно, в цикле. Выигрыш оказывается тем больше, чем большее число раз происходит обращение по указанному адресу. С другой стороны, возможности этого режима адресации невелики, и на практике чаще используют более сложные способы, которые будут рассмотрены ниже.
Регистровая косвенная адресация со смещением (базовая и индексная). Адресуется память (байт или слово). Относительный адрес операнда определяется, как сумма содержимого регистра BX, BP, SI или DI и указанной в команде константы, иногда называемой смещением. Смещение может быть числом или адресом. Так же, как и в случае базовой адресации, при использовании регистров BX, SI и DI подразумевается сегмент, адресуемый через DS, а при использовании ВР подразумевается сегмент стека и, соответственно, регистр SS.
Рассмотрим применение косвенной адресации со смещением на примере прямого вывода в видеобуфер.

mov AX,0B800h ;Сегментный адрес
mov ES,AX ;видеобуфера в ES
mov DI, 80*2*24 ;Смещение к нижней строке экрана
mov byte ptr ES: [DI] ,'О' ;Символ на экран
mov byte ptr ES:2[DI],'К' ;Запишем символ в следующую позицию
mov byte ptr ES:4[DI],' ! ' ;Запишем символ в следующую позицию

В этом примере в качестве базового выбран регистр DI; в него заносится базовый относительный адрес памяти, в данном случае смещение в видеобуфере к началу последней строки экрана. Модификация этого адреса с целью получить смещение по строке экрана осуществляется с помощью констант 2 и 4, которые при вычислении процессором исполнительного адреса прибавляются к содержимому базового регистра DI.
Иногда можно встретиться с альтернативными обозначениями того же способа адресации, которые допускает ассемблер. Вместо, например, 4[ВХ] можно с таким же успехом написать [ВХ+4], 4+[ВХ] или [ВХ]+4. Такая неоднозначность языка ничего, кроме путаницы, не приносит, однако ее надо иметь в виду, так как с этими обозначениями можно столкнуться, например, рассматривая текст деассемблированной программы.
Рассмотрим теперь пример использования базовой адресации со смещением при обращении к стеку:

;Основная программа
push DS ;В стек загружаются значения
push ES ;трех регистров,
push SI ;передаваемых подпрограмме
call mysub ;Вызов подпрограммы mysub,
;использующей эти параметры
;Подпрограмма mysub
mov BP,SP ;Поместим в ВР текущий адрес вершины стека
mov АХ,2[ВР], ;Читаем в АХ последний параметр (SI)
mov ВХ,4[ВР] ;Читаем в ВХ предыдущий параметр (ES)
mov CX,6[BP] ;Читаем в СХ первый параметр (DS)

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

Рис.2.15. Состояние стека после загрузки в него трех параметров и перехода на подпрограмму

Если бы подпрограмма просто сняла со стека находящиеся там параметры, она первым делом изъяла бы из стека адрес возврата, и лишила бы себя возможности вернуться в основную программу (подробнее вопросы вызова подпрограммы и возврата из нее будут обсуждаться в последующих разделах). Поэтому в данном случае вместо команд pop удобнее воспользоваться командами mov. Подпрограмма копирует в ВР содержимое трех параметров и перехода на мое SP и использует затем этот адрес в качестве базового, модифицируя его с помощью базовой адресации со смещением.
Кстати, мы опять сталкиваемся здесь с той весьма обычной ситуацией, когда программист не имеет возможности обращаться по наглядным символическим адресам, которых в стеке, естественно, нет, а вынужден определять "вручную" смещения к интересующим его элементам стека. При этом необходимо учесть и алгоритм выполнения команды call, которая, сохраняя в стеке адрес возврата в основную программу, смещает указатель стека еще на одно слово.
В нашем фрагментарном примере мы не рассматриваем вопрос возврата в основную программу. Вдумчивый читатель мог также усомниться в правильности или, лучше сказать, в разумности текста подпрограммы. Ведь перенося параметры из стека в регистры общего назначения, подпрограмма затирает их исходное содержимое. Если же они не содержали ничего нужного, то ими можно было воспользоваться для передачи параметров в подпрограмму, а не связываться с мало наглядными операциями со стеком. Действительно, ради краткости мы опустили операции, практически необходимые в любой подпрограмме - сохранение в стеке (опять в стеке!) тех регистров, которые будут использоваться в подпрограмме. Кстати, это относится и к регистру ВР. В реальной подпрограмме эти действия следовало выполнить, что привело бы к изменению смещений при регистре ВХ, которые приняли бы значения (с учетом сохранения 4 регистров) 10, 12 и 14.
Во всех приведенных выше примерах регистр использовался для хранения базового адреса, а смещение, если оно требовалось, указывалось в виде константы. Возможна и обратная ситуация, когда в качестве смещения выступает адрес массива, а в регистре находится индекс адресуемого элемента в этом массиве. Рассмотрим относительно реальный пример такого рода.
Пусть нам надо заполнить массив из 10000 слов натуральным рядом чисел. Зарезервируем в сегменте данных место под этот массив, а в сегменте команд организуем цикл занесения в последовательные слова массива ряда нарастающих чисел. Нам придется воспользоваться несколькими новым командами (inc, add и loop), которые в дальнейшем будут рассмотрены более подробно.

;Сегмент данных
array dw 10000
;Сегмент команд
mov SI, 0 ;Начальное значение индекса элемента в массиве
mov АХ, 0 ;Первое число-заполнитель
mov СХ,10000;Число шагов в цикле (всегда в СХ)
fill: mov array[SI],AX ;Занесение числа в элемент массива
inc AX ;Инкремент числа-заполнителя
add SI,2 ;Смещение в массиве к следующему слову
loop fill ;Возврат на метку fill (СХ раз)

Цикл начинается с команды, помеченной меткой fill (правила образования имен меток такие же, как и для имен полей данных). В этой команде содержимое АХ, поначалу равное 0, переносится в ячейку памяти, адрес которой вычисляется, как сумма адреса массива array и содержимого индексного регистра SI, в котором в первом шаге никла тоже 0. В результате в первое слово массива заносится 0. Далее содержимое регистра АХ увеличивается на 1, содержимое регистра SI - на 2 (из-за того, что массив состоит из слов), и командой loop осуществляется переход на метку fill, после чего тело цикла повторяется при новых значениях регистров АХ и SI. Число шагов в цикле, отсчитываемое командой loop, определяется исходным содержимым регистра СХ.
Базово-индексная адресация. Адресуется память (байт или слово). Относительный адрес операнда определяется, как сумма содержимого следующих пар регистров:

[ВХ] [SI] (подразумевается DS:[BX][SI])
[ВХ][DI] (подразумевается DS:[BX][DI])
[ВР] [SI] (подразумевается SS:[BP][SI])
[ВР] [DI] (подразумевается SS:[BP][DI])

Это чрезвычайно распространенный способ адресации, особенно, при работе с массивами. В нем используются два регистра, при этом одним из них должен быть базовый (ВХ или ВР), а другим - индексный (SI или DI). Как правило, в одном из регистров находится адрес массива, а в другом - индекс в нем, при этом совершенно безразлично, в каком что. Трансформируем предыдущий пример, введя в него более эффективную базово-индексную адресацию.

;Сегмент данных
array dw 10000
;Сегмент команд
mov BX,offset array ;Базовый адрес массива в
;базовом регистре
mov SI, 0 ;Начальное значение индекса
;элемента в массиве
mov АХ, 0 ;Первое число-заполнитель
mov CX,10000 ;Число шагов в цикле
fill: mov [BX][SI],AX ;Отправим число в массив
inc AX ;Инкремент числа-заполнителя
add SI, 2 ;Смещение в массиве к следующему слову
loop fill ;На метку fill (CX раз)

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

sims db "QWERTYUIOP{}'
db "ЙЦУКЕНПШЦЗХЪ'

Последовательность команд
mov BX,12 ;Число байтов в строке
mov SI, 6
mov DL,syms[BX][SI]

загрузит в регистр DL элемент с индексом 6 из второго ряда, т.е. код ASCII буквы Г. Тот же результат можно получить, загрузив в один из регистров не индекс, а адрес массива:

mov BX, off set sym
mov SI,6
mov DL, 12 [BX] [SI]

2.6. Переходы
Как уже отмечалось в гл. 1, присущий процессору алгоритм выполнения программы заставляет его выполнять команды программы друг за другом, в том порядке, как они были описаны в исходном тексте программы и содержатся в выполнимом модуле. Однако часто программисту требуется нарушить этот порядок, заставив процессор обойти некоторый участок программы, перейти на выполнение другой ветви или передать управление подпрограмме, имея в виду после ее завершения вернуться на прежнее место. Все эти операции осуществляются с помощью команд переходов. Переходы разделяются на безусловные, когда передача управления в другую точку программы осуществляется в безусловном порядке, независимо ни от каких обстоятельств, и условные, осуществляемые или не осуществляемые в зависимости от тех или иных условий: результатов сравнения, анализа, поиска и т.п. Безусловные переходы подразделяются на собственно переходы (без возврата в точку перехода) и вызовы подпрограмм (с возвратом после завершения подпрограммы).
Операции переходов и вызовов подпрограмм, помимо их практической ценности, представляют значительный методический интерес, так как затрагивают основу архитектуры процессора - сегментную адресацию памяти. Многочисленные разновидности команд переходов и вызовов обязаны своим существованием не столько потребностям практического программирования, сколько принципиальным архитектурным особенностям процессора. Отчетливое понимание этих особенностей и, соответственно, условий применения и возможностей различных операций переходов необходимо не только при использовании языка ассемблера, но и при программировании на языках высокого уровня, где иногда используется несколько иная терминология, но существо дела остается без изменения.
Безусловные переходы осуществляются с помощью команды jmp, которая может использоваться в 5 разновидностях. Переход может быть:
прямым коротким (в пределах -128... + 127 байтов);
прямым ближним (в пределах текущего сегмента команд):
прямым дальним (в другой сегмент команд);
косвенным ближним (в пределах текущего сегмента команд через ячейку
с адресом перехода);
косвенным дальним (в другой сегмент команд через ячейку с адресом
перехода).
Рассмотрим последовательно структуру программ с переходами разного вида.
Прямой короткий (short) переход. Прямым называется переход, в команде которого в явной форме указывается метка, на которую нужно перейти. Разумеется, эта метка должна присутствовать в том же программном сегменте, при этом помеченная ею команда может находиться как до, так и после команды jmp. Достоинство команды короткого перехода заключается в том, что она занимает лишь 2 байт памяти: в первом байте записывается код операции (EBh), во втором - смещение к точке перехода. Расстояние до точки перехода отсчитывается от очередной команды, т.е. команды, следующей за командой jmp. Поскольку требуется обеспечить переход как вперед, так и назад, смещение рассматривается, как число со знаком и, следовательно, переход может быть осуществлен максимум на 127 байт вперед или 128 байт назад. Прямой короткий переход оформляется следующим образом:

code segment
…
jmp short go ;Код ЕВ dd
go:
…
code ends

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

tasm /m2 p,p,p

то описатель short можно опустить, так как ассемблер сам определит, что расстояние до точки перехода укладывается в короткий переход, даже если метка go расположена после строки с командой jmp. При использовании транслятора MASM указание описателя short обязательно (если метка go расположена после команды jmp). Здесь проявляются незначительные различия ассемблеров разных разработчиков.
В комментарии указан код команды; dd (от displacement, смещение) обозначает байт со смещением к точке перехода от команды, следующей за командой jmp.
При выполнении команды прямого короткого перехода процессор прибавляет значение байта dd к младшему байту текущего значения указателя команд IP (который, как уже говорилось, всегда указывает на команду, следующую за выполняемой). В результате в IP оказывается адрес точки перехода, а предложения, находящиеся между командой jmp и точкой перехода, не выполняются. Между прочим, конструкция с прямым переходом вперед часто используется для того, чтобы обойти данные, которые по каким-то причинам желательно разместить в сегменте команд.
Прямой ближний (near), или внутрисегментный переход. Этот переход отличается от предыдущего только тем, что под смещение к точке перехода отводится целое слово. Это дает возможность осуществить переход в любую точку 64-кбайтного сегмента.

code segment
…
jmp go ;Код Е9 dddd
…
go:
…
code ends

Метка go может находиться в любом месте сегмента команд, как до, так и после команды jmp. В коде команды dddd обозначает слово с величиной относительного смещения к точке перехода от команды, следующей за командой jmp.
При выполнении команды прямого ближнего перехода процессор должен прибавить значение слова dddd к текущему значению указателя команд IP и сформировать тем самым адрес точки перехода. Что представляет собой смещение ddddl Какая это величина, со знаком или без знака? Если рассматривать смещение как величину без знака, то переход будет возможен только вперед, что, конечно, неверно. Если же смещение является величиной со знаком, то переход возможен не более, чем на полсегмента вперед или на полсегмента назад, что тоже неверно. В действительности, рассматривая вычисление адреса точки перехода, следует иметь в виду явление оборачивания, суть которого можно кратко выразить такими соотношениями:

FFFFh+0001h=0000h
0000h-0001h=FFFFh

Если последовательно увеличивать содержимое какого-либо регистра или ячейки памяти, то, достигнув верхнего возможного предела FFFFh, число "перевалит" через эту границу, станет равным нулю и продолжит нарастать в области малых положительных чисел (1, 2, 3, и т.д.). Точно так же, если последовательно уменьшать некоторое положительное число, то оно, достигнув нуля, перейдет в область отрицательных (или, что то же самое, больших беззнаковых) чисел, проходя значения 2, 1, 0, FFFFh, FFFEh и т.д.
Таким образом, при вычислении адреса точки перехода смещение следует считать числом без знака, но при этом учитывать явление оборачивания. Если команда jmp находится где-то в начале сегмента команд, а смещение имеет величину порядка 64 К, то переход произойдет вперед, к концу сегмента. Если же команда находится в конце сегмента команд, а смещение имеет ту же величину порядка 64 К, то для определения точки перехода надо двигаться по сегменту вперед, дойти до его конца и продолжать перемещаться от начала сегмента по-прежнему вперед, пока не будет пройдено заданное в смещении число байтов. Для указанных условий мы попадем в точку, находящуюся недалеко от команды jmp со стороны меньших адресов.
Итак, с помощью команды ближнего перехода (команда jmp без каких-либо спецификаторов) можно перейти в любую точку в пределах данного сегмента команд. Для того, чтобы перейти в другой сегмент команд, следует воспользоваться командой дальнего перехода.
Прямой дальний (far), или межсегментный переход. Этот переход позволяет передать управление в любую точку любого сегмента. При этом, очевидно, предполагается, что программный комплекс включает несколько сегментов команд. Команда дальнего перехода имеет длину 5 байт и включает, кроме кода операции EAh, еще и полный адрес точки перехода, т.е. сегментный адрес и смещение. Транслятору надо сообщить, что этот переход - дальний (по умолчанию команда jmp транслируется, как команда ближнего перехода). Это делается с помощью описателя far ptr, указываемого перед именем точки перехода.

codel segment
assume CS: codel ;Сообщим транслятору, что это сегмент команд
…
jmp far ptr go ;Код EA dddd ssss
…
codel ends
code2 segment
assume CS : code2 ; Сообщим транслятору, что это сегмент команд
…
gо:
…
code2 ends

Метка go находится в другом сегменте команд этой двухсегментной программы. В коде команды ssss - сегментный адрес сегмента code2, a dddd - смещение точки перехода go в сегменте команд code2.
Заметим, что при наличии в программе нескольких сегментов команд, каждый из них необходимо предварять директивой ассемблера assume СS:имя_сегмента, которая сообщает транслятору о том, что начался очередной сегмент команд. Это поможет транслятору правильно обрабатывать адреса меток, встречающихся в этом сегменте.
Освоив применение команд дальних переходов, мы получили возможность создавать программы любой длины. Действительно, предусмотрев в конце каждого программного сегмента команду дальнего перехода на начато следующего, мы можем включить в программный комплекс любое число сегментов по 64 Кбайт. Единственное ограничение - чтобы они все поместились в памяти. В действительности так, конечно, не делают. Разумнее дополнительные сегменты команд заполнить подпрограммами и вызывать их из основного сегмента (или друг из друга) по мере необходимости. Однако и в этом случае команды вызовов подпрограмм должны быть дальними. Разновидности подпрограмм и команд их вызова будут рассмотрены ниже.
Все виды прямых переходов требуют указания в качестве точки перехода программной метки. С одной стороны, это весьма наглядно; просматривая текст программы, можно сразу определить, куда осуществляется переход. С другой стороны, такой переход носит статический характер - его нельзя настраивать по ходу программы. Еще более серьезный недостаток прямых переходов заключается в том, что они не дают возможность перейти по известному абсолютному адресу, т.е. не позволяют обратиться ни к системным средствам, ни вообще к другим загруженным в память программам (например, резидентным). Действительно, программы операционной системы не имеют никаких меток, так как метка - это атрибут исходного текста программы, а программы операционной системы транслировались не нами и присутствуют в компьютере только в виде выполнимых модулей. А вот адреса каких-то характерных точек системных программ определить можно, хотя бы из векторов прерываний. Для обращения по абсолютным адресам надо воспользоваться командами косвенных переходов, которые, как и прямые, могут быть ближними и дальними.
Косвенный ближний (внутрисегментный) переход. В отличие от команд прямых переходов, команды косвенных переходов могут использовать различные способы адресации и, соответственно, иметь много разных вариантов. Общим для них яштястся то, что адрес перехода не указывается явным образом в виде метки, а содержится либо в ячейке памяти, либо в одном из регистров. Это позволяет при необходимости модифицировать адрес перехода, а также осущестштять переход по известному абсолютному адресу. Рассмотрим случай, когда адрес перехода хранится в ячейке сегмента данных. Если переход ближний, то ячейка с адресом состоит из одного слова и содержит только смещение к точке перехода.

code segment
…
jmp DS:go_addr ;Код FF 26 dddd
…
go: ; Точка перехода
…
code ends
data segment
…
go_addr dw go ;Адрес перехода (слово)
…
data ends

Точка перехода go может находиться в любом месте сегмента команд. В коде команды dddd обозначает относительный адрес слова go_addr в сегменте данных, содержащем эту ячейку.
В приведенном фрагменте адрес точки перехода в слове go_addr задан однозначно указанием имени метки go. Такой вариант косвенного перехода выполняет фактически те же функции, что и прямой (переход по единственному заданному заранее адресу), только несколько более запутанным образом. Достоинства косвенного перехода будут более наглядны, если представить, что ячейка go_addr поначалу пуста, а по ходу выполнения программы в нес, в зависимости от каких-либо условий, помещается адрес той или иной точки перехода:

mov go_addr, offset gol
mov go_addr, offset go2
mov go_addr, offset go3

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

jmp DS:go_addr
jmp word ptr go_addr
jmp go_addr

В первом варианте, использованном в приведенном выше фрагменте, указано, через какой сегментный регистр надлежит обращаться к ячейке go_addr, содержащей адрес перехода. Здесь допустима замена сегмента, если сегмент с ячейкой go_addr адресуется через другой сегментный регистр, например, ES или CS.
Во втором варианте подчеркивается, что переход осуществляется через ячейку размером в одно слово и, следовательно, является ближним. Ячейка go_addr могла быть объявлена с помощью директивы dd и содержать полный двухсловный адрес перехода, требуемый для реализации дальнего перехода. Однако ею можно воспользоваться и для ближнего перехода. Описатель word ptr перед именем ячейки с адресом перехода засташшет транслятор считать, что она имеет размер 1 слово (независимо от того, как она была объявлена), и что переход, следовательно, является ближним.
Наконец, возможен и самый простой, третий вариант, который совпадает по форме с прямым переходом, но, тем не менее, является косвенным, так как символическое обозначение go_addr является именем поля данных, а не программной меткой. В этом варианте предполагается, что сегмент, в котором находится ячейка go_addr, адресуется по умолчанию через регистр DS, хотя, как и во всех таких случаях, допустима замена сегмента. Тип перехода (ближний или дальний) определяется, исходя из размера ячейки go_addr. Однако этот вариант не всегда возможен. Для его правильной трансляции необходимо, чтобы транслятору к моменту обработки предложения

jmp go_addr

было уже известно, что собой представляет имя go_addr. Этого можно добиться двумя способами. Первый - расположить сегмент данных до сегмента команд, а не после, как в приведенном выше примере. Второй - заставить транслятор обрабатывать исходный текст программы не один раз, как он это делает по умолчанию, а несколько. Число проходов для транслятора TASM можно задать при его вызове с помощью ключа /m#, где # - требуемое число проходов. В нашем случае достаточно двух проходов.
В приведенных примерах адрес поля памяти с адресом точки перехода задавался непосредственно в коде команды косвенного перехода. Однако этот адрес можно задать и в одном из регистров общего назначения (ВХ, SI или DI). Для приведенного выше примера косвенного перехода в точку go, адрес которой находится в ячейке go_addr в сегменте данных, перс-ход с использованием косвенной регистровой адресации может выглядеть следующим образом:

mov BX, offset go_addr ;В ВХ смещение поля с адресом перехода
jmp [BX] ;Переход в точку gо

Особенно большие возможности предоставляет методика косвенного перехода с использованием базово-индексной адресации через пары регистров, например, [BX][SI] или [BX][DI]. Этот способ удобен в тех случаях, когда имеется ряд альтернативных точек перехода, выбор которых зависит от некоторых условий. В этом случае в сегменте данных создается не одно поле с адресом, а таблица адресов переходов. В базовый регистр ВХ загружается адрес этой таблицы, а в один из индексных регистров - определенный тем или иным способом индекс в этой таблице. Переход осуществляется в точку, соответствующую заданному индексу. Структура программы, использующий такую методику, выглядит следующим образом:

code segment
mov BX, off set go_tbl ;Загружаем в ВХ базовый адрес таблицы
mov SI, 4 ;Вычисленное каким-то
;образом смещение в таблице
jmp [BX] [SI] ;Если индекс =4, переход в точку goЗ
…
gol: ;1-я точка перехода
…
gо2 : ;2-я точка перехода
…
gоЗ: ;3-я точка перехода
…
code ends
data segment
go_tbl label word ;Таблица адресов переходов
gol_addr dw gol ;Адрес первой альтернативной
;точки перехода
go2_addr dw go2 ;Адрес второй альтернативной
;точки перехода
go3_addr dw доЗ ;Адрес третьей альтернативной
;точки перехода
data ends

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

mov BX, off set gol jmp BX

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

codel segment
assume CS:codel,DS:data
…
jmp DS:go_addr ; Код FF 2E dddd
…
codel ends
code2 segment
assume CS:code2
…
go: ;Точка перехода в другом сегменте команд
…
code2 ends
data segment
…
go_addrdd go ;Двухсловный адрес точки перехода
…
data ends

Точка перехода go находится в другом сегменте команд этой двухсегментной программы. В коде команды dddd обозначает относительный адрес слова go_addr в сегменте данных. Ячейка go_addr объявляется директивой dd (define double, определить двойное слово) и содержит двухсловный адрес точки перехода; в первом слове содержится смещение go в сегменте команд codel, во втором слове сегментный адрес codel. Оба компонента адреса перехода могут быть вычислены и помещены в ячейку go_addr по ходу выполнения программы.
Как и в случае ближнего косвенного перехода, ассемблер допускает различные формы описания дальнего косвенного перехода через ячейку сегмента данных:

jmp DS:go_addr ;Возможна замена сегмента
jmp dword ptr go_addr ;Если поле go_addr объявлено
;операторами dw
jmp go_addr ;Характеристики ячейки должны
;быть известны

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

mov BX,offset go_addr
jmp [BX]

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

2.7. Вызовы подпрограмм

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

drawline proc ;Подпрограмма-процедура
. . . ;Тело подпрограммы
ret ;Команда возврата в вызывающую программу
drawline endp

С таким же успехом можно обойтись без процедуры, просто пометив первую строку программы некоторой меткой:

drawline: ;Подпрограмма, начинающаяся с метки
. . . ;Тело подпрограммы
ret ;Команда возврата в вызывающую программу
. . . ;Продолжение основной программы или
;другие подпрограммы

В любом случае вызов подпрограммы осуществляется командой call. Подпрограмма должна завершаться командой ret, служащей для возврата управления в ту точку, откуда подпрограмма была вызвана.
Вопросы использования подпрограмм, передачи в них параметров и возвращения результата будут рассмотрены в следующей главе. Здесь мы остановимся только на таких принципиальных архитектурных вопросах, как механизм выполнения и возможности команд call и ret. При этом надо иметь в виду, что синтаксические особенности и закономерности использования команд call и jmp во многом совпадают, и значительная часть пояснений к командам перехода справедлива и для команд вызова.
Команда вызова подпрограммы call может использоваться в 4 разновидностях. Вызов может быть:
прямым ближним (в пределах текущего сегмента команд);
прямым дальним (в другой сегмент команд);
косвенным ближним (в пределах текущего сегмента команд через ячейку с адресом перехода);
косвенным дальним (в другой сегмент команд через ячейку с адресом
перехода).
Рассмотрим последовательно перечисленные варианты.
Прямой ближний вызов. Как и в случае прямого ближнего перехода, в команде прямого вызова в явной форме указывается адрес (смещение) точки входа в подпрограмму; в качестве этого адреса можно использовать как имя процедуры, так и имя метки, характеризующей точку входа в подпрограмму. В код команды, кроме кода операции E8h, входит смещение к вызываемой подпрограмме. В приведенном ниже примере подпрограмма оформлена в виде процедуры.

code segment
main proc ;Основная программа
…
call sub ;Код Е8 dddd
…
main endp
sub proc near ;Подпрограмма
…
ret ;Код СЗ
sub endp
code ends

Процедура-программа находится в том же сегменте команд, что и вызывающая программа. В коде команды dddd обозначает смещение в сегменте команд к точке входа в подпрограмму. При выполнении команды call процессор помещает адрес возврата (содержимое регистра IP) в стек выполняемой программы (рис. 2.16), после чего к текущему содержимому IP прибавляет dddd. В результате в IP оказывается адрес подпрограммы. Команда ret, которой заканчивается подпрограмма, выполняет обратную процедуру - извлекает из стека адрес возврата и заносит его в IP.

Рис. 2.16. Участие стека в механизме вызова ближней подпрограммы.

Участие стека в механизме вызова подпрограммы и возврата из нее является решающим. Поскольку в стеке хранится адрес возврата, подпрограмма, сама используя стек, например, для хранения промежуточных результатов, обязана к моменту выполнения команды ret вернуть стек в исходное состояние. Команда ret, естественно, никак не анализирует состояние или содержимое стека. Она просто снимает со стека верхнее слово, считая его адресом возврата, и загружает это слово в указатель команд IP. Если к моменту выполнения команды ret указатель стека окажется смещенным в ту или иную сторону, команда ret по-прежнему будет рассматривать верхнее слово стека, как адрес возврата, и передаст по нему управление, что неминуемо приведет к краху системы.
Прямой дальний вызов. Этот вызов позволяет обратиться к подпрограмме из другого сегмента. В код команды, кроме кода операции 9Ah, входит полный адрес (сегмент плюс смещение) вызываемой подпрограммы. Обычно в исходном тексте программы с помощью описателя far ptr указывается, что вызов является дальним, хотя, если транслятор настроен на трансляцию в два прохода, этот описатель не обязателен. Структура программного комплекса, содержащая дальний вызов подпрограммы, может выглядеть следующим образом:

codel segment
assume CS:codel
main proc ;Основная программа
call far ptr subr ; Код 9А dddd ssss
…
main endp
codel ends
code2 segment
assume CS:code2
subr proc far ;Объявляем подпрограмму дальней
…
ret ;Код СВ - дальний возврат
subr endp
code2 ends

Процедура-подпрограмма находится в другом сегменте команд той же программы. В коде команды dddd обозначает относительный адрес точки входа в подпрограмму в ее сегменте команд, a ssss - се сегментный адрес. При выполнении команды call процессор помещает в стек сначала сегментный адрес вызывающей программы, а затем относительный адрес возврата (рис. 2.17). Далее в сегментный регистр CS заносится 5555 (у нас это значение code2), а в IP - dddd (у нас это значение subr). Поскольку процедура-подпрограмма атрибутом far объявлена дальней, команда ret имеет код, отличный от кода аналогичной команды ближней процедуры и выполняется по-другому: из стека извлекаются два верхних слова и переносятся в IP и CS, чем и осуществляется возврат в вызывающую программу, находящуюся в другом сегменте команд. В языке ассемблера существует и явное мнемоническое обозначение команды дальнего возврата - retf.

Рис. 2.17. Участие стека в механизме вызова дальней подпрограммы.

Косвенный ближний вызов. Адрес подпрограммы содержится либо в ячейке памяти, либо в регистре. Это позволяет, как и в случае косвенного ближнего перехода, модифицировать адрес вызова, а также осуществлять вызов не с помощью метки, а по известному абсолютному адресу. Структура программы с косвенным вызовом подпрограммы может выглядеть следующим образом:
code segment
main proc ;Основная программа
…
call DS:subadr ;Код FF 16 dddd
main endp
subr proc near ;Подпрограмма
…
ret ;Код СЗ
subr endp
code ends
data segment
…
subadr dw subr ;Яейка с адресом подпрограммы
data ends

Процедура-программа с атрибутом near находится в том же сегменте, что и вызывающая программа, а ее относительный адрес в ячейке subadr в сегменте данных. В коде команды dddd обозначает относительный адрес слова subadr в сегменте данных. Второй байт кода команды (16h в данном примере) зависит от способа адресации. Косвенный вызов позволяет использовать разнообразные способы адресации подпрограммы:

call BX ; В ВХ адрес подпрограммы
call[BX] ; В ВХ адрес ячейки с адресом подпрограммы
call[BX][SI] ;В ВХ адрес таблицы адресов подпрограмм,
;в SI индекс в этой таблице.
tbl[SI] ;tbl - адрес таблицы адресов подпрограмм,
;в SI индекс в этой таблице

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

codel segment
main proc ;Основная программа
call dword ptr subadr ;Код FF IE dddd
…
main endp
codel ends
code2 segment
subr proc far ;Подпрограмма
…
ret ;Код СВ
subr endp
code2 ends
data segment
…
subadr dd subr ;Двухсловная ячейка с
;адресом подпрограммы
data ends

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

2.8. Макросредства ассемблера

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

sym='A' ;Начальное значение временной переменной
symbols: ;Имя массива для ссылок на него
rept 32 ;Повторять столько раз
db sym ;Повторяемая директива
sym=sym+l ;Изменение переменной
endm ;Конец блока повторения

Как видно из приведенного фрагмента, блок повторения начинается с директивы ассемблера rept (от repetition, повторение), а заканчивается директивой endm (end macro, конец макроса). Реально в сегменте данных выделяется 32 байт, заполненных числами от 81h до 9Fh, которые предполагается рассматривать, как последовательность русских букв. Того же результата можно было достигнуть с помощью следующего предложения:

symbols db "А", "Б", "В", "Г", и т.д. до буквы Я
или проще, хотя и менее наглядно:

symbols db 128,129,130,131, и т.д. до числа 159.
Макрос повторения несколько сокращает время, требуемое для описания в тексте программы требуемого массива, хотя, возможно, снижает наглядность этого описания.
При подключении к компьютеру измерительного или управляющего оборудования иногда возникает необходимость замедлить работу процессора при обращении к портам этого оборудования. Замедление осуществляется включением в текст программы одной или, если требуется, нескольких команд безусловного перехода на следующее предложение:

in AL,300h ;Первое обращение к оборудованию
jmp a ;Задержка на время
a: jmp b ;выполнения
b: jmp с ;трех команд jmp
c: in AL,301h ;Следующее обращение к оборудованию

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

in AL, 300h ;Первое обращение к оборудованию
jmp $+2 ;Задержка на время
jmp $+2 ;выполнения
jmp $+2 ;трех команд jmp
in AL,301h ;Следующее обращение к оборудованию

Здесь используется обозначение счетчика текущего адреса S. При трансляции любой команды в счетчике текущего адреса содержится адрес этой команды (смещение ее первого байта). Команда короткого перехода занимает 2 байт, поэтом}' команда jmp $+2 осуществляет переход на команду, идущую следом.
Часто в подобных случаях ограничиваются одной командой jmp, которая создает необходимую задержку в доли микросекунды. В тех случаях, однако, когда устройство сопряжения с оборудованием работает заметно медленнее процессора, приходится включать между командами обращения к портам 5-6 команд jmp. Такой фрагмент можно оформить в виде блока повторения:

rept 6 jmp $+2 endm

Это, пожатуй, проще, чем писать 6 команд jmp.
Макросы повторения имеют несколько разновидностей, которые мы не будем здесь рассматривать.
Макрокоманды
Программы, написанные на языке ассемблера, часто содержат повторяющиеся участки текста с одинаковой структурой. Такой участок текста можно оформить в виде макроопределения, характеризующегося произвольным именем и необязательным списком формальных аргументов. После того, как такое определение сделано, появление в программе строки, содержащей имя макроопределения и список фактических аргументов (все это вместе называют макрокомандой), приводит к генерации всего требуемого текста, называемого макрорасширением. Варьируя фактические аргументы, можно, сохраняя неизменной структуру макрорасширения, изменить отдельные его элементы.
Макроопределение должно начинаться строкой с именем макроопределения и директивой macro, в поле аргументов которой указывается список формальных аргументов. Заканчивается макроопределение директивой endm.
Пусть в программе требуется неоднократно сохранять в стеке содержимое трех регистров, но в каждом конкретном случае номера регистров и их порядок отличаются. Оформим эти действия в виде макроопределения:

psh macroa,b,c

push a
push b
push с
endm

Появление в исходном тексте программы строки

psh АХ, ВХ, СХ

приведет к генерации следующего фрагмента текста:

push AX push BX push CX

Если же в исходном тексте имеется строка

psh DX, ES, ВР

то соответствующее макрорасширение будет иметь вид:

push DX
push ES
push BP

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

psh mem, [BX], ES: [17h]

приведет к следующему макрорасширению:

push mem
push [BX]
push ES : [17h]

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

delay macro
local point
mov CX,200
point: loop point
endm

Макрос delay создает задержку фиксированной длительности. Если в текст программы включить две макрокоманды delay
…
delay
…
delay

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

…
mov CX, 20000
??0000: loop ??0000
…
mov CX, 20000
??0000: loop ??0000

При повторных подстановках макроопределения транслятор заменяет обозначение метки point на различающиеся обозначения ??0000, ??0001 и т.д., обеспечивая тем самым правильное выполнение команд циклов и переходов.
Макрокоманды схожи с подпрограммами в том отношении, что в обоих случаях мы описываем некоторый программный фрагмент один раз, а обращаемся к нему многократно, возможно, с передачей различных параметров. Однако эти вычислительные средства различаются как по способу использования, так и по своим возможностям.
Подпрограммы позволяют сократить объем выполнимого файла за счет описания повторяющихся участков программы лишь однажды. При каждом вызове подпрограммы командой call происходит переход на один и тот же фрагмент программы, содержащий подпрограмму, а после выполнения подпрограммы - возврат назад в точку вызова. Текст подпрограммы полностью определяется на этапе ее написания, и изменения в ходе выполнения подпрограммы возможны только за счет передачи ей тех или иных конкретных значений.
Механизм использования макроса иной. Каждая макрокоманда, встретившаяся транслятору в тексте программы, заменяется им на полный текст макроопределения. Если макрокоманда содержит параметры, то в процессе этой замены происходит подстановка параметров в текст макроопределения. Образованное таким образом макрорасширение составляет часть текста программы, неотличимо от остальных предложений программы и не нуждается в каких-либо вызовах. В силу этих обстоятельств макрокоманды оказываются несколько эффективнее подпрограмм по скорости выполнения, особенно, если учесть время, требуемое для подготовки параметров перед вызовом подпрограммы (например, проталкивание их в стек). Вряд ли стоит, однако, проводить такое сравнение. Подпрограммы и макрокоманды имеют различные области применения.
Подпрограммы служат для сокращения объема программы, повышения ее наглядности и упрощения перестройки алгоритма выполнения всего программного комплекса путем изменения состава и порядка вызываемых подпрограмм. При этом активное использование подпрограмм может уменьшить размер всей программы в десятки раз.
Смысл использования макрокоманд совсем иной. Макрокоманды позволяют упростить процесс написания программы и, можно сказать, являются средством автоматизации программирования. При этом язык макрокоманд предоставляет большие возможности по изменению текста макрорасширения в зависимости от указываемых в макрокоманде параметров. Проиллюстрируем эти возможности на простом примере макрокоманды вывода на экран символа. Такой макрокомандой можно пользоваться в процессе отладки сложных программ, чтобы получать информацию о содержимом любых ячеек памяти. Пример оформлен в виде законченной программы, которая носит чисто демонстрационный характер.

;Пример 2-1. Использование макрокоманды
sym macroc ;Имя и формальный аргумент
push AX ;Сохраним используемые
push DX ;в макроопределении регистры
mov АН, 02h ;Функция DOS вывода символа
mov DL,c ;Заберем символ
int 21h ;Вызов DOS
pop DX ;Восстановим
pop AX ;регистры
endm ;Конец макроопределения
code segment
assume cs:code
main proc
sym 'w' ;Символ указан непосредственно
sym ES : 0 ;Вывод первого байта PSP
sym CS:msg ;Вывод первой буквы из msg
lea BX,msg-t-l ;Адрес второй буквы из msg
sym [BX] ;Вывод второй буквы
mov AX, 40h ;Настроим DS
mov DS,AX ;на начало памяти
sym DS:49h ;Вывод номера видеорежима
mov AX,4C00h ;Завершение программы
int 21h
main endp
msg db 'OK'
code ends

Тексты макроопределений обычно размещаются в самом начале программы, что дает возможность вызывать макрокоманды из любых точек программы. Содержательная часть макроса sym состоит в вызове функции 02h DOS, которая выводит на экран символ из регистра DL. Поскольку макрос использует регистры АХ и DX, они в начале макроса сохраняются в стеке, а перед его завершением восстанавливаются. В качестве параметра макрокоманды можно использовать любое обозначение ассемблера, которое может интерпретироваться, как адрес символа.
Сама программа умышленно построена несколько нестандартным образом. В ней имеется единственный сегмент с текстом программы, в конце которого помещена строка данных (слово 'ОК'). Такое расположение данных допустимо, однако для обращения к ним необходимо использовать замену сегмента (как это сделано в третьей строке программы), так как программный сегмент адресуется через регистр CS. Сегмент стека в программе отсутствует, что не очень хорошо, но для небольших программ допустимо. Фактически под стек будет использован самый низ сегмента команд, начиная с адреса FFFEh. Поскольку наша программа имеет размер, существенно меньше 64К, такое расположение стека не приведет ни к каким неприятностям (при большом размере программы стек мог бы начать затирать нижние строки программы).
В программе проиллюстрировано использование в качестве фактического аргумента макрокоманды различных конструкций языка: непосредственного обозначения символа (что, наверное, лишено смысла), прямого обращения к различным участкам памяти по абсолютным адресам через регистры ES и DS, адресации с использованием символического обозначения поля данных. На рис. 2.18 приведен вывод программы.

Рис. 2.18. Вывод программы 2.1.

Как уже отмечалось, при загрузке программы в память в регистры DS и ES заносится сегментный адрес префикса программы, поэтому адресация через ES позволяет прочитать содержимое PSP. Префикс содержит, главным образом, данные, необходимые системе для обслуживания текущей программы, но, кроме того, и несколько команд. В частности, префикс начинается с команды CD 20h, которая уже давно не используется, но в префиксе присутствует ради обеспечения совместимости со старыми версиями DOS. Первый байт этой команды, если его рассматривать, как код символа, соответствует элементу двойной горизонтальной рамки (длинный знак равенства).
Занеся в регистр DS число 40h, мы настроили его на начало области данных BIOS, которая начинается с абсолютного адреса 400h, занимает 256 байт и содержит разнообразные данные, используемые BIOS в процессе обслуживания аппаратуры компьютера. Так, например, по адресу 0 от начала этой области хранится базовый адрес первого последовательного порта; по адресу 8 - адрес первого параллельного порта, а по адресу 491i - код текущего видеорежима. При работе в DOS видеоадаптер обычно настраивается на режим 3 (80x25 символов, 16 цветов). Будучи выведен на экран, код 3 образует изображение червонного туза.
В тех случаях, когда макрокоманды составляются для конкретной программы, они включаются в текст программы так, как это было сделано в примере 2.1. Однако часто программист оформляет в виде макрокоманд стандартные процедуры общего назначения, например, программную задержку или вывод на экран строки текста. В этом случае тексты макроопределений целесообразно поместить в макробиблиотеку.
Макробиблиотека представляет собой файл с текстами макроопределений. Макроопределения записываются в этот файл точно в таком же виде, как и в текст программы. Ниже приведен текст файла макробиблиотеки с произвольным именем MYMACRO.MAC, содержащей две макрокоманды.

;Макрокоманда endpr завершения программы
endpr macro ;Макрокоманда без параметров
mov AX,4C00h
int 2 In
endm ;Конец макрокоманды
;Макрокоманда delay настраиваемой программной задержки
delay macro time ;Параметр - число шагов
locallabell,Iabel2 ;Локальные метки
push CX ;Сохраним внешний счетчик
mov CX,time ;Получим фактический параметр
Iabel2 : push CX ;Сохраним его в стеке
mov CX, 0 ;Пусть будет 64К шагов
labell: loop lanell ;Внутренний цикл
pop CX ;Извлечем внешний счетчик
loop Iabel2 ;Внешний цикл
pop CX ;Восстановим CX программы
endm ;Конец макрокоманды

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

include my macro, mac

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

;debug=l ;Удалите символ ';'для отладочной трансляции
;debug=0 ;Удалите ';' для рабочей трансляции
... ;Текст программы
if debug ;Транслировать только если debug=l
call regs;Вызов отладочной подпрограммы
endif ;Конец блока условной трансляции
… ;Продолжение программы
if debug ;Следующее включение отладочного блока
call regs
endif
... ;Продолжение программы
Разумеется, можно отлаживать программу в отладочном варианте, а затем удалить все вызовы вспомогательной подпрограммы regs вручную и получить рабочий вариант, однако на практике обычно (или даже всегда) оказывается, что после эксплуатации программы в течение некоторого времени в ней обнаруживаются незамеченные ранее ошибки, что приводит к необходимости снова вставлять в нее отладочные строки. Часто эту процедуру приходится повторять многократно. Использование в программе директив условной трансляции сокращают процедуру преобразования программы из отладочного варианта в рабочий или наоборот до операции стирания одного символа ";" в начале программы и устраняют вероятность случайного внесения в программу новых ошибок в процессе удаления или вставки отладочных строк.
Рассмотрим еще один пример применения директив условной трансляции. Как уже отмечалось, современные процессоры предоставляют программисту значительное количество дополнительных команд, которые можно использовать в программах реального режима, но только, разумеется, если компьютер оснащен соответствующим процессором. Нетрудно составить универсальную программу', которую можно выполнять как на современных процессорах (в более эффективном режиме), так и на более старых (с некоторой потерей эффективности), если включить в нее директивы условной трансляции этих дополнительных команд. К таким командам, в частности, относятся команды сохранения в стеке всех регистров общего назначения pusha и восстановления всех регистров рора. Приведем пример условной трансляции этих команд, в котором используется конструкция макроязыка if... else... endif:

i386=l
if i386
.386
endif
code segment use16
assume CS:code
main proc
…
if i386
push ;Сохранение всех регистров одной командой
else
push AX
push CX
push DX
push BX
push BP
push SI
push DI
endif
. . . ;Использование регистров после
;сохранения их значений
if 1386
рора ;Восстановление всех регистров одной командой
else
pop DI
pop SI
pop BP
pop BX
pop DX
pop CX
pop AX
endif

Если в начале программы имеется объявление i386=1, то, во-первых, в программу будет включена директива .386, позволяющая использовать в программе дополнительные команды, а во-вторых, в последующих условных блоках будут транслироваться те их участки, которые содержат команды процессора 80386. Если же объявление i386=1 изъять, то в условных блоках будут транслироваться эквивалентные по существу, но менее эффективные последовательности команд МП 86.


Глава 3

3. КОМАНДЫ И АЛГОРИТМЫ

    • 3.1. Организация приложений MS-DOS

    • 3.2. Циклы и условные переходы
    • 3.3. Обработка строк
    • 3.4. Использование подпрограмм
    • 3.5. Двоично-десятичные числа
    • 3.6. Программирование аппаратных средств

3.1. Организация приложений MS-DOS
Как уже отмечалось выше, язык ассемблера является отражением архитектуры процессора, и изучение языка в сущности означает изучение системы команд и способов адресации, реализуемых процессором. Одна ко любой язык программирования полезен лишь постольку, поскольку на нем можно написать какие-то работоспособные программы. В то же время трудно представить себе реальную программу, которая выполняет чисто логические или вычислительные действия, ничего не вводя и не выводя и не взаимодействуя с другими программами. Однако такие вопросы, как организация выполнимой программы, ее запуск, взаимодействие с разнообразными аппаратными и программными объектами вычислительной системы (клавиатурой, дисками, таймером, памятью, системными драй верами и проч.) и, наконец, завершение являются прерогативой операционной системы. Поэтому в программах на языке ассемблера всегда широко используются системные средства, например, для вывода па экран или ввода с клавиатуры, чтения или записи файлов, управления памятью и проч. Более того, сама внутренняя организация программы, ее структура и, в определенной степени, алгоритмы поведения в сильной степени определяются правилами организации вычислительного процесса, заложенными в DOS. Изучение языка ассемблера в отрыве от конкретной операционной системы вырождается в схоластическое занятие, результатом которого будет знание формальных правил написания программных предложений без возможности применить эти правила для создания работоспособных программ.
В то же время возможности даже такой относительно простой операционной системы, как MS-DOS, весьма велики и многообразны, и их изучение составляет самостоятельный раздел программирования. В настоящей книге средства DOS рассматриваются лишь в том минимальном объеме, который необходим для создания простых, но работоспособных программ на языке ассемблера, а также для демонстрации основных алгоритмов и приемов программирования.
Желающие получить более глубокое представление о возможностях MS-DOS и использовании функций DOS в прикладном программировании, могут обратиться к книге: К.Г.Финогенов "Самоучитель по системным функциям MS-DOS", M., Радио и связь, Энтроп, 1995.
К числу важнейших вопросов, требующих хотя бы минимального рассмотрения, следует отнести требования, предъявляемые MS-DOS к структуре прикладных программ, а также к особенностям их взаимодействия с самой DOS и с другими программами.
Программы, предназначенные для выполнения под управлением MS-DOS, можно классифицировать по разным признакам. По внутренней организации все программы принадлежат к одному из двух типов, которым соответствуют расширения имен программных файлов .ЕХЕ и .СОМ. По взаимодействию с самой DOS программы подразделяются на транзитные и резидентные. Наконец, следует выделить важнейший класс программ, служащих для обработки аппаратных или программных прерываний, и называемых обычно обработчиками прерываний. Мы не касаемся здесь таких специфических программ, как устанавливаемые драйверы устройств, командные процессоры (к их числу принадлежит COMMAND.COM) или оболочки DOS (например, широко распространенная программа Norton Commander), которые можно выделить в самостоятельные классы.
Первый пример законченной программы, рассмотренный нами в гл. 2, относился к наиболее распространенному типу .ЕХЕ-приложений. Для такой программы характерно наличие отдельных сегментов команд, данных и стека; для адресации к полям каждого сегмента используется свой сегментный регистр. Удобство .ЕХЕ-программы заключается в том, что ее можно почти неограниченно расширять за счет увеличения числа сегментов. В случае большого объема вычислений в программу можно включить несколько сегментов команд, обеспечив, разумеется, переходы из сегмента в сегмент с помощью команд дальних переходов или дальних вызовов подпрограмм. Если же программа должна обрабатывать большие объемы данных, в ней можно предусмотреть несколько сегментов данных. Каждый сегмент не может иметь размер более 64 Кбайт, однако в сумме их объем ограничивается только наличной оперативной памятью. Правда, в реальном режиме затруднительно обратиться к памяти за пределами 1 Мбайт адресного пространства, так что максимальный размер программы, если не предусматривать в ней какие-то специальные средства поочередной загрузки сегментов, ограничен величиной 550 ... 600 Кбайт. Наличие в МП 86 лишь двух сегментных регистров данных (DS и ES) несколько усложняет алгоритмы обработки больших объемов данных, так как приходится постоянно переключать эти регистры с одного сегмента на другой. Однако реально в современных процессорах имеются не два, а четыре сегментных регистра данных (DS, ES, FS и GS), которые вполне можно использовать в приложениях DOS, упростив тем самым процедуры обращения к данным и ускорив выполнение программ. Позже все эти возможности будут рассмотрены более подробно.
Во многих случаях объем программы оказывается невелик - меньше, а часто и много меньше, чем 64 Кбайт. Такую программу нет никакой необходимости составлять из нескольких сегментов: и команды, и данные, и стек можно разместить в единственном сегменте, настроив на его начало все 4 сегментных регистра. Для односегментных программ в MS-DOS существует специальный формат и специальные правила их составления. Программные файлы с программами, составленными по этим правилам, имеют расширение .СОМ. В формате .СОМ обычно пишутся резидентные программы и драйверы, хотя любую прикладную программу небольшого объема можно оформить в виде .СОМ-приложения. Если посмотреть список системных программ, входящих в DOS, и реализующих, в частности, внешние команды DOS, то можно заметить, что приблизительно треть этих программ написана в формате .COM (COMMAND.COM, FORMAT.COM, SYS.COM и др.), а две трети - в формате .EXE (FC.EXE: PRINT.EXE, XCOPY.EXE и т.д.). Ниже мы рассмотрим правила составления и особенности исполнения как .ЕХЕ-, так и .СОМ-программ.
Другой критерий классификации программ определяет способ взаимодействия прикладной программы с другими программами и самой DOS. По этому критерию программы делятся на два вида: транзитные и резидентные.
Ход выполнения транзитной программы (а к транзитным относится подавляющее большинство приложений DOS) выглядит следующим образом. Пользователь запускает программу, вводя с клавиатуры ее имя, завершаемое нажатием клавиши Enter. Соответствующие программы-компоненты DOS отыскивают на диске файл с указанным именем, загружают его в память и передают управление на входную точку этой программы. Далее программа выполняется, фактически монополизируя ресурсы компьютера. Пока она не завершилась, пользователь не имеет доступа к DOS и, соответственно, лишен возможности запустить другую программу или выполнить какую-либо команду DOS. Ввод с клавиатуры возможен только в ответ на запрос текущей программы, если в ней предусмотрено обращение к клавиатуре за получением каких-либо данных.
Совсем по-другому функционирует резидентная программа. Пользователь запускает ее точно так же, как и транзитную, вводя с клавиатуры ее имя. Программы DOS загружают программный файл в память и передают управление на точку входа. Однако дальше вычислительный процесс развивается поиному. Программа выполняет только свой начальный, инициализирующий фрагмент, после чего вызывает специальную функцию DOS (с номером 31h). Эта функция завершает программу и возвращает управление в DOS, но не освобождает память от завершившейся программы, а оставляет эту программу в памяти, делая ее резидентной. Программа остается в памяти и, можно сказать, ничего не делает. Поскольку управление передано DOS, пользователь может вводить с клавиатуры любые команды и, в частности, запускать другие транзитные (или резидентные) программы. Когда будет активизирована находящаяся в памяти резидентная программа? Как правило, резидентные программы включают в себя обработчики аппаратных или программных прерываний. Если, например, в резидентной программе имеется обработчик прерываний от системного таймера, который, как известно, выдает сигналы прерываний приблизительно 18 раз в секунду, то каждое такое прерывание будет предавать управление резидентной программе, которая может, например, периодически выводить на экран текущее время или какую-то иную информацию. Работа резидентной программы будет протекать независимо от других программ и параллельно с ними. Другим классическим примером резидентной программы является русификатор клавиатуры, который получает управление при нажатии любой клавиши, независимо от того, какая программа сейчас выполняется. Задача русификатора - определить по имеющемуся в нем флагу, на каком языке работает пользователь, и в необходимых случаях сформировать соответствующий нажатой клавише код ASCII русской буквы.
Следует заметить, что необходимость в резидентных программах возникла лишь потому, что MS-DOS является существенно однозадачной системой. В многозадачной операционной системе Windows понятие резидентной программы в принципе отсутствует.
Разумеется, своими особенностями составления и функционирования обладают и обработчики прерываний - чрезвычайно важный класс программ, обслуживающих многочисленные внешние устройства компьютера - клавиатуру, мышь, магнитные диски и проч., а также нестандартную аппаратуру, если компьютер используется для управления научной установкой или технологическим процессом.
Рассмотрим основные правила составления и функционирования перечисленных типов программ, чтобы в дальнейшем можно было исполь-зовать их в примерах, иллюстрирующих те или иные средства языка ассемблера.

Программа типа .ЕХЕ
Характерные особенности программ типа .ЕХЕ подробно рассматри-вались в предыдущих главах. Приведем еще несколько обобщающих сооб-ражений. Структура типичной программы на языке ассемблера выглядит следующим образом.

Программа начинается с директивы ассемблера .586, разрешающей использовать в тексте программы весь набор команд процессора Pentium (кроме привилегированных). Если программа будет использовать только базовый набор команд МП 86, указание этой директивы не обязательно.
С другой стороны, ее указание не обязывает нас обязательно использовать команды Pentium. Если в программе предполагается использовать лишь дополнительные команды процессоров 486 или 386, то вместо .586 можно написать .486 или .386.
Указание любого номера 32-разрядного процессора приведет к тому, что по умолчанию программа будет транслироваться, как 32-разрядное приложение, в то время как нам нужно создать обычное 16-разрядное приложение. Для того, чтобы все адреса в программе рассматривались, как 16-битовые, необходимо придать сегментам команд и данных описатели use16. Для сегмента стека этот описатель не нужен, так как в стеке нет поименованных ячеек.
Программа состоит из трех сегментов - команд, данных и стека. Имена сегментов выбраны произвольно. Собственно программа обычно состоит из процедур. Деление на процедуры не обязательно, но повышает ее наглядность и облегчает передачу управления на подпрограммы. В рассматриваемом примере сегмент команд содержит единственную процедуру main, открываемую оператором ргос (от procedure, процедура) и закрываемую оператором endp (end procedure, конец процедуры). Перед обоими операторами указывается имя процедуры, которое в дальнейшем может использоваться в качестве относительного адреса процедуры (в сущности, относительного адреса первого выполнимого предложения этой процедуры). У нас это имя выступает в качестве параметра завершающей программу директивы end. Имена процедур, так же, как и имена сегментов, выбираются произвольно.
Если программа имеет сегмент данных с какими-либо данными, то для того, чтобы к этим данным можно было обратиться, необходимо занести сегментный адрес сегмента данных в один из сегментных регистров. Обычно в качестве такого регистра выбирают DS. Таким образом, предложения, с которых начался текст главной процедуры

mov AX,data ;Инициализация
mov DS,АХ ;сегментного регистра DS

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

mov AX,4C00h ;Вызов функции DOS
int 21h ;завершения программы

в которых вызывается функция DOS с номером 4Ch. Эта функция, как уже отмечалось, завершает программу, освобождает занимаемую ею память и передает управление командному процессору COMMAND.COM. Еще два замечания следует сделать относительно процедуры трансляции и компоновки программы. Если сегмент данных расположить после сегмента команд, как это сделано в нашем примере, то у транслятора возникнут сложности при обработке встречающихся в программных пред-
ложениях имен полей данных, так как эти имена еще неизвестны транслятору. Для того, чтобы такие, как говорят, "ссылки вперед" могли правильно обрабатываться, следует в команде вызова транслятора TASM заказать два прохода. Это делается указанием ключа /m2.
С другой неприятностью мы столкнемся, если попытаемся включить в программу операции с 32-разрядными операндами (даже и с командами МП 86). Компоновщик TASM по умолчанию запрещает такого рода операции. Чтобы преодолеть этот запрет, следует в команде вызова компоновщика указать ключ /3.
Таким образом, приведенный в гл. 1 командный файл должен выглядеть (для подготовки программы P.ASM) следующим образом:

tasm /z /zi /n /m2 p,p,p
tlink /x /v /3 p,p

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

Рассмотрим текст приведенного примера. После настройки сегментного регистра DS на сегмент данных, вызывается функция DOS с номером 19h, которая позволяет получить код текущего диска. У этой функции нет никаких параметров, а результат своей работы она возвращает в регистре AL в виде условного кода. 0 обозначает диск А:, 1 диск В:, 2 диск С: и т.д. Если, например, пользователь работает на диске F, то функция 19h вернет в AL код 5.
Для преобразования кода диска в его буквенное обозначение, мы воспользовались широко распространенным приемом. В полях данных определена символьная строка, которая будет выводиться на экран. Для удобства работы она разделена на две части, каждая из которых имеет свое имя. Началу строки присвоено имя msg, а той ее части, которая начинается с обозначения диска А:, имя disk (разумеется, имена выбраны произвольно). Если посмотреть на таблицу кодов ASCII, то можно заметить, что код каждой следующей буквы алфавита на 1больше предыдущей. Таким образом, если к коду латинской буквы A (41h) прибавить 1, получится код буквы В, а если прибавить, например, 5, получится код буквы F. Именно эта операция и выполняется в предложении

add disk,AL ;Преобразуем номер в код ASCII

где к байту с адресом disk прибавляется код, возвращенный функцией DOS.
Выполнив модификацию строки, мы выводим ее на экран уже знакомой нам функцией DOS 09h. Она выводит все символы строки, пока не встретится с символом $, которым наша строка и завершается. Перед знаком S в строке имеются два числа: 13 и 10. При выводе текстовой строки на экран любой функцией DOS код 13 трактуется DOS, как команда вернуть курсор в начато строки ("возврат каретки"), а код 10 - как команда на перевод строки. Два эти кода переводят курсор в начало следующей строки экрана. В данном случае, когда на экран ничего больше не выводится, можно было обойтись и без этих кодов, которые включены лишь в познавательных целях.
Между прочим, правильная работа программы основана на том предположении (безусловно правильном), что ассемблер расположит наши данные в памяти в точности в том же порядке, как они описаны в программе. Именно это обстоятельство и позволяет дробить единую строку на части, не опасаясь, что в память они попадут в разные места, что привело бы, разумеется, к непредсказуемому результату.После вывода на экран сообщения о текущем диске в программе вызывается функция DOS с номером 01h. Эта функция вводит с клавиатуры один символ. Если символов нет (мы после запуска программы не нажимали на клавиши), функция 01h ждет нажатия фактически любой клавиши (более точно - любой алфавитно-цифровой или функциональной кла-виши). Такой весьма распространенный прием позволяет остановить выполнение программы до нажатия клавиши, что дает возможность программисту посмотреть, что вывела программа на экран, и оценить правильность ее работы.
Наконец, последнее действие носит, как уже отмечалось, сакраментальный характер. Вызовом функции DOS 4Ch программа завершается с передачей управления DOS.
Взглянем еще раз на текст программы 3-1. Если не считать первых предложений инициализации регистра DS, то в программе имеется лишь одна строка, носящая, можно сказать, вычислительный характер - это прибавление полученного кода диска к содержимому байта памяти. Все остальные строки служат для вызова тех или иных функций DOS - получения информации о текущем диске, вывода строки на экран, остановки программы и, наконец, ее завершения. Это подтверждает высказанное выше утверждение о важности изучения системных средств и широком использовании их в программах на языке ассемблера. Разумеется, в программе могут быть и сколь угодно сложные и протяженные участки обработки данных и других вычислений, но такие операции, как ввод с клавиатуры, вывод на экран, работа с файлами, получение, как в нашем примере, системной информации и многое другое выполняется исключительно с помощью вызова тех или иных функций DOS (или BIOS). Программу на языке ассемблера просто невозможно написать без использования системных средств.

Структура и образ памяти программы .СОМ
Как уже отмечалось, программа типа .СОМ отличается от программы типа .ЕХЕ тем, что содержит лишь один сегмент, включающий все компоненты программы: PSP, программный код (т.е. оттранслированные в машинные коды программные строки), данные и стек. Струкгура типичной программы типа .СОМ на языке ассемблера выглядит следующим образом:

Программа содержит единственный сегмент code. В операторе ASSUME указано, что сегментные регистры CS и DS будут указывать на этот единственный сегмент. Оператор ORG 100h резервирует 256 байт для PSP. Заполнять PSP будет попрежнему система, но место под него в начале сегмента должен отвести программист. В программе нет необходимости инициализировать сегментный регистр DS, поскольку его, как и остальные сегментные регистры, инициализирует система. Данные можно разместить после программной процедуры (как это показано в приведенном примере), или внутри нес, или даже перед ней. Следует только иметь в виду, что при загрузке программы типа .СОМ регистр IP всегда инициализируется числом 100h, поэтому сразу вслед за оператором ORG 100h должна стоять первая выполнимая команда программы. Если данные желательно расположить в начале программы, перед ними следует помес-тить оператор перехода на фактическую точку входа, например jmp entry.
Образ памяти программы типа .СОМ показан на рис. 3.2. После загрузки программы все сегментные регистры указывают на начато единственного сегмента, т.е. фактически на начато PSP. Указатель стека автоматически инициализируется числом FFFEh. Таким образом, независимо от фактического размера программы, ей выделяется 64 Кбайт адресного пространства, всю нижнюю часть которого занимает стек. Поскольку верхняя граница стека не определена и зависит от интенсивности и способа использования стека программой, следует опасаться затирания стеком нижней части программы. Впрочем, такая опасность существует и в программах типа .ЕХЕ, так как в реальном режиме нет никаких механизмов защиты, и при сохранении в стеке большего объема данных, чем может так поместиться, данные начнут затирать поля того сегмента, который рас-положен за стеком (если таковой сегмент существует).

Рис. 3.2. Образ памяти программы .СОМ

Программы типа .СОМ отличаются от .ЕХЕ-программ не только отсутствием сегментов данных и стека. В гл. 2 было показано, что при выравнивании сегментов на байт, что делается с помощью описателя byte

data segment byte

системные программы располагают сегменты загружаемой программы с некоторым перекрытием, что позволяет избежать пустых промежутков между сегментами в памяти, возникающих из-за того, что размеры сегментов могут быть не кратны величине параграфа - 16 байт. Такое распо-ложение сегментов требует изменения значений ссылок на адреса ячеек памяти. В состав программного файла с расширением .ЕХЕ входит таблица с перечнем байтов программы, содержимое которых может подвергнуться изменению в процессе загрузки программы в память. Поэтому, кстати, размер файла с расширением .ЕХЕ может превышать истинный размер программы в памяти.
Программа типа .СОМ состоит из единственного сегмента, и проблема настройки ссылок не возникает. Файл с расширением .СОМ почти в точности отражает содержимое памяти после загрузки программы. Отличие заключается только в том, что в программном файле отсутствует префикс программы PSP, который система вставляет в программу в процессе ее загрузки. Таким образом, файл с расширением .СОМ обычно оказывается на 256 байт короче своего образа в памяти.
Если оттранслировать и скомпоновать программу, написанную в формате .СОМ, обычным образом, образуется программный файл с расширением .ЕХЕ. Этот файл можно запустить на выполнение, однако работать он будет неверно. Дело в том, что система, загружая файл типа .ЕХЕ в память, пристраивает перед загруженной программой префикс и настраивает на него регистры DS и ES. В результате значение DS окажется на 10h меньше, чем сегментный адрес сегмента с командами и данными, что приведет к неправильной адресации при обращении к полям данных. Программу, написанную в формате .СОМ, можно запускать только в виде файла с расширением .СОМ, для которого в DOS предусмотрен CBI алгоритм загрузки и запуска. Для того, чтобы компоновщик создал файл с расширением .СОМ, в строке запуска компоновщика необходимо предусмотреть ключ /t (при использовании компоновщика TLINK.EXE):

tlink /x /v /3 /t p,p

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

В начале программы отведено 256 байт под PSP; в программе отсутствует инициализация регистра DS; поле данных размещено в программном сегменте непосредственно после последней команды. Для разнообразия в строку, выводимую на экран, включены коды 16 и 17, которые отображаются на экране в виде залитых треугольников (рис. 3.3). Как видно из этого рисунка, программа имела имя Р. СОМ и запускалась из каталога F:\CURRENT.
Рассмотрим важный в принципиальном плане вопрос о месте размещения данных в .СОМ-программе. В нашем примере данные описаны в конце программного сегмента вслед за процедурой main, которая, как и в предыдущих примерах, введена скорее для порядка, чем по необходимости.

Рис. 3.3. Вывод программы 3.2.

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

Таким образом, данные могут быть размещены как после программы, так и среди выполнимых предложений программы. Важно только соблюсти обязательное условие: ни при каких обстоятельствах на данные не должно быть передано управление. В первом случае (пример 3-2) данные помещены за вызовом функции DOS, завершающей программу. Ясно, что после выполнения этой функции управление уже не вернется в нашу программу, а будет передано командному процессору, поэтому размещение здесь данных вполне возможно. В последнем фрагменте данные описаны, можно сказать, в середине программы. Однако перед ними стоит команда безусловного перехода jmp, которая приводит при выполнении программы к обходу данных.
А вот чего нельзя было сделать, так это разместить данные после зак-рытия сегмента, как это сделано в приведенном ниже (неправильном!) фрагменте:

Это второе обязательное условие: из чего бы ни состояла программа, все ее компоненты должны входить в те или иные сегменты. Вне сегментов допускаются только нетранслируемые директивы ассемблера типа .586 или assume.
Наконец, третье условие, о котором уже говорилось, относится только к программам типа .COM. DOS, загрузив программу в память, инициализирует указатель команд числом 100h, т.е. адресом первой команды вслед за оператором org 100h. Поэтому главная процедура .СОМ-программы (если в ней имеется несколько процедур) обязательно должна быть первой, причем первое предложение этой процедуры должно быть выполнимой командой (например, командой jmp, как это показано выше).

Обработчики аппаратных прерываний
Обработчики прерываний являются важнейшей составной частью многих программных продуктов. Как было показано в гл. 1, прерывания разделяются на внутренние, возникающие в самом микропроцессоре в случае определенных сбоев (попытка деления на 0, несуществующая команда), внешние, приходящие из периферийного оборудования (клавиатура, мышь, диски, нестандартные устройства, подключенные к компьютеру) и программные, являющиеся реакцией процессора на команду int с тем или иным номером. В прикладных программах приходится обрабатывать, главным образом, внешние и программные прерывания. Общие принципы обслуживания тех и других прерываний одинаковы, однако условия функционирования обработчиков аппаратных прерываний имеют значительную специфику, связанную, главным образом, с тем, что прерывания от аппаратуры приходят в произвольные моменты времени и могут прервать текущую программу в любой ее точке. Обработчик прерывания должен быть написан таким образом, чтобы его выполнение ни в какой степени не отразилось на правильном функционировании текущей (прерываемой) программы.
Рассмотрим схематически структуру и функционирование программного комплекса, включающего собственный обработчик какого-либо ап-паратного прерывания (рис. 3.4).

Рис. 3.4. Функционирование программного комплекса с обработчиком прерываний.

Обработчик прерываний может входить в состав программы в виде процедуры, или просто являться частью программы, начинающейся с некоторой метки (входной точки обработчика) и завершающейся командой выхода из прерывания iret. Пока мы не будем рассматривать более сложный случай, когда обработчик представляет собой самостоятельную резидентную программу.
Программа, начиная свою работу, прежде всего должна выполнить инициализирующие действия по установке обработчика прерываний. В простейшем случае эти действия заключаются в занесении в соответствующий вектор полного адреса (сегмента и смещения) обработчика. Поскольку обработчик входит в состав программы, его относительный адрес известен; это имя его процедуры или метка входной точки. Что же касается сегментного адреса, то обработчик может входить в сегмент основной части программы, если она невелика по объему и занимает один сегмент, но может образовывать и самостоятельный сегмент. В любом случае в качестве сегментного адреса можно использовать имя соответствующего сегмента.
Часто инициализация обработчика, помимо установки вектора, предполагает и другие действия: сохранение исходного содержимого вектора прерывания, размаскирование соответствующего уровня прерываний в контроллере прерываний, посылка в устройство команды разрешения прерываний и проч.
Установив обработчик, программа может приступить к дальнейшей работе. В случае прихода прерывания, процессор сохраняет в стеке флаги и текущий адрес программы, извлекает из вектора адрес обработчика и передает управление на его входную точку. Все время, пока выполняется программа обработчика, основная программа, естественно, стоит. Завершающая обработчик команда irct извлекает из стека сохраненные там данные и возвращает управление в прерванную программу, которая может продолжить свою работу. Последующие прерывания обслуживаются точно так же.
Функции обработчика прерываний зависят от решаемой задачи и назначения того устройства, от которого поступают сигналы прерываний. В случае прерываний от клавиатуры задача обработчика прерываний - принять и сохранить код нажатой клавиши. Прерывания от мыши свидетельствуют о ее перемещении, что требует обновления положения курсора на экране. Если обслуживаемым устройством является физическая установка, то сигнал прерывания может говорить о том, что в установке накоплен определенный объем данных, которые надо перенести из памяти установки в память компьютера. В любом случае обработчик прерываний должен быть программой несложной, для выполнения которой не требуется много машинного времени.
Рассмотрим структуру программы с обработкой аппаратных прерываний. Наиболее удобным аппаратным прерыванием, которое можно использовать в исследовательских целях, является прерывание от системного таймера, которое генерируется 18.2 раза в секунду и служит источником сигналов для хода системных часов, отсчитывающих время, истекшее после включения машины. Замена системного обработчика на прикладной не приводит к каким-либо неприятностям, кроме, возможно, остановки на некоторое время системных часов.
Будем считать, что наш программный комплекс представляет собой программу типа .ЕХЕ и что обработчик прерываний входит в общий с основной программой программный сегмент. Для определенности будем использовать вектор 08h, хотя, разумеется, для любого другого аппаратного вектора структура программы останется той же. Поначалу приведем текст программы с некоторыми купюрами.

В приведенном примере обработчик прерываний расположен в конце программы, после главной процедуры main. Взаимное расположение процедур не имеет ни малейшего значения; с таким же успехом обработчик можно было поместить в начале программы. Не имеет также значения, выделен ли обработчик в отдельную процедуру или просто начинается с метки.
Для того, чтобы прикладной обработчик получал управление в результате прерываний, его адрес следует поместить в соответствующий вектор прерывания. При этом исходное содержимое вектора будет затерто, и если прерывания будут поступать и после завершения программы, возникнет весьма неприятная ситуация, когда управление будет передаваться по адресу, по которому в памяти может располагаться что угодно. Поэтому стандартной методикой является сохранение в памяти исходного содержимого вектора и восстановление этого содержимого перед завершением программы.
Хотя и чтение, и заполнение вектора прерываний можно выполнить с помощью простых команд mov, однако предпочтительнее использовать специально предусмотренные для этого функции DOS. Для чтения вектора используется функция с номером 35h. В регистр AL помещается номер вектора. Функция возвращает исходное содержимое вектора в парс регистров ES:BX (легко догадаться, что в ES сегментный адрес, а в ВХ смещение). Для хранения исходного содержимого вектора в сегменте данных предусмотрена двухсловная ячейка old_08. В младшем слове этой ячейки (с фактическим адресом old_08) будет хранится смещение, в старшем (с фактическим адресом old_08+2) - сегментный адрес. Для того, чтобы обратиться к словам, составляющим эту ячейку, приходится использовать описатель word ptr, который как бы заставляет транслятор на время забыть о начальном объявлении ячейки и позволяет рассматривать ее, как два отдельных слова. Если бы мы отвели для исходного вектора две 16-битовые ячейки, например

то к ним можно было бы обращаться без всяких описателей.
Сохранив исходный вектор, можно установить в нем адрес нашего обработчика. Для установки вектора в DOS предусмотрена функция 25h. Она требует указания номера устанавливаемого вектора в регистре AL, a его полного адреса - в парс регистров DS:DX. Здесь нас подстерегает неприятность. Занести в регистр DX смещение нашего обработчика new_08 не составляет труда, это делается командой

mov DX,offset new_08 ;Смещение нашего обработчика

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

В примере 3-3 использован другой прием - содержимое CS отправляется в стек и тут же извлекается оттуда в регистр DS:

push CS pop DS

После возврата из DOS надо не забыть восстановить исходное содержимое DS, сохраненное в стеке. Инициализация обработчика прерываний закончена. Начиная с этого момента, каждый сигнал таймера будет приводить к прерыванию продолжающейся основной программы и передаче управления на процедуру new_08.
Перед завершением программы необходимо поместить в вектор 8 адрес исходного, системного обработчика, который был сохранен в двухсловном поле old_08. Перед вызовом функции 25h установки вектора в регистры DS:DX надо занести содержимое этого двухсловного поля. Эту операцию можно выполнить одной командой Ids, если указать в качестве ее первого операнда регистр DX, а в качестве второго - адрес двухсловной ячейки, в нашем случае old_08. Именно имея в виду использование этой команды, мы и объявили поле для хранения вектора двухсловным, отчего возникли некоторые трудности при его заполнении командами mov. Если бы мы использовали второй предложенный выше вариант и отвели для хранения вектора две однословные ячейки (old_08_offs и old_08_seg), то команду Ids пришлось бы снабдить описателем изменения размера ячейки:

Ids DX,dword ptr old_08_offs

Между прочим, здесь так же разрушается содержимое DS, но поскольку сразу же вслед за функцией 25h вызывается функция 4Ch завершения программы, это не имеет значения.
Последнее, что нам осталось рассмотреть - это стандартные действия по завершению самого обработчика прерываний. Выше уже говорилось, что последней командой обработчика должна быть команда iret, возвращающая управление в прерванную программу. Однако перед ней необходимо выполнить еще одно обязательное действие - послать в контроллер
прерываний команду конца прерываний. Дело в том, что контроллер прерываний, передав в процессор сигнал прерывания INT, блокирует внутри себя линии прерываний, начиная с той, которая вызвала данное прерывание, и до последней в порядке возрастания номеров IRQ. Таким образом, прерывание, пришедшее, например, по линии IRQ 6 (гибкий диск) заблокирует дальнейшую обработку прерываний по линиям 6 и 7, а прерывание от таймера (IRQ0) блокирует вообще все прерывания (IRQ0...IRQ7, а также и IRQ8...IRQ15, поскольку все они являются разветвлением уровня IRQ2, см, гл. 1, рис. 1.11). Любой обработчик аппаратного прерывания обязан перед своим завершением снять блокировку в контроллере прерываний, иначе вся система прерываний выйдет из строя. Снятие блокировки осуществляется посылкой команды с кодом 20h в один из двух портов, закрепленных за контроллером прерываний. Для ведущего контроллера эта команда посылается в порт 20h, для ведомого - в порт A0h. Таким образом, если бы мы обрабатывали прерывания от часов реального времени (линия прерываний IRQ8, вектор 70h, ведомый контроллер), то команда конца прерывания выглядела бы так:

Указанную последовательность команд иногда называют приказом, или командой EOI (от end of interrupt, конец прерывания).
Разобравшись в этих общих вопросах, рассмотрим пример реальной программы, включающей обработчик прерываний от таймера. Для того, чтобы приведенную выше фрагментарную программу преобразовать в действующую, надо написать содержательную часть самого обработчика, а также придумать, что будет делать основная программа после инициализации прерываний. Все это сделать очень просто.
Пусть наш обработчик в ответ на каждое прерывание от таймера выводит на экран какой-нибудь символ. Для этого можно воспользоваться функцией 0Eh прерывания BIOS 10h. Это прерывание обслуживает большое количество различных функций, обеспечивающих управление экраном. Сюда входят функции вывода символов и строк, настройки режимов видеосистемы, загрузки нестандартных таблиц символов и многие другие. Функция 0Eh предназначена для вывода на экран отдельных символов. Она требует указания в регистре AL кода выводимого символа. Процедура new_08 будет выглядеть в этом случае следующим образом:

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

mov AH,01h
int 21h

В результате программа, дойдя до этих строк, остановится (фактически будет выполняться цикл опроса клавиатуры в ожидании нажатия клавиши, включенный в состав программы реализации функции 01h DOS), а на экран непрерывной чередой будут выводиться символы коммерческого at (рис. 3.5). После нажатия на любую клавишу программа завершится.

Рис. 3.5. Вывод программы 3-3.

Приведенный пример позволяет обсудить чрезвычайно важный вопрос о взаимодействии обработчиков аппаратных прерываний с прерываемой программой и операционной системой. Особенностью аппаратных прерываний является то, что они могут возникнуть в любой момент времени и, соответственно, прервать выполняемую программу в любой точке. Текущая программа, разумеется, использует регистры, как общего назначения, так и сегментные. Если в обработчике прерывания мы разрушим содержимое хотя бы одного из регистров процессора, прерванная программа по меньшей мере продолжит свое выполнение неправильным образом, а скорее всего произойдет зависание системы. Поэтому в любом обработчике аппаратных прерываний необходимо в самом его начале сохранить все регистры, которые будут использоваться в программе обработчика, а перед завершением обработчика (перед командой iret) восстановить их. Регистры, которые обработчиком не используются, сохранять не обязательно.
В нашем простом обработчике используется только один регистр АХ. Его мы и сохраняем в стеке первой же командой push AX, восстанавливая в самом конце обработчика командой pop AX.
Вторая неприятность может возникнуть из-за того, что в обработчике аппаратного прерывания мы воспользовались для вывода на экран функцией BIOS. Вообще говоря, считается, что в обработчиках аппаратных прерываний нельзя использовать никакие системные средства. Причина такого запрета заключается в том, что аппаратное прерывание может придти в любой момент, в частности тогда, когда прерываемая программы сама выполняет какую-либо функцию DOS или BIOS. Однако, если мы прервем выполнение системной функции на полпути, и начнем выполнять ту же самую или даже другую функцию с начала, произойдет разрушение системы, которая не предусматривает такое "вложенное" выполнение своих программ. В настоящее время разработаны программные приемы, позволяющие эффективно обойти указанный запрет, однако использование их в программе драматически увеличивает ее сложность и объем, и рассматривать эти приемы мы здесь не будем.
В нашем случае дело усугубляется тем, что прерывания от таймера не только могут придти в тот момент, когда выполняется функция DOS, но и неминуемо приходят только в такие моменты, так как мы остановили программу с помощью вызова функции 01h прерывания 2Hi и, следовательно, все время, пока наша программа ждет нажатия клавиши, в действительности выполняются внутренние программы DOS. Именно поэтому мы отказались от использования в обработчике прерывания функции DOS и выводим на экран символы с помощью прерывания BIOS. Выполнение функции BIOS "на фоне" DOS не так опасно. Надо только следить за тем, чтобы наше прерывание BIOS в обработчике не оказалось вложенным в такое же прерывание BIOS в прерываемой программе.
Рассмотренный пример имеет существенный недостаток. Записав в вектор прерываний 8 адрес нашего обработчика, мы затерли исходное содержимое вектора и тем самым ликвидировали (в логическом плане) исходный, системный обработчик. Практически это приведет к тому, что на время работы нашей программы остановятся системные часы, и если в системе есть какие-то другие программы, использующие прерывания от таймера, они перестанут работать должным образом. Ликвидировать указанный недостаток очень просто: надо "сцепить" наш обработчик с системным так, чтобы в ответ на каждый сигнал прерывания активизировались последовательно оба обработчика. Рассмотрим методику сцепления обработчиков.
При инициализации прикладного обработчика, сцепляемого с системным, следует точно так же, как и раньше, сохранить в программе адрес системного обработчика и поместить в вектор прерывания адрес прикладного обработчика. Изменениям подвергнется только программа самого обработчика, начало которой должно выглядеть следующим образом:

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

Рис. 3.6. Стек прерванной программы в процессе выполнения прикладного обработчика прерываний.
CS1 - сегментный адрес прерванного процесса;
IP1 - смещение точки возврата в прерванную программу;
CS2 - сегментный адрес прикладного обработчика;
IP2 - смещение точки возврата в прикладной обработчик.

Именно такая структура данных должна быть на верху стека, чтобы команда iret, которой завершается любой обработчик прерываний, могла вернуть управление в прерванную программу.
Первая команда нашего обработчика pushf засылает в стек еще раз слово флагов, а команда дальнего вызова процедуры call cs:old_08 (где ячейка old_08 объявлена с помощью оператора dd двойным словом) в процессе передачи упражнения системному обработчику помещает в стек двухсловный адрес возврата на следующую команду прикладного обработчика. В результате в стеке формируется трехсловная структура, которая нужна команде iret.
Системный обработчик, закончив обработку данного прерывания, завершается командой iret. Эта команда забирает из стека три верхние слова и осуществляет переход по адресу CS2:IP2, т.е. на продолжение прикладного обработчика.
Завершающая команда нашего обработчика iret снимает со стека три верхних слова и передает упражнение по адресу CS1:IP1.
Описанная методика сцепления прикладного обработчика с систем-ным используется чрезвычайно широко и носит специальное название перехвата прерывания.

Обработчики программных прерываний
Программные прерывания вызываются командой int, операндом которой служит номер вектора с адресом обработчика данного прерывания. Команда int используется прежде всего, как стандартный механизм вызова системных средств. Так, команда int 2 Hi позволяет обратиться к многочисленным функциям DOS, а команды int 10h, int 13h или int 16h - к группам функций BIOS, отвечающим за управление теми или иными аппаратными средствами компьютера. В этих случаях обработчики прерываний представляют собой готовые системные программы, и в задачу программиста входит только вызов требуемого программного средства с помощью команды int с подходящим номером.
В некоторых специальных случаях, однако, программисту приходится писать собственный обработчик прерывания, которое уже обслуживается системой. Таким образом, например, осуществляется управление резидентными программами, которые для связи с внешним миром обычно используют прерывание 2Fh. В каждой резидентной программе имеется собственный обработчик этого прерывания, который, выполнив свою долю действий, передает управление "предыдущему", адрес которого находил-ся ранее в векторе 2Fh, и был сохранен обработчиком в своих полях данных. Другой пример - перехват прерываний BIOS в обработчиках аппаратных прерываний с целью обнаружения моментов времени, когда ни одна из наличных программ не использует данное прерывание и, следовательно, сам обработчик может им воспользоваться.
Наконец, прикладной программист может воспользоваться одним из свободных векторов, написать собственный обработчик соответствующего прерывания и оставить его резидентным в памяти. После этого любые программы могут с помощью команды int вызывать этот обработчик, который, таким образом, становится резидентной программой общего пользования.

Резидентные программы
Большой класс программ, обеспечивающих функционирование вычислительной системы (драйверы устройств, оболочки DOS, русификаторы, интерактивные справочники и др.), должны постоянно находиться в памяти и мгновенно реагировать на запросы пользователя, или на какие-то события, происходящие в вычислительной системе. Такие программы носят названия программ, резидентных в памяти (Terminate and Stay Resident, TSR), или просто резидентных программ. Сделать резидентной можно как программу типа .СОМ, так и программу типа .ЕХЕ, однако поскольку резидентная программа должна быть максимально компактной, чаще всего в качестве резидентных используют программы типа .СОМ.
Программы, предназначенные для загрузки и оставления в памяти, обычно состоят из двух частей (секций) - инициализирующей и рабочей (резидентной). В тексте программы резидентная секция размещается в начале, инициализирующая - за ней.
При первом вызове программа загружается в память целиком и управление передается секции инициализации, которая заполняет или модифицирует векторы прерываний, настраивает программу на конкретные условия работы (возможно, исходя из параметров, переданных программе при ее вызове) и с помощью прерывания DOS Int 21h с функцией 31h завершает программу, оставляя в памяти ее резидентную часть. Размер резидентной части программы (в параграфах) передается DOS в регистре DX. Указывать при этом сегментный адрес программы нет необходимости, так как он известен DOS. Для определения размера резидентной секции ее можно завершить предложением вида

ressize=$-main

где main - смещение начала программы, а при вызове функции ЗШ в регистр DX заслать результат вычисления выражения (rcssLze+10Fh)/16.
Разность S - main представляет собой размер главной процедуры. Однако перед главной процедурой размещается префикс программы, имеющий размер 100h байт, который тоже надо оставить в памяти. Далее, при целочисленном делении отбрасывается остаток, т.е. происходит округление результата в сторону уменьшения. Для компенсации этого дефекта можно прибавить к делимому число 15 = Fh. Деление всего этого выражения на 16 даст требуемый размер резидентной части программы в параграфах (возможно, с небольшим кусочком секции инициализации величиной до 15 байт).
Функция 31h, закрепив за резидентной программой необходимую для ее функционирования память, передает управление командному процессору COMMAND.СОМ, и вычислительная система переходит, таким образом, в исходное состояние. Наличие программы, резидентной в памяти, никак не отражается на ходе вычислительного процесса за исключением того, что уменьшается объем свободной памяти. Одновременно может быть загружено несколько резидентных программ.
Для того, чтобы активизировать резидентную программу, ей надо как-то передать управление и, возможно, параметры. Как правило, активизация резидентной программы осуществляется с помощью механизма прерываний.
Кроме того, специально для взаимодействия с резидентными программами в DOS предусмотрено мультиплексное прерывание 2Fh.
Рассмотрим типичную структуру резидентной программы и системные средства оставления ее в памяти. Как уже отмечалось, резидентные программы чаще всего пишутся в формате .СОМ:

При первом запуске программы с клавиатуры управление передается на начато процедуры main (первый байт после префикса программы). Командой jmp осуществляется переход на секцию инициализации, в которой, в частности, подготавливаются условия для дальнейшей активизации программы уже в резидентном состоянии. Последними строками секции инициализации вызывается функция ЗШ, которая выполняет завершение программы с оставлением в памяти указанной ее части. С целью экономии памяти секция инициализации располагается в конце программы и отбрасывается при ее завершении.
Содержательная часть резидентной программы, начинающаяся с метки entry, активизируется, как уже отмечаюсь выше, с помощью аппаратного или программного прерывания и заканчивается командой iret. На рис. 3.7 приведена типичная структура резидентной программы.

Рис. З.7. Структура резидентной программы.

Как видно из рис. 3.7, резидентная программа имеет по крайней мере две точки входа. После загрузки программы в память командой оператора, вводимой на командной строке, управление передается в точку, указанную в поле завершающего текст программы оператора end (на рисунке - начало процедуры main). Для программ типа .СОМ эта точка входа должна соответствовать самой первой строке программы, идущей вслед за префиксом программы. Поскольку при загрузке программы должна выполниться ее установка в памяти, первой командой программы всегда является команда перехода на секцию инициализации и установки (jmp init на рисунке).
После установки в памяти резидентная программа остается пассивной и никак не проявляет своего существования, пока не будет активизирована предусмотренным в ней для этого способом. Эта, вторая точка вызова обозначена на рисунке меткой entry.
К сожалению, резидентные программы, выполняющие полезную работу, оказываются довольно сложными. Мы же в качестве примера можем рассмотреть только совсем простую резидентную программу, в принципе правильную и работоспособную, но не претендующую на практическую ценность. Программа активизируется прерыванием от клавиши Print Screen и выводит на экран содержимое сегментного регистра CS, что позволяет определить ее положение в памяти.
Как известно, клавиша Print Screen в DOS выполняет печать содержимого экрана на принтере. Каков механизм этой операции? При нажатии на любую клавишу клавиатуры возникает сигнал прерывания, инициирующий активизацию обработчика прерываний от клавиатуры, находящегося в ПЗУ BIOS. При нажатии на алфавитно-цифровые и некоторые другие клавиши (например, функциональные клавиши <F1>...F<12>) обработчик сохраняет в определенном месте памяти код нажатой клавиши и завершается. Текущая программа может с помощью соответствующих функций DOS или BIOS извлечь этот код и использовать его в своих целях. Если же пользователь нажимает на клавишу Print Screen, то обработчик прерываний, в числе прочих действий, выполняет команду hit 5, передавая управление через вектор 5 на обработчик этого программного прерывания, который тоже располагается в ПЗУ BIOS. Задача обработчика прерывания 5 заключается в чтении содержимого видеобуфера и выводе его на устройство печати.
Таким образом, если мы напишем собственный обработчик прерывания и поместим его адрес в вектор с номером 5, он будет активизироваться нажатием клавиши Print Screen. Обратите внимание на то обстоятельство, что прерывание 5 является прерыванием программным; оно возбуждается командой int 5 и не имеет отношения к контроллеру прерываний. Однако активизируется это прерывание не командой int в прикладной программе, а нажатием клавиши, т.е., фактически, аппаратным прерыванием.
Перехват прерывания 5 осуществляется значительно проще, через перехват "истинного" аппаратного прерывания от клавиш клавиатуры, из-за чего мы и воспользовались им в нашем примере.

Структура программы соответствует описанной ранее. В секции инициализации выполняется установка обработчика прерывания 05h, при этом исходное содержимое вектора 5 не сохраняется. Это, разумеется, очень плохо, так как лишает нас возможности этот вектор восстановить. С другой стороны, восстанавливать перехваченные векторы надлежит при за-вершении программы, а применительно к резидентной программе - при ее выгрузке из памяти. Однако в нашей простой программе не предусмотрено средств выгрузки (процедура выгрузки довольно сложна), и программе придется находиться в памяти до перезагрузки машины.
Установив вектор, программа завершается с оставлением в памяти ее резидентной части с помощью функции 31h.
Резидентная часть программы является классическим обработчиком программного прерывания. В первых же предложениях сохраняются регистры АХ и ВХ, используемые далее в программе, а затем содержимое сегментного регистра CS переносится в регистр ВХ. С таким же успехом можно было скопировать содержимое любого из регистров DS, ES или SS, так как в программе типа .СОМ все регистры настроены на один и тот же сегментный адрес (см. рис. 3.2). Копирование из сегментного регистра в регистр общего назначения понадобился потому, что в дальнейшем нам придется работать с отдельными половинками сегментного адреса, а у сегментных регистров половинок нет.
Далее старшая половина сегментного адреса заносится в регистр AL, и вызовом уже знакомой нам функции BIOS 0 Eh этот код выводится на экран. Затем таким же образом выводится младшая половина сегментного адреса. Наконец, после восстановления регистров ВХ и АХ (в обратном порядке по отношению к их сохранению) командой iret управление возвращается в прерванную программу, которой в данном случае является COMMAND.COM.
Вывод программы (ей для наглядности было дано имя TSR.COM) для конкретного прогона показан на рис. 3.8.

Рис. 3.8. Вывод программы 3-4.

Полученный результат далек от наглядности. Действительно, разделив сегментный адрес на две половины длиной в байт каждая, мы просто записали в видеобуфер эти числа. Каждое число размером в байт можно трактовать, как код ASCII какого-то символа. При выводе числа на экран эти символы и отображаются. Изображение пикового туза соответствует коду 06, а знак равенства имеет код 3Dh (см. таблицу кодов ACSII на рис. 3.1). Таким образом, сегментный адрес находящейся в памяти резидентной программы оказался равен 063Dh, что соответствует приблизительно 25 Кбайт. Так и должно быть, так как конфигурация компьютера, использованного для подготовки примеров, предусматривала хранение большей части DOS в расширенной памяти, в области НМА. В основной памяти в этом случае располагается кусочек DOS вместе с драйверами обслуживания расширенной памяти и частью программы COMMAND.COM общим объемом около 25 Кбайт.
Для того, чтобы получить на экране сегментный адрес в привычной нам форме, его двоичное машинное представление необходимо преобразовать в коды ASCII, отображающие шестнадцатеричное (или, если угодно, десятичное) представление этого числа. В нашем примере, чтобы получить на экране изображение числа 063Dh, надо было сформировать та-кую цепочку кодов ASCII (в шестнадцатеричном представлении):

30 36 33 44 68

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

3.2. Циклы и условные переходы
Циклы, позволяющие выполнить некоторый участок программы многократно, в любом языке являются одной из наиболее употребительных конструкций. В системе команд МП 86 циклы реализуются, главным образом, с помощью команды loop (петля), хотя имеются и другие способы организации циклов. Во всех случаях число шагов в цикле определяется содержимым регистра СХ, поэтому максимальное число шагов составляет 64 К.
Рассмотрим простой пример организации цикла. Пусть в программе зарезервировано место для массива размером 10000 слов, и этот массив надо заполнить натуральным рядом чисел от 0 до 9999. Эти числа, заполняющие последовательные элементы массива, иногда называют числами-заполнителями. Соответствующий фрагмент программы будет выглядеть следующим образом:

На этапе подготовки мы заносим в регистр ВХ относительный адрес начала массива, отождествляемый с его именем array, устанавливаем начальное значение индекса элемента массива в регистре SI (с таким же успехом можно бьшо взять DI) и начальное значение числа-заполнителя. Сам цикл состоит из трех команд - единственной содержательной команды засылки числа-заполнителя в очередной элемент массива (по адресу, который вычисляется, как сумма содержимого регистров ВХ и SI), а также модификации числа-заполнителя и индекса очередного элемента массива. Завершающей командой loop управление передается на метку fill, и цикл повторяется столько раз, каково содержимое СХ, в данном случае 10000 шагов.
Следует обратить внимание на команду модификации индекса - в каждом шаге к содержимому SI добавляется 2, так как массив состоит из двухбайтовых слов. Если бы нужно бьшо заполнить байтовый массив, то в каждом шаге содержимое регистра цикла SI следовало увеличивать на 1.
Стоит отметить некоторые детали, связанные с механизмом выполнения команды loop. При реализации этой команды процессор сначала уменьшает содержимое регистра СХ на 1, а затем сравнивает полученное число с нулем. Если СХ > 0, переход на указанную метку выполняется. Если СХ = 0, цикл разрывается и процессор переходит на команду, следующую за командой loop. Поэтому после нормального выхода из цикла содержимое СХ всегда равно 0.
Другое обстоятельство связано с кодированием команды loop. В ее коде под смещение к точке перехода отводится всего 1 байт. Поскольку смещение должно являться величиной со знаком, максимальное расстояние, на которое можно передать управление командой loop, составляет от -128 до +127 байт (хотя довольно трудно представить себе цикл, в котором переход осуществляется вперед). Другими словами, тело цикла ограничивается всего 128 байтами. Если циклически повторяемый фрагмент программы имеет большую длину, цикл придется организовать другим, более сложным способом:

В этом, весьма типичном фрагменте мы "вручную" уменьшаем содержимое счетчика цикла и сравниваем полученное значение с 0. Если СХ = О, это значит, что в цикле выполнено заданное число шагов, и командой условного перехода je осуществляется переход на продолжение программы (метка finish). Если СХ еще не равно нулю, командой безусловного перехода jmp осущестатяется возврат в начало цикла. Как было показано в гл. 2, команда jmp позволяет перейти в любую точку сегмента, и огра-ничение на размер тела цикла снимается.
При необходимости организовать вложенные циклы, для сохранения счетчика внешнего цикла на время выполнения внутреннего удобно воспользоваться стеком. В следующем фрагменте организуется временная задержка длительностью несколько секунд (конкретная величина задержки зависит от скорости работы процессора).

Программные задержки удобно использовать при отладке программ, чтобы замедлить их работу и успеть рассмотреть их частичные результаты; иногда программные задержки позволяют синхронизовать работу аппаратуры, подключенной к компьютеру, если скорость отработки аппаратурой посылаемых в нее из компьютера команд меньше скорости процессора.
В приведенном выше фрагменте внешний цикл выполняется 2000 раз; внутренний - 65536 раз. При счете числа шагов внутреннего цикла используется явление оборачивания, которое уже упоминалось ранее. Начальное значение в регистре СХ равно нулю; после выполнения тела цикла 1 раз команда loop уменьшает содержимое СХ на 1, что дает число FFFFh (которое можно рассматривать, как -1). В результате цикл повторяется еще 65535 раз, а в сумме - точно 64 К шагов.
Команда loop внутреннего цикла передает управление на саму себя, т.е. тело внутреннего цикла состоит из единственной команды loop. В этом нет ничего незаконного. Любая команда, в том числе и loop, требует какого-то времени для своего выполнения, и повторение 64 К раз команды loop дает некоторую временную задержку (на современных процессорах порядка тысячной доли секунды).
Перейдем теперь к рассмотрению команд условных переходов.
В приведенном выше фрагменте для реализации длинного цикла использовалась команда условного перехода по равенству je. В системе команд МП 86 имеется свыше трех десятков команд условных переходов, позволяющих осуществлять переходы при наличии разнообразных усло-вий: равенства, неравенства, положительности или отрицательности ре-зультата и проч. При выполнении всех этих команд процессор анализирует содержимое регистра флагов и осуществляет (или не осуществляет) переход на указанную метку в зависимости от состояния отдельных флагов или их комбинаций. Поскольку на состояние регистра флагов влияют многие команды процессора, командами условных переходов можно пользоваться не только после команд сравнения или анализа, но и после многих других команд, если внимательно изучить влияние этих команд на флаги процессора. Приведем несколько абстрактных примеров.

В гл. 2 отмечалось, что двоичные числа, записываемые в регистры процессора или ячейки памяти, можно рассматривать, либо как числа существенно положительные, т.е. числа без знака, либо как числа со знаком. Например, адреса ячеек, разумеется, не могут быть отрицательными. Поэтому число FFFFh, если по смыслу программы оно является адресом, обозначает 65535. Если, однако, то же число FFFFh получилось в арифметической операции вычитания 2 из 1, то его надо рассматривать, как - 1. Точно так же понятие знака бессмысленно по отношению к кодам символов, которые с равным успехом могут принимать любое значение из диапазона 0...255. С другой стороны, мы можем условно считать, что коды символов первой половины таблицы ASCII положительны, а коды второй половины таблицы (у них установлен старший бит) отрицательны, и использовать для обработки символов команды, чувствительные к знаку.
В составе команд условных переходов имеются две группы команд для сравнения чисел без знака (это команды ja, jae, jb, jbc, jna, jnae, jnb и jnbe) и чисел со знаком (jg, jge, jl, jle, jng, jnge, jnl и jnle). В аббревиатурах этих команд для сравнения чисел без знака используются слова above (выше) и below (ниже), а для чисел со знаком - слова greater (больше) и less (меньше).
Разница между теми и другими командами условных переходов заключается в том, что команды для чисел со знаком рассматривают понятия "больше-меньше" применительно к числовой оси -32К...0...+32К, а команды для чисел без знака - применительно к числовой оси 0...64К. Поэтому для первых команд число 7FFFh (+32767) больше числа S000h (-32768), а для вторых число 7FFFh (32767) меньше числа S000h (32768). Аналогично, команды для чисел со знаком считают, что 0 больше, чем FFFFh (-1), а команды для чисел без знака - меньше.
Рассмотрим пример использования команд условных переходов для обработки символов. Пусть мы вводим с клавиатуры некоторую строку символов (например, имя файла), и хотим, чтобы в программе эта строка была записана прописными буквами, независимо от того, какие буквы использовались при ее вводе. Между прочим, при вводе с клавиатуры команд DOS система всегда выполняет эту операцию, поэтому и команды, и ключи, и имена файлов можно вводить как прописными, так и строчными буквами - DOS во всех случаях преобразует все буквы в прописные.

В начале программы на экран выводится служебное сообщение "Вводите!", которое служит запросом программы, адресованным пользователю. Далее с помощью функции DOS 3Fh выполняется ввод строки текста с клавиатуры. Функция 3Fh может вводить данные из разных устройств - файлов, последовательного порта, клавиатуры. Различные устройства идентифицируются их дескрипторами. При работе с файлами дескриптор каждого файла создается системой в процессе операции открытия или создания этого файла, а для стандартных устройств - клавиатуры, экрана, принтера и последовательного порта действуют дескрипторы, закрепляемые за этими устройствами при загрузке системы. Для ввода с клавиатуры используется дескриптор 0, для вывода на экран дескриптор 1.
При вызове функции 3Fh в регистр ВХ следует занести требуемый дескриптор, в регистр DX - адрес области в программе, выделенной для приема вводимых с клавиатуры символов, а в регистр СХ - максимальное число вводимых символов. Мы считаем, что пользователь не будет вводить более 80 символов. Можно ввести и меньше; в любом случае ввод строки следует завершить нажатием клавиши <Enter>. Функция 3Fh, отработав, вернет в регистре АХ реальное число введенных символов (включая коды 13 и 10, образуемые при нажатии клавиши <Enter>). В примере 3.5 число введенных символов сохраняется в ячейке actlen с целью использования далее по ходу программы.
Далее в цикле из actlen шагов выполняется анализ каждого введенного символа путем сравнения с границами диапазонов строчных русских букв. Русские строчные буквы размещаются в двух диапазонах кодов ASCII (а...п и р...с), причем для преобразования в прописные букв первого диапазона их код следует уменьшать на 20h, а для преобразования букв второго диапазона - на 50h. Поэтому анализ проводится с помощью четырех команд сравнения сmр и соответствующих команд условных переходов. Модифицированный символ записывается на то же место в буфере buf.
После завершения анализа и преобразования введенных символов, выполняется контрольный вывод содержимого buf на экран. Поскольку мы заранее не знаем, сколько символов будет введено, вывод на экран осуществляется функцией 40h, среди параметров которой указывается число выводимых символов. Так же, как и в случае функции ввода 3Fh, для функции вывода 40h в регистре ВХ необходимо указать дескриптор устройства ввода, в данном случае экрана, а в регистре DX - адрес выво-димой строки.
Коды символов являются числами без знака, и использование в данном случае команд условных переходов для чисел без знака представляется логичным и даже единственно возможным. Если, однако, внимательно рассмотреть понятия больше-меньше для чисел со знаком и без знака, то легко увидеть, что пока мы сравниваем друг с другом только "положительные" или только "отрицательные" числа, команда ja эквивалентна команде jg, а команда jb эквивалентна команде jl. Однако при сравнении, например, кодов цифр с кодами русских букв, правильный результат можно получить лишь при использовании команд переходов для чисел без знака. Впрочем, всегда нагляднее и надежнее использовать те команды, которые соответствуют существу рассматриваемых данных, даже если такой же правильный результат получится и при использовании "неправильных" команд.
Более отчетливо разница между числами со знаком и без знака проявляется при использовании арифметических операций, например, операций умножения или деления. Здесь для чисел со знаком и чисел без знака предусмотрены отдельные команды:

mul - команда умножения чисел без знака;
imul - команда умножения чисел со знаком;
div - команда деления чисел без знака;
idiv - команда деления чисел со знаком.

Поясним различия этих команд на формальных примерах.

Обе команды, mul и imul, дают в данном случае одинаковый результат, так как положительные числа со знаком совпадают с числами без знака. Не так обстоит дело при умножении отрицательных чисел.

Здесь действие команд mul и imul над одними и теми же операндами дает разные результаты. В первом примере число без знака FCh, которое интерпретируется, как 252, умножается на 4, давая в результате число без знака 3F0, т.е. 1008. Во втором примере то же число FCh рассматривается, как число со знаком. В этом случае оно составляет -4. Умножение на 4 дает FFF0h, т.е. -16.

3.3. Обработка строк
Для работы со строками, или цепочками символов или чисел (т.е. попросту говоря, с массивами произвольных данных) в МП предусмотрен ряд специальных команд:

movs - пересылка строки;
cmps - сравнение двух строк;
seas - поиск в строке заданного элемента;
lods - загрузка аккумулятора (регистров AL или АХ) из строки;
stos - запись элемента строки из аккумулятора (регистров АХ или AL).

Эти команды очень удобны, однако их использование сопряжено с некоторыми трудностями, так как процессор, выполняя эти команды, неявным образом использует ряд своих регистров. Только если все эти регистры настроены должным образом, команды будут выполняться правильно. В результате включение в программу предложения с командой, например, movs, требует иной раз 6-7 дополнительных предложений, в которых осуществляется подготовка условий для правильного выполнения этой команды.
Хотя команды обработки строк, как правило, включаются в программу без явного указания операндов, однако каждая команда, в действительности, использует два операнда. Для команд seas и stos операндом-источником служит аккумулятор, а операнд-приемник находится в памяти. Для команды lods, наоборот, операнд-источник находится в памяти, а приемником служит аккумулятор. Наконец, для команд movs и cmps оба операнда, и источник, и приемник, находятся в памяти.
Все рассматриваемые команды, выполняя различные действия, подчиняются одинаковым правилам, перечисленным ниже. Операнды, находящиеся в памяти, всегда адресуются единообразно: операнд-источник через регистры DS:SI, а операнд-приемник через регистры ES:DI. При однократном выполнении команды обрабатывают только один элемент, а для обработки строки команды должны предваряться одним из префиксов повторения. В процессе обработки строки регистры SI и DI автоматически смещаются по строке вперед (если флаг DF = 0) или назад (если флаг DF = 1), обеспечивая адресацию последующих элементов. Каждая команда имеет модификации для работы с байтами или словами (напри-мер, movsb и movsw).
Таким образом, для правильного выполнения команд обработки строк необходимо (в общем случае) предварительно настроить регистры DS:SI и ES:DI, установить или сбросить флаг DF, занести в СХ длину обрабаты-ваемой строки, а для команд seas и stos еще поместить операнд-источник в регистр АХ (или AL при работе с байтами).
Однако сама операция, после всей этой настройки, осуществляется одной командой, которая обычно даже не содержит операндов, хотя может иметь префикс повторения.
Стоит подчеркнуть, что строки, обрабатываемые рассматриваемыми командами, могут находиться в любом месте памяти: в полях данных программы, в системных областях данных, в ПЗУ, в видеобуфере. Например, с помощью команды movs можно скопировать массив данных из одной массивной переменной в другую, а можно переслать страницу текста на экран терминала. Рассмотрим несколько примеров использования команд обработки строк, ограничившись лишь теми фрагментами программ, которые имеют отношение к рассматриваемому вопросу.

Известно, что в ПЗУ BIOS, сегментный адрес которого составляет F000h (см. рис. 1.5), наряду с программами управления аппаратурой компьютера, хранятся еще и некоторые идентификаторы. Так, в восьми байтах ПЗУ, начиная с адреса F000h:FFFSh, записана в кодах ASCII дата разработки ПЗУ. В примере 3.6 выполняется чтение этой даты, сохранение ее в памяти и вывод на экран для контроля. Поскольку интересующая нас дата хранится в ПЗУ BIOS в кодах ASCII, никаких преобразований содержимого этого участка ПЗУ перед выводом на экран не требуется.
В программе осуществляется настройка всех необходимых для выполнения команды movs регистров (DS:SI, ES:DI, CX и флага DF) и одной командой movsb с префиксом rep содержимое требуемого участка ПЗУ переносится в поле bios. Перенос строки байтами подчеркивает ее формат (в строке записаны байтовые коды ASCII), однако в нашем примере, при четном числе переносимых байтов, более эффективно осуществить пере-нос по словам. В этом варианте команда movs будет фактически повторяться не 8 раз, а только 4. Для этого достаточно занести в СХ число 4 (вместо 8) и использовать вариант команды niovsw.
Для выполнения команды movs нам пришлось настроить сегментный регистр DS на сегмент BIOS. Если в дальнейшем предполагается обращение к полям данных программы, как это имеет место в примере 3-6, в регистр DS следует занести сегментный адрес сегмента данных. После этого, настроив остальные регистры для вызова функции 40h, прочитанную из BIOS строку можно вывести на экран.
В рассмотренном примере неявно предполагалось, что программа будет в дальнейшем как-то использовать полученную из BIOS информацию. Если задача программы заключается просто в выводе на экран даты выпуска BIOS, то нет необходимости сначала копировать эту дату из BIOS в поля данных программы, а потом выводить ее на экран. Можно было поступить гораздо проще: настроив регистр DS на сегмент BIOS, а регистр DX на адрес строки с датой, вызвать функцию 40h и вывести на экран текст непосредственно из сегмента BIOS. Тогда содержательная часть программы сократится в два раза и примет такой вид:

Приведенный фрагмент не имеет отношения к данному разделу, так как в нем уже нет команд обработки строк. В то же время он подчеркивает важность сегментных регистров и гибкость сегментной адресации. Функция 40h ожидает найти адрес выводимой на экран строки в регистрах DS:DX, и никакие другие регистры в этом случае использовать нельзя. С другой стороны, эти регистры можно настроить на любой участок памяти и вывести на экран (а также и на принтер, в файл или в последовательный порт) данные откуда угодно.
Рассмотрим теперь пример работы с командами lods и stos, которые можно использовать как по отдельности, так и в паре друг с другом. Эти команды очень удобны, в частности, для прямого обращения к видеопамяти.
К экрану, как и к любому другому устройству, входящему в состав компьютера, можно обращаться тремя способами: с помощью функций DOS (прерывание 21h), с использованием прерывания BIOS (для управления экраном используется прерывание 10h) и, наконец, путем прямого программирования аппаратуры, в данном случае видеобуфера (видеопамяти). Функции DOS позволяют выводить только черно-белый текст и имеют ряд других ограничений (нельзя очистить экран, нет средств позиционирования курсора); при использовании прерывания BIOS все эти ограничения снимаются, однако программирование с помощью средств BIOS весьма трудоемко; наконец, прямая запись в видеопамять, предоставляя возможность вывода цветного текста в любую точку экрана, является процедурой очень простой и, к тому же, повышает скорость вывода (по сравнением с использованием системных средств) в десятки и сотни раз. Прямое обращение к видеобуферу удобно использовать, например, в обработчиках прерываний, где запрещен вызов функций DOS и имеются ограничения на обращение к средствам BIOS.
Пусть по ходу программы необходимо вывести в нижнюю строку экрана предупреждающее сообщение. Для этого в программу надо включить следующие предложения:

Регистры DS:SI настраиваются на адрес начата выводимой строки; регистры ES:DI - на адрес требуемой позиции в видеобуфере. В регистр СХ надо поместить длину строки в байтах, а флаг DF сбросить, чтобы двигаться по строке вперед. На экран будет выводиться содержимое регистра АХ, в младшем байте которого должен находиться код ASCII выводимого символа, а в старшем байте - атрибут символа, т.е. код цвета символа (в младшем полубайте) и код цвета фона (в старшем полубайте). В примере число 31h образует синие символы по бирюзовому фону. При желании можно выбрать другую комбинацию цветов, выбрав ее с помощью табл. 3.1.

Таблица 3.1. Коды цветов стандартной цветовой палитры

Код Цвет Код Цвет
0h Черный 8h Серый
1h Синий 9h Голубой
2h Зеленый 10h Салатовый
3h Бирюзовый 11h Светло-бирюзовый
4h Красный 12h Розовый
5h Фиолетовый 13h Светло-фиолетовый
6h Коричневый 14h Желтый
7h Белый 15h Ярко-белый

Выбирая цвета, следует иметь в виду, что при стандартной настройке видеосистемы для цвета фона можно использовать лишь значения из левого столбца таблицы; выбор любого яркого цвета из правого столбца приведет в выводу мерцающего символа. Например, атрибут символа Bill образует синий мерцающий символ на бирюзовом фоне (а не синий символ на светло-бирюзовом фоне).
Содержательную часть цикла вывода образуют две команды lodsb и stosw. Первая команда загружает в регистр AL код очередного символа, вторая выводит его вместе с атрибутом, хранящемся в АН, на экран. При этом после каждого выполнения команды lodsb содержимое SI увеличивается процессором на 1, смещая адресацию к следующему символу строки; в то же время каждое выполнение команды stosw увеличивает DI на 2 (потому что команда stosw работает со словами), смещая адресацию на экране на 2 байт, т.е. как раз к позиции следующего символа.
Примеры использования команд cmps и seas можно найти в Приложении.

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

В тексте программы сначала описана процедура-подпрограмма, затем основная программа. Как уже отмечалось, порядок их описания роли не играет; важно только, чтобы в завершающей директиве окончания трансляции end был указан в качестве точки входа адрес основной программы (main в нашем примере).
Подпрограмма реализует задержку с помощью вложенных циклов с командой loop, использующей в качестве счетчика шагов регистр СХ. В основной программе этот регистр используется для организации цикла вывода трех строк. Поэтому первое, что должна сделать подпрограмма - это сохранить содержимое регистра СХ, для чего естественно использовать стек. Перед завершающей командой ret регистр СХ должен быть восстановлен. Фрагмент, реализующий задержку, был описан ранее, в разделе 3.2.
Основная программа выводит на экран с помощью функции 09h три строки текста. Для упрощения программы, а также чтобы продемонстрировать некоторые приемы программирования, вывод строк реализован в цикле. Строки сделаны одной длины, и модификация смещения к очередной строке выполняется прибавлением к содержимому регистра DX длины строки. Полезно обратить внимание на организацию цикла в основной программе. В цикл, помимо команды вызова подпрограммы задержки и предложения, модифицирующего регистр DX, включена лишь команда int 21h. Регистр АН с номером функции заново не настраивается. Это и не нужно, так как DOS, выполняя затребованную операцию, первым делом сохраняет все регистры программы, а перед возвратом в программу их восстанавливает. Поэтому, вызывая функции DOS (или BIOS) можно не заботиться о сохранении регистров - их содержимое система на разрушает. Надо только иметь в виду, что многие функции DOS и BIOS после своего завершения возвращают в программу некоторую информацию (число реально введенных символов, доступный объем памяти, номер видео-режима и т.п.) Обычно эта информация возвращается в регистре АХ, однако могут использоваться и другие регистры или их сочетания. Поэтому, обращаясь в программе к системным функциям, необходимо ознакомиться с их описанием и, в частности, посмотреть, какие регистры они могут использовать для возвращаемых значений.
Запустив программу, можно убедиться в том, что строки текста появляются на экране через заметные промежутки времени.
В примере 3-8 подпрограмма не требовала параметров. Чаще, однако, подпрограмма должна принимать один или несколько параметров и возвращать результат. В этом случае необходимо организовать взаимодействие основной программы и подпрограммы. Никаких специальных средств языка для этого не существует; передачу параметров в подпрограмму и из нее программист организует по своему усмотрению. Для передачи параметров как в одну, так и в другую сторону можно использовать регистры общего назначения, ячейки памяти или стек. Например, нетрудно преобразовать подпрограмму delay из примера 3-8 так, чтобы ей можно было передавать величину требуемой задержки. Пусть эта величина (в числе шагов внешнего цикла) передается в регистре SI.

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

Эксперименты показали, что для получения правильной задержки значение параметра, обозначающее число секунд, следует умножать на 600. Поскольку при умножении в системе команд МП 86 первый сомножитель должен находиться в регистре АХ, а второй не может быть непосредственным значением и тоже, следовательно, должен быть помещен в один из регистров, и, к тому же, произведение занимает два регистра DX:AX, приходится сохранять при входе в подпрограмму не один регистр, как в предыдущем примере, а 4. Передаваемый в SI параметр переносится в АХ, в ВХ загружается второй сомножитель, а из полученного с помощью команды mul произведения используется младшая часть, находящаяся в АХ. Таким образом, для данного варианта подпрограммы значение задержки не должно превышать 109 с (109 х 600 = 65500, что почти совпадает с максимально возможным значением 65535).
Следует обратить внимание на опасность, подстерегающую нас при выполнении операции умножения. Пусть значение передаваемого параметра составляет всего 5. При умножении на 600 получится число 3000, которое безусловно помещается в регистре АХ. Однако операция умноже-ния 16-разрядных операндов

mul BX

всегда, независимо от конкретной величины произведения, помещает его в пару регистров DX:AX, и, следовательно, при небольшой величине произведения регистр DX будет обнуляться. Поэтому, хотя мы и не используем старшую часть произведения и фактически ее может и не быть, сохранение и последующее восстановление регистра DX является обязательным.
Передача параметров в подпрограмму через регистры общего назначения или даже через сегментные регистры вполне возможна, однако на практике для передачи параметров чаще всего используют стек, хотя бы потому, что регистров немного, а в стек можно поместить любое число параметров. При этом применяется своеобразная методика работы со стеком не с помощью команд push и pop, а с помощью команд mov с кос-венной адресацией через регистр ВР, который архитектурно предназначен именно для адресации к стеку. Преобразуем пример 3-8а так, чтобы единственный в этом примере параметр (условная величина задержки) передавался в подпрограмму не через регистр SI, а через стек. Вызов подпрограммы delay в этом случае должен выполняться следующим образом:

push 2000 ;Проталкиваем в стек значение параметра
call delay ;Вызываем подпрограмму delay

Текст подпрограммы подвергнется значительным изменениям:

Команда call, передавая управление подпрограмме, сохраняет в стеке адрес возврата в основную программу. Подпрограмма сохраняет в стеке еще два 16-разрядных регистра. В результате стек оказывается в состоянии, изображенном на рис. 3.9.
После сохранения в стеке исходного содержимого регистра ВР (в основной программе нашего примера этот регистр не используется, однако в общем случае это может быть и не так), в регистр ВР копируется содержимое указателя стека, после чего в ВР оказывается смещение вершины стека. Далее командой mov в регистр СХ заносится содержимое ячейки стека, на 6 байтов ниже текущей вершины. В этом месте стека как раз находится передаваемый в подпрограмму параметр, как это показано в левом столбце рис. 3.9. Конкретную величину смещения относительно вершины стека надо для каждой подпрограммы определять индивидуально,

Рис. 3.9. Состояние стека в подпрограмме после сохранения регистров.

исходя из того, сколько слов сохранено ею в стеке к этому моменту. Напомним, что при использовании косвенной адресации с регистром ВР в качестве базового, по умолчанию адресуется стек, что в данном случае и требуется.
Параметр, полученный таким образом, используется далее в подпрограмме точно так же, как и в примере 3-8а.
Выполнив возложенную на нее задачу, подпрограмма восстанавливает сохраненные ранее регистры и осуществляет возврат в основную программу с помощью команды ret, в качестве аргумента которой указывается число байтов, занимаемых в стеке отправленными туда перед вызовом подпрограммы параметрами. В нашем случае единственный параметр занимает 2 байт. Если здесь использовать обычную команду ret без аргумента, то после возврата в основную программу параметр останется в стеке, и его надо будет оттуда извлекать (между прочим, не очень понятно, куда именно, ведь все регистры у нас могут быть заняты). Команда же с аргументом, осуществив возврат в вызывающую программу, увеличивает содержимое указателя стека на значение ее аргумента, тем самым осуществляя логическое снятие параметра. Физически этот параметр, как, впрочем, и все остальные данные, помещенные в стек, остается в стеке и будет затерт при дальнейших обращениях к стеку.
Разумеется, в стек можно было поместить не один, а сколько угодно параметров. Тогда для их чтения надо было использовать несколько команд mov со значениями смещения ВР+6, ВР+8, BP+0Ah и т.д.
Рассмотренная методика может быть использована и при дальних вызовах подпрограмм, но в этом случае необходимо учитывать, что дальняя команда call сохраняет в стеке не одно, а два слова, что повлияет на величину рассчитываемого смещения относительно вершины стека.

3.5. Двоично-десятичные числа
В гл. 2 уже говорилось о двоично-десятичных числах - специальном формате хранения данных, используемом в ряде технических приложений. Часто эти числа называют BCD-числами (от binary-coded decimal, двоично-кодированные десятичные числа). Для обработки BCD-чисел (сложения, вычитания, умножения и деления) в МП 86 предусмотрены специальные команды. Рассмотрим этот вопрос на комплексном примере обработки показаний КМОП-часов реального времени.
Как известно, в современных компьютеров имеются два независимых таймера. Один из них ("часы реального времени") включен в состав микросхемы с очень низким потреблением тока, питается от батарейки или аккумулятора, находящегося на системной плате, и работает даже на выключенной из сети машине. В этом таймере хранится и автоматически наращивается текущее календарное время (год, месяц, день, час, минута и секунда).
После включения компьютера вступает в работу другой таймер, который обычно называют системным. Датчиком сигналов времени для него служит кварцевый генератор, работающий на частоте 1,19318 МГц, сигналы от которого, после пересчета в отношении 65536:1, поступают в контроллер прерываний и инициируют прерывания через вектор 8 с частотой 18,2065 Гц. Эти прерывания активизируют программу BIOS, периодически выполняющую инкремент содержимого четырехбайтовой ячейки памяти с текущим временем, находящейся по адресу 46Ch. После включения машины программы BIOS считывают из часов реального времени текущее время суток, преобразуют его в число тактов системного таймера (т.е. в число интервалов по 1/18,2065 с) и записывают в ячейку текущего времени. Датее содержимое этой ячейки наращивается уже системным таймером, работающим в режиме прерываний.
Для определения текущего времени прикладная программа может вызвать соответствующие функции прерывания 21h DOS (конкретно, с номером 2Ah для получения даты и 2Ch для получения времени суток), а может прочитать время непосредственно из часов реального времени с помощью прерывания lAh BIOS. При этом прерывание 1А1г позволяет, помимо чтения текущего времени (функция 02h) и текущей даты (функция 04h), выполнять и целый ряд других функций, среди которых мы отметим только возможность установить "будильник", т.е. записать в микросхему часов значение календарного времени, когда часы должны выдать сигнал аппаратного прерывания. Этот сигнал через вектор 70h инициирует обработчик прерываний, входящий в состав BIOS, который проверяет, возникло ли данное прерывание в результате достижения времени установки будильника (часы реального времени могут инициировать прерывания и по других причинам), тестирует заодно батарейное питание микросхемы, а затем посылает в оба контроллера прерываний команды конца прерываний и завершается командой iret. Однако по ходу своего выполнения обработчик прерывания 70h выполняет команду hit 4Ah, которая передает управление на обработчик этого прерывания, тоже входящий в состав BIOS. Системный обработчик прерывания 4Ah ничего особенно полезного не делает, в сущности представляя собой просто программу-заглушку. Однако программист имеет возможность записать в вектор 4Ah адрес прикладного обработчика прерываний, который будет активизироваться прерыванием будильника. Функции прикладного обработчика определяет программист.
В примере 3-9 устанавливается прикладной обработчик прерывания 4All, который сам по себе вызваться никогда не будет, так как по умолча-нию будильник часов реального не работает. Если, однако, прочитать системное время с помощью функции 02h прерывания lAh, прибавить к нему некоторую величину, например, 1 секунду, и установить будильник на это время (с помощью функции 06h прерывания lAh), то через одну секунду будет активизирован наш обработчик. В примере 3-9 этот процесс сделан бесконечным: в обработчике прерываний будильника снова выполняется чтение времени, прибавление к нему 1 секунды и установка будильника на новое время. В результате наш обработчик будет вызываться каждую секунду до завершения всей программы.
Помимо служебной функции установки будильника на следующую секунду, обработчик прерываний выполняет и полезную работу: он выводит текущее время в определенное место экрана. Поскольку обработчик активизируется каждую секунду, выводимое значение времени будет обновляться каждую секунду.
Как уже говорилось, в часах реального времени значение времени хранится в виде упакованных двоично-десятичных чисел. При выполнении арифметических операций с числами BCD (а нашем случае операции заключаются в прибавлении 1) необходимо использовать предназначенные для этого команды процессора. В примере проиллюстрировано использование одной из этих команд, конкретно, команды daa.
Для того, чтобы вывести на экран значение времени, его надо преобразовать в последовательность кодов ASCII. Процедура преобразования упакованных двоично-десятичных чисел в строку символов также включена в рассматриваемый пример.

В примере 3-9 используются несколько команд, отсутствующих в МП 86: команды сохранения в стеке и восстановления всех регистров общего назначения pusha и рора, а также команда сдвига shl с числовым операндом. Для того, чтобы эти команды распознавались ассемблером, в программу включена директива .586 (можно было бы обойтись и директивой .386). В этом случае необходимо оба сегмента объявить с описателем use16.
Программа состоит из главной процедуры main, процедуры new_4a обработчика прерываний от будильника, а также трех вспомогательных процедур-подпрограмм add_time, add_unit и conv. Главная процедура сохраняет исходный вектор прерывания 4Ah, устанавливает новый обработчик этого прерывания, читает текущее время и устанавливает будильник на время, отстоящее от текущего на 1 секунду, а затем останавливается в ожидании нажатия любой клавиши. Пока программа стоит, обрабатываются прерывания от будильника и в правый верхний угол экрана каждую секунду выводится текущее время. После нажатия любой клавиши программа завершается, предварительно сбросив будильник и восстановив исходное содержимое вектора 4Ah.
Легко видеть, что в предложенном варианте программа имеет мало практического смысла, так как она не выполняет, кроме вывода времени, никакой полезной работы. В то же время, пока эта программа не завершилась, запустить другую программу нельзя, так как DOS является однозадачной системой. Если, однако, написать нашу программу в формате.СОМ и сделать ее резидентной, мы получим возможность запускать любые программы и одновременно наблюдать на экране текущее время. Такого средства в DOS нет, и в какой-то ситуации оно может оказаться полезным. Методика разработки резидентных программ описана выше; читатель может выполнить необходимые преобразования самостоятельно.
Рассмотрим теперь программу обработчика прерываний будильника. Прежде всего в нем командой pusha (push all, сохранить все) сохраняются все регистры общего назначения и, кроме того, два сегментных регистра DS и ES, которые будут использоваться в обработчике. Далее регистр DS настраивается на сегментный адрес того сегмента, в который входит ячейка hour, т.е. фактически на наш сегмент команд. На первый взгляд это действие может показаться бессмысленным. Ведь в начале процедуры main в регистр DS уже был помещен адрес нашего сегмента данных data. Зачем же эту операцию повторять? Дело в том, что процедура new_4a, будучи формально обработчиком программного прерывания 4Ah, фактически представляет собой обработчик аппаратного прерывания от часов реального времени, которое, как и любое аппаратное прерывание, может придти в любой момент времени. В принципе прерываемая программа в этот момент может выполнять любые действия, и содержимое регистра DS может быть любым. Если же говорить о нашей программе, то она находится в цикле ожидания нажатия клавиши. Этот цикл организует функция 01h DOS, которая, между прочим, время от времени обращается к своему драйверу клавиатуры, а тот - к программам BIOS ввода символа с клавиатуры. Вполне вероятно (а на самом деле так оно и есть), что при выполнении упомянутых операций используется регистр DS, который в этом случае указывает уже не на наш сегмент данных, а на различные системные области. Другими словами, при входе в обработчик прерывания содержимое регистра DS неизвестно, и его следует инициализировать заново, обязательно сохранив исходное значение. Если перед выходом из обработчика это исходное значение не восстановить, будет неминуемо разрушена DOS.
Сохранив регистры и настроив DS, мы вызываем функцию 02h прерывания lAh чтения текущего времени. Время возвращается, как уже говорилось, в упакованном двоично-десятичном формате (по две цифры в байте) в регистрах СН (часы), CL (минуты) и DH (секунды). Нам это время понадобится еще раз в конце обработчика для установки будильника заново, и чтобы второй раз не вызывать функцию 02h, полученное время (т.е. содержимое регистров СХ и DX) сохраняется в стеке.
Далее выполняется последовательное преобразование BCD-цифр, составляющих время, в коды ASCII соответствующих символов. Число часов (две упакованные BCD-цифры) переносится в регистр AL, и вызывается подпрограмма conv, которая преобразует старшую цифру часов в код ASCII и возвращает его в регистре АН. Этот код помещается в объявленную в сегменте данных строку-шаблон hour, в которой заготовлены пустые пока места для символов цифр, составляющих время, а также имеются разделительные двоеточия. Для удобства обращения к элементам этой строки, она разделена на части и каждая часть снабжена собственным именем - min для поля минут и sec для поля секунд.
Подпрограмма conv преобразования BCD-цифры в код ASCII состоит всего из трех предложений, не считая заключительной команды ret. Двух разрядное BCD-число передается в подпрограмму в регистре AL. После обнуления регистра АН, который будет служить приемником для образования конечного результата, содержимое AL сдвигается командой shl влево на 4 бит, в результате чего старший полубайт регистра AL, т.е. старшая цифра числа, перемещается в регистр АН (рис. 3.10). Двоично-десятичная цифра представляет собой просто двоичное представление цифры; прибавление к ее коду кода символа "0" (числа 30h) дает код ASCII этой цифры.
Мы преобразовали пока только старший полубайт регистра СН. Для выделения младшего полубайта на регистр СН накладывается маска 0Fh,

Рис. 3.10. Алгоритм работы подпрограммы conv.

которая обнуляет старший полубайт, не затрагивая младшего. Прибавление кода ASCII нуля к коду десятичной цифры образует код ASCII этой цифры, который и переносится затем в строку-шаблон. Описанная процедура повторяется затем для регистров CL (минуты) и DH (секунды).
Для вывода строки с временем на экран используется прямое обращение в видеопамяти. В регистр ES заносится сегментный адрес видеобуфера BS00h, а в регистр DI - требуемое смещение видеопамяти к тому месту, начиная с которого мы хотим вывести строку. В регистр SI заносится адрес строки-источника, в регистр СХ - число шагов, а в регистр АН - выбранный нами атрибут символов (красные символы по синему полю). Поскольку перемещение и по строке-шаблону, и по экрану должно осуществляться вперед, командой сld сбрасывается флаг DF. Наконец, циклическое выполнение пары команд

lodsb stosw

приводит к выводу в заданное место экрана всей строки hour.
Выполнив вывод на экран текущего времени, надо снова установить будильник. Для этого сначала запрещается работа ранее установленного будильника, восстанавливается текущее время в регистрах DX и СХ, и вызовом процедуры add_time к текущему времени прибавляется 1 секунда. Далее вызовом функции 06h заново устанавливается будильник, восстанавливаются сохраненные в начале программы обработчика регистры, и, наконец, командой iret обработчик завершает свою работу.
Рассмотрим теперь процедуру прибавления 1 к текущему времени. Она состоит из двух компонентов - подпрограммы add_time, которая организует правильное сложение чисел, обозначающих время, чтобы прибавление 1 секунды к 59 секундам дало 0 секунд и увеличило на 1 число минут (и то же самое для минут) и подпрограммы add_uuit, выполняющей прибавление 1 к упакованному коду BCD.
Подпрограмма add_time переносит число секунд из DH в AL, с помощью подпрограммы add_unit увеличивает его на 1 и возвращает в DH. Подпрограмма add_unit сигнализирует установкой флага CF о необходимости переноса 1 в следующий разряд времени (число секунд составляло 59). Поэтому после возврата из add_iuit проверяется флаг CF и, если он сброшен, т.е. следующий разряд времени модифицировать не надо, подпрограмма add_time завершается. Если же флаг CF установлен, выполняется аналогичная процедура прибавления 1 к числу минут, которое находится в регистре CL. Далее опять анализируется флаг CF, и если он установлен (текущее время было 59 мин 59 с), прибавляется 1 к числу часов. Наконец, подпрограмма завершается командой ret.
Подпрограмма add_unit получает упакованное двоично-десятичное число, к которому надо прибавить 1, в регистре AL. Командой add к нему прибавляется 1, после чего в некоторых случаях образуется правильная сумма, а в некоторых - неправильная. Так, 14h + 1 = 15h, что правильно, однако 19h + 1 = lAh, что неверно. Такого двоично-десятичного числа не существует, а после прибавления 1 к 19 должно получиться 20 (и записано в виде 20h). Коррекцию после сложения BCD-чисел осуществляет команда daa, которая в приведенном примере преобразует lAh в 20h, и которая должна всегда следовать за командой сложения.
Наши двоично-десятичные числа специфичны в том отношении, что они не могут превышать 59. Поэтому после коррекции результат сравнивается с 60h. Если сумма меньше 60h, флаг CF сбрасывается и выполняется команда ret. Если сумма равна 60h, регистр AL обнуляется, флаг CF устанавливается, сигнализируя о переносе 1 в следующий разряд времени (минут или часов) и выполняется та же команда ret. Таким образом, флаг CF процессора в точке возврата из подпрограммы add_unit говорит не о наличии или отсутствии арифметического переноса, а выполняет роль флага "исключительной ситуации" - перехода времени на следующую минуту или на следующий час. Такое нестандартное использование флага CF является общеупотребительным приемом.

3.6. Программирование аппаратных средств
Программирование аппаратуры - как штатных периферийных устройств компьютера, таких, как видеосистема, клавиатура, последовательный или параллельный интерфейс и др., так и нестандартных измерительных или управляющих устройств, подключаемых к компьютеру, если он используется для автоматизации научных исследований или управления технологическим процессом - является одним из важнейших и наиболее оправданных применения языка ассемблера. Во-первых, от программ управления аппаратурой часто требуется максимальное быстродействие. Во-вторых, эти программы, призванные управлять аппаратурой на низком уровне, путем обращения к регистрам и их отдельными битам, часто ничего не выигрывают от использования языков высокого уровня, в которых те же операции реализуются с помощью процедур языка, менее наглядных и эффективных, чем "чистые" команды процессора. В-третьих, при программировании аппаратуры, особенно, экспериментальной, важно жестко соблюдать временную и событийную последовательность команд и сигналов, воспринимаемых программируемым устройством, что естественным образом достигается при использовании языка ассемблера, в котором каждое предложение языка реализуется вполне определенной командой процессора.
В зависимости от назначения и способа функционирования аппаратуры, она может требовать различных режимов программного управления. В основном существуют три режима, или способа взаимодействия программы и аппаратуры: режим свободного доступа, режим ожидания готовности и режим прерываний.
Режим свободного доступа используется в тех случаях, когда момент обращения к устройству целиком определяется программой. Например, регистры, управляющие работой аппаратуры, обычно доступны в любой момент времени. Программа может в любой момент прочитать содержимое этих регистров и определить по нему текущий режим работы устройства, или, наоборот, послать в управляющие регистры требуемую последовательность команд с целью изменения рабочего режима.
Режим ожидания готовности необходимо использовать в тех случаях, когда после приема некоторой команды устройству требуется определенное время для ее выполнения. Например, в последовательный порт, через который компьютер связывается с другими компьютерами или телефон ной сетью, нельзя посылать следующую порцию информации (байт), пока устройствами последовательного интерфейса не будет отправлена в канат связи предыдущая порция. Режимом ожидания готовности часто пользуются для приема информации из измерительной аппаратуры, если требуется обеспечить максимальную скорость ее получения.
Режим прерываний является важнейшим способом связи с относительно медленным периферийным оборудованием. В этом случае устройство подключается не только к линиям адресов, данных и управления системной магистрали компьютера, но и к одной из специально выделенных линий прерываний. В режиме прерывания устройство само решает, когда ему требуется обслуживание, и посылкой в компьютер сигнала прерывания оповещает об этом процессор. Типичным примером является клавиатура, посылающая сигнал прерывания каждый раз, когда пользователь нажимает на ту или иную клавишу. Большая часть штатных устройств компьютера - мышь, диски, таймер и др. - используют режим прерываний. Типичен этот режим также и для связи с измерительной аппаратурой в тех случаях, когда аппаратура регистрирует относительно редкие события, или измерительные данные накапливаются в аппаратуре в течение заметного времени и затем пересылаются в компьютер сразу целой пачкой.
Как уже отмечалось в гл. 1, связь с аппаратными средствами самого компьютера, а также с подключаемыми к нему устройствами осуществляется главным образом через адресное пространство ввода-вывода. Это значит, что за каждым устройством закрепляется один или, чаще, несколько портов, и программирование устройства осуществляется исключительно с помощью команд in и out (а также ins и cuts, если программируемое устройство может посылать данные потоком).
В простейшем случае программирование устройства сводится к выполнению единственной команды in в случае чтения из устройства, или out в случае записи в него. Рассмотрим, например, процедуры маскирования и размаскирования аппаратных прерываний. В каждом из двух контроллеров прерываний, включаемых в состав компьютера, имеется регистр маски (рис. 3.11). Значение 0 в бите маски разрешает прохождение сигнала прерывания, значение 1 запрещает. Пройдя через маску и через последующие узлы контроллера прерываний (не показанные на рис. 3.11), сигнал прерываний поступает на вход INT микропроцессора. Программирование регистров маски осуществляется через порт 21h для ведущего контроллера и A1h для ведомого.
Исходное значение маски устанавливается программами начальной загрузки компьютера в зависимости от конфигурации вычислительной системы. Типичным является значение A8h, показанное на рис. 3.11. При этом значении маски размаскированными оказываются системный таймер, клавиатура, мышь, подключенная к первому последовательному порту

Рис. 3.11. Регистр маски ведущего контроллера прерываний.

СОМ1, гибкий диск, а также выход от ведомого контроллера, подключаемый ко входу IRQ2 ведущего. Замаскированы оба параллельного порта (принтер, подключаемый к порту LPT1, обычно не использует прерываний, а второй параллельный порт часто просто отсутствует) и второй последовательный порт, к которому ничего не подключено. Другими словами, размаскировано все нужное, и замаскировано все ненужное.
В раде случаев возникает необходимость замаскировать прерывания от системного таймера, который является единственным постоянно активным источником прерываний. Такая ситуация типична, в частности, для автоматизированных измерительных систем, в которых недопустимо прерывать поток данных, поступающих от измерительной установки в компьютер. Любое прерывание процесса приема данных может привесит к потере части принимаемой информации и нарушению работы установки. Для запрета прерываний от таймера надо выполнить такую последовательность команд:

in AL,21h ;Чтение регистра маски
or AL,1 ;Установка 1 в бите 0
out 21h,AL ;Запись нового значения маски

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

in AL,21h ;Чтение регистра маски
and AL, 0FEh ;Установка 0 в бите 0
out 21h,AL ;Запись нового значения маски

Другим примером использования режима свободного доступа к устройству является программирование энергонезависимой КМОП-микросхемы, включающей в себя часы реального времени, о которых уже говорилось в разделе 5 этой главы, а также информацию о конфигурации компьютера и в некоторых случаях пароль. Общий объем КМОП-памяти составляет 64 байт (от 00h до 3Fh); доступ к байтам КМОП-памяти осуществляется через порты 70h и 71h.
В КМОП-микросхеме реализован способ обращения к ее отдельным ячейкам, широко используемый в микропроцессорной технике. Если программировать КМОП-память прямым образом, для обращения к ее 64 ячейкам в адресном пространстве ввода-вывода пришлось бы выделить 64 адреса. Для сокращения числа используемых адресов в состав микросхемы введены два служебных регистра - адресный и данных. В адресный регистр (порт 70h) заносится номер той ячейки КМОП-памяти, к которой требуется обращение. После этого чтение регистра данных (порт 7 Hi) позволяет прочитать содержимое выбранной ячейки, а запись в регистр данных выполняет передачу данного в эту ячейку. Приведем полный текст программы, которая читает содержимое ячейки с номером 0Dh. В ней хранится состояние батареи, питающей КМОП-микросхему. Если бит 7 этой ячейки установлен, батарея исправна; если этот бит сброшен, напряжение батареи упало ниже допустимого предела, и ее надо менять.

;Пример 3-10. Чтение ячейки КМОП-микросхемы
code segment
assume cs:code
main proc
mov AL,ODh ;Будем читать ячейку ODh
out 70h,AL ;Задание номера ячейки
in AL,71h ;чтение из ячейки
test AL,80h ;Проверка бита 7
jnz ok ;Бит 7 = 1, перейти на OK
mov AH,02h ;Бит 7=0, питания нет
mov DL,'-' ; Выведем в знак этого
int 21h ; Символ минус
jmp exit ;Переход на завершение
ok: mov АН,02h ;Батарея в порядке,
mov DL,'+' ;выведем в знак этого
int 21h ;символ плюс
;Завершим программу
exit: mov AX,4COOh
int 21h
main endp
code ends
end main

Рассмотрим теперь программирование периферийного оборудования в режиме ожидания готовности на примере параллельного интерфейса. В стандартной конфигурации компьютера к параллельному интерфейсу обычно подключается принтер, однако его можно использовать и для связи с нестандартным (измерительным или управляющим) оборудованием.
В компьютерах используется разновидность параллельного интерфейса под названием Centronics, отличающаяся относительно высокой скоростью передачи данных (до 150 Кбайт/с) и простотой программирования. Правда, Centronics позволяет передавать данные только в одном направлении - из компьютера в устройство, однако эту проблему можно частично решить, если воспользоваться для приема данных линиями состояния интерфейса.
Разумеется, в установке, подключаемой к компьютеру через параллельный интерфейс, должно быть предусмотрено устройство сопряжения, воспринимающее и вырабатывающее сигналы обмена с интерфейсом.
Интерфейс Centronics подключается к периферийному устройству (принтеру) с помощью кабеля, содержащего 17 сигнальных линий и несколько линий нуля. Управление интерфейсом осуществляется через три закрепленных за ним порта: порта данных с адресом 378h, порта состояния принтера с адресом 379h и порта управления принтером с адресом 37Аh. Порты фактически представляют собой 8-разрядные регистры, биты которых соответствуют сигналам интерфейса. Некоторые из этих сигналов, конкретно, сигналы портов данных и управления, являются для интерфейса выходными; их должна устанавливать программа, управляющая передачей информации. Другие сигналы, наоборот, поступают из периферийного устройства и отображаются в состоянии закрепленных за ними битов порта состояния; программа должна читать и анализировать эти биты. На рис. 3.12 показаны порты интерфейса Centronics с указанием сигналов, соответствующим конкретным битам.

378h порт данных

379h порт состояния

37Ah портуправления

Рис. 3.12. Порты интерфейса Centronics

Программирование параллельного интерфейса требует некоторых сведений о его протоколе, т.е. последовательности и взаимодействии сигналов, которыми интерфейс обменивается с подключенным к нему устройством. Некоторые из этих сигналов имеют узко специализированное назначение и возникают лишь в особых случаях (например, сигнал РЕ - конец бумаги), другие же принимают обязательное участие в процедуре передачи данных. К последним относятся 8 бит данных и три управляющих сигнала STROBE', BUSY и АСК' (рис. 3.13).

Рис. 3.13. Протокол передачи данных для интерфейса Centronics.

Сигнал BUSY считается активным, когда он имеет высокое значение. В противоположность этому активное состояние сигналов STROBE' и АСК' низкое, отчего они и обозначаются с тем или иным дополнительным значком (с чертой наверху, со знаком минус или с апострофом, как у нас). Прослеживая соответствие сигналов интерфейса состоянию битов его портов, необходимо иметь в виду, что для некоторых сигналов (SLCT, РЕ, STROBE) в порты записываются их прямые значения, а для других (ERROR, АСК, BUSY) - инверсные.
Вывод на принтер каждого байта данных состоит из трех этапов. Прежде всего программа должна дождаться неактивного состояния сигналов BUSY и АСК (это и есть ожидание готовности устройства). Убедившись, что биты 6 и 7 порта состояния 379h установлены в 1 (см. рис. 3.12), програм-ма посылает в порт данных 378h байт данных, что приводит к установке кода данных на линиях интерфейса D7...D0. Наконец, программа должна установить на короткое время сигнал STROBE, что реализуется путем установки и затем сброса бита 0 порта управления 37All. Следующие бай-ты посылаются точно таким же образом.
Выполняя все эти операции, необходимо учитывать временные ха-рактеристики интерфейса. Сигнал STROBE можно посылать в порт уп-равления не ранее, чем через 0,5 мкс после установки данных, что может потребовать введению в программу небольшой программной задержки (одной или нескольких команд jmp, см. приведенный ниже текст про-граммы). То же относится и к длительности сигнала STROBE, которая не должна быть меньше той же величины 0,5 мкс. Практически программные задержки часто оказываются не нужны.
Обратимся еще раз к рис. 3.13. Принтер, сняв с линий данных байт данных, и начав его обработку (вывод на печать или сохранение во внутренней памяти), устанавливает ответный сигнал BUSY, действующий все время, пока принтер занят обработкой байта данных. Закончив обработку байта, принтер на некоторое время устанавливает сигнал АСК и сбрасывает сигнал BUSY. Окончание сигнала АСК (при сброшенном состоянии сигнала BUSY) говорит интерфейсу об окончании данной операции обмена и о возможности посылки следующего байта данных. Ввиду краткости сигнала АСК часто оказывается, что ожидать его снятия нет необходимости; достаточно дождаться неактивного состояния сигнала BUSY (т.е. 1 в бите 7 порта состояния). Вообще следует заметить, что различные принтеры могут несколько поразному выполнять свою часть протокола обмена. Рассмотренный ниже пример отлаживался на принтере Epson LQ100.
Приведем текст программы, в которой принтер программируется, как говорят, на физическом уровне, т.е. путем обращения к его портам. Разумеется, в большинстве случаев для вывода на принтер текста из выполняемой программы проще воспользоваться функциями DOS. Однако в некоторых специальных случаях приходится прибегать и к программированию через порты, например, если принтер используется в нестандартном режиме, или параллельный интерфейс служит для связи с нестандартным устройством.
В приведенном примере предполагается, что принтер выбран и установлен в исходное рабочее состояние, что обычно выполняется автоматически при его включении. Свидетельством этого будут установленные биты 2 и 3 (SLCT IN и INIT) в порте управления, а также бит 4 (SLCT) в порте состояния. В программе не выполняется анализ байта состояния на наличие ошибки или конца бумаги, что при работе с принтером, вообще говоря, следует предусматривать.
Третий метод программирования периферийного устройства - режим прерываний - рассмотрим на примере обработки прерывания от мыши. Как известно, мышь обычно подключается к первому последовательному порту СОМ1 и работает в режиме прерываний. Нажатие или отпускание любой клавиши, так же, как даже минимальное перемещение по столу, вырабатывает сигналы прерываний, сопровождаемые определенными кодами, которые поступают в порт данных интерфейса. Написав собственный обработчик прерываний для последовательного порта, мы получим возможность выполнять заданные действия, например, при нажатии левой и правой клавиш мыши. Следует подчеркнуть, что эти действия начнут выполняться практически в тот же момент, когда мы нажали на клавишу. В приведенной ниже программе при нажатии левой клавиши в центр экрана выводится цветная надпись "Левая!", а при нажатии правой клавиши - надпись "Правая" другого цвета.
Для того, чтобы приведенная программа работала, следует загрузить драйвер мыши, который инициализирует последовательный интерфейс и саму мышь. В состав этого драйвера входит свой обработчик прерываний. Мы замещаем его адрес в векторе 0Ch адресом нашего обработчика, и поскольку в программе не предусмотрено сцепление обработчиков, на время действия программы стандартная обработка прерываний от мыши отключается. Перед завершением программы содержимое вектора 0Сh восстанавливается, и мышь опять начинает работать, как обычно.
Каждое нажатие (или отпускание) клавиши мыши, так же, как и ее перемещение, в действительности вырабатывают не по одному, а по три последовательных прерывания с различными кодами в порте данных интерфейса. Так, нажатие левой клавиши мыши дает последовательность кодов 60h, 0, 0, нажатие правой клавиши - последовательность 50h, 0, 0, отпускание любой клавиши - 40h, 0, 0, перемещение вверх - 4Ch, 0, 3Fh, перемещение вниз - 40h, 0, 1 и т.д. Таким образом, по-настоящему надо было сохранять в обработчике прерываний все три кода и затем анализировать всю последовательность. Мы для простоты ограничились анализом только первого кода. Как видно из приведенного выше перечня, анализ одного кода не дает возможность отличить, например, отпускание клавиши от перемещения вниз.
Коды, генерируемые мышью, могут зависеть от ее типа, что надо учитывать при подготовке этого примера. Для получения значений генерируемых кодов можно предусмотреть в обработчике прерываний вывод их на экран с помощью функции прерывания 10h BIOS, как это было сделано, например, в примере 3-5, или прямым выводом в видеобуфер. Следует только иметь в виду, что перехват любого прерывания от последовательного интерфейса должен обязательно сопровождаться чтением из его порта данных, так как интерфейс может принять очередной байт данных только после чтения предыдущего и освобождения своего регистра данных.

;Пример 3-12. Программирование мыши в режиме прерываний
.586 ;Будут команды новых процессоров
code segment use16 ;16-разрядное приложение
assume CS : code,DS:code ;Данные в сегменте команд
main proc
push CS ;Настроим DS
pop DS ;на сегмент команд
;Сохраним обработчик прерываний последовательного порта
mov AX,350Ch ;Функция 35h, вектор 0Сh
int 21h
mov word ptr old_Oc,BX ;Сохраним смещение
mov word ptr old_Oc+2,ES ;Сохраним сегмент
;Установим наш обработчик прерываний последовательного порта
mov AX,25ОСЬ ;Функция 25h, вектор 0Сh
mov DX,offset new_0c ;Адрес нашего обработчика
int 21h
;Остановим программу функцией ввода с клавиатуры
mov AH,01h
int 21h
;Восстановим исходный обработчик драйвера мыши
mov AX,250Ch ;Функция 25h, вектор 0Сh
Ids DX,old_0c ;Сохраненный адрес
int 21h
mov AX,4C00h ;Завершим программу
int 21h
main endp
new_0c proc
pusha ;Сохраним все регистры
push DS ;Сегментные регистры не
push ES ;сохраняются командой pusha
mov DX,3F8h ;Порт данных
in AL,DX ;Прочитаем
cmp AL, 60h ;Левая клавиша — код 60h
je Ibtn ;Переход на отработку
cmp AL, 5Oh ;Правая клавиша — код 5Oh
je rbtn ;Переход на отработку
;Завершение обработчика прерываний
outret:pop ES ;Восстановим сегментные
pop DS ;регистры
mov AL,20h ;Команда EOI
out 20h,AL ;в контроллер прерываний
рора ;Восстановим все регистры
iret ;Выход из прерывания
;Если нажата левая клавиша мыши
Ibtn: mov АН, 1Eh ;Атрибут символов желтый по
; синему
mov SI,offset msgdn ;Адрес выводимой строки
jmp commn ;Ha общую часть вывода
;Если нажата правая клавиша мыши
rbtn: mov AH,2Eh ;Атрибут символов желтый по
;зеленому
mov SI,offset msgdn ;Адрес выводимой строки
;Общая часть вывода на экран диагностической строки
commn: mov BX,OB800h ;Настроим ES
mov ES,BX ;на видеобуфер
push CS ;Настроим DS
pop DS ;на наш сегмент
mov CX,6 ;Число выводимых символов
mov DI,2000 ;Смещение на экране
cld ;Движение вперед
scr: lodsb ;АL=очередной символ
stosw ;Из АХ на экран
loop scr ;Цикл
jmp outret ;После вывода завершить
;обработку прерывания
new_0c endp
old_0c dd 0 ;Ячейка для исходного
;вектора
msgdn db "Левая!" ;Выводимые сообщения
msgup db "Правая"
code ends
stk segment stack
dw 128 dup(O)
stk ends
end main

Приведенный пример с точки зрения его структуры построен обычным образом. Исходное содержимое вектора 0Ch сохраняется в ячейке old_0c и используется перед завершением программы для восстановления вектора. Для упрощения установки обработчика прерываний программа написана без сегмента данных; ее немногие данные размещены в сегменте команд. Поскольку в начале программы регистр DS настраивается на сегмент команд, адресация к данным (в основной программе) возможна через DS. Для того, чтобы можно было наблюдать обработку прерываний от мыши, основная программа после выполнения инициализирующих действий останавливается с помощью функции 01h DOS ожидания ввода символа с клавиатуры. После нажатия любой клавиши программа завершается, восстановив предварительно исходное состояние вектора последовательного порта.
Действия, которые должны инициироваться нажатием левой или правой клавиш мыши (например, включение или выключение некоторого оборудования), в программе заменены выводом на экран коротких диагностических сообщений. Вывод осуществляется прямой записью в видеобуфер, поскольку, как уже говорилось ранее, в обработчике аппаратных прерываний нельзя использовать функции DOS и рискованно - функции BIOS. Вывод на экран с помощью команд обработки строк lodsb и stosw требует настройки большого количества регистров - в DS:SI должен находиться адрес строки-источника, в ES:DI адрес позиции в видеобуфере, в СХ число выводимых символов. Кроме этого, в обработчике прерываний используются регистры АХ, ВХ и DX. Для сохранения всех регистров общего назначения используется команда pusha, а для их восстановления команда рора. Однако эти команды не принимают в расчет сегментные регистры, и их приходится сохранять и восстанавливать от дельными командами.
С восстановлением регистров может возникнуть некоторая сложность. Обработчик прерывания должен завершаться посылкой в контроллер прерываний команды EOI, а для этого необходим регистр AL. Поэтому восстановление регистров, во всяком случае, регистра АХ, необходимо выполнять после команды EOI. С другой стороны, команда EOI разблокирует нижележащие уровни прерываний в контроллере прерываний (см. гл. 3), что может привести к прохождению через контроллер очередного (вложенного в наше) прерывания, которое прервет наш обработчик в точке, где еще не восстановлены регистры. Это неминуемо приведет к краху системы. Однако в процессоре предусмотрены меры устранения этого неприятного явления. Остановимся на них более подробно.
Процессор, приняв любой сигнал прерывания, сбрасывает флаг IF в своем регистре флагов, запрещая тем самым все аппаратные прерывания. Поэтому вход в обработчик прерываний всегда осуществляется при запрещенных прерываниях. Блокировка нижележащих уровней в контроллере прерываний просто накладывается на этот общий запрет и новых ограничений не вносит.
Если в тексте обработчике прерываний нет команды разрешения прерываний sti, то прерывания будут запрещены до самого его конца, до завершающей команды iret. Эта команда извлекает из стека и восстанавливает исходное содержимое регистров CS:IP, а также регистра флагов. В момент прерывания в регистре флагов был безусловно установлен флаг IF, иначе прерывание не могло бы возникнуть. Восстановление регистра флагов приводит к установке этого флага и разрешению всех аппаратных прерываний, но уже после завершения обработчика прерываний. Таким образом, снятие аппаратной блокировки прерываний командой EOI в действительности не приводит к разрешению прерываний, и любые строки, стоящие после этой команды, выполняются при запрещенных прерываниях. В результате никаких проблем с восстановлением регистров после команды EOI не возникает.
Обычно, однако, используется другой вариант построения обработчика прерываний. В этом варианте в начале программы обработчика выполняется команда sti, устанавливающая флаг IF и разрешающая все аппаратные прерывания, кроме тех, которые заблокированы в контроллере прерываний. В результате программа обработчика может быть прервана любым прерыванием более высокого уровня IRQ (т.е. уровня с меньшим номером), но не прерывается сигналами прерываний этого же и более низких уровней. Такое построение обработчиков прерываний удобно тем, что "более важные" прерывания, например, от таймера или клавиатуры, могут быть обработаны без задержки. Для того, чтобы исключить возможные неприятности с восстановлением регистра АХ после команды EOI, перед ней прерывания запрещаются командой cli и структура обработчика прерываний приобретает приблизительно такой вид:

sti
pusha ;Сохранение регистров
... ;Тело обработчика
cli ;Запрещение всех прерываний
mov AL,20h ;Команда EOI
out 20h,AL ;контроллеру прерываний
рора ;Восстановление регистров
iret ;Возврат из обработчика
Команды рора и iret выполняются в этому случае при запрещенных прерываниях, но после отработки команды iret в регистре флагов восстанавливается его исходное содержимое (в котором IF = 1), и прерывания, таким образом, снова разрешаются.


Глава 4

4. Расширенные возможности современных микропроцессоров

    • 4.1. Архитектурные особенности

    • 4.2. Дополнительные режимы адресации

    • 4.3. Использование средств 32-разрядных процессоров в программировании

    • 4.4. Основы защищенного режима

4.1. Архитектурные особенности
Операционная система MS-DOS, язык ассемблера МП 86 и методы программирования микропроцессоров корпорации Intel разрабатывались применительно к 16-разрядному процессору 8086 и тому режиму, который впоследствии получил название реального. Появление процессора 80386 знаменовало собой начато нового этапа в развитии операционных систем и прикладного программирования - этапа многозадачных графических операционных систем защищенного режима типа Windows и 32-разрядных прикладных программ. При этом, как уже отмечалось во введении, все архитектурные средства 86-го процессора входят в состав любого современного процессора, который, таким образом, можно условно разделить на две части - МП 86 и дополнительные средства, обеспечивающие защищенный режим, 32-разрядную адресацию и прочее. Из этих дополнительных средств можно выделить те, которые обеспечивают защищенный режим, и в реальном режиме не используются (во всяком случае, явным образом; в действительности, процессор, даже работая в реальном режиме, использует по крайней мере некоторые из этих средств). Сюда, например, относятся регистры таблиц дескрипторов, регистры тестирования и отладки, привилегированные команды защищенного режима, система страничного отображения адресов и др. С другой стороны, часть новых свойств современных процессоров можно использовать и в реальном режиме, выполняя программы под управлением MS-DOS. Сюда, прежде всего, относится использование 32-битовых операндов, некоторых новых команд процессора и расширенных возможностей старых команд. Настоящая глава будет в основном посвящена именно этим средствам процессоров 80386, i486 и Pentium, которые в дальнейшем мы будем обобщенно называть 32-разрядными процессорами. Вопрос о программировании защищенного режима слишком сложен, чтобы его можно было осветить в рамках этой книги, хотя основные принципы защищенного режима будут описаны.
32-разрядные процессоры содержат несколько десятков программно-адресуемых регистров (не считая регистров сопроцессора), из которых шесть являются 16-разрядными, а остальные - 32-разрядными. Регистры принято объединять в семь групп: регистры общего назначения (или регистры данных), регистры-указатели, сегментные регистры, управляющие регистры, регистры системных адресов, отладочные регистры и регистры тестирования. Кроме того, в отдельную группу выделяют счетчик команд и регистр флагов. Регистры, используемые в реальном режиме, показаны на рис. 4.1.

Рис. 4.1. Основные регистры 32-разрядных процессоров.

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

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

Все сегментные регистры, как и в МП 86, являются 16-разрядными. В их состав включено еще два регистра - FS и GS, которые могут использоваться для хранения сегментных адресов двух дополнительных сегментов данных. Таким образом, при использовании расширенных возможностей современных процессоров программе одновременно доступны четыре сегмента данных, а не два, как в МП 86.
Регистр указателя команд также является 32-разрядным и обычно при описании процессора его называют EIP. Младшие шестнадцать разрядов этого регистра соответствуют регистру IP процессора МП 86. Весь регистр EIP используется только в 32-разрядных приложениях; в 16-разрядных программах адреса могут быть только 16-разрядными и, соответственно, для адресации в программном сегменте используется младшая половина регистра EIP.
Регистр флагов принято называть EFLAGS (от extended flags, расширенные флаги). Хотя он имеет длину 32 бит, только младшие 18 бит (да и то не все) содержат значащую информацию. Дополнительно к шести флагам состояния (CF, PF, AF, ZF, SF и OF) и трем флагам управления состоянием процессора (TF, IF и DF), назначение которых было описано в гл. 1, он включает новые флаги задачи, рестарта и виртуального режима, а также двухбайтовое поле привилегий ввода-вывода. Все эти биты используются только в защищенном режиме и здесь рассматриваться не будут.

4.2. Дополнительные режимы адресации
Режимы адресации 32-разрядных процессоров разработаны, исходя из требований образования 32-битового смещения. Другими словами, они предназначены для 32-разрядных приложений, в которых сегменты данных или стека (как, впрочем, и сегменты команд) могут иметь размеры до 232 = 4 Гбайт. Однако в реальном режиме размер любого сегмента ограничивается величиной 216 = 64 Кбайт, и 32-битовые смещения не имеют смысла. С другой стороны, ничто не мешает нам использовать для образования 16-битового смещения 32-разрядные регистры (ЕВХ, ESI и проч.), если, конечно, их реальное содержимое не будет превышать величины FFFFh. Указание в качестве операндов команд 32-разрядных регистров позволяет использовать дополнительные возможности 32-разрядных процессоров по части адресации памяти, что в некоторых случаях может оказаться полезным. Следует подчеркнуть, что речь идет здесь только о тех операндах, или, правильнее сказать, аргументах команды, которые описывают косвенную (через регистры) адресацию памяти.
В отличие от МП 86, где базовыми регистрами могут быть только ВХ и ВР, а индексными только SI и DI, 32-разрядные процессоры допускают использование в качестве и базовых, и индексных практически всех регистров общего назначения. Таким образом, вполне законна команда вида

mov ЕАХ,[ЕСХ][EDX]

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

inc word ptr [ЕАХ] [ЕСХ*2]

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

inc word ptr [AX] [ECX*2]

или

inc word ptr [ЕАХ] [СХ*2]

рассматриваются ассемблером, как неправильные.
Режимы косвенной адресации памяти, предоставляемые 32-разрядными процессорами при использовании 32-разрядных регистров, изображены на рис. 4.2.
Из рисунка видно, что в качестве базового можно использовать все регистры общего назначения, включая даже указатель стека ESP. При этом, если в качестве базового выступает один из регистров ESP или ЕВР, то по умолчанию адресация осуществляется через сегментный регистр SS, хотя возможна замена сегмента. Во всех остальных случаях адресация по умолчанию осуществляется через сегментный регистр DS. Использование регистра ЕВР в качестве индексного не адресует нас к стеку: адресация попрежнему осуществляется с помощью регистра DS.

Рис. 4.2. Режимы косвенной адресации с использованием 32-разрядных регистров.

Прочерк во второй колонке подчеркивает, что регистр ESP нельзя использовать в качестве индексного. Это не означает, что ESP нельзя указывать в качестве второго операнда:

mov ЕАХ,[ЕСХ][ESP]

Недопустима только конструкция, в которой содержимое ESP умножается на масштабирующий множитель:

mov ЕАХ,[ЕСХ][ESP*8]

Полезно также отметить, что смещение в команде вида

mov ЕАХ,[ЕВХ][ЕСХ]+20

может быть только или 8-битовым, или 32-битовым. 16-битовые смещения не образуются. Если указанная в команде величина смещения помещается в байт, как это имеет место в приведенном выше примере команды, то смещение в коде команды занимает 1 байт. Если же величина смещения больше 255, то под него в коде команды отводится сразу 32 бит.
Таким образом, понятия базовой и индексной адресации в 32-разрядных процессорах несколько размываются. Если регистр указывается с масштабирующим множителем, то это, конечно, индексная адресация. Если же множитель отсутствует, то адресацию и через ЕВХ, и через ESI с равным успехом можно отнести как к базовой, так и к индексной.
Использование для адресации памяти 16-разрядных регистров резко сужает возможности адресации 32-разрядных процессоров (рис. 4.3). В этом случае мы фактически имеем дело с МП 86.

Рис. 4.3. Режимы косвенной адресации с использованием 16-разрядных регистров.

Напомним, что в 16-разрядном режиме допустимы не все сочетания базовых и индексных регистров. В качестве базового регистра можно использовать только ВХ или ВР, а в качестве индексного только SI или DI.

4.3. Использование средств 32-разрядных процессоров в программировании
Как уже отмечалось, при разработке 16-разрядных программ реального режима, предназначенных для выполнения по управлением операционной системы MS-DOS, вполне допустимо использование ряда дополнительных возможностей 32-разрядных процессоров. В реальном режиме можно использовать:
32-разрядные операнды;
дополнительные команды и расширенные возможности команд МП 86;
дополнительные режимы адресации;
четыре сегментных регистра для адресации данных вместо двух.
Для того, чтобы транслятор распознавал все эти средства, необходимо начать программу с директивы .586 (или, при желании, .486 или .386) и указать при этом для сегментов команд и данных описатель use 16, чтобы программа осталась 16-разрядной.
Следует заметить, что возможности использования в программах реального режима дополнительных средств 32-разрядных процессоров, хотя и кажутся привлекательными, в действительности весьма ограничены. Новых команд не так уж много, и они не имеют принципиального характера; 32-разрядные данные используются в прикладных программах относительно редко (если не касаться вычислительных программ, содержащих действительные числа, но такие программы редко пишут на языке ассемблера); расширенные возможности адресации в полной мере проявляются лишь в 32-разрядных программах, не работающих в DOS. Тем не менее в каких-то случаях привлечение средств 32-разрядных процессоров может оказаться полезным и в 16-разрядных программах, и мы приведем несколько примеров их использования.
Среди системных данных DOS и BIOS есть данные, требующие для своего размещения 2 слов. К таким данным, в частности, относится системное время, накапливаемое в 4х-байтовой ячейке с абсолютным адресом 46Ch. Выше, в разделе 3.5, уже описывалась системная процедура отсчета текущего времени. В процессе начальной загрузки компьютера в ячейку с адресом 46Ch переносится из часов реального времени время, истекшее от начала суток, а затем содержимое этой ячейки увеличивается на 1 каждым прерыванием от системного таймера, подключенного к вектору 8. Чтение ячейки 46Ch позволяет определить текущее время с погрешностью приблизительно в 1/18 секунды, что позволяет достаточно точно измерять интервалы времени. Арифметические действия с системным временем удобно выполнять в расширенных 32-разрядных регистрах.
Рассмотрим программу, которая позволяет установить требуемый временной интервал и отработать некоторым образом его окончание. Поскольку MS-DOS является однозадачной системой, единственным способом организации параллельных процессов - выполнения программы и ожидания окончания временного интервала - является использование механизма прерываний. В нашем случае программа содержит обработчик прерываний от системного таймера, который 18 раз в секунду читает системное время и сравнивает его значение с заданной заранее величиной. При достижении равенства обработчик прерываний либо сам отрабатывает это событие, либо устанавливает флаг окончания временного интервала, который периодически тестируется в основной программе. Первый вариант позволяет измерить временной интервал с большей точностью, но второй предоставляет больше возможностей, так как в обработчике прерываний нельзя обращаться к функциям DOS, а основная программа может делать все, что ей заблагорассудится.
Приведенный ниже пример выполнен в виде программы типа .СОМ. Такая организация программы упрощает обработчик прерываний и облегчает его написание. Дело заключается в том, что процессор, переходя по аппаратному прерыванию на обработчик прерывания, модифицирует только регистры CS:IP (значениями, полученными из вектора прерываний). Все остальные регистры, в том числе и сегментные, сохраняют те значения, которые они имели на момент прерывания. Значения эти могут быть какими угодно, особенно, если основная программа вызывает функции DOS. Поэтому, если в обработчике прерываний необходимо обратиться к данным, хранящимся в основной программе, нам необходимо настроить какой-либо из сегментных регистров (например, DS или ES) на сегментный адрес сегмента данных основной программы. Если же программа написана в формате .СОМ, то ее поля данных входят в тот же (единственный) сегмент, где расположены команды, и для обращения к данным можно воспользоваться регистром CS, который при вызове обработчика настраивается процессором.

; Пример 4-1. Чтение и сравнение системного времени
;по прерываниям от таймера
.586 ;Будут 32-разрядные операнды
assume CS : code, DS:code
code segment use16 ;16-разрядное приложение
org 100h ;Формат .COM
main proc
;Сохраним исходный вектор
mov AX,3508h
int 21h
mov word ptr old_08,BX
mov word ptr old_08+2,ES
;Установим наш обработчик
mov AX,2508h
mov DX,offset new_08
int 21h
;Прочитаем системное время, прибавим требуемый интервал
;и сохраним в двухсловной ячейке памяти
mov AX,40h ;Настройка ES на
mov ES,AX ;область данных BIOS
mov EAX, ES : 6Ch ;Получаем системное время
add EAX,time_int ;Прибавить интервал
mov time_end,EAX ;Сохраним в памяти
;Имитация рабочего цикла программы с опросом флага
again: test flag,0FFh ;Проверка флага готовности
jnz ok ;Если установлен, на OK
mov АН,02h ;B ожидании окончания
mov DL,'.' ;временного интервала
int 2 In ;выводим на экран точки
jmp again ;И снова на проверку флага
ok: mov АН,09h ;Интервал завершен.
mov DX,offset msg ;Выполним, что хотели
int 2 In
;Завершим программу, восстановив сначала исходный вектор
lds DX,old_08
mov AX,2508h
int 21h
mov AX,4C00h
int 21n
main endp
;Наш обработчик прерываний от системного таймера
new_08 proc
pushf ;Запишем флаги в стек
call CS:old_08 ;и вызовем системный обработчик
push EAX ;Сохраним используемые
push ES ;регистры
mov AX,40h ;Настроим ES
mov ES,AX ;на область данных BIOS
mov EAX,ES:6Ch;Прочитаем текущее время
cmp EAX,CS:time_end ;Сравним с вычисленным
jb ex ;Если меньше, на выход
inc CS:flag ;Интервал истек, установим флаг
ex: mov AL,20h ;Команда конца прерывания
out 20h,AL ;в контроллер прерываний
pop ES ;Восстановим
pop EAX ;сохраненные регистры
iret ;Выход из обработчика
new_08 endp
;Поля данных программы
old_08 dd 0 ;Для хранения исходного вектора
time_int dd 18*2 ;Требуемый интервал (~2с)
time_end dd 0 ;Момент истечения интервала
flag db 0 ;Флаг истечения интервала
msg db "Время истекло !$' ;Информационное сообщение
code ends
end main

Организация программного комплекса с обработчиком прерываний от системного таймера уже рассматривалась в примере 3-3 в гл. 3. Установка обработчика в рассматриваемом примере выполняется немного проще, так как нет необходимости настраивать регистр DS на сегмент данных - он и так уже настроен на единственный сегмент программы. Установив обработчик, программа настраивает регистр ES на область данных BIOS и считывает из ячейки с адресом 46Ch текущее системное время командой add к нему прибавляется заданный в ячейке time_int интервал (в примере - приблизительно 2 с), и результат сохраняется в ячейке timc_cnd.
Действия по установке обработчика закончены, и программа может приступить к выполнению запланированных для нее действий. В данном примере программа в цикле вызывает функцию DOS 02h вывода на экран символа точки; в действительности она может, например, выполнять обработку и вывод на экран некоторых данных. В каждом шаге цикла происходит тестирование флага окончания временного интервала flag, который должен быть установлен обработчиком прерываний по истечении заданного временного интервала. Пока флаг сброшен, цикл продолжается. Как только флаг окажется установлен, программа приступает к выполнению действий по отработке этого события. В рассматриваемом примере выполняется вывод на экран информационного сообщения и завершение программы с обязательным восстановлением исходного содержимого вектора 8.
Обработчик прерываний new_08 прежде всего выполняет вызов исходного обработчика, адрес которого мы сохранили в ячейке old_08. Методика сцепления обработчиков прерываний рассматривалась в гл.З (см. пример 3-4). В данном случае сцепление обработчиков необходимо, так как подключение к вектору 8 нашего обработчика не должно нарушить ход системных часов.
После возврата из системного обработчика выполняется сохранение используемых регистров, настройка регистра ES на область данных BIOS, чтение текущего времени и сравнение его с записанным в ячейке time_end. Пока текущее время меньше заданного, обработчик просто завершается командой iret, послав предварительно в контроллер прерываний команду конца прерывания EOI и восстановив сохраненные ранее регистры. Если же заданный временной интервал истек, и текущее время оказывается равным (или большим) значению в ячейке time_end, обработчик перед своим завершением устанавливает флаг flag, инициируя в основной программе запланированные для этого события действия. Если такими действиями должно быть, например, включение или выключение аппаратуры, подключенной к компьютеру, это можно сделать в самом обработчике прерываний. В этом случае флаг flag не нужен, и действия основной программы и обработчика прерывании протекают параллельно и независимо.
Рассмотренную программу нетрудно модифицировать так, чтобы флаг flag устанавливался не после истечения заданного интервала, а в заданный момент календарного времени. Эта задача позволит нам проиллюстрировать приемы выполнения арифметических операций с 32-разрядными операндами.
Пример 4-2 отличается от предыдущего только изменением алгоритма вычисления времени и служебными полями данных. Процедуры установки обработчика прерываний, цикла ожидания установки флага и самого обработчика прерываний полностью совпадают с примером 4-1.
Для получения требуемого значения времени в тех же единицах, которые используются системой при работе с ячейкой 46Ch, надо сначала вычислить время в секундах от начала суток, а затем для получения времени в тактах таймера умножить эту величину на 18,2065 (см. раздел 3.5). Для того, чтобы не привлекать арифметический сопроцессор и оставаться в рамках целых 32-битовых чисел, умножение числа секунд на 18.2065 выполняется по следующей формуле:

Такты = t*18 + t/5 + t/154

Отлаживая на машине пример 4-2, надо следить за тем, чтобы заданное время было больше текущего по машинным часам, иначе программа будет вечно ожидать установки флага. Попытка завершить ее нажатием комбинации <Ctrl>/<C> приведет к зависанию системы, так как в этом случае не будут выполнены строки восстановления исходного содержимого перехваченного программой вектора. По-настоящему в программах, содержащих обработчики каких-либо прерываний, используемых системой, необходимо предусматривать собственные средства обработки нажатия <Ctrl>/<C>, чтобы аварийное завершение программы выполнялось так же корректно, как и штатное, с предварительным с восстановлением векторов.

;Пример 4-2. Ожидание заданного момента времени
;по прерываниям от таймера
.586
assume CS:code,DS:code
code segment use16
org 100h
main proc
;Сохраним исходный вектор
 ...
;Установим наш обработчик
 ...
;Преобразуем требуемое календарное время в количество
;интервалов по 55 мс
mov EAX,hour ;Возьмем часы
mov EBX,3600 ; Коэффициент преобразования в секунды
mul EBX ;Преобразуем часы в секунды в EDX:EAX
mov temp,EAX ;Сохраним часы в temp
mov EAX,min ;Возьмем минуты
mov EBX,60 ;Коэффициент преобразования в секунды
mul EBX ;Преобразуем минуты в секунды в EDX:EAX
add temp,EAX ;Прибавим минуты в temp
mov EAX,sec ;Возьмем секунды
add temp,EAX ;Прибавим секунды в temp
mov EAX,temp ;Число секунд
mov EBX,18 ;Будем умножать на 18
mul EBX ;Умножим на 18
mov time,EAX ;Сохраним в time
xor EDX,EDX ;Подготовимся к делению
mov EAX,temp ;Будем делить число секунд
mov EBX,5 ;Будем делить на 5
div EBX ;Поделим
add time,EAX ;Прибавим к time
xor EDX,EDX ;Подготовимся к делению
mov EAX,temp ;Будем делить число секунд
mov EBX,154 ;Будем делить на 154
div EBX ;Поделим
add time,EAX ;Прибавим к time
;Имитация рабочего цикла программы с опросом флага
...
;Завершим программу, восстановив сначала исходный вектор
...
main endp
new_08 proc
...
new_08 endp
old_08 dd 0
hour dd 13 ;Часы
min dd 45 ;Минуты
sec dd 0 ;Секунды
time dd 0 ;Вычисленное время в тактах таймера
temp dd 0 ;Ячейка для промежуточного результат
flag db 0 ;Флаг наступления заданного времени
msg db "Время наступило!$'
code ends
end main

Рассмотрим некоторые детали приведенного примера.
Три ячейки для хранения заданного времени (часов, минут и секунд) объявлены оператором dd, как двойные слова для упрощения программы и ускорения загрузки этих значений в расширенный регистр ЕАХ. Если бы мы, экономя память, отводимую по данные, объявили бы эти ячейки как байты, то загрузка, например, числа часов в регистр ЕАХ выглядела бы следующим образом:

xor EAX,EAX
mov AL,hour

Для преобразования часов в секунды мы должны число часов умножить на 3600. Оба сомножителя (3600 и максимум 23) представляют собой небольшие числа, которые поместились бы в 16-разрядный регистр. Однако результат может достигнуть величины 82800, которая в регистр АХ уже не поместится. Если бы мы выполнили умножение двух 16-разрядных регистров, например, АХ на ВХ, то результат (по правилам выполнения команды mul) оказался бы в паре регистров DX:AX, и нам пришлось бы эти два числа объединять в одно несколькими операциями переноса и сдвига:

push AX ; Сохраняем на время АХ
mov AX,DX ;Старшая половина произведения
sal ЕАХ,1б ;Сдвигаем в старшую половину ЕАХ
pop AX ;Младшая половина произведения

Выполняя умножение с использованием 32-разрядных регистров, мы получаем результат опять же в паре регистров EDX:EAX, но поскольку в нашем случае произведение никогда не превысит 4 Г, все оно целиком будет находиться в одном регистре ЕАХ, и мы избавляемся от приведенной выше процедуры. Результат умножения сохраняется во вспомогательной ячейке temp.
Аналогичным образом выполняется перевод числа минут в секунды; полученный результат прибавляется к содержимому ячейки temp.
Число секунд преобразовывать не надо, оно просто прибавляется к содержимому temp.
Полученное число секунд умножается на 18, и результат помещается в ячейку time, которая затем будет опрашиваться в обработчике прерываний.
К полученному числу тактов таймера надо прибавить еще две корректирующих величины - результаты деления числа секунд на 5 и на 154. При использовании в операции деления 32-разрядных регистров делимое помещается в пару регистров EDX:EAX. В нашем случае делимое целиком помещается в ЕАХ, и регистр EDX необходимо обнулить. Для этого можно было выполнить команду

mov ЕАХ,0

но более эффективна операция

хоr ЕАХ,ЕАХ

которая при любом содержимом ЕАХ оставляет в нем 0.
При делении EDX:EAX на ЕВХ частное помещается в ЕАХ, остаток в EDX. Остаток нас не интересует, а частное (первая корректирующая величина) прибавляется к содержимому ячейки temp.
Аналогичным образом то же число секунд из ячейки tmp делится на 154, и результат прибавляется к содержимому time. Преобразование закончено.
В заключение рассмотрим пример упорядочивания массива 32-разрядных чисел в убывающем порядке методом пузырьковой сортировки. В приведенном алгоритме используются расширенные возможности адресации 32-разрядных процессоров.

;Пример 4-3. Пузырьковая сортировка
.586
assume CS:code,DS:data
code segment use16
main proc
mov AX, data ;Настроим DS
mov DS,AX ;на сегмент данных
mov ESI,offset list ;ESI-> начало массива
mov ECX,1000 ;Число элементов в массиве
start: mov EDX, 0 ;Индекс сравниваемой пары
sort: cmp EDX,ECX ;Индекс пары дошел до
jge stop ;индекса массива? К следующей паре
mov EAX,[ESI+EDX*4+4];Второй элемент пары
cmp [ESI+EDX*4],EAX ;Сравним с предыдущим
jge noswap ;Если первый больше, то хорошо
xchg [ESI+EDXM] , EAX ;Первый меньше. Обменять
mov [ESI+EDXM + 4],EAX ;первый на второй
noswap: inc EDX ;Увеличим индекс пары
jmp sort ;И на сравнение
stop: loop start ;Цикл по всем элементам
mov AX,4C00h
int 21h
main endp
code ends
data segment
list label ;Имя тестового массива
nmb=0 ;Заполним массив на этапе
rept 1000 /трансляции числами от 0
ddnmb /до 990
nmb=nmb+10 /через 10
endm
data ends
stk segment stack
dw 128 dup (0)
stk ends
end main

Алгоритм пузырьковой сортировки предусматривает выполнение двух вложенных циклов. Во внутреннем цикле сравниваются пары элементов. Первый элемент берется по адресу [ESI + EDX * 4], второй - по следующему адресу [ESI + EDX * 4 + 4]. Если второй элемент больше первого, происходит обмен значений этих элементов, и элемент с меньшим значением "всплывает" на одно место выше (т.е. перемещается по большему адресу). После этого увеличивается индекс пары и выполняется сравнение второго элемента со следующим. Если оказывается, что следующий элемент больше предыдущего, они меняются местами. В результате элемент с самым маленьким значением всплывает на самый верх списка.
Внутренний цикл, пройдясь по всем парам, повторяется сначала, обеспечивая всплывание следующего по величине элемента. Каждый следующий проход внутреннего цикла требует на 1 меньше шагов, чем предыдущий. После завершения упорядочивания элементы выстраиваются по возрастающим адресам в порядке уменьшения их значений.
В примере 4-3 тестовый массив данных образован из возрастающих (на 10) чисел от 0 до 990. В результате упорядочивания они должны расположиться в обратном порядке, от больших к меньшим. В примере не предусмотрены средства вывода на экран элементов массива, поэтому его изучение следует проводить в отладчике, наблюдая всплывание каждого элемента.
Как уже отмечалось, в 32-разрядных процессорах увеличено до 4 число сегментных регистров данных. Это дает возможность совместной работы с четырьмя сегментами данных (общим объемом до 256 Кбайт) без перенастройки сегментных регистров. Структура такого рода программы может выглядеть следующим образом:

.586
datal segment use16
first dw 7000h dup(')
datal ends
data2 segment use6
second dw 7000h dup (')
data2 ends
data3 segment use16
third dw 7000h dup (')
data3 ends
data4 segment use16
forth dw 7000h dup (')
data4 ends
code segment use16
assume DS:datal,ES:data2,FS:data3,GS:data4
main proc
;Настроим все 4 сегментных регистра на базовые адреса
; соответствующих сегментов
mov AX,datal ;DS->datal
mov word ptr[BX],1111h ;Обращение через DS по умолчанию
;Обращение к разным сегментам с явным указанием
;требуемого сегментного регистра (замена сегмента)
mov word ptr ES:[BX],2222h
mov word ptr FS:[BX],3333h
mov word ptr GS:[BX],4444h
;Обращение по именам полей данных разных сегментов ; с учетом действия директивы assume
mov first,1 ;Запись в сегмент datal
mov second,2 ;Запись в сегмент data2
mov third,3 ;Запись в сегмент data3
mov fourth,4 ;Запись в сегмент data4
; Перенос данных из сегмента в сегмент
push first
pop second+2
push third
pop fourth+2
...
main endp
code ends

В программе объявлены 4 сегмента данных с именами datal, data2, data3 и data4, содержащие массивы 16-разрядных данных с именами first, second, third и fourth. Длина каждого массива составляет 56 Кбайт, и, таким образом, общий объем данных, доступных программе в любой момент, составляет более 200 Кбайт. Сегменты данных описаны до сегмента команд, что в данном случае имеет значение. В сегменте команд с помощью директивы assume указано соответствие каждому из сегментов своего сегментного регистра (DS, ES, FS и GS). Это даст нам возможность обращаться по именам полей сегментов без явного указания соответствующих этим сегментам сегментных регистров.
Программа начинается с обычной практически для всех программ процедуры настройки всех сегментных регистров. Стоит еще раз повторить, что директива assume лишь обеспечивает правильную трансляцию программы, но не инициализирует сегментные регистры. "Правильная трансляция" в данном случае заключается в том, что при обработке команд, в которых упоминаются имена данных того или иного сегмента, ассемблер автоматически предваряет эти команды, префиксом замены сегмента, выбирая для замены сегментный регистр, указанный в директиве assume для данного сегмента. Так, команда

mov first, I

преобразуется в последовательность кодов (по листингу' трансляции)

С7 06 0000r 0001

где С7 06 - это код команды mov в случае прямой адресации памяти и использования непосредственного операнда, 0000г - смещение адресуемой ячейки, а 0001 - непосредственный операнд (все числа, разумеется, шестнадцатеричные). Здесь нет префикса замены сегмента, потому что адресуется сегмент, которому соответствует регистр DS, используемый процессором по умолчанию. Однако команды с обращением к другим сегментам транслируются с включением в их коды соответствующих пре фиксов, несмотря на то, что в исходных предложениях не указаны сегментные регистры, а содержатся только ссылки на (уникальные) имена ячеек тех или иных сегментов:

mov second, 2 ; Код команды 26: С7 06 0000r 0002
mov third, 3 ;Код команды 64: С7 06 0000r 0003
mov fourth, 4 ; Код команды 65: С7 06 0000r 0004

Настроив сегментные регистры, мы можем обращаться к полям данных всех четырех сегментов с использованием любых способов адресации. В приведенном фрагменте в регистр ВХ помещается смещение последней ячейки любого из массивов, после чего с помощью косвенной базовой адресации в последние слова всех четырех массивов записываются произвольные числа 1111h, 2222h, 3333h и 4444h. Во всех случаях требуется описатель word ptr, так как по виду команды ассемблер не может определить, хотим ли мы занести в память байт, слово или двойное слово. При обращении к сегментам, адресуемых не через DS, необходимо явное указание сегментного регистра (которое будет преобразовано в код префикса замены сегмента), потому что по виду команды с адресацией через регистры транслятор не может определить, к какому сегменту происходит обращение.
Проще обстоит дело, если в команде указаны имена ячеек сегментов. В этом случае, как уже говорилось выше, транслятор автоматически включает в код команды требуемый префикс замены сегмента.
Наконец, в конце программы приведены строки пересылки данных из сегмента в сегмент через стек. Они убедительны в том отношении, что в четырех последовательных командах производится обращение к четырем различным сегментам программы без перенастройки сегментных регистров, которую пришлось бы выполнить, если бы мы ограничились возможностями МП 86.

4.4. Основы защищенного режима
Микропроцессоры Pentium, так же, как и его предшественники (начиная с 80268), могут работать в двух режимах: реального адреса и виртуального защищенного адреса. Обычно эти режимы называют просто реальным и защищенным. В реальном режиме 32-разрядные микропроцессоры функционируют фактически так же, как МП 86 с повышенным быстродействием и расширенным набором команд. Многие весьма привлекательные возможности микропроцессоров принципиально не реализуются в реальном режиме, который введен лишь для обеспечения совместимости с предыдущими моделями процессоров. Характерной особенностью реального режима является ограничение объема адресуемой оперативной памяти величиной 1 Мбайт.
Только перевод микропроцессора в защищенный режим позволяет полностью реализовать все возможности, загаженные в его архитектуру и недоступные в реальном режиме. Сюда можно отнести:
- увеличение адресуемого пространства до 4 Гбайт;
- возможность работать в виртуальном адресном пространстве, превышающем максимально возможный объем физической памяти и составляющем огромную величину 64 Тбайт;
- организация многозадачного режима с параллельным выполнением нескольких программ (процессов). Собственно говоря, многозадачный режим организует многозадачная операционная система, однако микропроцессор предоставляет необходимый для этого режима мощный и надежный механизм защиты задач друг от друга с помощью четырехуровневой системы привилегий;
- страничная организация памяти, повышающая уровень защиты задач
друг от друга и эффективность их выполнения.
При включении микропроцессора в нем автоматически устанавливается режим реального адреса. Переход в защищенный режим осуществляется программно путем выполнения соответствующей последовательности команд. Поскольку многие детали функционирования микропроцессора в реальном и защищенном режимах существенно различаются, программы, предназначенные для защищенного режима, должны быть написаны особым образом. При этом различия реального и защищенного режимов настолько велики, что программы реального режима не могут выполняться в защищенном режиме и наоборот. Другими словами, реальный и защищенный режимы не совместимы.
Архитектура современного микропроцессора необычайно сложна. Столь же сложными оказываются и средства защищенного режима, а также программы, использующие эти средства. С другой стороны, при решении многих прикладных задач (например, при отладке приложений Windows, работающих, естественно, в защищенном режиме), нет необходимости в понимании всех деталей функционирования защищенного режима; достаточно иметь знакомство с его основными понятиями. В настоящем разделе дается краткое описание основ защищенного режима и его наиболее важных структур и алгоритмов функционирования.
Пожалуй, наиболее важным отличием защищенного режима от реального является иной принцип формирования физического адреса. Вспомним, что в реальном режиме физический адрес адресуемой ячейки памяти состоит из двух компонентов - сегментного адреса и смещения. Оба компонента имеют размер 16 бит, и процессор, обращаясь к памяти, пользуется следующим правилом вычисления физического адреса:

Физический адрес = сегментный адрес * 16 + смещение

И сегментный адрес, и смещение не могут быть больше FFFFh, откуда следуют два важнейших ограничения реального режима: объем адресного пространства составляет всего 1 Мбайт, а сегменты не могут иметь размер, превышающий 64 Кбайт.
В защищенном режиме программа по-прежнему состоит из сегментов, адресуемых с помощью 16-разрядных сегментных регистров, однако местоположение сегментов в физической памяти определяется другим способом.
В сегментные регистры в защищенном режиме записываются не сегментные адреса, а так называемые селекторы, биты 3...15 которых рассматриваются, как номера (индексы) ячеек специальной таблицы, содержащей дескрипторы сегментов программы. Таблица дескрипторов обычно создастся операционной системой защищенного режима (например, системой Windows) и, как правило, недоступна программе. Каждый дескриптор таблицы дескрипторов имеет размер 8 байт, и в нем хранятся все характеристики, необходимые процессору для обслуживания этого сегмента. Среди этих характеристик необходимо выделить в первую очередь две: адрес сегмента и его длину (рис. 4.4).

Рис. 4.4. Дескрипторы сегментов и их селекторы.

Под адрес сегмента в дескрипторе выделяется 32 бит, и, таким образом, сегмент может начинаться в любой точке адресного пространства объемом 23- = 4 Гбайт. Это адресное пространство носит название линейного. В простейшем случае, когда выключено страничное преобразование, о котором речь будет идти позже, линейные адреса отвечают физическим. Таким образом, процессор может работать с оперативной памятью объемом до 4 Гбайт.
Как и в реальном режиме, адрес адресуемой ячейки вычисляется процессором, как сумма базового адреса сегмента и смещения:
Линейный адрес = базовый адрес сегмента + смещение
В 32-разрядных процессорах смещение имеет размер 32 бит, поэтому максимальная длина сегмента составляет 2" = 4 Гбайт.
На рис. 4.4 приведен гипотетический пример программы, состоящей из трех сегментов, первый из которых имеет длину 1 Мбайт и расположен в начале адресного пространства, второй, размером 100 Кбайт, вплотную примыкает к первому, а третий, имеющий размер всего 256 байт, расположен в середине девятого по счету мегабайта.
Адреса, используемые программой для обращения к ячейкам памяти, и состоящие всегда из двух компонентов - селектора и смещения - иногда называются виртуальными. Система сегментной адресации преобразует виртуальные адреса в линейные. Поскольку таблица дескрипторов, с помощью которой осуществляется это преобразование, обычно недоступна программе, программа может не знать, в каких именно участках логического адресного пространства находятся се компоненты. Фактически это сводится к тому, что, загружая программу в память, вы не знаете, в каких местах памяти будут находиться ее сегменты, и каков будет порядок их размещения. Программисту доступны только виртуальные адреса, преобразование же их в линейные и затем в физические берет на себя операционная система.
Каков объем виртуального адресного пространства? Программа указывает номер нужного ей дескриптора с помощью селектора, в котором для индекса дескриптора отведено 13 бит. Отсюда следует, что в дескрипторной таблице может быть до 1" = 8. К дескрипторов. Однако в действительности их в два раза больше, так как программа может работать не с одной, а с двумя дескрипторными таблицами - одной глобальной, разделяемой всеми выполняемыми задачами, и одной локальной, принадлежащей конкретной задаче. В селекторе предусмотрен специальный бит (бит 2), состояние которого говорит о типе требуемой программе дескрипторной таблицы. Таким образом, всего программе могут быть доступны 214 = 16 К дескрипторов, т.е. 16 К сегментов. Поскольку размер каждого сегмента, определяемый максимальной величиной смещения, может достигать 2-1 = 4 Гбайт, объем виртуального адресного пространства оказывается равным 16 К * 4 Кбайт = = 64 Тбайт.
Реально, однако, оперативная память компьютера с 32-разрядной адресной шиной не может быть больше 4 Гбайт, т.е. при сделанных выше предположениях (16 К сегментов размером 4 Гбайт каждый) в памяти может поместиться максимум один сегмент из более чем 16 тысяч. Где же будут находиться все остальные?
Полный объем виртуального пространства может быть реализован только с помощью многозадачной операционной системы, которая хранит все неиспользуемые в настоящий момент сегменты на диске, загружая их в память по мере необходимости. Разумеется, если мы хотим полностью реализовать возможности, заложенные в современные процессоры, нам потребуется диск довольно большого объема - 64 Тбайт. Однако и при нынешних более скромных технических средствах (память до 100 Мбайт, жесткий диск до 10 Гбайт) принцип виртуальной памяти используется всеми многозадачными операционными системами с большой эффективностью. С другой стороны, для прикладного программиста этот вопрос не представляет особого интереса, так как сброс сегментов на диск и подкачка их с диска осуществляются операционной системой, а не программой, и вмешательство эту процедуру вряд ли целесообразно.
Как уже отмечалось, адрес, вычисляемый процессором на основе селектора и смещения, относится к линейному адресному пространству, не обязательно совпадающему с физическим. Преобразование линейных адресов в физические осуществляется с помощью так называемой страничной трансляции, частично реализуемой процессором, а частично - операционной системой. Если страничная трансляция выключена, все ли-нейные адреса в точности совпадают с физическими; если страничная трансляция включена, то линейные адреса преобразуются в физические в соответствии с содержимым страничных таблиц (рис. 4.5).

Рис. 4.5. Цепочка преобразований виртуального адреса в физический.

Страницей называется связный участок линейного или физического адресного пространства объемом 4 Кбайт. Программа работает в линейном адресном пространстве, не подозревая о существовании страничного преобразования или даже самих страниц. Механизм страничной трансляции отображает логические страницы на физические в соответствии с информацией, содержащейся в страничных таблицах. В результате отдельные 4х-килобайтовыс участки программы могут реально находиться в любых несвязных друг с другом 4х-килобайтовых областях физической памяти (рис. 4.6). Порядок размещения физических страниц в памяти может не соответствовать (и обычно не соответствует) порядку следования логических страниц. Более того, некоторые логические страницы могут перекрываться, фактически сосуществуя в одной и той же области физической памяти.
Страничная трансляция представляет собой довольно сложный механизм, в котором принимают участие аппаратные средства процессора и находящиеся в памяти таблицы преобразования. Назначение и взаимодействие элементов системы страничной трансляции схематически изображено на рис. 4.7.
Система страничных таблиц состоит из двух уровней. На первом уровне находится каталог таблиц страниц (или просто каталог страниц) - резидентная в памяти таблица, содержащая 1024 4х-байтовых поля с адресами таблиц страниц. На втором уровне находятся таблицы страниц, каждая из которых содержит так же 1024 4х-байтовых поля с адресами физических страниц памяти. Максимально возможное число таблиц страниц определяется числом полей в каталоге и может доходить до 1024. Поскольку размер страницы составляет 4 Кбайт, 1024 таблицы по 1024 страницы перекрывают все адресное пространство (4 Гбайт).

Рис. 4.6. Отображение логических адресов на физические.

Рис. 4.7. Страничная трансляция адресов.

Не все 1024 таблицы страниц должны обязательно иметься в наличии (кстати, они заняли бы в памяти довольно много места - 4 Мбайт). Если программа реально использует лишь часть возможного линейного адресного пространства, а так всегда и бывает, то неиспользуемые поля в каталоге страниц помечаются, как отсутствующие. Для таких полей система, экономя память, не выделяет страничные таблицы.
При включенной страничной трансляции линейный адрес рассматривается, как совокупность трех полей: 10-битового индекса в каталоге страниц, 10-битовбго индекса в выбранной таблице страниц и 12-битового смещения в выбранной странице. Напомним, что линейный адрес образуется путем сложения базового адреса сегмента, взятого из дескриптора сегмента, и смещения в этом сегменте, предоставленного программой.
Старшие 10 бит линейного адреса образуют номер поля в каталоге страниц. Базовый адрес каталога хранится в одном из управляющих регистров процессора, конкретно, в регистре CR3. Из-за того, что каталог сам представляет собой страницу и выровнен в памяти на границу 4 Кбайт, в регистре CR3 для адресации к каталогу используются лишь старшие 20 бит, а младшие 12 бит зарезервированы для будущих применений.
Поля каталога имеют размер 4 байт, поэтому индекс, извлеченный из линейного адреса, сдвигается влево на 2 бит (т.е. умножается на 4) и полученная величина складывается с базовым адресом каталога, образуя адрес конкретного поля каталога. Каждое поле каталога содержит физический базовый адрес одной из таблиц страниц, причем, поскольку таблицы страниц сами представляют собой страницы и выровнены в памяти на границу 4 Кбайт, в этом адресе значащими являются только старшие 20 бит.
Далее из линейного адреса извлекается средняя часть (биты 12...21), сдвигается влево на 2 бит и складывается с базовым адресом, хранящимся в выбранном поле каталога. В результате образуется физический адрес страницы в памяти, в котором опять же используются только старшие 20 бит. Этот адрес, рассматриваемый, как старшие 20 бит физического адреса адресуемой ячейки, носит название страничного кадра. Страничный кадр дополняется с правой стороны младшими 12 битами линейного адреса, которые проходят через страничный механизм без изменения и играют роль смещения внутри выбранной физической страницы.
Рассмотрим абстрактный пример, позволяющий проследить цепочку преобразования виртуального адреса в физический. Пусть программа выполняет команду

mov ЕАХ,DS:[ЕВХ]

при этом содержимое DS (селектор) составляет 1167И, а содержимое ЕВХ (смещение) 31678U.
Старшие 13 бит селектора (число 116U) образуют индекс дескриптора в системной дескрипторной таблице. Каждый дескриптор включает в себя довольно большой объем информации о конкретном сегменте и, в частности, его линейный адрес. Пусть в ячейке дескрипторной таблицы с номером 116h записан линейный адрес (базовый адрес сегмента) 0l0Sl000h.
Тогда полный линейный адрес адресуемой ячейки определится, как сумма базового адреса и смещения:
Базовый адрес сегмента 0l0Sl000h
Смещение 0003167811
Полный линейный адрес 0108267811
При выключенной табличной трансляции величина 010826У811 будет представлять собой абсолютный физический адрес ячейки, содержимое которой должно быть прочитано приведенной выше командой mov. Легко сообразить, что эта ячейка находится в самом начале 17-го мегабайта оперативной памяти.
Посмотрим, как будет образовываться физический адрес при использовании страничной трансляции адресов. Полученный линейный адрес надо разделить на три составляющие для выделения индексов и смещения (рис. 4.8)

Рис. 4.8. Пример линейного адреса.

Индекс каталога составляет 4h. Умножение его на 4 даст смещение от начала каталога. Это смещение равно 10h.
Индекс таблицы страниц оказался равным 82h. После умножения на 4 получаем смещение в таблице страниц, равное в данном случае 210h.
Предположим, что регистр CR3 содержит число S000h. Тогда физический адрес ячейки в каталоге, откуда надо получить адрес закрепленной за данным участком программы таблицы страниц, составит S000h + l0h = 8010h. Пусть по этому адресу записано число 4602lh. Его 12 младших битов составляют служебную информацию (в частности, бит 1 свидетельствует о присутствии этой таблицы страниц в памяти, а бит 5 говорит о том, что к этой таблице уже были обращения), а старшие биты, т.е. число 46000h образуют физический базовый адрес таблицы страниц. Для получения адреса требуемой ячейки этой таблицы к базовому адресу надо прибавить смещение 210h. Результирующий адрес составит 462101г.
Будем считать, что по адресу 4621011 записано число 01FF502111. Отбросив служебные биты, получим адрес физической страницы в памяти 01FF5000U. Этот адрес всегда оканчивается тремя нулями, так как страницы выровнены в памяти на границу 4 Кбайт. Для получения физического адреса адресуемой ячейки следует заполнить 12 младших бит полученного адреса битами смещения из линейного адреса нашей ячейки, в которых в нашем примере записано число 678h. В итоге получаем физический адрес памяти 01FF567811, расположенный в конце 32-го Мбайта.
Как видно из этого примера, и со страничной трансляцией, и без нее вычисление физических адресов адресуемых ячеек выполняется в защищенном режиме совсем не так, как в реальном. Неприятным практическим следствием правил адресации защищенного режима является уже упоминавшаяся "оторванность" прикладной программы от физической памяти. Программист, отлаживающий программу защищенного режима (например, приложение Windows), может легко заглянуть в сегментные регистры и определить селекторы, выделенные программе. Однако селекторы абсолютно ничего не говорят о физических адресах, используемых программой. Физические адреса находятся в таблицах дескрипторов, а эти таблицы недоступны прикладной программе. Таким образом, программист не знает, где в памяти находится его программа или используемые ею области данных.
С другой стороны, использование в процессе преобразования адресов защищенных системой таблиц имеет свои преимущества. Обычно многозадачная операционная система создает для каждой выполняемой задачи свой набор таблиц преобразования адресов. Это позволяет каждой из задач использовать весь диапазон виртуальных адресов, при этом, хотя для разных задач виртуальные адреса могут совпадать (и, как правило, по крайней мере частично совпадают), однако сегментное и страничное преобразования обеспечивают выделение для каждой задачи несовпадающих областей физической памяти, надежно изолируя виртуальные, адресные пространства задач друг от друга.
Вернемся теперь к таблицам дескрипторов и рассмотрим их более детально. Существует два типа дескрипторных таблиц: таблица глобальных дескрипторов (GDT от Global Descriptor Table) и таблицы локальных дескрипторов (LDT от Local Descriptor Table).Обычно для каждой из этих таблиц в памяти создаются отдельные сегменты, хотя в принципе это не обязательно. Таблица глобальных дескрипторов существует в единственном экземпляре и обычно принадлежит операционной системе, а локальных таблиц может быть много (это типично для многозадачного режима, в котором каждой задаче назначается своя локальная таблица).
Виртуальное адресное пространство делится на две равные половины. К одной половине обращение происходит через GDT, к другой половине через LDT. Как уже отмечалось, все виртуальное пространство состоит из 214 сегментов, из которых 213 сегментов адресуются через GDT, и еще 213 - чрез LDT.
Когда многозадачная система переключает задачи, глобальная таблица остается неизменной, а текущая локальная таблица заменяется на локальную таблицу новой задачи. Таким образом, половина виртуального пространства в принципе доступна всем задачам в системе, а половина переключается от одной задачи к другой по мере переключения самих задач.
Для программирования защищенного режима и даже для отладки прикладных программ, работающих в защищенном режиме, полезно представлять себе структуру дескриптора и смысл его отдельных полей. Следует заметить, что существует несколько типов дескрипторов, которым присущи разные форматы. Так, дескриптор сегмента памяти (наиболее распространенный тип дескриптора) отличается от дескриптора шлюза, используемого, в частности, для обслуживания прерываний. Рассмотрим формат дескриптора памяти (рис. 4.9).

Рис. 4.9. Формат дескриптора памяти.

Как видно из рисунка, дескриптор занимает 8 байт. В байтах 2...4 и 7 записывается линейный базовый адрес сегмента. Полная длина базового адреса - 32 бит. В байтах 0-1 записываются младшие 16 бит границы сегмента, а в младшие четыре бита байта атрибутов 2 - оставшиеся биты 16...19. Границей сегмента называется номер его последнего байта. Мы видим, что граница описывается 20-ю битами, и ее численное значение не может превышать 1М. Однако, единицы, в которых задается граница, можно изменять, что осуществляется с помощью бита дробности G (бит 7 байта атрибутов 2). Если G=0, граница указывается в байтах; если 1 - в блоках по 4 Кбайт. Таким образом, размер сегмента можно задавать с точностью до байта, но тогда он не может быть больше 1 Мбайт; если же установить G=l, то сегмент может достигать 4 Гбайт, однако его размер будет кратен 4 Кбайт. База сегмента и в том, и в другом случае задастся с точностью до байта.
Рассмотрим теперь атрибуты сегмента, которые занимают два байта дескриптора.
Бит A (Accessed, было обращение) устанавливается процессором в тот момент, когда в какой-либо сегментный регистр загружается селектор данного сегмента. Далее процессор этот бит не сбрасывает, однако его может сбросить программа (разумеется, если она имеет доступ к содержимому дескриптора, что обычно является прерогативой операционной системы). Анализируя биты обращения различных сегментов, программа может судить о том, было ли обращение к данному сегменту' после того, как она сбросила бит А.
Тип сегмента занимает 3 бит (иногда бит А включают в поле типа, и тогда тип занимает 4 бит) и может иметь 8 значений. Тип определяет правила доступа к сегменту. Так, если сегмент имеет тип 1, для него разрешены чтение и запись, что характерно для сегментов данных. Назначив сегменту тип 0, мы разрешим только чтение этого сегмента, защитив его тем самым от любых модификаций. Тип 4 обозначает разрешение исполнения, что характерно для сегментов команд. Используются и другие типы сегментов.
Подчеркнем, что защита сегментов памяти от несанкционированных его типом действий выполняется не программой, и даже не операционной системой, а процессором на аппаратном уровне. Так, при попытке записи в сегмент типа 0 возникнет так называемое исключение общей защиты. Исключением называется внутреннее прерывание, возбуждаемое процессором при возникновении каких-либо неправильных с его точки зрения ситуаций. Попытка записи в сегмент, для которого запись запрещена, и относится к такого рода ситуациям. Исключению общей защиты соответствует вектор 13, в котором должен находиться адрес обработчика этого исключения.
Стоит еще обратить внимание на тип 4. Для сегмента команд разрешается только исполнение, но не запись и даже не чтение. Это значит, что в защищенном режиме программа не может случайно залезть в свой сегмент команд и затереть его; не может она также и сознательно модифицировать команды в процессе своего выполнения - методика, иногда используемая в программах реального режима для защиты от их расшифровки любознательными программистами
Бит 4 байта атрибутов 1 является идентификатором сегмента. Если он равен 1, как это показано на рис. 4.9, дескриптор описывает сегмент памяти. Значение этого бита 0 характеризует дескриптор системного сегмента.
Поле DPL (Descriptor Privilege Level, уровень привилегий дескриптора) служит для защиты программ друг от друга. Уровень привилегий может принимать значения от 0 (максимальные привилегии) до 3 (минимальные). Программам операционной системы обычно назначается уровень 0, прикладным программам - уровень 3, в результате чего исключается возможность некорректным программам разрушить операционную систему. С другой стороны, если прикладная программа сама выполняет функции операционной системы, переводя процессор в защищенный режим и работая далее в этом режиме, ее сегментам следует назначить наивысший (нулевой) уровень привилегий, что откроет ей доступ ко всем средствам защищенного режима.
Бит Р говорит о присутствии сегмента в памяти. В основном он используется для организации виртуальной памяти. С помощью этого бита система может определить, находится ли требуемый сегмент в памяти, и при необходимости загрузить его с диска. В процессе выгрузки ненужного пока сегмента на диск бит Р в его дескрипторе сбрасывается.
Младшая половина байта атрибутов 2 занята старшими битами границы сегмента. Бит AVL (от Available, доступный) не используется и не анализируется процессором и предназначен для использования прикладными программами.
Бит D (Default, умолчание) определяет действующий по умолчанию размер для операндов и адресов. Он изменяет характеристики сегментов двух типов: исполняемых и стека. Если бит D сегмента команд равен 0, в сегменте по умолчанию используются 16-битовые адреса и операнды, если 1 - 32-битовые.
Атрибут сегмента, действующий по умолчанию, можно изменить на противоположный с помощью префиксов замены размера операнда (66h) и замены размера адреса (67п). Таким образом, для сегмента с D=0 префикс 66h перед некоторой командой заставляет ее рассматривать свои операнды, как 32-битовые, а для сегмента с D=l тот же префикс 66h, наоборот, сделает операнды 16-битовыми. В некоторых случаях транслятор сам включает в объектный модуль необходимые префиксы, в других случаях их приходится вводить в программу "вручную".
Рассмотрим теперь для примера простую программу, которая, будучи запущена обычным образом под управлением MS-DOS, переключает процессор в защищенный режим, выводит на экран для контроля символ, переходит назад в реальный режим (чтобы не вывести компьютер из равновесия) и завершается стандартным для DOS образом.
Для того, чтобы наша программа могла бы хоть что-то сделать в защищенном режиме, для нее необходимо создать среду защищенного режима, в первую очередь, таблицу глобальных дескрипторов с описанием всех сегментов, с которыми программа будет работать. Кроме нас никто эту таблицу (при работе в DOS) не создаст. Таким образом, наша программа будет в какой-то мере выполнять функции операционной системы защищенного режима.
Для практического исследования защищенного режима придется выполнить некоторую работу по переконфигурированию компьютера. В наше время компьютеры обычно конфигурируются так, что при их включении сразу загружается система Windows. Работы, для которых требуется DOS, выполняются либо в режиме эмуляции DOS, либо в сеансе DOS, организуемом системой Windows. Для запуска прикладной программы защищенного режима такой способ не годится. Нам понадобится DOS в "чистом виде", без следов Windows. Более того, перед запуском программы необходимо выгрузить все драйверы обслуживания расширенной памяти (HIMEM.SYS и EMM386.EXE) и программы, использующие расширенную память, например, SMARTDRV.EXE. Лучше всего загружать DOS с системной дискеты, подготовив файлы CONFIG.SYS и AUTOEXEC.BAT в минимальном варианте.
Обсуждая в начале этого раздела основы защищенного режима, мы не затронули многие, в том числе принципиальные вопросы, с которыми придется столкнуться при написании работоспособной программы. Необходимые пояснения будут даны в конце этого раздела.
Пример 4-4. Программирование защищенного режима
.586Р ;Разрешение трансляции всех команд МП 586
;Структура для описания дескрипторов сегментов
dcr struc ;Имя структуры
limit dw 0 ;Граница (биты 0...15)
base_l dw 0 ;База, биты 0...15
base_m db 0 ;База, биты 16...23
attr_l db 0 ;Байт атрибутов 1
attr_2 db ;Граница (биты 16...19) и атрибуты 2
base_h db 0 ;База, биты 24...31
dcr ends ;
data segment use16 ;
;Таблица глобальных дескрипторов GDT
gdt_null dcr <0,0,0,0,0,0> ;Селектор 0-обязательный
;нулевой дескриптор
gdt_data dcr <data_size-l,0,0,92h,0,0> ;Селектор 8,
;сегмент данных
gdt_code dcr <code_size-l,0,0,98h,0,0>;Селектор 16,
;сегмент команд
gdt_stack dcr <511,0,0,92h,0,0> ;Селектор 24 -
;сегмент стека
gdt_screen dcr <4095,B000h,OBh,92h,0,0> ;Селектор 32,
;видеобуфер
pdescr df 0 ;Псевдодескриптор для команды Igdt
data_size=$-gdt_null ;Размер сегмента данных
data ends ;Конец сегмента данных
text segment use16 ;Сегмент команд, 16-разрядный режим
assume CS:text,DS:data;
main proc ;
xor EAX,EAX ;Очистим ЕАХ
mov AX,data ;Загрузим в DS сегментный
mov DS,AX ;адрес сегмента данных
;Вычислим 32-битовый линейный адрес сегмента данных
;и загрузим его в дескриптор сегмента данных в GDT.
;В регистре АХ уже находится сегментный адрес.
;Умножим его на 16 сдвигом влево на 4 бита
shl ЕАХ,4 ;В ЕАХ линейный базовый адрес
mov EBP, ЕАХ ;Сохраним его в ЕВР для будущего
mov BX,offset gdt_data ;В ВХ адрес дескриптора
mov [BX].base_l,AX ;Загрузим младшую часть базы
rol ЕАХ,16 ;Обмен старшей и младшей половин ЕАХ
mov [BX].base_m,AL ;Загрузим среднюю часть базы
;Вычислим 32-битовый линейный адрес -сегмента команд
;и загрузим его в дескриптор сегмента команд в GDT
хог ЕАХ, ЕАХ ;Очистим ЕАХ
mov AX,CS ;Сегментный адрес сегмента команд
shl ЕАХ,4 ;В ЕАХ линейный базовый адрес
mov BX,offset gdt_code ;В ВХ адрес дескриптора
mov [BX] .base_l,AX ;Загрузим младшую часть базы
rol ЕАХ,16 ;Обмен старшей и младшей половин ЕАХ
mov [BX].base_m,AL ;Загрузим среднюю часть базы
;Вычислим 32-битовый линейный адрес сегмента стека
хог ЕАХ, ЕАХ ;Все, как и для других
mov AX,SS ;дескрипторов
shl ЕАХ,4
mov BX,offset gdt_stack
mov [BX].base_l,AX
rol EAX,16
mov [BX].base_m,AL
;Подготовим псевдодескриптор pdescr для загрузки регистра GDTR
mov dword ptr pdescr+2,EBP ;База GDT
mov word ptr pdescr, 39 ;Граница GDT
Igdt pdescr ;Загрузим регистр GDTR
cli ;Запрет прерываний
;Переходим в защищенный режим
mov EAX,CR0 ;Получим содержимое CR0
or EAX,1 ;Установим бит защищенного режима
mov CRO,ЕАХ ;Запишем назад в CR0
 ;---------------------------------------------------------;
;Теперь процессор работает в защищенном режиме ;
;---------------------------------------------------------;
;Загружаем в CS:IP селектор:смещение точки continue
db OEAh ;Код команды far jmp
dw offset continue ;Смещение
dw 16 ;Селектор сегмента команд
continue:
;Делаем адресуемыми данные
mov AX, 8 ;Селектор сегмента данных
mov DS,AX ;Загрузим в DS
;Делаем адресуемым стек
mov AX,24 ;Селектор сегмента стека
mov SS,AX ;Загрузим в SS
;Инициализируем ES и выводим символ
mov AX,32 ;Селектор сегмента видеобуфера
mov ES,AX ;Загрузим в ES
mov BX,2000 ;Начальное смещение на экране
mov AX,09FOFh ;Символ с атрибутом
mov ES : [BX] , АХ;Вывод в видеобуфер
;Вернемся в реальный режим
mov gdt_data.limit,0FFFFh ;Установим
mov gdt_code.limit,0FFFFh ;значение границы
mov gdt_stack.limit,0FFFFh;для реального
mov gdt_screen.limit,0FFFFh ;режима
mov AX,8 ;Загрузим теневой регистр
mov DS,AX ;сегмента данных
mov AX,24 ;To же для
mov SS,AX ;стека
mov AX,32 ;To же
mov ES, AX ;для регистра ES
;Выполним дальний переход, чтобы заново загрузить
;селектор в CS и модифицировать его теневой регистр
db0Eah ;Код команды jmp far
dwoffset go ;Смещение точки перехода
dw!6 ;Селектор сегмента команд
;Переключим режим процессора
go: mov EAX,CR0 ;Получим содержимое CR0
and EAX,0FFFFFFFEh;Сбросим бит РЕ
mov CR0,EAX ;Запишем назад в CR0
db0Eah ; Код команды far jmp
dwoffset return ;Смещение точки перехода
dwtext ;Сегментный адрес
;---------------------------------------------;
;Теперь процессор снова работает в реальном режиме ;
;---------------------------------------------;
;Восстановим операционную среду реального режима
return: mov AX,data ;Загрузим сегментный
mov DS,AX ;регистр DS
mov AX,stk ;Загрузим сегментный
mov SS,AX ;регистр SS
mov SP,512 ;Восстановим SP
sti ;Разрешим прерывания
mov AX,4C00h ;Завершим программу
;обычным образом
int 2 In main endp
code_size=$-main ;Размер сегмента команд
text ends /Конец сегмента команд
stk segment stack ;Сегмент
db 512 dup (') ;стека stk ends
end main ;Конец программы и точка входа
Для того, чтобы разрешить использование всех, в том числе привилегированных команд 32-разрядных процессоров, в программу включена директива .586Р.
Программа начинается с объявления структуры dcr, с помощью которой будут описываться дескрипторы сегментов. Сравнивая описание структуры dcr в программе с рис. 4.9, нетрудно проследить их соответствие друг другу. Для удобства программного обращения в структуре dcr база описывается тремя полями: младшим словом (base_l) и двумя байтами: средним (base_m) и старшим (base_h).
В байте атрибутов 1 задается ряд характеристик сегмента. В примере 4.4 используются сегменты двух типов: сегмент команд, для которого байт attr_l должен иметь значение 98h (присутствующий, только исполнение, DPL=0), и сегмент данных (или стека) с кодом 92h (присутствующий, чтение и запись, DPL=0).
Некоторые дополнительные характеристики сегмента указываются в старшем полубайте байта attr_2. Для всех наших сегментов значение этого полубайта равно 0 (бит G=0, так как граница указывается в байтах, а D=0, так как программа 16-разрядная).
Сегмент данных data начинается с описания важнейшей системной структуры - таблицы глобальных дескрипторов. Как уже отмечалось выше, обращение к сегментам в защищенном режиме возможно исключительно через дескрипторы этих сегментов. Таким образом, в таблице дескрипторов должно быть описано столько дескрипторов, сколько сегментов использует программа. В нашем случае в таблицу включены, помимо обязательного нулевого дескриптора, всегда занимающего первое место в таблице, четыре дескриптора для сегментов данных, команд, стека и дополнительного сегмента данных, который мы наложим на видеобуфер, чтобы обеспечить возможность вывода в него символов. Порядок дескрипторов в таблице (кроме нулевого) не имеет значения.
Поля дескрипторов для наглядности заполнены конкретными данными явным образом, хотя объявление структуры dcr с нулями во всех полях позволяет описать дескрипторы несколько короче, например:

gdt_null dcr <> ;Селектор 0 - обязательный
;нулевой дескриптор
gdt_data dcr <data_size - l, , , 92h> ;Селектор 8 - сегмент данных

В дескрипторе gdt_data, описывающем сегмент данных программы, заполняется поле границы сегмента (фактическое значение размера сегмента data_size будет вычислено транслятором, см. последнее предложение сегмента данных), а также байт атрибутов 1. База сегмента, т.е. линейный адрес его начата, в явной форме в программе отсутствует, поэтому ее придется программно вычислить и занести в дескриптор уже на этапе выполнения.
Дескриптор gdt_codc сегмента команд заполняется схожим образом.
Дескриптор gdt_stack сегмента стека имеет, как и любой сегмент данных, код атрибута 92h, что разрешает его чтение и запись, и явным образом заданную границу - 255 байт, что соответствует размеру стека. Базовый адрес сегмента стека так же придется вычислить на этапе выполнения программы.
Последний дескриптор gdt_scrcen описывает страницу 0 видеобуфера. Размер видеостраницы, как известно, составляет 4096 байт, поэтому в поле границы указано число 4095. Базовый физический адрес страницы известен, он равен BS000h. Младшие 16 бит базы (число 8000И) заполняют слово base_l дескриптора, биты 16...19 (число OBU) - байт base_m. Биты 20...31 базового адреса равны 0, поскольку видеобуфер размещается в первом мегабайте адресного пространства.
Первая половина программы посвящена подготовке перехода в защищенный режим. Прежде всего надо завершить формирование дескрипторов сегментов программы, в которых остались незаполненными базовые адреса сегментов.
Базовые (32-битовые) адреса определяются путем умножения значений сегментных адресов на 16. После обнуления регистра ЕАХ и инициализации сегментного регистра DS, которая позволит нам обращаться к полям данных программы в реальном режиме, содержимое ЕАХ командой sill сдвигается влево на 4 бита, образуя линейный 32-битовый адрес. Поскольку этот адрес будет использоваться и в последующих фрагментах программы, он запоминается в регистре ЕВР (или любом другом свободном регистре общего назначения). В ВХ загружается адрес дескриптора данных, после чего в дескриптор заносится младшая половина линейного адреса из регистра АХ. Поскольку к старшей половине регистра ЕАХ (где нас интересуют биты 17...24) обратиться невозможно, над всем содержимым ЕАХ с помощью команды rol выполняется циклический сдвиг на 16 бит, в результате которого младшая и старшая половины ЕАХ меняются местами.
После сдвига содержимое AL (где теперь находятся биты 17...24 линейного адреса) заносится в поле base_m дескриптора. Аналогично Вычисляются линейные адреса сегмента команд и сегмента стека.
Следующий этап подготовки к переходу в защищенный режим - загрузка в регистр процессора GDTR (Global Descriptor Table Register, регистр таблицы глобальных дескрипторов) информации о таблице глобальных дескрипторов. Эта информация включает в себя линейный базовый адрес таблицы и ее границу и размещается в 6 байтах поля данных, называемого иногда псевдодескриптором. Для загрузки GDTR предусмотрена специальная привилегированная команда Igdt (load global descriptor table, загрузка таблицы глобальных дескрипторов), которая требует указания в качестве операнда имени псевдодескриптора. Формат псевдодескриптора приведен на рис. 4.10.

Рис. 4.10. Формат псевдодескриптора.

В нашем примере заполнение псевдодескриптора упрощается вследствие того, что таблица глобальных дескрипторов расположена в начале сегмента данных, и ее базовый адрес совпадает с базовым адресом всего сегмента, который мы благоразумно сохранили в регистре ЕВР. Границу GDT в нашем случае легко вычислить в уме: 5 дескрипторов по 8 байт занимают объем 40 байт, и , следовательно, граница равна 39. Команда Igdt загружает регистр GDTR и сообщает процессору о местонахождении и размере GDT.
Еще одна важная операция, которую необходимо выполнить перед переходом в защищенный режим, заключается в запрете всех аппаратных прерываний. Дело в том, что в защищенном режиме процессор выполняет процедуру прерывания не так, как в реальном. При поступлении сигнала прерывания процессор не обращается к таблице векторов прерываний в первом килобайте памяти, как в реальном режиме, а извлекает адрес программы обработки прерывания из таблицы дескрипторов прерываний, построенной схоже с таблицей глобальных дескрипторов и располагаемой в программе пользователя (или в операционной системе). В примере 4.4 такой таблицы
нет, и па время работы нашей программы прерывания придется запретить. Запрет всех аппаратных прерываний осуществляется командой cli.
Теперь, наконец, можно перейти в защищенный режим, что делается на удивление просто. Для перевода процессора в защищенный режим достаточно установить бит 0 в управляющем регистре CRO. Всего в процессоре имеется 4 программно-адресуемых управляющих регистра с мнемоническими именами CRO, CR1, CR2 и CR3. Регистр CR1 зарезервирован, регистры CR2 и CR3 управляют страничным преобразованием, которое у нас выключено, а регистр CRO содержит целый ряд управляющих битов, из которых нас сейчас будут интересовать только биты 31 (разрешение страничного преобразования) и 0 (включение защиты). При включении процессора оба эти бита сбрасываются, и в процессоре устанавливается реальный режим с выключенным страничным преобразованием. Установка в 1 младшего бита CR0 переводит процессор в защищенный режим, сброс этого бита возвращает его в режим реальных адресов.
Для того, чтобы в процессе установки бита 0 не изменить состояние других битов регистра CR0, сначала его содержимое считывается командой mov в регистр ЕАХ, там с помощью команды or устанавливается младший бит, после чего второй командой mov измененное значение загружается в CR0. Процессор начинает работать по правилам защищенного режима.
Хотя защищенный режим установлен, однако действия по настройке системы еще не закончены. Действительно, во всех используемых в программе сегментных регистрах хранятся не селекторы дескрипторов сегментов, а базовые сегментные адреса, не имеющие смысла в защищенном режиме. Между прочим, отсюда можно сделать вывод, что после перехода в защищенный режим программа не должна работать, так как в регистре CS пока еще нет селектора сегмента команд, и процессор не может обращаться к этому сегменту. В действительности это не совсем так.
В процессоре для каждого из сегментных регистров имеется так называемый теневой регистр дескриптора, который имеет формат дескриптора (рис. 4.11). Теневые регистры недоступны программисту; они автоматически загружаются процессором из таблицы дескрипторов каждый раз, когда процессор загружает соответствующий сегментный регистр. Таким образом, в защищенном режиме программист имеет дело с селекторами, т.е. номерами дескрипторов, а процессор - с самими дескрипторами, хранящимися в теневых регистрах. Именно содержимое теневого регистра (в первую очередь, линейный адрес сегмента) определяет область памяти, к которой обращается процессор при выполнении конкретной команды.
В реальном режиме теневые регистры заполняются не из таблицы дескрипторов, а непосредственно самим процессором. В частности, процессор заполняет поле базы каждого теневого регистра линейным базовым адресом сегмента, полученным путем умножения па 16 содержимого сегментного регистра, как это и положено в реальном режиме. Поэтому после перехода в защищенный режим в теневых регистрах находятся правильные линейные базовые адреса, и программа будет выполняться правильно, хотя с точки зрения правил адресации защищенного режима содержимое сегментных регистров лишено смысла.

Рис. 4.11. Сегментные регистры и теневые регистры дескрипторов.

Тем не менее после перехода в защищенный режим прежде всего следует загрузить в используемые сегментные регистры селекторы соответствующих сегментов. Это позволит процессору правильно заполнить все поля теневых регистров из таблицы дескрипторов. Пока эта операция не выполнена, некоторые поля теневых регистров (в частности, границы сегментов) содержат неверную информацию.
Загрузить селекторы в сегментные регистры DS, SS и ES не представляет труда. Но как загрузить селектор в регистр CS, в который запрещена прямая запись? Для этого можно воспользоваться искусственно сконструированной командой дальнего перехода, которая, как известно, приводит к смене содержимого и IP, и CS. Фрагмент

db OEAh ;Код команды far jmp
dw offset continue ;Смещение
dw 16 ;Селектор сегмента команд

выглядящий совершенно нелепо в сегменте команд, как раз и демонстрирует эту методику. В реальном режиме мы поместили бы во второе слово адреса сегментный адрес сегмента команд, в защищенном же мы записываем в него селектор этого сегмента (число 16).
Команда дальнего перехода, помимо загрузки в CS селектора, выполняет еще одну функцию - она очищает очередь команд в блоке предвыборки команд процессора. Как известно, в современных процессорах с целью повышения скорости выполнения программы используется конвейерная обработка команд программы, позволяющая совместить во времени фазы их обработки. Одновременно с выполнением текущей (первой) команды осуществляется выборка операндов следующей (второй), дешифрация третьей и выборка из памяти четвертой команды. Таким образом, в момент перехода в защищенный режим уже могут быть расшифрованы несколько следующих команд и выбраны из памяти их операнды. Однако эти действия выполнялись, очевидно, по правилам реального, а не защищенного режима, что может привести к нарушениям в работе программы. Команда перехода очищает очередь предвыборки, заставляя процессор заполнить ее заново уже в защищенном режиме.
Далее выполняется загрузка в сегментные регистры DS и SS значений соответствующих селекторов, и на этом, наконец, заканчивается процедура перехода в защищенный режим.
Следующий фрагмент программы является, можно сказать, диагностическим. В нем инициализируется (по правилам защищенного режима!) сегментный регистр ES и в видеобуфер из регистра АХ выводится один символ. Код 0Fh соответствует изображению большой звездочки, а атрибут 9Fh - ярко-белому мерцающему символу на синем поле. Появление этого символа на экране служит подтверждением правильного функционирования программы в защищенном режиме.
Почему мы не предусмотрели вывод на экран хотя бы одной содержательной строки? Дело в том, что в защищенном режиме запрещены любые обращения к функциям DOS или BIOS. Причина этого совершенно очевидна - и DOS, и BIOS являются программами реального режима, в которых широко используется сегментная адресация реального режима, т.е. загрузка в сегментные регистры сегментных адресов. В защищенном же режиме в сегментные регистры загружаются не сегментные адреса, а селекторы. Кроме того, обращение к функциям DOS и BIOS осуществляется с помощью команд программного прерывания int с определенными номерами, а в защищенном режиме эти команды приведут к совершенно иным результатам. Поэтому в программе, работающей в защищенном режиме и не имеющей специальных и довольно сложных средств перехода в так называемый режим виртуального 86-го процессора, вывод на экран можно осуществить только прямым программированием видеобуфера. Нельзя также выполнить запись или чтение файла; более того, нельзя даже завершить программу средствами DOS. Сначала се надо вернуть в реальный режим.
Возврат в реальный режим можно осуществить разными способами. Мы воспользуемся для этого тем же регистром CRO, с помощью которого мы перевели процессор а защищенный режим. Казалось бы, для возврата в реальный режим достаточно сбросить бит 0 этого регистра. Однако дело обстоит не так просто. Для корректного возврата в реальный режим надо выполнить некоторые подготовительные операции, рассмотрение которых позволит нам глубже вникнуть в различия реального и защищенного режимов.
При работе в защищенном режиме в дескрипторах сегментов записаны, среди прочего, их линейные адреса и границы. Процессор при выполнении команды с адресацией к тому или иному сегменту сравнивает полученный им относительный адрес с границей сегмента и, если команда пытается адресоваться за пределами сегмента, формирует прерывание (исключение) нарушения общей защиты. Если в программе предусмотрена обработка исключений, такую ситуацию можно обнаружить и както исправить. Таким образом, в защищенном режиме программа не может выйти за пределы объявленных ею сегментов, а также не может выполнить действия, запрещенные атрибутами сегмента. Так, если сегмент объявлен исполняемым (код атрибута 1 981т), то данные из этого сегмента нельзя читать или модифицировать; если атрибут сегмента равен 92h, то в таком сегменте не может быть исполняемых команд, на зато данные можно как читать, так и модифицировать. Указав для какого-то сегмента код атрибута 90h, мы получим сегмент с запрещением записи. При попытке записи в этот сегмент процессор сформирует исключение общей защиты.
Как уже отмечалось, дескрипторы сегментов хранятся в процессе выполнения программы в теневых регистрах (см. рис. 4.11), которые загружаются автоматически при записи в сегментный регистр селектора.
При работе в реальном режиме некоторые поля теневых регистров должны быть заполнены вполне определенным образом. В частности, поле границы любого сегмента должно содержать число FFFFh, а бит дробности сброшен. Следует подчеркнуть, что границы всех сегментов должны быть точно равны FFFFh; любое другое число, например, FFFEh, "не устроит" реальный режим.
Если мы просто перейдем в реальный режим сбросом бита 0 в регистре CR0, то в теневых регистрах останутся дескрипторы защищенного режима и при первом же обращении к любому сегменту программы возникнет исключение общей защиты, так как ни один из наших сегментов не имеет границы, равной FFFFh. Поскольку мы не обрабатываем исключения, произойдет либо сброс процессора и перезагрузка компьютера, либо зависание. Таким образом, перед переходом в реальный режим необходимо исправить дескрипторы всех наших сегментов: команд, данных, стека и видеобуфера К сегментным регистрам FS и GS мы не обращались, и о них можно не заботиться.
Теневые регистры, куда, собственно, надо записать значение границы, нам недоступны. Для из модификации придется прибегнуть к окольному маневру: записать в поля границ всех четырех дескрипторов значение FFFFh, а затем повторно загрузить селекторы в сегментные регистры, что приведет к перезаписи содержимого теневых регистров. С сегментным регистром CS так поступить нельзя, поэтому его загрузку придется выполнить, как и ранее, с помощью искусственно сформированной команды дальнего перехода.
Настроив все использовавшиеся в программе сегментные регистры, можно сбросить бит 0 в CR0. После перехода в реальный режим нам придется еще раз выполнить команду дальнего перехода, чтобы очистить очередь команд в блоке предвыборки и загрузить в регистр CS вместо хранящегося там селектора обычный сегментный адрес регистра команд.
Теперь процессор снова работает в реальном режиме, причем, хотя в сегментных регистрах DS, ES и SS остались незаконные для реального режима селекторы, программа будет какое-то время выполняться правильно, так как в теневых регистрах находятся правильные линейные адреса (оставшиеся от защищенного режима) и законные для реального режима границы (загруженные туда нами). Если, однако, в программе встретятся команды сохранения и восстановления содержимого сегментных регистров, например

push DS
...
pop DS

выполнение программы будет нарушено, так как команда pop DS загрузит в DS не сегментный адрес реального режима, а селектор, т.е. число 8 в нашем случае. Это число будет рассматриваться процессором, как сегментный адрес, и дальнейшие обращения к полям данных приведут к адресации физической памяти начиная с абсолютного адреса 80h, что, конечно, лишено смысла. Даже если в нашей программе нет строк сохранения и восстановления сегментных регистров, они неминуемо встретятся, как только произойдет переход в DOS по команде int 21h, так как диспетчер DOS сохраняет, а затем восстанавливает все регистры задачи, в том числе и сегментные. Поэтому после перехода в реальный режим необходимо загрузить в используемые далее сегментные регистры соответствующие сегментные адреса, что и выполняется в программе для регистров DS и SS. Надо также не забыть разрешить запрещенные нами ранее аппаратные прерывания (команда sti).
Можно еще заметить, что в той части программы, которая выполняется в защищенном режиме, не используется стек. Учитывая это, можно было несколько сократить текст программы, удалив из нее строки настройки регистра SS как при подготовке перехода в защищенный режим, так и при возврате в реальный. Не было также необходимости заново инициализировать указатель стека, так как его исходное содержимое - смещение дна стека, равное 512, никуда из SP не делось бы.
Программа завершается обычным образом функцией DOS 4Ch. Нормальное завершение программы и переход в DOS в какой-то мере свидетельствует о ее правильности.
У рассмотренной программы имеется серьезный недостаток - полное отсутствие средств отладки. Для отладки программ защищенного режима используется механизм прерываний и исключений, в нашей же программе этот механизм не активизирован. Поэтому всякие неполадки при работе в защищенном режиме, которые с помощью указанного механизма можно было бы обнаружить и проанализировать, в данном случае будут приводить к сбросу процессора.
В приведенном примере проиллюстрированы лишь базовые средства программирования защищенного режима: понятие селекторов и дескрипторов, создание глобальной таблицы дескрипторов, переход в защищенный режим и обратно, адресация в защищенном режиме. За кадром остались такие важные вопросы, как обработка исключений и аппаратных прерываний, уровни привилегий и защита по привилегиям, раздельные операционные среды и таблицы локальных дескрипторов, создание и взаимодействие задач, режим виртуального процессора 8086 и другие. Желающие познакомиться с этими вопросами могут обратиться к одному из изданий книги: П.И.Рудаков, К.Г.Финогенов "Программируем на языке ассемблера IBM PC" (Обнинск, "Принтер"), где основы программирования защищенного режима рассмотрены значительно более подробно (хотя тоже далеко не исчерпывающе).


Приложение

Приложение.

Система команд процессоров Intel

    • ААА ASCII-коррекция регистра АХ после сложения
    • AAD ASCII-коррекция регистра АХ перед делением
    • ААМ ASCII-коррекция регистра АХ после умножения
    • AAS ASCII-коррекция регистра AL после вычитания
    • ADC Целочисленное сложение с переносом
    • ADD Целочисленное сложение
    • AND Логическое И
    • 386Р+ ARPL Коррекция запрашиваемого уровня привилегий селектора
    • 386+ BOUND Проверка индекса массива на выход за границы массива
    • 386+ BSF Прямое сканирование битов
    • 386+ BSR Обратное сканирование битов
    • 486+ BSWAP Обмен байтов
    • 386+ ВТ Проверка бита
    • 386+ ВТС Проверка и инверсия бита
    • 386+ ВТК Проверка и сброс бита
    • 386+ BTS Проверка и установка бита
    • CALL Вызов подпрограммы
    • CBW Преобразование байта в слово
    • 386+ CDQ Преобразование двойного слова в четверное
    • CLC Сброс флага переноса
    • CLD Сброс флага направления
    • CLI Сброс флага прерываний
    • 386Р+ CUTS Сброс флага переключения задачи в управляющем регистре 0
    • CMC Инвертирование флага переноса
    • СМР Сравнение
    • CMPS Сравнение строк
    • CMPSB Сравнение строк по байтам
    • CMPSW Сравнение строк по словам
    • 386+ CMPSD Сравнение строк по двойным словам
    • 486+ CMPXCHG Сравнение и обмен
    • Pentium+ CMPXCHG8B Сравнение и обмен 8 байтов
    • Pentium+ CPUID Идентификация процессора
    • CWD Преобразование слова в двойное слово
    • 386+ CWDE Преобразование слова в двойное слово с расширением
    • DAA Десятичная коррекция в регистре AL после сложения
    • DAS Десятичная коррекция в регистре AL после вычитания
    • DEC Декремент (уменьшение на 1)
    • DIV Деление целых чисел без знака
    • 386+ ENTER Создание стекового кадра для параметров процедуры
    • HLT Останов
    • IDIV Деление целых чисел со знаком
    • IMUL Умножение целых чисел со знаком
    • IN Ввод из порта
    • INC Инкремент (увеличение на 1)
    • 386+
    • INS Ввод строки из порта
    • INSB Ввод байта из порта
    • INSW Ввод слова из порта
    • INSD Ввод двойного слова из порта
    • INT Программное прерывание
    • INTO Прерывание по переполнению
    • IRET Возврат из прерывания
    • 386+ IRETD Возврат из прерывания в 32-разрядном режиме
    • Jcc Команды условных переходов
    • JMP Безусловный переход
    • LAHF Загрузка флагов в регистр АН
    • 386Р+ LAR Загрузка прав доступа
    • LDS Загрузка указателя с использованием регистра DS
    • LEA Загрузка исполнительного адреса
    • 386+ LEAVE Выход из процедуры высокого уровня
    • LES Загрузка указателя с использованием регистра ES
    • 386+
    • LFS Загрузка указателя с использованием регистра FS
    • LGS Загрузка указателя с использованием регистра FS
    • LSS Загрузка указателя с использованием регистра FS
    • 386Р+ LGDT Загрузка регистра таблицы глобальных дескрипторов
    • 386Р+ LIDT Загрузка регистра таблицы дескрипторов прерываний
    • 386Р+ LLDT Загрузка регистра таблицы локальных дескрипторов
    • 3863+ LMSW Загрузка слова состояния машины
    • LOCK Запирание шины
    • LODS Загрузка операнда из строки
    • LODSB Загрузка байта из строки
    • LODSW Загрузка слова из строки
    • 386+ LODSD Загрузка двойного слова из строки
    • L00P Циклическое выполнение, пока содержимое СХ не равно нулю
    • L00PE/L00PZ Циклическое выполнение, пока равно/циклическое выполнение, пока нуль
    • L00PNE/L00PNZ Циклическое выполнение, пока не равно/циклическое выполнение, пока не нуль
    • 386Р+ LSL Загрузка границы сегмента
    • 386Р+ LTR Загрузка регистра задачи TR
    • MOV Пересылка данных
    • 386Р+ MOV Пересыпка в\из специальных регистров
    • MOVS Пересылка данных из строки в строку
    • MOVSB Пересылка байта данных из строки в строку
    • MOVSW Пересылка слова данных из строки в строку
    • 386+ MOVSD Пересылка двойного слова из строки в строку
    • 386+ MOVSX Пересылка с расширением знака
    • 386+ MOVZX Пересылка с расширением нуля
    • MUL Умножение целых чисел без знака
    • NEG Изменение знака, дополнение до 2
    • NOP Холостая команда
    • NOT Инверсия, дополнение до 1. логическое отрицание
    • OR Логическое ВКЛЮЧАЮЩЕЕ ИЛИ
    • OUT Вывод в порт
    • 386+
    • OUTS Вывод строки в порт
    • OUTSB Вывод байта в порт
    • OUTSW Вывод слова в порт
    • OUTSD Вывод двойного слова в порт
    • POP Извлечение слова из стека
    • 386+ РОРА Восстановление из стека всех регистров
    • 386+ POPAD Восстановление из стека всех регистров в 32-разрядном режиме
    • POPF Восстановление из стека регистра флагов
    • 386+ POPFD Восстановление из стека расширенного регистра флагов
    • PUSH Занесение операнда в стек
    • 386+ PUSHA Сохранение в стеке всех регистров
    • 386+ PUSHAD Сохранение в стеке всех регистров в 32-разрядном режиме
    • 386+ PUSHFD Занесение в стек содержимого расширенного регистра флагов
    • RCL Циклический сдвиг влево через бит переноса
    • RCR Циклический сдвиг вправо через бит переноса
    • Pentium+P RDMSR Чтение особого регистра модели
    • REP Повторение
    • REPE Повторение пока равно
    • REPZ Повторение пока нуль
    • REPNE Повторение пока равно
    • REPNZ Повторение пока не равно
    • RET Возврат из процедуры
    • RETN Возврат из ближней процедуры
    • RETF Возврат из дальней процедуры
    • ROL Циклический сдвиг влево
    • ROR Циклический сдвиг вправо
    • SAHF Запись содержимого регистра АН в регистр флагов
    • SAL Арифметический сдвиг влево
    • SAR Арифметический сдвиг вправо
    • SBB Целочисленное вычитание с займом
    • SCAS Сканирование строки с целью сравнения
    • SCASB Сканирование строки байтов с целью сравнения
    • SCASW Сканирование строки слов с целью сравнения
    • 386+ SCASD Сканирование строки двойных слов с целью сравнения
    • 386+ SETcc Установка байта по условию
    • SHL Логический сдвиг влево
    • 386+ SHLD Логический сдвиг влево с двойной точностью
    • SHR Логический сдвиг вправо
    • 386+ SHRD Логический сдвиг вправо с двойной точностью
    • 386Р+ SIDT Сохранение в памяти содержимого регистра таблицы дескрипторов прерываний
    • 386Р+ SLDT Сохранение содержимого регистра таблицы локальных дескрипторов
    • 386Р+ SMSW Сохранение слова состояния машины
    • STC Установка флага переноса
    • STD Установка флага направления
    • ST1 Установка флага прерывания
    • STOS Запись в строку данных
    • STOSB Запись байта в строку данных
    • STOSW Запись слова в строку данных
    • 386+ STOSD Запись двойного слова в строку данных
    • 3S6P+ STR Сохранение содержимого регистра состояния задачи
    • SUB Вычитание целых чисел
    • TEST Логическое сравнение
    • 386Р+ VERR Проверка сегмента на чтение
    • 386Р+ VERW Проверка сегмента на запись
    • 486+ XADD Обмен и сложение
    • XCHG Обмен данными между операндами
    • XLAT Табличная трансляция
    • 386+XLAT
    • 386+XLATB
    • XOR Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ

Система команд процессоров Intel

Ниже приводится алфавитный перечень команд процессоров Intel с кратким описанием действия каждой команды и примерами ее использования.
В разделах статей, начинающихся с обозначения 386+, описываются отличия действия рассматриваемой команды в современных 32-разрядных процессорах (80386, i486, Pentium). Как правило, эти отличия заключаются в возможности использования не только 8- и 16-разрядных, но и 32-разрядных операндов, а также расширенных режимов адресации памяти. Обычные 16-разрядные программы реального режима вполне могут использовать расширенные регистры процессора (ЕАХ, ЕВХ и проч.), 32-битовые ячейки памяти и варианты команд для их обработки. Для того, чтобы ассемблер правильно транслировал команды с 32-разрядными операндами, в программу необходимо включить директиву ассемблера .386 (можно также использовать директивы .486 или .586), а сегменту команд (и во многих случаях сегменту данных) придать описатель use 16:
.386
codes segment use 16
assume CS:codes
codes ends
data segment use16
data ends
Кроме этого, необходимо разрешить компоновщику обрабатывать 32-разрядные операнды, что для компоновщика TLINK осуществляется указанием ключа /3.
Отдельные статьи, начинающиеся с обозначений 386+ , 486+ и Pentium+, посвящены командам, отсутствующим в МП 86. Многие из этих команд (например, команды проверки бита Ы или условной установки байта set) носят прикладной характер и могут использоваться в обычных программах реального режима.
Новые команды, реализованные впервые в МП 80386, сохраняют свое значение и в более современных процессорах. Для того, чтобы ассемблер распознавая команды МП 80386, в программе должна присутствовать директива .386.
Новые команды, реализованные впервые в МП 80486, сохраняют свое значение и в процессорах Pentium. Для того, чтобы ассемблер распознавая команды МП 80486, в программе должна присутствовать директива .486.
Для того, чтобы ассемблер распознавал команды, реализованные впервые в процессоре Pentium, в программе должна присутствовать директива .586.
Ряд команд требует для своего выполнения специальных условий, обычно отсутствующих в приложениях MS-DOS. Так, например, команда bound (проверки индекса массива на выход за границы) при обнаружении выхода за границы генерирует прерывание с вектором 5. Это прерывание в защищенном режиме как раз и является исключением нарушения границ массива, но в приложениях MS-DOS используется для вывода на печать содержимого экрана. Поэтому использование таких команд в реальном режиме затруднено.
Отдельные статьи, начинающиеся с обозначения 386Р+, посвящены привилегированным командам современных процессоров, работающих в расширенном режиме, и отсутствующим в МП 86. Для использования этих команд в программу необходимо включить директиву ассемблера .386Р (можно также использовать директивы .486Р или .586Р). Если при этом программа реализуется, как 16-разрядное приложение MS-DOS, сегмент команд должен иметь описатель usc16 (при наличии директивы .386 транслятор по умолчанию создает 32-разрядное приложение). Следует, однако, иметь в виду, что привилегированные команды защищенного режима предназначены для использования не в прикладных программах, а в операционных системах защищенного режима. В прикладных программах привилегированные команды приходится использовать лишь в весьма специальных случаях, когда, например, прикладная программа запускается в реальном режиме под управлением MS-DOS, но затем переводит процессор в защищенный режим и далее использует преимущества этого режима. Типичный пример программы такого рода - приложение DOS, которому требуется использовать всю оперативную память компьютера. В настоящей книге, посвященной, в основном, реальному режиму, дается лишь перечисление привилегированных команд защищенного режима.

 ААА ASCII-коррекция регистра АХ после сложения
Команда ааа используется вслед за операцией сложения add в регистре AL двух неупакованных двоично-десятичных (BCD) чисел, если в АХ находится двухразрядное неупакованное двоично-десятичное число. Команда не имеет параметров. Она преобразует результат сложения в неупакованное двоично-десятичное число, младший десятичный разряд которого находится в AL. Если результат превышает 9, выполняется инкремент содержимого регистра АН. Команда воздействует на флаги AF и CF.

Пример
mov AX,0605h ; Неупакованное BCD 65
add AL,09h ;Неупакованное BCD 9, AX=060Eh
ааа ;AX=0704h, неупакованное BCD 74

AAD ASCII-коррекция регистра АХ перед делением
Команда aad используется перед операцией деления неупакованного двоично-десятичного (BCD) числа в регистре АХ на другое неупакован
ное двоично-десятичное число. Команда не имеет параметров. Она преобразует делимое в регистре АХ в двоичное число без знака, чтобы в результате деления получились правильные неупакованные двоично-десятичные числа (частное в AL, остаток в АН). Команда воздействует на флаги SF, ZF и PF.

Пример
raov AX,0207h ;Неупакованное BCD 27
mov DL,06h ;Неупакованное BCD 6
aad;AX=001Bh=27
div DL ;AX=0304h, т.е. 4 и З в остатке< /FONT>

AAM ASCII-коррекция регистра АХ после умножения
Команда aam используется вслед за операцией умножения двух неупакованных двоично-десятичных чисел. Команда не имеет параметров. Она преобразует результат умножения, являющийся двоичным числом, в правильное неупакованное двоично-десятичное (BCD) число, младший разряд которого помещается в AL, а старший - в АН. Команда воздействует на флаги SF, ZF и PF.

Пример
mov AL,08h ;Неупакованное BCD 8
mov CL,07h ;Неупакованное BCD 7
mulCL;AX=0038h=56
aam ;AX=0506h, BCD 56

AAS ASCII-коррекция регистра AL после вычитания
Команда aas используется вслед за операцией вычитания одного неупакованного двоично-десятичного числа (BCD) из другого в AL. Команда не имеет параметров. Она преобразует результат вычитания в неупакованное двоично-десятичное число. Если результат вычитания оказывается меньше 0, выполняется декремент содержимого регистра АН. Команда воздействует на флаги AF и CF; после ее выполнения AF=1, CF=1.
Пример
mov AX,0708h ;Неупакованное BCD 78
mov CL,09h ;Неупакованное BCD 9
sub AL,CL ;AX=07FFh
aas ;AX=0609h, неупакованное BCD 69< /FONT>

ADC Целочисленное сложение с переносом
Команда adc осуществляет сложение первого и второго операндов, прибаатяя к результату значение флага переноса CF. Исходное значение первого операнда (приемника) теряется, замещаясь результатом сложения. Второй операнд не изменяется. В качестве первого операнда команды adc можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда adc (совместно с командой add) обычно используется для сложения 32-разрядных чисел. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AX,1125h
adc AX,2C25h ;AX=3D4Bh, если CF был = 1
;AX=3D4Ah, если CF был= 0< /FONT >

Пример 2
; В полях данных:
numlow dw 0FFFFh ;Младшая часть 2-го слагаемого
numhigh dw 000Sh ;Старшая часть 2-го слагаемого
;Число 0005FFFFh=393215
;В программном сегменте:
mov AX,000Sh ;Младшая часть 1-го слагаемого
mov BX,0002h ;Старшая часть 1-го слагаемого
;Число 00020005h=131077
add АХ,numlow ;Сложение младших частей. АХ=4, CF=1
adc BX, numhigh ;Сложение старших частей с
;переносом.ВХ:АХ=0008:0004h.
;Число 00080004h=524292

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. Команда adc с 32-разрядными операндами может использоваться для сложения 64-разрядных целых чисел.

Пример
; В полях данных
mem321 dd 0FFFFFFFFh ;Младшая часть 1-го слагаемого
mem32h dd 98765432h ;Старшая часть 1-го слагаемого
; В программном сегменте
mov EAX,1 ;Младшая часть 2-го слагаемого
mov EBX,0 ;Старшая часть 2-го слагаемого
add EAX,mem321 ;Складываем младшие половины
;Сумма=100000000Ь>32 бит
;EAX=000000h, перенос
adc EBX,mem32h ;Складываем старшие половины
;и перенос. EBX=90000001h ;Сумма: 9876543300000000h

ADD Целочисленное сложение
Команда add осуществляет сложение первого и второго операндов. Исходное значение первого операнда (приемника) теряется, замещаясь результатом сложения. Второй операнд не изменяется. В качестве первого операнда команды add можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячей-ку памяти или непосредственное значение, однако не допускается опре-делять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команду add можно использовать для сложения как обычных целых чи-сел, так и двоично-десятичных (с использованием регистра АХ для хра-нения результата). Если складываются неупакованные двоично- десятич-ные (BCD) числа, после команды add следует использовать команду ааа; если складываются упакованные числа, то команду daa. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov BX,lFFEh
mov CX,3
add BX,CX ;BX=2001h, CX= 0003h< /FONT>

Пример 2
mov AX,25h
add AX,12h ;AX=0037h

Пример 3
; В полях данных:
mem dw 128
;B программном сегменте:
add mem,100 ;mem=228

Пример 4
mov AX,0507h ;BCD распакованное 57 add AL,05h ;BCD 5, AX=050Ch aaa ;AX=0602h, BCD 62

Пример 5
mov AL,57h ;BCD упакованное 57 add AL,05h ;BCD 5, AL=5Ch daa ;AL=62h, BCD 62

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,98765432h
add EAX,11111111h ; EAX=A9876543h

 AND Логическое И
Команда and осущестшшет логическое (побитовое) умножение первого операнда на второй. Исходное значение первого операнда (приемника) теряется, замещаясь результатом умножения. В качестве первого операнда команды and можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами. Команда воздействует на флаги SF, ZF и PF.
Правила побитового умножения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0001

Пример 1
mov AX,0FFEh
and AX,5555h ;AX=0554h< /FONT>

Пример 2
; В полях данных:
mem dw 0С003h
;В программном сегменте:
mov AX,700Eh
and AX,mem ;AX=4002h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EDX, 0FA8 8 0 0 4 lh
and EDX,0FF00000Fh ; EDX = FA000001h

386P+ ARPL
Коррекция запрашиваемого уровня привилегий селектора
Команда aprl сравнивает селектор с образцом, содержащим максимально допустимый уровень привилегий (обычно используется селектор CS) и устанавливает проверяемое значение в соответствии с меньшим из двух уровней привилегий. Если изменение уровня не потребовалось, флаг ZF сбрасывается, если потребовалось - устанавливается. В качестве первого операнда команды aprl может использоваться 16-разрядный регистр или слово памяти с проверяемым селектором; в качестве второго операнда - 16-разрядный регистр с селектором-образцом.

386+ BOUND
Проверка индекса массива на выход за границы массива
Команда bound проверяет, лежит ли указанный индекс, рассматриваемый, как число со знаком, внутри заданных вторым операндом границ.
Если индекс выходит за границы массива снизу или сверху, генерируется прерывание с вектором 5. Первый операнд должен быть регистром, содержащим проверяемый индекс, второй - адресом поля памяти с двумя границами проверяемого массива. В команде bound допустимо использование как 16-битовых, так и 32-битовых операндов (но и первый, и второй операнды должны быть одного типа).

386+ BSF Прямое сканирование битов
Команда bsf сканирует слово или двойное слово в поисках бита, равного 1. Сканирование выполняется от младшего бита (0) к старшему. Если в слове не найдено устаноштенных битов, то устанавливается флаг ZF. Если установленные биты есть, то номер первого установленного бита заносится в указанный в команде регистр. Номером бита считается его позиция в слове, отсчитываемая от бита 0. В качестве первого операнда команды bsf следует указывать регистр, куда будет помещен результат сканирования, в качестве второго - регистр или ячейку памяти со сканируемым словом. В команде bsf допустимо использование как 16-битовых, так и 32-битовых операндов (но и первый, и второй операнды должны быть одного типа).

Пример 1
mov BX,70h ;Анализируемое данное
bsf АХ,ВХ ;АХ=4, ZF= 0< /FONT>

Пример 2
mov SI,0 ;Анализируемое данное
bsf BX,SI ;ZF=1, в ВХ прежнее значение

Пример 3
mov SI,8 ;Анализируемое данное
bsf BX,SI ;BX=3, ZF=1

Пример 4
;В полях данных
mem dw 9000h Анализируемое данное
;В программном сегменте:
bsfAX,mem;AX=000Ch=12, ZF=0

386+ BSR Обратное сканирование битов
Команда bsf сканирует слою или двойное слово в поисках бита, равного 1. Сканирование выполняется от старшего бита (15 или 31) к младшему. Если в слове не найдено установленных битов, то устанашгивается флаг ZF. Если установленные биты есть, то номер первого устаноштенного бита заносится в указанный в команде регистр. Номером бита считается его позиция в слове, отсчитываемая от бита 0. В качестве первого операнда команды bsf следует указывать регистр, куда будет помещен результат сканирования, в качестве второго - регистр или ячейку памяти со сканируемым словом. В команде bsf допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд - константа).

Пример 1
mov BX,70h ;Анализируемое данное bsr AX,BX ;AX=6, ZF= 0< /FONT>

Пример 2
mov SI,0 ;Анализируемое данное
bsr BX,SI ;ZF=1, в ВХ прежнее значение< /FONT>

Пример 3
mov SI,8 ;Анализируемое данное
bsf BX,SI ;BX=3, ZF=1

Пример 4
; В полях данных
mem dw 9000h ; Анализируемое данное
;В программном сегменте:
bsr AX, mem;AX=000Fh= 15, ZF=0< /FONT>

486+ BSWAP Обмен байтов
Команда bswap изменяет порядок байтов в своем единственном операнде, в качестве которого может выступать только 32-разрядный регистр общего назначения. Биты 7...0 обмениваются с битами 31...24, а биты 15... 18 с битами 23...16. Другими словами, нумерация байтов регистра изменяется на противополжную (вместо 3, 2, 1,0 - 0, 1, 2, 3). Команда не воздействует на флаги процессора.

Пример
mov ЕАХ, 01234567h
bswapEAX ;EAX=67452301h< /FONT>

386+ ВТ Проверка бита
Команда bt позволяет определить, установлен ли в заданном слове определенный бит. Анализируемое слово выступает в качестве первого операнда, номер бита - в качестве второго. Первым операндом команды bt может служить регистр или ячейка памяти, вторым - регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд - константа).
Значение проверяемого бита копируется в флаг CF.

Пример 1
mov AX,00FFh ;Анализируемое данное
bt AX,5 ; бит 5=1, ZF= 1< /FONT>

Пример 2
mov AX,00FFh ;Анализируемое данное
bt AX,8 ;бит 8=0, ZF= 0< /FONT>

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
bt АХ,ВХ ;бит 15 = 1, ZF=1

Пример 4
;В полях данных
mem dw IFh ;Анализируемое данное
;В программном сегменте:
bt mem, 4 ;бит 4 = 1, ZF=1

386+ ВТС Проверка и инверсия бита
Команда btc проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и инвертирует. Номер бита выступает в качестве второго операнда. Первым операндом команды btc может служить регистр или ячейка памяти, вторым - регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд - константа).

Пример 1
mov AX,00FFh ;Анализируемое данное
btc АХ, 5 ;AX= 00DFh Бит 5 был= 1
;Сброс бита 5, ZF=1

Пример 2
mov AX,OOFFh /Анализируемое данное
btc АХ, 8 ;AX=lFFh Бит 8 был = О
;Установка бита 8, ZF=0

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
btc AX,BX ;AX=0001h, ZF=1

Пример 4
; В полях данных
mem dw IFh
;В программном сегменте: ; Анализируемое данное
btc mem, I /mem=lEh, ZF=1

386+ BTR Проверка и сброс бита
Команда btr проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и сбрасывает. Номер бита выступает в качестве второго операнда. Первым операндом команды btr может служить регистр или ячейка памяти, вторым - регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд - константа).

Пример 1
Пример 1
mov AX,00Fh ;Анализируемое данное
btr АХ, 5 ;AX=00DFh. Бит 5 был = 1
;Сброс бита 5, ZF=1< /FONT>

Пример 2
mov AX,00FFh ;Анализируемое данное
btr AX, 8 ;AX=0FFh Бит 8 был =0
;Остался 0, ZF=0

Пример 3
mov AX,8001h ;Анализируемое данное
mov BX,15 ;Номер проверяемого бита
btr AX,BX ;AX=0001h Бит 15 был = 1
;Сброс бита 15, ZF=1

Пример 4
;В полях данных
mem dw IFh
;В программном сегменте: ;Анализируемое данное
btr mem,10 ;mem=lFh Бит 10 был = 0
;Остался 0, ZF=0

386+ BTS Проверка и установка бита
Команда bts проверяет определенный бит в слове, заданном первым операндом, копирует его значение в флаг CF и устанашшвает. Номер бита выступает в качестве второго операнда. Первым операндом команды bts может служить регистр или ячейка памяти, вторым - регистр или непосредственное значение. В команде допустимо использование как 16-битовых, так и 32-битовых операндов, но и первый, и второй операнды должны быть одного типа (за исключением случая, когда второй операнд - константа).

Пример 1

mov AX,OOFFh ;Анализируемое данное
bts AX, 5 ;AX=OOFFh Бит 5 был = 1
;Остался 1, ZF=1

Пример 2
mov AX,OOFFh ;Анализируемое данное
bts AX, 8 ;AX=lFFh Бит 8 был = О
;Установка бита 8, ZF=0< /FONT>

Пример 3
mov AX,8001h ; Анализируемое данное
mov BX,15 ;Номер проверяемого бита
bts AX,BX ;AX=8001h Бит 15 был = 1
;Остался 1, ZF=1

Пример 4
; В полях данных
mem dw IFh ; Анализируемое данное
;В программном сегменте:
bts mem,10 ;mem=4lFh Бит 10 был = О
; Установка бита 10, ZF=0

CALL Вызов подпрограммы
Команда call передает управление подпрограмме, сохранив перед этим в стеке смещение к точке возврата. Команда ret, которой обычно заканчивается подпрограмма, забирает из стека адрес возврата и возвращает управление на команду, следующую за командой call. Команда не воздействует на флаги процессора.
Команда call имеет четыре модификации:
- вызов прямой ближний (в пределах текущего программного сегмента);
- вызов прямой дальний (вызов подпрограммы, расположенной в другом программном сегменте);
- вызов косвенный ближний;
- вызов косвенный дальний.
Все разновидности вызовов имеют одну и ту же мнемонику call, хотя и различающиеся коды операций. Во многих случаях транслятор может определить вид вызова по контексту, в тех же случаях, когда это невозможно, следует использовать атрибутные операторы:
near ptr - прямой ближний вызов;
far ptr - прямой дальний вызов;
word ptr - косвенный ближний вызов;
dword ptr - косвенный дачьний вызов.
Команда call прямого ближнего вызова заносит в стек относительный адрес точки возврата в текущем программном сегменте и модифицирует IP так, чтобы в нем содержатся относительный адрес точки перехода в том же программном сегменте. Необходимая для вычисления этого адреса величина смещения от точки возврата до точки перехода содержится в коде команды, который занимает 3 байт (код операции E8h и смещение к точке перехода).
Команда call прямого дальнего вызова заносит в стек два слова - сначала сегментный адрес текущего программного сегмента, а затем (выше, в слово с меньшим адресом) относительный адрес точки возврата в текущем программном сегменте. Далее модифицируются регистры IP и CS: в IP помещается относительный адрес точки перехода в том сегменте, куда осуществляется переход, а в CS - сегментный адрес этого сегмента. Обе эти величины берутся из кода команды, который занимает 5 байтов (код операции 9А1г, относительный адрес вызываемой подпрограммы и ее сегментный адрес).
Косвенные вызовы отличаются тем, что адрес перехода извлекается не из кода команды, а из ячеек памяти; в коде команды содержится информация о том, где находится адрес вызова. Длина кода команды зависит от используемого способа адресации.

Примеры прямого ближнего вызова
call near ptr subl ;Вызов подпрограммы subl
;из того же сегмента
call subl ;To же самое

Косвенные ближние вызовы

Пример 1
mov BX,offset subl ;ВХ=адрес подпрограммы
call BX ;Вызов подпрограммы

Пример 2
; В полях данных:
addr dw subl ;Ячейка с адресом подпрограммы
;В программном сегменте:
call DS:addr ;Вызов подпрограммы
call word ptr addr ;To же самое

Пример 3
;В полях данных:
addr dw subl ;Ячейка с адресом подпрограммы
;В программном сегменте:
mov SI,offset addr ;SI=адрес ячейки с адресом
;подпрограммы
call [SI] ;Вызов подпрограммы

Пример 4
;В полях данных:
tbl dw subl ;Ячейка с адресом
;подпрограммы 1
dw sub2 ;Ячейка с адресом
;подпрограммы 2
dw sub3 ;Ячейка с адресом
;подпрограммы 3
;В программном сегменте:
mov BX,offset tbl ;ВХ=адрес таблицы адресов
;подпрограмм
mov SI, 2 ;SI=смещение к адресу sub2
call [BX] [SI] ;Вызов подпрограммы 2

Пример прямого дальнего вызова
call far ptr subl ;Вызов подпрограммы sub2,
;расположенной в другом
;программном сегменте

Косвенные дальние вызовы

Пример 1
;В полях данных:
addr dd subl ;Поле с двухсловным
;адресом подпрограммы
;В программном сегменте:
call DS:addr ;Вызов подпрограммы
call dword ptr addr;To же самое

Пример 2
;В полях данных:
addr dd subl ;Поле с двухсловным
;адресом подпрограммы
;В программном сегменте:
mov DI,offset addr ;В1=адрес поля с адресом
;подпрограммы
call [DI] ;Вызов подпрограммы

Пример 3
; В полях данных:
tbl dd subl ;Адрес подпрограммы 1
dd sub2 ;Адрес подпрограммы 2
dd sub3 ;Адрес подпрограммы 3
;В программном сегменте:
mov SI,offset tbl ;DI=адрес таблицы адресов
mov DI,8 ;Смещение к адресу sub3
call [SI] [DI] ;Вызов подпрограммы sub3

386+ Допустимо использование дополнительных режимов адресации 32-разрядных процессоров. В 32-разрядных приложениях допустимо использование 32-битовых операндов. В защищенном режиме роль сегментного адреса выполняет селектор.

Примеры
call [EAX] ;Косвенный вызов
call 8[ЕСХ] ;Косвенный вызов

CBW Преобразование байта в слово
Команда cbw заполняет регистр АН знаковым битом числа, находящегося в регистре AL, что дает возможность выполнять арифметические операции над исходным операндом-байтом, как над словом в регистре АХ. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1

mov AL,5
cdw ;AX=0005h

Пример 2
mov AL, -2;AL=FEh=-2 (байт)
cdv;AX=FFFEh=-2 (слово)

386+ CDQ Преобразование двойного слова в четверное
Команда cdq расширяет знак двойного слова в регистре ЕАХ на регистр EDX. Эту команду можно использовать для образования четырсхсловного делимого из двухсловного перед операцией двухсловного деления. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1
;В полях данных
mem dd -2 ; Отрицательное число
;В программном сегменте
mov ЕАХ,mem ;EAX=FFFFFFFEh
cdq ;EDX=FFFFFFFFh, EAX=FFFFFFFEh

Пример 2
;В полях данных
mem dd 7FFFFFFEh ,'Положительное число
;В программном сегменте
mov ЕАХ,mem ;EAX=7FFFFFFEh
cdq ;EDX=00000000h, EAX= 7FFFFFFEh< /FONT>

CLC Сброс флага переноса
Команда clc сбрасывает флаг переноса CF в регистре флагов. Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
clc ;CF=0, независимо от
;исходного состояния

CLD Сброс флага направления
Команда eld сбрасывает флаг направления DF в регистре флагов, устанавливая прямое (в порядке возрастания адресов) направление выполнения операций со строками (цепочками). Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
cld ;DF=0, независимо от
;исходного состояния

CL1 Сброс флага прерываний
Команда sti сбрасывает флаг разрешения прерываний IF в регистре флагов, запрещая (до установки этого флага командой sti) все аппаратные прерывания (от таймера, клавиатуры, дисков и т.д.) Команда не запрещает процессору выполнение команды hit (реализация программных прерываний); также не запрещаются немаскируемые прерывания, поступающие на вход NMI микропроцессора. Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
cli ;IF=0, независимо от
;исходного состояния

386Р+ CLTS Сброс флага переключения задачи в управляющем регистре 0
Команда cits сбрасывает флаг TS в регистре CR0.

CMC Инвертирование флага переноса
Команда сmс изменяет значение флага переноса CF в регистре флагов на обратное. Команда не имеет операндов и не воздействует на остальные флаги процессора.

Пример
cmc ;Состояние флага CF
;изменяется на обратное

СМР Сравнение
Сами операнды не изменяются. Таким образом, если команду сравнения записать в общем виде

стр операнд_1, операнд_2
то ее действие можно условно изобразить следующим образом:

операнд_1 - операнд_2 -> флаги процессора

В качестве первого операнда команды сmр можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно, как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Обычно вслед за командой сmр стоит одна из команд условных переходов, анализирующих состояние флагов процес
сора. При сравнении чисел без знака необходимо использовать команды условных переходов, предназначенные для анализа чисел без знака (ja, jb и проч.). При сравнении чисел со знаком необходимо использовать команды условных переходов, предназначенные для анализа чисел со знаком (jl, jg и проч.).

Пример 1
cmp АХ,10000 ;АХ-10000
je eqlOOOO ;Переход на метку eqlOOOO,
;если АХ=10000

Пример 2
;В полях данных:
base dw 8002h
;В программном сегменте:
cmp DX,base DX-base
jb below ;Переход на метку below,
;если DX, рассматриваемое
;как число без знака, меньше
;числа без знака 8002h=32770

Пример 3
;В полях данных:
base dw 8002h
;В программном сегменте:
cmp DX,base DX-base
jl less ;Переход на метку less, если
;DX, рассматриваемое как
;число со знаком, меньше
;числа со знаком 8002h=-32766

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
cmp EAX,8000000 Oh
ja above ;Переход, если
;EAX=80000001h...FFFFFFFFh< /FONT>

CMPS Сравнение строк

CMPSB Сравнение строк по байтам

CMPSW Сравнение строк по словам
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они сравнивают по одному элементу каждой строки, фактически осуществляя вычитание второго операнда из первого и устанавливая в соответствии с результатом вычитания флаги CF, PF, AF, ZF, SF и OF. Команда cmpsb выполняет сравнение по байтам, команда cmpsw - по словам, а команда cmps может быть использована для сравнения как байтов, так и слов. В последнем случае размер сравниваемых элементов определяется их описанием (с помощью директив db или dw). Первый операнд адресуется через DS:SI, второй - через ES:DI. Таким образом, операцию сравнения можно условно изобразить следующим образом:
(DS:SI) - (ES:DI) -> флаги процессора
После каждой операции сравнения регистры SI и DI получают положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера сравниваемых элементов (байт или слово).
Вариант команды cmps имеет формат
cmps строка_1, строка_2
(что не избавляет от необходимости инициализировать регистры DS:SI и ES:DI адресами строк строка_1 и строка_2 соответственно). В этом формате возможна замена сегмента первой строки:
cmps ES:строка_ 1, строка_2
Рассматриваемые команды могут предваряться префиксами повторения repe/repz (повторять, пока элементы равны, т.е. до первого неравенства) и repne/repiiz (повторять, пока элементы не равны, т.е. до первого равенства). В любом случае выполняется не более СХ операций над последовательными элементами.
После выполнения рассматриваемых команд регистры SI и DI указывают на ячейки памяти, находящиеся за теми (если DF=0) или перед теми (если DF= 1) элементами строк, на которых закончились операции сравнения.< /FONT>

Пример 1
;В полях данных сегмента данных, адресуемого через DS:
strl db 'FILE.001' ;1-я строка
;В полях данных сегмента данных, адресуемого через ES:
str2 db 'FILE.012' ;2-я строка
;В программном сегменте:
eld ;Сравнение вперед
mov SI,offset strl ;DS:SI ® strl
mov DI, off set str2 ;ES:DI ® str2
mov CX,8 ;Длина сравниваемых строк
repe cmpsb ;Поиск различия в строках
je equal ;Переход, если строки
;совпадают
notequ: ;Продолжение, если строки
;не совпадают

В примере 1 строки не совпадают, и команда je выполнена не будет. После завершения сравнения строк управление будет передано на метку notequ. Содержимое регистров в этой точке: СХ=1 (так как не выполнено сравнение одной последней пары символов), SI = <смещение strl> + 7, DI= <смещение strl> + 7 (выполнено сравнение 7 пар символов). < /FONT >

Пример 2
;В полях данных сегмента данных, адресуемого через DS:
strl db '12345678*90' ;1-я строка
;В полях данных сегмента данных, адресуемого через ES:
str2 db ' abcdefgh*ij' ;2-я строка
; В программном сегменте:
cld ;Сравнение вперед
mov SI,offset strl;DS:SI ' strl
mov DI,offset str2;ES:DI ' str2
mov CX,11 ;Длина сравниваемых строк
repne cmpsb ;Поиск первой пары
;одинаковых элементов
jne notequ ;Переход, если таковой нет
found: ;Продолжение, если пара
;одинаковых элементов найдена

В примере 2 имеется пара одинаковых элементов (*) в позиции 8 от начата строк. Поэтому команда jne выполнена не будет. После завершения сравнения строк управление будет передано на метку found. Содержимое регистров в этой точке: СХ=2 (так как не выполнено сравнение двух последних пар символов), SI = <смещенис strl> + 9, DI= <смешенис strl> + 9 (выполнено сравнение 9 пар символов). < /FONT >

Пример 3
;В полях данных сегмента, адресуемого через ES:
strl db '09.12.1998' ;1-я строка
str2 db '09.12.1998' ;2-я строка
;В программном сегменте:
eld ;Сравнение вперед
mov SI, off set strl ;DS:SI -> strl
mov DI,offset str2 ;ES:DI -> str2
mov CX,10 ;Длина сравниваемых строк
repe cmps ES:str1,ES:str2 ;Поиск различия в строках
je equal ; Переход, если строки
;одинаковы
notequal: ;Продолжение, строки
;различаются

В примере 3 строки одинаковы и после завершения сравнения управление будет передано на метку equal. Поскольку строки описаны с помощью директив db, фактически выполняется команда cmpsb, т.е. побайто вое сравнение.
386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

386+ CMPSD Сравнение строк по двойным словам
Команда аналогична командам МП 86 cmpsb и cmpsw, но позволяет сравнивать 32-битовые участки строк, адресуемых через регистры DS:ESI и ES:EDI (или, в 16-разрядных приложениях, через DS:SI и ES:DI). Использование мнемоники cinpsd с префиксом rep не означает, что в качестве счетчика будет автоматически использоваться расширенный регистр ЕСХ.

Пример
;В полях данных сегмента, адресуемого через DS
areal dd 152345,168666,954333
area2 dd 152345,168666,954331
;B программном сегменте
push DS
pop ES ;ES=DS
mov SI,offset areal ;DS:SI ->areal
mov DI,offset area2 ;ES:DI ->area2
mov CX,3 ;Будем сравнивать З числа
repe cmpsd
je equal

В приведенном примере в строках (фактически это целочисленные массивы) различаются последние элементы, и команды jc выполнена не будет.

486+ CMPXCHG Сравнение и обмен
Команда cmpxchg выполняет в одной операции сравнение и обмен операндов. Команда требует два параметра и неявным образом использует третий операнд - регистр ЕАХ. Первый операнд (приемник) должен находиться в 16- или 32-битовой ячейке памяти, второй операнд (источник) - в регистре общего назначения такого же размера. Команда выполняет сравнение операнда-приемника с содержимым неявного операнда - регистра ЕАХ. Если сравниваемые значения совпадают, операнд-приемник замещается операндом-источником (т.е. содержимое регистра записывается в память). Если сравниваемые значения не совпадают, содержимое памяти (приемник) поступает в регистр ЕАХ (рис. П1). Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Рис.П1. Действие команды cmpxchg

Пример 1
; В полях данных
mem dw 135
; В программном сегменте
mov AX,135
mov BX,60
cmpxchg mem,BX ;mem=AX. Регистр ® память ;
mem=60, BX=60, АХ=135

Пример 2
; В полях данных
mem dw 135
;В программном сегменте
mov AX,148
mov BX,60
cmpxchg mem,BX ;mem<>AX. Память ® АХ
;mem=135, BX=60, AX=148

Pentium+ CMPXCHG8B Сравнение и обмен 8 байтов
Команда cmpxchgSb выполняет в одной операции сравнение и обмен 8-байтовых операндов. Команда требует один параметр и неявным образом использует еще два операнда - пары регистров EDX:EAX и ЕСХ:ЕВХ. В качестве явного операнда команды (приемника) может выступать только 64-битная (8-байтовая) ячейка памяти. Команда выполняет сравнение операнда-приемника в памяти с содержимым EDX:EAX. Если сравниваемые значения совпадают, то операнд-приемник в памяти замещается 64-битным значением ЕСХ:ЕВХ. Если сравниваемые значения не совпадают, содержимое памяти поступает в пару регистров EDXrEAX, замещая один из сравниваемых операндов (рис. П2). Команда воздействует на флаг ZF.

Рис. П2. Действие команды cmpxchg8b

Пример 1
; В полях данных
mem dq 1122334455667788h
;В программном сегменте
mov ЕСХ,9
mov ЕВХ,5
mov EDX,11223344h
mov EAX,55667788h
cmpxchgSb mem ;mem=EDX:EAX. ECX:EBX ® mem ;
mem=0000000900000005h

Пример 2
; В полях данных
mem dq 1122334455667788h
;B программном сегменте
mov ECX,9
mov EBX,5
mov EDX,11223344h
mov EAX,55667789h
cmpxchgSb mem ;memOEDX: EAX. Mem -" EDX : EAX ;mem=1122334455667788h ;EDX=11223344h, EAX=55667788h

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

Пример 3
; В полях данных
meml db '12345678' ;Строка-операнд
mem2 db '12345678' ;Сравниваемая строка
;В программном сегменте
mov ECX,68676665h ;'efgh'
mov EBX,64636261h ;'abcd'
mov EDX,dword ptr mem2+4 ;Забираем старшую
;часть строки
mov EAX,dword ptr mem2 ;Забираем младшую
;часть строки
cmpxchg8b gword ptr meml ;Операнды совпадают
;ZF=1, mem1=''abcdefgh''
;ECX:EBX без изменений
;EDX:EAX без изменений

Пример 4
;В полях данных
meml db '12345678' ;Строка-операнд
mem2 db 'abcdefgh' ;Сравниваемая строка
; В программном сегменте
mov ECX,68676665h ;'efgh'
mov EBX,64636261h ;'abed'
mov EDX,dword ptr mem2+4 ;3абираем старшую
;часть строки
mov EAX,dword ptr mem2 ;Забираем младшую
;часть строки
cmpxchg8b qword ptm mem1 ;Операнды не совпадают
;ZF=0, EDX=38373635='5678'
;EAX=34333231='1234'
;mem1s без изменения
;При неравенстве ЕСХ:ЕВХ не принимают участие в операции

Pentium+ CPUID Идентификация процессора
Команда cpuid позволяет получить код идентификации процессора, установленного на данном компьютере. Команда в качестве неявного операнда использует регистр ЕАХ. Для процессоров Pentium регистр ЕАХ перед вызовом команды cpuid может принимать два значения: 0 и 1. Если ЕАХ=0, то команда возвращает в регистре ЕАХ код 1, а в регистрах ЕВХ, EDX и ЕСХ (именно в таком порядке) - три части символьной строки, идентифицирующей изготовителя процессора. Для процессоров Intel возвращаемая строка в целом имеет вид "Genumclatcl".
Если перед вызовом команды cpuid значение ЕАХ равно 1, то команда возвращает в регистре ЕАХ коды разработки конкретной версии процессора, а в регистре EDX код IBFli, содержащий информацию о возможностях процессора.
Коды разработки в регистре ЕАХ хранятся в следующем формате:
биты 0 ... 3 - номер поколения (например, 3);
биты 4 ... 7 - модель (например, 4);
биты 8 ... 11 - семейство (5 для Pentium).
Содержимое регистра EDX включает конфиденциальную информацию изготовителя, а также говорит о наличии на кристалле микропроцессора арифметического сопроцессора (бит 0) и поддержке команды cmpxchgSb (бит 8).

Пример
;В полях данных mem dd 0,0,0 ;В программном сегменте
mov ЕАХ,О
cpuid ;EAX=0001h
mov mem, ЕВХ
mov mem+4, EDX
mov mem+8, ECX ;mem='Genuinelntel'
cpuid EAX=543h (например) ,EDX = lBFh

CWD Преобразование слова в двойное слово
Команда cwd заполняет регистр DX знаковым битом содержимого регистра АХ, преобразуя тем самым 16-разрядное число со знаком в 32-разрядное, размещаемое в регистрах DX:AX. Команду удобно использовать для преобразования двухбайтового делимого в четырсхбайтовое (двойное слово) при делении на 16-разрядный операнд. Команда не имеет параметров и не воздействует на флаги процессора.

Пример 1
mov AX,32767 ;AX=7FFFh
cwd ;AX=7FFFh, DX=OOOOh.
;DX:AX=32767

Пример 2
mov AX,-32768 ;AX=8000h
cwd ;AX=8000h, DX=FFFFh.
;DX:AX=-32768

386+ CWDE
Преобразование слова в двойное слово с расширением
Команда cwde заполняет старшую половину регистра ЕАХ знаковым битом содержимого регистра АХ, преобразуя тем самым 16-разрядное число со знаком в 32-разрядное, размещаемое в расширенном регистре ЕАХ. Команда не имеет операндов и не воздействует на флаги процессора.

Пример
; В полях данных
mem dw - 3
;В программном сегменте
mov AX,mem ;AX=FFFD
cwde ;EAX=FFFFFFFDh

DAA Десятичная коррекция в регистре AL после сложения

DAA Десятичная коррекция в регистре AL после сложения
Команда daa корректирует результат сложения в регистре AL двух упакованных двоично-десятичных (BCD) чисел (по одной цифре в каждом полубайте), чтобы получить пару правильных упакованных двоично-десятичных цифр. Команда используется вслед за операцией сложения упакованных двоично-десятичных чисел. Если результат сложения превышает 99, возникает перенос и устанавливается флаг CF. Команда воздействует на флаги SF, ZF, AF, PF и CF.

Пример 1
mov AL,87h ;Упакованное BCD 87
add AL,04h ;После сложения AL=8Bh
daa ;AL=91h, т.е. упакованное BCD 91

Пример 2
mov AL,87h ;Упакованное BCD 87
add AL,11h ;После сложения AL=97h
daa ;AL=97h, т.е. упакованное
;BCD 97 (в данном случае
;команда daa ничего не делает)

DAS Десятичная коррекция в регистре AL после вычитания
Команда das корректирует результат вычитания в регистре AL двух упакованных двоично-десятичных (BCD) чисел (по одной цифре в каждом полубайте), чтобы получить пару правильных упакованных десятичных цифр. Команда используется вслед за операцией вычитания упакованных двоично-десятичных чисел. Если для вычитания требовался заем, устанавливается флаг CF. Команда воздействует на флаги SF, ZF, AF, PF и CF.

Пример 1
mov AL,55h ;Упакованное BCD 55
sub AL,19h ;После вычитания AL=3Ch
das ;AL=36h, т.е. упакованное BCD 36

Пример 2
mov AL,55h ;Упакованное BCD 55
sub AL,15h ;После вычитания AL=40h
das ;AL=40h, т.е. упакованное
;BCD 40 (в данном случае
;команда das ничего не делает)

DEC Декремент (уменьшение на 1)
Команда dec вычитает 1 из операнда, в качестве которого можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Операнд интерпретируется как число без знака. Команда воздействует на флаги OF, SF, ZF, AF и PF.

Пример 1
mov AX,0FFFFh
dec AX ;AX=FFFEh

Пример 2
mov CX,0
dec CX ;CX=FFFFh

Пример 3
mov CX,3500h
dec CL ;CX=35FFh

Пример 4
; В полях данных
mem dw 68
;B программном сегменте
dec mem mem=67

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX, 0
dec EAX ;EAX=FFFFFFFFh

DIV Деление целых чисел без знака
Команда div выполняет деление целого числа без знака, находящегося в регистрах АХ (в случае деления на байт) или DX:AX (в случае деления на слово), на операнд-источник (целое число без знака). Размер делимого в два раза больше размеров делителя и остатка.
Для однобайтовых операций делимое помещается в регистр АХ; после выполнения операции частное записывается в регистр AL, остаток - в регистр АН.
Для двухбайтовых операций делимое помещается в регистры DX:AX (в DX - старшая часть, в АХ - младшая); после выполнения операции частное записывается в регистр АХ, остаток - в регистр DX.
В качестве операнда-делителя команды div можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается деление на непосредственное значение. Если делитель равен 0, или если частное не помещается в назначенный регистр, возбуждается прерывание с вектором 0. Команда не воздействует на флаги процессора.
Команду div можно использовать для целочисленного деления неупакованного двоично-десятичного числа в регистре АХ не неупакованный двоично-десятичный делитель, если перед ней выполнить команду aad (см. пример 3).

Пример 1
mov AX,506 ;Делимое
mov BL,50 ;Делитель
div BL ;AL=0Ah (частное), AH=06h (остаток)

Пример 2
; В полях данных
long dd 65537 ;Делимое
;В программном сегменте
mov DX,word ptr long+2 ;DX=0001h, старшая
;часть делимого
mov AX,word ptr long ;AX=0001h, младшая
;часть делимого
mov CX,256 ;Делитель
div CX ;AX=0100h (частное),
;DX=0001h (остаток)

Пример З
mov AX,0807h ;Неупакованное BCD 87
mov DL,09h ;Неупакованное BCD 9
aad ;AX=0057h=87
div DL ;AX=0609h, т.е. 9 и 6 в остатке

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если делитель представляет 32-битовую величину, то возможен только один вариант команды деления, когда делимое находится в парс регистров EDX:EAX. В этом случае частное будет помещено в регистр ЕАХ, остаток - в EDX.

Пример
mov ЕАХ, 0FFFFFFFh ;Младшая часть делимого
mov EDX,0 ;Старшая часть делимого
mov EBX,256 ;Делитель
div EBX ;Частное в EAX=000FFFFFh,
;Остаток в EDX=000000FFh

386+ ENTER
Создание стекового кадра для параметров процедуры
Команда enter, обычно являющаяся первой командой процедуры, выделяет заданный объем стекового пространства для локальных (автоматических) параметров процедуры, предоставляя процедуре указатель на выделенную область (в качестве такого указателя используется регистр ЕВР) и смещая указатель стека ESP так, чтобы он указывал на начало свободного стекового пространства. В результате процедура имеет возможность обращаться по ходу своего выполнения к своим локальным параметрам и, в то же время, пользоваться оставшимся пространством стека для временного сохранения в нем любых данных с помощью команд push и pop. Команда leave в конце процедуры выполняет обратные действия, возвращая стек в исходное состояние и уничтожая область локальных переменных. Локальными, как известно, называются как раз те переменные, которые существуют только в течение времени выполнения некоторой процедуры, и автоматически исчезают после се завершения.
Команды enter и leave используются многими языками высокого уровня для управления доступом к локальным переменным вложенных процедур.
Команда enter имеет два операнда. Первый (16-битовое непосредственное значение) определяет число байтов, выделяемых в стеке для локальных переменных. Для 32-разрядных приложений место в стеке выделяется двойными словами (по 4 байт), для 16-разрядных - словами (по 2 байт). Второй операнд (8-битовос непосредственное значение) задаст так называемый лексический уровень процедуры, характеризующий степень ее вложенности. В зависимости от значения лексического уровня, команда enter выполняется по-разному. При лексическом уровне, равном 0, реа-лизуется невложенная форма команды enter. В этом случае после входа в процедуру (командой call) с сохранением в стеке адреса возврата, в стек заносится текущее содержимое регистра ЕВР, в ЕВР копируется текущее значение указателя стека, а указатель стека смещается на число байтов, заданное первым операндом команды enter (рис. ПЗ). Создаваемая на сте-ке структура носит название стекового кадра, а регистр ЕВР выполняет в данном случае функцию указателя стекового кадра.

Рис. ПЗ. Состояние стека после входа в подпрограмму и выполнения команды enter 8,0 (на рисунке адреса ячеек стека уменьшаются вниз).

Подпрограмма имеет возможность обращаться к своим локальным переменным по адресам ESP-4 и ESP-8 (для случая резервирования места под две переменные). Занеся в стек по этим адресам некоторые данные (полученные в качестве параметров вызова через регистры общего назначения или созданные самостоятельно) подпрограмма может затем многократно к ним обращаться, не боясь их затирания в процессе использования стека. Поскольку команда enter настроила указатель стека на область, находящуюся за пределами локальных переменных, программа может использовать команды push для сохранения в стеке временных данных.
Команда leave, размещаемая в самом конце процедуры, перед завершающей командой ret, копирует содержимое ЕВР в ESP, освобождая (в логическом плане) область локальных переменных, и снимает со стека сохраненное там исходное содержимое ЕВР. После этого командой ret можно вернуться в вызывающую процедуру.
Поскольку первый параметр команды enter имеет размерность слова, процедура в принципе имеет возможность зарезервировать в стеке для своих локальных переменных до 64 Кбайт стекового пространства.
Лексические уровни, отличные от 0, используются в тех случаях, когда по правилам языка высокого уровня каждая вложенная процедура имеет право обращаться к локальным переменным всех вышележащих процедур, но не к процедурам, находящимся на параллельных с ней ветвях вложенности. Другими словами, область видимости переменных распространяется на все вложенные процедуры, но две подпрограммы, вызываемые из одной и той же (вышележащей) процедуры, "не видят" друг друга.
В таких случаях главной процедуре назначается лексический уровень 1, все вызываемые из нее подпрограммы получают значение лексического уровня 2, подпрограммы, вызываемые из этих процедур, имеют уровень 3 и т.д. Команды enter при ненулевом значения второго параметра создают в стеке стековые кадры с более сложной структурой. Отличие такого стекового кадра от рассмотренного выше заключается в том, что в него, помимо области локальных переменных, входят также указатели стековых кадров всех вышележащих процедур. В результате любая подпрограмма может с помощью своего указателя (т.е. содержимого ESP) обратиться к собственных! переменным, а используя хранящиеся в стеке указатели кадров вышележащих процедур, "дотянуться" и до их локальных переменных. По-прежнему команды leave освобождают стек от стековых кадров вместе со всеми находящимися в них данными.

Пример
;Вызывающая процедура
call subrl
;Подпрограмма subrl
subrl proc
enter2048,0 ;Место под локальные данные
. . . ;Работа с локальными данными
leave
ret

HLT Останов
Команда hlt прекращает выполнение программы и переводит процессор в состояние останова. Работа процессора возобновляется после операции запуска, а также в случае прихода немаскируемого или разрешенного маскируемого прерываний.

IDIV Деление целых чисел со знаком
Команда IDIV выполняет деление целого числа со знаком, находящегося в регистрах АХ (в случае деления на байт) или DX:AX (в случае деления на слово), на операнд-источник (целое число со знаком). Размер делимого в два раза больше размеров делителя и остатка. Оба результата рассматриваются как числа со знаком, причем знак остатка равен знаку делимого.
Для однобайтовых операций делимое помещается в регистр АХ; после выполнения операции деления частное записывается в регистр AL, остаток - в регистр АН.
Для двухбайтовых операций делимое помещается в регистры DX:AX (в DX - старшая часть, в АХ - младшая); после выполнения операции деления частное записывается в регистр АХ, остаток - в регистр DX.
В качестве операнда-делителя команды idiv можно указывать регистр данных или ячейку памяти; не допускается деление на непосредственное значение. Если делитель равен 0, или если частное не помещается в назначенный регистр, возбуждается прерывание через вектор 0. Команда не воздействует на флаги процессора.

Пример 1
mov AX,506 ;Делимое
mov BL,50 ;Делитель
idiv BL ;AL=0Ah (частное), AH=06h
; (остаток)

Рис.П3. Состояние стека после входа в подпрограмму и выполнения команды enter8,0(на рисунке адреса ячеек уменьшаются в низ)

Пример 2
;В полях данных
long dd 0F0007h ;Делимое
; В программном сегменте
mov DX,word ptr long+2;DX=000Fh, старшая
;часть делимого
mov AX,word ptr long;AX=0007h, младшая
;часть делимого
mov CX,256 ;Делитель
idiv СХ ;AX=0F00h (частное),
;DX=0007h (остаток)
Пример 3
mov AX,-506 ;AX=FE06h, делимое
mov BL,50 ;Делитель
idiv BL ;AL=F6h (-10), AH=FAh (-6)

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если делитель представляет 32-битовую величину, то возможен только один вариант команды деления, когда делимое находится в парс регистров EDX:EAX. В этом случае частное будет помещено в регистр БАХ, остаток - в EDX.

Пример
; В полях данных
dvd dq -100001 Делимое
;B программном сегменте
mov EAX,dword ptr dvd EAX=FFFE795Fh
;(младшая часть делимого)
mov EDX,dword ptr dvd+4 EDX=FFFFFFFFh (старшая
;часть делимого)
mov EBX,50 Делитель
idiv EBX Частное в EAX=FFFFF830h=
;-2000, остаток в EDX=FFFFFFFFh=-1

IMUL Умножение целых чисел со знаком
Команда IMUL выполняет умножение целого числа со знаком, находящегося в регистре AL (в случае умножения на байт) или АХ (в случае умножения на слово), на операнд-источник (целое число со знаком). Размер произведения в два раза больше размера сомножителей.
Для однобайтовых операций один из сомножителей помещается в регистр AL; после выполнения операции произведение записывается в регистр АХ.
Для двухбайтовых операций один из сомножителей помещается в регистр АХ; после выполнения операции произведение записывается в регистры DX:AX (в DX - старшая часть, в АХ - младшая).
В качестве операнда-сомножителя команды imul можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается умножение на непосредственное значение. Команда воздействует на флаги OF и CF. Если АН или DX представляют собой просто знаковое расширение AL или АХ, соответственно (т.е. результат умножения со знаком верен), OF и CF сбрасываются в 0; в противном случае (результат со знаком не помещается в АХ или DX:AX) OF и CF устанавливаются в 1.

Пример 1
mov AL,5 ;Первый сомножитель
mov BL,3 ;Второй сомножитель
imul BL ;AX=000Fh (произведение)

Пример 2
mov AX,256 ;Первый сомножитель
mov BX,256 ;Второй сомножитель
imul BX ;DX=0001h, AX=0000h
;(число 65536)

Пример 3
mov AL,-5 ;AL=FBh
mov BL,3 ;BL=03h
imul BL ;AX-'FFF1h (-15)

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. Имеются также варианты команды с двумя и тремя операндами.
Для команды imul с одним операндом второй сомножитель должен располагаться в AL, АХ или ЕАХ. Процессор выбирает размерность второго сомножителя, исходя из размерности первого, указанного в качестве операнда. 16-, 32- или 64-битовый знаковый результат помещается в регистры АХ, DX:AX или EDX:EAX, соответственно. Если после операции умножения содержимое АН, DX или EDX является лишь знаковым расширением AL, АХ или ЕАХ, соответственно, то флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.
Для команды imul с двумя операндами их произведение записывается в первый операнд; второй операнд не изменяется. В качестве первого операнда могут выступать 16- или 32-разрядные регистры общего назначения; в качестве второго операнда - 16- или 32-разрядные регистры общего назначения, 16- или 32-битовые ячейки памяти или непосредственное значение. Оба операнда должны иметь один размер. Если результат умножения помещается в первый операнд, флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.
Для команды imul с тремя операндами произведение второго и третьего операндов записывается в первый операнд. В качестве первого операнда могут выступать 16- или 32-разрядные регистры общего назначения; в качестве второго операнда - 16- или 32-разрядные регистры общего назначения или 16- или 32-битовые ячейки памяти; в качестве третьего операнда - только непосредственное значение. Два первых операнда должны иметь один размер. Если результат умножения помещается в первый операнд, флаги CF и OF сбрасываются в 0. В противном случае они устанавливаются в 1.

Пример 1
mov EAX,-1 ;Первый сомножитель
mov ESI,100000000 ;Второй сомножитель
imul ESI ;EDX=FFFFFFFFh,
;EAX=FA0AlF00h
;Результат=-100000000

Пример 2
;В полях данных
ор2 dd 100h ;Первый сомножитель
; В программном сегменте
mov EAX,400000h ;Второй сомножитель
imul EAX,op2 ;EAX=40000000h
Пример 3
mov BX,300h
imul АХ,ВХ,4 ;AX=300h*4=0C00h

IN Ввод из порта
Команда in вводит в регистр AL или АХ соответственно байт или слово из порта, указываемого вторым операндом. Адрес порта помещается в регистр DX. Если адрес порта не превышает 255, он может быть указан непосредственным значением. Указание регистра-приемника (AL или АХ) обязательно, хотя с другими регистрами команда in не работает, и их указывать нельзя. Команда не воздействует на флаги процессора.

Пример 1
in AL, 60h ;Ввод байта из порта 60h

Пример 2
mov DX,3D5h ;Адрес порта
in AL,DX ;Ввод байта из порта 3D5h

386+ Допустимо использование в качестве операнда-приемника расширенного регистра ЕАХ (если адресуемое устройство позволяет прочитать из его порта двойное слово).

Пример
mov DX,345h ;Адрес порта
in EAX,DX

INC Инкремент (увеличение на 1)
Команда inc прибавляет 1 к операнду, в качестве которого можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Операнд интерпретируется как число без знака. Команда воздействует на флаги OF, SF, ZF, AF и PF. Команда не воздействует на флаг CF; если требуется воздействие на этот флаг, необходимо использовать команду add op,l.

Пример 1
mov AX,0563h
inc AX ;AX=0564h

Пример 2
mov BH,15h
inc BH ;BH=16h

Пример 3
mov AX,A5FFh
inc AL ;AX=A500h
inc AH ;AX=A600h

Пример 4
mov AX,0FFFFh
inc AX ;AX=0000h, ZF=1, CF=0
;Для сравнения:
mov CX,0FFFFh
add CX,1 ;CX=0000h, ZF=1, CF=1

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных
mem dd 12345678h
; В программном сегменте
inc mem ;mem=12345679h

386+
INS Ввод строки из порта
INSB Ввод байта из порта
INSW Ввод слова из порта
INSD Ввод двойного слова из порта
Команды предназначены для ввода данных из порта непосредственно в память. Адрес порта указывается, как и для команды in, в регистре DX, при этом задание адреса порта непосредственным значением не допускается. Данные пересылаются по адресу, находящемуся в паре регистров ES:EDI. Замена сегмента не допускается. Команда insb переносит из порта 1 байт, команда insw - 1 слово, команда insd - 1 двойное слово, а команда ins может быть использована для передачи байтов, слов и двойных слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db, dw или dd). После передачи данных регистр EDI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1, 2 или 4, в зависимости от размера передаваемых данных. Вариант команды ins имеет формат

ins строка, DX

(что не избавляет от необходимости инициализировать регистры ES:EDI адресом строки).
Если устройство, адресуемое через порт, может передавать последовательность данных, то команды ins можно предварить префиксом повторения rep. В этом случае из порта принимается СХ элементов данных заданного размера.
Команды ins не воздействуют на флаги процессора.

Пример
;В сегменте данных, адресуемых через DS
mem dw 0
;В программном сегменте
push DS
pop ES ;ES=DS
mov DI,offset mem;ES:DI -> mem
mov DX,303h ;Адрес порта
insw ;Ввод из порта 16-битового данного

INT Программное прерывание
Команда hit инициирует в процессоре процедуру прерывания, в результате которой управление передается на обработчик прерывания с номером n, который указан в качестве операнда команды int. В стек текущей программы заносится содержимое регистра флагов, сегментного регистра CS и указателя команд IP, после чего в регистры IP и CS передается содержимое двух слов из вектора прерывания с номером n (расположенных по адресам 0:n*4 и 0:n*4+2). Команда сбрасывает флаги IF и TF в 0. Команда iret, которой всегда завершается обработчик прерывания, восстанавливает исходное состояние этих флагов.

Пример 1
int 60h ;Переход на прикладной
;обработчик прерывания 60h

Пример 2
mov AH,1 ;Функция MS-DOS - ввод с
;клавиатуры кода ASCII символа
int 2h ;Вызов MS-DOS

Пример 3
mov АН, 0 ;Функция BIOS (прерывание
;16h) - ввод с клавиатуры
;кода ASCII и скен-кода символа
int 16h ;Вызов BIOS

INTO Прерывание по переполнению
Команда into, будучи установлена вслед за какой-либо арифметической, логической или строковой командой, вызывает обработчик прерываний через вектор 4, если предшествующая команда установила флаг переполнения OF. Перед использованием команды INTO прикладной программист должен поместить в вектор прерывания 4 двухсловный адрес своей программы обработки прерывания по переполнению. Команда сбрасывает флаги IF и TF в 0. Команда iret, которой всегда завершается обработчик прерывания, восстанавливает исходное состояние этих флагов.

Пример
add AX,BX ;Произвольная команда
into ;Вызов прикладного
;обработчика через вектор 4,
;если OF=1
... ;Продолжение программы, если OF=0

IRET Возврат из прерывания
Команда iret возвращает управление прерванному в результате аппаратного или программного прерывания процессу. Команда извлекает из стека три верхние слова и помещает их в регистры IP, CS и флагов (см. команду int). Командой iret должен завершаться любой обработчик прерываний, как аппаратных, так и программных (от команды int). Команда не воздействует на флаги, однако она загружает в регистр флагов из стека его исходное содержимое, которое было там сохранено процессором в процессе обслуживания прерывания. Если требуется, чтобы после возврата из обработчика программного прерывания командой iret какие-либо флаги процессора были установлены требуемым образом (весьма распространенный прием), их установку надо выполнить в копии флагов в стеке.

386+ IRETD
Возврат из прерывания в 32-разрядном режиме
Команда iretd используется в защищенном режиме для возврата из обработчика прерывания или исключения, а также для переключения на исходную задачу. В отличие от 16-разрядной команды iret, данная команда, завершая обработку прерывания или исключения, снимает со стека 3 двойных слова, содержащие расширенный регистр флагов EFALGS, CS и расширенный указатель команд EIP. В случае переключения задач команда iretd выполняет переключение контекстов задач - сохранение состояния завершающейся задачи в ее сегменте состояния задачи и загрузку регистров процессора из сегмента состояния исходной задачи.

Jcc Команды условных переходов
Команды, обозначаемые (в книгах, не в программах!) Jcc, осуществляют переход по указанному адресу при выполнении условия, заданного мнемоникой команды. Если заданное условие не выполняется, переход не осуществляется, а выполняется команда, следующая за командой Jcc. Переход может осуществляться как вперед, так и назад в диапазоне + 127...-128 байтов.
В составе команд процессора предусмотрены следующие команды условных переходов:
Команда Перейти, если Условие перехода
ja выше CF=0 и ZF=0
jae выше или равно CF=0
jb ниже CF=1
jbe ниже или равно CF=1 или ZF=1
jc перенос CF=1
jcxz CX=0 CX=0
je равно ZF=1
jg больше ZF=0 или SF=OF
jge больше или равно SF=OF
jl меньше SF не равно OF
jle меньше или равно ZF=1 или SF не равно OF
jna не выше CF=1 или ZF=1
jnae не выше и не равно CF=1
jnb не ниже CF=0
jnbe не ниже и не равно CF=0 и ZF=0
jnc нет переноса CF=0
jne не равно ZF=0
jng не больше ZF=1 или SF не равно OF
jnge не больше и не равно SF не равно OF
jnl не меньше SF=OF
jnle не меньше и не равно ZF=0 и SF=OF
jno нет переполнения OF=0
jnp нет четности PF=0
jns знаковый бит равен О SF=0
jnz не нуль ZF=0
jo переполнение OF=1
jp есть четность PF=1
jpe сумма битов четная PF=1
jpo сумма битов нечетная PF=0
js знаковый бит равен SF=1
jz нуль ZF= I
Команды условных переходов, осуществляющие переход по условию "выше - ниже", предназначены для анализа чисел без знака; команды, осуществляющие переход по условию "больше - меньше", предназначены для анализа чисел со знаком.

Пример 1
cmp СХ,0 ;CX=0?
je equal ;Если да, перейти па метку equal

Пример 2
cmp AX,1000 ;Пусть AX=8000h=32768
;(=-32768)
ja above ;32768 > 1000. Переход будет

Пример 3
cmp AX,1000h ;Пусть AX=8000h=-32768
; (=32768)
jg greater ;-32768 < 1000h. Перехода не будет

Пример 4
int 21h ;Вызов системной функции
jc error ;Если CF=1 (ошибка), перейти
; на метку error

386+ Команды условных переходов имеют варианты 16- и 32-разрядной адресации (при тех же мнемонических обозначениях) и могут передавать управление в диапазоне -32768...+32767 байт для сегментов с атрибутом размера 16 и в диапазоне -231...+23|-1 байт для сегментов с атрибутом размера 32.

JMP Безусловный переход
Команда jmp передает управление в указанную точку того же или другого программного сегмента. Адрес возврата не сохраняется. Команда не воздействует на флаги процессора.
Команда jmp имеет пять разновидностей:
- переход прямой короткий (в пределах -128... + 127 байтов);
- переход прямой ближний (в пределах текущего программного сегмента) ;
- переход прямой дальний (в другой программный сегмент);
- переход косвенный ближний;
- переход косвенный дальний.
Все разновидности переходов имеют одну и ту же мнемонику jmp, хотя и различающиеся коды операций. Во многих случаях транслятор может определить вид перехода по контексту, в тех же случаях, когда это невозможно, следует использовать атрибутные операторы:
short - прямой короткий переход;
near ptr - прямой ближний переход;
far ptr - прямой дальний переход;
word ptr - косвенный ближний переход;
dword ptr - косвенный дальний переход.

Примеры прямого короткого перехода
jmp short shpt ;Переход на метку shpt
;в пределах +127...-128 байтов
jmp shpt ;To же самое, если shpt
;находится выше по тексту программы

Примеры прямого ближнего перехода
jmp pt ;Переход на метку pt
;в пределах текущего сегмента
jmp near ptr pt ;To же самое

Примеры косвенных ближних переходов
Пример 1
mov BX,offset pt ;ВХ=адрес точки перехода
jmp BX ;Переход в точку pt

Пример 2
;В полях данных:
addr dw pt ;Ячейка с адресом точки перехода
;В программном сегменте:
jmp DS:addr ;Переход в точку pt
jmp word ptr addr ;To же самое

Пример 3
; В полях данных:
addr dw pt ;Ячейка с адресом точки перехода
;В программном сегменте:
mov DI,offset addr ;В1=адрес ячейки с адресом
;точки перехода
jmp [DI] ;Переход в точку pt

Пример 4
;В полях данных:
tbl dw ptl ;Ячейка с адресом 1
dw pt2 ;Ячейка с адресом 2
dw pt3 ;Ячейка с адресом 3
;В программном сегменте:
mov BX,offset tbl ;BX=aflpec таблицы адресов переходов
mov SI, 4 ;31=смещение к адресу pt3
call [BX][SI] ;Переход в точку pt3

Примеры прямых дальних переходов
jmp far ptr farpt ;Переход на метку farpt в
;другом программном сегменте
jmp farpt ;Переход на метку farpt в другом
;программном сегменте, если farpt
;объявлена дальней меткой
;директивой farpt label far

Примеры косвенных дальних переходов

Пример 1
; В полях данных:
addr dd pt ;Поле с двухсловным
;адресом точки перехода ;В программном сегменте:
jmp DS:addr ;Переход в точку pt
jmp dword ptr addr ;To же самое

Пример 2
; В полях данных:
addr dd pt ;Поле с двухсловным
;адресом точки перехода
;В программном сегменте:
mov DI,offset addr ;DI =адрес поля с адресом
;точки перехода jmp [DI] ;Переход в точку pt

386+ Допустимо использование дополнительных режимов адресации 32-разрядных процессоров. Для 32-разрядных приложений допустимо использование 32-битовых операндов. В защищенном режиме вместо сегментного адреса сегмента (при дальних переходах) выступает его селектор.

LAHF Загрузка флагов в регистр АН
Команда lahf копирует флаги SF, ZF, AF, PF и CF соответственно в разряды 7, 6, 4, 2 и 0 регистра АН. Значение битов 5, 3 и 1 регистра АН не определено. Команда не имеет параметров и не изменяет флаги процессора.
Команда lahf (совместно с командой sahf) дает возможность читать и изменять значения флагов процессора, в том числе флагов SF, ZF, AF и PF, которые нельзя изменить непосредственно. Однако следует иметь в виду, что команда lahf переносит в АН только младший байт регистра флагов. Поэтому нельзя изменить с ее помощью, например, состояние флага OF.

Пример 1
lahf ;Регистр АН отображает
;состояние регистра флагов
or AH,80h ;Установка бита 7 = SF
sahf ;Загрузка АН в регистр
;флагов, где теперь SF = 1

Пример 2
lahf ;Регистр АН отображает
;состояние регистра флагов
and AH,0BFh ;Сброс бита 6 = ZF
sahf ;Загрузка АН в регистр
;флагов, где теперь ZF = О

386Р+ LAR Загрузка прав доступа
Команда lar загружает в первый операнд (16- или 32-разрядный регистр) поле атрибутов сегмента из дескриптора сегмента, заданного селектором во втором операнде. В качестве операнда с селектором может использоваться 16- или 32-разрядный регистр или ячейка памяти. В операнд-приемник поступают два байта атрибутов селектора с замаскированным полем старших битов границы сегмента.

LDS Загрузка указателя с использованием регистра DS
Команда Ids считывает из памяти по указанному адресу двойное слово (32 бит), содержащее указатель (полный адрес некоторой ячейки), и загружает младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр, а старшую половину указателя (т.е. сегментный адрес) в регистр DS. Таким образом, команда

Ids reg, mem

эквивалентна следующей группе команд:

mov reg,word ptr mem
mov DS,word ptr mem+2

В качестве первого операнда команды Ids указывается регистр общего назначения; в качестве второго - ячейка памяти с двухсловным содержимым. Указатель, содержащийся в этой ячейке, может быть адресом как процедуры, так и поля данных. Команда не воздействует на флаги процессора.

Пример 1
; В полях данных:
addr dd myproc ;Двухсловный адрес процедуры
;myproc
;В программном сегменте:
Ids SI,addr ;DS:SI -> myproc

Пример 2
; В полях данных:
mem dw 25 ;Ячейка памяти с
; произвольным содержимым
addr dd myproc ;Двухсловный адрес этой
;ячейки
;В программном сегменте:
mov BX,offset addr ;ВХ=адрес ячейки addr
Ids DX, [BX] ;DХ=смещение ячейки mem,
;DS=сегментный адрес ячейки
;mem

Пример 3
; В полях данных:
dptr dd procl ;Полный адрес процедуры
; р г о с 1
dd proc2 ;Полный адрес процедуры
;ргос2
dd ргосЗ ;Полный адрес процедуры
; р г о с 3
;В программном сегменте:
mov SI, 8 ; Смещение к адресу ргосЗ
Ids DI,dptr[SI] ;DS:DI ® ргосЗ

386+ Допустимо использование 32-разрядного регистра-приемника и 32-битового смещения в памяти, а также дополнительных режимов адресации 32-разрядных процессоров. В защищенном режиме вместо сегментного адреса сегмента выступает его селектор.

LEA Загрузка исполнительного адреса
Команда lea загружает в регистр, указанный в команде в качестве первого операнда, относительный адрес второго операнда (не значение операнда!). В качестве первого операнда следует указывать регистр общего назначения (не сегментный), в качестве второго - ячейку памяти. Команда

lea reg,mem

эквивалентна команде

mov reg,offset mem

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

Пример 1
; В полях данных:
message db ; 'Идут измерения'
;В программном сегменте:
lea SI,message ;DS:SI -> message

Пример 2
; В полях данных:
nmb db '0123456789'
;В программном сегменте:
mov SI,7 ;Смещение символа '7'
lea DX,nmb[SI] ;ВХ=адрес символа '7'

Пример 3
; В полях данных:
nmb db '0123456789'
;В программном сегменте:
mov BX, off set msg
mov SI, 9 ;Смещение символа '9'
lea SI, [BX] [SI] ;31=адрес символа '9'

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

386+ LEAVE Выход из процедуры высокого уровня
Команда leave выполняет действия, противоположные действиям последней команды enter. Она логически уничтожает созданный командой enter стековый кадр со всеми содержащимися в нем локальными переменными и подготавливает стек к выполнению команды irct, завершающей переход в вызывающую процедуру. Команда leave не имеет параметров. Более подробное описание и пример см. в описании команды enter.

LES Загрузка указателя с использованием регистра ES
Команда les считывает из памяти по указанному адресу двойное слово (32 бит), содержащее указатель (полный адрес некоторой ячейки), и загружает младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр, а старшую половину указателя (т.е. сегментный адрес) в регистр ES. Таким образом, команда

les reg,mem

эквивалентна следующей группе команд:

mov reg,word ptr mem mov ES,word ptr mem+2

В качестве первого операнда команды les указывается регистр общего назначения; в качестве второго - ячейка памяти с двухсловным содержимым. Указатель, содержащийся в этой ячейке, может быть адресом как процедуры, так и поля данных. Команда не воздействует на флаги процессора.

Пример 1
;В полях данных:
addr dd myproc ;Двухсловный адрес процедуры
;myproc
;В программном сегменте:
les SI,addr ;ES:SI ® myproc

Пример 2
;В полях данных:
mem dw 25 ;Ячейка памяти с
;произвольным содержимым
addr dd myproc ;Двухсловный адрес этой ячейки
;В программном сегменте:
mov BX,offset addr ;ВХ=адрес ячейки addr
…
les DX, [BX] ; DХ=смещение ячейки mem,
;ЕS=сегментный адрес ячейки mem

Пример 3
;В полях данных:
dptr dd procl ;Полный адрес процедуры prod
dd proc2 ;Полный адрес процедуры ргос2
dd ргосЗ ;Полный адрес процедуры ргосЗ
;В программном сегменте:
mov SI, 8 ;Смещение к адресу ргосЗ
les DI,dptr[SI] ;ES:DI -> ргосЗ

386+ Допустимо использование 32-разрядного регистра-приемника и 32-битового смещения в памяти, а также дополнительных режимов адресации 32-разрядных процессоров. В защищенном режиме вместо сегментного адреса сегмента выступает его селектор.

386+
LFS Загрузка указателя с использованием регистра FS
LGS Загрузка указателя с использованием регистра FS
LSS Загрузка указателя с использованием регистра FS
Команды считывают из памяти полный указатель, состоящий из селектора и 16-битового или 32-битового смещения, и загружают младшую половину указателя (т.е. относительный адрес) в указанный в команде регистр общего назначения, а старшую половину указателя (т.е. селектор) в сегментный регистр, указанный в мнемонике команды.
В качестве первого операнда всех перечисленных команд указывается 16- или 32-разрядный регистр общего назначения; в качестве второго - ячейка памяти с 32- или 48-битовым содержимым. Команда не воздействует на флаги процессора.
Примеры см. в описании команд Ids и les.

386Р+ LGDT
Загрузка регистра таблицы глобальных дескрипторов
Команда Igdt загружает регистр таблицы глобальных дескрипторов (GDTR) из 48-битового псевдодескриптора, содержащего 32-битовый базовый адрес и 16-битовую границу таблицы глобальных дескрипторов, находящейся в памяти. В качестве операнда команды Igdt выступает относительный адрес псевдодескриптора.

386Р+ LIDT
Загрузка регистра таблицы дескрипторов прерываний
Команда lidt загружает регистр таблицы дескрипторов прерываний (IDTR) из 48-битового псевдодескриптора, содержащего 32-битовый базовый адрес и 16-битовую границу таблицы дескрипторов прерываний, находящейся в памяти. В качестве операнда команды lidt выступает относительный адрес псевдодескриптора.

386Р+ LLDT
Загрузка регистра таблицы локальных дескрипторов
Команда lldt загружает регистр таблицы локальных дескрипторов (LDTR) селектором, определяющим таблицу локальных дескрипторов (LDT). Селектор LDT должен входить в таблицу глобальных дескрипторов. В качестве операнда команды lldt, содержащего селектор LDT, можно использовать 16- или 32-разрядный регистр общего назначения или 16-или 32-битовое поле памяти.

386Р+ LMSW Загрузка слова состояния машины
Команда Imsw загружает в регистр слова состояния машины (так называется младшая половина управляющего регистра процессора CRO) слово состояния машины, взятое из указанного в команде операнда. В качестве операнда можно использовать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти.
Команду Imsw можно использовать для перевода процессора из реального в защищенный режим или наоборот. В первом случае после чтения слова состояния командой smsw надо установить в нем бит 0 (бит РЕ) и загрузить назад в CRO командой Imsw. Во втором случае после после чтения слова состояния командой smsw надо сбросить в нем бит 0 и загрузить назад в CRO командой Imsw.

LOCK Запирание шины
Префикс lock, помещенный перед командой, устанавливает сигнал на линии LOCK системной шины и запрещает доступ к шине другим процессорам на время выполнения данной команды. Этот префикс предназначен для использования в многопроцессорных многозадачных системах для обеспечения исключительного доступа к памяти данного процесса (и данного процессора) на время проверки или модификации некоторой ячейки памяти. Типичный пример операций такого рода - работа с семафорами.
Если два (или более) процесса, идущие на разных процессорах, используют какой-либо общий ресурс, например, файл или лазерный диск, то необходимо обеспечить механизм, запрещающий одновременное обращение процессов к общему ресурсу. Эта задача решается с помощью семафора - ячейки памяти (байта или даже бита), состояние которого отражает доступность или, наоборот, занятость ресурса.
Если процессору понадобился общий ресурс, он читает состояние семафора и, если ресурс занят, продолжает опрашивать семафор до тех пор, пока другой процессор, использующий в настоящий момент общий ресурс, не освободит его. Обнаружив, что ресурс свободен, первый процессор устанавливает семафор в состояние "занят" и использует ресурс. Закончив работу с ресурсом, процессор сбрасывает его семафор и дает возможность другому процессу обратиться к тому же ресурсу.
Описанный алгоритм будет работать только в том случае, если операция чтения-модификации-записи семафора будет выполняться в непрерываемом режиме. В противном случае оба процесса могут, одновременно обратившись к семафору, увидеть, что ресурс свободен, и начать его совместное использование. Избежать такой ситуации и позволяет префикс lock в сочетании с командами типа btr или bts, выполняющими комплексные операции проверки и сброса или проверки и установки бита.
Будем считать, что семафор расположен в бите 0 байта по адресу sem, причем сброшенное состояние бита свидетельствует о занятости ресурса, а установленное состояние о том, что ресурс свободен. Тогда типичная процедура ожидания освобождения ресурса выглядит следующим образом:

mov SI,offset sem ;Адрес байта с семафором
getsem:
lock btr byte ptr [SI],1 ;Проверка и сброс бита 0
jnc getsem

Проверка состояния семафора и его модификация (запись в бит семафора 0, т.е. признака "занят") осуществляется в одной команде btr. На время выполнения этой команды шина многопроцессорной системы блокируется префиксом lock, и другой процессор обратиться к тому же семафору не может. Блокировка шины снимается уже после того, как семафор будет переведен в состояние занятости.
Если при обращении к байту sem оказывается, что в битс семафора записан 0, т.е. ресурс занят другим процессом, команда btr сбрасывает флаг CF (путем переноса в него содержимого анализируемого бита), что приводит к многократному повторению процедуры getsem, т.е. к циклу ожидания освобождения ресурса.
Типичная процедура освобождения занятого данным процессом ресурса выглядит следующим образом:

freesem:
lock bts byte ptr [SI],1 ;Проверка и установка бита 0

Собственно говоря, никакая проверка здесь не выполняется, однако процесс будет освобождать ресурс лишь если он этот ресурс использует, и проверять состояние флага в этой операции нет необходимости. Однако и здесь необходимо запирание шины на время записи в бит семафора 1, чтобы исключить одновременное обращение двух процессов к одной ячейке памяти.
386+ Префикс lock может быть использован только со следующими командами (и лишь при условии, что при их выполнении происходит обращение к памяти): adc, add, and, bt, bts, btr, btc, or, sbb, sub, xor, xchg, dec, inc, neg, not.

LODS Загрузка операнда из строки
LODSB Загрузка байта из строки
LODSW Загрузка слова из строки
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они загружают в регистр AL (в случае операций над байтами) или АХ (в случае операций над словами) содержимое ячейки памяти по адресу, находящемуся в паре регистров DS:SI. Команда lodsb загружает 1 байт, команда lodsw - 1 слово, а команда lods может быть использована для загрузки как байтов, так и слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db или dw). После операции загрузки регистр SI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2, в зависимости от размера загружаемого элемента. Команда не имеет параметров и не воздействует на флаги процессора.
Вариант команды lods имеет формат

lods строка

(что не избавляет от необходимости инициализировать регистры DS:SI адресом строки). В этом формате возможна замена сегмента строки строка:

lods ES:строка

Пример 1
;В полях данных сегмента данных, адресуемого через DS:
str db 'qwertyuiop'
; В программном сегменте:
сld ;Двигаемся по строке вперед
mov SI, off set str ;Адрес строки
add SI,BX ;Добавим смещение (пусть ВХ=4)
lodsb ;AL='t', SI -> 'у'

Пример 2
;В полях данных сегмента данных, адресуемого через ES:
str db 'qwertyuiop'
;В программном сегменте:
сld ;Двигаемся по строке вперед
mov SI,offset str ;Адрес строки
lodsbES:str ;AL='q', ES:SI -> 'w'

386+ LODSD Загрузка двойного слова из строки
Команда аналогична командам МП 86 lodb и lodsw, но позволяет загрузить из строки, адресуемой через регистры DS:ESI (DS:SI для 16-разрядных приложений), двойное слово в регистр ЕАХ.

Пример
; В полях данных
dat dd 12789,200000,550000,8000000
;В программном сегменте
mov SI,offset dat
add SI, 4*3 ;DS:SI -> 4-й элемент массива чисел
lodsd ;EAX=8000000

LOOP Циклическое выполнение, пока содержимое СХ не равно нулю
Команда loop выполняет декремент содержимого регистра СХ, и если оно не равно 0, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128... + 127 байт. Обычно метка помещается перед первым предложением тела цикла, а команда loop является последней командой цикла. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536 (если перед входом в цикл СХ=0). Команда не воздействует на флаги процессора.

Пример 1
;В полях данных:
array dw 4096 dup (?) ;Массив из 4096 слов
;В программном сегменте:
lea BX,array ;ВХ -> array
xor SI,SI ;SI=0
mov CX,4096 ;Счетчик повторений
mov AX,1 ;Число-заполнитель
array: mov [BX] [SI],AX ;Очистка элемента массива
inc SI ;Сдвиг к следующему
inc SI ;слову массива
loop array ;Повторить СХ раз

Пример 2
mov CX,20
delay :loop delay ;Небольшая задержка

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того чтобы в 16-разрядном приложении процессор при выполнении команды loop использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командой loop необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ,О
zzzz: db 67h ;Префикс замены размера адреса
loop zzzz ;Цикл из 232: шагов, реализующий
;программную задержку порядка минут

LOOPE/LOOPZ Циклическое выполнение,пока равно/циклическое выполнение, пока нуль
Оба обозначения представляют собой синонимы и относятся к одной команде. Команда выполняет декремент содержимого регистра СХ, и если оно не равно 0, и флаг ZF установлен, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128...+127 байтов. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536. Команда не воздействует на флаги процессора.

Пример
;В полях данных, адресуемых через DS:
command db 80 dup (' ')
;В программном сегменте:
. . . ;Копирование в поле command строки,
;содержимое которой следует анализировать
lea SI, command ;Настроим DS:SI
сld ;Обработка вперед
mov CX,80 ;Обрабатывать не более 80
;байтов
pass: lodsb ;Загрузим в AL очередной
;символ
сmр АL, ' ' ;Пропустим все пробелы в
loopepass ;начале строки
dec SI ;Сдвиг на 1 символ назад
;DS:SI -> первый символ, отличный от пробела

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того, чтобы в 16-разрядном приложении процессор при выполнении команд loope/loopz использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командами loope/loopz необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ, 1000000 ;Предельное число шагов
хххх: ... ;Тело цикла
db 67h
loopexxxx

LOOPNE/LOOPNZ Циклическое выполнение, пока не равно/циклическое выполнение, пока не нуль
Оба обозначения представляют собой синонимы и относятся к одной команде. Команда выполняет декремент содержимого регистра СХ, и если оно не равно 0, и флаг ZF сброшен, осуществляет переход на указанную метку вперед или назад в том же программном сегменте в диапазоне -128... + 127 байтов. Содержимое регистра СХ рассматривается как целое число без знака, поэтому максимальное число повторений группы включенных в цикл команд составляет 65536. Команда не воздействует на флаги процессора.

Пример
;В полях данных:
command db 80 dup (0)
;В программном сегменте:
. . . ;Копирование в поле command строки,
; содержимое которой следует анализировать
lea SI,command ;Настроим DS:SI
cld ;Обработка вперед
mov CX,80 ;Обрабатывать не более 80
;байтов
slash: lodsb ;Загрузим в AL очередной символ
cmp AL, ' / ' ;Ищем знак ' / '
loopne slash ;во всей строке
;DS:SI -> первый символ за знаком '/'

386+ При использовании в качестве счетчика расширенного регистра ЕСХ максимальное число шагов в цикле увеличивается до 232. Для того чтобы в 16-разрядном приложении процессор при выполнении команд loopne/loopnz использовал не 16-разрядный регистр СХ, а 32-разрядный регистр ЕСХ, перед командами loopne/loopnz необходимо указать префикс замены размера адреса 67h.

Пример
mov ЕСХ,1000000 ;Предельное число шагов
хххх: . . . ;Тело цикла
db 67h
loopne xxxx

386Р+ LSL Загрузка границы сегмента
Команда Isl загружает в первый операнд границу сегмента из дескриптора сегмента, заданного селектором во втором операнде.
В качестве первого операнда команды Isl можно использовать 16- или 32-разрядный регистр общего назначения; в качестве второго - 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти.

386Р+ LTR Загрузка регистра задачи TR
Команда Itr загружает регистр задачи TR селектором сегмента состояния задачи TSS из второго операнда, в качестве которого можно использовать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовое поле памяти. Команда используется в защищенном режиме, если программный комплекс выполнен в виде нескольких самостоятельных задач, и переключения между ними осуществляются с использованием включенных в процессор аппаратных средств поддержки многозадачности.

MOV Пересылка данных
Команда mov замещает первый операнд (приемник) вторым (источником). При этом исходное значение первого операнда теряется. Второй операнд не изменяется. В зависимости от описания операндов, пересылается слово или байт. Если операнды описаны по-разному или режим адресации не позволяет однозначно определить размер операнда, для уточнения размера передаваемых данных в команду следует включить один из атрибутных операторов byte ptr или word ptr. Команда не воздействует на флаги процессора. В зависимости от используемых режимов адресации, команда mov может осуществлять пересылки следующих видов:
- из регистра общего назначения в регистр общего назначения;
- из регистра общего назначения в ячейку памяти;
- из регистра общего назначения в сегментные регистры DS, ES и SS;
- из ячейки памяти в регистр общего назначения;
- из ячейки памяти в сегментный регистр;
- из сегментного регистра в регистр общего назначения;
- из сегментного регистра в ячейку памяти;
- непосредственный операнд в регистр общего назначения;
- непосредственный операнд в ячейку памяти.
Запрещены пересылки из ячейки памяти в ячейку памяти (для этого предусмотрена команда movs), а также загрузка сегментного регистра непосредственным значением, которое, таким образом, приходится загружать через регистр общего назначения:

mov AX,seg mem ;Сегментный адрес ячейки mem
mov DS,AX ;Загрузка его в регистр DS

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

push DS
pop ES ; DS копируется в ES

Примеры
;В полях данных:
memb db 5,6
memd dd 0 ;Двухсловная ячейка
;В программном сегменте:
mov DX,AX ;Из регистра в регистр
mov AL,memb ;Из памяти в регистр
mov AX,0B800h ;Непосредственное значение в
;регистр
mov ES,AX ;Из регистра в сегментный
;регистр
mov word ptr memd+2,ES ;Из сегментного
;регистра в память
mov word ptr memd, 2000;Непосредственное
;значение в память
mov BX,word ptr memb ;Слово из памяти в
;регистр (число 0605)
mov DI,word ptr memd ;Слово из памяти в
;регистр
mov ES,word ptr memd+2;Слово из памяти в
;сегментный регистр

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример 1
mov EAX,ESI

Пример 2
; В полях данных
mem dd 0
;В программном сегменте
mov mem,EBP

386Р+ MOV Пересылка в\из специальных регистров
Этот вариант команды mov (с той же мнемоникой, но другими кодами операций) используется в защищенном режиме и предназначен для обмена данными со специальными регистрами процессора: управляющими CRO...CR3, тестирования TR6 и TR7, а также регистрами отладки DRO...DR7. Один из операндов команды mov должен быть 32-разрядным регистром общего назначения, другим - один из специальных регистров процессора.

MOVS Пересылка данных из строки в строку
MOVSB Пересылка байта данных из строки в строку
MOVSW Пересылка слова данных из строки в строку
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они пересылают по одному элементу строки, который может быть байтом или словом. Первый операнд (приемник) адресуется через ES:DI, второй (источник) - через DS:SI. Операцию пересылки можно условно изобразить следующим образом:

(DS:SI) -> (ES:DI)

После каждой операции пересылки регистры SI и DI получают положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера пересылаемых элементов. Вариант команды movs имеет формат:

movs строка_1, строка_2

В этом случае байты или слова из строки строка_2 пересылаются на место строки строка_]. Размер пересылаемых элементов определяется описанием строк (с помощью директив db или dw). Это не избавляет от необходимости инициализировать регистры ES:DI и DS:SI адресами строк строка _1 и строка_2. В этом формате возможна замена сегмента второй строки (источника):

movs строка_1, ES:строка_2

Рассматриваемые команды могут предваряться префиксом повторения rep (повторять СХ раз). После выполнения рассматриваемых команд регистры SI и DI указывают на ячейки памяти, находящиеся за теми (если DF=0) или перед теми (если DF=1) элементами строк, на которых закончились операции пересылки. Если флаг DF сброшен, то пары регистров DS:SI и ES:DI следует инициализировать начальными адресами строк-операндов; строка-источник будет пересылаться от се начала, в порядке возрастания номеров ее байтов. Если флаг DF установлен, то пары регистров DS:SI и ES:DI следует инициализировать конечными адресами строк-операндов; строка-источник будет пересылаться от ее конца, в порядке уменьшения номеров ее байтов. Команды не воздействует на флаги процессора.

Пример 1
;В полях данных основного сегмента данных,
;адресуемого через DS:
txt db 'Урок 1' ;Пересылаемая строка
txt_len equ S-txt ;Ee длина
;В 'полях данных дополнительного сегмента данных,
;адресуемого через ES :
string db 80 dup (' ')
;В программном сегменте:
lea SI,txt ;DS:SI -> txt
lea DI,string+10.;ES:DI -> string+10
сld ;Движение по строке вперед
mov CX,txt_len ;Столько байтов переслать
rep movsb ;Пересылка

Пример 2
;В полях данных сегмента данных, адресуемого через DS:
txt db 'А',84h, 'В',84h, 'A',84h, 'P',
db 84h,'И',84h,'Я',84h,'!',84h
txt_len=$-txt ;B программном сегменте:
mov AX,0B800h ;Сегментный адрес видеобуфера
mov ES,AX ;Инициализируем ES
;Выведем на экран текст
mov DI,1672 ;Смещение к середине экрана
lea SI,txt ;DS:SI ® txt
сld ;Движение по строке вперед
mov CX,txt_len/2 ;Столько слов переслать
rep movsw ;Пересылка в середину экрана
;красной мерцающей (атрибут
;84h) надписи 'АВАРИЯ!'

Пример 3
;В полях данных сегмента данных, адресуемого через DS:
datal dw 10000 dup(') ;Массив произвольных данных
data2 dw 5000 dup(') ;Массив-приемник
;В программном сегменте
push DS ;Настроим
pop ES ;ES на тот же сегмент данных
mov SI,offset datal;SI -> datal
add SI,5000 ;Сместим SI к середине
;массива
mov DI,offset data2;DI -> data2
mov CX,2500 ;Размер половины массива (в
;словах)
сld ;Движение вперед
rep movsw ;Перешлем вторую половину
;массива datal на место data2

Пример 4
;В полях данных сегмента, адресуемого через DS
file db 'MYFILE.01.DAT1,0 ;Строка-источник
fname db 128 dup(?) ;Строка-приемник
;В программном сегменте
push DS
pop ES ;Теперь ES=DS
mov SI,offset file;DS:SI -> strl
mov SI,128 ;Максимальная длина имени
;файла
сld ;Движение по строке вперед
null: lodsb ;Загрузим в AL очередной
; символ
cmp AL, 0 ;Ищем 0 в конце имени файла
loopne null
;DS:SI ® Первый символ за концом имени файла (за
;завершающим нулем)
dec SI ;SI -> байт с 0 std ;Движение по строке назад
mov ВХ,128 ;Из начального значения СХ
sub BX,CX ;вычтем то, что в СХ осталось
mov СХ,ВХ ;СХ=число символов в имени (с 0)
dec ВХ ;Смещение к 0 от начала имени файла
lea DI,fname[ВХ] ;Смещение завершающего 0
rep movsb ;Перешлем все имя (от конца к началу)

386+ MOVSD Пересылка двойного слова из строки в строку
Команда аналогична командам МП 86 movsb и movsw, но позволяет скопировать двойное слово из строки, адресуемой через регистры DS:ESI, в строку, адресуемую через регистры ES:EDI.

Пример 1
;В полях данных сегмента, адресуемого через DS
strl db '01234567890ABCDEF' ;Строка-источник
str2 db 16 dup(?) ;Строка-приемник
;B программном сегменте
push DS
pop ES ;Теперь ES=DS
mov SI,offset strl ;DS:SI ®strl
mov DI,offset str2 ;ES:DI -> str2
сld ;Движение по строке вперед
mov CX,4 ;Коэффициент повторения
rep movsd ;Копирование по 4*4 байт

386+ MOVSX Пересылка с расширением знака
Команда пересылает байт в слово или двойное слово, а также слово в двойное слово с расширением знака. В качестве первого операнда (приемника) может использоваться 16- или 32-разрядный регистр общего назначения, в качестве второго - 8- или 16-разрядный регистр общего назначения или ячейка памяти такого же размера. Недопустима пересылка из памяти в память, в или из сегментного регистра, а также непосредственного значения. Фактически команда movsx увеличивает размер как положительного, так и отрицательного числа, ни изменяя ни его значения, ни знака.

Пример 1
mov CL,-5 ;CL=FBh
movsxAX,CL ;AX=FFFBh

Пример 2
mov CL,+5 ;CL=05h
movsxAX,CL ;AX=0005h

Пример 3
mov BL,-128 ;BL=80h
movsxECX,BL ;ECX=FFFFFF80h

Пример 4
; В полях данных
mem dw -3 ;mem=FFFDh
;В программном сегменте
movsxEB-X,mem ; EBX=FFFFFFFDh

386+ MOVZX Пересылка с расширением нуля
Команда пересылает байт в слово или двойное слово, а также слово в двойное слово с заполнением старших разрядов нулями. В качестве первого операнда (приемника) может использоваться 16- или 32-разрядный регистр общего назначения, в качестве второго - 8- или 16-разрядный регистр общего назначения или ячейка памяти такого же размера. Недопустима пересылка из памяти в память, в или из сегментного регистра, а также непосредственного значения. Фактически команда movzx увеличивает размер числа, считая его числом без знака.

Пример 1
mov CL,5 ;CL=05h
movsxAX,CL ;AX=0005h

Пример 2
mov CL,-5 ;CL=FBh
movsxAX,CL ;AX=00FBh

Пример 3
mov BL,80h ;BL=80h
movsxECX,BL ;ECX=00000080h

Пример 4
;B полях данных
mem dw 0FFFFh ;mem=FFFFh
;B программном сегменте
movsxEBX,mem ;EBX=0000FFFFh

MUL Умножение целых чисел без знака
Команда inul выполняет умножение целого числа без знака, находящегося в регистре AL (в случае умножения на байт) или АХ (в случае умножения на слово), на операнд-источник (целое число без знака). Размер произведения в два раза больше размера сомножителей.
Для однобайтовых операций один из сомножителей помещается в регистр AL; после выполнения операции произведение записывается в регистр АХ.
Для двухбайтовых операций один из сомножителей помещается в регистр АХ; после выполнения операции произведение записывается в регистры DX:AX (в DX - старшая часть, в АХ - младшая). Предыдущее содержимое регистра DX затирается.
Если содержимое регистра АН после однобайтового умножения или содержимое регистра DX после двухбайтового умножения не равны 0, флаги CF и OF устанавливаются в 1. В противном случае оба флага сбрасываются в 0.
В качестве операнда-сомножителя команды mul можно указывать регистр (кроме сегментного) или ячейку памяти; не допускается умножение на непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AL,5 ;Первый сомножитель
mov BL,3 ;Второй сомножитель
mul BL ;AX=000Fh, произведение

Пример 2
mov AX,256 ;Первый сомножитель
mov BX,256 ;Второй сомножитель
mul BX ;DX=0001h, AX=0000h
;(DX:AX=65536, произведение)

Пример 3
;В полях данных
coef db 100 ;Первый сомножитель
datal db 126 ;Второй сомножитель
mov AL,datal ;AL=7Eh=126
mul coef ;AX=3138h=12600,произведение

Пример 4
;B полях данных
coef dw 5000 ;Первый сомножитель
datal dw 1200 ;Второй сомножитель
mov AX,datal ;AX=4BOh=1200
mul coef ;DX=005Bh, AX=8D80h
;Произведение=ВХ:AX=
;5B8D80h=6000000

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров. При этом, если указанный операнд представляет собой 32-байтовую величину, то результат размещается в регистрах EDX:EAX.

Пример 1
mov EAX,200h ;Первый сомножитель
mov ESI,l000000lh ;Второй сомножитель
mul ESI ;Произведение в EDX:EAX
;EDX=00000020h,EAX=00000200h

Пример 2
; В полях данных
nmbs db 15,22,36,78,84,98,100
;В программном сегменте
mov EDX,offset nmbs ;Относительный адрес
movzx EDX,DX ;массива
mov ECX,5 ;Смещение в массиве
mov AL,10 ;Множитель
mul byte ptr [EDX] [ЕСХ] ;Умножаем элемент
;массива с индексом 5 (98)
;на AL (10) Результат в
;АХ=980

NEG Изменение знака, дополнение до 2
Команда neg выполняет вычитание целочисленного операнда со знаком из нуля, превращая положительное число в отрицательное и наоборот. Исходный операнд затирается. В качестве операнда можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov АХ,О О 01
neg AX ;AX=FFFFh=-l

Пример 2
mov BX,-2 ;BX=FFFEh=-2
neg BX ;BX=0002h

Пример 3
;В полях данных
nmb dw 800lh ;Если число со знаком,
;то -32767
;В программном сегменте
neg nmb ;nmb=7FFFh=+32767

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov ECX, 5
neg ECX ;ECX=FFFFFFFBh=-5

NOP Холостая команда
По команде пор процессор не выполняет никаких действий, кроме увеличения на 1 содержимого указателя команд IP (поскольку команда пор занимает 1 байт). Команда иногда используется в отладочных целях, чтобы "забить" какие-то ненужные команды, не изменяя длину загрузочного модуля или, наоборот, оставить место в загрузочном модуле для последующей вставки команд. В ряде случаев команды пор включаются в текст объектного модуля транслятором. Команда не имеет ни параметров, ни операндов и не воздействует на флаги процессора.

NOT Инверсия, дополнение до 1, логическое отрицание
Команда not выполняет инверсию битов указанного операнда, заменяя 0 на 1 и наоборот. В качестве операнда можно указывать регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда не воздействует на флаги процессора.

Правила побитовой инверсии:
Операнд-бит 0 1
Бит результата 1 0

Пример 1
mov AX,0FFFFh
not AX ;AX=0000h

Пример 2
mov SI,5551h
not SI ;SI=AAAEh

386+ Допустимо использование 32-битовых операндов и дополнитель ных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,0C00SFF00h
not EAX ;EAX=3FFC00FFh

OR Логическое ВКЛЮЧАЮЩЕЕ ИЛИ
Команда or выполняет операцию логического (побитового) сложения двух операндов. Результат замещает первый операнд (приемник); второй операнд (источник) не изменяется. В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды команды or могут быть байтами или словами. Команда воздействует на флаги OF, SF, ZF, PF и CF, при этом флаги CF и OF всегда сбрасываются в 0.

Правила побитового сложения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0111

Пример 1
mov AX,000Fh
mov BX,00F0h
or AX,BX ;AX=00FFh, BX=00F0h

Пример 2
mov AX,000Fh
mov BX,00F7h
or AX,BX ;AX=00FFh, BX=00F7h

Пример 3
mov AX,000Fh
or AX,8001h ;AX=800Fh

Пример 4
; В полях данных
mask db 80h
;B программном сегменте
mov CH,17h
or CH,mask ;CH=97h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных
mem dd 80000000h
; В программном сегменте
or mem,0C0h ;mem=800000C0h

OUT Вывод в порт
Команда out выводит в порт, указываемый первым операндом, байт или слово соответственно из регистра AL или АХ. Адрес порта помещается в регистр DX. Если адрес порта не превышает 255, он может быть указан непосредственным значением. Указание регистра-источника (AL или АХ) обязательно, хотя с другими регистрами команда out не работает, и их указывать нельзя. Команда не воздействует на флаги процессора.

Пример 1
mov AL,20h ;Команда конца прерывания (EOI)
out 20h,AL ;Вывод команды EOI в порт
;20h контроллера прерываний

Пример 2
mov DX,3CEh ; Адрес порта
mov AL,5 ;Данное
out DX,AL ;Вывод байта из AL в порт 3CEh

386+ Допустимо использование в качестве операнда-источника расширенного регистра ЕАХ (если адресуемое устройство позволяет записать в его порт двойное слово).

Пример
mov ЕАХ,1А008РРЗh;Пересылаемое данное
mov DX,345h ;Адрес порта
out DX,EAX ;Вывод в порт двойного слова

386+
OUTS Вывод строки в порт
OUTSB Вывод байта в порт
OUTSW Вывод слова в порт
OUTSD Вывод двойного слова в порт
Команды предназначены для вывода данных в порт непосредственно из памяти. Адрес порта указывается, как и для команды out, в регистре DX, при этом задание адреса порта непосредственным значением не допускается. Данные извлекаются из памяти по адресу, находящемуся в паре регистров DS:ESI. Замена сегмента не допускается. Команда outsb передает в порт 1 байт, команда outsw - 1 слово, команда outsd - 1 двойное слово, а команда outs может быть использована для передачи байтов, слов и двойных слов. В последнем случае размер загружаемого данного определяется описанием строки (с помощью директив db, dw или dd). После передачи данных регистр ESI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1, 2 или 4 в зависимости от размера передаваемых данных.
Вариант команды outs имеет формат

outs DX, строка

(что не избавляет от необходимости инициализировать регистры DS:ESI адресом строки).
Если устройство, адресуемое через порт, может принимать последовательность данных, то команды outs можно предварить префиксом повторения rep. В этом случае в порт пересылается СХ элементов данных заданного размера.
Команды outs не воздействуют на флаги процессора.

Пример 1
; В полях данных
mem dw 0FFh
;В программном сегменте
mov SI, offset mem;ES:DI ® mem
mov DX,303h ;Адрес порта
outsb ;Вывод в порт 8-битового
;данного

Пример 2
; В полях данных
string dw 0FFh,1,5,0Bh, 0
; В программном сегменте
mov SI, off set mem;ES:DI -> mem
mov DX,340h Адрес порта
mov CX, 5 ;Число данных
eld ;Движение по данным вперед
rep outsb ;Последовательный вывод в
;порт пяти 8-битовых данных

POP Извлечение слова из стека
Команда pop выталкивает 16-битовое данное из стека, т.е. пересылает слово из вершины стека (на которую указывает регистр SP) по адресу операнда-приемника. После этого содержимое SP увеличивается на 2, и SP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Выталкивать из стека можно только целые слова (не байты). Программа должна строго следить за тем, чтобы каждой команде проталкивания в стек push отвечала обратная команда выталкивания из стека pop. Если стек используется для временного хранения некоторых данных, то извлекать эти данные из стека следует в порядке, обратном их сохранению.
В качестве операнда-приемника можно использовать любой 16-разрядный регистр (кроме CS) или ячейку памяти. Команда не воздействует на флаги процессора.
Пара команд push - pop часто используется для пересылки данного из регистра в регистр (особенно, в сегментный) через стек.

Пример 1
push AX ;Временное сохранение
push BX ;в стеке
push DS ;трех операндов
pop DS ;Восстановление из стека
pop BX ;трех операндов
pop AX ; в обратном порядке

Пример 2
push CS ;Пересылка CS через стек pop DS ;Теперь DS=CS

Пример 3
;В полях данных
mem dw 0
; В программном сегменте
pop mem ;Восстановление из стека в память

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
pop EAX ;Извлечение из стека двойного слова

386+ РОРА Восстановление из стека всех регистров
Команда рора восстанавливает из стека содержимое всех регистров, предварительно сохраненных в стеке командой pusha. Заполнение из стека регистров осуществляется в следующем порядке: DI, SI, BP, SP, ВХ, DX, СХ, АХ. Исходное содержимое указателя стека SP, сохраненное в стеке командой pusha, командой рора из стека извлекается, но отбрасывается. Команда не имеет параметров.

Пример
рора

386+ POPAD Восстановление из стека всех регистров в 32-разрядном режиме
Команда popad восстанавливает из стека содержимое всех расширенных регистров, предварительно сохраненных в стеке командой pushad. Заполнение из стека регистров осуществляется в следующем порядке: EDI, ESI, EBP, ESP, ЕВХ, EDX, ЕСХ, ЕАХ. Исходное содержимое указателя стека ESP, сохраненное в стеке командой pusha, командой рора из стека извлекается, но отбрасывается. Команда не имеет параметров.

Пример
popad

POPF Восстановление из стека регистра флагов
Команда popf пересылает верхнее слово стека (на которое указывает регистр SP) в регистр флагов FLAGS. После этого содержимое SP увеличивается на 2, и SP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Команда popf не имеет параметров; она воздействует на все флаги процессора, кроме флагов VM и RF.

Пример 1
popf ;Регистр флагов загружается из стека

Пример 2
pushf ;Отправим флаги в стек
mov BP,SP ;Настроим ВР на флаги в стеке
or [BP],100h ;Установим бит 100h (флаг TF)
popf ;Вытолкнем в регистр флагов.
;Теперь в регистре флагов TF=1

386+ POPFD
Восстановление из стека расширенного регистра флагов
Команда popfd пересылает верхнее слово стека (на которое указывает регистр ESP) в расширенный регистр флагов EFLAGS. После этого содержимое ESP увеличивается на 4, и ESP указывает на предыдущее слово стека, которое теперь является его новой вершиной. Команда popfd не имеет параметров; она воздействует на все флаги процессора.

Пример
popfd ;Регистр EFLGS загружается из стека

PUSH Занесение операнда в стек
Команда push уменьшает на 2 содержимое указателя стека SP и заносит на эту новую вершину двухбайтовый операнд-источник (проталкивает в стек новое данное). Проталкивать в стек можно только целые слова (не байты). Программа должна строго следить за тем, чтобы каждой команде проталкивания в стек push отвечала обратная команда выталкивания из стека pop. Если стек используется для временного хранения некоторых данных, то извлекать эти данные из стека следует в порядке, обратном их сохранению.
В качестве операнда-источника может использоваться любой 16-разрядный регистр (включая сегментный) или ячейка памяти. Не допускается занесение в стек непосредственного значения, хотя некоторые трансляторы преобразуют команду вида

push 1234h

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

Пример 1
push ES:mem ;Сохранение содержимого
push DS ;слова памяти mem из
push BP ;дополнительного сегмента
;а также регистров DS и ВР
… ;
pop PP ;Восстановление из стека
pop DS ;трех операндов
pop ES: mem ;в обратном порядке

Пример 2
push DS ;Пересылка DS через стек
pop ES ;Теперь ES=DS

386+ Допустима засылка в стек 32-битовых операндов (регистров и ячеек памяти), а также занесение в стек 8-, 16- и 32-битовых непосредственных значений. Каждое 8-битовое значение занимает в стеке целое слово. Операнды любого допустимого размера могут заноситься з стек вперемежку', если это не вступает в противоречие с операциями по извлечению этих данных из стека.

Пример 1
push AX ; Сохранение в стеке регистра АХ
push 32h ;Сохранение в стеке
;константы 32h (она займет в
;стеке 1 слово)
push EAX ;Сохранение в стеке регистра
;ЕАХ (два слова стека)

386+ PUSHA Сохранение в стеке всех регистров
Команда pusha сохраняет в стеке содержимое всех регистров в следующем порядке: АХ, СХ, DX, ВХ, значение указателя стека SP перед выполнением данной команды, дачее ВР, SI и DI. Команда не имеет параметров и не воздействует на флаги процессора.

Пример
pusha

386+ PUSHAD Сохранение в стеке всех регистров в 32-разрядном режиме
Команда pushad сохраняет в стеке содержимое всех регистров в следующем порядке: EAX, ECX, EDX, ЕВХ, значение указателя стека ESP перед выполнением данной команды, далее EBP, ESI и EDI. Команда не имеет параметров и не воздействует на флаги процессора.

Пример
pushad

386+ PUSHFD Занесение в стек содержимого расширенного регистра флагов
Команда pushfd уменьшает на 4 содержимое указателя стека ESP и заносит на эту новую вершину содержимое расширенного регистра флагов EFALGS. При этом сохраняются все флаги процессора. Команда pushfd не имеет параметров и не воздействует на флаги процессора.

Пример
pushfd ;Содержимое регистра флагов
;сохраняется в стеке

RCL Циклический сдвиг влево через бит переноса
Команда гсl осуществляет сдвиг влево всех битов операнда. Если команда записана в формате

rcl операнд,1

сдвиг осуществляется на 1 бит. В младший бит операнда заносится значение флага CF; старший бит операнда загружается в CF. Если команда записана в формате

rcl операнд,CL

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

Рис. П4. Действие команды rcl.

В качестве операнда команды rcl можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
clc ;Сбросим CF
mov AX, 7
rcl АХ,1 ' ;AX=000Eh=14, CF=0

Пример 2
stc ;Установим CF
mov DL,7
rcl DL,1 ;DL=0Fh=15, CF=0

Пример 3
clc ;Сбросим CF
mov BX,0FFFFh
rcl BX,1 ' ;BX=FFFEh, CF=1

Пример 4
clc ;Сбросим CF
mov DH,3
mov CL,4 ;Счетчик сдвигов
rcl DH,CL ;DH=30h=48, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа битов сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EAX,0С0000003h
clc ;Сбросим CF
rcl EAX,2 ;EAX=0000000Dh, CF=1

RCR Циклический сдвиг вправо через бит переноса
Команда rсl осуществляет сдвиг вправо всех битов операнда. Если команда записана в формате

rcl операнд,1

сдвиг осуществляется на 1 бит. В старший бит операнда заносится значение флага CF; младший бит операнда загружается в CF. Если команда записана в формате

rcl операнд,CL

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

Рис. П.5. Действие команды rcl.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
clc ;Сбросим флаг CF
mov AX, 2
rcr АХ,1 ;АХ=1, CF=0

Пример 2
stc ;Установим флаг CF
mov DL,8
rcr DL,1 ;DL=84h, CF=0

Пример 3
clc ;Сбросим флаг CF
mov BX,OFh
rcr BX,1 ;BX=7, CF=1

Пример 4
clc ;Сбросим флаг CF
mov DH,80h
mov CL,5 ;Счетчик сдвигов
rcr DH,CL ;DH=4, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov ESI,0FFFF000lh
clc ;Сбросим флаг CF
rcr ESI,8 ;ESI=02FFFF00h, CF=0

Pentium+P RDMSR Чтение особого регистра модели
Команда читает содержимое внутреннего регистра, специфического для конкретной модели процессора.

REP Повторение
REPE Повторение пока равно
REPZ Повторение пока нуль
REPNE Повторение пока равно
REPNZ Повторение пока не равно
Префиксы повторения, позволяющие организовывать циклическое выполнение команд обработки строк cmps, movs и seas, и при этом проверять наличие указанного в префиксе условия.
Префикс rep, будучи установлен перед строковой командой movs или stos, заставляет ее выполняться СХ раз.
Префикс rере (и полностью эквивалентный ему префикс repz), будучи установлен перед строковой командой cmps или seas, заставляет ее выполняться до тех пор, пока результат выполнения равен 0 и, соответственно, ZF=1, но не более СХ раз.
Префикс rерnе (и полностью эквивалентный ему префикс repnz), будучи установлен перед строковой командой cmps или seas, заставляет ее выполняться до тех пор, пока результат выполнения не равен 0 и, соответственно, ZF=0, но не более СХ раз.
Примеры использования префиксов повторения см. в описаниях строковых команд cmps, movs и seas.

RET Возврат из процедуры
RETN Возврат из ближней процедуры
RETF Возврат из дальней процедуры
Команда ret извлекает из стека адрес возврата и передает управление назад в программу, первоначально вызвавшую процедуру. Если командой ret завершается ближняя процедура, объявленная с атрибутом near, или используется модификация команды retn, со стека снимается одно слово- относительный адрес точки возврата. Передача управления в этом случае осуществляется в пределах одного программного сегмента. Если командой ret завершается дальняя процедура, объявленная с атрибутом far, или используется модификация команды retf, со стека снимаются два слова: смещение и сегментный адрес точки возврата. В этом случае передача управления может быть межсегментной.
В команду ret может быть включен необязательный операнд (кратный 2), который указывает, на сколько байтов дополнительно смещается указатель стека после возврата в вызывающую программу. Прибавляя эту константу к новому значению SP, команда ret обходит аргументы, помещенные в стек вызывающей программой (для передачи процедуре) перед выполнением команды call. Обе разновидности команды не воздействуют на флаги процессора.

Пример 1
…
call subr ;Вызов подпрограммы
subr proc near
. . . ;Тело процедуры-подпрограммы
ret subr endp

Пример 2
push AX ;Параметр 1, передаваемый в
;подпрограмму
push SI ;Параметр 2, передаваемый в
;подпрограмму
call subr ;Вызов подпрограммы
…
subr proc near
…
;Извлечение из стека параметров
; (без изменения содержимого SP)
ret 4 ;Возврат в вызывающую
;программу и снятие со стека
;двух слов с параметрами
subr endp

ROL Циклический сдвиг влево
Команда rol осуществляет сдвиг влево всех битов операнда. Если команда записана в формате

rol операнд,1

сдвиг осуществляется на 1 бит. Старший бит операнда загружается в его младший разряд и одновременно заносится в флаг CF (рис. П6). Если команда записана в формате

rol операнд,CL

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

Рис. П6. Действие команды rol.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AX,1
rol AX,1 ;AX=0002h, CF=0

Пример 2
mov DL,8 Oh
rol DL,1 ;DL=01h, CF=1

Пример 3
mov DX,3000h
mov CL,4
rol DX,CL ;DX=0003h, CF=1

Пример 4
mov DX,2000h
mov CL,4
rol DX,CL ;DX=0002h, CF=0
386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov ЕАХ,012345678h
rol EАХ,16 ;EAX=56781234h

ROR Циклический сдвиг вправо
Команда ROR осуществляет циклический сдвиг вправо всех битов операнда. Если команда записана в формате

ror операнд,1

сдвиг осуществляется на 1 бит. Младший бит операнда записывается в его старший разряд и одновременно поступает в флаг CF (рис.Ш). Если команда записана в формате

ror операнд,CL

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

Рис. П7. Действие команды ror.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги OF и CF.

Пример 1
mov AX,2
ror AX,1 ;AX=0001h, CF=0

Пример 2
mov DL,81h
ror DL,1 ;DL=C0h, CF=1

Пример 3
mov BX,000Eh
mov CL,4
ror BX,CL ;BX=E000h, CF=1

Пример 4
mov BX,0009h
mov CL,4
ror BX,CL ;BX=9000h, CF=1

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EDI,90000001h
ror EDI,12 ;EDI=00190000h

SAHF Запись содержимого регистра АН в регистр флагов
Команда sahf копирует разряды 7, 6, 4, 2 и 0 регистра АН в регистр флагов процессора, устанавливая тем самым значения флагов SF, ZF, AF, PF и CF соответственно. Команда не имеет операндов.
Команда sahf (совместно с командой lahf) дает возможность читать и изменять значения флагов процессора, в том числе флагов SF, ZF, AF и PF, которые нельзя изменить непосредственно. Однако следует иметь в виду, что команда sahf заполняет только младший байт регистра флагов. Поэтому нельзя изменить с ее помощью, например, состояние флага OF.

Пример 1
lahf ;Регистр АН отображает
;состояние регистра флагов
or АН,80h ;Установка бита 7 = SF
sahf ;Загрузка АН в регистр
;флагов, где теперь будет SF = 1

Пример 2
lahf ;Регистр АН отображает
;состояние регистра флагов
and АН,0BFh ;Сброс бита 6 = ZF
sahf ;Загрузка АН в регистр
;флагов, где теперь будет ZF = О

Пример 3
mov АН, 5
sahf ;Устанавливаются флаги PF и
;CF и сбрасывается флаги SF,
;ZF и AF

SAL Арифметический сдвиг влево
Команда sal осуществляет сдвиг влево всех битов операнда. Старший бит операнда поступает в флаг CF. Если команда записана в формате

sal операнд, 1

сдвиг осуществляется на 1 бит. В младший бит операнда загружается 0. Если команда записана в формате

sal операнд,CL

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

Рис. П8. Действие команды sal.

В качестве операнда команды sal можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение.
Каждый сдвиг влево эквивалентен умножению знакового числа на 2, поэтому команду sal удобно использовать для возведения операнда в степень 2.
Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AL,7
sal AL,1 ;AL= 0Eh=7*2, CF=0

Пример 2
mov AX,IFh
mov CL,8
sal AX,CL ;AX=lF00h=lFFh*256, CF=0

Пример 3
mov SI,-1 ;SI=FFFFh
mov CL,4
sal SI,CL ;SI=FFF0h=-l*16=-16, CF=1

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EBX,0000C835h
sal EBX,5 ;EBX=001906A0h

SAR Арифметический сдвиг вправо
Команда sar осуществляет сдвиг вправо всех битов операнда. Младший бит операнда поступает в флаг CF. Если команда записана в формате

sar операнд,1

сдвиг осуществляется на 1 бит. Старший бит операнда сохраняет свое значение. Если команда записана в формате

sar операнд,CL

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

Рис. П9. Действие команды sar.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение.
Каждый сдвиг вправо эквивалентен делению знакового числа на 2, поэтому команду sar удобно использовать для деления операнда на целые степени 2. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AL,7
sar AL,1 ;AL=3=7/2, CF=1. Остаток
;потерян

Пример 2
mov AX,lFF0h
mov CL,4
sar AX,CL ;AX=01FFh=lFF0h/16, CF=0

Пример 3
mov BX,-8 ;BX=FFF8h
mov CL,2
sar BX,CL ;BX=FFFEh=-2=-8/4, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример
mov EAX,0F0001234h
sar EAX,8 EAX=FFF00012h

SBB Целочисленное вычитание с займом
Команда sbb вычитает второй операнд (источник) из первого (приемника). Результат замещает первый операнд, предыдущее значение которого теряется. Если установлен флаг CF, из результата вычитается еще 1. Таким образом, если команду вычитания записать в общем виде

sbb операнд__1, операнд_2

то ее действие можно условно изобразить следующим образом:
операнд_1 - операнд_2 - CF -> операнд_1

В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда sbb обычно используется для вычитания 32-разрядных чисел. Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AX,76A5h
sbb AX,76A3h ;AX=1, если CF был = 1
;AX=2, если CF был = 0

Пример 2
; В полях данных:
numlow dw 000Ah ;Младшая часть вычитаемого
numhigh dw 0001h ;Старшая часть вычитаемого
;Число 1000Ah=65546
;В программном сегменте:
mov AX, 0 ;Младшая часть уменьшаемого
mov DX,0002 ;Старшая часть уменьшаемого
;Число 20000h=131072
sub AX, numlow ;Вычитание младших частей.
;AX=FFF6h, CF=1
sbb DX,numhigh ;Вычитание старших частей с
;займом.
;DX:AX=0000:FFF6h=65526

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
;В полях данных:
nlow dd 0Ch ;Младшая часть вычитаемого
nhi dd 40000000h ;Старшая часть вычитаемого
;Число 400000000000000Ch ;В программном сегменте:
mov EAX,0Bh ;Младшая часть уменьшаемого
mov EBX,60000000h ;Старшая часть уменьшаемого
;Число 600000000000000Bh
sub EAX,nlow ;Вычитание младших частей.
;EAX=FFFFFFFFh, CF=1
sbb EBX,nhi ;Вычитание старших частей с
;займом. EBX=lFFFFFFFh
;Разность IFFFFFFFFFFFFFFFh

SCAS Сканирование строки с целью сравнения
SCASB Сканирование строки байтов с целью сравнения
SCASW Сканирование строки слов с целью сравнения
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они сравнивают содержимое регистра AL (в случае операций над байтами) или АХ (в случае операций над словами) с содержимым ячейки памяти по адресу, находящемуся в паре регистров ES:DI. Операция сравнения осуществляется путем вычитания содержимого ячейки памяти из содержимого AL или АХ. Результат операции воздействует на регистр флагов, но не изменяет ни один из операндов. Таким образом, операцию сравнения можно условно изобразить следующим образом:

АХ или AL - (ES:DI) ® флаги процессора

После каждой операции сравнения регистр DI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2, в зависимости от размера сравниваемых элементов.
Вариант команды SCAS имеет формат

sсas строка

(что не избавляет от необходимости инициализировать регистры ES:DI адресом строки строка). Замена сегмента строки невозможна.
Рассматриваемые команды могут предваряться префиксами повторения repe/repz (повторять, пока элементы равны, т.е. до первого неравенства) и repne/repiiz (повторять, пока элементы не равны, т.е. до первого равенства). В любом случае выполняется не более СХ операций над последовательными элементами.
После выполнения рассматриваемых команд регистр DI указывает на ячейку памяти, находящуюся за тем (если DF=0) или перед тем (если DF=1) элементом строки, на котором закончились операции сравнения. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
;В полях данных сегмента данных, адресуемого через ES:
string db ' /Т:4'
;В программном сегменте:
сld ;Поиск вперед по строке
lea DI,string ;ES:DI ® string
mov AL, ' ' ;Символ, который мы пропускаем
mov CX,8 ;Длина строки repe scasb
;Поиск первого символа,
;отличного от пробела
je blank ;Символ не найден - одни пробелы
gotit: ;Продолжение, если символ найден

В примере 1 в строке имеются символы, отличные от искомого (кода пробела), и команда je выполнена не будет. После завершения сканирования управление будет передано на метку gotit. Содержимое регистров в этой точке: СХ=3 (так как не выполнено сканирование 3 символов), DI = <смещение string> + 5 (выполнено сканирование 5 символов). ES:D1 указывают на символ строки, следующий за отличным от пробела (в данном случае символ "Т").

Пример 2
; В полях данных сегмента данных, адресуемого через ES:
string db ' /Т:4'
; В программном сегменте:
cld ;Поиск вперед по строке
lea DI,string ;ES:DI ® string
mov AL, '/' ;Искомый символ
mov CX,8 ;Длина строки
repne scasb ;Поиск символа / в строке
jne blank ;Искомого символа нет
g'otit: ;Искомый символ найден

В примере 2 в строке имеется искомый символ ("/"), и команда jne выполнена не будет. После завершения сканирования управление будет передано на метку gotit. Содержимое регистров в этой точке: СХ=3 (так как не выполнено сканирование 3 символов), DI = <смещение string> + 5 (выполнено сканирование 5 символов). ES:DI указывают на символ строки, следующий за найденным знаком "/" (в данном случае символ "Т").

386+ SCASD
Сканирование строки двойных слов с целью сравнения
Команда аналогична командам МП 86 scab и scasw, но позволяет сравнивать содержимое расширенного регистра ЕАХ с двойным словом в памяти, адресуемым через регистры ES:EDI.

Пример
;В полях данных сегмента данных, адресуемого через ES:
nums dd 156000,432666,100000,0,4567890, ...
;В программном сегменте:
cld ;Поиск вперед
mov DI,offset nums ;ES:DI -> nums
mov EAX,0 ;Искомое число
mov CX,10000 ;Максимальное число элементов
repne scasd ;Поиск 0 среди чисел массива
jne no0 ;Искомого числа нет
isO: ;Искомое число найдено

В примере в массиве чисел имеется искомое число 0 (с таким же успехом можно было искать любое другое число, например, 4567890). Команда jne выполнена не будет. После завершения сканирования управление будет передано на метку is0. Содержимое регистров в этой точке: СХ уменьшится на 4 (просмотрено 4 элемента), DI будет увеличено на 10h (4 числа по 4 байт в числе).

386+ SETcc Установка байта по условию
Команды, обозначаемые (в книгах, не в программах!) SETcc, осуществляют запись в указанный байтовый операнд 1 или 0 в зависимости от одного из 16 условий, определяемых флагами состояния. Если условие ее выполняется, команда записывает в операнд 1; если условие не выполняется - 0.В качестве операнда можно использовать байтовый регистр или 8-битовую ячейку памяти.
В составе команд процессора предусмотрены следующие команды условной установки байта:
Команда Установить 1, если Условие установки 1
seta выше CF=0 и ZF=0
setae выше или равно CF=0
setb ниже CF= I
setbe ниже или равно CF=1 или ZF=1
setc перенос CF=1
sete равно ZF=1
setg больше ZF=0 или SF=OF
setge больше или равно SF=OF
setl меньше SF не равно OF
setle меньше или равно ZF=1 или SF не равно ОР
setna не выше CF=1 или ZF=1
setnae не выше и не равно CF=1
setnb не ниже CF=0
setnbe не ниже и не равно CF=0 и ZF=0
setnc нет переноса CF=0
setne не равно ZF=0
setng не больше ZF=1 или SF не равно OF
setnge не больше и не равно SF не равно OF
setnl не меньше SF=OF
setnle не меньше и не равно ZF=0 и SF=OF
setno нет переполнения OF=0
setnp нет четности PF=0
setns знаковый бит равен О SF=0
setnz не нуль ZF=0
seto переполнение OF=1
setp есть четность PF=1
setpe сумма битов четная PF=1
setpo сумма битов нечетная PF=0
sets знаковый бит равен SF=1
setz нуль ZF= I

Команды, осуществляющие установку по условию "выше - ниже", предназначены для анализа чисел без знака; команды, осуществляющие установку по условию "больше - меньшее", предназначены для анализа чисел со знаком.

Пример 1
cmp AX,35h
seta CH ;Если AX>35h, CH=1
;Если AX<=35h, CH=0

Пример 2
; В полях данных
flag db ?
;В программном сегменте
test AX,8000h
sete flag ;Если в АХ установлен бит 7,
;flag=l. Иначе flag=0

386Р+ SGDT Сохранение в памяти содержимого регистра таблицы глобальных дескрипторов
Команда копирует содержимое регистра таблицы глобальных дескрипторов GDTR (линейный базовый адрес таблицы и ее границу) в поле из 6 байт, указанное в качестве операнда.

SHL Логический сдвиг влево
Команда полностью эквивалентна команде sal (арифметический сдвиг влево). См. описание команды sal.

386+ SHLD Логический сдвиг влево с двойной точностью
Трехоперандная команда shld с операндами op1, ор2 и орЗ осуществляет сдвиг влево первого из своих операндов opl. Число битов сдвига определяется третьим операндом орЗ. По мере сдвига операнда opl влево, выдвигаемые из него старшие биты, пройдя через флаг CF, теряются, ; на освобождающиеся места со стороны его младших битов поступают старшие биты второго операнда ор2, как если бы он вдвигался своим левым (старшим) концом в opl. Однако после завершения сдвига значение операнда ор2 не изменяется (рис. П10). Во флаге CF остается последний выдвинутый из операнда opl бит. Максимальное число битов сдвига составляет 31.

Рис. П10. Действие команды shld.

В качестве первого операнда op1можно указывать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовую ячейку памяти. Вторым операндом ор2 может служить только 16- или 32-разрядный регистр общего назначения. Третий операнд, характеризующий число битов сдвига, может находиться в регистре CL или быть непосредственным значением.
Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AX,OC001h
mov BX,900Fh
shld AX,BX,1 ;AX=8003h, BX=900Fh, CF=1

Пример 2
mov AX,0C001h
mov BX,900Fh
shld AX,BX,2 ;AX=0006h, BX=900Fh, CF=1

Пример 3
mov AX,0C001h
mov BX,900Fh
shld AX,BX,3 ;AX=000Ch, BX=900Fh, CF=0

Пример 4
mov EBX,0FFCS000h
mov ESI,12340000h
mov CL,16
shld EBX,ESI,CL ;EBX=80001234h,
;ESI=12340000h, CF=0

SHR Логический сдвиг вправо
Команда shr осуществляет сдвиг вправо всех битов операнда. Младший бит операнда поступает в флаг CF. Если команда записана в формате

SHR операнд,1

сдвиг осуществляется на 1 бит. В старший бит операнда загружается 0, а младший теряется. Если команда записана в формате

SHR onepand,CL

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

Рис. П11. Действие команды shr.

В качестве операнда можно указывать любой регистр (кроме сегментного) или ячейку памяти размером как в байт, так и в слово. Не допускается использовать в качестве операнда непосредственное значение. Команда воздействует на флаги CF, OF, PF, SF и ZF.

Пример 1
mov AL,7
shr AL,1 ;AL=3, CF=1

Пример 2
mov AX, lFF0h
mov CL,4
shr AX,CL ;AX=01FFh, CF=0

Пример 3
mov DX,9513h
mov CL,8
shr DX,CL ;DX=0095h, CF=0

386+ Допустим сдвиг 32-битовых операндов. Допустимо указание числа бит сдвига как с помощью регистра CL, так и непосредственным значением. Максимальная величина сдвига составляет 31 бит.

Пример 1
mov ESI,0FFFF0009h
shr ESI,8 ;ESI=00FFFF00h, CF=0

Пример 2
; В полях данных
mem dd 11111111h
;B программном сегменте
shr mem,12 ;mem=00011111h, CF=0

386+ SHRD Логический сдвиг вправо с двойной точностью
Трехоперандная команда shrd с операндами opl, ор2 и орЗ осуществляет сдвиг вправо первого из своих операндов opl. Число битов сдвига определяется третьим операндом орЗ. По мере сдвига операнда opl вправо выдвигаемые из него младшие биты, пройдя через флаг CF, теряются, а на освобождающиеся места со стороны его старших битов поступают младшие биты второго операнда ор2, как если бы он вдвигался своим правым (младшим) концом в opl. Однако после завершения сдвига значение операнда ор2 не изменяется (рис. П12). Во флаге CF остается последний выдвинутый из операнда opl бит. Максимальное число битов сдвига составляет 31.
В качестве первого операнда opl можно указывать 16- или 32-разрядный регистр общего назначения или 16- или 32-битовую ячейку памяти. Вторым операндом ор2 может служить только 16- или 32-разрядный регистр общего назначения.

Рис. П12. Действие команды shrd.

Третий операнд, характеризующий число битов сдвига, может находиться в регистре CL или быть непосредственным значением.
Команда воздействует на флаги OF, SF, ZF, PF и CF.

Пример 1
mov AX,0C001h
mov BX,900Eh
shrd AX,BX,1 ;AX=6000h, BX=900Eh, CF=1

Пример 2
mov AX,0C001h
mov BX,900Eh
shrd AX,BX,2 ;AX=B000h, BX=900Eh, CF=0

Пример 3
mov AX,0C001h
mov BX,900Eh
shrd AX,BX,3 ;AX=D800h, BX=900Eh, CF=0

Пример 4
mov EBX,0FFCS000h
mov ESI,12345678h
mov CL,16
shrd EBX,ESI,CL ;EBX=5678FFC8h,
;ESI=12345678h, CF=0

386P+ SIDT Сохранение в памяти содержимого регистра таблицы дескрипторов прерываний
Команда копирует содержимое регистра таблицы дескрипторов прерываний IDTR (линейный базовый адрес таблицы и ее границу) в поле из 6 байт, указанное в качестве операнда.

386Р+ SLDT Сохранение содержимого регистра таблицы локальных дескрипторов
Команда копирует содержимое регистра таблицы локальных дескрипторов LDTR (селектор таблицы) в 16- или 32-разрядный регистр или в 16- или 32-битовое поле памяти, указанные в качестве операнда.

386Р+ SMSW Сохранение слова состояния машины
Команда smsw считывает слово состояния машины (так называется младшая половина управляющего регистра процессора CRO) и загружает его в указанный в команде 16-разрядный регистр общего назначения или 16-битовое поле памяти.
Команду srnsw можно использовать для перевода процессора из реального в защищенный режим или наоборот. В первом случае после чтения слова состояния командой smsw надо установить в нем бит 0 (бит РЕ) и загрузить назад в CRO командой Imsw. Во втором случае после после чтения слова состояния командой smsw надо сбросить в нем бит 0 и загрузить назад в CRO командой Imsw.

STC Установка флага переноса
Команда stc устанавливает флаг переноса CF в регистре флагов. Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
stc ;Флаг CF устанавливается

STD Установка флага направления
Команда STD устанавливает флаг направления DF в регистре флагов, определяя тем самым обратное направление выполнения строковых операций (в порядке убывания адресов элементов строки). Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
std ;Флаг направления устанавливается

STI Установка флага прерывания
Команда STI устанавливает флаг разрешения прерываний IF в регистре флагов, разрешая все аппаратные прерывания (от таймера, клавиатуры, дисков и т.д.). Команда не имеет параметров и не воздействует на остальные флаги процессора.

Пример
sti ;Разрешение аппаратных прерываний

STOS Запись в строку данных
STOSB Запись байта в строку данных
STOSW Запись слова в строку данных
Команды предназначены для операций над строками (строкой называется последовательность байтов или слов памяти с любым содержимым). Они копируют содержимое регистра AL (в случае операций над байтами) или АХ (в случае операций над словами) в ячейку памяти соответствующего размера по адресу, определяемому содержимым пары регистров ES:DI. После операции копирования регистр DI получает положительное (если флаг DF=0) или отрицательное (если флаг DF=1) приращение. Величина приращения составляет 1 или 2 в зависимости от размера копируемого элемента.
Вариант команды stos имеет формат

stos строка

(что не избавляет от необходимости инициализировать регистры ES:DI адресом строки строка). Заменить сегментный регистр ES нельзя.
Рассматриваемые команды могут предваряться префиксом повторения rep. В этом случае они повторяются СХ раз, заполняя последовательные ячейки памяти одним и тем же содержимым регистра AL или АХ. Команда не воздействует на флаги процессора.

Пример 1
;В полях данных сегмента данных, адресуемого через ES:
id db 'ID:'
;В программном сегменте:
eld ;Движение по строке вперед
mov DI,offset id+3;DI -> за знаком ':'
mov AL,'3' ;Код ASCII цифры 3
stosb ;Отправим в строку
mov AL,'9' ;Код ASCII цифры 9
stosb ;Отправим в строку
;Теперь в строке id записано 'ID:39'

Пример 2
;В полях данных сегмента данных, адресуемого через ES:
array dw 10000 dup (?) ;Место под массив слов
; В программном сегменте:
mov AX,-1 ;Число-заполнитель
mov CX,10000 ;Заполнить 10000 слов
сld ;Движение по строке вперед
lea DI,array ;ES:DI ® array
rep stosw ;Bce 10000 элементов массива
;получают значение -1 (FFFFh)

Пример 3
;В полях данных сегмента данных, адресуемого через ES:
line db 80 dup (' ') ;Пустая пока строка
;В программном сегменте:
mov AL,'>' ;Код ASCII знака '>'
mov CX,5 ;Заполнить 5 слов
eld ;Движение по строке вперед
lea DI,line ;ES:DI -" line
rep stos line ;Первые 5 байт строки line
;заполняются кодом ASCII
;знака ' >'

Пример 4
;В полях данных сегмента данных, адресуемого через ES:
line dw 80 dup (0) ;Строка, заполненная нулями
;В программном сегменте:
mov AL,'>' ;Код ASCII знака '>'
mov AH,31h ;Атрибут (синий по бирюзовому)
mov CX,5 ; Заполнить 5 слов
сld ;Движение по строке вперед
lea DI,line ;ES:DI -" line
rep stos line ;Первые 5 слов строки line
;заполняются кодом ASCII
;знака '>'вместе с атрибутом
;для последующего вывода на экран

386+ STOSD Запись двойного слова в строку данных
Команда аналогична командам МП 86 stosb и stosw, но позволяет записать в строку, адресуемую через регистры ES:EDI, двойное слово из регистра ЕАХ.

Пример
; В полях данных
dat dd 12789,2,550000,100000
; В программном сегменте
mov ЕАХ,444777
push DS
pop ES ;ES=DS
mov DI, off set dat
add DI,4*2 ;DS:SI -" 3-й элемент массива
;чисел
stosd ;dat=12789,2,444777,100000

386P+ STR
Сохранение содержимого регистра состояния задачи
Команда str копирует содержимое регистра задачи TR (селектор сегмента состояния задачи) в двухбайтовый регистр общего назначения или 16-битовую ячейку памяти, указанные в качестве операнда.

SUB Вычитание целых чисел
Команда sub вычитает второй операнд (источник) из первого (приемника) и помещает результат на место первого операнда. Исходное значение первого операнда (уменьшаемое) теряется. Таким образом, если команду вычитания записать в общем виде

sub операнд_1, операнд_2

то ее действие можно условно изобразить следующим образом:

операнд_1 - операнд_2 -> операнд_1

В качестве первого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Пример 1
mov AX,100
mov BX,60
sub АХ,ВХ ;АХ=40 (АХ-ВХ) , ВХ=60

Пример 2
mov DL, '8 ' mov DH, '0 '
sub DL,DH ;DL=8 (преобразование кода
; ASCII в цифру))

Пример 3
; ; В полях данных
datl dw -168
dat2 dw 10
; ; В программном сегменте
mov AX,data2
sub mem,AX ;mem = -178

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,1000000
mov EBX,60000
sub EAX,EBX ;EAX=40000

TEST Логическое сравнение
Команда test выполняет операцию логического умножения И над двумя операндами и, в зависимости от результата, устанавливает флаги SF, ZF и PF. Флаги OF и CF сбрасываются, a AF имеет неопределенное значение. Состояние флагов можно затем проанализировать командами условных переходов. Команда test не изменяет ни один из операндов.
В качестве первого операнда команды test можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака.

Правила побитового умножения:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0001
Флаг SF устанавливается в 1, если в результате выполнения команды образовалось число с установленным знаковым битом.
Флаг ZF устанавливается в 1, если в результате выполнения команды образовалось число, состоящее из одних двоичных нулей.
Флаг PF устанавливается в 1, если в результате выполнения команды образовалось число с четным количеством двоичных единиц в его битах.

Пример 1
test AX,1
jne bityes ;Переход, если бит 0 в АХ установлен
je bitno ;Переход, если бит 0 в АХ сброшен

Пример 2
test SI,8
jne bityes ;Переход, если бит 3 в SI установлен
je bitno ;Переход, если бит 0 в АХ сброшен

Пример 3
test DX,0FFFFh
jz null ;Переход, если DX=0
jnz smth ;Переход, если DX не 0

Пример 4
test CX,0F000h
jne bitsyes ;Переход, если какие-либо из
;4 старших битов СХ установлены
je bitsno ;Переход, если все 4 старших бита
; СХ сброшены

Пример 5
test AX,AX
jz zero ;Переход, если АХ=0
jnz notzero ;Переход, если АХ не 0

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
test ЕАХ,80000000h
jz b31 ;Переход, если бит 31 ЕАХ равен 0
jnz nob31 ;Переход, если бит 31 ЕАХ равен 1

386Р+ VERR Проверка сегмента на чтение
Команда verr позволяет определить, разрешено ли чтение из сегмента, за которым закреплен селектор, передаваемый команде в качестве ее операнда. Операндом может служить 16-разрядный регистр общего назначения или 16-битовая ячейка памяти.

386Р+ VERW Проверка сегмента на запись
Команда verw позволяет определить, разрешена ли запись в сегмент, за которым закреплен селектор, передаваемый команде в качестве ее операнда. Операндом может служить 16-разрядный регистр общего назначения или 16-битовая ячейка памяти.

486+ XADD Обмен и сложение
Команда xadd выполняет в одной операции сложение и обмен операндов. Команда требует двух операндов, причем первый операнд должен быть ячейкой памяти, а второй - регистром. После сложения операндов исходное содержимое памяти переносится во второй операнд (регистр), а полученная сумма записывается в память (на место первого слагаемого) (рис. П13). Команда воздействует на флаги OF, SF, ZF, AF, PF и CF.

Рис. П13. Действие команды xadd.

Пример
; В полях данных
mem dw 99
;В программном сегменте
mov AX,48
xadd mem,AX ;mem=147, AX=99

XCHG Обмен данными между операндами
Команда xchg пересылает значение первого операнда во второй, а второго - в первый. В качестве любого операнда можно указывать регистр (кроме сегментного) или ячейку памяти, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами и представлять числа со знаком или без знака. Команда не воздействует на флаги процессора.

Пример 1
mov AX,OFF01h
mov SI,1000h
xchg AX,SI ;AX=01000h, SI=FF01h

Пример 2
;В полях данных:
mem dw 0F0F0h
;В программном сегменте
mov CX,1256h
xchg CX,mem ;CX=F0F0h, mem=1256h

Пример 3
mov AX,6031h
xchg AH,AL ;AX=3160h

386+ Допустимо использование 32-битовых операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
xchg ESI,EDI ;ESI и EDI обмениваются содержимым

XLAT Табличная трансляция
Команда xlat осуществляет выборку байта из массива байтов, который в этом случае называют таблицей трансляции. В регистре ВХ должен находиться относительный адрес таблицы, а в регистре AL - смещение в таблице к выбираемому байту (его индекс). Выбранный байт загружается в регистр AL, замещая находившееся в нем смещение. Длина таблицы может достигать 256 байт. Таблица должна находиться в сегменте данных, адресуемом через сегментный регистр DS. Замена сегмента не допускается. Команда xlat не имеет параметров, но требует предварительной настройки регистров ВХ и AL. Команда не воздействует на флаги процессора.

Пример
;Пример демонстрирует преобразование первых 14 скен-кодов
;(фактически это скен-коды клавиш верхнего ряда
;клавиатуры) в коды ASCII соответствующих символов
; В полях данных:
table db 0,27, ' 1234567890- = \';Таблица кодов ASCII
;В программном сегменте
lea BX,table
mov AL,5 ;Скен-код 5 клавиши <4/$>
xlat ;AL=34h, код ASCII символа 4

386+ XLAT
386+ XLATB
Команда xlatb эквивалентна команде xlat МП 86 за исключением того, что для 32-разрядных приложений относительный адрес таблицы размещается в расширенном регистре ЕВХ.
Команда xlat может иметь в качестве операнда относительный адрес таблицы трансляции; в этом случае помещение адреса таблицы в регистр ЕВХ не требуется. Действие команды от этого не изменяется, однако возможна замена сегмента.

Пример
;В сегменте, адресуемом через сегментный регистр ES:
table db 0,27,'1234567890-=\';Таблица кодов ASCII
;В программном сегменте
mov AL,13 ;Скен-код клавиши <=/+>
xlat ES:table ;AL=3Dh, код ASCII символа =

XOR Логическое ИСКЛЮЧАЮЩЕЕ ИЛИ
Команда хог выполняет операцию логического (побитового) ИСКЛЮЧАЮЩЕГО ИЛИ над своими двумя операндами. Результат операции замещает первый операнд; второй операнд не изменяется. Каждый бит результата устанавливается в 1, если соответствующие биты операндов различны, и сбрасывается в 0, если соответствующие биты операндов совпадают.
В качестве первого операнда команды хог можно указывать регистр (кроме сегментного) или ячейку памяти, в качестве второго - регистр (кроме сегментного), ячейку памяти или непосредственное значение, однако не допускается определять оба операнда одновременно как ячейки памяти. Операнды могут быть байтами или словами. Команда воздействует на флаги OF, SF, ZF, PF и CF, причем флаги OF и CF всегда сбрасываются, а остальные флаги устанавливаются в зависимости от результата.

Правила побитового ИСКЛЮЧАЮЩЕГО ИЛИ:
Первый операнд-бит 0101
Второй операнд-бит 0011
Бит результата 0110

Пример 1
mov AX,0Fh
хог AX,0FFFFh ;AX=FFF0h

Пример 2
хог ВХ,ВХ ;Обнуление ВХ

Пример 3
mov SI,0AAAAh
mov BX,5555h
xor SI,BX ;SI=FFFFh,BX=5555h

386+ Допустимо использование 32-битовьгх операндов и дополнительных режимов адресации 32-разрядных процессоров.

Пример
mov EAX,4444AAAAh
xor EAX,4441AAACh ;EAX=00050006h