Глава 6
Свойства Макроассемблера
В этой части будут описаны некоторые свойства макроассемблера,
разработанного в фирме IBM. Хотя мы уже рассмотрели все команды
процессора 8088, в ассемблере имеются и другие команды. Мы уже
обсудили некоторые из этих псевдокоманд, например, операторы
определения данных DB и DW. В этой главе будут введены более мощные
средства языка ассемблера. Их объединяет то, что их использование
делает написание программ на языке ассемблера более простым и
легким.
В этой главе мы рассмотрим две больших темы. Первая - механизм
макрокоманд Макроассемблера. Макрокоманды - мощный инструмент
генерирования машинных команд. Пониманние механизма макрокоманд
будет необходимо в главе 7, где обсуждается математический
сопроцессор 8087. Вторая тема касается структур данных. Мы уже
рассматривали определение данных и их типов. Здесь мы определим
структуры данных, состоящие из наборов байт и слов. В этой главе
рассматриваются сегменты, структуры и записи для определения
данных.
Макроопределения
Макрокоманда - это программный инструмент, который позволяет вам
создавать собственные операции ассемблера. На самом деле макро-
определения относятся к механизму препроцессора. Макропроцессор
позволяет определять новые коды операций для процессора. В этом
определении вы, в частности, сообщаете ассемблеру текст выполняемой
операции. Когда ассемблер встречает этот вновь определенный код
операции, он обращается к сохраненному определению макрокоманды и
помещает в транслируемый участок программы текст из этого
определения. Например, в программе могут быть определены в качестве
макрокоманд часто используемые последовательности команд. Каждый
раз, когда эти команды должны быть вставлены в текст программы,
программист может вместо этого воспользоваться макрокомандой.
В использовании макрокоманды можно выделить два шага. На первом
шаге макрокоманда определяется в программе. Программист присваивает
ей имя и определение. Определение состоит из из операций ассемблера
и команд, которые будут генерироваться каждый раз при появлении
имени макрокоманды. Второй шаг - применение макрокоманды. Это
происходит когда ассемблер встречает ее имя в качестве кода
операции. Ассемблер заменяет это имя указанными в определении
командами.
Возьмем в качестве примера команды сопроцессора 8087, который
мы обсудим в глве 7. В написании программ с использованием команд
числового процессора 8087 возникают некоторые трудности. В
макроассемблере отсутствуют коды операций 8087. Для использования
8087 вы должны сформировать его команды с помощью либо оператора
определения данных, либо кодов операций WAIT и ESC. Лучше всего это
делать через определение макрокоманды, что позволит вам писать
команды 8087. После этого программа может пользоваться командами
8087, хотя они и не входят в язык ассемблера.
В программировании на языке ассемблера макрокоманды исполь-
зуются наиболее часто. Хотя видимых причин не применять макропро-
цессор в языках высокого уровня нет, там макрокоманды встречаются
довольно редко. Макроассемблер для IBM PC поддерживает
макрокоманды. Как мы уже отмечали, существует две версии
ассемблера. Малый ассемблер, ASM, не поддерживает это средство.
Полный ассемблер, MASM, допускает все макрооперации, которые обсуж-
даются в этой главе. Для использования MASM ваш персональный
компьютер должен иметь как миимум 96K оперативной памяти.
Простейшая макрокоманда, которую можно использовать как код
операции 8087, - FENI. Макроассеблер 8088 не распознает ключевого
слова FENI, которое в действительности является командой для 8087.
Фиг. 6.1 показывает два шага макро-процесса: определение
макрокоманды FENI и ее последующий вызов в программе. Фиг.6.1
состоит из двух частей: часть (a) - это исходный файл для
программы, а часть (b) содержит листинг ассемблера для нее. Два
варианта на Фиг. 6.1 разделены, чтобы показать, какой из них
написан программистом, а какой сгенерирован макропроцессором.
Программа определяет макрокоманду с помощью ключевого слова
MACRO. На Фиг. 6.1 макроопределение выглядит так:
FENI MACRO
;---- Тело макрокоманды
ENDM
Оператор MACRO является кодом псевдооперации. Эта конкретная псев-
дооперация сообщает ассемблеру, что начинается определение макроко-
манды. В поле имени операции указано это имя, которое программа
приписывает определяемой макрокоманде, в нашем случае FENI. Команды
PAGE ,132
TITLE Фиг. 6.1 Макрокоманда
FENI MACRO
DB 0DBH, 0E0H
ENDM
CODE SEGMENT
ASSUME CS:CODE
FENI
CODE ENDS
END
Фиг. 6.1 (a) Исходный файл для программы
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:02:38
Фиг. 6.1 Макрокоманда Page 1-1
PAGE ,132
TITLE Фиг. 6.1 Макрокоманда
FENI MACRO
DB 0DBH, 0E0H
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE
FENI
0000 DB E0 1 DB 0DBH, 0E0H
0002 CODE ENDS
END
Фиг. 6.1 (b) Листинг ассмблера программы
Фиг.6.1 Макроопределение.(a) исходный файл; (b) листинг ассемблера.
(или действия ассемблера), которые будут заменять имя макрокоманды,
следуют за строкой заголовка. Наконец, ключевое слово ENDM
указывает ассемблеру на конец определения. Текст между операторами
MSCRO и ENDM называется телом макрокоманды. На Фиг. 6.1 телом
макрокоманды FENI является оператор определения байтов. Поскольку в
8088 нет команды, которая соответствовала бы команде FENI код
машинного языка для этой команды должен состять из операторов DB.
Важно заметить, что во время определения макрокоманды код
машинного языка еще не генерируется. Это можно утверждать, потому
что колонки адреса и данных в листинге ассемблера пусты. Когда
ассемблер впервые встречае макроопределение, он сохраняет его для
дальнейшего использования. Затем программа на Фиг. 6.1 привлекает
макрокоманду FENI. Программист использует имя макрокоманды FENI как
если бы это был код оперции ассемблера типа CLD или DAA, а
ассемблер обращается к сохраненному определению макрокоманды FENI.
Ассемблер берет текст из тела макроопределения и помещает его в той
же позиции транслируемой программы. Знак "+", появляющийся слева от
оператора DB в распечатке ассемблера, указывает на то, что эта
строка вставлена макропроцессором. Если сравнить исходный текст с
ассемблируемым, вы увидите в исходном тексте только команду FENI, в
то время как на листинге ассемблера за командой FENI следует тело
макрокоманды. В данном случае оно представлено одним оператором DB.
Этот простой пример демонстрирует большие возможности
макропроцессора. Возникла необходимость в коде операции FENI,
который не предусмотрен ассемблером. При отсутствии механизма
макрокоманд программист был бы вынужден вместо операции FENI каждый
раз записывать ее код:
DB 0DBH, 0E0H
Имея же в распоряжении такой механизм, можно определить
макрокоманду FENI и в дальнейшем в этой же программе использовать
как код операции только ее. Для использования подобных макрокоманд
есть две серьезные причины. Во-первых, они облегчают написание
программы. Во-вторых, при чтении текста программы оператор FENI,
выглядит гораздо более осмысленным, чем DB 0DBH,0E0H.
Макрокоманду можно сравнить с подпрограммой. Подпрограмма - это
участок программы, определяемый в единственном месте программы,
Программа может передать управление подпрограмме из любой своей
точки. Использование подпрограмм экономит время написания и объем
памяти, занимаемый программой. Вместо того, чтобы каждый раз
переписывать команды подпрограммы, когда необходимо ее выполнение,
вы вставляете ее вызов. Подпрограмма выполняет свою определенную
функцию, и управление возвращантся в точку вызова.
Макрокоманда точно также определяется в ассемблируемой
программе в единственном месте. После того, как макрокоманда
определена, ее можно привлечь ("вызвать") в любой точке
транслируемой программы. Использование макрокоманды экономит время
составления программ и место, занимаемое исходным файлом. Вместо
того, чтобы всякий раз, когда в них возникает необходимость,
переписывать входящие в макрокоманду команды, в программу
вставляется вызов макрокоманды. Ассемблер генерирует заданные в
определении команды и переходит к обработке следующего кода
операции.
Разница между макрокомандой и подпрограммой заключается в
моменте их использования. Макрокоманда является операцией текстовой
обработки. Она определяется и "выполняется" во время
ассемблирования. Выполнение макрокоманды состоит в замене имени
макрокоманды текстом, составляющим тело макрокоманды. Подпрограмма
же хотя и определяется при ассемблировании, но выполняется не
раньше, чем сама программа. Мы будем говорить, что макрокоманда
выполняется при ассемблировании, а подпрограмма - во время
выполнения программы.
Лучший способ отличить макрокоманду от подпрограммы - это
запомнить, когда они проявляются. На самом деле, макропроцессор не
является атрибутом языка программирования. Допустим, вы - адвокат,
и составляете завещания для множества разных людей. Так как
завещания часто похожи, то вы могли бы задать набор
"макрозавещаний", которые будут содержать совпадающие части
составляемых завещаний. Первая часть завещания, где перечислены
участвующие в нем стороны, будет уникальной. Оставшаяся часть будет
состоять из различных макрозавещаний, охватывающих стандартные
куски завещаний. Результатом работы "процессора завещаний" будет
текстовый документ. Макрозавещания раскрываются и образуют
стандартную часть завещания. Вам остается только заполнить
переменные фрагменты документа между макрозавещаниями.
Но если макрокоманды и подпрограммы во многих отношениях так
похожи, то зачем использовать вместо процедуры макрокоманду?
Действительно, во многих случаях применима любая из них.
Последовательность команд может быть определена и как макрокоманда,
и как подпрограмма. Когда вам потребуется эта последовательность,
вы можете вызвать соответственно макрокоманду или подпрограмму.
Какую именно - зависит от того, как вы определили данную
последовательность команд. Выбор последнего задается соображениями
времени выполнения программы и объема занимаемой ею памяти. В
большинстве случаев использование макрокоманд приводит к более
длинным программа, т.е. для реализации одной и той же функции
требуется больше байтов объектного кода. Однако такая программа
выполняется быстрее так как отсутствуют временные издержки,
связанные с вызовом подпрограммы и возвратом в программу каждый
раз, когда требуется данная последовательность команд. Для
минимизации размера программы слудет использовать подпрограммы.
Чтобы иметь программу с максимальным быстродействием, вы
пользуетесь макрокомандами.
В случае макрокоманды FENI на Фиг. 6.1, выбор в пользу
макрокоманды очевиден. Здесь соответствующий участок программы в
качестве макрокоманды не только выполняется быстрее нежели, в
качестве подпрограммы, но и занимает меньше памяти. Команда CALL
для близкой процедуры требует три байта. Макрокоманды FENI - только
два байта. В случае макрокоманд для процессора 8087 для реализации
тех же функций через процедуры потребовалось бы больше байтов
объектного кода. Кроме того использование макрокоманд сокращает
время выполнения программы.
Аргументы макрокоманд
В обработке макрокоманд применяется одно из ценных свойств
процедур: в генерацию макрокоманды можно вносить изменения с
помощью параметров. Точно так же, как параметры подпрограммы могут
влиять на ее выполнение, параметры макрокоманды определяют
фактически генерируемые команды. И так же, как и процедуры,
макрокоманды без параметров встречаются относительно редко.
Рассмотрим еще один простой пример. Мы составили программу так,
что во многих ее местах содержимое определенной ячейки памяти
складывается с различными константами. Вместо того, чтобы много раз
писать команду
ADD MEMORY_BYTE,5
или
ADD MEMORY_BYTE,7
нам хотелось бы воспользоваться для нее соответствующей
макрокомандой. Однако во всех приведенных командах константы
разные. Поэтому мы сделаем константу параметром макрокоманды. На
Фиг. 6.2 показаны определение и применение макрокоманды ADDBYTE. В
этом примере в качестве параметра в определении макрокоманды
используется символическое имя CONSTANT. Любые символичесике имена,
появляющиеся в поле операнда оператора MACRO, интерпретируются как
параметры. В момент определения макрокоманды у имени CONSTANT нет
никакого значения: оно просто резервирует место в тексте
макрокоманды. Позднее, при вызове и обработке текста макрокоманды,
вместо символического имени в определении макрокоманды
подставляется определенное значение параметра.
Важно отметить, что параметр макрокоманды - это текстовый
параметр. Так как макропроцессор фактически является текстовым
процессорорм, то он не отличает цифры от букв и наоборот. Это
позволяет при вызове макрокоманды использовать вместо чисел
символические имена. Любой смысл приписывается символьной строке не
макропроцессором, а ассемблером. Макропроцессор подставляет
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:02:43
Фиг. 6.2 Макрокоманда с аргументом Page 1-1
PAGE ,132
TITLE Фиг. 6.2 Макрокоманда с аргументом
ADDBYTE MACRO CONSTANT
ADD MEMORY_BYTE, CONSTANT
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE
0000 ?? MEMORY_BYTE DB ?
= 0004 FOUR EQU 4 ; Симвользое изображение константы
ADDBYTE 2
0001 2E: 80 06 0000 R 02 1 ADD MEMORY_BYTE, 2
ADDBYTE 4
0007 2E: 80 06 0000 R 04 1 ADD MEMORY_BYTE, 4
ADDBYTE FOUR
000D 2E: 80 06 0000 R 04 1 ADD MEMORY_BYTE, FOUR
0013 CODE ENDS
END
Фиг. 6.2 Аргументы макрокоманды
текстовую строку из вызова макрокоманды на место символического
имени в определении макрокоманды. Таким образом программа может
использовать константное значение "FOUR" с тем же успехом, что и
константу "4".
Возможность использовать символические имена в качестве
параметров макрокоманд принципиально важна для следующего примера
макрокоманды. Этой макрокоманда, одной из команд сопроцессора 8087,
требуется параметр, который при обычном ее использовании почти
всегда бывает символическим именем. Макрокоманда FLDCW - это
команда сопроцессора 8087, которая задает ячейку памяти. Так как в
программах на языке ассемблера в большинстве случаев обращаются к
ячейкам памяти с помощью символических имен, то желательно
сохранить этот способ и для программирования сопроцессора 8087.
На Фиг.6.3 приводится макрокоманда FLDCW и несколько обрашений
к ней. Заметьте, что макрокоманда FLDCW использует в качестве
параметра символическое имя "SOURCE". Параметр SOURCE является
адресом, с которого сопроцессор 8087 загружает управляющее слово.
Для генерации требуемого машинного кода макрокоманда FLDCW
использует команду 8088 ESC. Однако для того, чтобы определить байт
mod=r/m команды, команде ESC требуется значение адреса. Как раз для
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:02:47
Фиг. 6.3 Макрокоманда для команды FLDCW Page 1-1
PAGE ,132
TITLE Фиг. 6.3 Макрокоманда для команды FLDCW
FLDCW MACRO SOURCE
DB 09BH
ESC 0DH, SOURCE
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE
0000 ???? MEMORY_LOCATION DW ?
FLDCW MEMORY_LOCATION
0002 9B 1 DB 09BH
0003 2E: D9 2E 0000 R 1 ESC 0DH, MEMORY_LOCATION
FLDCW ES:[DI]
0008 9B 1 DB 09BH
0009 26: D9 2D 1 ESC 0DH, ES:[DI]
FLDCW MEMORY_LOCATION[BX+SI]
000C 9B 1 DB 09BH
000D 2E: D9 A8 0000 R 1 ESC 0DH, MEMORY_LOCATION[BX+SI]
0012 CODE ENDS
END
Фиг. 6.3. Макрокомнда FLDCW
этого макрокоманда и использует параметр SOURCE. Такая организация
макрокоманды FLDCW позволяет программировать весьма естественным
способом. Точно так же как пишется
INC MEMORY_LOCATION
вы можно написать команду для сопроцессора 8087
FLDCW MEMORY_LOCATION
Это справедливыо не только для адресов, заданных символическими
именами, но и для других способов адресации. На Фиг. 6.3 показано
несколько примеров задания операнда с помощью адресации по базе и
индексу. Так как макропроцессор воспринимает параметр как какой-то
фрагмент текста, то параметр может быть образован любой символьной
строкой, какую вы пожелаете.
Можно задать макрокоманду и с несколькими параметрами.
Единственное, что ограничивает число параметров макрокоманды, это
длина ассемблерной строки. Все, что следует за оператором MACRO
интерпретируется макропроцессором как параметр. Для разделения
символических имен в определении макрокоманды пользуются запятыми.
Оператор MACRO с тремя параметрами будет выглядеть следующим
образом:
EXAMPLE MACRO ARG1, ARG2, ARG3
Аналогично, при вызове макрокоманды вы должны задать значение
каждого из параметров. Если вы хотите пропустить какой-то параметр,
то ассемблер подставит вместо него символьную строку нулевой длины.
Иногда это полезно, но часто приводит к неправильной трансляции.
Если макрокоманда имеет более одного параметра, то при вызове
макрокоманды относящийся к параметрам текст разделяется запятыми.
Это в точности совпадает со способом задания нескольких параметров
к любой из команд микропроцессора 8088, поэтому будет вполне
естественным для вас. Вызов макрокоманды с тремя параметрами может
выглядеть так:
EXAMPLE 5, [BX], MEMORY_BYTE
В следующем примере вы увидите некоторые возможности множествен-
ности параметров.
Ассемблирование по условию
До сих пор макрокоманды не отличались от подпрограмм с точки зрения
как их функционирования, так и использования параметров. Далее, нам
требуется возмоность ассемблирования в зависимости от условия. Так
же как ход выполнения подпрограммы может меняться в зависимости от
некоторых условицй в момент выполнения, так и у макрокоманды должна
быть возможность изменять в момент транслирования генерацию
соответствующего машинного кода в зависимости от удовлетворения
условий.
Макроассемблер фирмы IBM допускает условное ассемблирование. На
самом деле, условное ассемблирование не обязательно входит только в
макрокоманду. Программа может использовать условное транслирование
в любом месте ассемблерного текста. Однако наиболее часто оно
встречается в макрокомандах. В IBM PC условное транслирование
поддерживается только Макроассемблером MASM.
Так же, как и выполнение макрокоманд, условное ассемблирование
происходит во время трансляции, а не выполнения программы. Условное
транслирование позволяет программисту "запрограммировать" ассемблер
на транслирование различных последовательостей кодов. Ассемблер
определяет, что ему транслировать по параметру, известному во время
ассемблирования. Хотя эта возможность может использоваться
программой в любой момент ассемблирования, мы изучим прежде всего,
как она влияет на ассемблирование макрокоманд.
Фиг. 6.4 иллюстрирует условное транслирование при расширении
макрокоманды FIDIVR сопроцессора 8087. Условное тарнслирование
требуетя данной макрокоманде из-за разделения переменных в
ассемблере по типам. Как мы увидим в гл.7, команда, обозначенная
FIDIVR, может применяться к операндам двух типов. Операнд может
быть двух- или четырехбайтовым целым числом. Мы хотим, чтобы
ассемблер выбрал правильный машинный код в зависимости от типа
операнда. Как мы вмдели, у команды ADD в действительности имеется
несколько форм, в зависимости от того, какие операнды представлены
ассемблеру, который выбирает верную форму машинной команды в
зависиммости от этих операндов. Мы хотим делать то же самое для
команды FIDIVR Но в теперь макропроцессор должен определить тип
операнда и сгенерировать правильную команду.
У команды FIDIVR может быть один из двух типов операндов, и в
зависимости от этого будут различаться результирующие команды.
Таким образом, расширение макрокоманды FIDIVR должно
соответствовать нужному операнду. Это обеспечивается двумя
средствами языка: условным транслированием и оператором TYPE.
В языке ассемблера имеется оператор TYPE, который возвращает
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:42
Фиг. 6.4 Условное транслирование Page 1-1
PAGE ,132
TITLE Фиг. 6.4 Условное транслирование
FIDIVR MACRO SOURCE
IFE 2 - TYPE SOURCE
DB 09BH ;; FWAIT
ESC 037H,SOURCE ;; FIDIVR слово
ENDIF
IFE 4 - TYPE SOURCE
DB 09BH ;; FWAIT
ESC 017H,SOURCE ;; FIDIVR короткое целое
ENDIF
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE,DS:CODE
0000 ???? TWO_BYTE DW ?
0002 ???????? FOUR_BYTE DD ?
0006 ?? ONE_BYTE DB ?
FIDIVR TWO_BYTE
0007 9B 1 DB 09BH ;
0008 DE 3E 0000 R 1 ESC 037H,TWO_BYTE ;
FIDIVR FOUR_BYTE
000C 9B 1 DB 09BH ;
000D DA 3E 0002 R 1 ESC 017H,FOUR_BYTE ;
FIDIVR ONE_BYTE
0011 CODE ENDS
END
Фиг. 6.4. Ассемблирование макрокоманд по условию
значение, равное длине операнда. В случае FIDIVR мы ожидаем, что
операнд будет двух- или четырехбайтовым целым числом.
Выражение
IFE 2-TYPE SOURCE
в макрокоманде FIDIVR проверяет длину операнда SOURCE.
Арифметическое выражение 2=TYPE SOURCE, равняется 0, если операнд
SOURCE является двухбайтовым целым числом, и принимает ненулевое
значение при любом другом типе операнда. Оператор IFE
(транслировать, если равно) сообщает ассемблеру, что нужно
транслировать все последующие команды, если выражение в поле
операнда равно 0. Таким образом, оператор IFE вырабатывает значение
истина, если операнд SOURCE является двухбайтовым целым числом. В
этом случае происходит трансляция всех команд, следующих за
оператором IFE до тех пор, пока не встретится оператор ENDIF. В
нашем примере это означает, что если операнд является двухбайтовым
целым числом, то ассемблируется участок программы
DB 09BH
ESC 37H,SOURCE
В первом вызове макрокоманды на Фиг. 6.4 в качестве операнда
используется двухбайтовое целое число. Поэтому, для расширения этой
макрокоманды ассемблер выбирает команду ESC 37H.
Так как команда FIDIVR имеет два варианта, соответствующие двум
разным типам операндов, то в при выполнении другого условия,
макрокоманда использует второй оператор IFE. Когда операнд является
четырехбайтовым целым числом, макрокоманда генерирует код ESC 17H.
На Фиг. 6.4 показаны два разных расширения одной макрокоманды.
Обратите внимание, что в последнем из показанных на Фиг. 6.4
вызовов макрокоманды операнд не удовлетворяет ни одному из условий.
Так как ни один из операторов IFE не вырабатывает значения
"истина", то и ассемблироваться не будет ни один из них. В этом
случае макропроцессор не генерирует никакого кода.
С помощью оператора IFE ассемблер может проверять выполнение
различных условий. Эти условия приведены в таблице на Фиг. 6.5.
Общая форма оператора IF имеет вид:
IFхх выражение
...
ELSE
...
ENDIF
Если значение условия "истина", то ассемблер обрабатывает
участок программы, следующий за оператором IFхх. Этот транслируемый
участок программы заканчивается либо оператором ELSE или ENDIF.
Оператор ELSE не является обязательным. Если он имеется, то
следующий за ним участок программы будет транслирован при
невыполнении условия в операторе IF. Оператор ENDIF завершает
условное ассемблирование и является обязательным.
IF-операция Ассемблировать если:
---------------------------------------------------------------
IF выражение Выражение не равно 0
IFE выражение Выражение равно 0
IFDEF имя Имя уже было описано как внешнее
IFNDEF имя Имя еще не описывалось
IFB <аргумент> Аргумент пуст
IFNB <аргумент> Аргумент не пуст
IFIDN <арг1>,<арг2> Строка арг1 идентична строке арг2
IFDIF <арг1>,<арг2> Строка арг1 отличается от строки арг2
IF1 Первый поход ассемблера
IF2 Второй проход ассемблера
---------------------------------------------------------------
Фиг. 6.5 Операторы IF для условного ассемблирования
Рассмотрим еще один пример, чтобы познакомиться с некоторыми
другими вариантами использования ассемблирования по условию. На
Фиг. 6.6 показано применение другого условного оператора - IFB, а
также использование вложенных условных операторов. Макрокомандой
здесь является FLD - команда загрузки сопроцессора 8087. Для
транслирования этой команды требуется несколько условных
операторов, так как она может применяться в следующих вариантах:
FLD
FLD 1
FLD короткое_вещественное
FLD длинное_вещественное
FLD временное_вещественное
Поле операнда макрокоманды FLD может быть пустым, содержать
константу, или четырехбайтовую, восьмибайтовую либо десятибайтовую
переменную. Макрокоманда должна распознать каждый из перечисленныз
случаев и сгенерировать правильный программный код. (все эти типы
данных рассмотрены очень подробно в гл.7.)
Оператор IFB проверяет наличие операнда. Если операнд
отсутствует, то ассемблер генерирует соответствующий этому случаю
программный код, так как оператор IFB вырабатывает значение
"истина". Это иллюстрирует первый вызов макрокоманды, когда
генерируется код
DB 09BH,0D9H,0C 1H
Оператор EXITM, содержащийся в этой части условного оператора
IF, реализует выход из макрокоманды. Каждый раз, когда при
расширении макрокоманды ассемблеру встречается этот оператор,
расширение заканчивается, как если бы встретился ENDM. В данном
случае ассемблер пропускает оставшуюся часть макроопределения. При
таком выходе из макрокоманды в ассемблерном листинге появляется
предупреждающее сообщение "Open conditionals:1" ("Незавершенные
условные операторы: 1"). Оно предупреждает вас, что ассемблер не Mincho"'> Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:47
Фиг. 6.6 Вложенные условные макрокоманды Page 1-1
PAGE ,132
TITLE Фиг. 6.6 Вложенные условные макрокоманды
FLD MACRO SOURCE
IFB <SOURCE>
DB 09BH,0D9H,0C1H ;; FLD ST(1)
EXITM
ELSE
IFE TYPE SOURCE
DB 09BH,0D9H,0C0H+SOURCE
;; FLD ST(i)
ENDIF
IFE 4 - TYPE SOURCE
DB 09BH
ESC 8,SOURCE ;; FLD короткое плавающее
ENDIF
IFE 8 - TYPE SOURCE
DB 09BH
ESC 40,SOURCE ;; FLD длинное плавающее
ENDIF
IFE 4 - TYPE SOURCE
DB 09BH
ESC 01DH,SOURCE ;; FLD временное плавающее
ENDIF
ENDIF
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE,DS:CODE
0000 ???????? FOUR_BYTE DD ?
0004 ???????????????? EIGHT_BYTE DQ ?
000C ??????????????????? TEN_BYTE DT ?
?
FLD
0016 9B D9 C1 1 DB 09BH,0D9H,0C1H ;
FLD 1
0019 9B D9 C1 1 DB 09BH,0D9H,0C0H+1
FLD FOUR_BYTE
001C 9B 1 DB 09BH
001D D9 06 0000 R 1 ESC 8,FOUR_BYTE ;
0021 9B 1 DB 09BH
0022 DB 2E 0000 R 1 ESC 01DH,FOUR_BYTE ;
FLD EIGHT_BYTE
0026 9B 1 DB 09BH
0027 DD 06 0004 R 1 ESC 40,EIGHT_BYTE ;
FLD TEN_BYTE
002B CODE ENDS
END
Фиг. 6.6 Вложенное условное ассемблирование
встретил оператора ENDIF, закрывающего условный оператор. Это
происходит в результате раннего входа из макрокоманды. Хотя это и
нежелательно, но ни к каким разрушительным последствиям не
приводит. Если оператор EXITM расположен вне условного оператора,
то предупреждение не выводится.
Оператор EXITM необходим в данной макрокоманде, так как
ассемблер проверяет все условные операторы, даже если они не
транслируются. В нашем случае, если операнд SOURCE пуст, оператор
ELSE предотвращает генерацию всех других вариантов команды FLD.
Однако, продолжая просмотр, ассемблер проверяет оператора
IFE TYPE SOURCE
хотя и не может сгенерировать никакого кода. Если SOURCE пусто,
то ассемблер фиксирует синтаксическую ошибку. Вы можете не обращать
внимания на эту ошибку, но принимать трансляцию с сообщениями об
ошибках идет в разрез с нашими правилами. С другой стороны,
использование оператора EXITM приводит к предупреждению "Открытое
условие". Оно нежелательно, но это меньшее из двух зол.
Заметим, что макрокоманда FLD использует ветвь ELSE чтобы
указать на необходимость вычисления выражения в поле операнда
только в случае непустоты этого поля. IF-выражения, содержащие
оператор TYPE, определяют, какой тип операнда использовался при
вызове макрокоманды. Хотя это не упоминается в Руководстве по
Макроассемблеру, оператор TYPE возвращает значение 0, если операнд
является не символическим именем, а константой. Выбор такого
способа в данной макрокоманде вызван скорее соображениями
работоспособности, чем стремлением к элегантности.
Макрокоманды повторения
Для тех случаев, когда нужно несколько раз повторять один и тот же
фрагмент программы в макроассемблере имеется несколько специальных
макрокоманд. Это операторы REPT, IRT и IRPC. Каждый из них
действует как макрокоманда в макрокоманде и приводит к генерации
следующего за ним участка программы, пока ассемблеру не встретится
операнд ENDM.
Для простого повторения последрвательности команд используется
макрокоманда REPT. Последовательность
REPT выражение
;... тело макрокоманды REPT
ENDM
дублирует команды, составляющих тело данной макрокоманды. Значение
выражения определяет число повторений текста.
С помощью макрокоманды IRP можно при каждом повторении
использовать разные параметры. При использовании конструкции
IRP фиктивный параметр,<список>
;... тело макрокоманды IRP
ENDM
ассемблер осуществляет столько проходов тела макрокоманды,
сколько указано элементов в списке. При каждом проходе ассемблер
подставляет вместо фиктивного параметра следующий по порядку
элемент списка. Элементы списка должны быть числовыми выражениями.
Если вы хотите использовать в списке символьные значения,
применяйте макрокоманду IRPC. Последователность
IRPC фиктивный параметр, строка символов
;... тело макрокоманды IRPC
ENDM
осуществляет по одному проходу тела макрокоманды для каждого
символа. При этом фиктивный параметр ассемблер каждый раз заменяет
следующим по порядку символом из строки. На Фиг. 6.7 показаны
примеры применения описанных макрокоманд повтора.
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:54
Фиг. 6.7 Макроповторения Page 1-1
PAGE ,132
TITLE Фиг. 6.7 Макроповторения
0000 CODE SEGMENT
ASSUME CS:CODE,DS:CODE
REPT 3 ; Повторить команду 3 раза
INC AX
ENDM
0000 40 1 INC AX
0001 40 1 INC AX
0002 40 1 INC AX
IRP VALUE,<5,10,15,20>
ADD AX,VALUE
ENDM
0003 05 0005 1 ADD AX,5
0006 05 000A 1 ADD AX,10
0009 05 000F 1 ADD AX,15
000C 05 0014 1 ADD AX,20
IRPC CHAR, ABCD
ADD AX,CHAR&X
ENDM
000F 03 C0 1 ADD AX,AX
0011 03 C3 1 ADD AX,BX
0013 03 C1 1 ADD AX,CX
0015 03 C2 1 ADD AX,DX
0017 CODE ENDS
END
Фиг. 6.7 Макрокоманды повтора
Макрооператоры
Пример макрокоманды IRPC на Фиг. 6.7 иллюстрирует, в частности,
применение символа "&". Это - оператор макрокоманд, который служит
для соединения двух элементов. В данном примере оператор "&"
соединяет параметр CHAR со строкой-константой "X". Как вы видите,
при этом формируется действительное имя регистра.
Еще одним удобным средством при программировании макрокоманд
является оператор LOCAL. Оператор LOCAL задает метку, которая
используется только в данной макрокоманде. Эта метка должна быть
уникальной для каждого вызова данной макрокоманды. Предположим, что
вы хотите написать макрокоманду, в которой должен быть кусок
программы следующего вида:
AAAAA: ADD AL,[BX]
INC BX
LOOP AAAAA
При первом вызове этой макрокоманды никаких сложностей не
Microsoft (R) Macro Assembler Version 5.00 4/2/89 16:06:59
Фиг. 6.8 Использование команды LOCAL Page 1-1
PAGE ,132
TITLE Фиг. 6.8 Использование команды LOCAL
PAUSE MACRO TIME
LOCAL LABEL
MOV CX,TIME
LABEL: LOOP LABEL
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE
PAUSE 100
0000 B9 0064 1 MOV CX,100
0003 E2 FE 1 ??0000: LOOP ??0000
PAUSE 1000
0005 B9 03E8 1 MOV CX,1000
0008 E2 FE 1 ??0001: LOOP ??0001
000A CODE ENDS
END
Фиг. 6.8 Использование команды LOCAL
возникает. Однако при ее повторном использовании в той же
программе, метка AAAAA появляется второй раз. Ассемблер не может
допустить двух меток с одним и тем же именем в одной программе и
отмечает это как ошибку.
Проблема будет разрешена, если объявить метку AAAAA локальной
(LOCAL) для данной макрокоманды. Тогда ассемблер установит сформи-
рованное им уникальное имя для каждого случая применения метки
AAAAA. Встретив имя типа LOCAL впервые, ассемблер меняет его на имя
"??0000". Во второй раз - на имя "??0001" и т.д. Каждое имя в
транслируемой программе будет уникальным, поэтому никаких ошибок не
возникнет. На Фиг. 6.8 показано использование оператора LOCAL.
Здесь макрокоманда PAUSE устанавливает счетчик цикла и затем орга-
низует цикл с помощью метки, объявленной LOCAL. Данная макрокоманда
позволяет организовывать в выполнении программы паузу переменной
длительности. Если в макрокоманде необходимо использовать описатель
LOCAL, то он должен быть первым оператором макрокоманды, сразу же
следуя за оператором MACRO.
Символ Значение
---------------------------------------------------------------
;; Комментарий, имспользуемый только в макроопределении
& Соединение текста с параметром
! Вводить следующий символ без интерпретации
% Преобразовать следующее выражение в значение
---------------------------------------------------------------
Фиг. 6.9 Макросимволы
Существуют специальные символы, которые помогут вам управлять
макрокомандами и их параметрами. В таблице на Фиг. 6.9 показаны
четыре таких символов с объяснением их значений.
На Фиг.6.10 приведен ассемблерный листинг программы, в которой
эти символы применяются. Мы уже видели символ ";;" в некоторых
макрокомандах сопроцессора 8087. Этот специальный указатель поля
комментариев указывает макропроцессору на необходимость исключить
поле комментариев при расширении данной макрокоманды. Это позволит
включать в макрокоманду комментарии, не допуская их появления при
каждом расширении данной макрокоманды. Применение символа "&" мы
также встречали в программе на Фиг. 6.7.
Символ "!" позволяет вам в качестве следующего за ним знака
поставить любой символ. Это понадобится, если вы захотите включить
в макрокоманду какой-либо специальный символ, например, "%", чтобы
он не вызывал при этом никакой макрооперации. И наконец, оператор
"%" преобразует символическое имя в числовое значение, которое это-
му имени в данный момент соответствует. Вы можете воспользоваться
этой возможностью для ведения нумерации при генерации макрокоманды.
В нашем примере на Фиг. 6.10 макрокоманда нумерует выводимые
сообщения в соответствии со значением символического имени VALUE.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:15
Фиг. 6.10 Специальные символы в макрокомандах Page 1-1
PAGE ,132
TITLE Фиг. 6.10 Специальные символы в макрокомандах
= 0000 VALUE EQU 0
EXAMPLE MACRO PARAMETER
DB 'MSG&PARAMETER' ;; Комментарий появится только в определении
INC AX
ENDM
0000 CODE SEGMENT
ASSUME CS:CODE
EXAMPLE %VALUE
0000 4D 53 47 30 1 DB 'MSG0' ;
0004 40 1 INC AX
0005 CODE ENDS
END
Фиг. 6.10 Специальные символы в макрокомандах
Команды INCLUDE
Оператор ассемблера INCLUDE осуществляет вставку текста из другого
файла в транслируемую программу. Оператор INCLUDE особенно удобен в
случае работы с набором макрокоманд, например, макрокомандами
сопроцессора 8087. Все команды сопроцессора 8087 представлены
макрокомандами. Весь такой набор макрокоманд или какое-то его под-
множемтво вам приходится включать в любую транслируемую программу,
где применяется сопроцессор 8087. Однако копировать эти макроко-
манды в каждый исходный файл неудобно. Кроме того, они занимают
столько места, что исходные файлы быстро заполнили бы всю вашу дис-
кету, если бы вы попытались копировать макрокоманды в каждый файл.
Эта проблема решается в языке ассемблера с помощью оператора
INCLUDE. Оператор
ICLUDE имяфайла
считывает указанный файл и включает его как часть в ассемблируемую
программу. Ассемблер помещает нужный файл в то место, где
расположен оператор INCLUDE. Оператор INCLUDE естественно
использовать по отношению к библиотекам макрокоманд, например,
макрокоманд сопроцессора 8087. Вы помещаете оператор INCLUDE в
начало программы - и любая команда сопроцессора 8087 в вашей
программе будет ассемблироваться правильно.
Аналогично, вы можете использовать оператор INCLUDE для
подключения других частей программы. Если вы захотите разбить вашу
программу на меньшие исходные файлы, но ассемблировать ее как один
файл, то главный исходный файл может состоять из операторов INCLUDE
для всех вспомогательных исходных файлов. Однако как было показано
в гл.5, ассемблирование небольших модулей и связывание их с помощью
программы LINK в большинстве случаев предпочтительнее.
Другой вариант для использования оператора INCLUDE - структура
данных. Вы можете использовать некоторую структуру данных в
нескольких программах. Определение этой структуры данных можно
хранить в виде отдельного файла, и с помощью оператора INCLUDE
обращаться к ней из любой программы, где они требуются. Далее в
этой главе мы рассмотрим способы, с помощью которых программа может
задавать и использовать структуры данных.
Если включаемый файл является файлом макроопределений, то не
имеет смысла, чтобы он появлялся в ассемблерном листинге каждый раз
при трансляции программы. Чтобы исключить макроопределения из
листинга, но сохранить их для генерации программного кода, вы
можете воспользоваться условным оператором IF1. Последовательность
IF1
INCLUDE 87MAC.LIB
ENDM
приводит к включению в программу файла 87MAC.LIB при первом
проходе ассемблера. Именно во время первого прохода ассемблер
осуществляет расширение всех макрокоманд до их окончательной формы.
При втором проходе макроопределения ассемблеру не нужны. Это
ускоряет процесс ассемблирования, так как во время второго прохода
файл макрокоманд не считывается. Ассемблер не печатает файл
макрокоманд, так как листинговый файл формируется при втором
проходе. На Фиг. 6.11 показано использование в программе
Microsoft (R) Macro Assembler Version 5.00 1/1/80 01:21:40
Фиг. 6.11 Вставка макроопределений для 8087 Page 1-1
PAGE ,132
TITLE Фиг. 6.11 Вставка макроопределений для 8087
0000 CODE SEGMENT
ASSUME CS:CODE,DS:CODE
0000 ???? TWO_BYTE DW ?
0002 9B DB E0 FENI
0005 9B DE 3E 0000 R FIDIVR TWO_BYTE
000A 9B D9 C1 FLD
000D CODE ENDS
END
Фиг. 6.11 Вставка макроопределений для 8087
последовательности IF1 ... INCLUDE ... ENDM для макрокоманд
сопроцессора 8087. Приведены как исходный, так и листинговый файлы
данной программы. Ассемблер правильно обрабатывает команды
сопроцессора 8087, опуская печать определений соответствующих
макрокоманд.
Сегменты
Ранее уже рассматривался оператор SEGMENT. Теперь есть возможность
рассмотреть его более подробно и исследовать дополнительные
возможности, которые он предоставляет.
До сих пор в большинстве примеров программ присутствовал только
один оператор SEGMENT. Так как программный код должен находиться в
некотором сегменте, то нужно присвоить ему имя. Учитывая, что
ассемблер должен суметь определить адрес сегмента, единственный
оператор ASSUME в прграмме идентифицирует только один сегмент
программы. В подобных случаях возможности сегментации программ
микропроцессора 8088 используются не полностью, но часто это и не
нужно. Если программа и ее данные помещаются в пределах одной и той
же адресуемой области памяти объемом 64 кбайт, то нет необходимости
использовать возможности процессора в сегментации памяти.
Существуют ситуации, когда в программе нужно использовать более
одного оператора SEGMENT. Одно из таких применений рассматривалоясв
гл.5 в нескольких примерах, использующих DOS. В этих примерах в
программе определялся сегмент STACK. Имя, выбранное для сегмента,
несущественно, но его тип, указанный в операторе SEGMENT, должен
быть STACK, так как файлу типа .EXE для выполнения программы
необходимо отвести стековую область. Если в программе не задать
сегмент STACK, то загрузчик DOS сохранит организацию стека в
некотором месте памяти, которое может оказаться неприемлемым. В
этом случае программа может работать недостаточно хорошо.
Другое назначение оператора SEGMENT - расположением данных в
определенном месте памяти. Как известно, при использовании DOS
лучше всего, если программа имеет перемещаемый программный сегмент.
В этом случае нас не заботит, куда DOS загружает программу. Но в
некоторых случаях фактическое расположение команд или данных
оказывается существенным. В этих случаях для задания местоположения
данных можно воспользоваться директивой AT оператора SEGMENT.
Чтобы понять значение указателя AT, рассмотрим пример. В этом
примере программа использует как Отправную точку систему BIOS, хра-
нящаяся в ПЗУ персональной ЭВМ. Хотя язык ассемблера является очень
эффективным средством программирования, с другой стороны это
довольно трудный инструмент, особенно для больших программ. Поэтому
выбор языка ассемблера обусловливается свойствами, которые делают
его выгодным для решения определенной задачи. В случае IBM PC язык
ассемблера - лучший язык для программирования функций, выполняемых
ROM BIOS. Эти функции можно охарактеризовать как управление устрой-
ствами ввода-вывода, где обычно требуется оперировать с отдельными
битами. Программирование подобных задач сводится к возможности ма-
нипулировать содержимым точно заданных ячеек памяти и портов ввода-
вывода. Язык ассемблера также используется в тех случаях, когда
необходима минимизация размера программы или максимальное быстро-
действие программы. Всем эти требования предъявляет и система ROM
BIOS.
В рассматриваемом примере используется часть BIOS. В одной из
последующих глав будет рассмотрено, как заменять части системы
BIOS. Однако в данном случае нас интересует доступ к наборам
данных, которые использует ROM BIOS. Если вы посмотрите
ассемблерный листинг для ROM BIOS (он приводится в приложении A
технического руководства по IBM PC), то увидите, что сегмент DATA
располагается в сегменте 40H или по абсолютному адресу 400H.
Приведенная на Фиг. 6.12 программа обращается в область данных ПЗУ
системы BIOS c определенной целью. В сегменте DATA имеется
переменная KB_FLAG, которая указывает текущее состояние
переключателя регистров. Одна из жалоб, часто высказываемых по
поводу клавиатуры IBM, состоит в том, что неизвестно, работаете ли
вы в верхнем регистре (CAPS LOCK) или в нижнем. Программа на Фиг.
6.12 считывает значение бита, соответствующего CAPS LOCK, и
изображает его в верхнем правом углу цветного графического дисплея.
Хотя в данной программе это не реализовано, мы будем предполагать,
что при реальном использовании этого фрагмента программы, верхний
правый угол экрана зарезервируется для описанного индикатора.
Сегмент DATA на Фиг. 6.12 показывает, как программист может
передать в программу информацию, расположенную по абсолютным адре-
сам. Оператор DATA SEGMENT использует директиву AT для того, чтобы
обеспечить безусловную привязку данного сегмента к параграфу 40H.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:25
Фиг. 6.12 Использование сегментов Page 1-1
PAGE ,132
TITLE Фиг. 6.12 Использование сегментов
0000 DATA SEGMENT AT 40H
0017 ORG 17H
0017 ?? KB_FLAG DB ?
= 0040 CAPS_STATE EQU 40H
0018 DATA ENDS
0000 VIDEO SEGMENT AT 0B800H
009E ORG 158
009E ?? INDICATOR DB ?
009F VIDEO ENDS
0000 CODE SEGMENT
ASSUME CS:CODE
0000 CAPS PROC FAR
0000 1E START: PUSH DS ; Адрес возврата
0001 B8 0000 MOV AX,0
0004 50 PUSH AX
0005 B8 ---- R MOV AX,DATA ; Адрес сегмента DATA
0008 8E D8 MOV DS,AX
ASSUME DS:DATA
000A B8 ---- R MOV AX,VIDEO ; Адрес сегмента VIDEO
000D 8E C0 MOV ES,AX
Фиг. 6.12 Расположение сегмента (начало)
ASSUME ES:VIDEO
000F DISPLAY_CAPS:
000F B0 18 MOV AL,18H ; Символ "стрелка вверх" имеет код 18H
0011 F6 06 0017 R 40 TEST KB_FLAG,CAPS_STATE ; Определение состояния клавиши CAPS
0016 75 02 JNZ CAPS_LOCK
0018 B0 19 MOV AL,19H ; Символ "стрелка вниз" имеет код 19H
001A CAPS_LOCK:
001A 26: A2 009E R MOV INDICATOR,AL ; Вывод в верхний левый угол экрана
001E B4 06 MOV AH,6 ; Функция ДОС ввода с клавиатуры
; и вывода на дисплей
0020 B2 FF MOV DL,0FFH ; Направление - ввод с клавиатуры
0022 CD 21 INT 21H
0024 3C 00 CMP AL,0 ; Проверка на наличие символа
0026 74 E7 JZ DISPLAY_CAPS ; Нет символа
0028 3C 25 CMP AL,'%' ; Проверка на символ конца
002A 74 08 JE RETURN
002C B4 02 MOV AH,2 ; Функция вывода на дисплей
002E 8A D0 MOV DL,AL ; Выводимый символ
0030 CD 21 INT 21H
0032 EB DB JMP DISPLAY_CAPS ; Повторение
0034 RETURN:
0034 CB RET ; Возврат в ДОС
0035 CAPS ENDP
0035 CODE ENDS
END START
Фиг. 6.12 Расположение сегмента (продолжение)
Просматривая листинг ROM BIOS, мы находим переменную KB_FLAG со
смещением 17H в сегменте DATA. Оператор ORG 17H данной программы
задает смещение этой переменной в оттранслированной программе.
Наконец, смысл оператора EQU, определяющего константу CAPS_STATE
следует непосредственно из листинга BIOS ПЗУ. Заданный этой
константой бит указывает текущее состояние переключателя CAPS LOCK.
В приведенной на Фиг. 6.12 программе имеется еще один оператор
SEGMENT. Он определяет сегмент VIDEO с адресом 0B800H. Это
сегментный адрес буфера для адаптера цветного- графического
дисплея. Этот адрес нужен для вывода состояния индикатора на экран
дисплея. Если мы хотим поместить символ в правый верхний угол
экрана, при условии, что строка на экране содержит 80 символов, то
смещение соответствующей ячейки должно быть равно 158 в десятичном
представлении. Программируемые характеристики оборудвания ПК
описываются в гл.8, а пока вы можете принять сказанное на веру.
Первая часть программы устанавливает необходимую адресацию
сегментов. Регистр DS указывает на сегмент DATA, а регистр ES - на
сегмент VIDEO. Хотя в программе эти сегменты объявлены директивой
AT абсолютными, ассемблер все же обозначает их значком "R", как
перемещаемые. Программа LINK, тем не менее, подставляет в
соответствующие поля данных правильные значения.
Программа тестирует переменную KB_FLAG, а ассемблер в
результате генерирует правильное смещение, равное 17H. В данном
примере символ стрелка вниз используется для обозначения обычного
режима, а стрелка вверх обозначает режим CAPS LOCK. Введенные с
клавиатуры символы считываются программой с помощью функции DOS,
выводящей эим символя на дисплей. В данном примере для выхода из
программы был произвольно выбран символ %. Если пользователь вводит
любой другой символ, то программа выводит его на дисплей и
возвращается к ожиданию ввода следующих.
Если ввести и запустить данную программу, то вы увидите в
верхнем правом углу цветного графического дисплея направленную вниз
или вверх стрелку. Если для цветного дисплея установлен режим 40
символов в строке, при выполнении данной программы
стрелка-индикатор будет выводиться во второй сверху строке. Если
нужно использовать эту программу с адаптером монохромного дисплея,
то измените адрес сегмента VIDEO на адрес 0B000H, соответственно
местоположению буфера монохромного дисплея.
При выполнении данной программы с адаптером цветного
графического дисплея в режиме 80 символов в строке вы увидите на
экране сильную помеху, "снег". Эта интерференция на экране
происходит из-за прямой передачи данных из программы в буфер
дисплея. В случае монохромного адаптера или цветного-графического
дисплея в режиме 40 символов в строке этой помехи не будет. О
причинах этого эффекта и о том, как его избежать, мы узнаем при
рассмотрении аппаратного обеспечения IBM PC.
Существуют и другие применения нескольких операторов SEGMENT в
одной программе. Если программе требуется область данных объмом
более 64 кбайт, то она должна организовать доступ к этим данным.
Как правило, вы воспользуетесь для обращения к этой области данных
некоторой схемой управления памятью. В такой ситуации вам будет
доступна вся эта область данных (за исключением некоторых
фиксированных участков) косвенную адресацию.
В качестве примера рассмотрим, как интерпретатор команд DOS
загружает программы. DOS загружает транзитную программу на границу
параграфа сразу за резидентной частью DOS. Размер этой резидентной
части может варьироваться в зависимости от числа дисководов в
системе. Кроме того, этот размер может существенно возрастать при
использовании в DOS прерывания INT 27H, которое заканчивает
выполнение программы, но оставляет ее резидентной в памяти. При
этом программный загрузчик DOS должен адресоваться к сегментному
префиксу PSP той программы, которую он загружает. Проще всего
задать эту структуру данных с помощью отдельного оператора SEGMENT.
На Фиг. 6.13 показано объявление сегмента, которое можно
использовать в двух различных местах. Если бы можно было посмотреть
текст исходной программы для загрузчика DOS, то мы бы обнаружили
там подобное объявление. В случае программы, использующей структуру
.EXE, такая сегментация могла бы обеспечить доступ к переменным в
сегментном префиксе PSP. В приведенном на Фиг. 5.6 примере
программы с применением функций DOS, использовалась структура файла
типа .COM. Это позволяло нам обращаться к различным ячейкам
сегмента PSP через смещение относительно блока PSP. Задача весьма
облегчалась тем, что DOS загружала программу в тот сегмент, который
содержал PSP.
В случае .EXE-файла блок PSP находится не в том же сегменте,
что и команды программы. Так как при передаче управления программе
типа .EXE DOS устанавливает регистры DS и ES на сегмент PSP, то
имеет смысл обращаться с PSP как с отдельным сегментом. Приведенный
на Фиг. 6.13 фрагмент программы из сегмента CODE, показывает, как
можно обращаться к данным в блоке PSP.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:31
Фиг. 6.13 Структура Программного Префикса Page 1-1
PAGE ,132
TITLE Фиг. 6.13 Структура Программного Префикса
0000 PROGRAM_SEGMENT_PREFIX SEGMENT
0000 0002[ INT_20 DB 2 DUP (?)
??
]
0002 ???? MEMORY_SIZE DW ?
0004 0005[ LONG_CALL DB 5 DUP (?)
??
]
0009 ???????? TERMINATE_ADDR DD ?
000D ???????? CTRL_BREAK DD ?
005C ORG 05CH
005C 0010[ FCB1 DB 16 DUP (?)
??
]
006C ORG 06CH
006C 0010[ FCB2 DB 16 DUP (?)
??
]
0080 ORG 080H
0080 0080[ DTA DB 128 DUP (?)
??
]
0100 PROGRAM_SEGMENT_PREFIX ENDS
0000 CODE SEGMENT
ASSUME CS:CODE,DS:PROGRAM_SEGMENT_PREFIX
0000 A1 0002 R MOV AX,MEMORY_SIZE
0003 CODE ENDS
END
Фиг. 6.13 Префикс программного сегмента
Обратите внимание, что сегмент PSP на Фиг. 6.13 на самом деле
не содержит никаких значений для переменных. Например, мы знаем,
что в первых двух байтах PSP содержится код прерывания INT 20H.
Однако мы решили показать, что в этом месте находится поле длиной 2
байта без каких-либо указаний о содержащихся там значениях. Мы
должны делать именно так, чтобы в результате нашего описания
сегмента ни редактор связей, ни загрузчик не попытались записать в
память каких-либо данных. Фактически, мы используем этот сегмент
как средство для объявления данных. Оператор SEGMENT объявляет
структуру данных, которую мы называем префиксом программного
сегмента. Ее местоположение в памяти не фиксировано, а определяется
одним из сегментных регистров. В нашем примере на Фиг. 6.13 это
местоположение определяется регистром DS.
Точно такой же способ можно использовать для обозначения любой
структуры данных, которая может быть расположена в произвольном
месте памяти микропроцессора 8088. Эта структура данных может быть,
например, болком управления для операционной системы, либо строкой
текста для текстового редактора, или даже блоком параметров
конкретной подпрограммы. Каждый объект структуры данных
располагается в своем отдельном сегменте. Таким образом, при
обращении программы к каждому элементу структуры данных сегментный
регистр указывает на начало (или на близкую к началу точку) этого
элемента. Программа не обращается к двум различным элементам с
одним и тем же значением сегментного регистра. Для каждого элемента
всегда устанавливается свой адрес сегмента.
Здесь следует немного остановиться на том, какие вообще есть
методы распределения памяти микропроцессора 8088. IBM PC с
микропроцессором 8088 может адресовать до 1Мбайт оперативной
памяти, но один сегмент может охватывать не более 64 кбайт. Даже с
четырьмя сегментными регистрами программа не имеет возможности
охватить всю память, не используя некоторых способов сегментации.
Если все данные помещаются в 64К, то нет нужды волноваться:
просто поместите все данные в один сегмент. Если же мы полагаем,
что программе требуется область данных, превышающая 64К, то нам
придется решать задачу распределения памяти. При этом возможны две
стратегии. В обоих случаях мы будем предполагать, что вся
совокупность данных может быть разбита на меньшие блоки (такие как
отдельные переменные, строки текста, управляющие блоки или
массивы), объемом не более 64К каждый.
Первый метод распределения памяти применяется в ситуации, когда
вашей главной заботой является экономия памяти. При этом методе вы
располагаете объекты данных в первых же свободных участках памяти.
Программа, управляющая доступом к областям данных, должна при этом
для каждой переменной использовать четырехбайтовый указатель. Из
них два байта используется для смещения и еще два байта для значе-
ния сегмента. Когда программе нужно полусить доступ к данным, она
извлекает адрес из области хранения адресов с помощью команд LDS
или LES. Если вам требуется еще большая экономия памяти, то вы
фактически можете хранить указатель в трехбайтовом поле. Два байта
содержат адрес сегмента данных, а оставшийся байт содержит смещение
данного объекта внутри сегмента. Начальное смещение всегда будет
иметь значение от 0 до 15, так как значение сегмента всегда кратно
16.
Хотя описанный метод наиболее эффективен в отношении объема
памяти, занимаемой данными, у него имеются пара недостатков.
Максимальная длина объекта данных немного меньше, чем 64 кбайт. В
рамках данной стратегии наихудшим окажется случай, когда абсолютный
адрес объекта данных кончается на 0FH. Так как максимальное
значение смещения в любом сегменте равно 0FFFFH, то максимальная
длина переменной будет 64К - 15, или 65521 байт. Второй недостаток
этого метода связан с затратами памяти для хранения указателей к
объектам данных. При большом числе объектов для хранения наряду с
ними всех четырехбайтовых (или трехбайтовых) указателей потребуется
много памяти.
Примером использования описанного метода распределения памяти
может служить блок управления файлом FCB. В последнем примере
работающей с DOS программы мы располагали блок FCB в произвольном
месте программы. Какого-либо выравнивания местоположения этой
структуры данных не производилось. Затем при обращении к DOS для
выполнения файловой операции программе понадобился четырехбайтовый
указатель. Идентификация блока FCB для DOS осуществлялось парой
регистров DS:DX.
При втором методе распределиня памяти все объекты данных
располагаются на границах параграфов. Это сразу же упрощает
указатель, определяющий объект данных. Этот указатель состоит
только из двух байтов, которые определяют местонахождение сегмента
с этими данными. Так как распределение памяти всегда начинается с
границы параграфа, то начальное смещение данных будет всегда равно
нулю. Однако при таком методе, расходуется дополнительная память.
Каждый раз, когда вы располагаете в памяти новый объект, возможна
потеря до 15 байт памяти. Это происходит, если последний байт
предыдущего объекта попадает точно на границу параграфа. Так как
граница следующего параграфа будет через 15 байт, то эти 15 байт в
промежутке теряются. Кроме того, при такой стратегии минимальная
длина объекта равна 16 байт. Даже если данные будут занимать меньше
места, оставшиеся байты все равно не могут быть использованы.
Как было отмечено, второй метод распределения памяти
используется загрузчиком DOS при запуске программ. DOS загружает
программу на ближайшую границу параграфа. Так как DOS исходит из
того, что в памяти располагается мало больших по размерам объектов,
то при данном методе издержки памяти будут невелики. Однако, если
ваша прикладная программа использует много небольших объектов, то
выравнивание по параграфам может оказаться слишком дорогим.
Второй метод распределения памяти, использующий выравнивание по
параграфам, позволяет определять области данных с помощью структуры
SEGMENT. Если же хотите использовать первый метод распределения
памяти, то вам потребуется другой способ определения структур
данных. Такой способ объявления данных как раз рассматривается в
следующем разделе.
Структуры
Структура данных - это организация данных, которая имеет для
программиста определенный смысл. Как показывает опыт, мы определяем
структуры данных когда одна и та же совокупность данных
используется более чем одной программой или программистами.
Благодаря определению, обе стороны имеют четкий образ этих данных.
Если программа A передает некотрые данные программе B, то
определение структуры данных гарантирует, что каждая из программ
ищет данные в одном и том же месте.
У нас уже был хороший пример структуры данных. Блок управления
файлом FCB является структурой данных. Блок FCB используется
программами для обмена информацией о файле с DOS. В блоке FCB
содержатся такие важные данные об обрабатываемом файле, как номер
текущей записи, длина файла и т.д. Кроме того, в блоке FCB имеется
зарезервированное поле, которое содержит информацию, используемую
только DOS. В блоке FCB находится вся информация, необходимая для
DOS и прикладных программ. Эта структура данных служит для передачи
параметров файла между DOS и прикладной программой.
Теперь нужно найти такой способ определения структур данных,
чтобы программа могла с удобством ими пользоваться. В
Макроассемблере фирмы IBM имеется оператор STRUC, позволяющий
определять структуру данных. С точки зрения программиста структура
данных выглядит как еще один сегмент. Определение данных
ассемблируется так же, как и обычные операторы данных, и описание
структуры, как и описание сегмента, заканчивается оператором ENDS.
Однако в действительности структура не генерирует данные. Оператор
STRUC определяет структуру данных для ассемблера. В дальнейшем имя
этой структуры данных используется в ассемблируемой программе для
генерации соответствующей области данных.
Если рассматривать оператор STRUC описанным выше образом, то он
больше похож на оператор MACRO. Программа определяет структуру
данных в одном месте, а ее вызов осуществляет позднее. Фактическая
генерация данных происходит при вызове структуры. Фиг. 6.14 поможет
понять работу оператора STRUC.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:36
Фиг. 6.14 Структуры Page 1-1
PAGE ,132
TITLE Фиг. 6.14 Структуры
FCB STRUC
0000 00 DRIVE DB 0 ; Номер устройства
0001 20 20 20 20 20 20 20 FILE_NAME DB ' ' ; Имя файла
20
0009 20 20 20 FILE_EXT DB ' ' ; Тип файла
000C 0000 CURRENT_BLOCK DW 0 ; Номер текущего блока
000E 0080 RECORD_SIZE DW 80H ; Логический размер записи
0010 00000000 FILE_SIZE DD 0 ; Размер файла в байтах
0014 0000 DATE DW 0 ; Дата последнего изменения
Фиг. 6.14 Структуры (начало)
0016 000A[ RESERVED DB 10 DUP (?) ; Зарезервировано ДОС
??
]
0020 00 SEQ_NUMBER DB 0 ; Номер текущей записи
0021 00000000 RANDOM_NUMBER DD 0 ; Номер записи при прямом
; доступе
0025 FCB ENDS
0000 STACK SEGMENT STACK
0000 0040[ DW 64 DUP (?)
????
]
0080 STACK ENDS
0000 CODE SEGMENT
ASSUME CS:CODE
0000 01 INPUT FCB <1,'FIG6-14','INP'>
0001 464947362D313420
0009 494E50
000C 0000
000E 0080
0010 00000000
0014 0000
0016 000A[
??
]
0020 00
0021 00000000
0025 02 OUTPUT FCB <2,'EXAMPLE','TST'>
0026 4558414D504C4520
002E 545354
0031 0000
0033 0080
0035 00000000
0039 0000
003B 000A[
??
]
0045 00
0046 00000000
004A STRUCTURES PROC FAR
004A 1E PUSH DS ; Установка адреса возврата
004B B8 0000 MOV AX,0
004E 50 PUSH AX
004F 0E PUSH CS ; Установка DS на сегмент CODE
0050 1F POP DS
ASSUME DS:CODE
0051 8D 16 0000 R LEA DX,INPUT ; Открытие вводимого файл
Фиг. 6.14 Структуры (продолжение)
0055 B4 0F MOV AH,0FH
0057 CD 21 INT 21H
0059 8D 16 0025 R LEA DX,OUTPUT ; Создание выводимого файла
005D B4 16 MOV AH,16H
005F CD 21 INT 21H
0061 8D 1E 0000 R LEA BX,INPUT
0065 C7 47 0E 0010 MOV [BX].RECORD_SIZE,16 ; Установка размера записи для ввода
006A C6 47 20 01 MOV [BX].SEQ_NUMBER,1 ; Пропуск первой записи
006E C7 06 0033 R 0010 MOV OUTPUT.RECORD_SIZE,16 ; Установка размера записи для
; вывода
0074 8D 16 0000 R LEA DX,INPUT ; Чтение второй записи из файла
0078 B4 14 MOV AH,14H
007A CD 21 INT 21H
007C 8D 16 0025 R LEA DX,OUTPUT ; Вывод введенной записи
0080 B4 15 MOV AH,15H
0082 CD 21 INT 21H
0084 B4 10 MOV AH,10H ; Закрытие выводимого файла
0086 CD 21 INT 21H
0088 CB RET
0089 STRUCTURES ENDP
0089 CODE ENDS
END
Microsoft (R) Macro Assembler Version 4.00 4/16/89 23:15:19
Фиг. 6.14 Структуры Symbols-1
Structures and Records:
N a m e Width # fields
Shift Width Mask Initial
FCB . . . . . . . . . . . . . . 0025 000A
DRIVE . . . . . . . . . . . . 0000
FILE_NAME . . . . . . . . . . 0001
FILE_EXT . . . . . . . . . . . 0009
CURRENT_BLOCK . . . . . . . . 000C
RECORD_SIZE . . . . . . . . . 000E
FILE_SIZE . . . . . . . . . . 0010
DATE . . . . . . . . . . . . . 0014
RESERVED . . . . . . . . . . . 0016
SEQ_NUMBER . . . . . . . . . . 0020
RANDOM_NUMBER . . . . . . . . 0021
Фиг. 6.14 Структуры (продолжение)
На Фиг. 6.14 приведена очень простая программа, которая
использует файлы системы DOS. Эта программа открывает файл DOS на
носителе в дисководе A:, считывает вторую запись этого файла и
записывает ее в файл на носителе в дисководе B:. Маловероятно,
чтобы вы когда-нибудь применили эту программу чтобы сделать
что-либо существенное, но сейчас она дает нам возможность
использовать структуру данных для блока FCB.
Первая часть программы на Фиг. 6.14 определяет структуру данных
FCB. Оператор языка ассемблера STRUC отмечает начало определения
структуры. Метка FCB является здесь именем данной конкретной
структуры. В примере определяется каждое поле структуры данных FCB.
Обратите внимание, что в столбцах слева ассемблер генерирует
объектный код данной структуры. Однако, при редактировании связей
ассемблированного объектного кода, область данных в программе
отсутствует. Ассемблер распечаьываеь данную структуру данных в
оттранслированном виде исключительно для вашего сведения.
Точно так же, как и вслучае макрокоманды, имя FCB становится
как бы новым оператором языка ассемблера. Первым оператором в
сегменте CODE является вызов структуры данных FCB. В примере этой
структуре данных присваивается имя INPUT. Данная структура FCB
идентифицирует входной набор данных. Заметьте, что в этом операторе
FCB имеются операнды. Они заменяют или перекрывают значения,
которые были включены в исходное определение структуры данных.
Если мы сравним объектный код определения структуры FCB с
объектным кодом структуры INPUT, то увидим, что они различаются по
значениям первых трех полей. В определении структуры данных поле
DRIVE равно 0, в структуре INPUT - 1. Первый операнд в угловых
скобках определения структуры INPUT равен 1. Это значение заменяет
исходно определенное значение 0. Аналогично в данном примере
изменяются значения второго и третьего полей, относящихся к имени
файла. Закрывающая угловая скобка в определении структуры INPUT
завершает процедуру замены значений полей структуры данных.
Оставшаяся часть структуры INPUT идентична определеной в структуре
данных FCB.
Программа может изменять любой из полей структуры FCB, если
определение этого поля содержит только один элемент. В
рассматриваемом примере программа может изменять любое поле
структруыр FCB, за исключением поля RESERVED. Мы определили это
поле поле как 10 отдельных элементов, и оно не может быть изменено.
Аналогично, если поле определено оператором
DB 10,20
то его нельзя перекрыть. При вызове структуры можно изменять только
поля, состоящие из единственного элемента. Символьная строка,
включающая несколько символов, рассматривается ассемблером как один
элемент. В данном примере поле FILE_NAME содержит несколько
символов, но является одним элементом, значение которого можно
изменить.
Операнды, перечисленные в угловых скобках, заменяют операнды,
входящие в определение, по позиционному принципу, как и в
макрокоманде. Если вы не хотите модифицировать заданное в
определении значение, но желаете изменить следующее за ним поле, то
в список значений нужно включить пустой параметр. Например, для
модификации полей FILE_NAME и CURRENT_BLOCK, оставляя в то же время
по умолчанию заданные значения полей DRIVE и FILE_EXT, структуру
FCB следует вызвать оператором:
EXAMPLE FCB <,NEWNAME,,12>
Первый параметр пуст, так что ассемблер использует для него
значение по умолчанию. В следующем поле NEWNAME заменяет строку
пробелов. Для значения поля FILE_EXT по умолчанию используется
строка пробелов, и наконец, нулевое значение поля CURRENT_BLOCK
заменяется на 12.
В следующей исходной программной строке на Фиг. 6.14 программа
определяет структуру FCB под именем OUTPUT для выходного файла.
Здесь снова модифицируются первые три поля определения данных путем
включения новых значений в поля операндов оператора FCB.
Выигрыш от использования структур данных проявляется в
действительных командах программы. Программа может ссылаться на
имена INPUT и OUTPUT так же, как и на любые другие метки в
программе. Вы можете видеть это, в том участке программы, где
открывается входной файл INPUT и оператор
LEA DX,INPUT
используется для установки адреса входной структуры данных FCB.
В программе можно использовать каждое из указанных полей
структуры данных. Значению каждого имени соответствует смещение
какого-либо поля в структуре данных. Например, программа помещает в
регистр BX адрес FCB INPUT. После этого программа обращается к
полям RECORD_SIZE и SEQ_NUMBER в режиме адресации по базе. Так как
регистр BX уже указывает на структуру данных FCB, то нужно задать
смещение относительно этой базы. Способ адресации
[BX].RECORD_SIZ
указывает ассемблеру, что в команде, которую он должен
сгенерировать, смещение поля RECORD_SIZE складывается со значением
базы, хранящимся в регистре BX. Если вы рассмотрите соответствующие
команды на машинном языке, то увидите, что в них присутствуют
смещения для полей RECORD_SIZE (0EH) и SEQ_NUMBER (20H). Символ "."
идентифицирует имена полей как смещения в структуре данных.
Помимо режимов адресации по базе и индексу можно использовать
структуру данных и для прямой адресации. Следующий участок
программы непосредственно изменяет поле RECORD_SIZE в FCB OUTPUT.
Программа именует это поле OUTPUT.RECORD_SIZE. Имя OUTPUT
определяет конкретную структуру данных, а RECORD_SIZE - имя поля в
этой структуре данных.
Прежде, чем покончить с этим примером, посмотрим, какой
информацией о структуре данных располагает ассемблер. Фиг. 6.14
включает в себя фрагмент таблицы символических имен. Ассемблер
выделяет один из разделов этой таблицы для структур и записей.
Ассемблер показывает вам всю информацию о структуре данных, которой
располагает. Этот раздел имеет заголовок "Structures and records"
("Структуры и записи"). В первой строке этого раздела для структуры
FCB из нашего примера указано, что эта структура имеет длину 25H
байт и содержит 0AH полей. Далее ассемблер перечисляет все эти
поля, печатая их с отступом по отношению к имени структуры. Для
каждого из полей приводится значение соответствующего смещения. Для
структур ассемблер использует два столбца, обозначеные "width"
(ширина) и "#dfields" (число полей). Вторая строка в меток колонок
используется для записей. Структуры данных, которые ассемблер
интерпретирует как записи, будут рассмотрены в следующем разделе.
Программа, приведенная на Фиг. 6.14, не делает ничего
полезного. Кроме того, в ней нет никакой обработки ошибок. Однако
она хорошо иллюстрирует применение оператора STRUC. Этот способ
определения данных особенно подходит для часто используемых
структур данных. Использование имен полей в качестве значений
смещений очень удобно при модификации структур данных на этапе
разработки программы. Если вы внесете изменения в структуру данных,
то ассемблер автоматически изменит значения смещений при повторном
ассемблировании программы. Кроме того, использование структур
данных делает программу на языке ассемблера более читабельной и
понятной.
Записи
Рассмотренные в предыдущем разделе структуры предназначены для
многобайтовых данных. Но в некоторых случаях требуется побитовое
определение объектов данных. Для таких случаев в Макроассемблере
имеется механизм описания данных, который называют записью
(RECORD). Действие оператора RECORD аналогичны действиям операторов
STRUC и MACRO. Оператор RECORD задает определенную конфигурацию
данных. Присвоенное записи имя становится для ассемблера еще одним
оператором. Вы можете использовать это имя записи для задания
специальных конфигураций данных. От оператора STRUC оператор RECORD
отличается тем, что он определяет объекты на уровне отдельных
битов. Каждому из полей оператор RECORD присваивает имя и указывает
его ширину в битах. Оператор RECORD можно использовать для
формирования битовых полей длиной до 16 бит.
Здесь мы опять воспользуемся примером. На Фиг. 6.15 приведена
еще одна ничего не делающая программа, связанная с установоением
даты изменения файла. В определении Блока управления файлом
содержится 16-битовое поле, в котором содержится дата формирования
или последнего изменения данного файла операционной системой. При
открыти файла DOS заполняет это поле в блоке FCB на основе
информации из каталога дискеты. В 16-ти батих поля даты
закодированы год, месяц и число. Из приведенного на Фиг. 6.15
оператора RECORD видно строение этого слова данных.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:43
Фиг. 6.15 Записи Page 1-1
PAGE ,132
TITLE Фиг. 6.15 Записи
DATE_WORD RECORD YEAR:7,MONTH:4,DAY:5
0000 STACK SEGMENT STACK
0000 0040[ DW 64 DUP (?)
????
]
0080 STACK ENDS
0000 CODE SEGMENT
ASSUME CS:CODE
0000 FCB LABEL BYTE
0000 01 DRIVE DB 1 ; Номер устройства
0001 46 49 47 36 2D 31 35 FILE_NAME DB 'FIG6-15 ' ; Имя файла
20
0009 41 53 4D FILE_EXT DB 'ASM' ; Тип файла
000C 0000 CURRENT_BLOCK DW 0 ; Номер текущего блока
000E 0080 RECORD_SIZE DW 80H ; Логический размер записи
0010 00000000 FILE_SIZE DD 0 ; Размер файла в байтах
0014 0000 DATE DATE_WORD <> ; Дата последнего изменения
0016 000A[ RESERVED DB 10 DUP (?) ; Зарезервировано ДОС
??
]
0020 00 SEQ_NUMBER DB 0 ; Номер текущей записи
0021 00000000 RANDOM_NUMBER DD 0 ; Номер записи при прямом
0025 RECORDS PROC FAR
0025 1E PUSH DS ; Адрес возврата
0026 B8 0000 MOV AX,0
0029 50 PUSH AX
002A 0E PUSH CS ; Установка DS на сегмент CODE
002B 1F POP DS
ASSUME DS:CODE
002C 8D 16 0000 R LEA DX,FCB ; Открытие файла
0030 B4 0F MOV AH,0FH
0032 CD 21 INT 21H
0034 A1 0014 R MOV AX,DATE
0037 25 FE00 AND AX,MASK YEAR ; Выделение года
003A B1 09 MOV CL,YEAR ; Сдвиг вправо
003C D3 E8 SHR AX,CL
003E 8A F8 MOV BH,AL ; Сохранение года в регистре BH
0040 A1 0014 R MOV AX,DATE
0043 25 01E0 AND AX,MASK MONTH
0046 B1 05 MOV CL,MONTH
0048 D3 E8 SHR AX,CL
004A 8A D8 MOV BL,AL ; Сохранение месяца в регистре BL
Фиг. 6.15 Записи (начало)
004C A1 0014 R MOV AX,DATE
004F 25 001F AND AX,MASK DAY ; Сохранение дня в регистре AL
0052 CB RET
0053 RECORDS ENDP
0053 CODE ENDS
END RECORDS
Фиг. 6.15 Записи (продолжение)
В примере на Фиг. 6.15 записи присваивается имя DATE_WORD. В
поле операндов оператора RECORD видно, что в записи DATE_WORD
имеется три поля. Первые 7 бит содержат год (YEAR), следующие 4
бита - месяц (MONTH) и последние 5 бит - число (DAY). Так же, как и
в случае оператора MACRO, данная исходная программа просто
определяет тип конкретной записи, названной DATE_WORD. Оператор
RECORD не формирует соответствующих данных в памяти до тех пор,
пока вы не используете имя записи в качестве ассемблерного
оператора.
В рассматриваемом примере запись DATE формируется в блоке FCB с
использованием определения записи DATE_WORD. Метка DATE обозначает,
а запись DATE_WORD формирует эту 16-битовую область данных. Как мы
увидим ниже, существуют способы определения для полей записей
значений по умолчанию и их изменения. В данном примере мы ничего не
делали для изменения стандартных нулевых значений каждого из
полей.
Приведенная на Фиг. 6.15 программа иллюстрирует не только
структуру оператора RECORD, но и некоторые операции, которые
упрощаются благодаря этому оператору. В первой части программы
открывается файл, имя которго указано в блоке FCB. Остальная часть
программы извлекает из FCB информацию о дате и перебрасывает
отдельные поля в регистры микропроцессора 8088.
Во-первых, программа выделяет в дате из блока FCB значение
года: переслав слово данных DATE в регистр AX, программа обнуляет в
нем командой AND значения, относящиеся к месяцу и числу. Обратите
здесь внимание на непосредственный операнд MASK YEAR. Так как YEAR
- это поле записи, то оператор MASK возвращает значение, которое
выделяет в данном слове поле YEAR. В данном случае это значение
равно 0FE00H. Первые 7 бит в нем - единицы, а остальные биты
обнулены. Это значение маски соответстувет битам, которые формируют
значения YEAR в слове данных. В результате логического умножения
данного значения и всей остальной записи остается только поле
YEAR.
Следующими командами программа перемещает поле YEAR в правый
конец слова. Полю YEAR соответствует значение, равное сдвигу,
который необходим для перемещения данного поля до правой границы
слова. В данном случае это значение равно девяти. Сдвиг вправо на 9
бит обращает значение года в число в регистре AL. (Следует помнить,
что DOS кодирует год в виде числа в перделах от 0 до 119. Эти
значения соответствуют годам от 1980 до 2099).
В последующей группе команд программа выделяет из записи поле
MONTH. Здесь также используются оператор MASK и значения сдвигов,
из записи DATE_WORD. Подобным образом программа выбирает из записи
значение поля DAY.
В данном примере не выполняется никакой полезной работы, так
как записанные в регистры значения оказываются потерянными при
возвращении управления DOS. Однако вы можете запустить программу
через отладчик и установить точку прерывания на команде возврата.
Отладчик выводит на дисплей содержимое регистров BH, BL и AL, так
что вы можете увидеть дату. Более практичная программа после
считывания значений, относящихся к дате, преобразовывала бы их в
код ASCII для вывода на экран. Либо вы могли бы оформить нашу
программу как процедуру, которая вырабатывает информацию о дате для
другой программу.
Имеется еще несколько особенностей операции RECORD, которые
следует рассмотреть. На Фиг. 6.15 приведен фрагмент таблицы
символических имен из ассемблерного листинга. В этой таблице
содержится информация, которая имеется у ассемблера о каждом из
полей записи. В этой таблице нас будет интересовать второй ряд
заголовков: "Shift Width Mask Initial" (Сдвиг Длина Маска Начальное
значение"). Как видно из таблицы символических имен, запись
DATE_WORD имеет длину 16 бит и состоит из трех полей. Каждое поле
имеет четые атрибута. Значение сдвига равно числу битов в записи,
остающихся справа от поля. Это значение указывает ассемблеру,
насколько нужно сдвинуть данное поле, чтобы выровнять его на правый
край. Значение маски служит для выделения поля в записи. Цифра 1 в
слове маски показывает, что соответствующая позиция относится к
данному полю.
Ассемблер может оперировать значением длины любого поля записи.
Вы можете задать длину поля при ассемблировании с помощью оператора
WIDTH. Например, команда
MOV AL,WIDTH YEAR
в нашем примере помещает в регистр AL значение, равное семи.
Столбец начальных значений в таблице символических имен
показывает, какие значения ассемблер вставляет при формировании
записи. Можно задавать записи с начальными значениями, отличными от
нуля. Имеется также возможность изменить эти значения при генерации
области данных. Для задания начальных значений вы после указания в
операторе RECORD каждого из полей ставите знак равенства и
соответствующее значение. Запись DATE_WORD с начальными значениями,
соответствующими 1 января 1983 года, будет иметь вид:
DATE_WORD RECORD YEAR:7=3, MONTH:4=1, DAY:5=1
Эти значения можно переназаначить точно таким же способом, как
и в случае структур. Когда вы формируете запись, в угловых скобках
содержатся конкретные значения для данной генерации. Если нужно,
чтобы дата была 5 января 1984 года, то вы можете сгенерировать
запись следующим образом:
DATE DATE_WORD <4,,5>
Как и в случае макрокоманд или структур, параметры здесь
являются позиционно-зависимыми. Так как параметр, соответствующий
месяцу, не указан, то ассемблер воспользуется для него начальным
значением, указанным в операторе RECORD.
Обратите внимание, что программа на Фиг. 6.15 определяет блок
FCB, не полбзуясь оператором STRUC, рассмотренным в предыдущем
разделе. Мы не могли воспользоваться оператором STRUC, потому что
данные каждого поля структуры должны определяться одним из
операторов типа DEFINE. Мы не можем использовать имя записи в
качестве одного из полей структуры. Тот способ, который мы
применили на Фиг. 6.15, - один из позволяющих обойти эту
трудность.
Можно решить эту же задачу другим способом. Хотя ассемблер и не
генерирует область данных, пока имя записи не использовано в
программе в качестве оператора, но он хранит описания полей,
указанных в операторе RECORD. Это позволяет определить в программе
запись DATE_WORD, не используя ее для задания поля DATE структуры
данных. Это все равно, что определить макрокоманду, но не вызывать
ее. Остальная часть программы остается без изменений. Имена
различных полей записи DATE_WORD имеют в программе смысл и могут
использоваться как параметры при сдвиге и маскировании.
То же самое верно и для оператора STRUC. Определение структуры
задает для ассемблера значения смещений, даже если вы не вызываете
затем эту структуру для формирования области данных. Вы можете
воспользоваться этим для размещения блока FCB по умолчанию по
адресу 05CH в префиксе программного сегмента. Так как блок FCB
существует всегда, то нет необходимости использовать эту структуру
для генерации соответствующей области данных. Программы приведенные
на Фиг. 6.16 и 6.15, почти идентичны, за исключением некотрых
деталей. В программе на Фиг. 6.16 блок FCB определяется не
последовательностью команд типа DEFINE, а оператором STRUC.
Обратите внимание, что при ассемблировании данной программы не
называется ни запись DATE_WORD, ни структура FCB: они служат лишь
для задания смещений в области данных.
Microsoft (R) Macro Assembler Version 5.00 1/1/80 04:03:49
Фиг. 6.16 Структуры и записи Page 1-1
PAGE ,132
TITLE Фиг. 6.16 Структуры и записи
DATE_WORD RECORD YEAR:7,MONTH:4,DAY:5
FCB STRUC
0000 00 DRIVE DB 0 ; Номер устройства
0001 20 20 20 20 20 20 20 FILE_NAME DB ' ' ; Имя файла
Фиг. 6.16 Структуры и записи (начало)
20
0009 20 20 20 FILE_EXT DB ' ' ; Тип файла
000C 0000 CURRENT_BLOCK DW 0 ; Номер текущего блока
000E 0080 RECORD_SIZE DW 80H ; Логический размер записи
0010 00000000 FILE_SIZE DD 0 ; Размер файла в байтах
0014 0000 DATE DW 0 ; Дата последнего изменения файла
0016 000A[ RESERVED DB 10 DUP (?) ; Зарезервировано ДОС
??
]
0020 00 SEQ_NUMBER DB 0 ; Номер записи в блоке
0021 00000000 RANDOM_NUMBER DD 0 ; Номер записи в файле
0025 FCB ENDS
0000 STACK SEGMENT STACK
0000 0040[ DW 64 DUP (?)
????
]
0080 STACK ENDS
0000 CODE SEGMENT
ASSUME CS:CODE
0000 RECORDS PROC FAR
0000 1E PUSH DS ; Занесение в стек адреса возврата
0001 B8 0000 MOV AX,0
0004 50 PUSH AX
ASSUME DS:CODE ; DS файтически указывает на PSP
0005 BA 005C MOV DX,05CH ; Адрес FCB в PSP
0008 B4 0F MOV AH,0FH ; Открыть файл
000A CD 21 INT 21H
000C BB 005C MOV BX,05CH ; Адрес FCB
000F 8B 47 14 MOV AX,[BX].DATE
0012 25 FE00 AND AX,MASK YEAR ; Выделение года из полной даты
0015 B9 0009 MOV CX,YEAR ; Выравнивание вправо
0018 D3 E8 SHR AX,CL
001A 8A F0 MOV DH,AL ; Сохранение года в регистре DH
001C 8B 47 14 MOV AX,[BX].DATE
001F 25 01E0 AND AX,MASK MONTH
0022 B9 0005 MOV CX,MONTH
0025 D3 E8 SHR AX,CL
0027 8A D0 MOV DL,AL ; Сохранение месяца в регистре DL
0029 8B 47 14 MOV AX,[BX].DATE
002C 25 001F AND AX,MASK DAY ; Сохранение дня в регистре AL
002F CB RET
0030 RECORDS ENDP
0030 CODE ENDS
END RECORDS
Фиг. 6.16 Структуры и записи (продолжение)
И последнее замечание об использовании записей и структур. Мы
применяем эти средства, потому что они позволяют писать программы
не вникая в конкретные детали структуры данных. Используя для
описания данных оператор STRUC, мы можем рассматривать ссылки на
каждое из полей как смещение внутри структуры данных. При этом
программисту не нужно знать фактическое смещение этого поля. То же
самое верно и для записи из битов. Если программа использует
операторы MASK и сдвига, то распределение бит этого поля достаточно
задать только в соответствующем операторе RECORD.
Выигрыш от применения этих программных средств становится
очевидным при разработке больших систем, с которыми связано большое
число программистов или программ. В этом случае вы наверняка будуте
модифицировать структуры данных по мере разработки программ. Если
при написании программ пользоваться описанными структурными
моделями, то изменить их и связанные с ними программы будет
несложно: вы модифицируете определенную структуру данных, а затем
вновь транслируете все программы с использовнием этой структуры.
Сами же программы в изменениях не нуждаются. Можно поступить еще
проще, если хранить структуру данных, как отдельный файл, который
будет включаться в каждую ассемблируемую программу оператором
INCLUDE: таким образом вы будете иметь только одну версию структуры
данных. Описанные средства упрощают процесс создания большой
программы со всеми изменениями, возникающими при ее разработке.