Cамоучитель по Assembler

         

Глава 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: таким образом вы будете иметь только одну версию структуры
    данных. Описанные средства упрощают процесс создания большой
    программы со всеми изменениями, возникающими при ее разработке.