Из урока 6

Структура машинной команды

Машинная команда представляет собой закодированное по определенным правилам указание микропроцессору на выполнение некоторой операции или действия. Каждая команда содержит элементы, определяющие:
  • что делать? (ответ на этот вопрос дает элемент команды, называемый кодом операции (КОП));
  • объекты, над которыми нужно что-то делать (эти элементы называются операндами);
  • как делать? (эти элементы называются типами операндов — обычно задаются неявно).
Приведенный на рис. 1 формат машинной команды является самым общим. Максимальная длина машинной команды — 15 байт. Реальная команда может содержать гораздо меньшее количество полей, вплоть до одного — только КОП.

Рис. 1. Формат машинной команды

Опишем назначения полей машинной команды.

  • Префиксы. Необязательные элементы машинной команды, каждый из которых состоит из одного байта или может отсутствовать. В памяти префиксы предшествуют команде. Назначение префиксов — модифицировать операцию, выполняемую командой. Прикладная программа может использовать следующие типы префиксов:
    • Префикс замены сегмента. В явной форме указывает, какой сегментный регистр используется в данной команде для адресации стека или данных. Префикс отменяет выбор сегментного регистра по умолчанию. Префиксы замены сегмента имеют следующие значения:
      • 2eh — замена сегмента cs;
      • 36h — замена сегмента ss;
      • 3eh — замена сегмента ds;
      • 26h — замена сегмента es;
      • 64h — замена сегмента fs;
      • 65h — замена сегмента gs.
    • Префикс разрядности адреса уточняет разрядность адреса (32 или 16-разрядный).
      Каждой команде, в которой используется адресный операнд, ставится в соответствие разрядность адреса этого операнда. Этот адрес может иметь разрядность 16 или 32 бит. Если разрядность адреса для данной команды 16 бит, это означает, что команда содержит 16-разрядное смещение (см. рис. 1), оно соответствует 16-разрядному смещению адресного операнда относительно начала некоторого сегмента. В контексте рис. 2 это смещение называется эффективный адрес. Если разрядность адреса 32 бит, это означает, что команда содержит 32-разрядное смещение (см. рис. 1 ), оно соответствует 32-разрядному смещению адресного операнда относительно начала сегмента и по его значению формируется 32-битное смещение в сегменте. С помощью префикса разрядности адреса можно изменить действующее по умолчанию значение разрядности адреса. Это изменение будет касаться только той команды, которой предшествует префикс.

      Рис. 2. Механизм формирования физического адреса в реальном режиме
    • Префикс разрядности операнда аналогичен префиксу разрядности адреса, но указывает на разрядность операндов (32 или 16-разрядные), с которыми работает команда. В соответствии с какими правилами устанавливаются значения атрибутов разрядности адреса и операндов по умолчанию?
      В реальном режиме и режиме виртуального i8086 значения этих атрибутов — 16 бит.
      В защищенном режиме значения атрибутов зависят от состояния бита D в дескрипторах исполняемых сегментов (см. урок 16). Если D = 0, то значения атрибутов, действующие по умолчанию, равны 16 бит; если D = 1, то 32 бит.
      Значения префиксов разрядности операнда 66h и разрядности адреса 67h. Вы можете с помощью префикса разрядности адреса в реальном режиме использовать 32-разрядную адресацию, но при этом необходимо помнить об ограниченности размера сегмента величиной 64 Кбайт. Аналогично префиксу разрядности адреса вы можете использовать префикс разрядности операнда в реальном режиме для работы с 32-разрядными операндами (к примеру, в арифметических командах).
    • Префикс повторения используется с цепочечными командами (командами обработки строк). Этот префикс “зацикливает” команду для обработки всех элементов цепочки. Система команд поддерживает два типа префиксов:
      • безусловные (rep — 0f3h), заставляющие повторяться цепочечную команду некоторое количество раз;
      • условные (repe/repz — 0f3h, repne/repnz — 0f2h), которые при зацикливании проверяют некоторые флаги, и в результате проверки возможен досрочный выход из цикла.
  • Код операции. Обязательный элемент, описывающий операцию, выполняемую командой. Многим командам соответствует несколько кодов операций, каждый из которых определяет нюансы выполнения операции.

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

  • Байт режима адресации modr/m. Значения этого байта определяет используемую форму адреса операндов. Операнды могут находиться в памяти в одном или двух регистрах. Если операнд находится в памяти, то байт modr/m определяет компоненты (смещение, базовый и индексный регистры), используемые для вычисления его эффективного адреса (см. рис. 2). В защищенном режиме для определения местоположения операнда в памяти может дополнительно использоваться байт sib (Scale-Index-Base — масштаб-индекс-база). Байт modr/m состоит из трех полей (см. рис. 1):
    • поле mod определяет количество байт, занимаемых в команде адресом операнда (см. рис. 1, поле смещение в команде).
      Поле mod используется совместно с полем r/m, которое указывает способ модификации адреса операнда смещение в команде.
      К примеру, если mod = 00, это означает, что поле смещение в команде отсутствует, и адрес операнда определяется содержимым базового и (или) индексного регистра. Какие именно регистры будут использоваться для вычисления эффективного адреса, определяется значением этого байта.
      Если mod = 01, это означает, что поле смещение в команде присутствует, занимает один байт и модифицируется содержимым базового и (или) индексного регистра.
      Если mod = 10, это означает, что поле смещение в команде присутствует, занимает два или четыре байта (в зависимости от действующего по умолчанию или определяемого префиксом размера адреса) и модифицируется содержимым базового и (или) индексного регистра.
      Если mod = 11, это означает, что операндов в памяти нет: они находятся в регистрах. Это же значение байта mod используется в случае, когда в команде применяется непосредственный операнд;
    • поле reg/коп определяет либо регистр, находящийся в команде на месте первого операнда, либо возможное расширение кода операции;
    • поле r/m используется совместно с полем mod и определяет либо регистр, находящийся в команде на месте первого операнда (если mod = 11), либо используемые для вычисления эффективного адреса (совместно с полем смещение в команде) базовые и индексные регистры.
  • Байт масштаб-индекс-база (байт sib) используется для расширения возможностей адресации операндов.
    На наличие байта sib в машинной команде указывает сочетание одного из значений 01 или 10 поля mod и значения поля r/m= 100. Байт sib состоит из трех полей:
    • поля масштаба ss. В этом поле размещается масштабный множитель для индексного компонента index, занимающего следующие три бита байта sib.
      В поле ss может содержаться одно из следующих значений: 1, 2, 4, 8.
      При вычислении эффективного адреса на это значение будет умножаться содержимое индексного регистра. Более подробно с практической точки зрения эта расширенная возможность индексации рассматривается на уроке 12 при обсуждении вопросов работы с массивами;
    • поля index — используется для хранения номера индексного регистра, который применяется для вычисления эффективного адреса операнда;
    • поля base — используется для хранения номера базового регистра, который также применяется для вычисления эффективного адреса операнда. Напомню, что в качестве базового и индексного регистров могут использоваться практически все регистры общего назначения.
  • Поле смещения в команде. 8, 16 или 32-разрядное целое число со знаком, представляющее собой, полностью или частично (с учетом вышеприведенных рассуждений), значение эффективного адреса операнда.
  • Поле непосредственного операнда. Необязательное поле, представляющее собой 8, 16 или 32-разрядный непосредственный операнд. Наличие этого поля, конечно, отражается на значении байта modr/m.

Способы задания операндов команды

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

Операнд задается неявно на микропрограммном уровне. В этом случае команда явно не содержит операндов. Алгоритм выполнения команды использует некоторые объекты по умолчанию (регистры, флаги в eflags и т. д.).
Например, команды cli и sti неявно работают с флагом прерывания if в регистре eflags, а команда xlat неявно обращается к регистру al и строке в памяти по адресу, определяемому парой регистров ds:bx.

Операнд задается в самой команде (непосредственный операнд). Операнд находится в коде команды, то есть является ее частью. Для хранения такого операнда в команде выделяется поле длиной до 32 бит (см. рис. 1). Непосредственный операнд может быть только вторым операндом (источником). Операнд получатель может находиться либо в памяти, либо в регистре.
Например: mov ax,0ffffh пересылает в регистр ax шестнадцатеричную константу ffff. Команда add sum,2 складывает содержимое поля по адресу sum с целым числом 2 и записывает результат по месту первого операнда, то есть в память.

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

  • 32-разрядные регистры EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP;
  • 16-разрядные регистры AX, BX, CX, DX, SI, DI, SP, BP;
  • 8-разрядные регистры AH, AL, BH, BL, CH, CL, DH, DL;
  • сегментные регистры CS, DS, SS, ES, FS, GS.
Например, команда add ax,bx складывает содержимое регистров ax и bx и записывает результат в bx. Команда dec si уменьшает содержимое si на 1.

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

В свою очередь, косвенная адресация имеет следующие разновидности:

  • косвенная базовая адресация; другое ее название — регистровая косвенная адресация;
  • косвенная базовая адресация со смещением;
  • косвенная индексная адресация со смещением;
  • косвенная базовая индексная адресация;
  • косвенная базовая индексная адресация со смещением.
Операндом является порт ввода/вывода. Как мы уже отмечали, помимо адресного пространства оперативной памяти микропроцессор поддерживает адресное пространство ввода-вывода, которое используется для доступа к устройствам ввода-вывода. Объем адресного пространства ввода-вывода составляет 64 Кбайт. Для любого устройства компьютера в этом пространстве выделяются адреса. Конкретное значение адреса в пределах этого пространства называется портом ввода-вывода. Физически порту ввода-вывода соответствует аппаратный регистр (не путать с регистром микропроцессора), доступ к которому осуществляется с помощью специальных команд ассемблера in и out.
Например,
in al,60h ;ввести байт из порта 60h
Регистры, адресуемые с помощью порта ввода-вывода, могут иметь разрядность 8, 16 или 32 бит, но для конкретного порта разрядность регистра фиксирована.
Команды in и out работают с фиксированной номенклатурой объектов. В качестве источника информации или получателя применяются так называемые регистры-аккумуляторы eax, ax, al. Выбор регистра определяется разрядностью порта. Номер порта может задаваться непосредственным операндом в командах in и out или значением в регистре dx. Последний способ позволяет динамически определить номер порта в программе. Например:

     mov  dx,20h    ;записать номер порта 20h в регистр dx
     mov  al,20h    ;записать значение 20h в регистр al
     out  dx,al     ;вывести значение 20h в порт 20H
Операнд находится в стеке.

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

  • регистр—регистр;
  • регистр—память;
  • память—регистр;
  • непосредственный операнд—регистр;
  • непосредственный операнд—память.
У данного правила есть исключения, которые касаются:
  • команд работы с цепочками, которые могут перемещать данные из памяти в память;
  • команд работы со стеком, которые могут переносить данные из памяти в стек, также находящийся в памяти;
  • команд типа умножения, которые кроме операнда, указанного в команде, используют еще и второй, неявный операнд.
Из перечисленных сочетаний операндов наиболее часто употребляются регистр—память и память—регистр.
Ввиду их важности рассмотрим их подробнее. Обсуждение мы будем сопровождать примерами команд ассемблера, которые будут показывать, как изменяется формат команды ассемблера при применении того или иного вида адресации. В связи с этим посмотрите еще раз на рис. 2, на котором показан принцип формирования физического адреса на адресной шине микропроцессора. Видно, что адрес операнда формируется как сумма двух составляющих — сдвинутого на 4 бит содержимого сегментного регистра и 16-битного эффективного адреса, который в общем случае вычисляется как сумма трех компонентов: базы, смещения и индекса.

Перечислим и затем рассмотрим особенности основных видов адресации операндов в памяти:

  • Прямая адресация
  • Косвенная базовая (регистровая) адресация
  • Косвенная базовая (регистровая) адресация со смещением
  • Косвенная индексная адресация со смещением
  • Косвенная базовая индексная адресация
  • Косвенная базовая индексная адресация со смещением

Прямая адресация

Это простейший вид адресации операнда в памяти, так как эффективный адрес содержится в самой команде и для его формирования не используется никаких дополнительных источников или регистров. Эффективный адрес берется непосредственно из поля смещения машинной команды (см. рис. 1), которое может иметь размер 8, 16, 32 бит. Это значение однозначно определяет байт, слово или двойное слово, расположенные в сегменте данных.

Прямая адресация может быть двух типов:

  • Относительная прямая адресация. Используется для команд условных переходов, для указания относительного адреса перехода. Относительность такого перехода заключается в том, что в поле смещения машинной команды содержится 8, 16 или 32-битное значение, которое в результате работы команды будет складываться с содержимым регистра указателя команд ip/eip. В результате такого сложения получается адрес, по которому и осуществляется переход.
    Например:

     jс m1     ;переход на метку m1, если флаг
cf = 1
     mov  al,2
...
m1:
    Несмотря на то, что в команде указана некоторая метка в программе, ассемблер вычисляет смещение этой метки относительно следующей команды (в нашем случае это mov al,2) и подставляет его в формируемую машинную команду jc.
  • Абсолютная прямая адресация. В этом случае эффективный адрес является частью машинной команды, но формируется этот адрес только из значения поля смещения в команде. Для формирования физического адреса операнда в памяти микропроцессор складывает это поле со сдвинутым на 4 бит значением сегментного регистра. В команде ассемблера можно использовать несколько форм такой адресации.
    Например:

        mov     ax,dword ptr [0000]     ;записать слово по адресу
        ;ds:0000 в регистр ax
    Но такая адресация применяется редко — обычно используемым ячейкам в программе присваиваются символические имена. В процессе трансляции ассемблер вычисляет и подставляет значения смещений этих имен в формируемую им машинную команду в поле смещение в команде (см. рис. 1). В итоге получается так, что машинная команда прямо адресует свой операнд, имея, фактически, в одном из своих полей значение эффективного адреса.
    Например:

data    segment
per1    dw      5
...
data    ends
code    segment
        mov     ax,data
        mov     ds,ax
...
        mov     ax,per1 ;записать слово per1 (его физический адрес ds:0000) в ax
    Мы получим тот же результат, что и при использовании команды mov ax,dword ptr [0000]
Остальные виды адресации относятся к косвенным. Слово “косвенный” в названии этих видов адресации означает то, что в самой команде может находиться лишь часть эффективного адреса, а остальные его компоненты находятся в регистрах, на которые указывают своим содержимым байт modr/m и, возможно, байт sib.

Косвенная базовая (регистровая) адресация

При такой адресации эффективный адрес операнда может находиться в любом из регистров общего назначения, кроме sp/esp и bp/ebp (это специфические регистры для работы с сегментом стека). Синтаксически в команде этот режим адресации выражается заключением имени регистра в квадратные скобки [ ]. К примеру, команда mov ax,[ecx] помещает в регистр ax содержимое слова по адресу из сегмента данных со смещением, хранящимся в регистре ecx. Так как содержимое регистра легко изменить в ходе работы программы, данный способ адресации позволяет динамически назначить адрес операнда для некоторой машинной команды. Это свойство очень полезно, например, для организации циклических вычислений и для работы с различными структурами данных типа таблиц или массивов.

Косвенная базовая (регистровая) адресация со смещением

Этот вид адресации является дополнением предыдущего и предназначен для доступа к данным с известным смещением относительно некоторого базового адреса. Этот вид адресации удобно использовать для доступа к элементам структур данных, когда смещение элементов известно заранее, на стадии разработки программы, а базовый (начальный) адрес структуры должен вычисляться динамически, на стадии выполнения программы. Модификация содержимого базового регистра позволяет обратиться к одноименным элементам различных экземпляров однотипных структур данных.
К примеру, команда mov ax,[edx+3h] пересылает в регистр ax слова из области памяти по адресу: содержимое edx + 3h.
Команда mov ax,mas[dx] пересылает в регистр ax слово по адресу: содержимое dx плюс значение идентификатора mas (не забывайте, что транслятор присваивает каждому идентификатору значение, равное смещению этого идентификатора относительно начала сегмента данных).

Косвенная индексная адресация со смещением

Этот вид адресации очень похож на косвенную базовую адресацию со смещением. Здесь также для формирования эффективного адреса используется один из регистров общего назначения. Но индексная адресация обладает одной интересной особенностью, которая очень удобна для работы с массивами. Она связана с возможностью так называемого масштабирования содержимого индексного регистра. Что это такое?
Посмотрите на рис. 1. Нас интересует байт sib. При обсуждении структуры этого байта мы отмечали, что он состоит из трех полей. Одно из этих полей — поле масштаба ss, на значение которого умножается содержимое индексного регистра.
К примеру, в команде mov ax,mas[si*2] значение эффективного адреса второго операнда вычисляется выражением mas+(si)*2. В связи с тем, что в ассемблере нет средств для организации индексации массивов, то программисту своими силами приходится ее организовывать.
Наличие возможности масштабирования существенно помогает в решении этой проблемы, но при условии, что размер элементов массива составляет 1, 2, 4 или 8 байт.

Косвенная базовая индексная адресация

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

        mov     eax,[esi][edx]
В данном примере эффективный адрес второго операнда формируется из двух компонентов (esi)+(edx).

Косвенная базовая индексная адресация со смещением

Этот вид адресации является дополнением косвенной индексной адресации. Эффективный адрес формируется как сумма трех составляющих: cодержимого базового регистра, cодержимого индексного регистра и значения поля смещения в команде.
К примеру, команда mov eax,[esi+5][edx] пересылает в регистр eax двойное слово по адресу: (esi) + 5 + (edx).
Команда add ax,array[esi][ebx] производит сложение содержимого регистра ax с содержимым слова по адресу: значение идентификатора array + (esi) + (ebx).


Из урока 7

Команды пересылки данных

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

Команды пересылки данных общего назначения

К этой группе относятся следующие команды:
mov <операнд назначения>,<операнд-источник>
xchg <операнд1>,<операнд2>
mov - это основная команда пересылки данных. Она реализует самые разнообразные варианты пересылки.
Отметим особенности применения этой команды:
  • командой mov нельзя осуществить пересылку из одной области памяти в другую. Если такая необходимость возникает, то нужно использовать в качестве промежуточного буфера любой доступный в данный момент регистр общего назначения.

    • К примеру, рассмотрим фрагмент программы для пересылки байта из ячейки fls в ячейку fld:

masm
model   small
.data
fls     db      5
fld     db      ?
.code
start:
...
        mov     al,fls
        mov     fld,al
        ...
end     start
  • нельзя загрузить в сегментный регистр значение непосредственно из памяти. Поэтому для выполнения такой загрузки нужно использовать промежуточный объект. Это может быть регистр общего назначения или стек. Если вы посмотрите листинги 3.1 и 5.1, то увидите в начале сегмента кода две команды mov, выполняющие настройку сегментного регистра ds. При этом из-за невозможности загрузить впрямую в сегментный регистр значение адреса сегмента, содержащееся в предопределенной переменной @data, приходится использовать регистр общего назначения ax;
  • нельзя переслать содержимое одного сегментного регистра в другой сегментный регистр. Это объясняется тем, что в системе команд нет соответствующего кода операции. Но необходимость в таком действии часто возникает. Выполнить такую пересылку можно, используя в качестве промежуточных все те же регистры общего назначения. Вот пример инициализации регистра es значением из регистра ds:
    •
        mov     ax,ds
        mov     es,ax

    Но есть и другой, более красивый способ выполнения данной операции — использование стека и команд push и pop:

        push    ds      ;поместить значение регистра ds в стек
        pop     es      ;записать в es число из стека

  • нельзя использовать сегментный регистр cs в качестве операнда назначения. Причина здесь простая. Дело в том, что в архитектуре микропроцессора пара cs:ip всегда содержит адрес команды, которая должна выполняться следующей. Изменение командой mov содержимого регистра cs фактически означало бы операцию перехода, а не пересылки, что недопустимо.
Для двунаправленной пересылки данных применяют команду xchg. Для этой операции можно, конечно, применить последовательность из нескольких команд mov, но из-за того, что операция обмена используется довольно часто, разработчики системы команд микропроцессора посчитали нужным ввести отдельную команду обмена xchg. Естественно, что операнды должны иметь один тип. Не допускается (как и для всех команд ассемблера) обменивать между собой содержимое двух ячеек памяти. К примеру,

        xchg    ax,bx   ;обменять содержимое регистров ax и bx
        xchg    ax,word ptr [si]        ;обменять содержимое регистра ax
                ;и слова в памяти по адресу в [si]

Команды ввода-вывода в порт

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

Рис. 1. Концептуальная схема управления оборудованием компьютера

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

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

Команды работы с адресами и указателями памяти

При написании программ на ассемблере производится интенсивная работа с адресами операндов, находящимися в памяти. Для поддержки такого рода операций есть специальная группа команд, в которую входят следующие команды:
lea назначение,источник — загрузка эффективного адреса;
lds назначение,источник — загрузка указателя в регистр сегмента данных ds;
les назначение,источник — загрузка указателя в регистр дополнительного сегмента данных es;
lgs назначение,источник — загрузка указателя в регистр дополнительного сегмента данных gs;
lfs назначение,источник — загрузка указателя в регистр дополнительного сегмента данных fs;
lss назначение,источник — загрузка указателя в регистр сегмента стека ss.

Команда lea похожа на команду mov тем, что она также производит пересылку. Однако, обратите внимание, команда lea производит пересылку не данных, а эффективного адреса данных (то есть смещения данных относительно начала сегмента данных) в регистр, указанный операндом назначение.

Часто для выполнения некоторых действий в программе недостаточно знать значение одного лишь эффективного адреса данных, а необходимо иметь полный указатель на данные. Вы помните, что полный указатель на данные состоит из сегментной составляющей и смещения.
Все остальные команды этой группы позволяют получить в паре регистров такой полный указатель на операнд в памяти. При этом имя сегментного регистра, в который помещается сегментная составляющая адреса, определяется кодом операции. Соответственно, смещение помещается в регистр общего назначения, указанный операндом назначение.
Но не все так просто с операндом источник. На самом деле, в команде в качестве источника нельзя указывать непосредственно имя операнда в памяти, на который мы бы хотели получить указатель.
Предварительно необходимо получить само значение полного указателя в некоторой области памяти и указать в команде получения полного адреса имя этой области. Для выполнения этого действия необходимо вспомнить директивы резервирования и инициализации памяти.
При применении этих директив возможен частный случай, когда в поле операндов указывается имя другой директивы определения данных (фактически, имя переменной). В этом случае в памяти формируется адрес этой переменной. Какой адрес будет сформирован (эффективный или полный), зависит от применяемой директивы. Если это dw, то в памяти формируется только 16-битное значение эффективного адреса, если же dd — в память записывается полный адрес. Размещение этого адреса в памяти следующее: в младшем слове находится смещение, в старшем — 16-битная сегментная составляющая адреса.

Например, при организации работы с цепочкой символов удобно поместить ее начальный адрес в некоторый регистр и далее в цикле модифицировать это значение для последовательного доступа к элементам цепочки. В листинге 1 производится копирование строки байт str_1 в строку байт str_2.
В строках 12 и 13 в регистры si и di загружаются значения эффективных адресов переменных str_1 и str_2.
В строках 16 и 17 производится пересылка очередного байта из одной строки в другую. Указатели на позиции байтов в строках определяются содержимым регистров si и di. Для пересылки очередного байта необходимо увеличить на единицу регистры si и di, что и делается командами сложения inc (строки 18, 19). После этого программу необходимо зациклить до обработки всех символов строки.

Листинг 1. Копирование строки
<1>;---------Prg_7_2.asm---------------
<2>     masm
<3>     model   small
<4>     .data
<5>     ...
<6>     str_1 db        ‘Ассемблер — базовый язык компьютера’
<7>     str_2   db      50 dup (‘ ‘)
<8>     full_pnt        dd str_1
<9>     ...
<10>    .code
<11>    start:
<12>    ...
<13>    lea     si,str_1
<14>    lea     di,str_2
<15>    les     bx,full_pnt     ;полный указатель на str1 в пару es:bx
<16>    m1:
<17>    mov     al,[si]
<18>    mov     [di],al
<19>    inc     si
<20>    inc     di
<21> ;цикл на метку m1 до пересылки всех символов
<22> ...
<23>    end     start
Необходимость использования команд получения полного указателя данных в памяти, то есть адреса сегмента и значения смещения внутри сегмента, возникает, в частности, при работе с цепочками.
В строке 14 листинга 1 в двойном слове full_pnt формируются сегментная часть адреса и смещение для переменной str_1. При этом 2 байта смещения занимают младшее слово full_pnt, а значение сегментной составляющей адреса — старшее слово full_pnt. В строке 14 командой les эти компоненты адреса помещаются в регистры bx и es.

Команды преобразования данных

К этой группе можно отнести множество команд микропроцессора, но большинство из них имеют те или иные особенности, которые требуют отнести их к другим функциональным группам.
Поэтому из всей совокупности команд микропроцессора непосредственно к командам преобразования данных можно отнести только одну команду:
xlat [адрес_таблицы_перекодировки]

Это очень интересная и полезная команда. Ее действие заключается в том, что она замещает значение в регистре al другим байтом из таблицы в памяти, расположенной по адресу, указанному операндом адрес_таблицы_перекодировки.
Слово “таблица” весьма условно — по сути это просто строка байт. Адрес байта в строке, которым будет производиться замещение содержимого регистра al, определяется суммой (bx) + (al), то есть содержимое al выполняет роль индекса в байтовом массиве.

При работе с командой xlat обратите внимание на следующий тонкий момент. Несмотря на то, что в команде указывается адрес строки байт, из которой должно быть извлечено новое значение, этот адрес должен быть предварительно загружен (например, с помощью команды lea) в регистр bx. Таким образом, операнд адрес_таблицы_перекодировки на самом деле не нужен (необязательность операнда показана заключением его в квадратные скобки). Что касается строки байт (таблицы перекодировки), то она представляет собой область памяти размером от 1 до 255 байт (диапазон числа без знака в 8-битном регистре).

В качестве иллюстрации работы данной команды мы рассмотрим программу, которая преобразует двузначное шестнадцатеричное число, вводимое с клавиатуры (то есть в символьном виде), в эквивалентное двоичное представление в регистре al. Ниже (листинг 2) приведен вариант этой программы с использованием команды xlat.

Листинг 2. Использование таблицы перекодировки
<1>;---------Prg_7_3.asm----------------------
<2>;Программа преобразования двузначного шестнадцатеричного числа
<3>;в двоичное представление с использованием команды xlat.
<4>;Вход: исходное шестнадцатеричное число; вводится с клавиатуры.
<5>;Выход: результат преобразования в регистре al.
<6>.data                ;сегмент данных
<7>  message    db      ‘Введите две шестнадцатеричные цифры,$’
<8>  tabl       db      48 dup (0),0,1,2,3,4,5,6,7,8,9, 8 dup (0),
<9>             db      0ah,0bh,0ch,odh,0eh,0fh,27 dup (0)
<10>    db      0ah,0bh,0ch,odh,0eh,0fh, 153 dup (0)
<11> .stack     256     ;сегмент стека
<12> .code
<13> ;начало сегмента кода
<14> proc       main    ;начало процедуры main
<15>    mov     ax,@data        ;физический адрес сегмента данных в регистр ax
<16>    mov     ds,ax   ;ax записываем в ds
<17>    lea     bx,tabl ;загрузка адреса строки байт в регистр bx
<18>    mov     ah,9
<19>    mov     dx,offset message
<20>    int     21h     ;вывести приглашение к вводу
<21>    xor     ax,ax   ;очистить регистр ax
<22>    mov     ah,1h   ;значение 1h в регистр ah
<23>    int     21h     ;вводим первую цифру в al
<24>    xlat    ;перекодировка первого введенного символа в al
<25>    mov     dl,al
<26>    shl     dl,4    ;сдвиг dl влево для освобождения места для младшей цифры
<27>    int     21h     ;ввод второго символа в al
<28>    xlat    ;перекодировка второго введенного символа в al
<29>    add     al,dl   ;складываем для получения результата
<30>    mov     ax,4c00h        ;пересылка 4c00h в регистр ax
<31>    int     21h     ;завершение программы
<32> endp       main    ;конец процедуры main
<33> code       ends    ;конец сегмента кода
<34> endmain    ;конец программы с точкой входа main
Сама по себе программа проста; сложность вызывает обычно формирование таблицы перекодировки. Обсудим этот момент подробнее.
Прежде всего нужно определиться с значениями тех байтов, которые вы будете изменять. В нашем случае это символы шестнадцатеричных цифр. Сконструируем в сегменте данных таблицу, в которой на места байтов, соответствующих символам шестнадцатеричных цифр, помещаем их новые значения, то есть двоичные эквиваленты шестнадцатеричных цифр. Строки 8-10 листинга 2 демонстрируют, как это сделать. Байты этой таблицы, смещения которых не совпадают со значением кодов шестнадцатеричных цифр, нулевые. Таковыми являются первые 48 байт таблицы, промежуточные байты и часть в конце таблицы.
Желательно определить все 256 байт таблицы. Дело в том, что если мы ошибочно поместим в al код символа, отличный от символа шестнадцатеричной цифры, то после выполнения команды xlat получим непредсказуемый результат. В случае листинга 2 это будет ноль, что не совсем корректно, так как непонятно, что же в действительности было в al — код символа “0” или что-то другое.
Поэтому, наверное, есть смысл здесь поставить “защиту от дурака”, поместив в неиспользуемые байты таблицы какой-нибудь определенный символ. После каждого выполнения xlat нужно будет просто контролировать значение в al на предмет совпадения с этим символом, и если оно произошло, выдавать сообщение об ошибке.
После того как таблица составлена, с ней можно работать. В сегменте команд строка 18 инициализирует регистр bx значением адреса таблицы tabl. Далее все очень просто. Поочередно вводятся символы двух шестнадцатеричных цифр, и производится их перекодировка в соответствующие двоичные эквиваленты.

Команды работы со стеком

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

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

Для работы со стеком предназначены три регистра:

  • ss — сегментный регистр стека;
  • sp/esp — регистр указателя стека;
  • bp/ebp — регистр указателя базы кадра стека.
Размер стека зависит от режима работы микропроцессора и ограничивается 64 Кбайт (или 4 Гбайт в защищенном режиме).
В каждый момент времени доступен только один стек, адрес сегмента которого содержится в регистре ss. Этот стек называется текущим. Для того чтобы обратиться к другому стеку (“переключить стек”), необходимо загрузить в регистр ss другой адрес. Регистр ss автоматически используется процессором для выполнения всех команд, работающих со стеком.

Перечислим еще некоторые особенности работы со стеком:

  • запись и чтение данных в стеке осуществляется в соответствии с принципом LIFO (Last In First Out — “последним пришел, первым ушел”);
  • по мере записи данных в стек последний растет в сторону младших адресов. Эта особенность заложена в алгоритм команд работы со стеком;
  • при использовании регистров esp/sp и ebp/bp для адресации памяти ассемблер автоматически считает, что содержащиеся в нем значения представляют собой смещения относительно сегментного регистра ss.
В общем случае стек организован так, как показано на рис. 2.

Рис. 2. Концептуальная схема организации стека

Для работы со стеком предназначены регистры ss, esp/sp и ebp/bp.
Эти регистры используются комплексно, и каждый из них имеет свое функциональное назначение.
Регистр esp/sp всегда указывает на вершину стека, то есть содержит смещение, по которому в стек был занесен последний элемент. Команды работы со стеком неявно изменяют этот регистр так, чтобы он указывал всегда на последний записанный в стек элемент. Если стек пуст, то значение esp равно адресу последнего байта сегмента, выделенного под стек.
При занесении элемента в стек процессор уменьшает значение регистра esp, а затем записывает элемент по адресу новой вершины.
При извлечении данных из стека процессор копирует элемент, расположенный по адресу вершины, а затем увеличивает значение регистра указателя стека esp.
Таким образом, получается, что стек растет вниз, в сторону уменьшения адресов.

Что делать, если нам необходимо получить доступ к элементам не на вершине, а внутри стека?
Для этого применяют регистр ebp. Регистр ebp — регистр указателя базы кадра стека.
Например, типичным приемом при входе в подпрограмму является передача нужных параметров путем записи их в стек. Если подпрограмма тоже активно работает со стеком, то доступ к этим параметрам становится проблематичным. Выход в том, чтобы после записи нужных данных в стек сохранить адрес вершины стека в указателе кадра (базы) стека — регистре ebp. Значение в ebp в дальнейшем можно использовать для доступа к переданным параметрам.

Начало стека расположено в старших адресах памяти. На рис. 2 этот адрес обозначен парой ss:ffff. Смещение ffff приведено здесь условно. Реально это значение определяется величиной, которую программист задает при описании сегмента стека в своей программе.
К примеру, для программы в листинге 2 началу стека будет соответствовать пара ss:0100h. Адресная пара ss:ffff — это максимальное для реального режима значение адреса начала стека, так как размер сегмента в нем ограничен величиной 64 Кбайт (0ffffh).

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

  • (sp) = (sp) – 2; значение sp уменьшается на 2;
  • значение из источника записывается по адресу, указываемому парой ss:sp.

Рис. 3. Принцип работы команды push

pop назначение — запись значения из вершины стека по месту, указанному операндом назначение. Значение при этом “снимается” с вершины стека.
Алгоритм работы команды pop обратен алгоритму команды push (рис. 4):

  • запись содержимого вершины стека по месту, указанному операндом назначение;
  • (sp) = (sp) + 2; увеличение значения sp.

Рис. 4. Принцип работы команды pop

pusha — команда групповой записи в стек.
По этой команде в стек последовательно записываются регистры ax, cx, dx, bx, sp, bp, si, di. Заметим, что записывается оригинальное содержимое sp, то есть то, которое было до выдачи команды pusha (рис. 5).

Рис. 5. Принцип работы команды pusha

pushaw — почти синоним команды pusha. В чем разница? На уроке 5 мы обсуждали один из атрибутов сегмента — атрибут разрядности. Он может принимать значение use16 или use32.
Рассмотрим работу команд pusha и pushaw при каждом из этих атрибутов:

  • use16 — алгоритм работы pushaw аналогичен алгоритму pusha.
  • use32 — pushaw не изменяется (то есть она нечувствительна к разрядности сегмента и всегда работает с регистрами размером в слово — ax, cx, dx, bx, sp, bp, si, di). Команда pusha чувствительна к установленной разрядности сегмента и при указании 32-разрядного сегмента работает с соответствующими 32-разрядными регистрами, то есть eax, ecx, edx, ebx, esp, ebp, esi, edi.

pushad — выполняется аналогично команде pusha, но есть некоторые особенности, которые вы можете узнать из “Справочника команд”.

Следующие три команды выполняют действия, обратные вышеописанным командам:
popa;
popaw;
popad.

Группа команд, описанная ниже, позволяет сохранить в стеке регистр флагов и записать слово или двойное слово в стеке. Отметим, что перечисленные ниже команды — единственные в системе команд микропроцессора, которые позволяют получить доступ (и которые нуждаются в этом доступе) ко всему содержимому регистра флагов.
pushf — сохраняет регистр флагов в стеке.
Работа этой команды зависит от атрибута размера сегмента:

  • use16 — в стек записывается регистр flags размером 2 байта;
  • use32 — в стек записывается регистр eflags размером 4 байта.

pushfw — сохранение в стеке регистра флагов размером в слово. Всегда работает как pushf с атрибутом use16.
pushfd — сохранение в стеке регистра флагов flags или eflags в зависимости от атрибута разрядности сегмента (то есть то же, что и pushf).

Аналогично, следующие три команды выполняют действия, обратные рассмотренным выше операциям:
popf
popfw
popfd

И в заключение отметим основные виды операции, когда использование стека практически неизбежно:

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


Из урока 8

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

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

Обзор группы арифметических команд и данных

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

Рис. 1. Классификация арифметических команд

Группа арифметических целочисленных команд работает с двумя типами чисел:

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

Целые двоичные числа

Целое двоичное число с фиксированной точкой — это число, закодированное в двоичной системе счисления.

Размерность целого двоичного числа может составлять 8, 16 или 32 бит. Знак двоичного числа определяется тем, как интерпретируется старший бит в представлении числа. Это 7-й, 15-й или 31-й биты для чисел соответствующей размерности (см. Типы данных ). При этом интересно то, что среди арифметических команд есть всего две команды, которые действительно учитывают этот старший разряд как знаковый, — это команды целочисленного умножения и деления imul и idiv. В остальных случаях ответственность за действия со знаковыми числами и, соответственно, со знаковым разрядом ложится на программиста. К этому вопросу мы вернемся чуть позже. Диапазон значений двоичного числа зависит от его размера и трактовки старшего бита либо как старшего значащего бита числа, либо как бита знака числа (табл. 1).

Таблица 1. Диапазон значений двоичных чисел

Размерность поля | Целое без знака | Целое со знаком
байт | 0...255 | –128...+127
слово | 0...65 535 | –32 768...+32 767
двойное слово | 0...4 294 967 295 | –2 147 483 648...+2 147 483 647
Как описать числа с фиксированной точкой в программе?

Это делается с использованием директив описания данных. К примеру, последовательность описаний двоичных чисел из сегмента данных листинга 1 (помните о принципе “младший байт по младшему адресу”) будет выглядеть в памяти так, как показано на рис. 2.
Листинг 1. Числа с фиксированной точкой
;prg_8_1.asm
masm
model   small
stack   256
.data           ;сегмент данных
per_1   db      23
per_2   dw      9856
per_3   dd      9875645
per_4   dw      29857
.code           ;сегмент кода
main:           ;точка входа в программу
        mov     ax,@data        ;связываем регистр dx с сегментом
        mov     ds,ax   ;данных через регистр ax
exit:           ;посмотрите в отладчике дамп сегмента данных
        mov     ax,4c00h        ;стандартный выход
        int     21h
end     main    ;конец программы

Рис. 2. Дамп памяти для сегмента данных листинга 1

Десятичные числа

Десятичные числа — специальный вид представления числовой информации, в основу которого положен принцип кодирования каждой десятичной цифры числа группой из четырех бит. При этом каждый байт числа содержит одну или две десятичные цифры в так называемом двоично-десятичном коде (BCD — Binary-Coded Decimal). Микропроцессор хранит BCD-числа в двух форматах (рис. 3):
  • упакованном формате — в этом формате каждый байт содержит две десятичные цифры. Десятичная цифра представляет собой двоичное значение в диапазоне от 0 до 9 размером 4 бита. При этом код старшей цифры числа занимает старшие 4 бита. Следовательно, диапазон представления десятичного упакованного числа в одном байте составляет от 00 до 99;
  • неупакованном формате — в этом формате каждый байт содержит одну десятичную цифру в четырех младших битах. Старшие четыре бита имеют нулевое значение. Это так называемая зона. Следовательно, диапазон представления десятичного неупакованного числа в одном байте составляет от 0 до 9.

Рис. 3. Представление BCD-чисел

Как описать двоично-десятичные числа в программе?

Для этого можно использовать только две директивы описания и инициализации данных — db и dt. Возможность применения только этих директив для описания BCD-чисел обусловлена тем, что к таким числам также применим принцип “младший байт по младшему адресу”, что, как мы увидим далее, очень удобно для их обработки. И вообще, при использовании такого типа данных как BCD-числа, порядок описания этих чисел в программе и алгоритм их обработки — это дело вкуса и личных пристрастий программиста. Это станет ясно после того, как мы ниже рассмотрим основы работы с BCD-числами. К примеру, приведенная в сегменте данных листинга 2 последовательность описаний BCD-чисел будет выглядеть в памяти так, как показано на рис. 4.

 Листинг 2. BCD-числа
;prg_8_2.asm
masm
model   small
stack   256
.data   ;сегмент данных
per_1   db      2,3,4,6,8,2
;неупакованное BCD-число 286432
per_3   dt      9875645 ;упакованное BCD-число 9875645
.code   ;сегмент кода
main:   ;точка входа в программу
        mov     ax,@data        ;связываем регистр dx с сегментом
        mov     ds,ax   ;данных через регистр ax
exit:   ;посмотрите в отладчике дамп сегмента данных
        mov     ax,4c00h        ;стандартный выход
        int     21h
end     main    ;конец программы

Рис. 4. Дамп памяти для сегмента данных листинга 2

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

Арифметические операции над целыми двоичными числами

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

Сложение двоичных чисел без знака

Микропроцессор выполняет сложение операндов по правилам сложения двоичных чисел. Проблем не возникает до тех пор, пока значение результата не превышает размерности поля операнда (см. табл. 1). Например, при сложении операндов размером в байт результат не должен превышать число 255. Если это происходит, то результат оказывается неверным. Рассмотрим, почему так происходит. К примеру, выполним сложение: 254 + 5 = 259 в двоичном виде. 11111110 + 0000101 = 1 00000011. Результат вышел за пределы восьми бит и правильное его значение укладывается в 9 бит, а в 8-битовом поле операнда осталось значение 3, что, конечно, неверно. В микропроцессоре этот исход сложения прогнозируется и предусмотрены специальные средства для фиксирования подобных ситуаций и их обработки. Так, для фиксирования ситуации выхода за разрядную сетку результата, как в данном случае, предназначен флаг переноса cf. Он располагается в бите 0 регистра флагов eflags/flags. Именно установкой этого флага фиксируется факт переноса единицы из старшего разряда операнда. Естественно, что программист должен предусматривать возможность такого исхода операции сложения и средства для корректировки. Это предполагает включение участков кода после операции сложения, в которых анализируется флаг cf. Анализ этого флага можно провести различными способами. Самый простой и доступный — использовать команду условного перехода jcc. Эта команда в качестве операнда имеет имя метки в текущем сегменте кода. Переход на эту метку осуществляется в случае, если в результате работы предыдущей команды флаг cf установился в 1. Если теперь посмотреть на рис. 1, то видно, что в системе команд микропроцессора имеются три команды двоичного сложения:
  • inc операнд — операция инкремента, то есть увеличения значения операнда на 1;
  • add операнд_1,операнд_2 — команда сложения с принципом действия: операнд_1 = операнд_1 + операнд_2
  • adc операнд_1,операнд_2 — команда сложения с учетом флага переноса cf. Принцип действия команды:

    операнд_1 = операнд_1 + операнд_2 + значение_cf

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

Рассмотрим пример вычисления суммы чисел (листинг 3).

 Листинг 3. Вычисление суммы чисел
<1>     ;prg_8_3.asm
<2>     masm
<3>     model   small
<4>     stack   256
<5>     .data
<6>     a       db      254
<7>     .code   ;сегмент кода
<8>     main:
<9>     mov     ax,@data
<10>    mov     ds,ax
<11>    ...
<12>    xor     ax,ax
<13>    add     al,17
<14>    add     al,a
<15>    jnc     m1      ;если нет переноса, то перейти на m1
<16>    adc     ah,0    ;в ax сумма с учетом переноса
<17>    m1:     ...
<18>    exit:
<19>    mov     ax,4c00h        ;стандартный выход
<20>    int     21h
<21>    end     main    ;конец программы
В листинге 3 в строках 13–14 создана ситуация, когда результат сложения выходит за границы операнда. Эта возможность учитывается строкой 15, где команда jnc (хотя можно было обойтись и без нее) проверяет состояние флага cf. Если он установлен в 1, то это признак того, что результат операции получился больше по размеру, чем размер операнда, и для его корректировки необходимо выполнить некоторые действия. В данном случае мы просто полагаем, что границы операнда расширяются до размера ax, для чего учитываем перенос в старший разряд командой adc (строка 15). Если у вас остались вопросы, исследуйте работу команд сложения без учета знака, для чего введите листинг 3, получите исполняемый модуль, запустите отладчик и откройте в нем окна View|Dump и View|Registers.

Сложение двоичных чисел со знаком

Теперь настала пора раскрыть небольшой секрет. Дело в том, что на самом деле микропроцессор не подозревает о различии между числами со знаком и без знака. Вместо этого у него есть средства фиксирования возникновения характерных ситуаций, складывающихся в процессе вычислений. Некоторые из них мы рассмотрели при обсуждении сложения чисел без знака:
  • флаг переноса cf, установка которого в 1 говорит о том, что произошел выход за пределы разрядности операндов;
  • команду adc, которая учитывает возможность такого выхода (перенос из младшего разряда).
Другое средство — это регистрация состояния старшего (знакового) разряда операнда, которое осуществляется с помощью флага переполнения of в регистре eflags (бит 11).

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

Пример 1.

 30566 = 01110111 01100110
 +
 00687 = 00000010 10101111
 =
 31253 = 01111010 00010101
Следим за переносами из 14-го и 15-го разрядов и правильностью результата: переносов нет, результат правильный.

Пример 2.

 30566 = 01110111 01100110
 +
 30566 = 01110111 01100110
 =
 61132 = 11101110 11001100
Произошел перенос из 14-го разряда; из 15-го разряда переноса нет. Результат неправильный, так как имеется переполнение — значение числа получилось больше, чем то, которое может иметь 16-битное число со знаком (+32 767).

Пример 3.

-30566  = 10001000 10011010
 +
 -04875 = 11101100 11110101
 =
 -35441 = 01110101 10001111
Произошел перенос из 15-го разряда, из 14-го разряда нет переноса. Результат неправильный, так как вместо отрицательного числа получилось положительное (в старшем бите находится 0).

Пример 4.

 -4875 = 11101100 11110101
 +
 -4875 = 11101100 11110101
 =
 -9750 = 11011001 11101010
Есть переносы из 14 и 15-го разрядов. Результат правильный.

Таким образом, мы исследовали все случаи и выяснили, что ситуация переполнения (установка флага of в 1) происходит при переносе:

  • из 14-го разряда (для положительных чисел со знаком);
  • из 15-го разряда (для отрицательных чисел).
И наоборот, переполнения не происходит (то есть флаг of сбрасывается в 0), если есть перенос из обоих разрядов или перенос отсутствует в обоих разрядах.

Итак, переполнение регистрируется с помощью флага переполнения of. Дополнительно к флагу of при переносе из старшего разряда устанавливается в 1 и флаг переноса cf. Так как микропроцессор не знает о существовании чисел со знаком и без знака, то вся ответственность за правильность действий с получившимися числами ложится на программиста. Проанализировать флаги cf и of можно командами условного перехода jc\jnc и jo\jno соответственно.

Что же касается команд сложения чисел со знаком, то они те же, что и для чисел без знака.

Вычитание двоичных чисел без знака

Как и при анализе операции сложения, порассуждаем над сутью процессов, происходящих при выполнении операции вычитания. Если уменьшаемое больше вычитаемого, то проблем нет, — разность положительна, результат верен. Если уменьшаемое меньше вычитаемого, возникает проблема: результат меньше 0, а это уже число со знаком. В этом случае результат необходимо завернуть. Что это означает? При обычном вычитании (в столбик) делают заем 1 из старшего разряда. Микропроцессор поступает аналогично, то есть занимает 1 из разряда, следующего за старшим, в разрядной сетке операнда. Поясним на примере.

Пример 5.

  05 = 00000000 00000101
 -10 = 00000000 00001010
 Для того чтобы произвести вычитание, произведем
воображаемый заем из старшего разряда:
 100000000 00000101
 -
 00000000 00001010
 =
 11111111 11111011
Тем самым по сути выполняется действие

(65 536 + 5) — 10 = 65 531,

0 здесь как бы эквивалентен числу 65 536. Результат, конечно, неверен, но микропроцессор считает, что все нормально, хотя факт заема единицы он фиксирует установкой флага переноса cf. Но посмотрите еще раз внимательно на результат операции вычитания. Это же –5 в дополнительном коде! Проведем эксперимент: представим разность в виде суммы 5 + (–10).

Пример 6.

 5    = 00000000 00000101
 +
 (-10)= 11111111 11110110
 =
        1111111111111011
то есть мы получили тот же результат, что и в предыдущем примере.

Таким образом, после команды вычитания чисел без знака нужно анализировать состояние флага cf. Если он установлен в 1, то это говорит о том, что произошел заем из старшего разряда и результат получился в дополнительном коде.

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

  • dec операнд — операция декремента, то есть уменьшения значения операнда на 1;
  • sub операнд_1,операнд_2 — команда вычитания; ее принцип действия:
    операнд_1 = операнд_1 – операнд_2
  • sbb операнд_1,операнд_2 — команда вычитания с учетом заема (флага cf ):
    операнд_1 = операнд_1 – операнд_2 – значение_cf
Как видите, среди команд вычитания есть команда sbb, учитывающая флаг переноса cf. Эта команда подобна adc, но теперь уже флаг cf выполняет роль индикатора заема 1 из старшего разряда при вычитании чисел.

Рассмотрим пример (листинг 4) программной обработки ситуации, разобранной в примере 6.
 Листинг 4. Проверка при вычитании чисел без знака
<1> ;prg_8_4.asm
<2> masm
<3> model small
<4> stack 256
<5> .data
<6> .code ;сегмент кода
<7> main: ;точка входа в программу
<8> ...
<9> xor ax,ax
<10> mov al,5
<11> sub al,10
<12> jnc m1 ;нет переноса?
<13> neg al ;в al модуль результата
<14> m1: ...
<15> exit:
<16> mov ax,4c00h ;стандартный выход
<17> int 21h
<18> end main ;конец программы
В этом примере в строке 11 выполняется вычитание. С указанными для этой команды вычитания исходными данными результат получается в дополнительном коде (отрицательный). Для того чтобы преобразовать результат к нормальному виду (получить его модуль), применяется команда neg, с помощью которой получается дополнение операнда. В нашем случае мы получили дополнение дополнения или модуль отрицательного результата. А тот факт, что это на самом деле число отрицательное, отражен в состоянии флага cf. Дальше все зависит от алгоритма обработки. Исследуйте программу в отладчике.

Вычитание двоичных чисел со знаком

Здесь все несколько сложнее. Последний пример (листинг 4) показал то, что микропроцессору незачем иметь два устройства — сложения и вычитания. Достаточно наличия только одного — устройства сложения. Но для вычитания способом сложения чисел со знаком в дополнительном коде необходимо представлять оба операнда — и уменьшаемое, и вычитаемое. Результат тоже нужно рассматривать как значение в дополнительном коде. Но здесь возникают сложности. Прежде всего они связаны с тем, что старший бит операнда рассматривается как знаковый. Рассмотрим пример вычитания 45 – (–127).

Пример 7.

Вычитание чисел со знаком 1
   45 = 0010 1101
 -
 -127 = 1000 0001
 =
  -44 = 1010 1100
Судя по знаковому разряду, результат получился отрицательный, что, в свою очередь, говорит о том, что число нужно рассматривать как дополнение, равное –44. Правильный результат должен быть равен 172. Здесь мы, как и в случае знакового сложения, встретились с переполнением мантиссы, когда значащий разряд числа изменил знаковый разряд операнда. Отследить такую ситуацию можно по содержимому флага переполнения of. Его установка в 1 говорит о том, что результат вышел за диапазон представления знаковых чисел (то есть изменился старший бит) для операнда данного размера, и программист должен предусмотреть действия по корректировке результата.

Другой пример разности рассматривается в примере 7, но выполним мы ее способом сложения.

Пример 8.

Вычитание чисел со знаком 2
 -45 — 45 = -45 + (-45)= -90.
 -45 = 1101 0011
 +
 -45 = 1101 0011
 =
 -90 = 1010 0110
Здесь все нормально, флаг переполнения of сброшен в 0, а 1 в знаковом разряде говорит о том, что значение результата — число в дополнительном коде.

Вычитание и сложение операндов большой размерности

Если вы заметили, команды сложения и вычитания работают с операндами фиксированной размерности: 8, 16, 32 бит. А что делать, если нужно сложить числа большей размерности, например 48 бит, используя 16-разрядные операнды? К примеру, сложим два 48-разрядных числа:

Рис. 5. Сложение операндов большой размерности

На рис. 5 по шагам показана технология сложения длинных чисел. Видно, что процесс сложения многобайтных чисел происходит так же, как и при сложении двух чисел “в столбик”, — с осуществлением, при необходимости, переноса 1 в старший разряд. Если нам удастся запрограммировать этот процесс, то мы значительно расширим диапазон двоичных чисел, над которыми мы сможем выполнять операции сложения и вычитания.

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

В завершение обсуждения команд сложения и вычитания отметим, что кроме флагов cf и of в регистре eflags есть еще несколько флагов, которые можно использовать с двоичными арифметическими командами. Речь идет о следующих флагах:

  • zf — флаг нуля, который устанавливается в 1, если результат операции равен 0, и в 1, если результат не равен 0;
  • sf — флаг знака, значение которого после арифметических операций (и не только) совпадает со значением старшего бита результата, то есть с битом 7, 15 или 31. Таким образом, этот флаг можно использовать для операций над числами со знаком.

Умножение чисел без знака

Для умножения чисел без знака предназначена команда

mul сомножитель_1

Как видите, в команде указан всего лишь один операнд-сомножитель. Второй операнд — сомножитель_2 задан неявно. Его местоположение фиксировано и зависит от размера сомножителей. Так как в общем случае результат умножения больше, чем любой из его сомножителей, то его размер и местоположение должны быть тоже определены однозначно. Варианты размеров сомножителей и размещения второго операнда и результата приведены в табл. 2.

Таблица 2. Расположение операндов и результата при умножении

сомножитель_1 | сомножитель_2 | Результат
Байт | al | 16 бит в ax: al — младшая часть результата; ah — старшая часть результата
Слово | ax | 32 бит в паре dx:ax: ax — младшая часть результата; dx — старшая часть результата
Двойное слово | eax | 64 бит в паре edx:eax: eax — младшая часть результата; edx — старшая часть результата

Из таблицы видно, что произведение состоит из двух частей и в зависимости от размера операндов размещается в двух местах — на месте сомножитель_2 (младшая часть) и в дополнительном регистре ah, dx, edx (старшая часть). Как же динамически (то есть во время выполнения программы) узнать, что результат достаточно мал и уместился в одном регистре или что он превысил размерность регистра и старшая часть оказалась в другом регистре? Для этого привлекаются уже известные нам по предыдущему обсуждению флаги переноса cf и переполнения of:

  • если старшая часть результата нулевая, то после операции произведения флаги cf = 0 и of = 0;
  • если же эти флаги ненулевые, то это означает, что результат вышел за пределы младшей части произведения и состоит из двух частей, что и нужно учитывать при дальнейшей работе.
Рассмотрим следующий пример программы.

 Листинг 5. Умножение
<1> ;prg_8_5.asm
<2> masm
<3> model small
<4> stack 256
<5> .data ;сегмент данных
<6> rez label word
<7> rez_l db 45
<8> rez_h db 0
<9> .code ;сегмент кода
<10> main: ;точка входа в программу
<11> ...
<12> xor ax,ax
<13> mov al,25
<14> mul rez_l
<15> jnc m1 ;если переполнение, то на м1
<16> mov rez_h,ah ;старшую часть результата в rez_h
<17> m1:
<18> mov rez_l,al
<19> exit:
<20> mov ax,4c00h ;стандартный выход
<21> int 21h
<22> end main ;конец программы
В этой программе в строке 14 производится умножение значения в rez_l на число в регистре al. Согласно информации в табл. 2, результат умножения будет располагаться в регистре al (младшая часть) и регистре ah (старшая часть). Для выяснения размера результата в строке 15 командой условного перехода jnc анализируется состояние флага cf и если оно не равно 1, то результат остался в рамках регистра al. Если же cf = 1, то выполняется команда в строке 16, которая формирует в поле rez_h старшее слово результата. Команда в строке 18 формирует младшую часть результата. Теперь обратите внимание на сегмент данных, а именно, на строку 6. В этой строке содержится директива label. Мы еще не раз будем сталкиваться с этой директивой. В данном случае она назначает еще одно символическое имя rez адресу, на который уже указывает другой идентификатор rez_l. Отличие заключается в типах этих идентификаторов — имя rez имеет тип слова, который ему назначается директивой label (имя типа указано в качестве операнда label). Введя эту директиву в программе, мы подготовились к тому, что, возможно, результат операции умножения будет занимать слово в памяти. Обратите внимание, что мы не нарушили принципа: младший байт по младшему адресу. Далее, используя имя rez, можно обращаться к значению в этой области как к слову.

Умножение чисел со знаком

Для умножения чисел со знаком предназначена команда

imul операнд_1[,операнд_2,операнд_3]

Эта команда выполняется так же, как и команда mul. Отличительной особенностью команды imul является только формирование знака.

Если результат мал и умещается в одном регистре (то есть если cf = of = 0), то содержимое другого регистра (старшей части) является расширением знака — все его биты равны старшему биту (знаковому разряду) младшей части результата.

В противном случае (если cf = of = 1) знаком результата является знаковый бит старшей части результата, а знаковый бит младшей части является значащим битом двоичного кода результата.

Если вы посмотрите описание команды imul, то увидите, что она допускает более широкие возможности по заданию местоположения операндов. Это сделано для удобства использования.

Деление чисел без знака

Для деления чисел без знака предназначена команда

div делитель

Делитель может находиться в памяти или в регистре и иметь размер 8, 16 или 32 бит. Местонахождение делимого фиксировано и так же, как в команде умножения, зависит от размера операндов. Результатом команды деления являются значения частного и остатка.

Варианты местоположения и размеров операндов операции деления показаны в табл. 3.

Таблица 3. Расположение операндов и результата при делении

Делимое | Делитель | Частное | Остаток
16 бит в регистре ax | Байт регистр или ячейка памяти | Байт в регистре al | Байт в регистре ah
32 бит dx — старшая часть ax — младшая часть | Слово 16 бит регистр или ячейка памяти | Слово 16 бит в регистре ax | Слово 16 бит в регистре dx
64 бит edx — старшая часть eax — младшая часть | Двойное слово 32 бит регистр или ячейка памяти | Двойное слово 32 бит в регистре eax | Двойное слово 32 бит в регистре edx

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

  • делитель равен нулю;
  • частное не входит в отведенную под него разрядную сетку, что может случиться в следующих случаях:
    • при делении делимого величиной в слово на делитель величиной в байт, причем значение делимого в более чем 256 раз больше значения делителя;
    • при делении делимого величиной в двойное слово на делитель величиной в слово, причем значение делимого в более чем 65 536 раз больше значения делителя;
    • при делении делимого величиной в учетверенное слово на делитель величиной в двойное слово, причем значение делимого в более чем 4 294 967 296 раз больше значения делителя.
К примеру, выполним деление значения в области del на значение в области delt (листинг 6).
 Листинг 6. Деление чисел
<1> ;prg_8.6.asm
<2> masm
<3> model small
<4> stack 256
<5> .data
<6> del_b label byte
<7> deldw 29876
<8> delt db 45
<9> .code ;сегмент кода
<10> main: ;точка входа в программу
<11> ...
<12> xor ax,ax
<13> ;последующие две команды можно заменить одной mov
 ax,del
<14> mov ah,del_b ;старший байт делимого в ah
<15> mov al,del_b+1 ;младший байт делимого в al
<16> div delt ;в al — частное, в ah — остаток
<17> ...
<18> endmain ;конец программы

Деление чисел со знаком

Для деления чисел со знаком предназначена команда

idiv делитель

Для этой команды справедливы все рассмотренные положения, касающиеся команд и чисел со знаком. Отметим лишь особенности возникновения исключения 0, “деление на ноль”, в случае чисел со знаком. Оно возникает при выполнении команды idiv по одной из следующих причин:

  • делитель равен нулю;
  • частное не входит в отведенную для него разрядную сетку.
Последнее в свою очередь может произойти:
  • при делении делимого величиной в слово со знаком на делитель величиной в байт со знаком, причем значение делимого в более чем 128 раз больше значения делителя (таким образом, частное не должно находиться вне диапазона от –128 до +127);
  • при делении делимого величиной в двойное слово со знаком на делитель величиной в слово со знаком, причем значение делимого в более чем 32 768 раз больше значения делителя (таким образом, частное не должно находиться вне диапазона от –32 768 до +32 768);
  • при делении делимого величиной в учетверенное слово со знаком на делитель величиной в двойное слово со знаком, причем значение делимого в более чем 2 147 483 648 раз больше значения делителя (таким образом, частное не должно находиться вне диапазона от –2 147 483 648 до +2 147 483 647).

Вспомогательные команды для целочисленных операций

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

Команды преобразования типов

Что делать, если размеры операндов, участвующих в арифметических операциях, разные? Например, предположим, что в операции сложения один операнд является словом, а другой занимает двойное слово. Выше сказано, что в операции сложения должны участвовать операнды одного формата. Если числа без знака, то выход найти просто. В этом случае можно на базе исходного операнда сформировать новый (формата двойного слова), старшие разряды которого просто заполнить нулями. Сложнее ситуация для чисел со знаком: как динамически, в ходе выполнения программы, учесть знак операнда? Для решения подобных проблем в системе команд микропроцессора есть так называемые команды преобразования типа. Эти команды расширяют байты в слова, слова — в двойные слова и двойные слова — в учетверенные слова (64-разрядные значения). Команды преобразования типа особенно полезны при преобразовании целых со знаком, так как они автоматически заполняют старшие биты вновь формируемого операнда значениями знакового бита старого объекта. Эта операция приводит к целым значениям того же знака и той же величины, что и исходная, но уже в более длинном формате. Подобное преобразование называется операцией распространения знака.

Существуют два вида команд преобразования типа:

  • Команды без операндов — эти команды работают с фиксированными регистрами:
    • cbw (Convert Byte to Word) — команда преобразования байта (в регистре al) в слово (в регистре ax) путем распространения значения старшего бита al на все биты регистра ah;
    • cwd (Convert Word to Double) — команда преобразования слова (в регистре ax) в двойное слово (в регистрах dx:ax) путем распространения значения старшего бита ax на все биты регистра dx;
    • cwde (Convert Word to Double) — команда преобразования слова (в регистре ax) в двойное слово (в регистре eax) путем распространения значения старшего бита ax на все биты старшей половины регистра eax;
    • cdq (Convert Double Word to Quarter Word) — команда преобразования двойного слова (в регистре eax) в учетверенное слово (в регистрах edx:eax) путем распространения значения старшего бита eax на все биты регистра edx.
  • Команды movsx и movzx, относящиеся к командам обработки строк (см. урок 11). Эти команды обладают полезным свойством в контексте нашей проблемы:
    • movsx операнд_1,операнд_2 — переслать с распространением знака. Расширяет 8 или 16-разрядное значение операнд_2, которое может быть регистром или операндом в памяти, до 16 или 32-разрядного значения в одном из регистров, используя значение знакового бита для заполнения старших позиций операнд_1. Данную команду удобно использовать для подготовки операндов со знаками к выполнению арифметических действий;
    • movzx операнд_1,операнд_2 — переслать с расширением нулем. Расширяет 8 или 16-разрядное значение операнд_2 до 16 или 32-разрядного с очисткой (заполнением) нулями старших позиций операнд_2. Данную команду удобно использовать для подготовки операндов без знака к выполнению арифметических действий.

К примеру, вычислим значение y = (a + b)/c, где a, b, c — байтовые знаковые переменные (листинг 7).

 Листинг 7. Вычисление простого выражения
<1> ;prg_8_9.asm
<2> masm
<3> model small
<4> stack 256
<5> .data
<6> a db ?
<7> b db ?
<8> c db ?
<9> y dw 0
<10> .code
<11> main: ;точка входа в программу
<12> ...
<13> xor ax,ax
<14> mov al,a
<15> cbw
<16> movsx bx,b
<17> add ax,bx
<18> idiv c ;в al — частное, в ah — остаток
<19> exit:
<20> mov ax,4c00h ;стандартный выход
<21> int 21h
<22> end main ;конец программы
В этой программе делимое для команды idiv (строка 17) готовится заранее. Так как делитель имеет размер байта, то делимое должно быть словом. С учетом этого сложение осуществляется параллельно с преобразованием размера результата в слово (строки 13–16). Для примера расширение операндов со знаком производится двумя разными командами — cbw и movsx.

Другие полезные команды

xadd назначение,источник — обмен местами и сложение.

Команда позволяет выполнить последовательно два действия:

  • обменять значения назначение и источник;
  • поместить на место операнда назначение сумму:
    назначение = назначение + источник.

neg операнд — отрицание с дополнением до двух.

Команда выполняет инвертирование значения операнд. Физически команда выполняет одно действие:
операнд = 0 – операнд, то есть вычитает операнд из нуля.
Команду neg операнд можно применять:

  • для смены знака;
  • для выполнения вычитания из константы.
Дело в том, что команды sub и sbb не позволяют вычесть что-либо из константы, так как константа не может служить операндом-приемником в этих операциях. Поэтому данную операцию можно выполнить с помощью двух команд:
neg ax ;смена знака (ax)
...
add ax,340 ;фактически вычитание: (ax)=340-(ax)

Арифметические операции над двоично-десятичными числами

В данном разделе мы рассмотрим особенности каждого из четырех основных арифметических действия для упакованных и неупакованных двоично-десятичных чисел:
  • сложение неупакованных BCD-чисел;
  • вычитание неупакованных BCD-чисел;
  • умножение неупакованных BCD-чисел;
  • деление неупакованных BCD-чисел;
  • сложение упакованных BCD-чисел;
  • вычитание упакованных BCD-чисел.

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

  • значения величин в формате слова и двойного слова имеют ограниченный диапазон. Если программа предназначена для работы в области финансов, то ограничение суммы в рублях величиной 65 536 (для слова) или даже 4 294 967 296 (для двойного слова) будет существенно сужать сферу ее применения (да еще в наших экономических условиях — тут уж никакая деноминация не поможет);
  • наличие ошибок округления. Представляете себе программу, работающую где-нибудь в банке, которая не учитывает величину остатка при действиях с целыми двоичными числами и оперирует при этом миллиардами? Не хотелось бы быть автором такой программы. Применение чисел с плавающей точкой не спасет — там существует та же проблема округления;
  • представление большого объема результатов в символьном виде (ASCII-коде). Деловые программы не просто выполняют вычисления; одной из целей их использования является оперативная выдача информации пользователю. Для этого, естественно, информация должна быть представлена в символьном виде. Перевод чисел из двоичного кода в ASCII- код, как мы уже видели, требует определенных вычислительных затрат. Число с плавающей точкой еще труднее перевести в символьный вид. А вот если посмотреть на шестнадцатеричное представление неупакованной десятичной цифры (в начале нашего занятия) и на соответствующий ей символ в таблице ASCII, то видно что они отличаются на величину 30h. Таким образом, преобразование в символьный вид и обратно получается намного проще и быстрее.
Наверняка, вы уже убедились в важности овладения хотя бы основами действий с десятичными числами. Далее рассмотрим особенности выполнения основных арифметических операций с десятичными числами. Для предупреждения возможных вопросов отметим сразу тот факт, что отдельных команд сложения, вычитания, умножения и деления BCD-чисел нет. Сделано это по вполне понятным причинам: размерность таких чисел может быть сколь угодно большой. Складывать и вычитать можно двоично-десятичные числа как в упакованном формате, так и в неупакованном, а вот делить и умножать можно только неупакованные BCD-числа. Почему это так, будет видно из дальнейшего обсуждения.

Арифметические действия над неупакованными BCD-числами

Сложение неупакованных BCD-чисел

Рассмотрим два случая сложения.

Пример 9.

Результат сложения не больше 9
 6 = 0000 0110
 +
 3 = 0000 0011
 =
 9 = 0000 1001
Переноса из младшей тетрады в старшую нет. Результат правильный.

Пример 10.

Результат сложения больше 9
 06 = 0000 0110
 +
 07 = 0000 0111
 =
 13 = 0000 1101
То есть мы получили уже не BCD-число. Результат неправильный. Правильный результат в неупакованном BCD-формате должен быть таким:
0000 0001 0000 0011 в двоичном представлении (или 13 в десятичном).

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

Назначение этих команд — в корректировке результата работы обычных арифметических команд для случаев когда операнды в них являются BCD-числами.

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

aaa (ASCII Adjust for Addition) — коррекция результата сложения для представления в символьном виде.

Эта команда не имеет операндов. Она работает неявно только с регистром al и анализирует значение его младшей тетрады:

  • если это значение меньше 9, то флаг cf сбрасывается в 0 и осуществляется переход к следующей команде;
  • если это значение больше 9, то выполняются следующие действия:
    • к содержимому младшей тетрады al (но не к содержимому всего регистра!) прибавляется 6, тем самым значение десятичного результата корректируется в правильную сторону;
    • флаг cf устанавливается в 1, тем самым фиксируется перенос в старший разряд, для того чтобы его можно было учесть в последующих действиях.
Так, в примере 10, предполагая, что значение суммы 0000 1101 находится в al, после команды aaa в регистре будет 1101 + 0110= 0011, то есть двоичное 0000 0011 или десятичное 3, а флаг cf установится в 1, то есть перенос запомнился в микропроцессоре. Далее программисту нужно будет использовать команду сложения adc, которая учтет перенос из предыдущего разряда. Приведем пример программы сложения двух неупакованных BCD-чисел.
Листинг 8. Сложение неупакованных BCD-чисел
<1> ;prg_8_8.asm
<2> ...
<3> .data
<4> lenequ 2 ;разрядность числа
<5> b db 1,7 ;неупакованное число 71
<6> c db 4,5 ;неупакованное число 54
<7> sum db 3 dup (0)
<8> .code
<9> main: ;точка входа в программу
<10> ...
<11> xor bx,bx
<12> mov cx,len
<13> m1:
<14> mov al,b[bx]
<15> adс al,c[bx]
<16> aaa
<17> mov sum[bx],al
<18> inc bx
<19> loop m1
<20> adc sum[bx],0
<21> ...
<22> exit:

В листинге 8 есть несколько интересных моментов, над которыми есть смысл поразмыслить. Начнем с описания BCD-чисел. Из строк 5 и 6 видно, что порядок их ввода обратен нормальному, то есть цифры младших разрядов расположены по меньшему адресу. Но это вполне логично по нескольким причинам:
  • во-первых, такой порядок удовлетворяет общему принципу представления данных для микропроцессоров Intel;
  • во-вторых, это очень удобно для поразрядной обработки неупакованных BCD-чисел, так как каждое из них занимает один байт.
Хотя, как уже было отмечено, программист сам волен выбирать способ описания BCD-чисел в сегменте данных. Строки 14–15 содержат команды, которые складывают цифры в очередных разрядах BCD-чисел, при этом учитывается возможный перенос из младшего разряда. Команда aaa в строке 16 корректирует результат сложения, формируя в al BCD-цифру и, при необходимости, устанавливая в 1 флаг cf. Строка 20 учитывает возможность переноса при сложении цифр из самых старших разрядов чисел. Результат сложения формируется в поле sum, описанном в строке 7.

Вычитание неупакованных BCD-чисел

Ситуация здесь вполне аналогична сложению. Рассмотрим те же случаи.

Пример 11.

Результат вычитания не больше 9
 6 = 0000 0110
 -
 3 = 0000 0011
 =
 3 = 0000 0011
Как видим, заема из старшей тетрады нет. Результат верный и корректировки не требует.

Пример 12.

Результат вычитания больше 9
  6 = 0000 0110
 -
  7 = 0000 0111
 =
 -1 = 1111 1111
Вычитание проводится по правилам двоичной арифметики. Поэтому результат не является BCD-числом.
Правильный результат в неупакованном BCD-формате должен быть 9 (0000 1001 в двоичной системе счисления). При этом предполагается заем из старшего разряда, как при обычной команде вычитания, то есть в случае с BCD числами фактически должно быть выполнено вычитание 16 – 7. Таким образом видно, что, как и в случае сложения, результат вычитания нужно корректировать. Для этого существует специальная команда:

aas (ASCII Adjust for Substraction) — коррекция результата вычитания для представления в символьном виде.

Команда aas также не имеет операндов и работает с регистром al, анализируя его младшую тетраду следующим образом:

  • если ее значение меньше 9, то флаг cf сбрасывается в 0 и управление передается следующей команде;
  • если значение тетрады в al больше 9, то команда aas выполняет следующие действия:
    1. из содержимого младшей тетрады регистра al (заметьте — не из содержимого всего регистра) вычитает 6;
    2. обнуляет старшую тетраду регистра al;
    3. устанавливает флаг cf в 1, тем самым фиксируя воображаемый заем из старшего разряда.
Понятно, что команда aas применяется вместе с основными командами вычитания sub и sbb. При этом команду sub есть смысл использовать только один раз, при вычитании самых младших цифр операндов, далее должна применяться команда sbb, которая будет учитывать возможный заем из старшего разряда. В листинге 9 мы обходимся одной командой sbb, которая в цикле производит поразрядное вычитание двух BCD-чисел.
Листинг 9. Вычитание неупакованных BCD-чисел
<1> ;prg_8_9.asm
<2> masm
<3> model small
<4> stack 256
<5> .data ;сегмент данных
<6> b db 1,7 ;неупакованное число 71
<7> c db 4,5 ;неупакованное число 54
<8> subs db 2 dup (0)
<9> .code
<10> main: ;точка входа в программу
<11> mov ax,@data ;связываем регистр dx с сегментом
<12> mov ds,ax ;данных через регистр ax
<13> xor ax,ax ;очищаем ax
<14> lenequ 2 ;разрядность чисел
<15> xor bx,bx
<16> mov cx,len ;загрузка в cx счетчика цикла
<17> m1:
<18> mov al,b[bx]
<19> sbb al,c[bx]
<20> aas
<21> mov subs[bx],al
<22> inc bx
<23> loop m1
<24> jc m2 ;анализ флага заема
<25> jmp exit
<26> m2:...
<27> exit:
<28> mov ax,4c00h ;стандартный выход
<29> int 21h
<30> end main ;конец программы

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

Умножение неупакованных BCD-чисел

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

Для того чтобы умножать числа произвольной размерности, нужно реализовать процесс умножения самостоятельно, взяв за основу некоторый алгоритм умножения, например “в столбик”.

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

  • поместить один из сомножителей в регистр al (как того требует команда mul);
  • поместить второй операнд в регистр или память, отведя байт;
  • перемножить сомножители командой mul (результат, как и положено, будет в ax);
  • результат, конечно, получится в двоичном коде, поэтому его нужно скорректировать.
Для коррекции результата после умножения применяется специальная команда

aam (ASCII Adjust for Multiplication) — коррекция результата умножения для представления в символьном виде.

Она не имеет операндов и работает с регистром ax следующим образом:

  • делит al на 10;
  • результат деления записывается так: частное в al, остаток в ah.
В результате после выполнения команды aam в регистрах al и ah находятся правильные двоично-десятичные цифры произведения двух цифр.

В листинге 10 приведен пример умножения BCD-числа произвольной размерности на однозначное BCD-число.
 Листинг 10. Умножение неупакованных BCD-чисел
<1> masm
<2> model small
<3> stack 256
<4> .data
<5> b db 6,7 ;неупакованное число 76
<6> c db 4 ;неупакованное число 4
<7> proizv db 4 dup (0)
<8> .code
<9> main: ;точка входа в программу
<10> mov ax,@data
<11> mov ds,ax
<12> xor ax,ax
<13> lenequ 2 ;размерность сомножителя 1
<14> xor bx,bx
<15> xor si,si
<16> xor di,di
<17> mov cx,len ;в cx длина наибольшего сомножителя  1
<18> m1:
<19> mov al,b[si]
<20> mul c
<21> aam ;коррекция умножения
<22> adc al,dl ;учли предыдущий перенос
<23> aaa ;скорректировали результат сложения с  переносом
<24> mov dl,ah ; запомнили перенос
<25> mov proizv[bx],al
<26> inc si
<27> inc bx
<28> loop m1
<29> mov proizv[bx],dl ;учли последний перенос
<30> exit:
<31> mov ax,4c00h
<32> int 21h
<33> end main
Данную программу можно легко модифицировать для умножения BCD-чисел произвольной длины. Для этого достаточно представить алгоритм умножения “в столбик”. Листинг 10 можно использовать для получения частичных произведений в этом алгоритме. После их сложения со сдвигом получиться искомый результат.

Перед окончанием обсуждения команды aam необходимо отметить еще один вариант ее применения. Эту команду можно применять для преобразования двоичного числа в регистре al в неупакованное BCD-число, которое будет размещено в регистре ax: старшая цифра результата в ah, младшая — в al. Понятно, что двоичное число должно быть в диапазоне 0...99.

Деление неупакованных BCD-чисел

Процесс выполнения операции деления двух неупакованных BCD-чисел несколько отличается от других, рассмотренных ранее, операций с ними. Здесь также требуются действия по коррекции, но они должны выполняться до основной операции, выполняющей непосредственно деление одного BCD-числа на другое BCD-число. Предварительно в регистре ax нужно получить две неупакованные BCD-цифры делимого. Это делает программист удобным для него способом. Далее нужно выдать команду aad:

aad (ASCII Adjust for Division) — коррекция деления для представления в символьном виде.

Команда не имеет операндов и преобразует двузначное неупакованное BCD-число в регистре ax в двоичное число. Это двоичное число впоследствии будет играть роль делимого в операции деления. Кроме преобразования, команда aad помещает полученное двоичное число в регистр al. Делимое, естественно, будет двоичным числом из диапазона 0...99.
Алгоритм, по которому команда aad осуществляет это преобразование, состоит в следующем:

  • умножить старшую цифру исходного BCD-числа в ax (содержимое ah) на 10;
  • выполнить сложение ah + al, результат которого (двоичное число) занести в al;
  • обнулить содержимое ah.
Далее программисту нужно выдать обычную команду деления div для выполнения деления содержимого ax на одну BCD-цифру, находящуюся в байтовом регистре или байтовой ячейке памяти.

Деление неупакованных BCD-чисел иллюстрируется листингом 11.
 Листинг 11. Деление неупакованных BCD-чисел
<1> ;prg_8_11.asm
<2> ...
<3> .data ;сегмент данных
<4> b db 1,7 ;неупакованное BCD-число 71
<5> c db 4 ;
<6> ch db 2 dup (0)
<7> .code ;сегмент кода
<8> main: ;точка входа в программу
<9> ...
<10> mov al,b
<11> aad ;коррекция перед делением
<12> div c ;в al BCD-частное, в ah BCD-остаток
<13> ...
<14> exit:
Аналогично aam, команде aad можно найти и другое применение — использовать ее для перевода неупакованных BCD-чисел из диапазона 0...99 в их двоичный эквивалент.

Для деления чисел большей разрядности, так же как и в случае умножения, нужно реализовывать свой алгоритм, например “в столбик”, либо найти более оптимальный путь.

Арифметические действия над упакованными BCD-числами

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

Сложение упакованных BCD-чисел

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

Пример 13.

Сложение упакованных BCD-чисел
 67  = 0110 0111
 +
 75  = 0111 0101
 =
 142 = 1101 1100 = 220
Как видим, в двоичном виде результат равен 1101 1100 (или 220 в десятичном представлении), что неверно. Это происходит по той причине, что микропроцессор не подозревает о существовании BCD-чисел и складывает их по правилам сложения двоичных чисел. На самом деле, результат в двоично-десятичном виде должен быть равен 0001 0100 0010 (или 142 в десятичном представлении).

Видно, что как и для неупакованных BCD-чисел, для упакованных BCD-чисел существует потребность как-то корректировать результаты арифметических операций.
Микропроцессор предоставляет для этого команду daa:

daa (Decimal Adjust for Addition) — коррекция результата сложения для представления в десятичном виде.
Команда daa преобразует содержимое регистра al в две упакованные десятичные цифры по алгоритму, приведенному в описании команды daa.
Получившаяся в результате сложения единица (если результат сложения больше 99) запоминается в флаге cf, тем самым учитывается перенос в старший разряд.

Проиллюстрируем сказанное на примере сложения двух двузначных BCD-чисел в упакованном формате (листинг 12).
 Листинг 12. Сложение упакованных BCD-чисел
<1> ;prg_8_12.asm
<2> ...
<3> .data ;сегмент данных
<4> b db 17h ;упакованное число 17h
<5> c db 45h ;упакованное число 45
<6> sumdb 2 dup (0)
<7> .code ;сегмент кода
<8> main: ;точка входа в программу
<9> ...
<10> mov al,b
<11> add al,c
<12> daa
<13> jnc $+4 ;переход через команду, если результат <= 99
<14> mov sum+1,ah ;учет переноса при сложении (результат > 99)
<15> mov sum,al ;младшие упакованные цифры результата
<16> exit:
В приведенном примере все достаточно прозрачно, единственное, на что следует обратить внимание, — это описание упакованных BCD-чисел и порядок формирования результата. Результат формируется в соответствии с основным принципом работы микропроцессоров Intel: младший байт по младшему адресу.

Вычитание упакованных BCD-чисел

Аналогично сложению, микропроцессор рассматривает упакованные BCD-числа как двоичные и, соответственно, выполняет вычитание BCD-чисел как двоичных.

Пример 14.

Вычитание упакованных BCD-чисел
Выполним вычитание 67-75. Так как микропроцессор
выполняет вычитание способом сложения, то и мы последуем
этому:
 67 = 0110 0111
 +
 -75 = 1011 0101
 =
  -8 = 0001 1100 = 28 ???
Как видим, результат равен 28 в десятичной системе счисления, что является абсурдом. В двоично-десятичном коде результат должен быть равен 0000 1000 (или 8 в десятичной системе счисления).

При программировании вычитания упакованных BCD-чисел программист, как и при вычитании неупакованных BCD-чисел, должен сам осуществлять контроль за знаком. Это делается с помощью флага cf, который фиксирует заем из старших разрядов.
Само вычитание BCD-чисел осуществляется простой командой вычитания sub или sbb. Коррекция результата осуществляется командой das:

das (Decimal Adjust for Substraction) — коррекция результата вычитания для представления в десятичном виде.
Команда das преобразует содержимое регистра al в две упакованные десятичные цифры по алгоритму, приведенному в описании команды das.


Логические команды

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

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

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

Логические данные

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

Исчисление высказываний очень гармонично сочетается с принципами работы компьютера и основными методами его программирования. Все аппаратные компоненты компьютера построены на логических микросхемах. Система представления информации в компьютере на самом нижнем уровне основана на понятии бита. Бит, имея всего два состояния: 0 (ложно) и 1 (истинно), — естественным образом вписывается в исчисление высказываний.

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

  • отрицание (логическое НЕ);
  • логическое сложение (логическое включающее ИЛИ);
  • логическое умножение (логическое И);
  • логическое исключающее сложение (логическое исключающее ИЛИ).
  • отрицание (логическое НЕ) — логическая операция над одним операндом, результатом которой является величина, обратная значению исходного операнда.
    Эта операция однозначно характеризуется следующей таблицей истинности (табл. 1).

    Таблица 1. Таблица истинности для логического отрицания

    Значение операнда | 0 | 1
    Результат операции | 1 | 0

  • логическое сложение (логическое включающее ИЛИ) — логическая операция над двумя операндами, результатом которой является “истина” (1), если один или оба операнда имеют значение “истина” (1), и “ложь” (0) — если оба операнда имеют значение “ложь” (0).
    Эта операция описывается с помощью следующей таблицы истинности (табл. 2).

    Таблица 2. Таблица истинности для логического включающего ИЛИ

    Значение операнда 1 | 0 | 0 | 1 | 1
    Значение операнда 2 | 0 | 1 | 0 | 1
    Результат операции | 0 | 1 | 1 | 1

  • логическое умножение (логическое И) — логическая операция над двумя операндами, результатом которой является “истина” (1) только в том случае, если оба операнда имеют значение “истина” (1). Во всех остальных случаях значение операции “ложь” (0).
    Эта операция описывается с помощью следующей таблицы истинности (табл.3).

    Таблица 3. Таблица истинности для логического И

    Значение операнда 1 | 0 | 0 | 1 | 1
    Значение операнда 2 | 0 | 1 | 0 | 1
    Результат операции | 0 | 0 | 0 | 1

  • логическое исключающее сложение (логическое исключающее ИЛИ) — логическая операция над двумя операндами, результатом которой является “истина” (1), если только один из двух операндов имеет значение “истина” (1), и ложь (0), если оба операнда имеют значение “ложь” (0) или “истина” (1).
    Эта операция описывается с помощью следующей таблицы истинности(табл. 4).

    Таблица 4. Таблица истинности для логического исключающего ИЛИ

    Значение операнда 1 | 0 | 0 | 1 | 1
    Значение операнда 2 | 0 | 1 | 0 | 1
    Результат операции | 0 | 1 | 1 | 0

Система команд микропроцессора содержит пять команд, поддерживающих данные операции. Эти команды выполняют логические операции над битами операндов. Размерность операндов, естественно, должна быть одинакова. Например, если размерность операндов равна слову (16 бит), то логическая операция выполняется сначала над нулевыми битами операндов и ее результат записывается на место бита 0 результата. Далее команда последовательно повторяет эти действия над всеми битами с первого до пятнадцатого. Возможные варианты размерности операндов для каждой команды приведены в “Справочнике команд”.

Логические команды

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

and операнд_1,операнд_2 — операция логического умножения.
Команда выполняет поразрядно логическую операцию И (конъюнкцию) над битами операндов операнд_1 и операнд_2. Результат записывается на место операнд_1.

or операнд_1,операнд_2 — операция логического сложения.
Команда выполняет поразрядно логическую операцию ИЛИ (дизъюнкцию) над битами операндов операнд_1 и операнд_2. Результат записывается на место операнд_1.

xor операнд_1,операнд_2 — операция логического исключающего сложения.
Команда выполняет поразрядно логическую операцию исключающего ИЛИ над битами операндов операнд_1 и операнд_2. Результат записывается на место операнд_1.

test операнд_1,операнд_2 — операция “проверить” (способом логического умножения).
Команда выполняет поразрядно логическую операцию И над битами операндов операнд_1 и операнд_2. Состояние операндов остается прежним, изменяются только флаги zf, sf, и pf, что дает возможность анализировать состояние отдельных битов операнда без изменения их состояния.

not операнд — операция логического отрицания.
Команда выполняет поразрядное инвертирование (замену значения на обратное) каждого бита операнда. Результат записывается на место операнда.

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

  • Для установки определенных разрядов (бит) в 1 применяется команда
    or операнд_1,операнд_2.
    В этой команде операнд_2, выполняющий роль маски, должен содержать единичные биты на месте тех разрядов, которые должны быть установлены в 1 в операнд_1.

        or      eax,10b ;установить 1-й бит в регистре eax

  • Для сброса определенных разрядов (бит) в 0 применяется команда
    and операнд_1,операнд_2.
    В этой команде операнд_2, выполняющий роль маски, должен содержать нулевые биты на месте тех разрядов, которые должны быть установлены в 0 в операнд_1.

        and     eax,fffffffdh   ;сбросить в 0 1-й бит в регистре eax

  • Команда xor операнд_1,операнд_2 применяется:
    • для выяснения того, какие биты в операнд_1 и операнд_2 различаются;
    • для инвертирования состояния заданных бит в операнд_1.

        xor     eax,10b ;инвертировать 1-й бит в регистре eax
        jz      mes     ;переход, если 1-й бит в al был единичным

    Интересующие нас биты маски (операнд_2) при выполнении команды xor должны быть единичными, остальные — нулевыми.

  • Для проверки состояния заданных бит применяется команда
    test операнд_1,операнд_2 (проверить операнд_1).
    Проверяемые биты операнд_1 в маске (операнд_2) должны иметь единичное значение. Алгоритм работы команды test подобен алгоритму команды and, но он не меняет значения операнд_1.
    Результатом команды является установка значения флага нуля zf:
    • если zf = 0, то в результате логического умножения получился нулевой результат, то есть один единичный бит маски, который не совпал с соответствующим единичным битом операнд_1;
    • если zf = 1, то в результате логического умножения получился ненулевой результат, то есть хотя бы один единичный бит маски совпал с соответствующим единичным битом операнд_1.

        test    eax,00000010h
        jz      m1      ;переход, если 4-й бит равен 1

Как видно из примера, для реакции на результат команды test целесообразно использовать команду перехода jnz метка (Jump if Not Zero) — переход, если флаг нуля zf ненулевой, или команду с обратным действием — jz метка (Jump if Zero) — переход, если флаг нуля zf = 0.

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

bsf операнд_1,операнд_2 (Bit Scaning Forward) - сканирование битов вперед.
Команда просматривает (сканирует) биты операнд_2 от младшего к старшему (от бита 0 до старшего бита) в поисках первого бита, установленного в 1. Если таковой обнаруживается, в операнд_1 заносится номер этого бита в виде целочисленного значения. Если все биты операнд_2 равны 0, то флаг нуля zf устанавливается в 1, в противном случае флаг zf сбрасывается в 0.

        mov     al,02h
        bsf     bx,al   ;bx=1
        jz      m1      ;переход, если al=00h
        ...

bsr операнд_1,операнд_2 (Bit Scaning Reset) — сканирование битов в обратном порядке.
Команда просматривает (сканирует) биты операнд_2 от старшего к младшему (от старшего бита к биту 0) в поисках первого бита, установленного в 1. Если таковой обнаруживается, в операнд_1 заносится номер этого бита в виде целочисленного значения.
При этом важно, что позиция первого единичного бита слева отсчитывается все равно относительно бита 0. Если все биты операнд_2 равны 0, то флаг нуля zf устанавливается в 1, в противном случае флаг zf сбрасывается в 0.

Листинг 1 демонстрирует пример применения команд bsr и bsf. Введите код и исследуйте работу программы в отладчике (в частности, обратите внимание на то, как меняется содержимое регистра bx после команд bsf и bsr).

 Листинг 1 Сканирование битов
;prg_9_1.asm
masm
model   small
stack   256
.data   ;сегмент данных
.code   ;сегмент кода
main:   ;точка входа в программу
        mov     ax,@data
        mov     ds,ax
;...
.486    ;это обязательно
        xor     ax,ax
        mov     al,02h
        bsf     bx,ax   ;bx=1
        jz      m1      ;переход, если al=00h
        bsr     bx,ax
m1:
;...
        mov     ax,4c00h        ;стандартный выход
        int     21h
end     main

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

bt операнд,смещение_бита (Bit Test) — проверка бита.
Команда переносит значение бита в флаг cf.

        bt      ax,5    ;проверить значение бита 5
        jnc     m1      ;переход, если бит = 0

bts операнд,смещение_бита (Bit Test and Set) — проверка и установка бита.
Команда переносит значение бита в флаг cf и затем устанавливает проверяемый бит в 1.

        mov ax,10
        bts     pole,ax ;проверить и установить 10-й бит в pole
        jсm1    ;переход, если проверяемый бит был равен 1
btr операнд,смещение_бита (Bit Test and Reset) — проверка и сброс бита.
Команда переносит значение бита в флаг cf и затем устанавливает этот бит в 0.

btc операнд,смещение_бита (Bit Test and Convert) — проверка и инвертирование бита.
Команда переносит значение бита в флаг cf и затем инвертирует значение этого бита.

Команды сдвига

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

коп операнд,счетчик_сдвигов

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

  • статически, что предполагает задание фиксированного значения с помощью непосредственного операнда;
  • динамически, что означает занесение значения счетчика сдвигов в регистр cl перед выполнением команды сдвига.

Исходя из размерности регистра cl, понятно, что значение счетчика сдвигов может лежать в диапазоне от 0 до 255. Но на самом деле это не совсем так.
В целях оптимизации микропроцессор воспринимает только значение пяти младших битов счетчика, то есть значение лежит в диапазоне от 0 до 31.
В последних моделях микропроцессора, в том числе и в микропроцессоре Pentium, есть дополнительные команды, позволяющие делать 64-разрядные сдвиги. Мы их рассмотрим чуть позже.

Все команды сдвига устанавливают флаг переноса cf.
По мере сдвига битов за пределы операнда они сначала попадают на флаг переноса, устанавливая его равным значению очередного бита, оказавшегося за пределами операнда. Куда этот бит попадет дальше, зависит от типа команды сдвига и алгоритма программы.

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

  • команды линейного сдвига;
  • команды циклического сдвига.

Команды линейного сдвига

К командам этого типа относятся команды, осуществляющие сдвиг по следующему алгоритму:
  • очередной “выдвигаемый” бит устанавливает флаг cf;
  • бит, вводимый в операнд с другого конца, имеет значение 0;
  • при сдвиге очередного бита он переходит во флаг cf, при этом значение предыдущего сдвинутого бита теряется!
Команды линейного сдвига делятся на два подтипа:
  • команды логического линейного сдвига;
  • команды арифметического линейного сдвига.
·

К командам логического линейного сдвига относятся следующие:

shl операнд,счетчик_сдвигов (Shift Logical Left) - логический сдвиг влево.
Содержимое операнда сдвигается влево на количество битов, определяемое значением счетчик_сдвигов. Справа (в позицию младшего бита) вписываются нули;

shr операнд,счетчик_сдвигов (Shift Logical Right) — логический сдвиг вправо.
Содержимое операнда сдвигается вправо на количество битов, определяемое значением счетчик_сдвигов. Слева (в позицию старшего, знакового бита) вписываются нули. На рис. 1 показан принцип работы этих команд.

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

Ниже показан фрагмент программы, который выполняет преобразование двух неупакованных BCD-чисел в слове памяти bcd_dig в упакованное BCD-число в регистре al.

        ...
bcd_dig dw      0905h   ;описание неупакованного BCD-числа 95
...
        mov     ax,bcd_dig      ;пересылка
        shl     ah,4    ;сдвиг влево
        add     al,ah   ;сложение для получения результата: al=95h
Команды арифметического линейного сдвига отличаются от команд логического сдвига тем, что они особым образом работают со знаковым разрядом операнда.

sal операнд,счетчик_сдвигов (Shift Arithmetic Left) — арифметический сдвиг влево.
Содержимое операнда сдвигается влево на количество битов, определяемое значением счетчик_сдвигов. Справа (в позицию младшего бита) вписываются нули. Команда sal не сохраняет знака, но устанавливает флаг cf в случае смены знака очередным выдвигаемым битом. В остальном команда sal полностью аналогична команде shl;

sar операнд,счетчик_сдвигов (Shift Arithmetic Right) — арифметический сдвиг вправо.
Содержимое операнда сдвигается вправо на количество битов, определяемое значением счетчик_сдвигов. Слева в операнд вписываются нули. Команда sar сохраняет знак, восстанавливая его после сдвига каждого очередного бита.
На рис. 2 показан принцип работы команд линейного арифметического сдвига.

Рис. 2. Схема работы команд линейного арифметического сдвига

Команды циклического сдвига

К командам циклического сдвига относятся команды, сохраняющие значения сдвигаемых бит. Есть два типа команд циклического сдвига:
  • команды простого циклического сдвига;
  • команды циклического сдвига через флаг переноса cf.
К командам простого циклического сдвига относятся:

rol операнд,счетчик_сдвигов (Rotate Left) — циклический сдвиг влево.
Содержимое операнда сдвигается влево на количество бит, определяемое операндом счетчик_сдвигов. Сдвигаемые влево биты записываются в тот же операнд справа.

ror операнд,счетчик_сдвигов (Rotate Right) — циклический сдвиг вправо.
Содержимое операнда сдвигается вправо на количество бит, определяемое операндом счетчик_сдвигов. Сдвигаемые вправо биты записываются в тот же операнд слева.

Рис. 3. Схема работы команд простого циклического сдвига

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

        ...
        mov     eax,ffff0000h
        mov     cl,16
        rol     eax,cl
Команды циклического сдвига через флаг переноса cf отличаются от команд простого циклического сдвига тем, что сдвигаемый бит не сразу попадает в операнд с другого его конца, а записывается сначала в флаг переноса cf. Лишь следующее исполнение данной команды сдвига (при условии, что она выполняется в цикле) приводит к помещению выдвинутого ранее бита с другого конца операнда (см. рис. 4).

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

rcl операнд,счетчик_сдвигов (Rotate through Carry Left) — циклический сдвиг влево через перенос.
Содержимое операнда сдвигается влево на количество бит, определяемое операндом счетчик_сдвигов. Сдвигаемые биты поочередно становятся значением флага переноса cf.

rcr операнд,счетчик_сдвигов (Rotate through Carry Right) — циклический сдвиг вправо через перенос.
Содержимое операнда сдвигается вправо на количество бит, определяемое операндом счетчик_сдвигов. Сдвигаемые биты поочередно становятся значением флага переноса cf.

Рис. 4. Команды циклического сдвига через флаг переноса cf

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

Дополнительные команды сдвига

Система команд последних моделей микропроцессоров Intel, начиная с i80386, содержит дополнительные команды сдвига, расширяющие возможности, рассмотренные нами ранее.
Это — команды сдвигов двойной точности:

shld операнд_1,операнд_2,счетчик_сдвигов — сдвиг влево двойной точности.
Команда shld производит замену путем сдвига битов операнда операнд_1 влево, заполняя его биты справа значениями битов, вытесняемых из операнд_2 согласно схеме на рис. 5. Количество сдвигаемых бит определяется значением счетчик_сдвигов, которое может лежать в диапазоне 0...31. Это значение может задаваться непосредственным операндом или содержаться в регистре cl. Значение операнд_2 не изменяется.

Рис. 5. Схема работы команды shld
shrd

операнд_1,операнд_2,счетчик_сдвигов — сдвиг вправо двойной точности.
Команда производит замену путем сдвига битов операнда операнд_1 вправо, заполняя его биты слева значениями битов, вытесняемых из операнд_2 согласно схеме на рис. 6. Количество сдвигаемых бит определяется значением счетчик_сдвигов, которое может лежать в диапазоне 0...31. Это значение может задаваться непосредственным операндом или содержаться в регистре cl. Значение операнд_2 не изменяется.

Рис. 6. Схема работы команды shrd

Как мы отметили, команды shld и shrd осуществляют сдвиги до 32 разрядов, но за счет особенностей задания операндов и алгоритма работы эти команды можно использовать для работы с полями длиной до 64 бит.
Например, рассмотрим, как можно осуществить сдвиг влево на 16 бит поля из 64 бит.

;...
.data
pole_l  dd      0b21187f5h1
pole_h  dd      45ff6711h
.code
;...
.386
        mov     cl,16   ;загрузка счетчика сдвига в cl
        mov     eax,pole_h
        shld    pole_l,eax,cl
        shl     pole_h,cl       ;pole_l=87f50000h, pole_h=6711b211h


Команды передачи управления

На предыдущих уроках мы познакомились с некоторыми командами, из которых формируются линейные участки программы. Каждая из них в общем случае выполняет некоторые действия по преобразованию или пересылке данных, после чего микропроцессор передает управление следующей команде. Но очень мало программ работают таким последовательным образом. Обычно в программе есть точки, в которых нужно принять решение о том, какая команда будет выполняться следующей. Это решение может быть
  • безусловным — в данной точке необходимо передать управление не той команде, которая идет следующей, а другой, которая находится на некотором удалении от текущей команды;
  • условным — решение о том, какая команда будет выполняться следующей, принимается на основе анализа некоторых условий или данных.
Как вы помните, программа представляет собой последовательность команд и данных, занимающих определенное пространство оперативной памяти. Эта пространство памяти может быть либо непрерывным, либо состоять из нескольких фрагментов.
На уроке 5 нами были рассмотрены средства организации фрагментации кода программы и ее данных на сегменты. То, какая команда программы должна выполняться следующей, микропроцессор узнает по содержимому пары регистров cs:(e)ip:
  • cs — сегментный регистр кода, в котором находится физический (базовый) адрес текущего сегмента кода;
  • eip/ip — регистр указателя команды, в котором находится значение, представляющее собой смещение в памяти следующей команды, подлежащей выполнению, относительно начала текущего сегмента кода.
    Напомню, почему мы записываем регистры eip/ip через косую черту. Какой конкретно регистр будет использоваться, зависит от установленного режима адресации use16 или use32. Если указано use16, то используется ip, если use32, то используется eip.

    Таким образом, команды передачи управления изменяют содержимое регистров cs и eip/ip, в результате чего микропроцессор выбирает для выполнения не следующую по порядку команду программы, а команду в некотором другом участке программы. Конвейер внутри микропроцессора при этом сбрасывается.

По принципу действия, команды микропроцессора, обеспечивающие организацию переходов в программе, можно разделить на четыре группы:
  1. Команды безусловной передачи управления:
    • команда безусловного перехода;
    • вызова процедуры и возврата из процедуры;
    • вызова программных прерываний и возврата из программных прерываний.
  2. Команды условной передачи управления:
    • команды перехода по результату команды сравнения cmp;
    • команды перехода по состоянию определенного флага;
    • команды перехода по содержимому регистра ecx/cx.
  3. Команды управления циклом:
    • команда организации цикла со счетчиком ecx/cx;
    • команда организации цикла со счетчиком ecx/cx с возможностью досрочного выхода из цикла по дополнительному условию.

Безусловные переходы

Предыдущее обсуждение выявило некоторые детали механизма перехода. Команды перехода модифицируют регистр указателя команды eip/ip и, возможно, сегментный регистр кода cs. Что именно должно подвергнуться модификации, зависит:
  • от типа операнда в команде безусловного перехода (ближний или дальний);
  • от указания перед адресом перехода (в команде перехода) модификатора; при этом сам адрес перехода может находиться либо непосредственно в команде (прямой переход), либо в регистре или ячейке памяти (косвенный переход).

Модификатор может принимать следующие значения:

  • near ptr — прямой переход на метку внутри текущего сегмента кода. Модифицируется только регистр eip/ip (в зависимости от заданного типа сегмента кода use16 или use32) на основе указанного в команде адреса (метки) или выражения, использующего символ извлечения значения СчА — $;
  • far ptr — прямой переход на метку в другом сегменте кода. Адрес перехода задается в виде непосредственного операнда или адреса (метки) и состоит из 16-битного селектора и 16/32-битного смещения, которые загружаются, соответственно, в регистры cs и ip/eip;
  • word ptr — косвенный переход на метку внутри текущего сегмента кода. Модифицируется (значением смещения из памяти по указанному в команде адресу, или из регистра) только eip/ip. Размер смещения 16 или 32 бит;
  • dword ptr — косвенный переход на метку в другом сегменте кода. Модифицируются (значением из памяти — и только из памяти, из регистра нельзя) оба регистра, cs и eip/ip. Первое слово/двойное слово этого адреса представляет смещение и загружается в ip/eip; второе/третье слово загружается в cs.

Команда безусловного перехода jmp

Синтаксис команды безусловного перехода
jmp [модификатор] адрес_перехода - безусловный переход без сохранения информации о точке возврата.

Адрес_перехода представляет собой адрес в виде метки либо адрес области памяти, в которой находится указатель перехода.

Всего в системе команд микропроцессора есть несколько кодов машинных команд безусловного перехода jmp.
Их различия определяются дальностью перехода и способом задания целевого адреса.
Дальность перехода определяется местоположением операнда адрес_перехода. Этот адрес может находиться в текущем сегменте кода или в некотором другом сегменте. В первом случае переход называется внутрисегментным, или близким, во втором — межсегментным, или дальним.
Внутрисегментный переход предполагает, что изменяется только содержимое регистра eip/ip.
Можно выделить три варианта внутрисегментного использования команды jmp:

  • прямой короткий;
  • прямой;
  • косвенный.

Процедуры

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

Для описания последовательности команд в виде процедуры в языке ассемблера используются две директивы: PROC и ENDP.

Синтаксис описания процедуры таков (рис. 1).

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

Из рис. 1 видно, что в заголовке процедуры (директиве PROC) обязательным является только задание имени процедуры. Среди большого количества операндов директивы PROC следует особо выделить [расстояние].
Этот атрибут может принимать значения near или far и характеризует возможность обращения к процедуре из другого сегмента кода. По умолчанию атрибут [расстояние] принимает значение near.

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

Условные переходы

Микропроцессор имеет 18 команд условного перехода (см. “Описание команд”). Эти команды позволяют проверить:
  • отношение между операндами со знаком (“больше — меньше”);
  • отношение между операндами без знака (“выше — ниже”)2;
  • состояния арифметических флагов zf, sf, cf, of, pf (но не af).
Команды условного перехода имеют одинаковый синтаксис:
jcc метка_перехода

Как видно, мнемокод всех команд начинается с “j” — от слова jump (прыжок), cc — определяет конкретное условие, анализируемое командой.
Что касается операнда метка_перехода, то эта метка может находится только в пределах текущего сегмента кода, межсегментная передача управления в условных переходах не допускается. В связи с этим отпадает вопрос о модификаторе, который присутствовал в синтаксисе команд безусловного перехода. В ранних моделях микропроцессора (i8086, i80186 и i80286) команды условного перехода могли осуществлять только короткие переходы — на расстояние от –128 до +127 байт от команды, следующей за командой условного перехода. Начиная с модели микропроцессора 80386 это ограничение снято, но, как видите, только в пределах текущего сегмента кода.

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

  • любая команда, изменяющая состояние арифметических флагов;
  • команда сравнения cmp, сравнивающая значения двух операндов;
  • состояние регистра ecx/cx.
Обсудим эти варианты, чтобы разобраться с тем, как работают команды условного перехода.

Команда сравнения cmp

Команда сравнения cmp имеет интересный принцип работы. Он абсолютно такой же, как и у команды вычитания.
sub операнд_1,операнд_2. Команда cmp так же, как и команда sub, выполняет вычитание операндов и устанавливает флаги. Единственное, чего она не делает — это запись результата вычитания на место первого операнда.

Синтаксис команды cmp:
cmp операнд_1,операнд_2 (compare) — сравнивает два операнда и по результатам сравнения устанавливает флаги.

Флаги, устанавливаемые командой cmp, можно анализировать специальными командами условного перехода. Прежде чем мы их рассмотрим, уделим немного внимания мнемонике этих команд условного перехода (табл. 1). Понимание обозначений при формировании названия команд условного перехода (элемент в названии команды jcc, обозначенный нами cc) облегчит их запоминание и дальнейшее практическое использование.

Таблица 1. Значение аббревиатур в названии команды jcc

Мнемоническое обозначение | Английский | Русский | Тип операндов
E e | equal | Равно | Любые
N n | not | Не | Любые
G g | greater | Больше | Числа со знаком
L l | less | Меньше | Числа со знаком
A a | above | Выше, в смысле “больше” | Числа без знака
B b | below | Ниже, в смысле “меньше” | Числа без знака
Таблица 2. Перечень команд условного перехода для команды cmp операнд_1,операнд_2
Типы операндов | Мнемокод команды условного перехода | Критерий условного перехода | Значения флагов для осществления перехода
Любые | je | операнд_1 = операнд_2 | zf = 1
Любые | jne | операнд_1<>операнд_2 | zf = 0
Со знаком | jl/jnge | операнд_1 < операнд_2 | sf <> of
Со знаком | jle/jng | операнд_1 <= операнд_2 | sf <> of or zf = 1
Со знаком | jg/jnle | операнд_1 > операнд_2 | sf = of and zf = 0
Со знаком | jge/jnl | операнд_1 => операнд_2 | sf = of
Без знака | jb/jnae | операнд_1 < операнд_2 | cf = 1
Без знака | jbe/jna | операнд_1 <= операнд_2 | cf = 1 or zf=1
Без знака | ja/jnbe | операнд_1 > операнд_2 | cf = 0 and zf = 0
Без знака | jae/jnb | операнд_1 => операнд_2 | cf = 0
Не удивляйтесь тому обстоятельству, что одинаковым значениям флагов соответствует несколько разных мнемокодов команд условного перехода (они отделены друг от друга косой чертой в табл. 2).
Разница в названии обусловлена желанием разработчиков микропроцессора облегчить использование команд условного перехода в сочетании с определенными группами команд. Поэтому разные названия отражают скорее различную функциональную направленность. Тем не менее, то, что эти команды реагируют на одни и те же флаги делает их абсолютно эквивалентными и равноправными в программе. Поэтому в табл. 2 они сгруппированы не по названиям, а по значениям флагов (условиям), на которые они реагируют.

Команды условного перехода и флаги

Мнемоническое обозначение некоторых команд условного перехода отражает название флага, с которым они работают, и имеет следующую структуру: первым идет символ “j” (jump, переход), вторым — либо обозначение флага, либо символ отрицания “n”, после которого стоит название флага.
Такая структура команды отражает ее назначение.
Если символа “n” нет, то проверяется состояние флага, и если он равен 1, производится переход на метку перехода.
Если символ “n” присутствует, то проверяется состояние флага на равенство 0, и в случае успеха производится переход на метку перехода.
Мнемокоды команд, названия флагов и условия переходов приведены в табл. 3.
Эти команды можно использовать после любых команд, изменяющих указанные флаги.

Таблица 3. Команды условного перехода и флаги

Название флага | Номер бита в eflags/flag | Команда условного перехода | Значение флага для осуществления перехода
Флаг переноса cf | 1 | jc | cf = 1
Флаг четности pf | 2 | jp | pf = 1
Флаг нуля zf | 6 | jz | zf = 1
Флаг знака sf | 7 | js | sf = 1
Флаг переполнения of | 11 | jo | of = 1
Флаг переноса cf | 1 | jnc | cf = 0
Флаг четности pf | 2 | jnp | pf = 0
Флаг нуля zf | 6 | jnz | zf = 0
Флаг знака sf | 7 | jns | sf = 0
Флаг переполнения of | 11 | jno | of = 0
Если внимательно посмотреть на табл. 2 и 3, видно, что многие команды условного перехода в них являются эквивалентными, так как в основе и тех, и других лежит анализ одинаковых флагов.

Команды условного перехода и регистр ecx/cx

Архитектура микропроцессора предполагает специфическое использование многих регистров.
К примеру, регистр eax/ax/al используется как аккумулятор, а регистры bp, sp - для работы со стеком.
Регистр ecx/cx тоже имеет определенное функциональное назначение — он выполняет роль счетчика в командах управления циклами и при работе с цепочками символов. Возможно, что функционально команду условного перехода, связанную с регистром ecx/cx, правильнее было бы отнести к этой группе команд.
Синтаксис этой команды условного перехода таков:
jcxz метка_перехода (Jump if cx is Zero) — переход, если cx ноль;
jecxz метка_перехода (Jump Equal ecx Zero) — переход, если ecx ноль.

Эти команды очень удобно использовать при организации цикла и при работе с цепочками символов.
Нужно отметить ограничение, свойственное команде jcxz/jecxz. В отличие от других команд условной передачи управления, команда jcxz/jecxz может адресовать только короткие переходы — на –128 байт или на +127 байт от следующей за ней команды.

Организация циклов

Цикл, как известно, представляет собой важную алгоритмическую структуру, без использования которой не обходится, наверное, ни одна программа.
Организовать циклическое выполнение некоторого участка программы можно, к примеру, используя команды условной передачи управления или команду безусловного перехода jmp. При такой организации цикла все операции по его организации выполняются “вручную”. Но, учитывая важность такого алгоритмического элемента, как цикл, разработчики микропроцессора ввели в систему команд группу из трех команд, облегчающую программирование циклов. Эти команды также используют регистр ecx/cx как счетчик цикла.
Дадим краткую характеристику этим командам:

loop метка_перехода (Loop) — повторить цикл. Команда позволяет организовать циклы, подобные циклам for в языках высокого уровня с автоматическим уменьшением счетчика цикла. Работа команды заключается в выполнении следующих действий:

  • декремента регистра ecx/cx;
  • сравнения регистра ecx/cx с нулем:
    • если (ecx/cx) > 0, то управление передается на метку перехода;
    • если (ecx/cx) = 0, то управление передается на следующую после loop команду.

loope/loopz метка_перехода (Loop till cx <> 0 or Zero Flag = 0) — повторить цикл, пока cx <> 0 или zf = 0.
Команды loope и loopz — абсолютные синонимы, поэтому используйте ту команду, которая вам больше нравиться. Работа команд заключается в выполнении следующих действий:
  • декремента регистра ecx/cx;
  • сравнения регистра ecx/cx с нулем;
  • анализа состояния флага нуля zf:
    • если (ecx/cx) > 0 и zf = 1, управление передается на метку перехода;
    • если (ecx/cx) = 0 или zf = 0, управление передается на следующую после loop команду.

loopne/loopnz метка_перехода (Loop till cx <> 0 or Not Zero flag=0) — повторить цикл пока cx <> 0 или zf = 1.
Команды loopne и loopnz также абсолютные синонимы. Работа команд заключается в выполнении следующих действий:

  • декремента регистра ecx/cx;
  • сравнения регистра ecx/cx с нулем;
  • анализа состояния флага нуля zf:
    • если (ecx/cx) > 0 и zf = 0, управление передается на метку перехода;
    • если (ecx/cx)=0 или zf=1, управление передается на следующую после loop команду.
Команды loope/loopz и loopne/loopnz по принципу своей работы являются взаимообратными. Они расширяют действие команды loop тем, что дополнительно анализируют флаг zf, что дает возможность организовать досрочный выход из цикла, используя этот флаг в качестве индикатора.

Недостаток команд организации цикла loop, loope/loopz и loopne/loopnz в том, что они реализуют только короткие переходы (от –128 до +127 байт). Для работы с длинными циклами придется использовать команды условного перехода и команду jmp, поэтому постарайтесь освоить оба способа организации циклов.


Цепочечные команды

Эти команды также называют командами обработки строк символов. Названия почти синонимичны.
Отличие в том, что под строкой символов здесь понимается последовательность байт, а цепочка — это более общее название для случаев, когда элементы последовательности имеют размер больше байта — слово или двойное слово.
Таким образом, цепочечные команды позволяют проводить действия над блоками памяти, представляющими собой последовательности элементов следующего размера:
  • 8 бит — байт;
  • 16 бит — слово;
  • 32 бита — двойное слово.

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

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

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

  • пересылка цепочки:
      movs адрес_приемника,адрес_источника
      movsb
      movsw
      movsd
  • сравнение цепочек:
      cmps адрес_приемника,адрес_источника
      cmpsb
      cmpsw
      cmpsd
  • сканирование цепочки:
      scas адрес_приемника
      scasb
      scasw
      scasd
  • загрузка элемента из цепочки:
      lods адрес_источника
      lodsb
      lodsw
      lodsd
  • сохранение элемента в цепочке:
      stos адрес_приемника
      stosb
      stosw
      stosd
  • получение элементов цепочки из порта ввода-вывода:
      ins адрес_приемника,номер_порта
      insb
      insw
      insd
  • вывод элементов цепочки в порт ввода-вывода:
      outs номер_порта,адрес_источника
      outsb
      outsw
      outsd
Логически к этим командам нужно отнести и так называемые префиксы повторения. Вспомните формат машинной команды и его первые необязательные байты префиксов. Один из возможных типов префиксов — это префиксы повторения. Они предназначены для использования цепочечными командами.

Префиксы повторения имеют свои мнемонические обозначения:

rep
repe или repz
repne или repnz

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

Отличия приведенных префиксов в том, на каком основании принимается решение о циклическом выполнении цепочечной команды: по состоянию регистра ecx/cx или по флагу нуля zf:

  • префикс повторения rep (REPeat). Этот префикс используется с командами, реализующими операции-примитивы пересылки и сохранения элементов цепочек — соответственно, movs и stos.
    Префикс rep заставляет данные команды выполняться, пока содержимое в ecx/cx не станет равным 0.
    При этом цепочечная команда, перед которой стоит префикс, автоматически уменьшает содержимое ecx/cx на единицу. Та же команда, но без префикса, этого не делает;
  • префиксы повторения repe или repz (REPeat while Equal or Zero). Эти префиксы являются абсолютными синонимами.
    Они заставляют цепочечную команду выполняться до тех пор, пока содержимое ecx/cx не равно нулю или флаг zf равен 1.
    Как только одно из этих условий нарушается, управление передается следующей команде программы. Благодаря возможности анализа флага zf, наиболее эффективно эти префиксы можно использовать с командами cmps и scas для поиска отличающихся элементов цепочек.
  • префиксы повторения repne или repnz (REPeat while Not Equal or Zero). Эти префиксы также являются абсолютными синонимами. Их действие на цепочечную команду несколько отличается от действий префиксов repe/repz. Префиксы repne/repnz заставляют цепочечную команду циклически выполняться до тех пор, пока содержимое ecx/cx не равно нулю или флаг zf равен нулю.
    При невыполнении одного из этих условий работа команды прекращается.
    Данные префиксы также можно использовать с командами cmps и scas, но для поиска совпадающих элементов цепочек.
Следующий важный момент, связанный с цепочечными командами, заключается в особенностях формирования физического адреса операндов адрес_источника и адрес_приемника.
Цепочка-источник, адресуемая операндом адрес_источника, может находиться в текущем сегменте данных, определяемом регистром ds.
Цепочка-приемник, адресуемая операндом адрес_приемника, должна быть в дополнительном сегменте данных, адресуемом сегментным регистром es.
Важно отметить, что допускается замена (с помощью префикса замены сегмента) только регистра ds, регистр es подменять нельзя.
Вторые части адресов - смещения цепочек — также находятся в строго определенных местах.
Для цепочки-источника это регистр esi/si (Source Index register — индексный регистр источника).
Для цепочки-получателя это регистр edi/di (Destination Index register - индексный регистр приемника).
Таким образом, полные физические адреса для операндов цепочечных команд следующие:
  • адрес_источника — пара ds:esi/si;
  • адрес_приемника — пара es:edi/di.
Вы, наверное обратили внимание на то, что все семь групп команд, реализующих цепочечные операции-примитивы, имеют похожий по структуре набор команд.
В каждом из этих наборов присутствует одна команда с явным указанием операндов и три команды, не имеющие операндов.
На самом деле, набор команд микропроцессора имеет соответствующие машинные команды только для цепочечных команд ассемблера без операндов. Команды с операндами транслятор ассемблера использует только для определения типов операндов.
После того как выяснен тип элементов цепочек по их описанию в памяти, генерируется одна из трех машинных команд для каждой из цепочечных операций. По этой причине все регистры, содержащие адреса цепочек, должны быть инициализированы заранее, в том числе и для команд, допускающих явное указание операндов.
В силу того, что цепочки адресуются однозначно, нет особого смысла применять команды с операндами. Главное, что вы должны запомнить, — правильная загрузка регистров указателями обязательно требуется до выдачи любой цепочечной команды.

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

  • от начала цепочки к ее концу, то есть в направлении возрастания адресов;
  • от конца цепочки к началу, то есть в направлении убывания адресов.
Как мы увидим ниже, цепочечные команды сами выполняют модификацию регистров, адресующих операнды, обеспечивая тем самым автоматическое продвижение по цепочке. Количество байт, на которые эта модификация осуществляется, определяется кодом команды. А вот знак этой модификации определяется значением флага направления df (Direction Flag) в регистре eflags/flags:
  • если df = 0, то значение индексных регистров esi/si и edi/di будет автоматически увеличиваться (операция инкремента) цепочечными командами, то есть обработка будет осуществляться в направлении возрастания адресов;
  • если df = 1, то значение индексных регистров esi/si и edi/di будет автоматически уменьшаться (операция декремента) цепочечными командами, то есть обработка будет идти в направлении убывания адресов.
Состоянием флага df можно управлять с помощью двух команд, не имеющих операндов:
cld (Clear Direction Flag) — очистить флаг направления. Команда сбрасывает флаг направления df в 0.
std (Set Direction Flag) — установить флаг направления. Команда устанавливает флаг направления df в 1.

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

Операция пересылки цепочек

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

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

movs адрес_приемника,адрес_источника (MOVe String) — переслать цепочку;
movsb (MOVe String Byte) — переслать цепочку байт;
movsw (MOVe String Word) — переслать цепочку слов;
movsd (MOVe String Double word) — переслать цепочку двойных слов.

Команда movs:

movs адрес_приемника,адрес_источника
Команда копирует байт, слово или двойное слово из цепочки, адресуемой операндом адрес_источника, в цепочку, адресуемую операндом адрес_приемника.
Размер пересылаемых элементов ассемблер определяет, исходя из атрибутов идентификаторов, указывающих на области памяти приемника и источника. К примеру, если эти идентификаторы были определены директивой db, то пересылаться будут байты, если идентификаторы были определены с помощью директивы dd, то пересылке подлежат 32-битовые элементы, то есть двойные слова.
Ранее уже было отмечено, что для цепочечных команд с операндами, к которым относится и команда пересылки movs адрес_приемника,адрес_источника, не существует машинного аналога.
При трансляции в зависимости от типа операндов транслятор преобразует ее в одну из трех машинных команд:
movsb, movsw или movsd.

Сама по себе команда movs пересылает только один элемент, исходя из его типа, и модифицирует значения регистров esi/si и edi/di. Если перед командой написать префикс rep, то одной командой можно переслать до 64 Кбайт данных (если размер адреса в сегменте 16 бит — use16) или до 4 Гбайт данных (если размер адреса в сегменте 32 бит - use32).
Число пересылаемых элементов должно быть загружено в счетчик — регистр cx (use16) или ecx (use32).

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

  • Установить значение флага df в зависимости от того, в каком направлении будут обрабатываться элементы цепочки — в направлении возрастания или убывания адресов.
  • Загрузить указатели на адреса цепочек в памяти в пары регистров ds:(e)si и es: (e)di.
  • Загрузить в регистр ecx/cx количество элементов, подлежащих обработке.
  • Выдать команду movs с префиксом rep.
На примере листинга 1 рассмотрим, как эти действия реализуются программно. В этой программе производится пересылка символов из одной строки в другую. Строки находятся в одном сегменте памяти. Для пересылки используется команда-примитив movs с префиксом повторения rep.

Листинг 1 Пересылка строк командой movs
;prg_11_1.asm
MASM
MODEL   small
STACK   256
.data
source  db      'Тестируемая строка','$'
;строка-источник
dest    db      19 DUP (' ')    ;строка-приёмник
.code
        assume  ds:@data,es:@data
main:   ;точка входа в программу
        mov     ax,@data        ;загрузка сегментных регистров
        mov     ds,ax   ;настройка регистров DS и ES
        ;на адрес сегмента данных
        mov     es,ax
        cld     ;сброс флага DF — обработка строки от начала к концу
        lea     si,source       ;загрузка в si смещения строки-источника
        lea     di,dest ;загрузка в DS смещения строки-приёмника
        mov     cx,20   ;для префикса rep — счетчик повторений (длина строки)
rep     movs    dest,source     ;пересылка строки
        lea     dx,dest
        mov     ah,09h  ;вывод на экран строки-приёмника
        int     21h
exit:
        mov     ax,4c00h
        int     21h
end     main

Операция сравнения цепочек

Команды, реализующие эту операцию-примитив, производят сравнение элементов цепочки-источника с элементами цепочки-приемника. Здесь ситуация с набором команд и методами работы с ними аналогична операции-примитиву пересылки цепочек.
TASM предоставляет программисту четыре команды сравнения цепочек, работающие с разными размерами элементов цепочки:
cmps адрес_приемника,адрес_источника(CoMPare String) — сравнить строки;
cmpsb (CoMPare String Byte) — сравнить строку байт;
cmpsw (CoMPare String Word) — сравнить строку слов;
cmpsd (CoMPare String Double word) — сравнить строку двойных слов.

Команда cmps

Синтаксис команды cmps:
cmps адрес_приемника,адрес_источника
Здесь:
  • адрес_источника определяет цепочку-источник в сегменте данных. Адрес цепочки должен быть заранее загружен в пару ds:esi/si;
  • адрес_приемника определяет цепочку-приемник. Цепочка должна находиться в дополнительном сегменте, и ее адрес должен быть заранее загружен в пару es:edi/di.
Алгоритм работы команды cmps заключается в последовательном выполнении вычитания (элемент цепочки-источника — элемент цепочки-получателя) над очередными элементами обеих цепочек.
Принцип выполнения вычитания командой cmps аналогичен команде сравнения cmp. Она, так же, как и cmp, производит вычитание элементов, не записывая при этом результата, и устанавливает флаги zf, sf и of.
После выполнения вычитания очередных элементов цепочек командой cmps, индексные регистры esi/si и edi/di автоматически изменяются в соответствии со значением флага df на значение, равное размеру элемента сравниваемых цепочек.
Чтобы заставить команду cmps выполняться несколько раз, то есть производить последовательное сравнение элементов цепочек, необходимо перед командой cmps определить префикс повторения.
С командой cmps можно использовать префикс повторения repe/repz или repne/repnz:
  • repe или repz — если необходимо организовать сравнение до тех пор, пока не будет выполнено одно из двух условий:
    • достигнут конец цепочки (содержимое ecx/cx равно нулю);
    • в цепочках встретились разные элементы (флаг zf стал равен нулю);
  • repne или repnz — если нужно проводить сравнение до тех пор, пока:
    • не будет достигнут конец цепочки (содержимое ecx/cx равно нулю);
    • в цепочках встретились одинаковые элементы (флаг zf стал равен единице).
Таким образом, выбрав подходящий префикс, удобно использовать команду cmps для поиска одинаковых или различающихся элементов цепочек.
Выбор префикса определяется причиной, которая приводит к выходу из цикла. Таких причин может быть две для каждого из префиксов. Для определения конкретной причины наиболее подходящим является способ, использующий команду условного перехода jcxz. Ее работа заключается в анализе содержимого регистра ecx/cx, и если оно равно нулю, то управление передается на метку, указанную в качестве операнда jcxz. Так как в регистре ecx/cx содержится счетчик повторений для цепочечной команды, имеющей любой из префиксов повторения, то, анализируя ecx/cx, можно определить причину выхода из зацикливания цепочечной команды. Если значение в ecx/cx не равно нулю, то это означает, что выход произошел по причине совпадения либо несовпадения очередных элементов цепочек.
Существует возможность еще больше конкретизировать информацию о причине, приведшей к окончанию операции сравнения. Сделать это можно с помощью команд условной передачи управления (табл. 1 и 2).

Таблица 1. Сочетание команд условной передачи управления с результатами команды cmps (для чисел со знаком)

Причина прекращения операции сравнения | Команда условного перехода, реализующая переход по этой причине
операнд_источник > операнд_приемник | jg
операнд_источник = операнд_приемник | je
операнд_источник <> операнд_приемник | jne
операнд_источник < операнд_приемник | jl
операнд_источник <= операнд_приемник | jle
операнд_источник >= операнд_приемник | jge

Таблица 2. Сочетание команд условной передачи управления с результатами команды cmps (для чисел без знака)

Причина прекращения операции сравнения | Команда условного перехода, реализующая переход по этой причине
операнд_источник > операнд_приемник | ja
операнд_источник = операнд_приемник | je
операнд_источник <> операнд_приемник | jne
операнд_источник < операнд_приемник | jb
операнд_источник <= операнд_приемник | jbe
операнд_источник >= операнд_приемник | jae

Как определить местоположение очередных совпавших или не совпавших элементов в цепочках?

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

 Листинг 2. Сравнение двух строк командой cmps
<1> ;prg_11_2.asm
<2> MODEL       small
<3> STACK       256
<4> .data
<5> match       db      0ah,0dh,'Строки совпадают.','$'
<6> failed      db      0ah,0dh,'Строки не совпадают','$'
<7> string1     db      '0123456789',0ah,0dh,'$';исследуемые строки
<8> string2     db      '0123406789','$'
<9> .code
<10> ASSUME     ds:@data,es:@data       ;привязка DS и ES к сегменту данных
<11> main:
<12>    mov     ax,@data        ;загрузка сегментных регистров
<13>    mov     ds,ax
<14>    mov     es,ax   ;настройка ES на DS
<15> ;вывод на экран исходных строк string1 и string2
<16>    mov     ah,09h
<17>    lea     dx,string1
<18>    int     21h
<19>    lea     dx,string2
<20>    int     21h
<21> ;сброс флага DF — сравнение в направлении возрастания  адресов
<22>    cld
<23>    lea     si,string1      ;загрузка в si смещения string1
<24>    lea     di,string2      ;загрузка в di смещения string2
<25>    mov     cx,10   ;длина строки для префикса repe
<26> ;сравнение строк (пока сравниваемые элементы строк равны)
<27> ;выход при обнаружении не совпавшего элемента
<28> cycl:
<29>    repe    cmps    string1,string2
<30>    jcxz    equal   ;cx=0, то есть строки совпадают
<31>    jne     not_match       ;если не равны — переход на not_match
<32> equal:             ;иначе, если совпадают, то
<33>    mov     ah,09h  ;вывод сообщения
<34>    lea     dx,match
<35>    int     21h
<36>    jmp     exit    ;выход
<37> not_match:         ;не совпали
<38>    mov     ah,09h
<39>    lea     dx,failed
<40>    int     21h     ;вывод сообщения
<41> ;теперь, чтобы обработать не совпавший элемент в
 строке, необходимо уменьшить значения регистров si и di
<42>    dec     si
<43>    dec     di
<44> ;сейчас в ds:si и es:di адреса несовпавших элементов
<45> ;здесь вставить код по обработке несовпавшего элемента
<46> ;после этого продолжить поиск в строке:
<47>    inc     si
<48>    inc     di
<49>    jmp     cycl
<50> exit:      ;выход
<51>    mov     ax,4c00h
<52>    int     21h
<53> end        main    ;конец программы
Программа достаточно прозрачна, только два момента, на мой взгляд, требуют пояснения:

  • во-первых, строки 42 и 43, в которых мы скорректировали адреса очередных элементов для получения адресов несовпавших элементов. Вы должны понимать, что если сравниваются цепочки с элементами слов или двойных слов, то корректировать содержимое esi/si и edi/di нужно на 2 и 4 байта соответственно;
  • во-вторых, строки 47–49. Смысл их в том, что для просмотра оставшейся части строк необходимо установить указатели на следующие элементы строк за последними несовпавшими. После этого можно повторить весь процесс просмотра и обработки несовпавших элементов в оставшихся частях строк.

    Операция сканирования цепочек

    Команды, реализующие эту операцию-примитив, производят поиск некоторого значения в области памяти. Логически эта область памяти рассматривается как последовательность (цепочка) элементов фиксированной длины размером 8, 16 или 32 бит.
    Искомое значение предварительно должно быть помещено в регистр al/ax/eax. Выбор конкретного регистра из этих трех должен быть согласован с размером элементов цепочки, в которой осуществляется поиск.

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

      scas адрес_приемника (SCAning String) — сканировать цепочку;
      scasb (SCAning String Byte) — сканировать цепочку байт;
      scasw (SCAning String Word) — сканировать цепочку слов;
      scasd (SCAning String Double Word) — сканировать цепочку двойных слов.

    Команда scas

    scas адрес_приемника
    Команда имеет один операнд, обозначающий местонахождение цепочки в дополнительном сегменте (адрес цепочки должен быть заранее сформирован в es:edi/di).
    Транслятор анализирует тип идентификатора адрес_приемника, который обозначает цепочку в сегменте данных, и формирует одну из трех машинных команд scasb, scasw или scasd.
    Условие поиска для каждой из этих трех команд находится в строго определенном месте. Так, если цепочка описана с помощью директивы db, то искомый элемент должен быть байтом и находиться в al, а сканирование цепочки осуществляется командой scasb; если цепочка описана с помощью директивы dw, то это — слово в ax, и поиск ведется командой scasw; если цепочка описана с помощью директивы dd, то это — двойное слово в eax, и поиск ведется командой scasd. Принцип поиска тот же, что и в команде сравнения cmps, то есть последовательное выполнение вычитания
    (содержимое_регистра_аккумулятора - содержимое_очередного_элемента_цепочки).
    В зависимости от результатов вычитания производится установка флагов, при этом сами операнды не изменяются.
    Так же, как и в случае команды cmps, с командой scas удобно использовать префиксы repe/repz или repne/repnz:
    • repe или repz — если нужно организовать поиск до тех пор, пока не будет выполнено одно из двух условий:
      • достигнут конец цепочки (содержимое ecx/cx равно 0);
      • в цепочке встретился элемент, отличный от элемента в регистре al/ax/eax;
    • repne или repnz — если нужно организовать поиск до тех пор, пока не будет выполнено одно из двух условий:
      • достигнут конец цепочки (содержимое ecx/cx равно 0);
      • в цепочке встретился элемент, совпадающий с элементом в регистре al/ax/eax.
    Таким образом, команда scas с префиксом repe/repz позволяет найти элемент цепочки, отличающийся по значению от заданного в аккумуляторе.
    Команда scas с префиксом repne/repnz позволяет найти элемент цепочки, совпадающий по значению с элементом в аккумуляторе.

    В качестве примера рассмотрим листинг 3, который производит поиск символа в строке.
    В программе используется команда-примитив scas.
    Символ задается явно (строка 20).
    Префикс повторения — repne.

 Листинг 3. Поиск символа в строке командой scas
<1> ;prg_11_3.asm
<2> MASM
<3> MODEL       small
<4> STACK       256
<5> .data
<6> ;тексты сообщений
<7> fnd db      0ah,0dh,'Символ найден! ','$'
<8> nochar      db      0ah,0dh,'Символ не найден.','$'
<9> ;строка для поиска
<10> string     db      'Поиск символа в этой строке.',0ah,0dh,'$'
<11> .code
<12> ASSUME     ds:@data,es:@data
<13> main:
<14>    mov     ax,@data
<15>    mov     ds,ax
<16>    mov     es,ax   ;настройка ES на DS
<17>    mov     ah,09h
<18>    lea     dx,string
<19>    int     21h     ;вывод сообщения string
<20>    mov     al,'а'  ;символ для поиска — `а`(кириллица)
<21>    cld             ;сброс флага df
<22>    lea     di,string       ;загрузка в es:di смещения строки
<23>    mov     cx,29   ;для префикса repne — длина строки
<24> ;поиск в строке (пока искомый символ и символ в строке не совпадут)
<25> ;выход при первом совпадении
<26> repne      scas    string
<27>    je      found   ;если равны — переход на обработку,
<28> failed:    ;иначе выполняем некоторые действия
<29> ;вывод сообщения о том, что символ не найден
<30>    mov     ah,09h
<31>    lea     dx,nochar
<32>    int     21h     ;вывод сообщения nochar
<33>    jmp     exit    ;на выход
<34> found:             ;совпали
<35>    mov     ah,09h
<36>    lea     dx,fnd
<37>    int     21h ;вывод сообщения fnd
<38> ;теперь, чтобы узнать место, где совпал элемент в  строке,
<39> ;необходимо уменьшить значение в регистре di и  вставить нужный обработчик
<40> ;  dec     di
<41> ... вставьте обработчик
<42> exit:              ;выход
<43>    mov     ax,4c00h
<44>    int     21h
<45> end        main

    Загрузка элемента цепочки в аккумулятор

    Эта операция-примитив позволяет извлечь элемент цепочки и поместить его в регистр-аккумулятор al, ax или eax. Эту операцию удобно использовать вместе с поиском (сканированием) с тем, чтобы, найдя нужный элемент, извлечь его (например, для изменения).
    Возможный размер извлекаемого элемента определяется применяемой командой.

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

      lods адрес_источника (LOaD String) — загрузить элемент из цепочки в регистр-аккумулятор al/ax/eax;
      lodsb (LOaD String Byte) — загрузить байт из цепочки в регистр al;
      lodsw (LOaD String Word) — загрузить слово из цепочки в регистр ax;
      lodsd (LOaD String Double Word) — загрузить двойное слово из цепочки в регистр eax.
    Рассмотрим работу этих команд на примере lods.

    Команда lods

    lods адрес_источника (LOaD String) — загрузить элемент из цепочки в аккумулятор al/ax/eax.
    Команда имеет один операнд, обозначающий строку в основном сегменте данных. Работа команды заключается в том, чтобы извлечь элемент из цепочки по адресу, соответствующему содержимому пары регистров ds:esi/si, и поместить его в регистр eax/ax/al. При этом содержимое esi/si подвергается инкременту или декременту (в зависимости от состояния флага df) на значение, равное размеру элемента.
    Эту команду удобно использовать после команды scas, локализующей местоположение искомого элемента в цепочке.
    Префикс повторения в этой команде может и не понадобиться — все зависит от логики программы.

    В качестве примера рассмотрим листинг 4. Программа сравнивает командой cmps две цепочки байт в памяти string1 и string2 и помещает первый несовпавший байт из string2 в регистр al. Для загрузки этого байта в регистр-аккумулятор al используется команда lods. Префикса повторения в команде lods нет, так как он попросту не нужен.

Листинг 4. Использование lods для загрузки байта в регистр al
<1> ;prg_11_4.asm
<2> MASM
<3> MODEL       small
<4> STACK       256
<5> .data
<6> ;строки     для     сравнения
<7> string1     db      'Поиск символа в этой строке.',0ah,0dh,'$'
<8> string2     db      'Поиск символа не в этой строке.',0ah,0dh,'$'
<9> mes_eq      db      'Строки совпадают.',0ah,0dh,'$'
<10> fnddb      'Несовпавший элемент в регистре al',0ah,0dh,'$'
<11> .code
<12> ;привязка ds и es к сегменту данных
<13> assume     ds:@data,es:@data
<14> main:
<15>    mov     ax,@data        ;загрузка сегментных регистров
<16>    mov     ds,ax
<17>    mov     es,ax   ;настройка es на ds
<18>    mov     ah,09h
<19>    lea     dx,string1
<20>    int     21h     ;вывод string1
<21>    lea     dx,string2
<22>    int     21h     ;вывод string2
<23>    cld     ;сброс флага df
<24>    lea     di,string1      ;загрузка в es:di смещения
<25> ;строки string1
<26>    lea     si,string2      ;загрузка в ds:si смещения
<27> ;строки string2
<28>    mov     cx,29   ;для префикса repe — длина строки
<29> ;поиск в строке (пока нужный символ и символ в строке не равны)
<30> ;выход — при первом несовпавшем
<31> repe       cmps    string1,string2
<32>    jcxz    eql     ;если равны — переход на eql
<33>    jmp     no_eq   ;если не равны — переход на no_eq
<34> eql:               ;выводим сообщение о совпадении строк
<35>    mov     ah,09h
<36>    lea     dx,mes_eq
<37>    int     21h     ;вывод сообщения mes_eq
<38>    jmp     exit    ;на выход
<39> no_eq:     ;обработка несовпадения элементов
<40>    mov     ah,09h
<41>    lea     dx,fnd
<42>    int     21h     ;вывод сообщения fnd
<43> ;теперь, чтобы извлечь несовпавший элемент из строки
<44> ;в регистр-аккумулятор,
<45> ;уменьшаем значение регистра si и тем самым перемещаемся
<46> ;к действительной позиции элемента в строке
<47>    dec     si      ;команда lods использует ds:si-адресацию
<48> ;теперь ds:si указывает на позицию в string2
<49>    lods    string2 ;загрузим элемент из строки в AL
<50> ;нетрудно догадаться, что в нашем примере это символ — "н"
<51> exit:              ;выход
<52>    mov     ax,4c00h
<53>    int     21h
<54> end        main

    Перенос элемента из аккумулятора в цепочку

    Эта операция-примитив позволяет произвести действие, обратное команде lods, то есть сохранить значение из регистра-аккумулятора в элементе цепочки.
    Эту операцию удобно использовать вместе с операцией поиска (сканирования) scans и загрузки lods, с тем, чтобы, найдя нужный элемент, извлечь его в регистр и записать на его место новое значение.
    Команды, поддерживающие эту операцию-примитив, могут работать с элементами размером 8, 16 или 32 бит.
    TASM предоставляет программисту четыре команды сохранения элемента цепочки из регистра-аккумулятора, работающие с элементами разного размера:
      stos адрес_приемника (STOre String) — сохранить элемент из регистра-аккумулятора al/ax/eax в цепочке;
      stosb (STOre String Byte) — сохранить байт из регистра al в цепочке;
      stosw (STOre String Word) — сохранить слово из регистра ax в цепочке;
      stosd (STOre String Double Word) - сохранить двойное слово из регистра eax в цепочке.

    Команда stos

    stos адрес_приемника (STOrage String) — сохранить элемент из регистра-аккумулятора al/ax/eax в цепочке.

    Команда имеет один операнд адрес_приемника, адресующий цепочку в дополнительном сегменте данных.
    Работа команды заключается в том, что она пересылает элемент из аккумулятора (регистра eax/ax/al) в элемент цепочки по адресу, соответствующему содержимому пары регистров es:edi/di. При этом содержимое edi/di подвергаются инкременту или декременту (в зависимости от состояния флага df) на значение, равное размеру элемента цепочки.
    Префикс повторения в этой команде может и не понадобиться — все зависит от логики программы. Например, если использовать префикс повторения rep, то можно применить команду для инициализации области памяти некоторым фиксированным значением.

    В качестве примера рассмотрим листинг 5. Программа производит замену в строке всех символов “а” на другой символ. Символ для замены вводится с клавиатуры.

Листинг 5. Замена командой stos символа в строке на вводимый с клавиатуры
;prg_11_5.asm
MASM
MODEL   small
STACK   256
.data
;сообщения
fnd     db      0ah,0dh,'Символ найден','$'
nochar  db 0ah,0dh,'Символ не найден.','$'
mes1    db      0ah,0dh,'Исходная строка:','$'
string  db      'Поиск символа в этой строке.',0ah,0dh,'$' ;строка для поиска
mes2    db      0ah,0dh,'Введите символ, на который следует заменить найденный'
        db      0ah,0dh,'$'
mes3    db      0ah,0dh,'Новая строка: ','$'
.code
 assume ds:@data,es:@data привязка ds и es к сегменту данных
main:           ;точка входа в программу
        mov     ax,@data        ;загрузка сегментных регистров
        mov     ds,ax
        mov     es,ax   ;настройка es на ds
        mov     ah,09h
        lea     dx,mes1
        int     21h     ;вывод сообщения mes1
        lea     dx,string
        int     21h     ;вывод string
        mov     al,'а'  ;символ для поиска-`а`(кириллица)
        cld             ;сброс флага df
        lea     di,string       ;загрузка в di смещения string
        mov     cx,29   ;для префикса repne — длина строки
;поиск в строке string до тех пор, пока
;символ в al и очередной символ в строке
;не равны: выход - при первом совпадении
cycl:
repne   scas string
        je      found   ;если элемент найден то переход на found
failed:         ;иначе, если не найден, то вывод сообщения nochar
        mov     ah,09h
        lea     dx,nochar
        int     21h
        jmp     exit    ;переход на выход
found:
        mov     ah,09h
        lea     dx,fnd
        int     21h ;вывод сообщения об обнаружении символа
;корректируем di для получения значения
;действительной позиции совпавшего элемента
;в строке и регистре al
        dec     di
new_char:       ;блок замены символа
        mov     ah,09h
        lea     dx,mes2
        int     21h     ;вывод сообщения mes2
;ввод символа с клавиатуры
        mov     ah,01h
        int     21h     ;в al — введённый символ
        stos    string  ;сохраним введённый символ
 ;(из al) в строке string в позиции старого символа
        mov     ah,09h
        lea     dx,mes3
        int     21h     ;вывод сообщения mes3
        lea     dx,string
        int     21h ;вывод сообщения string
;переход на поиск следующего символа ‘а’ в
строке
        inc     di              ;указатель в строке string на следующий,
 ;после совпавшего, символ
        jmp     cycl    ;на продолжение просмотра string
exit:           ;выход
        mov     ax,4c00h
        int     21h
end     main    ;конец программы
    Следующие две команды появились впервые в системе команд микропроцессора i386. Они позволяют организовать эффективную передачу данных между портами ввода-вывода и цепочками в памяти. Следует отметить, что эти две команды позволяют достичь скорости передачи данных со скоростью выше той, которую может обеспечить контроллер DMA (Direct Memory Access — прямой доступ к памяти). Контроллер DMA — это специальная микросхема, предназначенная для того, чтобы освободить микропроцессор от управления процессом ввода-вывода больших массивов данных между внешним устройством (диском) и памятью.

    Ввод элемента цепочки из порта ввода-вывода

    Данная операция позволяет произвести ввод цепочки элементов из порта ввода-вывода и реализуется командой ins, имеющей следующий формат:

    ins адрес_приемника,номер_порта (Input String) - ввести элементы из порта ввода-вывода в цепочку.

    Эта команда вводит элемент из порта, номер которого находится в регистре dx, в элемент цепочки, адрес которого определяется операндом адрес_приемника.
    Несмотря на то, что цепочка, в которую вводится элемент, адресуется указанием этого операнда, ее адрес должен быть явно сформирован в паре регистров es:edi/di.
    Размер элементов цепочки должен быть согласован с размерностью порта — он определяется директивой резервирования памяти, с помощью которой выделяется память для размещения элементов цепочки.
    После пересылки команда ins производит коррекцию содержимого edi/di на величину, равную размеру элемента, участвовавшего в операции пересылки. Как обычно, при работе цепочечных команд учитывается состояние флага df.
    Подобно командам, реализующим рассмотренные выше цепочечные операции-примитивы, транслятор преобразует команду ins в одну из трех машинных команд без операндов, работающих с цепочками элементов определенного размера:

      insb (INput String Byte) — ввести из порта цепочку байт;
      insw (INput String Word) — ввести из порта цепочку слов;
      insd (INput String Double Word) — ввести из порта цепочку двойных слов.

    К примеру, выведем 10 байт из области памяти pole в порт 5000h.

.data
pole    db      10 dup (‘ ‘)
.code
...
        push    ds
        pop     es      ;настройка es на ds
        mov     dx,5000h
        lea     di,pole
        mov     cx,10
rep     insb
...

    Вывод элемента цепочки в порт ввода-вывода

    Данная операция позволяет произвести вывод элементов цепочки в порт ввода-вывода. Она реализуется командой outs, имеющей следующий формат:
    outs номер_порта,адрес_источника (Output String) — вывести элементы из цепочки в порт ввода-вывода.

    Эта команда выводит элемент цепочки в порт, номер которого находится в регистре dx. Адрес элемента цепочки определяется операндом адрес_источника. Несмотря на то, что цепочка, из которой выводится элемент, адресуется указанием этого операнда, значение адреса должно быть явно сформировано в паре регистров ds:esi/si.
    Размер структурных элементов цепочки должен быть согласован с размерностью порта. Он определяется директивой резервирования памяти, с помощью которой выделяется память для размещения элементов цепочки.
    После пересылки команда outs производит коррекцию содержимого esi/si на величину, равную размеру элемента цепочки, участвовавшего в операции пересылки. При этом, как обычно, учитывается состояние флага df.

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

      outsb (OUTput String Byte) — вывести цепочку байт в порт ввода-вывода;
      outsw (OUTtput String Word) — вывести цепочку слов в порт ввода-вывода;
      outsd (OUTput String Double Word) — вывести цепочку двойных слов в порт ввода- вывода.

    В качестве примера рассмотрим фрагмент программы, которая выводит последовательность символов в порт ввода-вывода, соответствующего принтеру (номер 378 (lpt1)).

.data
str_pech        db      'Текст для печати'
.code
...
        mov     dx,378h
        lea     di,str_pech
        mov     cx,16
rep     outsb
...
    В заключение напомню, что для организации работы с портами недостаточно знать их номера и назначение. Не менее важно знать и понимать алгоритм их работы. Эти сведения можно найти в документации на устройство (но, к сожалению, далеко не всегда).


Арифметические команды микропроцессора

    Целочисленные арифметические команды

    Преобразования типов
  • cbw
  • cwd
  • cbw
  • cdq
  • movsx
  • movzx

    • Двоичной арифметики
  • Сложения
    • add
    • adc
    • inc
  • Вычитания
    • sub
    • sbb
    • dec
  • Умножения
    • imul
    • mul
  • Деления
    • idiv
    • div
  • Изменения знака
    • neg
    Десятичной арифметики
  • Коррекция сложения
    • aaa
    • daa
  • Коррекция вычитания
    • aas
    • das
  • Коррекция для умножения
    • aam
  • Коррекция для деления
    • aad
  • Вспомогательные команды
    • bswap
  • Прочие команды с арифметическим принципом действия
    • cmp
    • cmpxchg
    • setcc
    • xadd


Описание системы команд микропроцессора Pentium

Описание системы команд

микропроцессоров Intel

Материал, приведенный в данном разделе справочной системы, связан с уроком 6, на котором мы рассматривали формат машинной команды микропроцессора и систему его команд в целом.
Выберите тему:

Знакомство с порядком описания команд и принятыми обозначениями
Описание команд микропроцессора, упорядоченное по алфавиту
Описание команд микропроцессора, упорядоченное по функциональному признаку

aaa | aad | aam | aas | adc | add | and
bound | bsf | bsr | bswap | bt | btc | btr
bts | call | cbw | cwde | clc | cld | cli
cmc | cmp | cmps/cmpsb /cmpsw/cmpsd | cmpxchg | cwd | cdq | daa
das | dec | div | enter | hlt | idiv | imul
in | inc | ins/insb /insw/insd | int | into | iret/iretd | jcc
jcxz | jecxz | jmp | lahf | lds | les | lfs
lgs | lss | lea | leave | lgdt | lidt | lods/lodsb /lodsw/lodsd
loop | loope | loopz | loopne | loopnz | mov | movs/movsb /movsw/movsd
movsx | movzx | mul | neg | nop | not | or
out | outs | pop | popa | popad | popf | popfd
push | pusha | pushad | pushf | pushfd | rcl | rcr
rep/repe/repz /repne/repnz | ret/retf | rol | ror | sahf | sal | sar
sbb | scas/scasb /scasw/scasd | setcc | sgdt | sidt | shl | shld
shr | shrd | stc | std | sti | stos/stosb /stosw/stosd | sub
test | xadd | xchg | xlat/xlatb | xor |  |

Порядок описания команд будет следующим:

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

Для описания команд приняты обозначения:

  1. Для описания состояния флагов после выполнения некоторой команды будем использовать выборку из таблицы, отражающей структуру регистра флагов eflags:
    2. 31 | 18 | 17 | 16 | 15 | 14 | 1312 | 11 | 10 | 09 | 08 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00
0 | 0 | VM | RF | 0 | NT | IOPL | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF
    В нижней строке этой таблицы приводятся значения флагов после выполнения команды. При этом используются следующие обозначения:
    • 1 — после выполнения команды флаг устанавливается (равен 1);
    • 0 — после выполнения команды флаг сбрасывается (равен 0);
    • r — значение флага зависит от результата работы команды;
    • ? — после выполнения команды флаг не определен;
    • пробел — после выполнения команды флаг не изменяется;
  2. Для представления операндов в синтаксических диаграммах используются следующие обозначения:
    • r8, r16, r32 — операнд в одном из регистров размером байт, слово или двойное слово;
    • m8, m16, m32, m48 — операнд в памяти размером байт, слово, двойное слово или 48 бит;
    • i8, i16, i32 — непосредственный операнд размером байт, слово или двойное слово;
    • a8, a16, a32 — относительный адрес (смещение) в сегменте кода.
  3. На многих диаграммах в целях компактности возможные сочетания операндов показаны в виде следующей конструкции:

    4. Конструируя команду на основе подобной синтаксической диаграммы, вы должны помнить о соответствии типов. В подобной диаграмме допустимы только следующие сочетания: "r8, m8", "r16, m16", "r32, m32". Например, сочетание "r8, m16" недопустимо. Однако есть единичные случаи, когда подобные сочетания возможны; тогда они специально оговариваются.

  4. Описанная в данном приложении система команд в полном объеме поддерживается микропроцессором Pentium. Предыдущие модели микропроцессора могут не поддерживать отдельные команды. Чтобы прояснить этот момент, мы будем указывать в примерах для каждой команды директиву типа .286. Это будет означать, что описываемая команда поддерживается всеми моделями микропроцессора, начиная с i286. Если ничего не указывается, то это означает, что данная команда работает на всех моделях микропроцессоров Intel, начиная с i8086/8088.

AAA

(Ascii Adjust after Addition)
ASCII-коррекция после сложения
Схема команды: aaa
Назначение: корректировка неупакованного результата сложения двух одноразрядных неупакованных BCD-чисел.

Синтаксис
Алгоритм работы:

  • проанализировать значение младшего полубайта регистра al и значение флага af;
  • если (значение младшего полубайта регистра al >9) или (AF=1), то выполнить следующие действия:
    • увеличить значение al на 6;
    • очистить старший полубайт регистра al;
    • увеличить значение ah на 1;
    • установить флаги: af = 1, cf = 1,
    иначе сбросить флаги af = 0 и cf = 0.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | ? | ? | r | ? | r
Применение:
Обычно команда aaa используется после сложения каждого разряда распакованных BCD-чисел командой add. Каждая цифра неупакованного BCD-числа занимает младший полубайт байта. Если результат сложения двух одноразрядных BCD-чисел больше 9, то число в младшем полубайте результата не есть BCD-число. Поэтому результат нужно корректировать командой aaa. Эта команда позволяет сформировать правильное BCD-число в младшем полубайте и запомнить единицу переноса в старший разряд путем увеличения содержимого регистра ah на 1.
К примеру, сложить два неупакованных BCD-числа: 08 + 05:
        mov     ah,08h  ;ah=08h
        mov     al,05h  ;al=05h
        add     al,ah   ;al=al+ah=05h+08h=0dh — не BCD-число
        xor     ah,ah   ;ah=0
        aaa             ;ah=01h,al=03h — результат скорректирован

См. также: урок 8, приложение 7 и команды aad, aam, aas, daa, das

AAD

(Ascii Adjust before Division)
ASCII-коррекция перед делением
Схема команды: aad
Назначение:
  • подготовкa двух неупакованных BCD-чисел для операции деления;
  • преобразование двузначного неупакованного ВCD-числа меньшего 63h (9910) в двоичное представление.
Синтаксис
Алгоритм работы:
  • умножить значение регистра ah на 10 и сложить полученное значение с содержимым регистра al: (ah*10)+al;
  • присвоить регистру al значение (ah*10)+al;
  • обнулить регистр ah.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | r | r | r | r | ?
Применение:
Команду aad используют для подготовки двузначного неупакованного BCD-числа в регистре ax для операции деления. Так как в системе команд микропроцессора нет команды деления для BCD-чисел, такое число нужно предварительно преобразовать в двоичный вид. Для этого старший разряд двузначного BCD-числа помещается в регистр ah, умножается на 10 и складывается с разрядом единиц двузначного BCD-числа 9 в регистре al. В результате этих действий и получается соответствующее двоичное число в регистре ax. Далее в программе уже можно применять обычную команду деления div, оперирующую двоичными данными. Команду aad можно применять и просто для преобразования неупакованного двузначного BCD-числа в его двоичный эквивалент. Есть еще интересный момент — если посмотреть на коды символов шестнадцатеричных цифр в таблице ASCII, то видно, что они похожи на BCD-числа. Исключение составляет лишь значение старшей тетрады (для BCD-числа это так называемая зона с нулевым значением) - оно равно 3. Можно сделать вывод, что если предварительно обнулить значение старшей тетрады для кодов двух символов (от 0 до 9), то эту команду вполне можно применять и для преобразования двузначных десятичных чисел в символьном представлении в их двоичный эквивалент, что и отражено в названии команды. Для иллюстрации рассмотрим два примера.

Пример 1. Разделить десятичное число 18 на 9. Подготовить результат к выводу на экран.
        mov     ah,01h  ;ah=01h
        mov     al,08h  ;al=08h => ax=0108h
        mov     bl,09   ;bl=09h
        aаd             ;al=12h — двоичный эквивалент десятичного числа 18
        div     bl      ;al=02h,ah=00h
        ог      al,30h  ;al=32h — ASCII-представление числа 2, можно выводить на экран

Пример 2. Преобразовать десятичное число 16 в символьном виде в эквивалентное двоичное число.
        mov     ax,3136h        ;ax=3136h
        and     ax,0f0fh        ;ax=0106h
        aаd             ;al=10h — получили его двоичный эквивалент

См. также: уроки 3, 8, приложение 7 и команды aaa, aam, aas, daa, das

AAM

(Ascii Adjust after Multiply)
ASCII-коррекция после умножения
Схема команды: aam
Назначение:
  • корректировка результата умножения двух неупакованных BCD-чисел;
  • преобразование двоичного числа меньшего 63h (9910) в его неупакованный BCD-эквивалент.
Синтаксис
Алгоритм работы:
  • разделить значение регистра al на 10;
  • записать частное в регистр ah, остаток — в регистр al.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | r | r | r | r | ?
Применение:
Команду aam используют для коррекции результата умножения двух неупакованных BCD-чисел. Специальной команды умножения BCD-чисел нет. Поэтому BCD-числа умножаются поразрядно, как обычные двоичные числа, командой mul. Максимальное число, которое получается при таком умножении, — это 9*9=8110=5116. Отсюда понятно, что значения, для которых командой aam можно получить их двузначный BCD-эквивалент в регистре ax, находятся в дипазоне от 00h до 51h. Эту команду можно применять и для преобразования двоичного числа из регистра ax (в диапазоне от 0 до 63h) в его десятичный эквивалент(соответственно, из диапазона от 0 до 9910).

Пример 1. Умножить десятичное число 8 на 9. Подготовить результат к выводу на экран.
        mov     ah,08h  ;ah=08h
        mov     al,09h  ;al= 09h
        mul     ah      ;al=48h — двоичный эквивалент 72
        aam             ;ah=07h,al=02h
        or      ax,3030h        ;ax=3732h — ASCII-представление числа 72
Пример 2. Преобразовать двоичное число 60h в эквивалентное десятичное число.
;поместим число 60h в регистр ax
        mov     ax,60h  ;ax=60h
        aаm             ;ax=0906h — получили десятичный эквивалент числа 60h
        or      ax,3030h        ;символьный эквивалент, можно выводить на экран
См. также: урок 8, приложение 7 и команды aaa, aad, aas, daa, das

AAS

(Ascii Adjust after Substraction)
ASCII-коррекция после вычитания
Схема команды: aas
Назначение: корректировка результата вычитания двух неупакованных одноразрядных BCD-чисел.

Алгоритм работы:
если (младший полубайт регистра al меньше 9) или (флаг af=1), то выполнить следующие действия:

  • уменьшить значение младшего полубайта регистра al на 6;
  • обнулить значение старшего полубайта регистра al;
  • установить флаги af и cf в 1;
иначе установить флаги af и cf в 1.

Состояние флагов после выполнения команды:

11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | ? | ? | r | ? | r
Применение:
Команду aas используют для коррекции результата вычитания двух неупакованных одноразрядных BCD-чисел после команды sub. Операндами в команде sub должны быть правильные одноразрядные BCD-числа. Рассмотрим возможные варианты вычитания одноразрядных BCD-чисел:
  • 5-9 — для вычитания необходимо сделать заем в старшем разряде. Факт такого заема в микропроцессоре фиксируется установкой флагов cf и af в 1 и вычитанием 1 из содержимого ah. В результате после команды aas в регистре al получается правильное значение (модуль результата), которое для нашего примера (с учетом заема из старшего разряда) составляет 6. Одновременно моделируется заем из старшего разряда, что позволяет производить вычитание длинных чисел.
  • 8-6 — для вычитания нет необходимости делать заем в старшем разряде. Поэтому производится сброс флагов cf и af в 0, а ah не изменяется. В результате после команды aas в регистре al получается правильное значение (модуль результата), которое для нашего примера составляет 2.
Пример 1. Вычесть десятичное число 8 из 5. Подготовить результат к выводу на экран.
        mov     al,05h
        mov     bl,08h
        sub     al,bl   ;al=0fdh
        aas             ;al=07, cf=af=1
        or      al,30h  ;al=37h — код символа 7
;вывод результата на экран
        mov     ah,2
        mov     dl,al
        int     21h

См. также: уроки 3, 8, приложение 7 и команды aaa, aad, aam, daa, das

ADC

(Addition with Carry)
Сложение с переносом
Схема команды: adc приемник,источник
Назначение: сложение двух операндов с учетом переноса из младшего разряда.

Синтаксис
Алгоритм работы:

  • сложить два операнда;
  • поместить результат в первый операнд: приемник=приемник+источник;
  • в зависимости от результата установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 02
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда adc используется при сложении длинных двоичных чисел. Ее можно использовать как самостоятельно, так и совместно с командой add. При совместном использовании команды adc с командой add сложение младших байтов/слов/двойных слов осуществляется командой add, а уже старшие байты/слова/двойные слова складываются командой adc, учитывающей переносы из младших разрядов в старшие. Таким образом, команда adc значительно расширяет диапазон значений складываемых чисел. В приложении 7 приведен пример программы сложения двоичных чисел произвольной размерности.
.data
sl1     dd      01fe544fh
sl2     dd      005044cdh
elderREZ        db      0 ;для учета переноса из старшего разряда результата
rez     dd      0
.code
...
        mov     ax,sl1
        add     ax,sl2  ;сложение младших слов слагаемых
        mov     rez,ax
        mov     ax,sl+2
        adc     ax,sl2+2        ;сложение старших слов слагаемых плюс cf
        mov     rez+2,ax
        adc     elderREZ,0      ;учесть возможный перенос

См. также: урок 8, приложение 7 и команды add, sub, sbb, xadd

ADD

(ADDition)
Сложение
Схема команды: add приемник,источник
Назначение: сложение двух операндов источник и приемник размерностью байт, слово или двойное слово.

Синтаксис
Алгоритм работы:

  • сложить операнды источник и приемник;
  • записать результат сложения в приемник;
  • установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда add используется для сложения двух целочисленных операндов. Результат сложения помещается по адресу первого операнда. Если результат сложения выходит за границы операнда приемник (возникает переполнение), то учесть эту ситуацию следует путем анализа флага cf и последующего возможного применения команды adc. Например, сложим значения в регистре ax и области памяти ch. При сложении следует учесть возможность переполнения.
chiclo  dw      2015
rez     dd      0
...
        add     ax,chislo       ;(ax)=(ax)+ch
        mov     word ptr rez,ax
        jnc     dop_sum         ;переход, если результат не вышел за разрядную сетку
        adc     word ptr rez+2,0        ;расширить результат, для учета переноса
                                ;в старший разряд
dop_sum:
...

См. также: урок 8, Приложение 7 и команды adc, sub, sbb, xadd

AND

(logical AND)
Логическое И
Схема команды: and приемник,источник
Назначение: операция логического умножения для операндов приемник и источник размерностью байт, слово или двойное слово.

Алгоритм работы:

  • выполнить операцию логического умножения над операндами источник и приемник: каждый бит результата равен 1, если соответствующие биты операндов равны 1, в остальных случаях бит результата равен 0;
  • записать результат операции в приемник;
  • установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 02 | 00
OF | SF | ZF | PF | CF
0 | r | r | r | 0
Применение:
Команда and используется для логического умножения двух операндов. Результат операции помещается по адресу первого операнда. Эту команду удобно использовать для принудительной установки или сброса определенных битов операнда.
Например, преобразуем двузначное упакованное BCD-число в его символьный эквивалент.
u_BCD   db      25h ;упакованное BCD-число
s_ch    dw      0 ;место для результата
...
        xor     ax,ax   ;очистка ax
        mov     al,u_BCD
        shl     ax,4    ;ax=0250
        mov     al,u_BCD        ;ax=0225
;преобразование в символьное представление:
        and     ax,3f3fh        ;ax=3235h
        mov     s_ch,ax

См. также: уроки 9, 12 и команды or, xor, test

BOUND

(check array BOUNDs)
Контроль нахождения индекса массива в границах
Схема команды: bound индекс,границы массива
Назначение: проверка нахождения значения индекса в границах массива.

Синтаксис
Алгоритм работы:
Cравнить значение в регистре индекс с двумя значениями, расположенными последовательно в ячейке памяти, адресуемой операндом границы массива. Диапазон значений индекса определяется используемым регистром индекс:

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

Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду bound очень удобно использовать для контроля выхода за нижнюю или верхнюю границы массива. Значения этих границ должны быть предварительно помещены в два последовательных слова (двойных слова) в памяти. Адрес этих слов (двойных слов) указывается вторым операндом. Далее динамически в ходе работы программы значение в регистре индекс, указываемом первым операндом, сравнивается со значениями этих двух границ, и если нижняя_граница<=(индексindex)<=верхняя_граница, то программа продолжает выполнение. В противном случае генерируется исключительная ситуация 5 (int 5). Далее в программе обработки этой ситуации можно выполнить необходимую корректировку и вернуться в программу (см. урок 17).

Фрагмент, который можно использовать при обработке одномерного массива с размерностью элементов в слово:
.286    ;это обязательная директива, так как bound
        ;входит в систему команд микропроцессоров, начиная с i286
.data
BoundMas        label   word
Low_Bound       dw      0
Upp_Bound       dw      20
mas     dw      10 dup (?)
...
        xor     di,di   ;очистка индексного регистра
cycl:
        mov     ax,mas[di]      ;перебор
элементов массива
        add     di,2
        bound   di,BoundMas
;если значение в di не будет попадать в границы, то будет вызван
;обработчик прерывания 5, где можно скорректировать
;значение ip/eip в стеке с тем, чтобы выйти
;из бесконечного ;цикла, например, на метку М2 или
;выполнить другие действия
        jmp     cycl
М2:
...

См. также: урок 17 и команду iret/iretd

BSF

(Bit Scan Forward)
Побитное сканирование вперед
Схема команды: bsf результат,источник
Назначение: для проверки наличия единичных битов в операнде источник.

Синтаксис
Алгоритм работы:

  • просмотр битов операнда источник, начиная с бита 0 и заканчивая битом 15/31, до тех пор, пока не встретится единичный бит;
  • если встретился единичный бит, то флаг zf устанавливается в 0 и в регистр первого операнда записывается номер позиции, где встретился единичный бит. Диапазон значений зависит от разрядности второго операнда: для 16-разрядного операнда — это 0...15; для 32-разрядного — это 0...31;
  • если единичных битов нет, то флаг zf устанавливается в 1.
Состояние флагов после выполнения команды:
06
ZF
r
Применение:
Команду bsf используют при работе на битном уровне для определения позиции в операнде крайних справа единичных битов.
Например, сдвинем содержимое регистра bx вправо таким образом, чтобы нулевой бит стал единичным:
.386
        mov     bx,0002h        ;bx=0000 0010b
...
        bsf     cx,bx   ;cx=0001h
        jz      null
        shr     bx,cl   ;bx=0000 0001b
...
null:

См. также: урок 9, 12 и команду bsr

BSR

(Bit Scan Reverse)
Побитное сканирование назад
Схема команды: bsr результат,источник
Назначение: проверка наличия единичных битов в операнде источник.

Синтаксис
Алгоритм работы:

  • просмотр битов операнда источник, начиная со старшего бита 15/31 и заканчивая битом 0, до тех пор, пока не встретится единичный бит;
  • если встретился единичный бит, флаг zf устанавливается в 0 и в регистр первого операнда записывается номер позиции (отсчет осуществляется относительно нулевой позиции), где встретился самый старший единичный бит. Диапазон значений зависит от разрядности второго операнда: для 16-разрядного операнда это 0...15; для 32-разрядного — 0...31;
  • если единичных битов нет, флаг zf устанавливается в 1.
Состояние флагов после выполнения команды:
06
ZF
r
Применение:
Команду bsr используют при работе на битном уровне для определения позиции крайних слева единичных битов.

Например, сдвинем содержимое регистра bx вправо таким образом, чтобы старший единичный бит исходного значения в bx переместился в нулевую позицию:
.386
        mov     bx,41h
...
        bsr     cx,bx   ;cx=06h
        jz      null
        shr     bx,ax   ;bx=0001h
...
null:...

См. также: уроки 9, 12 и команду bsf

BSWAP

(Byte SWAP)
Перестановка байтов
Схема команды: bswap источник
Назначение:
  • изменение порядка следования байтов;
  • переход от одной формы адресации к другой.
Под формой адресации здесь понимается принцип "младший байт по младшему адресу" или обратный ему. Существует ряд систем, например использующих микропроцессоры Motorola или большие ЭВМ, где применяется принцип размещения многобайтовых значений обратный тому, который используется в микропроцессорах Intel. Поэтому эту команду можно использовать для разработки программ-конверторов между подобными платформами и IBM РС.

Алгоритм работы: Схема алгоритма
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду bswap используют для изменения формы адресации. В качестве операнда может быть указан только 32-разрядный регистр. Эта команда используется в моделях микропроцессоров, начиная с i486.
.486
        mov     ebx,1a2c345fhh
        bswap   ebx     ;ebx=5f342c1ah

См. также: урок 7, и команду xchg

BT

(Bit Test)
Проверка битов
Схема команды: bt источник,индекс
Назначение: извлечение значения заданного бита в флаг cf.

Синтаксис
Алгоритм работы:

  • получить бит по указанному номеру позиции в операнде источник;
  • установить флаг cf согласно значению этого бита.
Состояние флагов после выполнения команды:
00
CF
r
Применение:
Команду bt используют для определения значения конкретного бита в операнде источник. Номер проверяемого бита задается содержимым второго операнда (значение числом из диапазона 0...31). После выполнения команды, флаг cf устанавливается в соответствии со значением проверяемого бита.
.386
        mov     ebx,01001100h
        bt      ebx,8   ;проверка состояния бита 8 и установка cf= в 1
        jc      m1      ;перейти на m1, если проверяемый бит равен 1
...

См. также: уроки 9, 12 и команды btc, btr, bts, test

BTC

(Bit Test and Complement)
Проверка бита с инверсией (дополнением)
Схема команды: btc источник,индекс
Назначение: извлечение значения заданного бита в флаг cf и изменение его значения в операнде на обратное.

Синтаксис
Алгоритм работы:

  • получить значение бита с номером позиции индекс в операнде источник;
  • инвертировать значение выбранного бита в операнде источник;
  • установить флаг сf исходным значением бита.
Состояние флагов после выполнения команды:
00
CF
r
Применение:
Команда btс используется для определения и инвертирования значения конкретного бита в операнде источник. Номер проверяемого бита задается содержимым второго операнда индекс (значение из диапазона 0...31). После выполнения команды флаг cf устанавливается в соответствии с исходным значением бита, то есть тем, которое было до выполнения команды.
.386
        mov     ebx,01001100h
;проверка состояния бита 8 и его обращение:
        btc     ebx,8   ;cf=1 и ebx=01001000h

См. также: уроки 9, 12 и команды bt, btr, bts, test

BTR

(Bit Test and Reset)
Проверка бита с его сбросом в 0
Схема команды: btr источник,индекс
Назначение: извлечение значения заданного бита в флаг cf и изменение его значения на нулевое.

Синтаксис
Алгоритм работы:

  • получить значение бита с указанным номером позиции в операнде источник;
  • установить флаг cf значением выбранного бита;
  • установить значение исходного бита в операнде в 0.
Состояние флагов после выполнения команды:
00
CF
r
Применение:
Команда btr используется для определения значения конкретного бита в операнде источник и его сброса в 0. Номер проверяемого бита задается содержимым второго операнда индекс (значение из диапазона 0...31). В результате выполнения команды флаг cf устанавливается в соответствии со значением исходного бита, то есть тем, что было до выполнения операции.
.386
        mov     ebx,01001100h
;проверка состояния бита 8 и его сброс в 0
        btr     ebx,8   ;cf=1 и ebx=01001000h

См. также: уроки 9, 12 и команды bt, btc, bts, test

BTS

(Bit Test and Set)
Проверка бита с его установкой в 1
Схема команды: bts источник,индекс
Назначение: извлечение значения заданного бита операнда в флаг cf и установка этого бита в единицу.

Синтаксис
Алгоритм работы:

  • получить значение бита с указанным номером позиции в операнде источник;
  • установить флаг cf значением выбранного бита;
  • установить значение исходного бита в операнде источник в 1.
Состояние флагов после выполнения команды:
00
CF
r
Применение:
Команда bts используется для определения значения конкретного бита в операнде источник и установки проверяемого бита в 1. Номер проверяемого бита задается содержимым второго операнда индекс (значение из диапазона 0...31). После выполнения команды флаг cf устанавливается в соответствии со значением исходного бита, то есть тем, что было до выполнения операции.
.386
        mov     ebx,01001100h
;проверка состояния бита 0 и его установка в 1
        bts     ebx,0   ;cf=0 ebx=01001001h

См. также: уроки 9, 12 и команды bt, btc, btr, test

CALL

(CALL)
Вызов процедуры или задачи
Схема команды: call цель
Назначение:
  • передача управления близкой или дальней процедуре с запоминанием в стеке адреса точки возврата;
  • переключение задач.
Синтаксис
Алгоритм работы:
определяется типом операнда:
  • метка ближняя — в стек заносится содержимое указателя команд eip/ip и в этот же регистр загружается новое значение адреса, соответствующее метке;
  • метка дальняя — в стек заносится содержимое указателя команд eip/ip и cs. Затем в эти же регистры загружаются новые значения адресов, соответствующие дальней метке;
  • r16, 32 или m16, 32 — определяют регистр или ячейку памяти, содержащие смещения в текущем сегменте команд, куда передается управление. При передаче управления в стек заносится содержимое указателя команд eip/ip;
  • указатель на память — определяет ячейку памяти, содержащую 4 или 6-байтный указатель на вызываемую процедуру. Структура такого указателя 2+2 или 2+4 байта. Интерпретация такого указателя зависит от режима работы микропроцессора:
  • в реальном режиме — в зависимости от размера адреса (use16 или use32) первые два байта трактуются как сегментный адрес, вторые два/четыре байта, как смещение целевой метки передачи управления. В стеке запоминается содержимое регистров cs и eip/ip;
  • в защищенном режиме — интерпретация цели передачи управления зависит от значения байта AR дескриптора, определяемого селекторной частью указателя. Целью здесь являются дальний вызов процедуры без изменения уровня привилегий, дальний вызов процедуры с изменением уровня привилегий или переключение задачи.
Состояние флагов после выполнения команды (кроме переключения задачи):
выполнение команды не влияет на флаги
При переключении задачи значения флажков изменяются в соответствии с информацией о регистре eflags в сегменте состояния TSS задачи, на которую производится переключение.
Применение:
Как видно из описания алгоритма, команда call позволяет организовать гибкую и многовариантную передачу управления на подпрограмму с сохранением адреса точки возврата. Подробно типовые примеры использования рассмотрены на уроках 10 и 14.
См. также: уроки 10, 14 и команду ret

CBW/CWDE

(Convert Byte to Word/Convert Word to Double Word Extended)
Преобразование байта в слово/слова в двойное слово
Схема команды: cbw
cwde
Назначение: расширение операнда со знаком.

Синтаксис
Алгоритм работы:
cbw — при работе команда использует только регистры al и ax:

  • анализ знакового бита регистра al:
    • если знаковый бит al=0, то ah=00h;
    • если знаковый бит al=1, то ah=0ffh.
cwde — при работе команда использует только регистры ax и eax:
  • анализ знакового бита регистра ax:
    • если знаковый бит ax=0, то установить старшее слово eax=0000h;
    • если знаковый бит ax=1, то установить старшее слово eax=0ffffh.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Данные команды используются для приведения операндов к нужной размерности с учетом знака. Такая необходимость может, в частности, возникнуть при программировании арифметических операций.
.386    ;только для cwde, cwd была для i8086
        mov     ebx,10fecd23h
        mov     ax,-3   ;ax=1111 1111 1111 1101
        cwde    ;eax=1111 1111 1111 1111 1111 1111 1111 1101
        add     eax,ebx

См. также: урок 8 и команды cdq, cwd

CLC

(CLear Carry flag)
Сброс флага переноса
Схема команды: clc
Назначение: сброс флага переноса cf.

Синтаксис
Алгоритм работы:
установка флага cf в ноль.
Состояние флагов после выполнения команды:

00
CF
0
Применение:
Данная команда используется для сброса флага cf в ноль. Такая необходимость может возникнуть при работе с командами сдвига, арифметическими командами либо действиями по индикации обнаружения ошибок и различных ситуаций в программе.
        clc             ;cf=0

См. также: уроки 8, 9 и команды cmc, stc

CLD

(CLear Direction flag)
Сброс флага направления
Схема команды: cld
Назначение: сброс в ноль флага направления df.

Синтаксис
Алгоритм работы:
установка флага df в ноль.
Состояние флагов после выполнения команды:

10
DF
0
Применение:
Данная команда используется для сброса флага df в ноль. Такая необходимость может возникнуть при работе с цепочечными командами. Нулевое занчение флага df вынуждает микропроцессор при выполнении цепочечных операций производить инкремент регистров si и di.
        cld             ;df=0

См. также: урок 11 и команды stc, movs/movsb/movsw/movsd,
cmps/cmpsb/cmpsw/cmpsd, scas/scasb/scasw/scasd,
lods/lodsb/lodsw/lodsd, stos/stosb/stosw/stosd,
ins/insb/insw/insd, outs

CLI

(CLear Interrupt flag)
Сброс флага прерывания
Схема команды: cli
Назначение: сброс флага прерывания if.

Синтаксис
Алгоритм работы:
установка флага if в ноль.
Состояние флагов после выполнения команды:

09
IF
0
Применение:
Данная команда используется для сброса флага if в ноль. Такая необходимость может возникнуть при разработке программ обработки прерываний.
        cli             ;if=0

См. также: урок 15 и команды int, iret/iretd, sti

CMC

(CoMplement Carry flag)
Инвертирование флага переноса
Схема команды: cmc
Назначение: изменение значения флага переноса cf на обратное.

Синтаксис
Алгоритм работы:
инвертирование значения флага переноса cf.
Состояние флагов после выполнения команды:

00
CF
r
Применение:
Данная команда используется для изменения значения флага cf на противоположное. В частности, этот флаг можно использовать для связи с процедурой и по его состоянию судить о результате работы данной процедуры. После выхода из процедуры этот флаг можно проанализировать командой условного перехода jc.
proc1   proc
...
        cmc
...
proc1   endp
...
        call    proc1
        jc      m1      ;если cf=1, то переход на m1
...
m1:
...

См. также: уроки 8, 9, 15 и команды clc, stc, jc, jnc

CMP

(CoMPare operands)
Сравнение операндов
Схема команды: cmp операнд1,операнд2
Назначение: сравнение двух операндов.

Синтаксис
Алгоритм работы:

  • выполнить вычитание (операнд1-операнд2);
  • в зависимости от результата установить флаги, операнд1 и операнд2 не изменять (то есть результат не запоминать).
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Данная команда используется для сравнения двух операндов методом вычитания, при этом операнды не изменяются. По результатам выполнения команды устанавливаются флаги. Команда cmp применяется с командами условного перехода и командой установки байта по значению setcc.
len     equ     10
...
        cmp     ax,len
        jne     m1      ;переход если (ax)<>len
        jmp     m2      ;переход если (ax)=len

См. также: уроки 10, 11, 12 и команды cmps/cmpsb/cmpsw/cmpsd, cmpxchg, sub, jcc, setcc

CMPS/CMPSB/CMPSW/CMPSD

(CoMPare String Byte/Word/Double word operands)
Сравнение строк байтов/слов/двойных слов
Схема команды: cmps приемник,источник
cmpsb
cmpsw
cmpsd
Назначение: сравнение двух последовательностей (цепочек) элементов в памяти.

Синтаксис
Алгоритм работы:

  • выполнить вычитание элементов (источник - приемник), адреса элементов предварительно должны быть загружены:
    • адрес источника — в пару регистров ds:esi/si;
    • адрес назначения — в пару регистров es:edi/di;
  • в зависимости от состояния флага df изменить значение регистров esi/si и edi/di:
    • если df=0, то увеличить содержимое этих регистров на длину элемента последовательности;
    • если df=1, то уменьшить содержимое этих регистров на длину элемента последовательности;
  • в зависимости от результата вычитания установить флаги:
    • если очередные элементы цепочек не равны, то cf=1, zf=0;
    • если очередные элементы цепочек или цепочки в целом равны, то cf=0, zf=1;
  • при наличии префикса выполнить определяемые им действия (см. команды repe/repne).
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команды без префиксов осуществляют простое сравнение двух элементов в памяти. Размеры сравниваемых элементов зависят от применяемой команды. Команда cmps может работать с элементами размером в байт, слово, двойное слово. В качестве операндов в команде указываются идентификаторы последовательностей этих элементов в памяти. Реально эти идентификаторы используются лишь для получения типов элементов последовательностей, а их адреса должны быть предварительно загружены в указанные выше пары регистров. Транслятор, обработав команду cmps и выяснив тип операндов, генерирует одну из машинных команд cmpsb, cmpsw или cmpsd. Машинного аналога для команды cmps нет. Для адресации назначения обязательно должен использоваться регистр es, а для адресации источника можно делать замену сегмента с использованием соответствующего префикса.
Для того чтобы эти команды можно было использовать для сравнения последовательности элементов, имеющих размерность байт, слово, двойное слово, необходимо использовать один из префиксов repe или repne. Префикс repe заставляет циклически выполняться команды сравнения до тех пор, пока содержимое регистра ecx/cx не станет равным нулю или пока не совпадут очередные сравниваемые элементы цепочек (флаг zf=1). Префикс repne заставляет циклически производить сравнение до тех пор, пока не будет достигнут конец цепочки (ecx/cx=0) либо не встретятся различающиеся элементы цепочек (флаг zf=0).
.data
obl1    db      'Строка для сравнения'
obl1    db      'Строка для сравнения'
a_obl1  dd      obl1
a_obl2  dd      obl2
.code
...
        cld             ;просмотр цепочки в направлении возрастания адресов
        mov     cx,20   ;длина цепочки
        lds     si,a_obl1       ;адрес источника в пару ds:si
        les     di,a_obl2       ;адрес назначения в пару ds:si
repe    cmpsb           ;сравнивать, пока равны
        jnz     m1      ;если не конец цепочки, то встретились разные элементы
...                     ;действия, если цепочки совпали
...
m1:
...                     ;действия, если цепочки не совпали

См. также: уроки 10, 11 и команды ins, lods, movs, outs, scas, stos, repe, repz, repne, repnz

CMPXCHG

(CoMPare and eXCHanGe)
Сравнение и обмен
Схема команды: cmpxchg приемник,источник(аккумулятор)
Назначение: сравнение и обмен значений между источником и приемником.

Синтаксис
Алгоритм работы:

  • выполнить сравнение элементов источник и приемник;
  • если источник и приемник не равны, то:
    • установить zf=0;
    • переслать содержимое операнда приемник в источник (регистр al/ax/eax).
  • если источник и приемник равны, то:
    • установить zf=1;
    • переслать содержимое операнда источник (регистр al/ax/eax) по месту операнда приемник.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команды сравнивают два операнда. Один из сравниваемых операндов находится в аккумуляторе (регистре al/ax/eax), другой может находиться в памяти или регистре общего назначения. Если значения равны, то производится замена содержимого операнда приемник содержимым источника, находящимся в регистре-аккумуляторе. Если значения не равны, то производится замена содержимого операнда источника находящимся в регистре-аккумуляторе содержимым операнда назначения. Определить тот факт, была ли произведена смена значения в аккумуляторе (то есть были ли не равны сравниваемые операнды), можно по значению флага zf.
.486
        mov     ax,114eh
        mov     bx,8e70h
        cmpxchg bx,ax
        jz      m1      ;переход, если zf=1, то есть операнды равны
                        ;и ax не изменился
...                     ;действия, если операнды не равны
m1:

См. также: уроки 7, 10 и команды cmp, xchg

CWD

(Convert Word to Double word)
Преобразование слова в двойное слово
Схема команды: cwd
Назначение: расширение слова со знаком до размера двойного слова со знаком.

Синтаксис
Алгоритм работы:
копирование значения старшего бита регистра ax во все биты регистра dx. Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команда cwd используется для расширения значения знакового бита в регистре ax на биты регистра dx. Данную операцию, в частности, можно использовать для подготовки к операции деления, для которой размер делимого должен быть в два раза больше размера делителя, либо для приведения операндов к одной размерности в командах умножения, сложения, вычитания.
        mov     ax,25
...
        mov     bx,4
        cwd
        div     bx

См. также: урок 8 и команды cbw, cdq, cwde, div, idiv, mul, imul, add, adc, sub, sbb

CDQ

(Convert Double word to Quad word)
Преобразование двойного слова в учетверенное слово
Схема команды: cdq
Назначение: расширение двойного слова со знаком до размера учетверенного слова (64 бита) со знаком.

Синтаксис
Алгоритм работы:
копирование значения старшего бита регистра eax на все биты регистра edx. Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду cdq можно использовать для распространения значения знакового бита в регистре eax на все биты регистра edx. Данную операцию, в частности, можно использовать для подготовки к операции деления, для которой размер делимого должен быть в два раза больше размера делителя.
.386
delimoe dd      ...
delitel dd      ...
...
        mov     eax,delimoe
        cdq
        idiv    delitel ;частное в eax, остаток в edx

См. также: урок 8 и команды cbw, cwd, cwde, div, idiv

DAA

(Decimal Adjust for Addition)
Десятичная коррекция после сложения
Схема команды: daa
Назначение: коррекция упакованного результата сложения двух BCD-чисел в упакованном формате.

Синтаксис
Алгоритм работы:
команда работает только с регистром al и анализирует наличие следующих ситуаций:

  • Ситуация 1. В результате предыдущей команды сложения флаг af=1 или значение младшей тетрады регистра al>9. Напомним, что флаг af устанавливается в 1 в случае переноса двоичной единицы из бита 3 младшей тетрады в старшую тетраду регистра al (если значение превысило 0fh). Наличие одного из этих двух признаков говорит о том, что значение младшей тетрады превысило 9h.
  • Ситуация 2. В результате предыдущей команды сложения флаг сf=1 или значение регистра al>9fh. Напомним, что флаг cf устанавливается в 1 в случае переноса двоичной единицы в старший бит операнда (если значение превысило 0ffh в случае регистра al). Наличие одного из этих двух признаков говорит о том, что значение в регистре al превысило 9fh.
Если имеет место одна из этих двух ситуаций, то регистр al корректируется следующим образом:
  • для ситуации 1 содержимое регистра al увеличивается на 6;
  • для ситуации 2 содержимое регистра al увеличивается на 60h;
  • если имеют место обе ситуации, то корректировка начинается с младшей тетрады.
Состояние флагов после выполнения команды (в случае, если были переносы):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | 1 | r | 1
Состояние флагов после выполнения команды (в случае, если переносов не было):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | 0 | r | 0
Применение:
Эту команду следует применять после сложения двух упакованных BCD-чисел с целью корректировки получающегося двоичного результата сложения в правильное двузначное десятичное число. После команды daa следует анализировать состояние флага cf. Если он равен 1, то это говорит о том, что был перенос единицы в старший разряд и это нужно учесть для сложения старших десятичных цифр BCD-числа.
        mov     al,69h  ;69h — упакованное BCD-число
        mov     bl,74h  ;74h — упакованное BCD-число
        adc     al,bl   ;al=0ddh
        daa             ;cf=1, al=43h
;если перенос, то переход на ту ветвь программы,
;где он будет учтен:
        jc m1

См. также: урок 8, Приложение 7 и команды aaa, aad, aam, aas, das

DAS

(Decimal Adjust for Subtraction)
Десятичная коррекция после вычитания
Схема команды: das
Назначение: коррекция упакованного результата вычитания двух BCD-чисел в упакованном формате.

Синтаксис
Алгоритм работы:
команда das работает только с регистром al и анализирует наличие следующих ситуаций:

  • Ситуация 1. В результате предыдущей команды сложения флаг af =1 или значение младшей тетрады регистра al>9. Напомним, что для случая вычитания флаг af устанавливается в 1 в случае заема двоичной единицы из старшей тетрады в младшую тетраду регистра al. Наличие одного из этих двух признаков говорит о том, что значение младшей тетрады превысило 9h и его нужно корректировать.
  • Ситуация 2. В результате предыдущей команды сложения флаг сf =1 или значение регистра al>9fh. Напомним, что для случая вычитания флаг cf устанавливается в 1 в случае заема двоичной единицы. Наличие одного из этих двух признаков говорит о том, что значение в регистре al превысило 9fh.
Если имеет место одна из этих ситуаций, то регистр al корректируется следующим образом:
  • для ситуации 1 содержимое регистра al уменьшается на 6;
  • для ситуации 2 содержимое регистра al уменьшается на 60h;
  • если имеют место обе ситуации, то корректировка начинается с младшей тетрады.
Состояние флагов после выполнения команды (в случае, если были переносы):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | 1 | r | 1
Состояние флагов после выполнения команды (в случае, если переносов не было):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | 0 | r | 0
Применение:
Команду das следует применять после вычитания двух упакованных BCD-чисел с целью корректировки получающегося двоичного результата вычитания в правильное двузначное десятичное число. После команды das следует анализировать состояние флага cf. Если он равен 1, то это говорит о том, что был заем единицы в старший разряд и это нужно учесть в дальнейших действиях. Если у вычитаемого нет больше старших разрядов, то результат следует трактовать как отрицательное двоичное дополнение. Для определения его абсолютного значения нужно вычесть 100 из результата в al. Если у вычитаемого еще есть старшие разряды, то факт заема нужно просто учесть уменьшением младшего из этих оставшихся старших разрядов на единицу.
        mov     ah,08h  ;ah=08h
        mov     al,05h  ;al=05h
        add     al,ah   ;al=al+ah=05h+08h=0dh — не BCD-число
        xor     ah,ah   ;ah=0
        aaa             ;ah=01h,al=03h — результат скорректирован

См. также: урок 8, Приложение 7 и команды aaa, aad, aam, aas, daa

DEC

(DECrement operand by 1)
Уменьшение операнда на единицу
Схема команды: dec операнд
Назначение: уменьшение значения операнда в памяти или регистре на 1.

Синтаксис
Алгоритм работы:
команда вычитает 1 из операнда. Состояние флагов после выполнения команды:

11 | 07 | 06 | 04 | 02
OF | SF | ZF | AF | PF
r | r | r | r | r
Применение:
Команда dec используется для уменьшения значения байта, слова, двойного слова в памяти или регистре на единицу. При этом заметьте то, что команда не воздействует на флаг cf.
        mov     al,9
...
        dec     al      ;al=8

См. также: урок 8 и команды inc, sub

DIV

(DIVide unsigned)
Деление беззнаковое
Схема команды: div делитель
Назначение: выполнение операции деления двух двоичных беззнаковых значений.

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

  • если делитель размером в байт, то делимое должно быть расположено в регистре ax. После операции частное помещается в al, а остаток — в ah;
  • если делитель размером в слово, то делимое должно быть расположено в паре регистров dx:ax, причем младшая часть делимого находится в ax. После операции частное помещается в ax, а остаток — в dx;
  • если делитель размером в двойное слово, то делимое должно быть расположено в паре регистров edx:eax, причем младшая часть делимого находится в eax. После операции частное помещается в eax, а остаток — в edx.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | ? | ? | ? | ? | ?
Применение:
Команда выполняет целочисленное деление операндов с выдачей результата деления в виде частного и остатка от деления. При выполнении операции деления возможно возникновение исключительной ситуации: 0 — ошибка деления. Эта ситуация возникает в одном из двух случаев: делитель равен 0 или частное слишком велико для его размещения в регистре eax/ax/al.
        mov     ax,10234
        mov     bl,154
        div     bl      ;ah=остаток, al=частное

См. также: урок 8, приложение 7 и команду idiv

ENTER

(setup parameter block for ENTERing procedure)
Установка кадра стека для параметров процедуры
Схема команды: enter loc_size,lex_lev
Назначение: установка границы в стеке для локальных переменных процедуры.

Синтаксис
Алгоритм работы:

  • поместить текущее значение регистра ebp/bp в стек;
  • сохранить текущее значение esp/sp в промежуточной переменной fp (имя переменной выбрано случайно);
  • если лексический уровень вложенности (операнд lex_lev) не равен нулю, то (lex_lev–1) раз делать следующее:
    • в зависимости от установленного режима адресации use16 или use32 выполнить вычитание (bp–2) или (ebp–4) и записать результат обратно в ebp/bp;
    • сохранить значение ebp/bp в стеке;
    • сохранить в стеке значение промежуточной переменной fp;
  • записать значение промежуточной переменной fp в регистр ebp/bp;
  • уменьшить значение регистра esp/sp на величину, заданную первым операндом, минус размер области локальных переменных loc_size: esp/sp=(esp/sp)–loc_size.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда enter специально введена в систему команд микропроцессора для поддержки блочно-структурированных языков высокого уровня типа Pascal или С. В этих языках программа разбивается на блоки. В блоках можно описать свои собственные (локальные) идентификаторы, которые не могут быть использованы вне этого блока. К примеру, на рисунке ниже в виде блоков изображена структура некоторой программы.
Изображение структуры некоторой программы в виде блоков

В правом верхнем углу каждого блока (процедуры) стоит номер лексического уровня вложенности этого блока относительно других блоков программы. Большинство блочно-структурированных языков в качестве основного метода распределения памяти для переменных в блоках используют автоматическое распределение памяти. Это означает, что при входе в блок (вызове процедуры и т. п.) в некотором месте памяти (или в стеке) выделяется область памяти для переменных этого блока (ее можно назвать областью инициализации). После выхода из этого блока связь программы с этой областью теряется, то есть эти переменные становятся недоступными. Но если, как в нашем примере, в этой процедуре есть вложенные блоки (процедуры), то для некоторого внутреннего блока (например, C) могут быть доступны области инициализации (переменные) блоков, объемлющих данный блок. В нашем примере для блока C доступны также переменные блоков B и A, но не D. Возникает вопрос: как же программа, находясь в конкретной точке своего выполнения, может отслеживать то, какие области инициализации ей доступны? Это делается с помощью структуры данных, называемой дисплеем. Дисплей содержит указатели на самую последнюю область текущего блока и на области инициализации всех блоков, объемлющих данный блок в программе. Например, если в программе A была вызвана сначала процедура B, а затем C, то дисплей содержит указатели на области инициализации A, B и C (см. рисунок ниже).

Если после этого вызвать процедуру D (в то время как B и C еще не завершены), то картина изменится.

После того как некоторый блок (процедура) завершает свою работу, ее область инициализации удаляется из памяти (стека) и одновременно соответствующим образом корректируется дисплей. Большинство языков высокого уровня хранят локальные данные блоков в стеке. Эти переменные называют еще автоматическими или динамическими. Память для них резервируется путем уменьшения значения регистра-указателя стека esp/sp на величину, равную длине области, занимаемой этими динамическими переменными. Доступ к этим переменным осуществляется посредством регистра ebp/bp. Если один блок вложен в другой, то для его динамических (локальных) переменных также выделяется место (кадр) в стеке, но в этот кадр помещается указатель на кадр стека для включающего его блока. Команды enter и leave как раз и позволяют поддержать в языке ассемблера принципы работы с переменными блоков как в блочно-структурированных языках. Дисплей организуется с помощью второго операнда команды enter и стека. Например, в начале работы главной процедуры A и после вызова процедуры B кадр стека будет выглядеть так.

Соответственно, после вызова процедур C и D стек будет выглядеть, как показано ниже.

Таким образом, видно, что используя дисплей, мы фактически имеем адреса областей инициализации, доступных по признаку вложенности объемлющих блоков. Обратный процесс завершения работы с блоками и удаления соответствующих областей инициализации поддерживается командой leave.
.286
proc1   proc
;зарезервировать в стеке место для локальных переменных
;proc1 16 байт
;лексический уровень вложенности 0
        enter   16,0
...
        leave
        ret
proc1   endp

См. также: урок 14 и команды leave, ret

HLT

(HaLT)
Остановка
Схема команды: hlt
Назначение: остановка микропроцессора до прерывания или перезагрузки.

Синтаксис
Алгоритм работы:
перевод микропроцессора в состояние остановки.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
В результате выполнения команды микропроцессор переходит в состояние остановки. Из этого состояния его можно вывести сигналами на входах RESET, NMI, INTR. Если для возобновления работы микропроцессора используется прерывание, то сохраненное значение пары cs:eip/ip указывает на команду, следующую за hlt. Для иллюстрации применения данной команды рассмотрим еще один способ переключения микропроцессора из защищенного в реальный режим и его возврата обратно в реальный режим (см. урок 16). Как известно, в микропроцессоре не предусмотрено специальных средств для подобного переключения. Сброс микропроцессора можно инициировать, если вывести байт со значением 0feh в порт клавиатуры 64h. После этого микропроцесор переходит в реальный режим и управление получает программа BIOS, которая анализирует байт отключения в CMOS-памяти по адресу 0fh. Для нас интерес представляют два значения этого байта — 5h и 0ah:
  • 5h — сброс микропроцессора инициирует инициализацию программируемого контроллера прерываний на значение базового вектора 08h (см. уроки 15 и 17). Далее управление передается по адресу, который находится в ячейке области данных BIOS 0040:0067;
  • 0ah — сброс микропроцессора инициирует непосредственно передачу управления по адресу в ячейке области данных BIOS 0040:0067 (то есть без перепрограммирования контроллера прерываний).
Таким образом, если вы не используете прерываний, то достаточно установить байт 0fh в CMOS-памяти в 0ah. Предварительно, конечно, вы должны инициализировать ячейку области данных BIOS 0040:0067 значением адреса, по которому необходимо передать управление после сброса. Для программирования CMOS-памяти используются номера портов 070h и 071h. Вначале в порт 070h заносится нужный номер ячейки CMOS-памяти, а затем в порт 071h — новое значение этой ячейки.
;работаем в реальном режиме, готовимся к переходу
;в защищенный режим:
        push    es
        mov     ax,40h
        mov     es,ax
        mov     word ptr es:[67h],offset ret_real
;ret_real — метка в программе, с которой должно
;начаться выполнение программы после сброса
        mov     es:[69h],cs
        mov     al,0fh  ;будем обращаться к ячейке 0fh в CMOS
        out     70h,al
        jmp     $+2     ;чуть задержимся, чтобы аппаратура отработала
;сброс без перепрограммирования контроллера
        mov     al,0ah
        out     71h,al
;переходим в защищенный режим установкой
;бита 0 cr0 в 1 (см. урок 16)
;работаем в защищенном режиме
;готовимся перейти обратно в реальный режим
        mov     al,01fch
        out     64h,al  ;сброс микропроцессора  hlt
;остановка до физического окончания процесса сброса
        ret_real:       ...     ;метка, на которую будет передано
                        ;управление после сброса

См. также: уроки 15, 16, 17

IDIV

(Integer DIVide)
Деление целочисленное со знаком
Схема команды: idiv делитель
Назначение: операция деления двух двоичных значений со знаком.

Синтаксис
Алгоритм работы:
Для команды необходимо задание двух операндов — делимого и делителя. Делимое задается неявно, и размер его зависит от размера делителя, местонахождение которого указывается в команде:

  • если делитель размером в байт, то делимое должно быть расположено в регистре ax. После операции частное помещается в al, а остаток — в ah;
  • если делитель размером в слово, то делимое должно быть расположено в паре регистров dx:ax, причем младшая часть делимого находится в ax. После операции частное помещается в ax, а остаток — в dx;
  • если делитель размером в двойное слово, то делимое должно быть расположено в паре регистров edx:eax, причем младшая часть делимого находится в eax. После операции частное помещается в eax, а остаток — в edx;
Остаток всегда имеет знак делимого. Знак частного зависит от состояния знаковых битов (старших разрядов) делимого и делителя.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | ? | ? | ? | ? | ?
Применение:
Команда выполняет целочисленное деление операндов с учетом их знаковых разрядов. Результатом деления являются частное и остаток от деления. При выполнении операции деления возможно возникновение исключительной ситуации: 0 — ошибка деления. Эта ситуация возникает в одном из двух случаев: делитель равен 0 или частное слишком велико для его размещения в регистре eax/ax/al.
;деление слов
        mov     ax,1045 ;делимое
        mov     bx,587  ;делитель
        cwd             ;расширение делимого dx:ax
        idiv    bx      ;частное в ax, остаток в dx

См. также: урок 8, приложение 7 и команду div

IMUL

(Integer MULtiply)
Умножение целочисленное со знаком
Схема команды: imul множитель_1
imul множ_1,множ_2
imul рез-т,множ_1,множ_2
Назначение: операция умножения двух целочисленных двоичных значений со знаком.

Синтаксис
Алгоритм работы:
Алгоритм работы команды зависит от используемой формы команды. Форма команды с одним операндом требует явного указания местоположения только одного сомножителя, который может быть расположен в ячейке памяти или регистре. Местоположение второго сомножителя фиксировано и зависит от размера первого сомножителя:

  • если операнд, указанный в команде, — байт, то второй сомножитель располагается в al;
  • если операнд, указанный в команде, — слово, то второй сомножитель располагается в ax;
  • если операнд, указанный в команде, — двойное слово, то второй сомножитель располагается в eax.
Результат умножения для команды с одним операндом также помещается в строго определенное место, определяемое размером сомножителей:
  • при умножении байтов результат помещается в ax;
  • при умножении слов результат помещается в пару dx:ax;
  • при умножении двойных слов результат помещается в пару edx:eax.
Команды с двумя и тремя операндами однозначно определяют расположение результата и сомножителей следующим образом:
  • в команде с двумя операндами первый операнд определяет местоположение первого сомножителя. На его место впоследствии будет записан результат. Второй операнд определяет местоположение второго сомножителя;
  • в команде с тремя операндами первый операнд определяет местоположение результата, второй операнд — местоположение первого сомножителя, третий операнд может быть непосредственно заданным значением размером в байт, слово или двойное слово.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | ? | ? | ? | ? | r
Команда imul устанавливает в ноль флаги of и cf, если размер результата соответствует регистру назначения. Если эти флаги отличны от нуля, то это означает, что результат слишком велик для отведенных ему регистром назначения рамок и необходимо указать больший по размеру регистр для успешного завершения данной операции умножения. Конкретными условиями сброса флагов of и cf в ноль являются следующие условия:
  • для однооперандной формы команды imul регистры ax/dx/edx являются знаковыми расширениями регистров al/ax/eax;
  • для двухоперандной формы команды imul для размещения результата умножения достаточно размерности указанных регистров назначения r16/r32;
  • то же для трехоперандной команды умножения.
Применение:
Команда выполняет целочисленное умножение операндов с учетом их знаковых разрядов. Для выполнения этой операции необходимо наличие двух сомножителей. Размещение и задание их местоположения в команде зависит от формы применяемой команды умножения, которая, в свою очередь, определяется моделью микропроцессора. Так, для микропроцессора i8086 возможна только однооперандная форма команды, для последующих моделей микропроцессоров дополнительно можно использовать двух- и трехоперандные формы этой команды.
.486
...
        mov     bx,186
        imul    eax,bx,8
;если результату не хватило размерности операнда1,
;то перейдем на m1, где скорректируем ситуацию:
        jc      m1

См. также: урок 8, приложение 7 и команду mul

IN

(INput operand from port)
Ввод операнда из порта
Схема команды: in аккумулятор,ном_порта
Назначение: ввод значения из порта ввода-вывода.

Синтаксис
Алгоритм работы:
Передает байт, слово, двойное слово из порта ввода-вывода в один из регистров al/ax/eax. Состояние флагов после выполнения команды: выполнение команды не влияет на флаги.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команда применяется для прямого управления оборудованием компьютера посредством портов. Номер порта задается вторым операндом в виде непосредственного значения или значения в регистре dx. Непосредственным значением можно задать порт с номером в диапазоне 0-255. При использовании порта с большим номером используется регистр dx. Размер данных определяется размерностью первого операнда и может быть байтом, словом, двойным словом. В качестве примера применения рассмотрим фрагмент обработчика прерывания от клавиатуры 9. Это прерывание вызывается всякий раз при нажатии любой клавиши на клавиатуре. Обработчик этого прерывания должен прочитать скан-код клавиши, подтвердить микропроцессору клавиатуры факт приема скан-кода, преобразовать этот код в соответствии с клавишами-переключателями и поместить преобразованный код в буфер клавиатуры, находящийся в области BIOS. Действия чтения и подтверждения приема скан-кода могут выглядеть, к примеру, так:
        in      al,60h  ;читаем скан-код
        push    ax      ;сохраним его на время
        in      al,61h  ;читаем порт 61h
        or      al,80h  ;старший бит байта из порта 61h в 1
        out     61h,al  ;подтверждаем факт приема скан-кода
        pop     ax
        out     61h,al  ;восстановили байт в порту 61h

См. также: урок 7 и команды out, ins/insb/insw/insd, outs

INC

(INCrement operand by 1)
Увеличить операнд на 1
Схема команды: inc операнд
Назначение: увеличение значения операнда в памяти или регистре на 1.

Синтаксис
Алгоритм работы:
команда увеличивает операнд на единицу.
Состояние флагов после выполнения команды:

11 | 07 | 06 | 04 | 02
OF | SF | ZF | AF | PF
r | r | r | r | r
Применение:
Команда используется для увеличения значения байта, слова, двойного слова в памяти или регистре на единицу. При этом команда не воздействует на флаг cf.
        inc     ax      ;увеличить значение в ax на 1

См. также: урок 8 и команды dec, add, adc

INS/INSB/INSW/INSD

(Input String Byte/Word/Double word operands)
Ввод строк байтов/слов/двойных слов из порта
Схема команды: ins приемник,порт
insb
insw
insd
Назначение: ввод из порта в память последовательности байт, слов, двойных слов.

Синтаксис
Алгоритм работы:

  • передать данные из порта ввода-вывода, номер которого загружен в регистр dx, в память по адресу es:edi/di;
  • в зависимости от состояния флага df изменить значение регистров edi/di:
    • если df=0, то увеличить содержимое этих регистров на длину структурного элемента последовательности;
    • если df=1, то уменьшить содержимое этих регистров на длину структурного элемента последовательности;
  • при наличии префикса выполнить определяемые им действия (см. команду rep).
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда вводит данные из порта ввода-вывода, номер которого загружен в регистр dx, в память по адресу es:edi/di. Сегментная составляющая адреса должна быть обязательно в регистре es. Замена сегментного регистра недопустима. Непосредственное задание порта в команде также недопустимо - для этого используется регистр dx. Размеры вводимых элементов зависят от применяемой команды. Команда ins может работать с элементами размером в байт, слово, двойное слово. В качестве операндов в команде указывается символическое имя ячейки памяти, в которую вводятся элементы из порта ввода-вывода. Реально это символическое имя используется лишь для получения типа элемента последовательности, а его адрес должен быть предварительно загружен в пару регистров es:edi/di. Транслятор, обработав команду ins и выяснив тип операнда, генерирует одну из машинных команд insb, insw или insd. Машинного аналога для команды ins нет. Для того чтобы эти команды можно было использовать для ввода последовательности элементов, имеющих размерность байт, слово, двойное слово, необходимо использовать префикс rep. Префикс rep заставляет циклически выполняться команду ввода до тех пор, пока содержимое регистра ecx/cx не станет равным нулю.
.286
;ввести 10 байт из порта 300h (номер порта bgr условно)
;в цепочку байт в памяти по адресу
str_10  db      10 dup(0)
adr_str dd      str_10
        les     di,adr_str
        mov     dx,300h
rep     insb
...

См. также: уроки 2, 11 и команды cmps/cmpsb/cmpsw/cmpsd, lods/lodsb/lodsw/lodsd, movs/movsb/movsw/movsd, outs, scas/scasb/scasw/scasd, stos/stosb/stosw/stosd, rep/repe/repz/repne/repnz

INT

(INTerrupt)
Вызов подпрограммы обслуживания прерывания
Схема команды: int номер_прерывания
Назначение: вызов подпрограммы обслуживания прерывания с номером прерывания, заданным операндом команды.

Синтаксис
Алгоритм работы:

  • записать в стек регистр флагов eflags/flags и адрес возврата. При записи адреса возврата вначале записывается содержимое сегментного регистра cs, затем содержимое указателя команд eip/ip;
  • сбросить в ноль флаги if и tf;
  • передать управление на программу обработки прерывания с указанным номером. Механизм передачи управления зависит от режима работы микропроцессора (см. уроки 15 и 17).
Состояние флагов после выполнения команды:
09 | 08
IF | TF
0 | 0
Применение:
Как видно из синтаксиса, существуют две формы этой команды:
  • int 3 — имеет свой индивидуальный код операции 0cch и занимает один байт. Это обстоятельство делает ее очень удобной для использования в различных программных отладчиках для установки точек прерывания путем подмены первого байта любой команды. Микропроцессор, встречая в последовательности команд команду с кодом операции 0cch, вызывает программу обработки прерывания с номером вектора 3, которая служит для связи с программным отладчиком.
  • Вторая форма команды занимает два байта, имеет код операции 0cdh и позволяет инициировать вызов подпрограммы обработки прерывания с номером вектора в диапазоне 0–255. Особенности передачи управления, как было отмечено, зависят от режима работы микропроцессора.
;вызов обработчика аппаратного прерывания 08h из программы:
        int     08h

См. также: уроки 15, 17 и команды into, iret/iretd

INTO

(INTerrupt if Overflow)
Прерывание, если переполнение
Схема команды: into
Назначение: инициирование прерывания с номером 4, если установлен флаг of.

Синтаксис
Алгоритм работы:
Проанализировать состояние флага of:

  • если of=0, то никаких действий производить не нужно — передать управление на следующую команду;
  • если of=1, то дальнейшие действия, как при команде int, то есть:
    • записать в стек регистр флагов eflags/flags и адрес возврата. При записи адреса возврата вначале записывается содержимое сегментного регистра cs, затем содержимое указателя команд eip/ip;
    • сбросить в ноль флаги if и tf;
    • передать управление на программу обработки прерывания с данным номером. Механизм передачи зависит от режима работы микропроцессора (см. уроки 15 и 17).
Состояние флагов после выполнения команды:
09 | 08
IF | TF
r | r
Применение:
Свойство этой команды инициировать вызов подпрограммы обработки прерывания с номером вектора 4 определяет варианты ее применения. Если предыдущая команда в программе может в результате своей работы установить флаг переполнения of (к примеру, арифметические команды), то для обнаружения и обработки такой ситуации можно использовать команду into. Особенности передачи управления и обработки (корректировки) результата зависят от режима работы микропроцессора.
.486
...
        mov     bx,186
        imul    eax,bx,8
;если результату не хватило размерности операнда1,
;то of установится в 1
;исправим ситуацию в обработчике прерывания 3
        into

См. также: уроки 8, 15, 17 и команды int, iret/iretd, imul

IRET/IRETD

(Interrupt RETurn)
Возврат из прерывания
Схема команды: iret
iretd
Назначение: используется в той точке программы обработки прерывания, откуда необходимо вернуть управление прерванной программе.

Синтаксис
Алгоритм работы:
Работа команды зависит от режима работы микропроцесссора:

  • в реальном режиме команда iret последовательно извлекает из стека и затем восстанавливает в микропроцессоре содержимое следующих регистров: eip/ip, cs, eflags/flags. Далее прерванная программа продолжается с точки прерывания;
  • в защищенном режиме действия команды зависят от состояния флага NT (вложенной задачи) в регистре флагов:
    • если NT=0, то производятся действия по возврату управления прерванной программе, при этом характер этих действий зависит от соотношения уровней привилегированности прерванной программы и программы обработки прерывания;
    • в случае NT=1 производятся действия по переключению задач.
Состояние флагов после выполнения команды:
11 | 10 | 09 | 08 | 07 | 06 | 04 | 02 | 00
OF | DF | IF | TF | SF | ZF | AF | PF | CF
r | r | r | r | r | r | r | r | r
Применение:
Команду iret необходимо применять для восстановления сохраненных командой int регистров флагов, указателя команд и сегментного регистра кода. Число этих команд в программе обработки прерывания должно соответствовать количеству точек выхода из нее. Команда iretd используется в старших моделях микропроцессоров для извлечения из стека и восстановления 32-битных регистров.
my_int1c        proc
;программа обработки прерывания 1Ch
...
        iret
        endp

См. также: уроки 15, 17 и команды int, into

JCC
JCXZ/JECXZ

(Jump if condition)
(Jump if CX=Zero/ Jump if ECX=Zero)
Переход, если выполнено условие
Переход, если CX/ECX равен нулю
Схема команды: jcc метка
jcxz метка
jecxz метка
Назначение: переход внутри текущего сегмента команд в зависимости от некоторого условия.

Синтаксис
Алгоритм работы команд (кроме jcxz/jecxz):
Проверка состояния флагов в зависимости от кода операции (оно отражает проверяемое условие):

  • если проверяемое условие истинно, то перейти к ячейке, обозначенной операндом;
  • если проверяемое условие ложно, то передать управление следующей команде.
Алгоритм работы команды jcxz/jecxz:
Проверка условия равенства нулю содержимого регистра ecx/cx:
  • если проверяемое условие истинно, то есть содержимое ecx/cx равно 0, то перейти к ячейке, обозначенной операндом метка;
  • если проверяемое условие ложно, то есть содержимое ecx/cx не равно 0, то передать управление следующей за jcxz/jecxz команде программы.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 05 | 04 | 03 | 02 | 01 | 00
OF | SF | ZF | 0 | AF | 0 | PF | 1 | CF
? | ? | ? |  | r |  | ? |  | r
Применение (кроме jcxz/jecxz):
Команды условного перехода удобно применять для проверки различных условий, возникающих в ходе выполнения программы. Как известно, многие команды формируют признаки результатов своей работы в регистре eflags/flags. Это обстоятельство и используется командами условного перехода для работы. Ниже приведены перечень команд условного перехода, анализируемые ими флаги и соответствующие им логические условия перехода.
Команда | Состояние проверяемых флагов | Условие перехода
JA | CF = 0 и ZF = 0 | если выше
JAE | CF = 0 | если выше или равно
JB | CF = 1 | если ниже
JBE | CF = 1 или ZF = 1 | если ниже или равно
JC | CF = 1 | если перенос
JE | ZF = 1 | если равно
JZ | ZF = 1 | если 0
JG | ZF = 0 и SF = OF | если больше
JGE | SF = OF | если больше или равно
JL | SF <> OF | если меньше
JLE | ZF=1 или SF <> OF | если меньше или равно
JNA | CF = 1 и ZF = 1 | если не выше
JNAE | CF = 1 | если не выше или равно
JNB | CF = 0 | если не ниже
JNBE | CF=0 и ZF=0 | если не ниже или равно
JNC | CF = 0 | если нет переноса
JNE | ZF = 0 | если не равно
JNG | ZF = 1 или SF <> OF | если не больше
JNGE | SF <> OF | если не больше или равно
JNL | SF = OF | если не меньше
JNLE | ZF=0 и SF=OF | если не меньше или равно
JNO | OF=0 | если нет переполнения
JNP | PF = 0 | если количество единичных битов результата нечетно (нечетный паритет)
JNS | SF = 0 | если знак плюс (знаковый (старший) бит результата равен 0)
JNZ | ZF = 0 | если нет нуля
JO | OF = 1 | если переполнение
JP | PF = 1 | если количество единичных битов результата четно (четный паритет)
JPE | PF = 1 | то же, что и JP, то есть четный паритет
JPO | PF = 0 | то же, что и JNP
JS | SF = 1 | если знак минус (знаковый (старший) бит результата равен 1)
JZ | ZF = 1 | если ноль
Логические условия "больше" и "меньше" относятся к сравнениям целочисленных значений со знаком, а "выше и "ниже" — к сравнениям целочисленных значений без знака. Если внимательно посмотреть, то у многих команд можно заметить одинаковые значения флагов для перехода. Это объясняется наличием нескольких ситуаций, которые могут вызвать одинаковое состояние флагов. В этом случае с целью удобства ассемблер допускает несколько различных мнемонических обозначений одной и той же машинной команды условного перехода. Эти команды ассемблера по действию абсолютно равнозначны, так как это одна и та же машинная команда. Изначально в микропроцессоре i8086 команды условного перехода могли осуществлять только короткие переходы в пределах -128...+127 байт, считая от следующей команды. Начиная с микропроцессора i386, эти команды уже могли выполнять любые переходы в пределах текущего сегмента команд. Это стало возможным за счет введения в систему команд микропроцессора дополнительных машинных команд. Для реализации межсегментных переходов необходимо комбинировать команды условного перехода и команду безусловного перехода jmp. При этом можно воспользоваться тем, что практически все команды условного перехода парные, то есть имеют команды, проверяющие обратные условия.
Применение jcxz/jecxz:
Команда | Состояние флагов в eflags/flags | Условие перехода
JCXZ | не влияет | если регистр CX=0
JECXZ | не влияет | если регистр ECX=0
Команду jcxz/jecxz удобно использовать со всеми командами, использующими регистр ecx/cx для своей работы. Это команды организации цикла и цепочечные команды. Очень важно отметить то, что команда jcxz/jecxz, в отличие от других команд перехода, может выполнять только близкие переходы в пределах -128...+127 байт, считая от следующей команды. Поэтому для нее особенно актуальна проблема передачи управления далее чем в указанном диапазоне. Для этого можно привлечь команду безусловного перехода jmp. Например, команду jcxz/jecxz можно использовать для предварительной проверки счетчика цикла в регистре cx для обхода цикла, если его счетчик нулевой.
...
        jcxz    m1      ;обойти цикл, если cx=0
cycl:
;некоторый цикл
        loop    cycl
m1:     ...

См. также: уроки 10, 11 и команду jmp

JMP

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

Синтаксис
Алгоритм работы:
Команда jmp в зависимости от типа своего операнда изменяет содержимое либо только одного регистра eip, либо обоих регистров cs и eip:

  • если операнд в команде jmp — метка в текущем сегменте команд (a8, 16, 32), то ассемблер формирует машинную команду, операнд которой является значением со знаком, являющимся смещением перехода относительно следующей за jmp команды. При этом виде перехода изменяется только регистр eip/ip;
  • если операнд в команде jmp — символический идентификатор ячейки памяти (m16, 32, 48), то ассемблер предполагает, что в ней находится адрес, по которому необходимо передать управление. Этот адрес может быть трех видов:
    • значением абсолютного смещения метки перехода относительно начала сегмента кода. Размер этого смещения может быть 16 или 32 бит в зависимости от режима адресации;
    • дальним указателем на метку перехода в реальном и защищенном режимах, содержащим два компонента адреса — сегментный и смещение. Размеры этих компонентов также зависят от установленного режима адресации (use16 или use32). Если текущим режимом является use16, то адрес сегмента и смещение занимают по 16 бит, причем смещение располагается в младшем слове двойного слова, отводимого под этот полный адрес метки перехода. Если текущим режимом является use32, то адрес сегмента и смещение занимают, соответственно, 16 и 32 бит, — в младшем двойном слове находится смещение, в старшем — адрес сегмента;
    • адресом в одном из 16 или 32-разрядных регистров — этот адрес представляет собой абсолютное смещение метки, на которую необходимо передать управление, относительно начала сегмента команд.
Для понимания различий механизмов перехода в реальном и защищенном режимах нужно помнить следующее. В реальном режиме микропроцессор просто изменяет cs и eip/ip в соответствии с содержимым указателя в памяти. В защищенном режиме микропроцессор предварительно анализирует байт прав доступа AR в дескрипторе, номер которого определяется по содержимому сегментной части указателя. В зависимости от состояния байта AR микропроцессор выполняет либо переход, либо переключение задач.
Состояние флагов после выполнения команды (за исключением случая переключения задач):
выполнение команды не влияет на флаги
Применение:
Команду jmp применяют для осуществления ближних и дальних безусловных переходов без сохранения контекста точки перехода.
См. также: урок 10, команды call, jcc

LAHF

(Load AH register from register Flags)
Загрузка регистра AH флагами из регистра eFlags/Flags
Схема команды: lahf
Назначение: извлечение содержимого младшего байта регистра eflags/flags, в котором содержатся пять флагов: cf, pf, af, zf и sf.

Синтаксис
Алгоритм работы:
команда загружает регистр ah содержимым младшего байта регистра eflags/flags. Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Из-за того, что регистр флагов непосредственно недоступен, команду lahf можно применять для анализа и последующего изменения командой sahf состояния некоторых флагов регистра eflags/flags.
;сбросить в ноль флаг cf
        lahf
        and     ah,11111110b
        sahf

См. также: команду sahf

LDS/LES/LFS/LGS/LSS

(Load pointer into ds/es/fs/gs/ss segment register)
Загрузка сегментного регистра ds/es/fs/gs/ss указателем из памяти
Схема команды: lds приемник,источник
les приемник,источник
lfs приемник,источник
lgs приемник,источник
lss приемник,источник
Назначение: получение полного указателя в виде сегментной составляющей и смещения.

Синтаксис
Алгоритм работы:
Алгоритм работы команды зависит от действующего режима адресации (use16 или use32):

  • если use16, то загрузить первые два байта из ячейки памяти источник в 16-разрядный регистр, указанный операндом приемник. Следующие два байта в области источник должны содержать сегментную составляющую некоторого адреса; они загружаются в регистр ds/es/fs/gs/ss;
  • если use32, то загрузить первые четыре байта из ячейки памяти источник в 32-разрядный регистр, указанный операндом приемник. Следующие два байта в области источник должны содержать сегментную составляющую, или селектор, некоторого адреса; они загружаются в регистр ds/es/fs/gs/ss.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Таким образом, с помощью данных команд в паре регистров ds/es/fs/gs/ss и приемник оказывается полный адрес некоторой ячейки памяти. Это обстоятельство можно использовать, к примеру, при работе с цепочечными командами, где существуют жесткие соглашения на размещение адресов обрабатываемых строк. Помните, что любая загрузка сегментного регистра приводит к обновлению соответствующего теневого регистра (см. урок 16). Смотрите также описание команды cmps с примером использования.
См. также: уроки 5, 7, 11, команды lea и операторы ассемблера seg и offset

LEA

(Load Effective Address)
Загрузка эффективного адреса
Схема команды: lea приемник,источник
Назначение: получение эффективного адреса (смещения) источника.

Синтаксис
Алгоритм работы:
алгоритм работы команды зависит от действующего режима адресации (use16 или use32):

  • если use16, то в регистр приемник загружается 16-битное значение смещения операнда источник;
  • если use32, то в регистр приемник загружается 32-битное значение смещения операнда источник.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Данная команда является альтернативой оператору ассемблера offset. В отличие от offset команда lea допускает индексацию операнда, что позволяет более гибко организовать адресацию операндов.
;загрузить в регистр bx адрес пятого
элемента массива mas
.data
mas     db      10 dup (0)
.code
...
        mov     di,4
        lea     bx,mas[di]
;или
        lea     bx,mas[4]
;или
        lea     bx,mas+4

См. также: уроки 5, 7, 11 и команды lea, lds, les, lss, lgs, lfs, операторы ассемблера seg и offset

LEAVE

(LEAVE from procedure)
Выход из процедуры
Схема команды: leave
Назначение: удаление из стека области локальных (динамических) переменных, выделенной командой enter.

Синтаксис
Алгоритм работы:
команда выполняет обратные команде enter действия:

  • содержимое ebp/bp копируется в esp/sp, тем самым восстанавливается значение esp/sp, которое было до вызова данной процедуры. С другой стороны, восстановление старого значения esp/sp означает освобождение пространства в стеке, отведенного для завершающейся процедуры (локальные переменные процедуры уничтожаются);
  • из стека восстанавливается содержимое ebp/bp, которое было до входа в процедуру. После этого действия значение esp/sp также становится таким, каким оно было до входа в процедуру.
В результате этих двух действий также восстанавливается кадр стека, если он был, вызывающей программы.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда leave не имеет операндов и выполняет обратные команде enter действия. Эта команда должна находиться непосредственно перед командой ret, которая в зависимости от соглашений конкретного языка по вызову процедур удаляет или не удаляет аргументы из стека (см. урок 14).
.286
proc1   proc
        enter   16,0
...
        leave
        ret
proc1   endp

См. также: урок 14 и команды enter, ret/retf

LGDT

(Load Global Descriptor Table)
Загрузка регистра глобальной дескрипторной таблицы
Схема команды: lgdt источник
Назначение: загрузка регистра gdtr значениями базового адреса и размера глобальной дескрипторной таблицы GDT.

Синтаксис
Алгоритм работы:
команда выполняет загрузку 16 бит размера и 32 бит значения базового адреса начала таблицы GDT в памяти в системный регистр gdtr. Эта загрузка производится в соответствии с форматом этого регистра (см. урок 16). Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду lgdt применяют при подготовке к переходу в защищенный режим для загрузки системного регистра gdtr. В качестве операнда в команде указывается адрес области в формате 16+32. Младшее слово области — размер GDT, двойное слово по старшему адресу — значение базового адреса начала этой таблицы. Данные два компонента должны быть сформированы в памяти заранее.
.286
;структура для описания псевдодескриптора gdtr
point   STRUC
lim     dw      0
adr     dd      0
 ENDS
.data
point_gdt       point
.code
...
;загружаем gdtr
        xor     eax,eax
        mov     ax,gdt_seg
        shl     eax,4
        mov     point_gdt.adr,eax
        lgdt    point_gdt
...

См. также: уроки 16, 17 и команду sgdt

LIDT

(Load Interrupt Descriptor Table)
Загрузка регистра глобальной дескрипторной таблицы
Схема команды: lidt источник
Назначение: загрузка регистра idtr значениями базового адреса и размера глобальной дескрипторной таблицы IDT.

Синтаксис
Алгоритм работы:
Команда lidt аналогична lgdt, но для дескрипторной таблицы прерываний IDT (см. урок 17).
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду lidt применяют при подготовке к переходу в защищенный режим для загрузки системного регистра idtr. В качестве операнда в команде указывается адрес области в формате 16+32. Младшее слово области — размер IDT, двойное слово по старшему адресу — значение базового адреса начала этой таблицы. Два данных компонента должны быть сформированы в памяти заранее.
.386
;структура для описания псевдодескрипторов gdtr и idtr
point   STRUC
lim     dw      0
adr     dd      0
 ENDS
.data
point_idt       point
.code
...
;загружаем idtr
        xor     eax,eax
        mov     ax,IDT_SEG
        shl     eax,4
        mov     point_idt.adr,eax
        lidt    point_idt
...

См. также: урок 17 и команду sidt

LODS/LODSB/LODSW/LODSD

(LOad String Byte/Word/Double word operands)
Загрузка строки байтов/слов/двойных слов
Схема команды: lods источник
lodsb
lodsw
lodsd
Назначение: загрузка элемента из последовательности (цепочки) в регистр-аккумулятор al/ax/eax.

Синтаксис
Алгоритм работы:

  • загрузить элемент из ячейки памяти, адресуемой парой ds:esi/si, в регистр al/ax/eax. Размер элемента определяется неявно (для команды lods) или явно в соответствии с применяемой командой (для команд lodsb, lodsw, lodsd);
  • изменить значение регистра si на величину, равную длине элемента цепочки. Знак этой величины зависит от состояния флага df:
    • df=0 — значение положительное, то есть просмотр от начала цепочки к ее концу;
    • df=1 — значение отрицательное, то есть просмотр от конца цепочки к ее началу.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команды извлекают элемент из ячейки памяти в один из регистров. Перед командой lods можно указать префикс повторения rep, но в этом нет особого смысла, так как обычно эту команду используют в некотором цикле для просмотра некоторой цепочки с элементами фиксированного размера.
str     db      ...
...
        cld
        lea     si,str
        lodsb   ;загрузить первый байт из str в al

См. также: урок 11 и команды ins/insb/insw/insd, cmps/cmpsb/cmpsw/cmpsd, movs/movsb/movsw/movsd, outs, scas/scasb/scasw/scasd, stos/stosb/stosw/stosd, rep/repe/repz/repne/repnz

LOOP

(LOOP control by register cx)
Управление циклом по cx
Схема команды: loop метка
Назначение: организация цикла со счетчиком в регистре cx.

Синтаксис
Алгоритм работы:

  • выполнить декремент содержимого регистра ecx/cx;
  • анализ регистра ecx/cx:
    • если ecx/cx=0, передать управление следующей за loop команде;
    • если ecx/cx=1, передать управление команде, метка которой указана в качестве операнда loop.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команду loop применяют для организации цикла со счетчиком. Количество повторений цикла задается значением в регистре ecx/cx перед входом в последовательность команд, составляющих тело цикла. Помните о двух важных моментах:
  • для предотвращения выполнения цикла при нулевом ecx/cx используйте команду jecxz/jcxz. Если этого не сделать, то при изначально нулевом ecx/cx цикл повторится 4 294 967 295/65 536 раз;
  • смещение метки, являющейся операндом loop, не должно выходить из диапазона -128...+127 байт. Это смещение, как и в командах условного перехода, является относительным от значения счетчика адреса следующей за loop команды.
        mov     cx,10
...
        jcxz    m1
cycl:
;тело цикла
        loop    cycl
m1:

См. также: урок 10 и команды jecxz/jcxz, loope/loopz, loopne/loopnz

LOOPE/LOOPZ
LOOPNE/LOOPNZ

(LOOP control by register cx not equal 0 and ZF=1)
(LOOP control by register cx not equal 0 and ZF=0)
Управление циклом по cx c учетом значения флага ZF
Схема команды: loope/loopz метка
loopne/loopnz метка
Назначение: организация цикла со счетчиком в регистре cx с учетом флага zf.

Синтаксис
Алгоритм работы:

  • выполнить декремент содержимого регистра ecx/cx;
  • проанализировать регистр ecx/cx:
    • если ecx/cx=0, передать управление следующей за loopxx команде;
    • если ecx/cx=1, передать управление команде, метка которой указана в качестве операнда loopxx;
  • анализ флага zf:
    • если zf=0, для команд loope/loopz это означает выход из цикла, для команд loopne/loopnz — переход к началу цикла;
    • если zf=1, для команд loope/loopz это означает переход к началу цикла, для команд loopne/loopnz — выход из цикла.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команды loopxx удобно использовать вместе с командами, которыe в результате своей работы меняют значение флага zf. Типичный пример — команда сравнения cmp.
;найти первый пробел в строке символов
str     db      'Найти первый пробел'
str_size=$-str
...
        cld
        mov     cx,str_size
        lea     si,str
cycl:
        lodsb
        cmp     al,' '
        loopne  cycl
        jcxz    m1      ;переход, если пробелов нет
        dec     si      ;в si — адрес пробела в строке str
...
m1

См. также: уроки 8, 10, 11 и команду loop

MOV

(MOVe operand)
Пересылка операнда
Схема команды: mov приемник,источник
Назначение: пересылка данных между регистрами или регистрами и памятью.

Синтаксис
Алгоритм работы:
копирование второго операнда в первый операнд.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команда mov применяется для различного рода пересылок данных, при этом, несмотря на всю простоту этого действия, необходимо помнить о некоторых ограничениях и особенностях выполнения данной операции:
  • направление пересылки в команде mov всегда справа налево, то есть из второго операнда в первый;
  • значение второго операнда не изменяется;
  • оба операнда не могут быть из памяти (при необходимости можно использовать цепочечную команду movs);
  • лишь один из операндов может быть сегментным регистром;
  • желательно использовать в качестве одного из операндов регистр al/ax/eax, так как в этом случае TASM генерирует более быструю форму команды mov.
        mov     al,5
        mov     bl,al
        mov     bx,ds

См. также: урок 10 и команды movs, lods/lodsb/lodsw/lodsd, stos/stosb,
stosw/stosd

MOV

(MOVe operand to/from system registers)
Пересылка операнда в системные регистры (или из них)
Схема команды: mov приемник,источник
Назначение: пересылка данных между регистрами или регистрами и памятью.

Синтаксис
Алгоритм работы:
копирование второго операнда в первый.
Состояние флагов после выполнения команды:

11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда mov применяется для обмена данными между системными регистрами. Это одна из немногих возможностей доступа к содержимому этих регистров. Данную команду можно использовать только на нулевом уровне привилегий либо в реальном режиме работы микропроцессора.
.286
;переключение микропроцессора в защищенный
режим36:
        mov     eax,cr0
        bts     eax,0
        mov     cr0,eax

См. также: уроки 16, 17 и команды mov, bts

MOVS/MOVSB/MOVSW/MOVSD

(MOVe String Byte/Word/Double word)
Пересылка строк байтов/слов/двойных слов
Схема команды: movs приемник,источник
movsb
movsw
movsd
Назначение: пересылка элементов двух последовательностей (цепочек) в памяти.

Синтаксис
Алгоритм работы:

  • выполнить копирование байта, слова или двойного слова из операнда источника в операнд приемник, при этом адреса элементов предварительно должны быть загружены:
    • адрес источника — в пару регистров ds:esi/si (ds по умолчанию, допускается замена сегмента);
    • адрес приемника — в пару регистров es:edi/di (замена сегмента не допускается);
  • в зависимости от состояния флага df изменить значение регистров esi/si и edi/di:
    • если df=0, то увеличить содержимое этих регистров на длину структурного элемента последовательности;
    • если df=1, то уменьшить содержимое этих регистров на длину структурного элемента последовательности;
  • если есть префикс повторения, то выполнить определяемые им действия (см. команду rep).
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команды пересылают элемент из одной ячейки памяти в другую. Размеры пересылаемых элементов зависят от применяемой команды. Команда movs может работать с элементами размером в байт, слово, двойное слово. В качестве операндов в команде указываются идентификаторы последовательностей этих элементов в памяти. Реально эти идентификаторы используются лишь для получения типов элементов последовательностей, а их адреса должны быть предварительно загружены в указанные выше пары регистров. Транслятор, обработав команду movs и выяснив тип операндов, генерирует одну из машинных команд movsb, movsw или movsd. Машинного аналога для команды movs нет. Для адресации операнда приемник обязательно должен использоваться регистр es.
Для того чтобы эти команды можно было использовать для пересылки последовательности элементов, имеющих размерность байт, слово, двойное слово, необходимо использовать префикс rep. Префикс rep заставляет циклически выполняться команды пересылки до тех пор, пока содержимое регистра ecx/cx не станет равным нулю.
str1    db      'str1 копируется в str2'
len_str1=$-str1
a_str1  dd      str1
str2    db      len_str1 dup (' ')
a_str2  dd      str2
...
        mov     cx,len_str1
        lds     si,str1
        les     di,str2
        cld
rep     movsb

См. также: урок 11 и команды cmps/cmpsb/cmpsw/cmpsd, ins/insb/insw/insd, lods/lodsb/lodsw/lodsd, outs, scas/scasb/scasw/scasd, stos/stosb/stosw/stosd, rep/repe/repz/repne/repnz

MOVSX

(MOVe and Sign eXtension)
Пересылка со знаковым расширением
Схема команды: movsx приемник,источник
Назначение: преобразование элементов со знаком меньшей размерности в эквивалентные им элементы со знаком большей размерности.

Синтаксис
Алгоритм работы:

  • считать содержимое источника;
  • записать содержимое операнда источника в операнд приемник, начиная с младших разрядов источника;
  • распространить значение знакового разряда источника на свободные старшие разряды операнда назначения.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команду movsx обычно используют для получения эквивалентного, но большего по размеру операнда со знаком. Это может понадобиться для приведения размера операнда к нужному значению с целью обеспечения работы следующих команд программы:
        mov     al,0ffh
        movsx   bx,al   ;bx=0ffffh

См. также: урок 8 и команды mov, movs, movzx, cbw, cwd, cdq

MOVZX

(MOVe and Zero eXtension)
Пересылка с нулевым расширением
Схема команды: movzx приемник,источник
Назначение: преобразование элементов без знака меньшей размерности в эквивалентные им элементы без знака большей размерности.

Синтаксис
Алгоритм работы:

  • считать содержимое источника;
  • записать содержимое операнда источника в операнд приемник, начиная с его младших разрядов;
  • распространить двоичный нуль на свободные старшие разряды операнда назначения.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команду movzx обычно используют для получения эквивалентного, но большего по размеру операнда без учета знака. Она может быть использована для согласования операндов различной размерности. Но не следует думать, что все эти разнотипные пересылки делает одна машинная команда. На самом деле существует несколько машинных команд, каждая из которых работает со своими размерами операндов. Генерацию же нужной команды обеспечивает транслятор на основе анализа исходного текста программы.
.data
sl      db      ?
.code
...
        mov     al,0ffh
        movzx   bx,al   ;bx=00ffh
...
;или из памяти:
        movzx   eax,byte ptr sl

См. также: урок 8 и команды mov, movs/movsb/
movsw/movsd, movsx, cbw, cwd, cdq

MUL

(MULtiply)
Умножение целочисленное без учета знака
Схема команды: mul множитель_1
Назначение: операция умножения двух целых чисел без учета знака.

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

  • если операнд, указанный в команде — байт, то второй сомножитель должен располагаться в al;
  • если операнд, указанный в команде — слово, то второй сомножитель должен располагаться в ax;
  • если операнд, указанный в команде — двойное слово, то второй сомножитель должен располагаться в eax.
Результат умножения помещается также в фиксированное место, определяемое размером сомножителей:
  • при умножении байтов результат помещается в ax;
  • при умножении слов результат помещается в пару dx:ax;
  • при умножении двойных слов результат помещается в пару edx:eax.
Состояние флагов после выполнения команды (если старшая половина результата нулевая):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
0 | ? | ? | ? | ? | 0
Состояние флагов после выполнения команды (если старшая половина результата ненулевая):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
1 | ? | ? | ? | ? | 1
Применение:
Команда mul выполняет целочисленное умножение операндов без учета их знаковых разрядов. Для этой операции необходимо наличие двух операндов-сомножителей, размещение одного из которых фиксировано, а другого задается операндом в команде. Контролировать размер результата удобно используя флаги cf и of.
mn_1    db      15
mn_2    db      25
...
        mov     al,mn_1
        mul     mn_2

См. также: урок 8 и команду imul

NEG

(NEGate operand)
Изменить знак операнда
Схема команды: neg источник
Назначение: изменение знака (получение двоичного дополнения) источника.

Синтаксис
Алгоритм работы:

  • выполнить вычитание (0 – источник) и поместить результат на место источника;
  • если источник=0, то его значение не меняется.
Состояние флагов после выполнения команды (если результат нулевой):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | 0
Состояние флагов после выполнения команды (если результат ненулевой):
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | 1
Применение:
Команда используется для формирования двоичного дополнения операнда в памяти или регистре. Операция двоичного дополнения предполагает инвертирование всех разрядов операнда с последующим сложением операнда с двоичной единицей. Если операнд отрицательный, то операция neg над ним означает получение его модуля.
        mov     al,2
        neg     al      ;al=0feh — число -2 в дополнительном коде

См. также: уроки 6, 8 и команду not

NOP

(No OPeration)
Нет операции
Схема команды: nop
Назначение: пустая команда.

Синтаксис
Алгоритм работы:
не производит никаких действий.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команда nop, занимая один байт, может использоваться для резервирования места в сегменте кода или организации программной задержки. В качестве иллюстрации можно обратиться к примеру, приведенному в описании команды hlt. В этом примере команду nop можно использовать вместо jmp $+2. Назначение jmp $+2 в этом фрагменте — задержка для синхронизации работы микропроцессора и аппаратуры компьютера.

NOT

(NOT operand)
Инвертирование операнда
Схема команды: not источник
Назначение: инвертирование всех битов операнда источник.

Синтаксис
Алгоритм работы:
инвертировать все биты операнда источника: из 1 в 0, из 0 в 1.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду not можно использовать для изменения байта, выполняющего роль некоторого флага, с целью отслеживания некоторых логических условий в программе. Но такой способ не оптимален, эту ситуацию мы обсуждали в книге на уроках 9 и 12.
flag    db      0ffh ;значение флага — истина
...
cycl:
...
        cmp     flag,0
        je      m1
...
m1:     not     flag    ;установить флаг в истину

См. также: уроки 9, 12 и команду neg

OR

(logical OR)
Логическое включающее ИЛИ
Схема команды: or приемник,маска
Назначение: операция логического ИЛИ над битами операнда назначения.

Синтаксис
Алгоритм работы:

  • выполнить операцию логического ИЛИ над битами операнда назначения, используя в качестве маски второй операнд — маска. При этом бит результата равен 0, если соответствующие биты операндов маска и назначения равны 0, в противном случае бит равен 1;
  • записать результат операции в источник (операнд маска остается неизменным);
  • установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
0 | r | r | ? | r | 0
Применение:
Команду or можно использовать для работы с операндами на уровне битов. Типичное использование команды — установка определенных разрядов первого операнда в единицу.
        mov     al,01h
        or      bl,al   ;установить нулевой бит в 1

См. также: урок 9 и команды and, xor, not

OUT

(OUT operand to port)
Вывод операнда в порт
Схема команды: out ном_порта,аккумулятор
Назначение: вывод значения в порт ввода-вывода.

Синтаксис
Алгоритм работы:
Передать байт, слово, двойное слово из регистра al/ax/eax в порт, номер которого определяется первым операндом.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команда применяется для прямого управления оборудованием компьютера посредством портов. Номер порта задается первым операндом в виде непосредственного значения или значения в регистре dx. Непосредственным значением можно задать порт с номером в диапазоне 0...255. Для указания порта с большим номером используется регистр dx. Размер данных определяется размерностью второго операнда и может быть байтом, словом или двойным словом.
        out     64h,al

См. также: уроки 2, 7, 16, 17 и команды in, ins/insb/insw/insd, outs

OUTS/OUTSB/OUTSW/OUTSD

(OUTput Byte/Word/Double word String to port)
Вывод строки байтов/слов/двойных слов в порт
Схема команды: outs порт,источник
outsb
outsw
outsd
Назначение: вывод в порт из памяти последовательности байт, слов, двойных слов.

Синтаксис
Алгоритм работы:

  • передать данные в порт ввода-вывода, номер которого загружен в регистр dx, из ячейки памяти по адресу ds:esi/si;
  • в зависимости от состояния флага df изменить значение регистров esi/si:
    • если df=0, то увеличить содержимое этих регистров на длину структурного элемента последовательности;
    • если df=1, то уменьшить содержимое этих регистров на длину структурного элемента последовательности;
  • при наличии префикса выполнить определяемые им deiqrbh (см. команду rep/repe/repz/repne/repnz).
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда выводит данные в порт ввода-вывода, номер которого загружен в регистр dx, из ячейки памяти по адресу ds:esi/si (допускается замена сегмента). Недопустимо задание номера порта в команде в виде непосредственного операнда — для этого используется регистр dx. Размеры вводимых элементов зависят от применяемой команды. Команда outs может работать с элементами размером в байт, слово или двойное слово. В качестве операнда в команде указывается символическое имя ячейки памяти, из которой элемент выводится в порт ввода-вывода. Реально символическое имя используется лишь для получения типа элемента последовательности, а ее адрес должен быть предварительно загружен в пару регистров ds:esi/si. Транслятор, обработав команду outs и выяснив тип операндов, генерирует одну из машинных команд outsb, outsw или outsd. Машинного аналога для команды outs нет.
Для того чтобы эти команды можно было использовать для вывода в порт последовательности элементов, имеющих размерность байт, слово или двойное слово, необходимо использовать префикс rep. Он заставляет циклически выполняться команду вывода в порт до тех пор, пока содержимое регистра ecx/cx не станет равным нулю.
.286
;вывести последовательность 10 байт в порт 300h
;(номер порта взят условно)
str_10  db      10 dup(0)
adr_str dd      str_10
        lds     si,adr_str
        mov     dx,300h
rep     outsb

См. также: уроки 2, 7, 11 и команды cmps/cmpsb/cmpsw/cmpsd, lods/lodsb/lodsw/lodsd, movs/movsb/movsw/movsd, ins/insb/insw/insd, scas/scasb/scasw/scasd, stos/stosb/stosw/stosd, rep/repe/repz/repne/repnz

POP

(POP operand from the stack)
Извлечение операнда из стека
Схема команды: pop приемник
Назначение: извлечение слова или двойного слова из стека.

Синтаксис
Алгоритм работы:
Алгоритм работы команды зависит от установленного атрибута размера адреса — use16 или use32:

  • загрузить в приемник содержимое вершины стека (адресуется парой ss:esp/sp);
  • увеличить содержимое esp/sp на 4 (2 байта) для use32 (соответственно для use16).
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда применяется для восстановления содержимого вершины стека в регистр, ячейку памяти или сегментный регистр. Заметим, что недопустимо восстановление значения в сегментный регистр cs.
my_proc proc    near
        push    ax
        push    bx
;тело процедуры, в которой изменяется содержимое
;регистров ax и bx
...
        pop     bx
        pop     ax
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды popa, popad, popf, popfd, push, pusha, pushad, pushf, pushfd

POPA

(POP All general registers from the stack)
Извлечение всех регистров общего назначения из стека
Схема команды: popa
Назначение: извлечение из стека регистров общего назначения di, si, bp, sp, bx, dx, cx, ax.

Синтаксис
Алгоритм работы:

  • извлечь из стека последовательно значения и загрузить ими регистры общего назначения di, si, bp, sp, bx, dx, cx, ax. Содержимое di восстанавливается первым. Содержимое sp извлекается, но не восстанавливается;
  • увеличить значение указателя стека esp/sp на 16.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда popa по принципу работы является обратной команде pusha и используется для восстановления содержимого всех регистров общего назначения значениями из стека. Эту команду можно использовать в процедурах и программах обработки прерываний для восстановления регистров общего назначения прерванной программы.
.386
my_proc proc    near
        pusha
;тело процедуры, в которой изменяется
;содержимое регистров общего назначения
...
        popa
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popad, popf, popfd, push, pusha, pushad, pushf, pushfd

POPAD

(POP All general Double word registers from the stack)
Извлечение всех 32-разрядных регистров общего назначения из стека
Схема команды: popad
Назначение: извлечение из стека регистров общего назначения edi, esi, ebp, esp, ebx, edx, ecx, eax.

Синтаксис
Алгоритм работы:

  • извлечь из стека последовательно значения и загрузить ими 32-разрядные регистры общего назначения edi, esi, ebp, esp, ebx, edx, ecx, eax. Содержимое edi восстанавливается первым. Содержимое esp извлекается но не восстанавливается;
  • увеличить значение указателя стека esp на 32.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда popad по принципу работы является обратной команде pushad и используется для восстановления всех 32-разрядных регистров общего назначения. Эту команду можно использовать в процедурах и программах обработки прерываний для восстановления регистров общего назначения прерванной программы.
.386
my_proc proc    near
        pushad
;тело процедуры, в которой изменяется
;содержимое регистров общего назначения
...
        popad
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popf, popfd, push, pusha, pushad, pushf, pushfd

POPF

(POP Flags register from the stack)
Извлечение регистра флагов из стека
Схема команды: popf
Назначение: извлечение из стека слова и восстановление его в регистр флагов flags.

Синтаксис
Алгоритм работы:

  • извлечь из вершины стека слово и поместить его в регистр flags;
  • увеличить значение указателя стека esp на 2.
Состояние флагов после выполнения команды:
14 | 1312 | 11 | 10 | 09 | 08 | 07 | 06 | 04 | 02 | 00
NT | IOPL | OF | DF | IF | TF | SF | ZF | AF | PF | CF
r | r | r | r | r | r | r | r | r | r | r
Применение:
Команда popf по принципу работы является обратной команде pushf и используется для восстановления из стека содержимого регистра флагов eflags. Возможным вариантом использования этой команды являются программы обработки прерываний или другие случаи, в которых необходимо сохранять некоторый локальный контекст процесса вычисления. Из-за того, что регистр eflags/flags непосредственно недоступен, команда popf является одной из немногих возможностей влияния на его содержимое.
;установить значение регистра flags в 03h
        mov     ax,3h
        push    ax
        popf

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popad, popfd, push, pusha, pushad, pushf, pushfd

POPFD

(POP eFlags Double word register from the stack)
Извлечение расширенного регистра флагов из стека
Схема команды: popfd
Назначение: извлечение из стека двойного слова и восстановление его в регистр флагов eflags.

Синтаксис
Алгоритм работы:

  • извлечь из вершины стека двойное слово и поместить его в регистр eflags;
  • увеличить значение указателя стека esp на 4.
Состояние флагов после выполнения команды:
17 | 16 | 14 | 1312 | 11 | 10 | 09 | 08 | 07 | 06 | 04 | 02 | 00
VM | RF | NT | IOPL | OF | DF | IF | TF | SF | ZF | AF | PF | CF
0 | r | r | r | r | r | r | r | r | r | r | r | r
Применение:
Команда popfd по принципу работы является обратной командой команде pushfd и используется для восстановления из стека содержимого регистра флагов eflags. Необходимо отметить, что команда popfd не влияет на состояние флагов vm и rf.
.386
;установить значение регистра eflags в 03h
        mov     eax,3h
        push    eax
        popfd   eax     ;установить новое значение eflags

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popad, popf, push, pusha, pushad, pushf, pushfd

PUSH

(PUSH operand onto stack)
Размещение операнда в стеке
Схема команды: push источник
Назначение: размещение содержимого операнда источник в стеке.

Синтаксис
Алгоритм работы:

  • уменьшить значение указателя стека esp/sp на 4/2 (в зависимости от значения атрибута размера адреса — use16 или use32);
  • записать источник в вершину стека (адресуемую парой ss:esp/sp).
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда push используется совместно с командой pop для записи значений в стек и извлечения их из стека. Размер записываемых значений — слово или двойное слово. Также в стек можно записывать непосредственные значения. Заметьте, что в отличие от команды pop в стек можно включать значение сегментного регистра cs. Другой интересный момент связан с регистром sp. Команда push esp/sp записывает в стек значение esp/sp по состоянию до выдачи этой команды. В микропроцессоре i8086 по этой команде записывалось скорректированное значение sp. При записи в стек 8-битных значений для них все равно выделяется слово или двойное слово (в зависимости от use16 или use32).
my_proc proc    near
        push    ax
        push    bx
;тело процедуры, в которой изменяется содержимое
;регистров ax и bx
...
        pop     bx
        pop     ax
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popad, popf, popfd, pusha, pushad, pushf, pushfd

PUSHA

(PUSH All general registers onto stack)
Размещение всех регистров общего назначения в стеке
Схема команды: pusha
Назначение: размещение в стеке регистров общего назначения в следующей последовательности: ax, cx, dx, bx, sp, bp, si, di.

Синтаксис
Алгоритм работы:

  • уменьшить значение указателя стека esp/sp на 32/16 (в зависимости от значения атрибута размера адреса — use16 или use32);
  • включить в стек последовательно значения регистров общего назначения ax, cx, dx, bx, sp, bp, si, di.
Содержимое di при этом будет на вершине стека. В стек помещается содержимое sp по состоянию до выполнения команды.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда pusha используется совместно с командой popa для сохранения и восстановления всех регистров общего назначения. Эти команды удобно использовать при работе с процедурами, программами обработки прерываний, а также в других случаях для сохранения и восстановления регистров общего назначения как части контекста некоторого вычислительного процесса.
my_proc proc    near
        pusha
;тело процедуры, в которой изменяется
;содержимое регистров общего назначения
...
        popa
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popad, popf, popfd, push, popa, pushad, pushf, pushfd

PUSHAD

(PUSH All general Double word registers onto stack)
Размещение всех регистров общего назначения в стеке
Схема команды: pushad
Назначение: размещение в стеке регистров общего назначения в следующей последовательности: eax, ecx, edx, ebx, esp, ebp, esi, edi.

Синтаксис
Алгоритм работы:

  • уменьшить значение указателя стека esp на 32;
  • включить в стек последовательно значения регистров общего назначения eax, ecx, edx, ebx, esp, ebp, esi, edi. Содержимое edi при этом будет на вершине стека. Содержимое esp включается по состоянию на момент, предшествовавший выполнению данной команды.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда pushad используется совместно с командой popad для сохранения и восстановления всех регистров общего назначения. Эти команды используются аналогично командам popa и pusha.
.386
my_proc proc    near
        pushad
;тело процедуры, в которой изменяется
;содержимое регистров общего назначения
...
        popad
        ret
        endp

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popf, popfd, push, pusha, popad, pushf, pushfd

PUSHF

(PUSH Flags register onto stack)
Размещение регистра флагов в стеке
Схема команды: pushf
Назначение: размещение в вершине стека (ss:sp) содержимого регистра флагов flags.

Синтаксис
Алгоритм работы:

  • уменьшить значение указателя стека sp на 2;
  • поместить в вершину стека содержимое регистра flags.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команда pushf может использоваться для получения содержимого регистра флагов. Как известно, прямой доступ к регистру флагов невозможен, поэтому данная команда является одной из немногих команд, позволяющих получить доступ к регистру флагов как к содержимому обычного регистра. Обратное действие, то есть восстановление — возможно измененного слова — в регистр флагов, осуществляется командой popf. Эта команда может использоваться в программах обработки прерываний и в других случаях, когда необходимо сохранить локальный контекст процесса вычисления.
;извлечь значение регистра flags и изменить
;значение флага cf на обратное
        pushf
        pop     ax
        xor     ax,01h
        push    ax
        popf

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popad, popfd, push, pusha, pushad, popf, pushfd

PUSHFD

(PUSH eFlags Double word register onto stack)
Размещение расширенного регистра флагов в стеке
Схема команды: pushfd
Назначение: размещение в стеке содержимого регистра флагов eflags.

Синтаксис
Алгоритм работы:

  • уменьшить значение указателя стека esp на 4;
  • записать в вершину стека двойное слово, представляющее собой содержимое регистра eflags.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команды pushfd и popfd используются аналогично командам pushf и popf. Команда pushfd применяется для получения содержимого регистра флагов. Как известно, прямой доступ к регистру флагов невозможен, поэтому данная команда является одной из немногих команд, позволяющих получить доступ к регистру флагов как к содержимому обычного регистра. Обратное действие, то есть восстановление — возможно измененного слова — в регистр флагов, осуществляется командой popfd. Эта команда может использоваться в программах обработки прерываний или в других случаях, когда необходимо сохранить локальный контекст процесса вычисления.
.386
;извлечь значение регистра eflags и изменить
;значение флага cf на обратное
        pushfd
        pop     eax
        xor     eax,01h
        push    eax
        popfd

См. также: уроки 7, 10, 14, 15, 16, 17 и команды pop, popa, popad, popf, popfd, push, pusha, pushad, pushf

RCL

(Rotate operand through Carry flag Left)
Циклический сдвиг операнда влево через флаг переноса
Схема команды: rcl операнд,количество_сдвигов
Назначение: операция циклического сдвига операнда влево через флаг переноса cf.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда влево на один разряд, при этом старший бит операнда становится значением флага переноса cf;
  • одновременно старое значение флага переноса cf вдвигается в операнд справа и становится значением младшего бита операнда;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда команды rcl.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Здесь обозначение ?r означает то, что анализ состояния флага имеет смысл при определенном сочетании операндов. В случае команды rcl флаг of представляет интерес, если сдвиг осуществляется на один разряд (см. ниже описание применения команды rcl).
Применение:
Команда rcl используется для циклического сдвига разрядов операнда влево. Особенность этого сдвига в том, что он происходит с некоторой задержкой, так как очередной сдвигаемый бит оказывается на некоторое время вне операнда. В это время можно произвести его извлечение и (или) подмену. Другой важный момент заключается в том, что для счетчика сдвига микропроцессор использует только пять младших разрядов операнда количество_разрядов. Таким образом, значение, большее 31, микропроцессором не допускается (аппаратно это ограничение реализуется тем, что игнорируются значения всех битов счетчика, кроме первых пяти). Обратите внимание на еще один интересный эффект, связанный с поведением флага of. В операциях сдвига на один разряд по изменению этого флага можно судить о факте изменения знакового (старшего) разряда операнда:
  • of=1, если текущее значение флага cf и выдвигаемого бита операнда слева различны;
  • of=0, если текущее значение флага cf и выдвигаемого бита операнда слева совпадают.
;сдвиг операнда, занимающего два двойных слова
;на четыре разряда влево
ch_l    dd      ...     ;младшая часть 64-битного операнда
ch-2    dd      ...     ;старшая часть 64-битного операнда
...
        mov     cx,4    ;счетчик сдвигов в cx
        mov     eax,ch_l
        mov     edx,ch_h
m1:     clc             ;очистка флага cf
        rcl     eax,1   ;старший бит eax в cf
        rcl     edx,1   ;cf в младший бит edx, старший бит edx в cf
        loop    m1

См. также: урок 9 и команды rcr, rol, ror, sal, sar, shl, shr

RCR

(Rotate operand through Carry flag Right)
Циклический сдвиг операнда вправо через флаг переноса
Схема команды: rcr операнд,количество_сдвигов
Назначение: операция циклического сдвига операнда вправо через флаг переноса cf.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда вправо на один разряд; при этом младший бит операнда становится значением флага переноса cf;
  • одновременно старое значение флага переноса — в операнд слева и становится значением старшего бита операнда;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда команды rcr.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Здесь обозначение ?r означает то, что анализ состояния флага имеет смысл при определенном сочетании операндов. В случае команды rcr флаг of представляет интерес, если сдвиг осуществляется на один разряд (см. ниже описание применения команды rcr).
Применение:
Команда rcr используется для циклического сдвига разрядов операнда вправо. Особенность этого сдвига в том, что он происходит с некоторой задержкой, так как очередной сдвигаемый бит оказывается на некоторое время вне операнда. В это время можно произвести его извлечение и (или) подмену. Другой важный момент заключается в том, что для счетчика сдвига микропроцессор использует только пять младших разрядов операнда количество_разрядов. Таким образом, значение, большее 31, не допускается (аппаратно это ограничение реализуется тем, что игнорируются значения битов счетчика старше пятого). Обратите внимание на еще один интересный эффект, связанный с поведением флага of, — его значение имеет смысл только в операциях сдвига на один разряд и обусловлено тем, что по изменению этого флага можно судить о факте изменения знакового разряда операнда:
  • of=1, если текущие (то есть до операции сдвига) значения флага cf и старшего, левого бита операнда различны;
  • of=0, если текущие (то есть до операции сдвига) значения флага cf и старшего, левого бита операнда слева совпадают.
;подсчет числа единичных битов в операнде
operand dw      ...
...
        mov     cx,16   ;размер операнда
        xor     al,al   ;счетчик единичных битов
cycl:   rcr     operand,1
        jc      $+4     ;переход, если очередной выдвинутый бит равен 1
        jmp     $+4     ;переход, если очередной выдвинутый бит равен 0
        inc     al      ;увеличить счетчик единичных битов
        loop    cycl

См. также: урок 9 и команды rcl, rol, ror, sal, sar, shl, shr

REP/REPE/REPZ/REPNE/REPNZ

(REPeat string operation)
Повторить цепочечную операцию
Схема команды: rep
repe
repz
repne
repnz
Назначение: указание условного и безусловного повторения следующей за данной командой цепочечной операции.

Синтаксис
Алгоритм работы:
Алгоритм работы зависит от конкретного префикса. Префиксы rep, repe и repz на самом деле имеют одинаковый код операции, их действия зависят от той цепочечной команды, которую они предваряют:

  • rep используется перед следующими цепочечными командами и их краткими эквивалентами: movs, stos, ins, outs. Действия rep:
    1. анализ содержимого cx:
      • если cx<>0, то выполнить цепочечную команду, следующую за данным префиксом и перейти к шагу 2;
      • если cx=0, то передать управление команде, следующей за данной цепочечной командой (выйти из цикла по rep);
    2. уменьшить значение cx=cx–1 и вернуться к шагу 1;
  • repe и repz используются перед следующими цепочечными командами и их краткими эквивалентами: cmps, scas. Действия repe и repz:
    1. анализ содержимого cx и флага zf:
      • если cx<>0 или zf<>0, то выполнить цепочечную команду, следующую за данным префиксом, и перейти к шагу 2;
      • если cx=0 или zf=0, то передать управление команде, следующей за данной цепочечной командой (выйти из цикла по rep);
    2. уменьшить значение cx=cx-1 и вернуться к шагу 1;
  • repne и repnz также имеют один код операции и имеют смысл при использовании перед следующими цепочечными командами и их краткими эквивалентами: cmps, scas. Действия repne и repnz:
    1. анализ содержимого cx и флага zf:
      • если cx<>0 или zf=0, то выполнить цепочечную команду, следующую за данным префиксом и перейти к шагу 2;
      • если cx=0 или zf<>0, то передать управление команде, следующей за данной цепочечной командой (выйти из цикла по rep);
    2. уменьшить значение cx=cx–1 и вернуться к шагу 1.
Состояние флагов после выполнения команды:
06
ZF
r
Применение:
Команды rep, repe, repz, repne и repnz в силу специфики своей работы называются префиксами. Они имеют смысл только при использовании цепочечных операций, заставляя их циклически выполняться и тем самым без организации внешнего цикла обрабатывать последовательности элементов фиксированной длины. Большинство применяемых префиксов являются условными, то есть они прекращают работу цепочечной команды при выполнении определенных условий.
См. также: урок 11 и команды cmps/cmpsb/cmpsw/cmpsd, ins/insb/insw/insd, outs, movs/movsb/movsw/movsd, scas/scasb/scasw/scasd, stos/stosb
/stosw/stosd

RET/RETF

(RETurn/RETurn Far from procedure)
Возврат ближний (дальний) из процедуры
Схема команды: ret
ret число
Назначение: возврат управления из процедуры вызывающей программе.

Синтаксис
Алгоритм работы:
Работа команды зависит от типа процедуры:

  • для процедур ближнего типа — восстановить из стека содержимое eip/ip;
  • для процедур дальнего типа — последовательно восстановить из стека содержимое eip/ip и сегментного регистра cs.
  • если команда ret имеет операнд, то увеличить содержимое esp/sp на величину операнда число; при этом учитывается атрибут режима адресации — use16 или use32:
    • если use16, то sp=(sp+число), то есть указатель стека сдвигается на число байт, равное значению число;
    • если use32, то sp=(sp+2*число), то есть указатель стека сдвигается на число слов, равное значению число.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команду ret необходимо применять для возврата управления вызывающей программе из процедуры, управление которой было передано по команде call. На самом деле микропроцессор имеет три варианта команды возврата ret - это ret, ее синоним retn, а также команда retf. Они отличаются типами процедур, в которых используются. Команды ret и retn служат для возврата из процедур ближнего типа. Команда retf — команда возврата для процедур дальнего типа. Какая конкретно команда будет использоваться, определяется компилятором; программисту лучше использовать команду ret и доверить транслятору самому сгенерировать ее ближний или дальний вариант. Количество команд ret в процедуре должно соответствовать количеству точек выхода из нее.
Некоторые языки высокого уровня, к примеру Pascal, требуют, чтобы вызываемая процедура очищала стек от переданных ей параметров. Для этого команда ret содержит необязательный параметр число, который, в зависимости от установленного атрибута размера адреса, означает количество байт или слов, удаляемых из стека по окончании работы процедуры.
my_proc proc
...
        ret     6
        endp

См. также: уроки 10, 14 и команду call

ROL

(Rotate operand Left)
Циклический сдвиг операнда влево
Схема команды: rol операнд,количество_сдвигов
Назначение: операция циклического сдвига операнда влево.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда влево на один разряд, при этом старший бит операнда вдвигается в операнд справа и становится значением младшего бита операнда;
  • одновременно выдвигаемый бит становится значением флага переноса cf;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Применение:
Команда rol используется для циклического сдвига разрядов операнда влево. Отличие этого сдвига от rcl в том, что очередной сдвигаемый бит одновременно вдвигается в операнд справа и становится значением флага cf. Так же, как и для других сдвигов, значение второго операнда (счетчика сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов операнда количество_разрядов. Аналогично другим командам сдвига сохраняется эффект, связанный с поведением флага of, значение которого имеет смысл только в операциях сдвига на один разряд:
  • если of=1, то текущее значение флага cf и выдвигаемого слева бита операнда различны;
  • если of=0, то текущее значение флага cf и выдвигаемого слева бита операнда совпадают.
Этот эффект, как вы помните, обусловлен тем, что флаг of устанавливается в единицу всякий раз при изменении знакового разряда операнда.
;поменять местами половинки регистра eax:
        mov     ax,0ffff0000h
        mov     cl,16
        rol     eax,cl  ;eax=0000ffffh

См. также: урок 9 и команды rcr, rcl, ror, sal, sar, shl, shr

ROR

Циклический сдвиг операнда вправо
ASCII-коррекция после сложения
Схема команды: ror операнд,количество_сдвигов
Назначение: операция циклического сдвига операнда вправо.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда вправо на один разряд, при этом младший бит операнда вдвигается в операнд слева и становится значением старшего бита операнда;
  • одновременно этот младший бит операнда становится значением флага переноса cf;
  • старое значение флага переноса cf вдвигается в операнд слева и становится значением старшего бита операнда;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Применение:
Команда ror используется для циклического сдвига разрядов операнда вправо. Отличие этого сдвига от rcr в том, что очередной сдвигаемый бит одновременно вдвигается в операнд слева и становится значением флага cf. Так же, как и для других сдвигов, значение второго операнда (счетчика сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов операнда количество_разрядов. Аналогично другим командам сдвига сохраняется эффект, связанный с поведением флага of, значение которого имеет смысл только в операциях сдвига на один разряд:
  • если of=1, то текущее значение флага cf и вдвигаемого слева бита операнда различны;
  • если of=0, то текущее значение флага cf и вдвигаемого слева бита операнда совпадают;
Этот эффект, как вы помните, обусловлен тем, что флаг of устанавливается в единицу всякий раз при изменении знакового разряда операнда.
;поместить четыре младших бита ax на место старших битов:
        ror     ax,4

См. также: уроки 9 и команды rcl, rcr, ror, sal, sar, shl, shr

SAHF

(Store AH register into register Flags)
Загрузка регистра флагов eFlags/Flags из регистра AH
Схема команды: sahf
Назначение: запись содержимого регистра ah в младший байт регистра eflags/flags, в котором содержатся пять флагов cf, pf, af, zf и sf.

Синтаксис
Алгоритм работы:
Команда загружает младший байт регистра eflags/flags содержимым регистра ah. В битах 7, 6, 4, 2 и 0 регистра ah должны, соответственно, содержаться новые значения флагов sf, zf, af, pf и cf.
Состояние флагов после выполнения команды:

07 | 06 | 04 | 02 | 00
SF | ZF | AF | PF | CF
r | r | r | r | r
Применение:
Эта команда используется совместно с командой lahf. Из-за того, что регистр флагов непосредственно недоступен, сочетание этих команд можно применять для анализа — и, возможно, изменения — состояния некоторых флагов в регистре eflags/flags. Содержимое старшей части регистра флагов не изменяется.
;сбросить в ноль флаг cf
        lahf
        and     ah,11111110b
        sahf

См. также: уроки 2, 7 и команду lahf

SAL

(Shift Arithmetic operand Left)
Сдвиг арифметический операнда влево
Схема команды: sal операнд,количество_сдвигов
Назначение: арифметический сдвиг операнда влево.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда влево на один разряд, при этом выдвигаемый слева бит становится значением флага переноса cf;
  • одновременно справа в операнд вдвигается нулевой бит;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Применение:
Команда sal используется для сдвига разрядов операнда влево. Так же, как и для других сдвигов, значение второго операнда (счетчика сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов количество_разрядов. Аналогично другим командам сдвига сохраняется эффект, связанный с поведением флага of, значение которого имеет смысл только в операциях сдвига на один разряд:
  • если of=1, то текущее значение флага cf и выдвигаемого слева бита операнда различны;
  • если of=0, то текущее значение флага cf и выдвигаемого слева бита операнда совпадают.
Этот эффект, как вы помните, обусловлен тем, что флаг cf устанавливается в единицу всякий раз при изменении знакового разряда операнда.
Команду sal удобно использовать для умножения целочисленных операндов без знака на степени 2. Кстати сказать, это самый быстрый способ такого умножения; умножить содержимое ax на 16 (2 в степени 4):
        mov     ax,17
        sal     ax,4

См. также: уроки 8, 9 и команды rcr, rcl, ror, rol, sar, shl, shr

SAR

(Shift Arithmetic operand Right)
Сдвиг арифметический операнда вправо
Схема команды: sar операнд,количество_сдвигов
Назначение: арифметический сдвиг операнда вправо.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда вправо на один разряд, при этом выдвигаемый справа бит становится значением флага переноса cf;
  • обратите внимание: одновременно слева в операнд вдвигается не нулевой бит, а значение старшего бита операнда, то есть по мере сдвига вправо освобождающиеся места заполняются значением знакового разряда. По этой причине этот тип сдвига и называется арифметическим;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Применение:
Команда sar используется для арифметического сдвига разрядов операнда вправо. Так же, как и для других сдвигов, значение второго операнда (счетчика сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов операнда количество_разрядов. В отличие от других команд сдвига флаг of всегда сбрасывается в ноль в операциях сдвига на один разряд.
Команду sar можно использовать для деления целочисленных операндов со знаком на степени 2.
        mov     ax,88
;(ax) разделить на 2 во второй степени, то есть на 4
        sar     ax,2

См. также: урок 8, 9 и команды rcr, rcl, ror, rol, sal, shl, shr

SBB

(SuBtract with Borrow)
Вычитание с заемом
Схема команды: sbb операнд_1,операнд_2
Назначение: целочисленное вычитание с учетом результата предыдущего вычитания командами sbb и sub (по состоянию флага переноса cf).

Синтаксис
Алгоритм работы:

  • выполнить сложение операнд_2=операнд_2+(cf);
  • выполнить вычитание операнд_1=операнд_1-операнд_2;
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда sbb используется для выполнения вычитания старших частей значений многобайтных операндов с учетом возможного предыдущего заема при вычитании младших частей значений этих операндов.
;выполнить вычитание 64-битных значений: vich_1-vich_2
vich_1  dd      2 dup (0)
vich_2  dd      2 dup (0)
rez     dd      2 dup (0)
...
;ввести значения в поля vich_1 и vich_2:
;младший байт по младшему адресу
...
        mov     eax,vich_1
        sub     eax,vich_2      ;вычесть младшие половинки чисел
        mov     rez,eax ;младшая часть результата
        mov     eax,vich_1+4
        sbb     eax,vich_2+4    ;вычесть старшие половинки чисел
        mov     rez+4,eax       ;старшая часть результата

См. также: урок 8, Приложение 7 и команды sub

SCAS/SCASB/SCASW/SCASD

Сканирование строки байтов/слов/двойных слов
ASCII-коррекция после сложения
Схема команды: scas приемник
scasb
scasw
scasd
Назначение: поиск значения в последовательности (цепочке) элементов в памяти.

Синтаксис
Алгоритм работы:

  • выполнить вычитание (элемент цепочки-(eax/ax/al)). Элемент цепочки локализуется парой es:edi/di. Замена сегмента es не допускается;
  • по результату вычитания установить флаги;
  • изменить значение регистра edi/di на величину, равную длине элемента цепочки. Знак этой величины зависит от состояния флага df:
    • df=0 — величина положительная, то есть просмотр от начала цепочки к ее концу;
    • df=1 — величина отрицательная, то есть просмотр от конца цепочки к ее началу.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команды сканирования сравнивают значение в регистре eax/ax/al с ячейкой памяти, локализуемой парой регистров es:edi/di. Размер сравниваемого элемента зависит от применяемой команды. Команда scas может работать с элементами размером в байт, слово или двойное слово. В качестве операнда в команде указывается идентификатор последовательности элементов в памяти. Реально этот идентификатор используется лишь для получения типа элементов последовательности, а ее адрес должен быть предварительно загружен в указанную выше пару регистров. Транслятор, обработав команду scas и выяснив тип операндов, генерирует одну из машинных команд:: scasb, scasw или scasd. Машинного аналога для команды scas нет. Для адресации операнда источник обязательно должен использоваться регистр es.
Для того чтобы эту команду можно было использовать для поиска значения в последовательности элементов, имеющих размерность байт, слово или двойное слово, необходимо использовать один из префиксов repe или repne. Эти префиксы не только заставляют циклически выполняться команду поиска, пока ecx/cx<>0, но и отслеживают состояние флага zf (см. команды rep/repe/repne).
;сосчитать число пробелов в строке str
.data
str     db      '...'
len_str=$-str
.code
        mov     ax,@data
        mov     ds,ax
        mov     es,ax
        lea     di,str
        mov     cx,len_str      ;длину строки — в cx
        mov     al,' '
        mov     bx,0    ;счетчик для подсчета пробелов в строке
        cld
cycl:
repe    scasb
        jcxz    exit    ;переход на exit, если цепочка просмотрена полностью
        inc     bx
        jmp     cycl
exit:   ...

См. также: урок 11 и команды cmps/cmpsb/cmpsw/cmpsd, ins/insb/insw/insd, lods/lodsb/lodsw/lodsd, movs/movsb/movsw/movsd, outs, stos/stosb
/stosw/stosd, rep/repe/repz/repne/repnz

SETcc

(byte SET on condition)
Установка байта по условию
Схема команды: setcc операнд
Назначение: установка операнда логическим значением в зависимости от истинности условия, заданного модификатором кода операции cc.

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

Команды установки байтов
Команда | Проверяемые флаги | Логическое условие
SETA/SETNBE | CF = 0 и ZF = 0 | (выше)/(не ниже или равно)
SETAE/SETNB | CF = 0 | (выше или равно)/(не ниже)
SETB/SETNAE | CF = 1 | (ниже)/(не выше или равно)
SETBE/SETNA | CF = 1 или ZF = 1 | (ниже или равно)/(не выше)
SETC | CF = 1 | перенос
SETE/SETZ | ZF = 1 | ноль
SETG/SETNLE | ZF = 0 или SF = OF | (больше)/(не меньше или равно)
SETGE/SETNL | SF = OF | (больше или равно)/(не меньше)
SETL/SETNGE | SF <> OF | если SF <> OF
SETLE/SETNG | ZF=1 или SF <> OF | (меньше или равно)/(не больше)
SETNC | CF = 0 | нет переноса
SETNE/SETNZ | ZF = 0 | не равно нулю
SETNO | OF=0 | нет переполнения
SETNP/SETPO | PF = 0 | (неравенство)/(нет контроля четности)
SETNS | SF = 0 | нет знака, число положительное
SETO | OF = 1 | переполнение
SETP/SETPE | PF = 1 | контроль четности/равенство
SETS | SF = 1 | если знак минус, число отрицательное
Если проверяемое условие (или содержимое соответствующих флагов на момент выдачи команды setcc) истинно, то установить значение операнда в 01h, если условие ложно — то в 00h.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Эти команды можно использовать после любой команды, изменяющей флаги, при необходимости анализа результата изменений. Если проанализировать условия для команд условного перехода, то обнаружится их полное соответствие с условиями, обрабатываемыми командой setcc, за исключением, конечно, команд jcxz и jecxz.
;подсчитать число единичных битов в регистре ax
        mov     cx,16
m1:     rol     ax,1
        setc    bl
        add     bh,bl
        clc
        loop    m1

См. также: урок 10 и команду jcc

SGDT

(Store Global Descriptor Table)
Сохранение регистра глобальной дескрипторной таблицы
Схема команды: sgdt источник
Назначение: извлечение содержимого системного регистра gdtr, содержащего значения базового адреса и размера глобальной дескрипторной таблицы GDT.

Синтаксис
Алгоритм работы:
Команда выполняет чтение содержимого системного регистра gdtr в область памяти размером 48 бит. Структурно эти 48 бит представляют 16 бит размера и 32 бита значения базового адреса начала таблицы GDT в памяти.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду sgdt применяют при работе системных программ с уровнем привилегий 0, в частности, при написании различных драйверов.
.286
;структура для описания псевдодескриптора gdtr
point   STRUC
lim     dw      0
adr     dd      0
        ENDS
.data
point_gdt       point
.code
...
;читаем содержимое gdtr
        sgdt    point_gdt
...

См. также: уроки 16, 17 и команду lgdt

SIDT

(Store Interrupt Descriptor Table)
Сохранение регистра глобальной дескрипторной таблицы прерываний
Схема команды: sidt источник
Назначение: извлечение содержимого системного регистра idtr, содержащего значения базового адреса и размера дескрипторной таблицы прерываний IDT.

Синтаксис
Алгоритм работы:
команда sidt выполняет чтение содержимого системного регистра idtr в область памяти размером 48 бит. Структурно эти 48 бит представляют 16 бит размера и 32 бита значения базового адреса начала таблицы IDT в памяти.
Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду sidt применяют при работе системных программ с уровнем привилегий 0, в частности, при написании различных драйверов. В качестве операнда в команде указывается адрес области в формате 16+32. Младшее слово области — размер IDT, двойное слово по старшему адресу — значение базового адреса начала этой таблицы.
.286
;структура для описания псевдодескрипторов gdtr и idtr
point   STRUC
lim     dw      0
adr     dd      0
        ENDS
.data
point_idt       point
.code
...
;читаем содержимое idtr
        sidt    point_idt
...

См. также: урок 17 и команду lidt

SHL

(SHift logical Left)
Сдвиг логический операнда влево
Схема команды: shl операнд,количество_сдвигов
Назначение: логический сдвиг операнда влево.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда влево на один разряд, при этом выдвигаемый слева бит становится значением флага переноса cf;
  • одновременно слева в операнд вдвигается нулевой бит;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 00
OF | CF
?r | r
Применение:
Команда shl используется для сдвига разрядов операнда влево. Ее машинный код идентичен коду sal, поэтому вся информация, приведенная для sal, относится и к команде shl. Команда shl используется для сдвига разрядов операнда влево. Так же, как и для других сдвигов, значение второго операнда (счетчикк сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов операнда количество_разрядов. Аналогично другим командам сдвига сохраняется эффект, связанный с поведением флага of, значение которого имеет смысл только в операциях сдвига на один разряд:
  • если of=1, то текущее значение флага cf и выдвигаемого слева бита операнда различны;
  • если of=0, то текущее значение флага cf и выдвигаемого слева бита операнда совпадают.
Этот эффект, как вы помните, обусловлен тем, что флаг of устанавливается в единицу всякий раз при изменении знакового разряда операнда.
Команду shl удобно использовать для умножения целочисленных операндов без знака на степени 2. Кстати сказать, это самый быстрый способ умножения; умножить содержимое ax на 16 (2 в степени 4).
        mov     ax,17
        shl     ax,4

См. также: урок 9 и команды rcr, rcl, ror, rol, sar, sal, shr

SHLD

(SHift Left Double word)
Сдвиг двойного слова влево
Схема команды: shld приемник,источник,количество_сдвигов
Назначение: логический сдвиг двойного слова влево.

Синтаксис
Алгоритм работы:

  • сдвинуть операнд приемник влево на количество битов, определяемое операндом количество_сдвигов;
  • одновременно сдвинуть операнд источник влево на количество битов, определяемое операндом количество_сдвигов. Важно заметить, что операнд источник только обеспечивает вдвигаемые в операнд приемник биты, сам он при этом не изменяется;
  • выдвигаемые во время сдвига влево из операнда источник биты вдвигаются в операнд приемник с его правого края.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | r | r | ? | r | r
Применение:
Команда shld используется для манипуляции битовыми строками длиной до 64 бит. Эту команду удобно использовать для быстрой вставки (или извлечения) битной строки в большую битную строку; при этом, что очень важно, не разрушается контекст (битное окружение) этих подстрок.
.386
;извлечь старшую половину eax в bx без разрушения eax
        mov     cl,16
        shld    ebx,eax,cl
        push    bx
        shl     ebx,cl
        shld    eax,ebx,cl      ;восстановим eax pop bx

См. также: урок 9 и команды rcr, rcl, ror, rol, sar, sal, shr, shrd

SHR

Сдвиг логический операнда вправо
ASCII-коррекция после сложения
Схема команды: shr операнд,кол-во_сдвигов
Назначение: логический сдвиг операнда вправо.

Синтаксис
Алгоритм работы:

  • сдвиг всех битов операнда вправо на один разряд; при этом выдвигаемый справа бит становится значением флага переноса cf;
  • одновременно слева в операнд вдвигается нулевой бит;
  • указанные выше два действия повторяются количество раз, равное значению второго операнда.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
?r | r | r | ? | r | r
Применение:
Команда shr используется для логического сдвига разрядов операнда вправо. Так же, как и для других сдвигов, значение второго операнда (счетчика сдвига) ограничено диапазоном 0...31. Это объясняется тем, что микропроцессор использует только пять младших разрядов операнда количество_разрядов. В отличие от других команд сдвига, флаг of всегда сбрасывается в ноль в операциях сдвига на один разряд.
Команду shr можно использовать для деления целочисленных операндов без знака на степени 2.
        mov     cl,4
        shr     eax,cl  ;(eax) разделить на 2 в степени 4

См. также: урок 9 и команды rcr, rcl, ror, rol, sal, shl, sar

SHRD

(SHift Right Double word)
Сдвиг двойного слова вправо
Схема команды: shrd приемник,источник,количество_сдвигов
Назначение: логический сдвиг двойного слова вправо.

Синтаксис
Алгоритм работы:

  • сдвинуть операнд приемник вправо на количество битов, определяемое операндом количество_сдвигов;
  • одновременно сдвинуть операнд источник вправо на количество битов, определяемое операндом количество_сдвигов. Важно заметить, что операнд источник только обеспечивает вдвигаемые в операнд приемник биты, сам он при этом не изменяется;
  • выдвигаемые вправо во время сдвига из операнда источник биты вдвигаются в операнд приемник с его левого конца.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
? | r | r | ? | r | r
Применение:
Команда shrd используется для манипуляции битными строками длиной до 64 бит. Эту команду удобно использовать для быстрой вставки (или извлечения) битной строки в большую битную строку, при этом, что очень важно, не разрушается контекст (битное окружение) этих подстрок.
.386
;разделить операнд размером 64 бит на степень 2
op_l    dd      ...     ;младшая часть операнда
op_h    dd      ...     ;старшая часть операнда
...
        mov     eax,op_h
        shrd    op_l,eax,4      ;разделить операнд на 4
;так как старшая часть операнда реально еще не сдвинулась,
;то нужно привести ее в соответствие с результатом
        shr     op_h,4

См. также: урок 9 и команды rcr, rcl, ror, rol, sar, sal, shr, shld

STC

(Set Carry Flag)
Установка флага переноса
Схема команды: stc
Назначение: установка флага переноса cf в 1.

Синтаксис
Алгоритм работы:
установить флаг cf в единицу.

Состояние флагов после выполнения команды:

00
CF
1
Применение:
Данная команда используется для установки флага cf в единицу. Такая необходимость может возникнуть при работе с командами сдвига, арифметическими командами или действиями по индикации ошибок в программах.
...
        stc             ;cf=1
...

См. также: уроки 2, 8, 9 и команды cmc, clc

STD

(SeT Direction Flag)
Установка флага направления
Схема команды: std
Назначение: установка флага направления df в 1.

Синтаксис
Алгоритм работы:
установить флаг df в единицу.

Состояние флагов после выполнения команды:

10
DF
1
Применение:
Данная команда используется для установки флага df в единицу. Такая необходимость может возникнуть при работе с цепочечными командами. Единичное состояние флага df вынуждает микропроцессор производить декремент регистров si и di при выполнении цепочечных операций.
...
        std             ;df=1
;смотрите материал урока 11

См. также: уроки 2, 11 и команду cld

STI

(SeT Interrupt flag)
Установка флага прерывания
Схема команды: sti
Назначение: установка флага прерывания if в единицу.

Синтаксис
Алгоритм работы:
установить флаг if в единицу.

Состояние флагов после выполнения команды:

09
IF
1
Применение:
Данная команда используется для установки флага if в единицу. Такая необходимость может возникнуть при разработке программ обработки прерываний.
...
        sti             ;if=1

См. также: урок 2, 15, 17 и команду cli

STOS/STOSB/STOSW/STOSD

(Store String Byte/Word/Double word operands)
Сохранение строки байтов/слов/двойных слов
Схема команды: stos приемник
stosb
stosw
stosd
Назначение: сохранение элемента из регистра-аккумулятора al/ax/eax в последовательности (цепочке).

Синтаксис
Алгоритм работы:

  • записать элемент из регистра al/ax/eax в ячейку памяти, адресуемую парой es:di/edi. Размер элемента определяется неявно (для команды stos) или конкретной применяемой командой (для команд stosb, stosw, stosd);
  • изменить значение регистра di на величину, равную длине элемента цепочки. Знак этого изменения зависит от состояния флага df:
    • df=0 — увеличить, что означает просмотр от начала цепочки к ее концу;
    • df=1 — уменьшить, что означает просмотр от конца цепочки к ее началу.
Состояние флагов после выполнения команды:
выполнение команды не влияет на флаги
Применение:
Команды сохраняют элемент из регистров al/ax/eax в ячейку памяти. Перед командой stos можно указать префикс повторения rep, в этом случае появляется возможность работы с блоками памяти, заполняя их значениями в соответствии с содержимым регистра ecx/cx.
;заполнить некоторую область памяти пробелами
str     db      'Какая-то строка'
len_str=$-str
...
        mov     ax,@data
        mov     ds,ax
        mov     es,ax
        cld
        mov     al,' '
        lea     di,str
        mov     cx,len_str
rep     stosb           ;заполняем пробелами строку str

;пример совместной работы stosb и lodsb:
;копировать одну строку в другую до первого пробела
str1    db      'Какая-то строка'
len_str1=$-str
str2    db      len_str1 dup (' ')
...
        mov     ax,@data
        mov     ds,ax
        mov     es,ax
        cld
        mov     cx,len_str1
        lea     si,str1
        lea     di,str2
m1:     lodsb
        cmp     al,' '
        jc      exit    ;выход, если пробел
        stosb
        loop    m1
exit:

См. также: урок 11 и команды ins/insb/insw/insd, cmps/cmpsb/cmpsw/cmpsd, movs/movsb/movsw/movsd, outs, scas/scasb/scasw/scasd, lods/lodsb/lodsw/lodsd, rep/repe/repz/repne/repnz

SUB

(SUBtract)
Вычитание
Схема команды: sub операнд_1,операнд_2
Назначение: целочисленное вычитание.

Синтаксис
Алгоритм работы:

  • выполнить вычитание операнд_1=операнд_2-операнд_1;
  • установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда sub используется для выполнения вычитания целочисленных операндов или для вычитания младших частей значений многобайтных операндов.
;выполнить вычитание 64-битных значений: vich_1-vich_2
vich_1  dd      2 dup (0)
vich_2  dd      2 dup (0)
rez     dd      2 dup (0)
...
;ввести значения в поля vich_1 и vich_2:
;младший байт по младшему адресу
...
        mov     eax,vich_1
        sub     eax,vich_2      ;вычесть младшие половинки чисел
        mov     rez,eax ;младшая часть результата
        mov     eax,vich_1+4
        sbb     eax,vich_2+4    ;вычесть старшие половинки чисел
        mov     rez+4,eax       ;старшая часть результата

См. также: урок 8, приложение 7 и команду sbb

TEST

(TEST operand)
Логическое И
Схема команды: test приемник,источник
Назначение: операция логического сравнения операндов приемник и источник размерностью байт, слово или двойное слово.

Синтаксис
Алгоритм работы:

  • выполнить операцию логического умножения над операндами приемник и источник: бит результата равен 1, если соответствующие биты операндов равны 1, в остальных случаях бит результата равен 0;
  • установить флаги.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 02 | 00
OF | SF | ZF | PF | CF
0 | r | r | r | 0
Применение:
Команда test используется для логического умножения двух операндов. Результат операции, в отличие от команды and, никуда не записывается, устанавливаются только флаги. Эту команду удобно использовать для получения информации о состоянии заданных битов операнда приемник. Для анализа результата используется флаг zf, который равен 1, если результат логического умножения равен нулю.
        test    al,01h
        jnz     m1      ;переход, если нулевой бит al равен 1

См. также: урок 9 и команды or, xor, and, bt

XADD

(eXchange and ADD)
Обмен и сложение
Схема команды: xadd приемник,источник
Назначение: суммирование и обмен двух значений.

Синтаксис
Алгоритм работы:

  • копировать содержимое операнда приемник в операнд источник;
  • выполнить сложение (приемник+источник);
  • поместить сумму в операнд приемник.
Состояние флагов после выполнения команды:
11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
r | r | r | r | r | r
Применение:
Команда xadd используется для выполнения операции обмена и сложения двух операндов.
        mov     al,08h
        mov     bl,01h
        xadd    al,bl   ;al=09h, bl=08h

См. также: уроки 7, 8 и команды add, xchg

XCHG

(eXCHanGe)
Обмен
Схема команды: xchg операнд_1,операнд_2
Назначение: обмен двух значений между регистрами или между регистрами и памятью.

Синтаксис
Алгоритм работы:
обмен содержимого операнд_1 и операнд_2.

Состояние флагов после выполнения команды:

выполнение команды не влияет на флаги
Применение:
Команду xchg можно использовать для выполнения операции обмена двух операндов с целью изменения порядка следования байт, слов, двойных слов или их временного сохранения в регистре или памяти. Альтернативой является использование для этой цели стека.
;поменять порядок следования байт в слове
ch1     label   byte
        dw      0f85ch
...
        mov     al,ch1
        xchg    ch1+1,al
        mov     ch1,al

См. также: урок 7 и команды bswap, cmpxchg, xadd

XLAT/XLATB

(transLATe Byte from table)
Преобразование байта
Схема команды: xlat адрес_таблицы_байтов xlatb
Назначение: подмена байта в регистре al байтом из последовательности (таблицы) байтов в памяти.

Синтаксис
Алгоритм работы:

  • вычислить адрес, равный ds:bx+(al);
  • выполнить замену байта в регистре al байтом из памяти по вычисленному адресу.
    • Несмотря на наличие операнда адрес_таблицы_байтов в команде xlat, адрес последовательности байтов, из которой будет осуществляться выборка байта для подмены в регистре al, должен быть предварительно загружен в пару ds:bx(ebx). Команда xlat допускает замену сегмента.

    Состояние флагов после выполнения команды:

    выполнение команды не влияет на флаги
    Применение:
    Команду xlat можно использовать для выполнения перекодировок символов. Для формирования адреса таблицы в регистрах bx(ebx) можно использовать команду lea или оператор ассемблера offset в команде mov.
    table   db      'abcdef'
int     db      0       ;значение индекса
...
        mov     al,3
        lea     bx,table
        xlat            ;(al)='c'

    См. также: урок 7 и команду lea

    XOR

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

    Синтаксис
    Алгоритм работы:

    • выполнить операцию логического исключающего ИЛИ над операндами: бит результата равен 1, если значения соответствующих битов операндов различны, в остальных случаях бит результата равен 0;
    • записать результат сложения в приемник;
    • установить флаги.
    Состояние флагов после выполнения команды:
    11 | 07 | 06 | 04 | 02 | 00
OF | SF | ZF | AF | PF | CF
0 | r | r | ? | r | 0
    Применение:
    Команда xor используется для выполнения операции логического исключающего ИЛИ двух операндов. Результат операции помещается в первый операнд. Эту операцию удобно использовать для инвертирования или сравнения определенных битов операндов.
    ;изменить значение бита 0 регистра al на обратное
        xor     al,01h

    См. также: урок 9 и команды and, or, not


Модель микропроцессора

Программная модель микропроцессора

На современном компьютерном рынке наблюдается большое разнообразие различных типов компьютеров. Поэтому возможно предположить возникновение у потребителя вопроса — как оценить возможности конкретного типа (или модели) компьютера и его отличительные особенности от компьютеров других типов (моделей). Рассмотрения для этого одной лишь только структурной схемы компьютера недостаточно, так как она принципиально мало чем различается у разных машин: у всех компьютеров есть оперативная память, процессор, внешние устройства.
Различными являются способы, средства и используемые ресурсы, с помощью которых компьютер функционирует как единый механизм. Чтобы собрать воедино все понятия, характеризующие компьютер с точки зрения его функциональных программно-управляемых свойств, существует специальный термин — архитектура ЭВМ.
Впервые понятие архитектура ЭВМ стало упоминаться с появлением машин 3-го поколения для их сравнительной оценки.

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

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

Пользовательские регистры

Как следует из названия, пользовательскими регистры называются потому, что программист может использовать их при написании своих программ. К этим регистрам относятся (рис. 1):
  • восемь 32-битных регистров, которые могут использоваться программистами для хранения данных и адресов (их еще называют регистрами общего назначения (РОН)):
    • eax/ax/ah/al;
    • ebx/bx/bh/bl;
    • edx/dx/dh/dl;
    • ecx/cx/ch/cl;
    • ebp/bp;
    • esi/si;
    • edi/di;
    • esp/sp.
  • шесть регистров сегментов: cs, ds, ss, es, fs, gs;
  • регистры состояния и управления:
    • регистр флагов eflags/flags;
    • регистр указателя команды eip/ip.

Рис. 1. Пользовательские регистры микропроцессоров i486 и Pentium

Почему многие из этих регистров приведены с наклонной разделительной чертой?
Нет, это не разные регистры — это части одного большого 32-разрядного регистра. Их можно использовать в программе как отдельные объекты.
Так сделано для обеспечения работоспособности программ, написанных для младших 16-разрядных моделей микропроцессоров фирмы Intel, начиная с i8086.
Микропроцессоры i486 и Pentium имеют в основном 32-разрядные регистры. Их количество, за исключением сегментных регистров, такое же, как и у i8086, но размерность больше, что и отражено в их обозначениях — они имеют
приставку e (Extended).

Разберемся подробнее с составом и назначением пользовательских регистров.

Регистры общего назначения

Все регистры этой группы позволяют обращаться к своим
“младшим” частям (см. рис. 1).
Рассматривая этот рисунок, заметьте, что использовать для самостоятельной адресации можно только младшие 16 и 8-битные части этих регистров. Старшие 16 бит этих регистров как самостоятельные объекты недоступны. Это сделано, как мы отметили выше, для совместимости с младшими 16-разрядными моделями микропроцессоров фирмы Intel.

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

  • eax/ax/ah/al (Accumulator register) — аккумулятор.
    Применяется для хранения промежуточных данных. В некоторых командах использование этого регистра обязательно;
  • ebx/bx/bh/bl (Base register) — базовый регистр.
    Применяется для хранения базового адреса некоторого объекта в памяти;
  • ecx/cx/ch/cl (Count register) — регистр-счетчик.
    Применяется в командах, производящих некоторые повторяющиеся действия. Его использование зачастую неявно и скрыто в алгоритме работы соответствующей команды.
    К примеру, команда организации цикла loop кроме передачи управления команде, находящейся по некоторому адресу, анализирует и уменьшает на единицу значение регистра ecx/cx;
  • edx/dx/dh/dl (Data register) — регистр данных.
    Так же, как и регистр eax/ax/ah/al, он хранит промежуточные данные. В некоторых командах его использование обязательно; для некоторых команд это происходит неявно.
Следующие два регистра используются для поддержки так называемых цепочечных операций, то есть операций, производящих последовательную обработку цепочек элементов, каждый из которых может иметь длину 32, 16 или 8 бит:
  • esi/si (Source Index register) — индекс источника.
    Этот регистр в цепочечных операциях содержит текущий адрес элемента в цепочке-источнике;
  • edi/di (Destination Index register) — индекс приемника (получателя).
    Этот регистр в цепочечных операциях содержит текущий адрес в цепочке-приемнике.
В архитектуре микропроцессора на программно-аппаратном уровне поддерживается такая структура данных, как стек. Для работы со стеком в системе команд микропроцессора есть специальные команды, а в программной модели микропроцессора для этого существуют специальные регистры:
  • esp/sp (Stack Pointer register) — регистр указателя стека.
    Содержит указатель вершины стека в текущем сегменте стека.
  • ebp/bp (Base Pointer register) — регистр указателя базы кадра стека.
    Предназначен для организации произвольного доступа к данным внутри стека.
Не спешите пугаться столь жесткого функционального назначения регистров АЛУ. На самом деле, большинство из них могут использоваться при программировании для хранения операндов практически в любых сочетаниях. Но, как мы отметили выше, некоторые команды используют фиксированные регистры для выполнения своих действий. Это нужно обязательно учитывать.
Использование жесткого закрепления регистров для некоторых команд позволяет более компактно кодировать их машинное представление. Знание этих особенностей позволит вам при необходимости хотя бы на несколько байт сэкономить память, занимаемую кодом программы.

Сегментные регистры

В программной модели микропроцессора имеется шесть сегментных регистров: cs, ss, ds, es, gs, fs.
Их существование обусловлено спецификой организации и использования оперативной памяти микропроцессорами Intel. Она заключается в том, что микропроцессор аппаратно поддерживает структурную организацию программы в виде трех частей, называемых сегментами. Соответственно, такая организация памяти называется сегментной.

Для того чтобы указать на сегменты, к которым программа имеет доступ в конкретный момент времени, и предназначены сегментные регистры. Фактически, с небольшой поправкой, как мы увидим далее, в этих регистрах содержатся адреса памяти с которых начинаются соответствующие сегменты. Логика обработки машинной команды построена так, что при выборке команды, доступе к данным программы или к стеку неявно используются адреса во вполне определенных сегментных регистрах. Микропроцессор поддерживает следующие типы сегментов:

  1. Сегмент кода. Содержит команды программы.
    Для доступа к этому сегменту служит регистр cs (code segment register) — сегментный регистр кода. Он содержит адрес сегмента с машинными командами, к которому имеет доступ микропроцессор (то есть эти команды загружаются в конвейер микропроцессора).
  2. Сегмент данных. Содержит обрабатываемые программой данные.
    Для доступа к этому сегменту служит регистр ds (data segment register) — сегментный регистр данных, который хранит адрес сегмента данных текущей программы.
  3. Сегмент стека. Этот сегмент представляет собой область памяти, называемую стеком.
    Работу со стеком микропроцессор организует по следующему принципу: последний записанный в эту область элемент выбирается первым. Для доступа к этому сегменту служит регистр ss (stack segment register) — сегментный регистр стека, содержащий адрес сегмента стека.
  4. Дополнительный сегмент данных.
    Неявно алгоритмы выполнения большинства машинных команд предполагают, что обрабатываемые ими данные расположены в сегменте данных, адрес которого находится в сегментном регистре ds.
    Если программе недостаточно одного сегмента данных, то она имеет возможность использовать еще три дополнительных сегмента данных. Но в отличие от основного сегмента данных, адрес которого содержится в сегментном регистре ds, при использовании дополнительных сегментов данных их адреса требуется указывать явно с помощью специальных префиксов переопределения сегментов в команде.
    Адреса дополнительных сегментов данных должны содержаться в регистрах es, gs, fs (extension data segment registers).

Регистры состояния и управления

В микропроцессор включены несколько регистров (см. рис. 1), которые постоянно содержат информацию о состоянии как самого микропроцессора, так и программы, команды которой в данный момент загружены на конвейер. К этим регистрам относятся:
  • регистр флагов eflags/flags;
  • регистр указателя команды eip/ip.
Используя эти регистры, можно получать информацию о результатах выполнения команд и влиять на состояние самого микропроцессора. Рассмотрим подробнее назначение и содержимое этих регистров:

eflags/flags (flag register) — регистр флагов. Разрядность eflags/flags — 32/16 бит. Отдельные биты данного регистра имеют определенное функциональное назначение и называются флагами. Младшая часть этого регистра полностью аналогична регистру flags для i8086. На рис. 2 показано содержимое регистра eflags.

Рис. 2. Содержимое регистра eflags

Исходя из особенностей использования, флаги регистра eflags/flags можно разделить на три группы:

  • 8 флагов состояния. Эти флаги могут изменяться после выполнения машинных команд.
    Флаги состояния регистра eflags отражают особенности результата исполнения арифметических или логических операций. Это дает возможность анализировать состояние вычислительного процесса и реагировать на него с помощью команд условных переходов и вызовов подпрограмм. В     табл. 1 приведены флаги состояния и указано их назначение;
  • 1 флаг управления. Обозначается df (Directory Flag).
    Он находится в 10-м бите регистра eflags и используется цепочечными командами. Значение флага df определяет направление поэлементной обработки в этих операциях: от начала строки к концу (df = 0) либо наоборот, от конца строки к ее началу (df = 1).
    Для работы с флагом df существуют специальные команды: cld (снять флаг df) и std (установить флаг df).
    Применение этих команд позволяет привести флаг df в соответствие с алгоритмом и обеспечить автоматическое увеличение или уменьшение счетчиков при выполнении операций со строками;
  • 5 системных флагов, управляющих вводом/выводом, маскируемыми прерываниями, отладкой, переключением между задачами и виртуальным режимом 8086.
    Прикладным программам не рекомендуется модифицировать без необходимости эти флаги, так как в большинстве случаев это приведет к прерыванию работы программы. В табл. 2 перечислены системные флаги, их назначение.
Таблица 1. Флаги состояния
Мнемоника флага | Флаг | Номер бита в eflags | Содержание и назначение
cf | Флаг переноса (Carry Flag) | 0 | 1 — арифметическая операция произвела перенос из старшего бита результата. Старшим является 7, 15 или 31-й бит в зависимости от размерности операнда; 0 — переноса не было
pf | Флаг паритета (Parity Flag) | 2 | 1 — 8 младших разрядов (этот флаг — только для 8 младших разрядов операнда любого размера) результата содержат четное число единиц; 0 — 8 младших разрядов результата содержат нечетное число единиц
af | Вспомогательный флаг переноса (Auxiliary carry Flag) | 4 | Только для команд работающих с BCD-числами. Фиксирует факт заема из младшей тетрады результата: 1 — в результате операции сложения был произведен перенос из разряда 3 в старший разряд или при вычитании был заем в разряд 3 младшей тетрады из значения в старшей тетраде; 0 — переносов и заемов в(из) 3 разряд(а) младшей тетрады результата не было
zf | Флаг нуля (Zero Flag) | 6 | 1 — результат нулевой; 0 — результат ненулевой
sf | Флаг знака (Sign Flag) | 7 | Отражает состояние старшего бита результата (биты 7, 15 или 31 для 8, 16 или 32-разрядных операндов соответственно): 1 — старший бит результата равен 1; 0 — старший бит результата равен 0
of | Флаг переполнения (Overflow Flag) | 11 | Флаг of используется для фиксирования факта потери значащего бита при арифметических операциях: 1 — в результате операции происходит перенос (заем) в(из) старшего, знакового бита результата (биты 7, 15 или 31 для 8, 16 или 32-разрядных операндов соответственно); 0 — в результате операции не происходит переноса (заема) в(из) старшего, знакового бита результата
iopl | Уровень Привилегий ввода-вывода (Input/Output Privilege Level) | 12, 13 | Используется в защищенном режиме работы микропроцессора для контроля доступа к командам ввода-вывода в зависимости от привилегированности задачи
nt | флажок вложенности задачи (Nested Task) | 14 | Используется в защищенном режиме работы микропроцессора для фиксации того факта, что одна задача вложена в другую
Таблица 2. Системные флаги
Мнемоника флага | Флаг | Номер бита в eflags | Содержание и назначение
tf | Флаг трассировки (Trace Flag) | 8 | Предназначен для организации пошаговой работы микропроцессора. 1 — микропроцессор генерирует прерывание с номером 1 после выполнения каждой машинной команды. Может использоваться при отладке программ, в частности отладчиками; 0 — обычная работа
if | Флаг прерывания (Interrupt enable Flag) | 9 | Предназначен для разрешения или запрещения (маскирования) аппаратных прерываний (прерываний по входу INTR). 1 — аппаратные прерывания разрешены; 0 — аппаратные прерывания запрещены
rf | Флаг возобновления (Resume Flag) | 16 | Используется при обработке прерываний от регистров отладки.
vm | Флаг виртуального (Virtual 8086 Mode) | 17 | Признак работы микропроцессора в режиме виртуального 8086. 1 — процессор работает в режиме виртуального 8086; 0 — процессор работает в реальном или защищенном режиме
ac | Флаг контроля выравнивания (Alignment Check) | 18 | Предназначен для разрешения контроля выравнивания при обращениях к памяти. Используется совместно с битом am в системном регистре cr0. К примеру, Pentium разрешает размещать команды и данные с любого адреса. Если требуется контролировать выравнивание данных и команд по адресам кратным 2 или 4, то установка данных битов приведет к тому, что все обращения по некратным адресам будут возбуждать исключительную ситуацию

eip/ip (Instraction Pointer register) — регистр-указатель команд.
Регистр eip/ip имеет разрядность 32/16 бит и содержит смещение следующей подлежащей выполнению команды относительно содержимого сегментного регистра cs в текущем сегменте команд. Этот регистр непосредственно недоступен программисту, но загрузка и изменение его значения производятся различными командами управления, к которым относятся команды условных и безусловных переходов, вызова процедур и возврата из процедур. Возникновение прерываний также приводит к модификации регистра eip/ip.

Системные регистры микропроцессора

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

Системные регистры можно разделить на три группы:

  • четыре регистра управления;
  • четыре регистра системных адресов;
  • восемь регистров отладки.

Регистры управления

В группу регистров управления входят 4 регистра:
cr0, cr1, cr2, cr3.

Эти регистры предназначены для общего управления системой.
Регистры управления доступны только программам с уровнем привилегий 0.

Хотя микропроцессор имеет четыре регистра управления, доступными являются только три из них — исключается cr1, функции которого пока не определены (он зарезервирован для будущего использования).

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

  • pe (Protect Enable), бит 0 — разрешение защищенного режима работы.
    Состояние этого флага показывает, в каком из двух режимов — реальном (pe=0) или защищенном (pe=1) — работает микропроцессор в данный момент времени.
  • mp (Math Present), бит 1 — наличие сопроцессора. Всегда 1.
  • ts (Task Switched), бит 3 — переключение задач.
    Процессор автоматически устанавливает этот бит при переключении на выполнение другой задачи.
  • am (Aligment Mask), бит 18 — маска выравнивания.
    Этот бит разрешает (am = 1) или запрещает (am = 0) контроль выравнивания.
  • cd (Cache Disable), бит 30, — запрещение кэш-памяти.
    С помощью этого бита можно запретить (cd = 1) или разрешить (cd = 0) использование внутренней кэш-памяти (кэш-памяти первого уровня).
  • pg (PaGing), бит 31, — разрешение (pg = 1) или запрещение (pg = 0) страничного преобразования.
    Флаг используется при страничной модели организации памяти.

Регистр cr2 используется при страничной организации оперативной памяти для регистрации ситуации, когда текущая команда обратилась по адресу, содержащемуся в странице памяти, отсутствующей в данный момент времени в памяти.
В такой ситуации в микропроцессоре возникает исключительная ситуация с номером 14, и линейный 32-битный адрес команды, вызвавшей это исключение, записывается в регистр cr2. Имея эту информацию, обработчик исключения 14 определяет нужную страницу, осуществляет ее подкачку в память и возобновляет нормальную работу программы;

Регистр cr3 также используется при страничной организации памяти.
Это так называемый регистр каталога страниц первого уровня. Он содержит 20-битный физический базовый адрес каталога страниц текущей задачи. Этот каталог содержит 1024 32-битных дескриптора, каждый из которых содержит адрес таблицы страниц второго уровня. В свою очередь каждая из таблиц страниц второго уровня содержит 1024 32-битных дескриптора, адресующих страничные кадры в памяти. Размер страничного кадра — 4 Кбайт.

Регистры системных адресов

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

При работе в защищенном режиме микропроцессора адресное пространство делится на:

  • глобальное — общее для всех задач;
  • локальное — отдельное для каждой задачи.
Этим разделением и объясняется присутствие в архитектуре микропроцессора следующих системных регистров:
  • регистра таблицы глобальных дескрипторов gdtr (Global Descriptor Table Register) имеющего размер 48 бит и содержащего 32-битовый (биты 16—47) базовый адрес глобальной дескрипторной таблицы GDT и 16-битовое (биты 0—15) значение предела, представляющее собой размер в байтах таблицы GDT;
  • регистра таблицы локальных дескрипторов ldtr (Local Descriptor Table Register) имеющего размер 16 бит и содержащего так называемый селектор дескриптора локальной дескрипторной таблицы LDT. Этот селектор является указателем в таблице GDT, который и описывает сегмент, содержащий локальную дескрипторную таблицу LDT;
  • регистра таблицы дескрипторов прерываний idtr (Interrupt Descriptor Table Register) имеющего размер 48 бит и содержащего 32-битовый (биты 16–47) базовый адрес дескрипторной таблицы прерываний IDT и 16-битовое (биты 0—15) значение предела, представляющее собой размер в байтах таблицы IDT;
  • 16-битового регистра задачи tr (Task Register), который подобно регистру ldtr, содержит селектор, то есть указатель на дескриптор в таблице GDT. Этот дескриптор описывает текущий сегмент состояния задачи (TSS — Task Segment Status). Этот сегмент создается для каждой задачи в системе, имеет жестко регламентированную структуру и содержит контекст (текущее состояние) задачи. Основное назначение сегментов TSS — сохранять текущее состояние задачи в момент переключения на другую задачу.

Регистры отладки

Это очень интересная группа регистров, предназначенных для аппаратной отладки. Средства аппаратной отладки впервые появились в микропроцессоре i486. Аппаратно микропроцессор содержит восемь регистров отладки, но реально из них используются только 6.

Регистры dr0, dr1, dr2, dr3 имеют разрядность 32 бит и предназначены для задания линейных адресов четырех точек прерывания. Используемый при этом механизм следующий: любой формируемый текущей программой адрес сравнивается с адресами в регистрах dr0...dr3, и при совпадении генерируется исключение отладки с номером 1.

Регистр dr6 называется регистром состояния отладки. Биты этого регистра устанавливаются в соответствии с причинами, которые вызвали возникновение последнего исключения с номером 1.

Перечислим эти биты и их назначение:

  • b0 — если этот бит установлен в 1, то последнее исключение (прерывание) возникло в результате достижения контрольной точки, определенной в регистре dr0;
  • b1 — аналогично b0, но для контрольной точки в регистре dr1;
  • b2 — аналогично b0, но для контрольной точки в регистре dr2;
  • b3 — аналогично b0, но для контрольной точки в регистре dr3;
  • bd (бит 13) — служит для защиты регистров отладки;
  • bs (бит 14) — устанавливается в 1, если исключение 1 было вызвано состоянием флага tf = 1 в регистре eflags;
  • bt (бит 15) устанавливается в 1, если исключение 1 было вызвано переключением на задачу с установленным битом ловушки в TSS t = 1.
Все остальные биты в этом регистре заполняются нулями. Обработчик исключения 1 по содержимому dr6 должен определить причину, по которой произошло исключение, и выполнить необходимые действия.

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

  • место регистрации контрольной точки — только в текущей задаче или в любой задаче. Эти биты занимают младшие восемь бит регистра dr7 (по два бита на каждую контрольную точку (фактически точку прерывания), задаваемую регистрами dr0, dr1, dr2, dr3 соответственно).
    Первый бит из каждой пары — это так называемое локальное разрешение; его установка говорит о том, что точка прерывания действует если она находится в пределах адресного пространства текущей задачи.
    Второй бит в каждой паре определяет глобальное разрешение, которое говорит о том, что данная контрольная точка действует в пределах адресных пространств всех задач, находящихся в системе;
  • тип доступа, по которому инициируется прерывание: только при выборке команды, при записи или при записи/чтении данных. Биты, определяющие подобную природу возникновения прерывания, локализуются в старшей части данного регистра.

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


Типы данных

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

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

    Эти два типа данных являются элементарными, или базовыми; работа с ними поддерживается на уровне системы команд микропроцессора. Используя данные этих типов, можно формализовать и запрограммировать практически любую задачу. Но насколько это будет удобно — вот вопрос.

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

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

  • байт — восемь последовательно расположенных битов, пронумерованных от 0 до 7, при этом бит 0 является самым младшим значащим битом;
  • слово — последовательность из двух байт, имеющих последовательные адреса. Размер слова — 16 бит; биты в слове нумеруются от 0 до 15. Байт, содержащий нулевой бит, называется младшим байтом, а байт, содержащий 15-й бит - старшим байтом. Микропроцессоры Intel имеют важную особенность — младший байт всегда хранится по меньшему адресу. Адресом слова считается адрес его младшего байта. Адрес старшего байта может быть использован для доступа к старшей половине слова.
  • двойное слово — последовательность из четырех байт (32 бита), расположенных по последовательным адресам. Нумерация этих бит производится от 0 до 31. Слово, содержащее нулевой бит, называется младшим словом, а слово, содержащее 31-й бит, - старшим словом. Младшее слово хранится по меньшему адресу. Адресом двойного слова считается адрес его младшего слова. Адрес старшего слова может быть использован для доступа к старшей половине двойного слова.
  • учетверенное слово — последовательность из восьми байт (64 бита), расположенных по последовательным адресам. Нумерация бит производится от 0 до 63. Двойное слово, содержащее нулевой бит, называется младшим двойным словом, а двойное слово, содержащее 63-й бит, — старшим двойным словом. Младшее двойное слово хранится по меньшему адресу. Адресом учетверенного слова считается адрес его младшего двойного слова. Адрес старшего двойного слова может быть использован для доступа к старшей половине учетверенного слова.

Рис. 1. Основные типы данных микропроцессора

Кроме трактовки типов данных с точки зрения их разрядности, микропроцессор на уровне команд поддерживает логическую интерпретацию этих типов (рис. 2):

  • Целый тип со знаком — двоичное значение со знаком, размером 8, 16 или 32 бита. Знак в этом двоичном числе содержится в 7, 15 или 31-м бите соответственно. Ноль в этих битах в операндах соответствует положительному числу, а единица — отрицательному. Отрицательные числа представляются в дополнительном коде. Числовые диапазоны для этого типа данных следующие:
    • 8-разрядное целое — от –128 до +127;
    • 16-разрядное целое — от –32 768 до +32 767;
    • 32-разрядное целое — от –231 до +231–1.
  • Целый тип без знака — двоичное значение без знака, размером 8, 16 или 32 бита. Числовой диапазон для этого типа следующий:
    • байт — от 0 до 255;
    • слово — от 0 до 65 535;
    • двойное слово — от 0 до 232–1.
  • Указатель на память двух типов:
    • ближнего типа — 32-разрядный логический адрес, представляющий собой относительное смещение в байтах от начала сегмента. Эти указатели могут также использоваться в сплошной (плоской) модели памяти, где сегментные составляющие одинаковы;
    • дальнего типа — 48-разрядный логический адрес, состоящий из двух частей: 16-разрядной сегментной части — селектора, и 32-разрядного смещения.
  • Цепочка — представляющая собой некоторый непрерывный набор байтов, слов или двойных слов максимальной длины до 4 Гбайт.
  • Битовое поле представляет собой непрерывную последовательность бит, в которой каждый бит является независимым и может рассматриваться как отдельная переменная. Битовое поле может начинаться с любого бита любого байта и содержать до 32 бит.
  • Неупакованный двоично-десятичный тип — байтовое представление десятичной цифры от 0 до 9. Неупакованные десятичные числа хранятся как байтовые значения без знака по одной цифре в каждом байте. Значение цифры определяется младшим полубайтом.
  • Упакованный двоично-десятичный тип представляет собой упакованное представление двух десятичных цифр от 0 до 9 в одном байте. Каждая цифра хранится в своем полубайте. Цифра в старшем полубайте (биты 4–7) является старшей.

Рис. 2. Основные логические типы данных микропроцессора

Отметим, что “Зн” на рис. 2 означает знаковый бит.

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

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

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

Директивы резервирования и инициализации данных простых типов имеют формат, показанный на рис. 3.

Рис. 3. Директивы описания данных простых типов

На рис. 3 использованы следующие обозначения:

  • ? показывает, что содержимое поля не определено, то есть при задании директивы с таким значением выражения содержимое выделенного участка физической памяти изменяться не будет. Фактически, создается неинициализированная переменная;
  • значение инициализации — значение элемента данных, которое будет занесено в память после загрузки программы. Фактически, создается инициализированная переменная, в качестве которой могут выступать константы, строки символов, константные и адресные выражения в зависимости от типа данных. Подробная информация приведена в приложении 1;
  • выражение — итеративная конструкция с синтаксисом, описанным на рис. 5.17. Эта конструкция позволяет повторить последовательное занесение в физическую память выражения в скобках n раз.
  • имя — некоторое символическое имя метки или ячейки памяти в сегменте данных, используемое в программе.

На рис. 3 представлены следующие поддерживаемые TASM директивы резервирования и инициализации данных:

  • db — резервирование памяти для данных размером 1 байт.
    Директивой db можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для чисел со знаком –128...+127;
      • для чисел без знака 0...255;
    • 8-битовое относительное выражение, использующее операции HIGH и LOW;
    • символьную строку из одного или более символов. Строка заключается в кавычки. В этом случае определяется столько байт, сколько символов в строке.
  • dw — резервирование памяти для данных размером 2 байта.
    Директивой dw можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для чисел со знаком –32 768...32 767;
      • для чисел без знака 0...65 535;
    • выражение, занимающее 16 или менее бит, в качестве которого может выступать смещение в 16-битовом сегменте или адрес сегмента;
    • 1- или 2-байтовую строку, заключенная в кавычки.
  • dd — резервирование памяти для данных размером 4 байта.
    Директивой dd можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 16-битового адреса сегмента и 16-битового смещения;
    • строку длиной до 4 символов, заключенную в кавычки.
  • df — резервирование памяти для данных размером 6 байт;
  • dp — резервирование памяти для данных размером 6 байт.
    Директивами df и dp можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей микропроцессоров Intel);
    • адресное выражение, состоящее из 16-битового сегмента и 32-битового смещения;
    • константу со знаком из диапазона –247...247–1;
    • константу без знака из диапазона 0...248-1;
    • строку длиной до 6 байт, заключенную в кавычки.
  • dq — резервирование памяти для данных размером 8 байт.
    Директивой dq можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для МП i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для МП i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей микропроцессоров Intel);
    • константу со знаком из диапазона –263...263–1;
    • константу без знака из диапазона 0...264–1;
    • строку длиной до 8 байт, заключенную в кавычки.
  • dt — резервирование памяти для данных размером 10 байт.
    Директивой dt можно задавать следующие значения:
    • выражение или константу, принимающую значение из диапазона:
      • для МП i8086:
        • для чисел со знаком –32 768...+32 767;
        • для чисел без знака 0...65 535;
      • для МП i386 и выше:
        • для чисел со знаком –2 147 483 648...+2 147 483 647;
        • для чисел без знака 0...4 294 967 295;
    • относительное или адресное выражение, состоящее из 32 или менее бит (для i80386) или 16 или менее бит (для младших моделей);
    • адресное выражение, состоящее из 16-битового сегмента и 32-битового смещения;
    • константу со знаком из диапазона –279...279-1;
    • константу без знака из диапазона 0...280-1;
    • строку длиной до 10 байт, заключенную в кавычки;
    • упакованную десятичную константу в диапазоне 0...99 999 999 999 999 999 999.

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

Для иллюстрации данного принципа рассмотрим листинг 1, в котором определим сегмент данных. В этом сегменте данных приведено несколько директив описания простых типов данных.

 Листинг 1. Пример использования директив резервирования и инициализации данных
masm
model	small
.stack	100h
.data
message	db	'Запустите эту программу в отладчике’,’$'
perem_1	db	0ffh
perem_2	dw	3a7fh
perem_3	dd	0f54d567ah
mas db	10	dup (' ')
pole_1	db	5 dup (?)
adr	dw	perem_3
adr_full	dd	perem_3
fin	db	'Конец сегмента данных программы $'
.code
start:
    mov	ax,@data
    mov	ds,ax
    mov	ah,09h
    mov	dx,offset message
    int	21h
    mov	ax,4c00h
    int	21h
end	start

Теперь наша цель — посмотреть, как выглядит сегмент данных программы листинга 1 в памяти компьютера. Это даст нам возможность обсудить практическую реализацию обозначенного нами принципа размещения данных. Для этого запустим отладчик TD.EXE, входящий в комплект поставки TASM. Результат показан на рис. 4.

Рис. 4. Окно дампа памяти для программы листинга 1

Обсудим рис. 4. На нем вы видите данные вашего сегмента в двух представлениях: шестнадцатеричном и символьном. Видно, что со смещением 0000 расположены символы, входящие в строку message. Она занимает 34 байта. После нее следует байт, имеющий в сегменте данных символическое имя perem_1, содержимое этого байта offh.
Теперь обратите внимание на то, как размещены в памяти байты, входящие в слово, обозначенное символическим именем perem_2. Сначала следует байт со значением 7fh, а затем со значением 3ah. Как видите, в памяти действительно сначала расположен младший байт значения, а затем старший.
Теперь посмотрите и самостоятельно проанализируйте размещение байтов для поля, обозначенного символическим именем perem_3.
Оставшуюся часть сегмента данных вы можете теперь проанализировать самостоятельно.
Остановимся лишь на двух специфических особенностях использования директив резервирования и инициализации памяти. Речь идет о случае использования в поле операндов директив dw и dd символического имени из поля имя этой или другой директивы резервирования и инициализации памяти. В нашем примере сегмента данных это директивы с именами adr и adr_full.
Когда транслятор встречает директивы описания памяти с подобными операндами, то он формирует в памяти значения адресов тех переменных, чьи имена были указаны в качестве операндов. В зависимости от директивы, применяемой для получения такого адреса, формируется либо полный адрес (директива dd) в виде двух байтов сегментного адреса и двух байтов смещения, либо только смещение (директива dw). Найдите в дампе на рис. 4 поля, соответствующие именам adr и adr_full, и проанализируйте их содержимое.

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

  1. Сегмент (seg) — адрес начала сегмента, содержащего переменную;
  2. Смещение (offset) в байтах от начала сегмента с переменной;
  3. Тип (type) — определяет количество памяти, выделяемой переменной в соответствии с директивой объявления переменной.

Получить и использовать значение этих атрибутов в программе можно с помощью рассмотренных нами операторов ассемблера seg, offset и type.

TASM поддерживает следующие сложные типы данных:

  • массивы;
  • структуры;
  • объединения;
  • записи.

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

Массивы

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

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

  • Как описать массив в программе?
  • Как инициализировать массив, то есть как задать начальные значения его элементов?
  • Как организовать доступ к элементам массива?
  • Как организовать массивы с размерностью более одной?
  • Как организовать выполнение типовых операций с массивами?

Описание и инициализация массива в программе

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

  1. Перечислением элементов массива в поле операндов одной из директив описания данных. При перечислении элементы разделяются запятыми. К примеру:

    ;массив из 5 элементов.Размер каждого
элемента 4 байта:
mas dd 1,2,3,4,5
  2. Используя оператор повторения dup. К примеру:

;массив из 5 нулевых элементов.
;Размер каждого элемента 2 байта:
mas	dw	5 dup (0)

    Такой способ определения используется для резервирования памяти с целью размещения и инициализации элементов массива.
  3. Используя директивы label и rept. Пара этих директив может облегчить описание больших массивов в памяти и повысить наглядность такого описания. Директива rept относится к макросредствам языка ассемблера и вызывает повторение указанное число раз строк, заключенных между директивой и строкой endm. К примеру, определим массив байт в области памяти, обозначенной идентификатором mas_b. В данном случае директива label определяет символическое имя mas_b, аналогично тому, как это делают директивы резервирования и инициализации памяти. Достоинство директивы label в том, что она не резервирует память, а лишь определяет характеристики объекта. В данном случае объект — это ячейка памяти. Используя несколько директив label, записанных одна за другой, можно присвоить одной и той же области памяти разные имена и разный тип, что и сделано в следующем фрагменте:

...
n=0
...
mas_b	label	byte
mas_w	label	word
rept	4
    dw	0f1f0h
endm

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

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

 Листинг 2 Инициализация массива в цикле
;prg_12_1.asm
MASM
MODEL small
STACK 256
.data
mes	db	0ah,0dh,'Массив- ','$'
mas	db	10 dup (?) ;исходный массив
i	db	0
.code
main:
    mov	ax,@data
    mov	ds,ax
    xor	ax,ax	;обнуление ax
    mov	cx,10	;значение счетчика цикла в cx
    mov	si,0	;индекс начального элемента в cx
go:				;цикл инициализации
    mov	bh,i	;i в bh
    mov	mas[si],bh	;запись в массив i
    inc	i		;инкремент i
    inc	si		;продвижение к следующему элементу массива
    loop	go	;повторить цикл
;вывод на экран получившегося массива
    mov	cx,10
    mov	si,0
    mov	ah,09h
    lea	dx,mes
    int	21h
show:
    mov	ah,02h	;функция вывода значения из al на экран
    mov	dl,mas[si]
    add	dl,30h	;преобразование числа в символ
    int	21h
    inc	si
    loop	show
exit:
    mov	ax,4c00h	;стандартный выход
    int	21h
end	main		;конец программы

Доступ к элементам массива

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

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

Давайте еще раз обратимся к описанию массива. К примеру, в программе статически определена последовательность данных:
mas	dw	0,1,2,3,4,5

Пусть эта последовательность чисел трактуется как одномерный массив. Размерность каждого элемента определяется директивой dw, то есть она равна 2 байта. Чтобы получить доступ к третьему элементу, нужно к адресу массива прибавить 6. Нумерация элементов массива в ассемблере начинается с нуля.
То есть в нашем случае речь, фактически, идет о 4-м элементе массива — 3, но об этом знает только программист; микропроцессору в данном случае все равно — ему нужен только адрес.

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

база + (индекс*размер элемента)

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

  • индексная адресация со смещением — режим адресации, при котором эффективный адрес формируется из двух компонентов:
    • постоянного (базового) — указанием прямого адреса массива в виде имени идентификатора, обозначающего начало массива;
    • переменного (индексного) — указанием имени индексного регистра.
      К примеру:
      mas	dw	0,1,2,3,4,5
...
    mov	si,4
;поместить 3-й элемент массива mas в регистр ax:
    mov	ax,mas[si]
  • базовая индексная адресация со смещением — режим адресации, при котором эффективный адрес формируется максимум из трех компонентов:
    • постоянного (необязательный компонент), в качестве которой может выступать прямой адрес массива в виде имени идентификатора, обозначающего начало массива, или непосредственное значение;
    • переменного (базового) — указанием имени базового регистра;
    • переменного (индексного) — указанием имени индексного регистра.

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

Напомним, что в качестве базового регистра может использоваться любой из восьми регистров общего назначения. В качестве индексного регистра также можно использовать любой регистр общего назначения, за исключением esp/sp.

Микропроцессор позволяет масштабировать индекс. Это означает, что если указать после имени индексного регистра знак умножения “*” с последующей цифрой 2, 4 или 8, то содержимое индексного регистра будет умножаться на 2, 4 или 8, то есть масштабироваться.

Применение масштабирования облегчает работу с массивами, которые имеют размер элементов, равный 2, 4 или 8 байт, так как микропроцессор сам производит коррекцию индекса для получения адреса очередного элемента массива. Нам нужно лишь загрузить в индексный регистр значение требуемого индекса (считая от 0). Кстати сказать, возможность масштабирования появилась в микропроцессорах Intel, начиная с модели i486. По этой причине в рассматриваемом здесь примере программы стоит директива .486. Ее назначение, как и ранее использовавшейся директивы .386, в том, чтобы указать ассемблеру при формировании машинных команд на необходимость учета и использования дополнительных возможностей системы команд новых моделей микропроцессоров.

В качестве примера использования масштабирования рассмотрим листинг 3, в котором просматривается массив, состоящий из слов, и производится сравнение этих элементов с нулем. Выводится соответствующее сообщение.
 Листинг 3. Просмотр массива слов с использованием
масштабирования
;prg_12_2.asm
MASM
MODEL	small
STACK	256
.data	;начало сегмента данных
;тексты сообщений:
mes1	db	'не равен 0!$',0ah,0dh
mes2	db	'равен 0!$',0ah,0dh
mes3	db	0ah,0dh,'Элемент $'
mas	dw	2,7,0,0,1,9,3,6,0,8	;исходный массив
.code
.486		;это обязательно
main:
    mov	ax,@data
    mov	ds,ax	;связка ds с сегментом данных
    xor	ax,ax	;обнуление ax
prepare:
    mov	cx,10	;значение счетчика цикла в cx
    mov	esi,0	;индекс в esi
compare:
    mov	dx,mas[esi*2]	;первый элемент массива в dx
    cmp	dx,0	;сравнение dx c 0
    je	equal	;переход, если равно
not_equal:		;не равно
    mov	ah,09h	;вывод сообщения на экран
    lea	dx,mes3
    int	21h
    mov	ah,02h	;вывод номера элемента массива на экран
    mov	dx,si
    add	dl,30h
    int	21h
    mov	ah,09h
    lea	dx,mes1
    int	21h
    inc	esi		;на следующий элемент
    dec	cx		;условие для выхода из цикла
    jcxz	exit	;cx=0? Если да — на выход
    jmp	compare	;нет — повторить цикл
equal:			;равно 0
    mov	ah,09h	;вывод сообщения mes3 на экран
    lea	dx,mes3
    int	21h
    mov	ah,02h
    mov	dx,si
    add	dl,30h
    int	21h
    mov	ah,09h	;вывод сообщения mes2 на экран
    lea	dx,mes2
    int	21h
    inc	esi		;на следующий элемент
    dec	cx		;все элементы обработаны?
    jcxz	exit
    jmp	compare
exit:
    mov	ax,4c00h	;стандартный выход
    int	21h
end	main	;конец программы

Еще несколько слов о соглашениях:

  • Если для описания адреса используется только один регистр, то речь идет о базовой адресации и этот регистр рассматривается как базовый:
    ;переслать байт из области данных, адрес
которой находится в регистре ebx:
 mov al,[ebx]
  • Если для задания адреса в команде используется прямая адресация (в виде идентификатора) в сочетании с одним регистром, то речь идет об индексной адресации. Регистр считается индексным, и поэтому можно использовать масштабирование для получения адреса нужного элемента массива:
     add eax,mas[ebx*4]
;сложить содержимое eax с двойным словом в памяти
;по адресу mas + (ebx)*4
  • Если для описания адреса используются два регистра, то речь идет о базово-индексной адресации. Левый регистр рассматривается как базовый, а правый — как индексный. В общем случае это не принципиально, но если мы используем масштабирование с одним из регистров, то он всегда является индексным. Но лучше придерживаться определенных соглашений.
    Помните, что применение регистров ebp/bp и esp/sp по умолчанию подразумевает, что сегментная составляющая адреса находится в регистре ss.

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

К примеру:
    mov	ax,mas[ebx][ecx*2]
;адрес операнда равен [mas+(ebx)+(ecx)*2]
...
    sub	dx,[ebx+8][ecx*4]
;адрес операнда равен [(ebx)+8+(ecx)*4]

Но имейте в виду, что масштабирование эффективно лишь тогда, когда размерность элементов массива равна 2, 4 или 8 байт. Если же размерность элементов другая, то организовывать обращение к элементам массива нужно обычным способом, как описано ранее.

Рассмотрим пример работы с массивом из пяти трехбайтовых элементов (листинг 4). Младший байт в каждом из этих элементов представляет собой некий счетчик, а старшие два байта — что-то еще, для нас не имеющее никакого значения. Необходимо последовательно обработать элементы данного массива, увеличив значения счетчиков на единицу.
 Листинг 4. Обработка массива элементов с нечетной длиной
;prg_11_3.asm
MASM
MODEL	small	;модель памяти
STACK	256	;размер стека
.data			;начало сегмента данных
N=5				;количество элементов массива
mas	db	5 dup (3 dup (0))
.code			;сегмент кода
main:			;точка входа в программу
    mov	ax,@data
    mov	ds,ax
    xor	ax,ax	;обнуление ax
    mov	si,0	;0 в si
    mov	cx,N	;N в cx
go:
    mov	dl,mas[si]	;первый байт поля в dl
    inc	dl			;увеличение dl на 1 (по условию)
    mov	mas[si],dl	;заслать обратно в массив
    add	si,3	;сдвиг на следующий элемент массива
    loop	go	;повтор цикла
    mov	si,0	;подготовка к выводу на экран
    mov	cx,N
show:			;вывод на экран содержимого
;первых байт полей
    mov	dl,mas[si]
    add	dl,30h
    mov	ah,02h
    int	21h
    loop	show
exit:
    mov	ax,4c00h	;стандартный выход
    int	21h
end	main			;конец программы

Двухмерные массивы

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

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

Если последовательность однотипных элементов в памяти трактуется как двухмерный массив, расположенный по строкам, то адрес элемента (i, j) вычисляется по формуле

(база + количество_элементов_в_строке * размер_элемента * i+j)

Здесь i = 0...n–1 указывает номер строки, а j = 0...m–1 указывает номер столбца.

Например, пусть имеется массив чисел (размером в 1 байт) mas(i, j) с размерностью 4 на 4
(i= 0...3, j = 0...3):

    23	04	05	67
    05	06	07	99
    67	08	09	23
    87	09	00	08

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

23 04 05 67 05 06 07 99 67 08 09 23 87 09 00 08

Если мы хотим трактовать эту последовательность как двухмерный массив, приведенный выше, и извлечь, например, элемент
mas(2, 3) = 23, то проведя нехитрый подсчет, убедимся в правильности наших рассуждений:

Эффективный адрес mas(2, 3) = mas + 4 * 1 * 2 + 3 = mas + 11

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

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

  • сочетание прямого адреса, как базового компонента адреса, и двух индексных регистров для хранения индексов:
    mov ax,mas[ebx][esi]
  • сочетание двух индексных регистров, один из которых является и базовым и индексным одновременно, а другой — только индексным:
    mov ax,[ebx][esi]

В программе это будет выглядеть примерно так:
;Фрагмент программы выборки элемента
;массива mas(2,3) и его обнуления
.data
mas	db
23,4,5,67,5,6,7,99,67,8,9,23,87,9,0,8
i=2
j=3
.code
...
    mov	si,4*1*i
    mov	di,j
    mov	al,mas[si][di]	;в al элемент mas(2,3)
...

В качестве законченного примера рассмотрим программу поиска элемента в двухмерном массиве чисел (листинг 5). Элементы массива заданы статически.
Листинг 5. Поиск элемента в двухмерном массиве
;prg_11_4.asm
MASM
MODEL	small
STACK	256
.data
;матрица размером 2x5 — если ее не инициализировать,
;то для наглядности она может быть описана так:
;array	dw	2 DUP (5 DUP (?))
;но мы ее инициализируем:
array	dw	1,2,3,4,5,6,7,3,9,0
;логически это будет выглядеть так:
;array= {1 2}
; {3 4}
; {5 6}
; {7 3}
; {9 0}
    elem	dw	3	;элемент для поиска
failed	db	0ah,0dh,'Нет такого элемента в массиве!','$'
success	db	0ah,0dh,'Такой элемент в массиве присутствует ','$'
foundtime	db	?	;количество найденных элементов
fnd	db	' раз(а)',0ah,0dh,'$'
.code
main:
    mov	ax,@data
    mov	ds,ax
    xor	ax,ax
    mov	si,0	;si=столбцы в матрице
    mov	bx,0	;bx=строки в матрице
    mov	cx,5	;число для внешнего цикла (по строкам)
external:		;внешний цикл по строкам
    mov	ax,array[bx][si]	;в ax первый элемент матрицы
    push	cx	;сохранение в стеке счётчика внешнего цикла
    mov	cx,2	;число для внутреннего цикла (по столбцам)
    mov	si,0
iternal:		;внутренний цикл по строкам
    inc	si		;передвижение на следующий элемент в строке
;сравниваем содержимое текущего элемента в ax с искомым элементом:
    cmp	ax,elem
;если текущий совпал с искомым, то переход на here для обработки,
;иначе цикл продолжения поиска
    je	here
;иначе — цикл по строке cx=2 раз
    loop	iternal
here:
    jcxz	move_next	;просмотрели строку?
    inc	foundtime	;иначе увеличиваем счётчик совпавших
move_next:			;продвижение в матрице
    pop	cx			;восстанавливаем CX из стека (5)
    add	bx,1		;передвигаемся на следующую строку
    loop	external	;цикл (внешний)
    cmp	foundtime,0h	;сравнение числа совпавших с 0
    ja	eql			;если больше 0, то переход
not_equal:	;нет элементов, совпавших с искомым
    mov	ah,09h	;вывод сообщения на экран
    mov	dx,offset failed
    int	21h
    jmp	exit	;на выход
eql:	;есть элементы, совпавшие с искомым
    mov	ah,09h	;вывод сообщений на экран
    mov	dx,offset success
    int	21h
    mov	ah,02h
    mov	dl,foundtime
    add	dl,30h
    int	21h
    mov	ah,09h
    mov	dx,offset fnd
    int	21h
exit:				;выход
    mov	ax,4c00h	;стандартное завершение программы
    int	21h
end	main			;конец программы

При анализе работы программы не забывайте, что в языке ассемблера принято элементы массива нумеровать с 0. При поиске определенного элемента массив просматривается от начала и до конца.
Приведенная программа сохраняет в поле foundtime количество вхождений искомого элемента в массив. В качестве индексных регистров используются si и bx.

Типовые операции с массивами

Для демонстрации основных приемов работы с массивами лучше всего подходят программы поиска или сортировки.

Рассмотрим одну такую программу, выполняющую сортировку массива по возрастанию (листинг 6).
Листинг 6. Сортировка массива
<1> ;prg_12_5.asm
<2> MASM
<3> MODEL	small
<4> STACK	256
<5> .data
<6> mes1	db	0ah,0dh,'Исходный массив — $',0ah,0dh
<7> ;некоторые сообщения
<8> mes2	db	0ah,0dh,'Отсортированный массив — $',0ah,0dh
<9> n	equ	9			;количество элементов в массиве, считая с 0
<10> mas	dw	2,7,4,0,1,9,3,6,5,8		;исходный массив
<11> tmp	dw	0		;переменные для работы с массивом
<12> i	dw	0
<13> j	dw	0
<14> .code
<15> main:
<16>	mov	ax,@data
<17>	mov	ds,ax
<18>	xor	ax,ax
<19> ;вывод на экран исходного массива
<20>	mov	ah,09h
<21>	lea	dx,mes1
<22>	int	21h			;вывод сообщения mes1
<23>	mov	cx,10
<24>	mov	si,0
<25>	show_primary:	;вывод значения элементов
<26> ;исходного массива на экран
<27>	mov	dx,mas[si]
<28>	add	dl,30h
<29>	mov	ah,02h
<30>	int	21h
<31>	add	si,2
<32>	loop	show_primary
<33>
<34> ;строки 40-85 программы эквивалентны следующему коду на языке С:
<35> ;for (i=0;i<9;i++)
<36> ; for (j=9;j>i;j--)
<37> ; if (mas[i]>mas[j])
<38> ; {tmp=mas[i];
<39> ; mas[i]=mas[j];
<40> ; mas[j]=tmp;}
<41>	mov	i,0			;инициализация i
<42> ;внутренний цикл по j
<43> internal:
<44>	mov	j,9			;инициализация j
<45>	jmp	cycl_j		;переход на тело цикла
<46> exchange:
<47>	mov	bx,i		;bx=i
<48>	shl	bx,1
<49>	mov	ax,mas[bx]	;ax=mas[i]
<50>	mov	bx,j		;bx=j
<51>	shl	bx,1
<52>	cmp	ax,mas[bx]	;mas[i] ? mas[j] — сравнение элементов
<53>	jle	lesser		;если mas[i] меньше, то обмен не нужен и
     ;переход на продвижение далее по массиву
<54> ;иначе tmp=mas[i], mas[i]=mas[j], mas[j]=tmp:
<55> ;tmp=mas[i]
<56>	mov	bx,i		;bx=i
<57>	shl	bx,1		;умножаем на 2, так как элементы — слова
<58>	mov	tmp,ax		;tmp=mas[i]
<59>
<60> ;mas[i]=mas[j]
<61>	mov	bx,j		;bx=j
<62>	shl	bx,1		;умножаем на 2, так как элементы — слова
<63>	mov	ax,mas[bx]	;ax=mas[j]
<64>	mov	bx,i		;bx=i
<65>	shl	bx,1		;умножаем на 2, так как элементы — слова
<66>	mov	mas[bx],ax	;mas[i]=mas[j]
<67>
<68> ;mas[j]=tmp
<69>	mov	bx,j		;bx=j
<70>	shl	bx,1		;умножаем на 2, так как элементы — слова
<71>	mov	ax,tmp		;ax=tmp
<72>	mov	mas[bx],ax	;mas[j]=tmp
<73> lesser:			;продвижение далее по массиву во внутреннем цикле
<74>	dec	j			;j--
<75>;тело цикла по j
<76> cycl_j:
<77>	mov	ax,j		;ax=j
<78>	cmp	ax,i		;сравнить j ? i
<79>	jg	exchange	;если j>i, то переход на обмен
<80> ;иначе на внешний цикл по i
<81>	inc	i			;i++
<82>	cmp	i,n			;сравнить i ? n — прошли до конца массива
<83>	jl	internal	;если i
<85> ;вывод отсортированного массива
<86>	mov	ah,09h
<87>	lea	dx,mes2
<88>	int	21h
<89> prepare:
<90>	mov	cx,10
<91>	mov	si,0
<92>	show:			;вывод значения элемента на экран
<93>	mov	dx,mas[si]
<94>	add	dl,30h
<95>	mov	ah,02h
<96>	int	21h
<97>	add	si,2
<98>	loop	show
<99> exit:
<100>	mov	ax,4c00h	;стандартный выход
<101>	int	21h
<102>	end	main	;конец программы

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

Структуры

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

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

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

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

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

Описать структуру в программе можно только один раз, а определить — любое количество раз.

Описание шаблона структуры

Описание шаблона структуры имеет следующий синтаксис:
имя_структуры	STRUC
    <описание полей>
имя_структуры	ENDS

Здесь <описание полей> представляет собой последовательность директив описания данных db, dw, dd, dq и dt.
Их операнды определяют размер полей и, при необходимости, начальные значения. Этими значениями будут, возможно, инициализироваться соответствующие поля при определении структуры.

Как мы уже отметили при описании шаблона, память не выделяется, так как это всего лишь информация для транслятора.

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

Рассмотрим работу со структурами на примере моделирования базы данных о сотрудниках некоторого отдела.
Для простоты, чтобы уйти от проблем преобразования информации при вводе, условимся, что все поля символьные.
Определим структуру записи этой базы данных следующим шаблоном:
worker	struc	;информация о сотруднике
nam	db	30	dup	(' ')	;фамилия, имя, отчество
sex	db	'м'	;пол, по умолчанию 'м' — мужской
position	db	30 dup (' ')	;должность
age	db	2 dup(‘ ’)		;возраст
standing	db	2 dup(‘ ’)	;стаж
salary	db	4 dup(‘ ’)		;оклад в рублях
birthdate	db	8 dup(‘ ’)	;дата рождения
worker	ends

Определение данных с типом структуры

Для использования описанной с помощью шаблона структуры в программе необходимо определить переменную с типом данной структуры. Для этого используется следующая синтаксическая конструкция:
[имя переменной] имя_структуры <[список значений]>

Здесь:

  • имя переменной — идентификатор переменной данного структурного типа.
    Задание имени переменной необязательно. Если его не указать, будет просто выделена область памяти размером в сумму длин всех элементов структуры.
  • список значений — заключенный в угловые скобки список начальных значений элементов структуры, разделенных запятыми.
    Его задание также необязательно.
    Если список указан не полностью, то все поля структуры для данной переменной инициализируются значениями из шаблона, если таковые заданы.
    Допускается инициализация отдельных полей, но в этом случае пропущенные поля должны отделяться запятыми. Пропущенные поля будут инициализированы значениями из шаблона структуры. Если при определении новой переменной с типом данной структуры мы согласны со всеми значениями полей в ее шаблоне (то есть заданными по умолчанию), то нужно просто написать угловые скобки.
    К примеру: victor worker <>.

Для примера определим несколько переменных с типом описанной выше структуры.
data	segment
sotr1	worker	<’Гурко Андрей Вячеславович’,,’художник’,’33’,‘15’,‘1800’,’26.01.64’<
sotr2	worker	<’Михайлова Наталья Геннадьевна’,’ж’,’программист’,’30’,’10’,’1680’,’27.10.58’<
sotr3	worker <’Степанов Юрий Лонгинович’,,’художник’,’38’,’20’,’1750’,’01.01.58’<
sotr4	worker	<’Юрова Елена Александровна’,’ж’,’свяэист’,’32’,’2’,,’09.01.66’<
sotr5	worker	<>	;здесь все значения по умолчанию
data ends

Методы работы со структурой

Идея введения структурного типа в любой язык программирования состоит в объединении разнотипных переменных в один объект.
В языке должны быть средства доступа к этим переменным внутри конкретного экземпляра структуры. Для того чтобы сослаться в команде на поле некоторой структуры, используется специальный оператор — символ "." (точка). Он используется в следующей синтаксической конструкции:
 адресное_выражение.имя_поля_структуры

Здесь:

  • адресное_выражение — идентификатор переменной некоторого структурного типа или выражение в скобках в соответствии с указанными ниже синтаксическими правилами (рис. 1);
  • имя_поля_структуры — имя поля из шаблона структуры.
    Это, на самом деле, тоже адрес, а точнее, смещение поля от начала структуры.

Таким образом оператор "." (точка) вычисляет выражение
 (адресное_выражение) + (имя_поля_структуры)

Рис. 5. Синтаксис адресного выражения в операторе обращения к полю структуры

Продемонстрируем на примере определенной нами структуры worker некоторые приемы работы со структурами.
К примеру, извлечь в ax значения поля с возрастом. Так как вряд ли возраст трудоспособного человека будет больше величины 99 лет, то после помещения содержимого этого символьного поля в регистр ax его будет удобно преобразовать в двоичное представление командой aad.
Будьте внимательны, так как из-за принципа хранения данных “младший байт по младшему адресу” старшая цифра возраста будет помещена в al, а младшая — в ah.
Для корректировки достаточно использовать команду xchg al,ah:
    mov	ax,word ptr sotr1.age	;в al возраст sotr1
    xchg	ah,al

а можно и так:

    lea	bx,sotr1
    mov	ax,word ptr [bx].age
    xchg	ah,al

Давайте представим, что сотрудников не четверо, а намного больше, и к тому же их число и информация о них постоянно меняются. В этом случае теряется смысл явного определения переменных с типом worker для конкретных личностей.
Язык ассемблера разрешает определять не только отдельную переменную с типом структуры, но и массив структур.
К примеру, определим массив из 10 структур типа worker:
mas_sotr	worker	10 dup (<>)

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

Аналогично другим идентификаторам, определенным в программе, транслятор назначает имени типа структуры и имени переменной с типом структуры атрибут типа. Значением этого атрибута является размер в байтах, занимаемый полями этой структуры. Извлечь это значение можно с помощью оператор type.
После того как стал известен размер экземпляра структуры, организовать индексацию в массиве структур не представляет особой сложности.
К примеру:
worker	struc
...
worker	ends
...
mas_sotr	worker	10 dup (<>)
...
    mov	bx,type worker	;bx=77
    lea	di,mas_sotr
;извлечь и вывести на экран пол всех сотрудников:
    mov	cx,10
cycl:
    mov	al,[di].sex
...
;вывод на экран содержимого поля sex структуры worker
    add	di,bx	;к следующей структуре в массиве mas_sort
    loop	cycl

Как выполнить копирование поля из одной структуры в соответствующее поле другой структуры? Или как выполнить копирование всей структуры? Давайте выполним копирование поля nam третьего сотрудника в поле nam пятого сотрудника:
worker	struc
...
worker	ends
...
mas_sotr	worker	10 dup (<>)
...
    mov	bx,offset mas_sotr
    mov	si,(type worker)*2	;si=77*2
    add	si,bx
    mov	di,(type worker)*4	;si=77*4
    add	di,bx
    mov	cx,30
rep	movsb

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

Наличие в языке следующих двух типов данных, наверное, объясняется стремлением “хозяйки” максимально эффективно использовать рабочую площадь стола (оперативной памяти) при приготовлении еды или для размещения продуктов (данных программы).

Объединения

Представим ситуацию, когда мы используем некоторую область памяти для размещения некоторого объекта программы (переменной, массива или структуры). Вдруг после некоторого этапа работы у нас отпала надобность в использовании этих данных. Обычно память останется занятой до конца работы программы. Конечно, в принципе, ее можно было бы использовать для хранения других переменных, но при этом без принятия специальных мер нельзя изменить тип и имя. Неплохо было бы иметь возможность переопределить эту область памяти для объекта с другим типом и именем. Язык ассемблера предоставляет такую возможность в виде специального типа данных, называемого объединением.

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

Описание объединений в программе напоминает описание структур, то есть сначала описывается шаблон, в котором с помощью директив описания данных перечисляются имена и типы полей:
имя_объединения	UNION
    <описание полей>
имя_объединения	ENDS

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

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

  • 16-битного смещения;
  • 32-битного смещения;
  • пары из 16-битного смещения и 16-битной сегментной составляющей адреса;
  • в виде пары из 32-битного смещения и 16-битного селектора.

Какие из этих указателей можно применять в конкретной ситуации, зависит от режима адресации (use16 или use32) и режима работы микропроцессора.
Так вот, описанный в листинге 7 шаблон объединения позволяет нам облегчить формирование и использование указателей различных типов.
Листинг 7 Пример использования объединения
masm
model	small
stack	256
.586P
pnt	struc		;структура pnt, содержащая вложенное объединение
union			;описание вложенного в структуру объединения
offs_16	dw	?
offs_32	dd	?
ends			;конец описания объединения
segm	dw	?
ends			;конец описания структуры
.data
point	union	;определение	объединения, содержащего вложенную структуру
off_16	dw	?
off_32	dd	?
point_16	pnt	<>
point_32	pnt	<>
point	ends
tst	db	"Строка для тестирования"
adr_data	point <> ;определение экземпляра объединения
.code
main:
    mov	ax,@data
    mov	ds,ax
    mov	ax,seg tst
;записать адрес сегмента строки tst в поле структуры adr_data
    mov	adr_data.point_16.segm,ax
;когда понадобится, можно извлечь значение из этого поля обратно, к примеру, в регистр bx:
    mov	bx,adr_data.point_16.segm
;формируем смещение в поле структуры adr_data
    mov	ax,offset tst	;смещение строки в ax
    mov	adr_data.point_16.offs_16,ax
;аналогично, когда понадобится, можно извлечь значение из этого поля:
    mov	bx,adr_data.point_16.offs_16
exit:
    mov	ax,4c00h
    int	21h
end	main

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

Записи

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

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

Запись — структурный тип данных, состоящий из фиксированного числа элементов длиной от одного до нескольких бит.
При описании записи для каждого элемента указывается его длина в битах и, что необязательно, некоторое значение.
Суммарный размер записи определяется суммой размеров ее полей и не может быть более 8, 16 или 32 бит.
Если суммарный размер записи меньше указанных значений, то все поля записи “прижимаются” к младшим разрядам.

Использование записей в программе, так же, как и структур, организуется в три этапа:

  1. Задание шаблона записи, то есть определение набора битовых полей, их длин и, при необходимости, инициализация полей.
  2. Определение экземпляра записи. Так же, как и для структур, этот этап подразумевает инициализацию конкретной переменной типом заранее определенной с помощью шаблона записи.
  3. Организация обращения к элементам записи.

Компилятор TASM, кроме стандартных средств обработки записей, поддерживает также и некоторые дополнительные возможности их обработки.

Описание записи

Описание шаблона записи имеет следующий синтаксис (рис. 6):
имя_записи RECORD <описание элементов>

Здесь:
<описание элементов> представляет собой последовательность описаний отдельных элементов записи согласно синтаксической диаграмме (см. рис. 6):

Рис. 6. Синтаксис описания шаблона записи

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

Определение экземпляра записи

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

Рис. 7. Синтаксис описания экземпляра записи

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

Если инициализировать поля не требуется, то достаточно указать ? при определении экземпляра записи:
...
iotest	record
i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
    ...
flag	iotest	?

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

Если требуется частичная инициализация элементов, то они заключаются в угловые (< и >) или фигурные ({ и }) скобки.
Различие здесь в том, что в угловых скобках элементы должны быть заданы в том же порядке, что и в определении записи. Если значение некоторого элемента совпадает с начальным, то его можно не указывать, но обязательно обозначить его запятой. Для последних элементов идущие подряд запятые можно опустить.
К примеру, согласиться со значениями по умолчанию можно так:
iotest	record
i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
...
flag	iotest	<>	;согласились со значением по умолчанию

Изменить значение поля i2 можно так:
iotest	record
i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
...
flag	iotest	<,10,>	; переопределили i2

Применяя фигурные скобки, также можно указать выборочную инициализацию полей, но при этом необязательно обозначать запятыми поля, со значениями по умолчанию которых мы согласны:
iotest	record
i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
...
flag	iotest	{i2=10}	;переопределили i2, не обращая внимания на порядок
;следования других компонентов записи

Работа с записями

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

  • Каждому имени элемента записи ассемблер присваивает числовое значение, равное количеству сдвигов вправо, которые нужно произвести для того, чтобы этот элемент оказался “прижатым” к началу ячейки. Это дает нам возможность локализовать его и работать с ним. Но для этого нужно знать длину элемента в битах.
  • Сдвиг вправо производится с помощью команды сдвига shr.
  • Ассемблер содержит оператор width, который позволяет узнать размер элемента записи в битах или полностью размер записи. Варианты применения оператора width:
        width	имя_элемента_записи	;значением оператора будет размер элемента в битах.

        width	имя_экземпляра_записи
или
    width	имя_типа_записи	;значением оператора будет размер всей записи в битах.

        mov	al,width i2
...
    mov	ax,width iotest
  • Ассемблер содержит оператор mask, который позволяет локализовать биты нужного элемента записи. Эта локализация производится путем создания маски, размер которой совпадает с размером записи. В этой маске обнулены биты на всех позициях, за исключением тех, которые занимает элемент в записи.
  • Сами действия по преобразованию элементов записи производятся с помощью логических команд.

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

Выделение элемента записи:
  • Поместить запись во временную память — регистр (8, 16 или 32-битный в зависимости от размера записи).
  • Получить битовую маску, соответствующую элементу записи, с помощью оператора mask.
  • Локализовать биты в регистре с помощью маски и команды and.
  • Сдвинуть биты элемента к младшим разрядам регистра командой shr. Число разрядов для сдвига получить с использованием имени элемента записи.

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

Работа с элементом записи:

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

Помещение измененного элемента на его место в запись:
  • Используя имя элемента записи в качестве счетчика сдвигов, сдвинуть влево биты элемента записи.
  • Если вы не уверены в том, что разрядность результата преобразований не превысила исходную, можно выполнить “обрезание” лишних битов, используя команду and и маску элемента.
  • Подготовить исходную запись к вставке измененного элемента путем обнуления битов в записи на месте этого элемента. Это можно сделать путем наложения командой and инвертированной маски элемента записи на исходную запись.
  • С помощью команды or наложить значение в регистре на исходную запись.

В качестве примера рассмотрим листинг 8, который обнуляет поле i2 в записи iotest.
Листинг 8. Работа с полем записи
;prg_12_7.asm
masm
model	small
stack	256
iotest	record	i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
.data
flag	iotest	<>
.code
main:
    mov	ax,@data
    mov	ds,ax
    mov	al,mask i2
    shr	al,i2	;биты i2 в начале ax
    and	al,0fch	;обнулили i2
;помещаем i2 на место
    shl	al,i2
    mov	bl,[flag]
    xor	bl,mask i2	;сбросили i2
    or	bl,al	;наложили
exit:
    mov	ax,4c00h	;стандартный выход
    int	21h
end	main		;конец программы

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

Записи: дополнительные возможности обработки

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

Для установки значения некоторого поля записи используется команда setfield с синтаксисом:
    setfield	имя_элемента_записи назначение,регистр_источник

Для выборки значения некоторого поля записи используется команда getfield с синтаксисом:
    getfield	имя_элемента_записи регистр_назначение, источник

Работа команды setfield заключается в следующем. Местоположение записи определяется операндом назначение, который может представлять собой имя регистра или адрес памяти.
Операнд имя_элемента_записи определяет элемент записи, с которым ведется работа (по сути, если вы были внимательны, он определяет смещение элемента в записи относительно младшего разряда). Новое значение, в которое необходимо установить указанный элемент записи, должно содержаться в операнде регистр_источник. Обрабатывая данную команду, транслятор генерирует последовательность команд, которые выполняют следующие действия:

  • сдвиг содержимого регистр_источник влево на количество разрядов, соответствующее расположению элемента в записи;
  • логическую операцию or над операндами назначение и регистр_источник. Результат операции помещается в операнд назначение.

Важно отметить, что setfield не производит предварительной очистки элемента, в результате после логического сложения командой or возможно наложение старого содержимого элемента и нового устанавливаемого значения. Поэтому требуется предварительно подготовить поле в записи путем его обнуления.

Действие команды getfield обратно setfield. В качестве операнда источник может быть указан либо регистр либо адрес памяти.
В регистр, указанный операндом регистр_назначение, помещается результат работы команды — значение элемента записи.
Интересная особенность связана с регистр_назначение. Команда getfield всегда использует 16-битный регистр, даже если вы укажете в этой команде имя 8-битного регистра.

В качестве примера применения команд setfield и getfield рассмотрим листинг 9.
 Листинг 9. Работа с полями записи
;prg_12_8.asm
masm
model	small
stack	256
iotest	record
i1:1,i2:2=11,i3:1,i4:2=11,i5:2=00
.data
flag	iotest	<>
.code
main:
    mov	ax,@data
    mov	ds,ax
    mov	al,flag
    mov	bl,3
    setfield	i5 al,bl
    xor	bl,bl
    getfield	i5 bl,al
    mov	bl,1
    setfield	i4 al,bl
    setfield	i5 al,bl
exit:
    mov	ax,4c00h	;стандартный выход
    int	21h
end	main	;конец программы

Результат работы команд setfield и getfield удобнее всего изучать в отладчике.
При установке значений полей не производится их предварительная очистка. Это сделано специально. Для такого рода операций лучше использовать некоторые универсальные механизмы, иначе велик риск внесения ошибок, которые трудно обнаружить и исправить. В качестве такого механизма можно предложить механизм макросредств.

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

  • eflags_1_7 — младший байт eflags/flags;
  • eflags_8_15 — второй байт eflags/flags;
  • eflags_h — старшая половина eflags.
eflags_l_7	record
    sf7:1=0,zf6:1=0,c5:1=0,af4:1=0,c3:1=0,pf2:1=0,c1:=1,cf0:1=0
eflags_l_15	record
    c15:1=0,nt14:1=0,iopl:2=0,of11:1=0,df10:1=0,if9:1=1,tf8:1=0
eflags_h	record
    c:13=0,ac18:1=0,vm17:1=0,rf16:1=0

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


Описание команд микропроцессора, упорядоченное по функциональному признаку

    Команды пересылки данных
    Арифметические команды
    Логические команды
    Команды передачи управления
    Цепочечные команды
    Команды общего назначения
    Команды двоичной арифметики
    Команды манипуляции битами
    Безусловные команды
    Команды работы со стеком
    Команды десятичной арифметики
    Команды сдвига
    Условные команды
    Команды преобразования данных


Логические команды микропроцессора

    Средства логической обработки данных

    Средства логической обработки данных
      Логические
        and
        or
        xor
        not
        test
      Обработки бит
      Сканирования бит
        bsf
        bsr
      Проверки модификации бит
        bt
        btc
        btr
        bts
      Сдвига
        Обычного сдвига
          sar
          sal
          shl
          shr
          shld
          shrd
        Циклического сдвига
          rcl
          rcr
          rol
          ror
    Операции. Средства логической обработки данных
      Логические операторы
        and
        or
        xor
        not
      Операторы сдвига
        shr
        shl
      Операторы сравнения
        eq
        ne
        lt
        le
        gt
        ge


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

Макросредства языка ассемблера

  • Понятие о макросредствах языка ассемблера
  • Псевдооператоры equ и =
  • Макрокоманды
  • Макродирективы
  • Директивы условной компиляции

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

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

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

  • расширения набора директив;
  • введения некоторых дополнительных команд, не имеющих аналогов в системе команд микропроцессора. За примером далеко ходить не нужно — команды setfield и getfield, которые скрывают от программиста рутинные действия и генерируют наиболее эффективный код;
  • введения сложных типов данных.

Но это все глобальные направления, по которым развивается сам транслятор от версии к версии.
Что же делать программисту для решения его локальной задачи, для облегчения работы в определенной проблемной области?
Для этого разработчики компиляторов ассемблера включают в язык и постоянно совершенствуют аппарат макросредств. Этот аппарат является очень мощным и важным.

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

Рис. 1. Макроассемблер в общей схеме трансляции программы на TASM

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

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

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

Псевдооператоры equ и =

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

Синтаксис псевдооператора equ:
имя_идентификатора	equ	строка или числовое_выражение

Синтаксис псевдооператора “=”:
имя_идентификатора	=	числовое_выражение

Несмотря на внешнее и функциональное сходство псевдооператоры equ и “=” отличаются следующим:

  • из синтаксического описания видно, что с помощью equ идентификатору можно ставить в соответствие как числовые выражения, так и текстовые строки, а псевдооператор “=” может использоваться только с числовыми выражениями;
  • идентификаторы, определенные с помощью “=”, можно переопределять в исходном тексте программы, а определенные с использованием equ — нельзя.

Ассемблер всегда пытается вычислить значение строки, воспринимая ее как выражение. Для того чтобы строка воспринималась именно как текстовая, необходимо заключить ее в угловые скобки: <строка>.
Кстати сказать, угловые скобки являются оператором ассемблера, с помощью которого транслятору сообщается, что заключенная в них строка должна трактоваться как текст, даже если в нее входят служебные слова ассемблера или операторы. Хотя в режиме Ideal это не обязательно, так как строка для equ в нем всегда трактуется как текстовая.

Псевдооператор equ удобно использовать для настройки программы на конкретные условия выполнения, замены сложных в обозначении объектов, многократно используемых в программе, более простыми именами и т. п.
К примеру:
masm
model	small
stack	256
mas_size	equ	10	;размерность массива
akk	equ	ax		;переименовать регистр
mas_elem	equ	mas[bx][si]	;адресовать элемент массива
.data
;описание массива из 10 байт:
mas	db	mas_size dup (0)
.code
    mov	akk,@data	;фактически mov ax,@data
    mov	ds,akk		;фактически mov ds,ax
...
    mov	al,mas_elem	;фактически — mov al,mas[bx][si]

Псевдооператор “=” удобно использовать для определения простых абсолютных (то есть не зависящих от места загрузки программы в память) математических выражений.
Главное условие то, чтобы транслятор мог вычислить эти выражения во время трансляции.
К примеру:
.data
adr1	db	5 dup (0)
adr2	dw	0
len = 43
len = len+1	;можно и так, через предыдущее определение
len = adr2-adr1

Как видно из примера, в правой части псевдооператора “=” можно использовать метки и ссылки на адреса — главное, чтобы в итоге получилось абсолютное выражение.

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

Набор этих директив следующий:

  • директива слияния строк catstr:
    идентификатор catstr строка_1,строка_2,... — значением этого макроса будет новая строка, состоящая из сцепленной слева направо последовательности строк строка_1,строка_2,...
    В качестве сцепляемых строк могут быть указаны имена ранее определенных макросов.
    К примеру:
    pre	equ	Привет,
name	equ	< Юля>
privet	catstr	pre,name	;privet= “Привет, Юля”
  • директива выделения подстроки в строке substr:
    идентификатор substr строка,номер_позиции,размер — значением данного макроса будет часть заданной строки, начинающаяся с позиции с номером номер_позиции и длиной, указанной в размер.
    Если требуется только остаток строки, начиная с некоторой позиции, то достаточно указать только номер_позиции без указания размера.
    К примеру:
    ;продолжение предыдущего фрагмента:
privet	catstr	pre,name	;privet= “Привет, Юля”
name	substr	privet,7,3	;name=“Юля”
  • директива определения вхождения одной строки в другую instr:
    идентификатор instr номер_нач_позиции,строка_1,строка_2 — после обработки данного макроса транслятором идентификатору будет присвоено числовое значение, соответствующее номеру (первой) позиции, с которой совпадают строка_1 и строка_2.
    Если такого совпадения нет, то идентификатор получит значение 0;
  • директива определения длины строки в текстовом макросе sizestr:
    идентификатор sizestr строка — в результате обработки данного макроса значение идентификатор устанавливается равным длине строки.
    ;как продолжение предыдущего фрагмента:
privet	catstr	pre,name	;privet= “Привет, Юля”
len	sizestr	privet	;len=10

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

Макрокоманды

Идейно макрокоманда представляет собой дальнейшее развитие механизма замены текста.
С помощью макрокоманд в текст программы можно вставлять последовательности строк (которые логически могут быть данными или командами) и даже более того — привязывать их к контексту места вставки.

Представим ситуацию, когда необходимо выполнить некоторые повторяющиеся действия. Программа из листинга 1 является ярким этому примером.
Листинг 1. Пример программы на ассемблере
<1> ;---------Prg_3_1.asm----------------------------------
<2> ;Программа преобразования двузначного шестнадцатеричного числа
<3> ;в символьном виде в двоичное представление.
<4> ;Вход: исходное шестнадцатеричное число из двух цифр,
<5> ;вводится с клавиатуры.
<6> ;Выход: результат преобразования должен
<7> ;быть в регистре al.
<8> ;------------------------------------------------------
<9> data segment para public 'data'	;сегмент данных
<10> message	db	'Введите две шестнадцатеричные цифры,$'
<11> data ends
<12> stk segment stack
<13> 	db	256 dup ('?')	;сегмент стека
<14> stk ends
<15> code segment para public 'code'	;начало сегмента кода
<16> main	proc	;начало процедуры main
<17> 	assume cs:code,ds:data,ss:stk
<18> 	mov	ax,data	;адрес сегмента данных в регистр ax
<19> 	mov	ds,ax	;ax в ds
<20> 	mov	ah,9
<21> 	mov	dx,offset message
<22> 	int	21h
<23> 	xor	ax,ax	;очистить регистр ax
<24> 	mov	ah,1h	;1h в регистр ah
<25> 	int	21h	;генерация прерывания с номером 21h
<26> 	mov	dl,al	;содержимое регистра al в регистр dl
<27> 	sub	dl,30h	;вычитание: (dl)=(dl)-30h
<28> 	cmp	dl,9h	;сравнить (dl) с 9h
<29> 	jle	M1	;перейти на метку M1 если dl<9h или dl=9h
<30> 	sub	dl,7h	;вычитание: (dl)=(dl)-7h
<31> M1:		;определение метки M1
<32> 	mov	cl,4h	;пересылка 4h в регистр cl
<33> 	shl	dl,cl	;сдвиг содержимого dl на 4 разряда влево
<34> 	int	21h	;вызов прерывания с номером 21h
<35> 	sub	al,30h	;вычитание: (dl)=(dl)-30h
<36> 	cmp	al,9h	;сравнить (al) с 9h	28
<37> 	jle	M2	;перейти на метку M2, если al<9h или al=9h
<38> 	sub	al,7h	;вычитание: (al)=(al)-7h
<39> M2:			;определение метки M2
<40> 	add	dl,al	;сложение: (dl)=(dl)+(al)
<41> 	mov	ax,4c00h	;пересылка 4c00h в регистр ax
<42> 	int	21h	;вызов прерывания с номером 21h
<43> main endp		;конец процедуры main
<44> code ends		;конец сегмента кода
<45> end main		;конец программы с точкой входа main

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

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

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

тело макроопределения

endm

Где должны располагаться макроопределения?
Есть три варианта:

  1. В начале исходного текста программы до сегмента кода и данных с тем, чтобы не ухудшать читабельность программы.
    Этот вариант следует применять в случаях, если определяемые вами макрокоманды актуальны только в пределах одной этой программы.
  2. В отдельном файле.
    Этот вариант подходит при работе над несколькими программами одной проблемной области. Чтобы сделать доступными эти макроопределения в конкретной программе, необходимо в начале исходного текста этой программы записать директиву include имя_файла, к примеру:
    masm
model	small
include	show.inc
;в это место будет вставлен текст файла show.inc
...
  3. В макробиблиотеке.
    Если у вас есть универсальные макрокоманды, которые используются практически во всех ваших программах, то их целесообразно записать в так называемую макробиблиотеку. Сделать актуальными макрокоманды из этой библиотеки можно с помощью все той же директивы include.

Недостаток двух последних способов в том, что в исходный текст программы включаются абсолютно все макроопределения.
Для исправления ситуации можно использовать директиву purge, в качестве операндов которой через запятую перечисляются имена макрокоманд, которые не должны включаться в текст программы.
К примеру,
...
include	iomac.inc
purge	_outstr,_exit
...

В данном случае в исходный текст программы перед началом компиляции TASM вместо строки include iomac.inc вставит строки из файла iomac.inc. Но вставленный текст будет отличаться от оригинала тем, что в нем будут отсутствовать макроопределения _outstr и _exit.

А теперь вернемся к программе из листинга 1. Проанализируем ее текст, выявим повторяющиеся участки и составим для них макроопределения (листинг 2).
Листинг 2. Пример 1 создания и использования макрокоманд
<1>;prg_3_1.asm с макроопределениями
<2>init_ds	macro

<3>;Макрос настройки ds на сегмент данных
<4>	mov	ax,data
<5>	mov	ds,ax
<6>	endm

<7>out_str	macro	str
<8>;Макрос вывода строки на экран.
<9>;На входе — выводимая строка.
<10>;На выходе - сообщение на экране.
<11>	push	ax
<12>	mov	ah,09h
<13>	mov	dx,offset str
<14>	int	21h
<15>	pop	ax
<16>	endm
<17>
<18>clear_r	macro	rg
<19>;очистка регистра rg
<20>	xor	rg,rg
<21>	endm
<22>
<23>get_char	macro
<24>;ввод символа
<25>;введенный символ в al
<26>	mov	ah,1h
<27>	int	21h
<28>	endm
<29>
<30>conv_16_2	macro
<31>;макрос преобразования символа шестнадцатеричной цифры
<32>;в ее двоичный эквивалент в al
<33>	sub	dl,30h
<34>	cmp	dl,9h
<35>	jle	$+5
<36>	sub	dl,7h
<37>	endm
<38>
<39>exit	macro
<40> ;макрос конца программы
<41>	mov	ax,4c00h
<42>	int	21h
<43>	endm
<44>
<45> data	segment para public 'data'
<46> message	db	'Введите две шестнадцатеричные цифры (буквы A,B,C,D,E,F — прописные): $'
<47> data	ends
<48>
<49> stk	segment	stack
<50>	db	256 dup('?')
<51> stk	ends
<52>
<53> code	segment para public 'code'
<54>	assume	cs:code,ds:data,ss:stk
<55> main	proc
<56>	init_ds
<57>	out_str	message

<58>	clear_r	ax
<59>	get_char
<60>	mov	dl,al
<61>	conv_16_2
<62>	mov	cl,4h
<63>	shl	dl,cl
<64>	get_char
<65>	conv_16_2
<66>	add	dl,al
<67>	xchg	dl,al	;результат в al
<68>	exit
<69> main	endp
<70> code	ends
<71> end	main

В листинге 2 в строках 3–7, 9–18, 20–23, 25–30, 32–38, 40–44 описаны макроопределения. Их назначение приведено сразу после заголовка в теле каждого макроопределения.

Все эти макроопределения можно использовать и при написании других программ. Посмотрите на модернизированный исходный текст программы из листинга 3.1 в листинге 2 (строки 56–70). Если не обращать внимания на некоторые неясные моменты, то сам сегмент кода стал внешне более читабельным и даже можно сказать, что в нем появился какой то смысл.

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

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

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

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

К примеру, рассмотрим самое короткое макроопределение в листинге 2 — clear_rg.
Как отмечено выше, результаты работы макроассемблера можно узнать, просмотрев файл листинга после трансляции. Покажем несколько его фрагментов, которые демонстрируют, как был описан текст макроопределения clear_rg (строки 24-27), как был осуществлен вызов макрокоманды clear_rg с фактическим параметром ax (строка 58) и как выглядит результат работы макрогенератора, сформировавшего команду ассемблера xor ax,ax (строка 75);
24		clear_r	macro	rg
25	;очистка регистра rg
26		xor	rg,rg
27		endm
...
74		clear_r	ax
75000E 33 C0	xor	ax,ax

Таким образом в итоге мы получили то, что и требовалось — команду очистки заданного регистра, в данном случае ax.
В другом месте программы вы можете выдать ту же макрокоманду, но уже с другим именем регистра.

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

  • строка может состоять из:
    • последовательности символов без пробелов, точек, запятых, точек с запятой;
    • последовательности любых символов, заключенных в угловые скобки: <...>. В этой последовательности можно указывать как пробелы, так и точки, запятые, точки с запятыми.
      Не забывайте о том, что угловые скобки < > — это тоже оператор ассемблера. Мы упоминали о них при обсуждении директивы equ;
  • для того чтобы указать, что некоторый символ внутри строки, представляющей фактический параметр, является собственно символом, а не чем-то иным, например некоторым разделителем или ограничивающей скобкой, применяется специальный оператор “!”.
    Этот оператор ставится непосредственно перед описанным выше символом, и его действие эквивалентно заключению данного символа в угловые скобки (см. предыдущий пункт);
  • если требуется вычисление в строке некоторого константного выражения, то в начале этого выражения нужно поставить знак “%”:
    % константное_выражение — значение константное_выражение вычисляется и подставляется в текстовом виде в соответствии с текущей системой счисления.

Теперь обсудим вопрос — как транслятор распознает формальные аргументы в теле макроопределения для их последующей замены на фактические аргументы?

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

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

  • REQ, которое говорит о том, что требуется обязательное явное задание фактического аргумента при вызове макрокоманды;
  • =<любая_строка> — если аргумент при вызове макрокоманды не задан, то в соответствующие места в макрорасширении будет вставлено значение по умолчанию, соответствующее значению любая_строка.
    Будьте внимательны: символы, входящие в любая_строка, должны быть заключены в угловые скобки.

Но не всегда ассемблер может распознать в теле макроопределения формальный аргумент. Это, например, может произойти в случае, когда он является частью некоторого идентификатора. В этом случае последовательность символов формального аргумента отделяют от остального контекста с помощью специального символа &.
Этот прием часто используется для задания модифицируемых идентификаторов и кодов операций.
К примеру, определим макрос, который предназначен для генерации в программе некоторой таблицы, причем параметры этой таблицы можно задавать с помощью аргументов макрокоманды:
...
def_table macro	type=b,len=REQ
tabl_&type	d&type	len dup (0)
endm
...
.data
def_tabl	b,10
def_tabl	w,5

После того как вы подвергнете трансляции текст программы, содержащий эти строки, вы получите следующие макрорасширения:
tabl_b	db	10 dup (0)
tabl_w	dw	10 dup (0)

Символ & можно применять и для распознавания формального аргумента в строке, заключенной в кавычки ' '. Например:
num_char	macro	message
;...
;подсчитать количество (num) символов в строке
    jmp	m1
elem	db	'Строка &message содержит '
;число символов в строке message в коде ASCII
num	db	2 dup (0)
    db	' символов',10,13,'$'	;конец строки для вывода функцией 09h
m1:
;...
;вывести elem на экран
    endm

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

Эту директиву необходимо размещать непосредственно за заголовком макроопределения.
Результатом работы этой директивы будет генерация в каждом экземпляре макрорасширения уникальных имен для всех идентификаторов, перечисленных в список_идентификаторов. Эти уникальные имена имеют вид ??xxxx, где хххх — шестнадцатеричное число.
Для первого идентификатора в первом экземпляре макрорасширения хххх= 0000, для второго — хххх= 0001 и т. д. Контроль за правильностью размещения и использования этих уникальных имен берет на себя ассемблер.

Для того чтобы вам окончательно все стало понятно, введем и подвергнем трансляции листинг 3. В нем, кроме некоторых ранее рассмотренных макрокоманд, содержится макрокоманда num_char. Ее назначение — подсчитывать количество символов в строке, адрес которой передается этой макрокоманде в качестве фактического параметра. Строка должна удовлетворять требованию, предъявляемому к строке, предназначенной для вывода на экран функцией 09h прерывания 21h, то есть заканчиваться символом $.
Другой момент, который нашел отражение в этой программе, — использование символа $ для распознавания формального аргумента в строке, заключенной в кавычки ' ' (см. последний фрагмент).

Листинг 3. Пример 2 создания и использования макрокоманд
;prg_13_2.asm
init_ds	macro
;макрос настройки ds на сегмент данных
    mov	ax,data
    mov	ds,ax
    xor	ax,ax
    endm
out_str	macro	str
;макрос вывода строки на экран.
;На входе — выводимая строка.
;На выходе — сообщение на экране.
    push	ax
    mov	ah,09h
    mov	dx,offset str
    int	21h
    pop	ax
    endm
exit	macro
;макрос конца программы
    mov	ax,4c00h
    int	21h
    endm
num_char	macro	message
    local
m1,elem,num,err_mes,find,num_exit
;макрос подсчета количества символов в строке.
;Длина строки — не более 99 символов.
;Вход: message — адрес строки символов, ограниченной '$'
;Выход: в al — количество символов в строке message и вывод сообщения
    jmp	m1
elem	db	'Строка &message содержит '
num	db	2 dup (0) ;число символов в строке message в коде ASCII
    db	' символов',10,13,'$'	;конец строки для вывода функцией 09h
err_mes	db	'Строка &message не содержит символа конца строки',10,13,'$'
m1:
;сохраняем используемые в макросе регистры
    push	es
    push	cx
    push	ax
    push	di
    push	ds
    pop	es	;настройка es на ds
    mov	al,'$'	;символ для поиска — `$`
    cld		;сброс флага df
    lea	di,message	;загрузка в es:di смещения строки message
    push	di	;запомним di — адрес начала строки
    mov	cx,99	;для префикса repne — максимальная длина строки
;поиск в строке (пока нужный символ и символ в строке не равны)
;выход — при первом совпавшем
repne	scasb
    je	find	;если символ найден — переход на обработку
;вывод сообщения о том, что символ не найден
    push	ds
;подставляем cs вместо ds для функции 09h (int21h)
    push	cs
    pop	ds
    out_str	err_mes
    pop	ds
    jmp	num_exit	;выход из макроса
find:	;совпали
;считаем количество символов в строке:
    pop	ax	;восстановим адрес начала строки
    sub	di,ax	;(di)=(di)-(ax)
    xchg	di,ax	;(di) <-> (ax)
    sub	al,3	;корректировка на служебные символы — 10, 13, '$'
    aam		;в al две упакованные BCD-цифры результата подсчета
    or	ax,3030h	;преобразование результата в код ASCII
    mov	cs:num,ah
    mov	cs:num+1,al
;вывести elem на экран
    push	ds
;подставляем cs вместо ds для функции 09h (int21h)
    push	cs
    pop	ds
    out_str	elem
    pop	ds
num_exit:
    push	di
    push	ax
    push	cx
    push	es
    endm

data	segment	para public 'data'
msg_1	db	'Строка_1 для испытания',10,13,'$'
msg_2	db	'Строка_2 для второго испытания',10,13,'$'
data ends

stk	segment	stack
    db	256 dup('?')
stk	ends

code	segment	para public 'code'
    assume	cs:code,ds:data,ss:stk
main	proc
    init_ds
    out_str	msg_1
    num_char	msg_1
    out_str	msg_2
    num_char	msg_2
    exit
main	endp
code	ends
end	main

В теле макроопределения можно размещать комментарии и делать это особым образом.
Если применить для обозначения комментария не одну, как обычно, а две подряд идущие точки с запятой, то при генерации макрорасширения этот комментарий будет исключен.
Если по какой-то причине необходимо присутствие комментария в макрорасширении, то его нужно задавать обычным образом, то есть с помощью одинарной точки с запятой. Например:
mes	macro	messsage
...		;этот комментарий будет включен в текст листинга
...		;;этот комментарий не будет включен в текст листинга
    endm

Макродирективы

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

  • директивы повторения WHILE, REPT, IRP и IRPC.
    Директивы этой группы предназначены для создания макросов, содержащих несколько идущих подряд одинаковых последовательностей строк. При этом возможна частичная модификация этих строк.
  • директивы управления процессом генерации макрорасширения EXITM и GOTO.
    Они предназначены для управления процессом формирования макрорасширения из набора строк соответствующего макроопределения. С помощью этих директив можно как исключать отдельные строки из макрорасширения, так и вовсе прекращать процесс генерации. Директивы EXITM и GOTO обычно используются вместе с условными директивами компиляции, поэтому они будут рассмотрены вместе с ними.

Директивы WHILE и REPT

Директивы WHILE и REPT применяют для повторения определенное количество раз некоторой последовательности строк.
Эти директивы имеют следующий синтаксис:
WHILE	константное_выражение
последовательность_строк
ENDM

REPT	константное_выражение
последовательность строк
ENDM

Обратите внимание, что последовательность повторяемых строк в обеих директивах ограничена директивой ENDM.

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

Директива REPT, подобно директиве WHILE, повторяет последовательность_строк столько раз, сколько это определено значением константное_выражение. Отличие этой директивы от WHILE состоит в том, что она автоматически уменьшает на единицу значение константное_выражение после каждой итерации.

В качестве примера рассмотрим листинг 4, в котором демонстрируется применение директив WHILE и REPT для резервирования области памяти в сегменте данных. Имя идентификатора и длина области задаются в качестве параметров для соответствующих макросов def_sto_1 и def_sto_2.

Листинг 4. Использование директив повторения
;prg_13_3.asm
def_sto_1	macro	id_table,ln:=<5>
;макрос резервирования памяти длиной len.
;Используется WHILE
id_table	label	byte
len=ln
    while	len
    db	0
    len=len-1
    endm
endm
def_sto_2	macro	id_table,len
;макрос резервирования памяти длиной len
id_table	label	byte
    rept	len
    db	0
    endm
endm

data	segment	para public 'data'
    def_sto_1	tab_1,10
    def_sto_2	tab_2,10
data	ends
;сегменты команд и стека в этой программе необязательны
end

Заметьте, что счетчик повторений в директиве REPT уменьшается автоматически после каждой итерации цикла. Проанализируйте результат трансляции листинга 13.3.

Таким образом, директивы REPT и WHILE удобно применять для “размножения” в тексте программы последовательности одинаковых строк без внесения в эти строки каких-либо изменений.

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

Директива IRP

Директива IRP имеет следующий синтаксис:
IRP формальный_аргумент,<строка_символов_1,...,строка_символов_N>
    последовательность_строк
ENDM

Действие данной директивы заключается в том, что она повторяет последовательность_строк N раз, то есть столько раз, сколько строк_символов заключено в угловые скобки во втором операнде директивы IRP. Но это еще не все.
Повторение последовательности_строк сопровождается заменой в ней формального_аргумента строкой символов из второго операнда.
Так, при первой генерации последовательности_строк формальный_аргумент в них заменяется на строка_символов_1.
Если есть строка_символов_2, то это приводит к генерации второй копии последовательности_строк, в которой формальный_аргумент заменяется на строка_символов_2. Эти действия продолжаются до строка_символов_N включительно.

К примеру, рассмотрим результат определения в программе следующей конструкции:
    irp	ini,<1,2,3,4,5>
    db	ini
    endm

Макрогенератором будет сгенерировано следующее макрорасширение:
    db	1
    db	2
    db	3
    db	4
    db	5

Директива IRPC

Директива IRPC имеет следующий синтаксис:
IRPC	формальный_аргумент,строка_символов
    последовательность строк
ENDM

Действие данной директивы подобно IRP, но отличается тем, что она на каждой очередной итерации заменяет формальный_аргумент очередным символом из строка_символов.
Понятно, что количество повторений последовательность_строк будет определяться количеством символов в строка_символов.
К примеру:
    irpc	rg,
    push	rg&x
    endm

В процессе макрогенерации эта директива развернется в следующую последовательность строк:
    push	ax
    push	bx
    push	cx
    push	dx

Директивы условной компиляции

Последний тип макросредств — директивы условной компиляции.
Существует два типа этих директив:

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

С этими директивами применяются директивы управления процессом генерации макрорасширений EXITM и GOTO.

Директива EXITM не имеет операндов, и ее действие заключается в том, что она немедленно прекращает процесс генерации макрорасширения, начиная с того места, где она встретилась в макроопределении.

Директива GOTO имя_метки переводит процесс генерации макроопределения в другое место, прекращая тем самым последовательное разворачивание строк макроопределения. Метка, на которую передается управление, имеет специальный формат:
:имя_метки

Примеры применения этих директив будут приведены ниже.

Директивы компиляции по условию

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

Введение в язык ассемблера этих директив значительно повышает его мощь.
Всего имеется 10 типов условных директив компиляции. Их логично попарно объединить в четыре группы:

  1. Директивы IF и IFE — условная трансляция по результату вычисления логического выражения.
  2. Директивы IFDEF и IFNDEF — условная трансляция по факту определения символического имени.
  3. Директивы IFB и IFNB — условная трансляция по факту определения фактического аргумента при вызове макрокоманды.
  4. Директивы IFIDN, IFIDNI, IFDIF и IFDIFI — условная трансляция по результату сравнения строк символов.

Условные директивы компиляции имеют общий синтаксис и применяются в составе следующей синтаксической конструкции:
IFxxx	логическое_выражение_или_аргументы
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

Заключение некоторых фрагментов текста программы — фрагмент_программы_1 и фрагмент_программы_2 — между директивами IFxxx, ELSE и ENDIF приводит к их выборочному включению в объектный модуль. Какой именно из этих фрагментов — фрагмент_программы_1 или фрагмент_программы_2 — будет включен в объектный модуль, зависит от конкретного типа условной директивы, задаваемого значением xxx, и значения условия, определяемого операндом (операндами) условной директивы логическое_выражение_или_аргумент(ы).

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

Директивы IF и IFE

Синтаксис этих директив следующий:
IF(E)	логическое_выражение
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

Обработка этих директив макроассемблером заключается в вычислении логического_выражения и включении в объектный модуль фрагмент_программы_1 или фрагмент_программы_2 в зависимости от того, в какой директиве IF или IFE это выражение встретилось:

  • если в директиве IF логическое выражение истинно, то в объектный модуль помещается фрагмент_программы_1.
    Если логическое выражение ложно, то при наличии директивы ELSE в объектный код помещается фрагмент_программы_2. Если же директивы ELSE нет, то вся часть программы между директивами IF и ENDIF игнорируется и в объектный модуль ничего не включается. Кстати сказать, понятие истинности и ложности значения логического_выражения весьма условно. Ложным оно будет считаться, если его значение равно нулю, а истинным — при любом значении, отличном от нуля.
  • директива IFE аналогично директиве IF анализирует значение логического_выражения. Но теперь для включения фрагмент_программы_1 в объектный модуль требуется, чтобы логическое_выражение имело значение “ложь”.

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

К примеру, составим макрос для определения в программе области памяти длиной не более 50 и не менее 10 байт (листинг 5).
Листинг 5. Использование условных директив IF и IFE
<1>;prg_13_4.asm
<2>masm
<3>model	small
<4>	stack	256
<5>	def_tab_50	macro	len
<6>if	len	GE 50
<7>GOTO	exit
<8>endif
<9>	if	len LT 10
<10>:exit
<11>EXITM
<12>endif
<13>rept	len
<14>	db	0
<15>endm
<16>endm
<17>.data
<18>def_tab_50	15
<19> def_tab_50	5
<20>.code
<21>main:
<22>	mov	ax,@data
<23>	mov	ds,ax
<24>exit:
<25>	mov	ax,4c00h
<26>	int	21h
<27>end	main
ENDIF

Введите и оттранслируйте листинг 5. При этом не забывайте о том, что условные директивы действуют на шаге трансляции, и поэтому результат их работы можно увидеть только после макрогенерации, то есть в листинге программы.
В нем вы увидите, что в результате трансляции строка 18 листинга 5 развернется в пятнадцать нулевых байт, а строка 19 оставит макрогенератор совершенно равнодушным, так как значение фактического операнда в строках 6 и 9 будет ложным. Обратите внимание, что для обработки реакции на ложный результат анализа в условной директиве мы использовали макродирективы EXITM и GOTO.

Другой интересный и полезный вариант применения директив IF и IFE — отладочная печать.
Суть здесь в том, что в процессе отладки программы почти всегда возникает необходимость динамически отслеживать состояние определенных программно- аппаратных объектов, в качестве которых могут выступать переменные, регистры микропроцессора и т. п. После этапа отладки отпадает необходимость в таких диагностических сообщениях. Для их устранения нужно корректировать исходный текст программы, после чего ее следует подвергнуть повторной трансляции. Но есть более изящный выход.
Можно определить в программе некоторую переменную, к примеру debug, и использовать ее совместно с условными директивами IF или IFE. К примеру,
<1>...
<2>debug	equ	1
<3>...
<4>.code
<5>...
<6>if	debug
<7>;любые команды и директивы ассемблера
<8>;(вывод на печать или монитор)
<9>endif

На время отладки и тестирования программы вы можете заключить отдельные участки кода в своеобразные операторные скобки в виде директив IF и ENDIF (строки 6-9 последнего фрагмента), которые реагируют на значение логической переменной debug. При значении debug = 0 транслятор полностью проигнорирует текст внутри этих условных операторных скобок; при debug = 1, наоборот, будут выполнены все действия, описанные внутри них.

Директивы IFDEF и IFNDEF

Синтаксис этих директив следующий:
IF(N)DEF	символическое_имя
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

Данные директивы позволяют управлять трансляцией фрагментов программы в зависимости от того, определено или нет в программе некоторое символическое_имя. Директива IFDEF проверяет, описано или нет в программе символическое_имя, и если это так, то в объектный модуль помещается фрагмент_программы_1. В противном случае, при наличии директивы ELSE, в объектный код помещается фрагмент_программы_2.
Если же директивы ELSE нет (и символическое_имя в программе не описано), то вся часть программы между директивами IF и ENDIF игнорируется и в объектный модуль не включается.

Действие IFNDEF обратно IFDEF. Если символического_имени в программе нет, то транслируется фрагмент_программы_1. Если оно присутствует, то при наличии ELSE транслируется фрагмент_программы_2. Если ELSE отсутствует, а символическое_имя в программе определено, то часть программы, заключенная между IFNDEF и ENDIF, игнорируется.

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

  • если switch = 0, то сгенерировать фрагмент для вычисления выражения
    y = x*2**n;
  • если switch = 1, то сгенерировать фрагмент для вычисления выражения
    y = x/2**n;
  • если switch не определен, то ничего не генерировать.

Соответствующий фрагмент исходной программы может выглядеть так:
ifndef	sw	;если sw не определено, то выйти из макроса
EXITM
else		;иначе — на вычисление
    mov	cl,n
ife	sw
    sal	x,cl	;умножение на степень 2 сдвигом влево
    else
    sar	x,cl	;деление на степень 2 сдвигом вправо
    endif
endif

Как видим, эти директивы логически связаны с директивами IF и IFE, то есть их можно применять в тех же самых случаях, что и последние.
Есть еще одна интересная возможность использования этих директив. На уроке 4 мы обсуждали формат командной строки и говорили об опциях, которые в ней можно задавать. Вспомните одну из опций командной строки TASM — опцию
/dидентификатор=значение.
Ее использование дает возможность управлять значением идентификатора прямо из командной строки транслятора, не изменяя при этом текста программы.
В качестве примера рассмотрим листинг 6, в котором мы попытаемся с помощью макроса контролировать процесс резервирования и инициализации некоторой области памяти в сегменте данных.
Листинг 6. Инициализация значения идентификатора из командной строки
<1>;prg_13_5.asm
<2>masm
<3>model	small
<4>	stack	256
<5>def_tab_50	macro	len
<6>ifndef	len
<7>display	'size_m не определено, задайте значение 10exitm
<9>else
<10>if	len GE 50
<11>GOTO exit
<12>endif
<13>if	len LT 10
<14>:exit
<15>EXITM
<16>endif
<17>rept	len
<18>	db	0
<19>endm
<20>endif
<21>endm
<22>;size_m=15
<23>.data
<24>def_tab_50	size_m
<25>
<26>.code
<27>main:
<28>	mov	ax,@data
<29>	mov	ds,ax
<30>exit:
<31>	mov	ax,4c00h
<32>	int	21h
<33>end	main

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

  1. Определите где-то в начале исходного текста программы значение этой переменной с помощью equ:
    size_m equ 15
  2. Запустите программу на трансляцию командной строкой вида
    tasm /dsize_m=15 /zi prg_13_2,,,

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

Директивы IFB и IFNB

Синтаксис этих директив следующий:
IF(N)B	аргумент
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

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

  • Директива IFB проверяет равенство аргумента пробелу. В качестве аргумента могут выступать имя или число.
    Если его значение равно пробелу (то есть фактический аргумент при вызове макрокоманды не был задан), то транслируется и помещается в объектный модуль фрагмент_программы_1.
    В противном случае, при наличии директивы ELSE, в объектный код помещается фрагмент_программы_1. Если же директивы ELSE нет, то при равенстве аргумента пробелу вся часть программы между директивами IFB и ENDIF игнорируется и в объектный модуль не включается.
  • Действие IFNB обратно IFB. Если значение аргумента в программе не равно пробелу, то транслируется фрагмент_программы_1.
    В противном случае, при наличии директивы ELSE, в объектный код помещается фрагмент_программы_1. Если же директивы ELSE нет, то вся часть программы (при неравенстве аргумента пробелу) между директивами IFNB и ENDIF игнорируется и в объектный модуль не включается.

В качестве типичного примера применения этих директив предусмотрим строки в макроопределении, которые будут проверять, указывается ли фактический аргумент при вызове соответствующей макрокоманды:
show	macro	reg
ifb
display	'не задан регистр'
exitm
endif
...
endm

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

Директивы IFIDN, IFIDNI, IFDIF и IFDIFI

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

Синтаксис этих директив:
IFIDN(I)	аргумент_1,аргумент_2
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

IFDIF(I)	аргумент_1,аргумент_2
фрагмент_программы_1
    ELSE
фрагмент_программы_2
ENDIF

В этих директивах проверяются аргумент_1 и аргумент_2 как строки символов. Какой именно код — фрагмент_программы_1 или фрагмент_программы_1 — будет транслироваться по результатам сравнения, зависит от кода директивы.
Парность этих директив объясняется тем, что они позволяют учитывать либо не учитывать различие строчных и прописных букв. Так, директивы IFIDNI и IFDIFI игнорируют это различие, а IFIDN и IFDIF — учитывают.

Директива IFIDN(I) сравнивает символьные значения аргумент_1 и аргумент_2.
Если результат сравнения положительный, то фрагмент_программы_1 транслируется и помещается в объектный модуль.
В противном случае, при наличии директивы ELSE, в объектный код помещается фрагмент_программы_1.
Если же директивы ELSE нет, то вся часть программы между директивами IFIDN(I) и ENDIF игнорируется и в объектный модуль не включается.

Действие IFDIF(I) обратно IFIDN(I).
Если результат сравнения отрицательный (строки не совпадают), транслируется фрагмент_программы_1.
В противном случае все происходит аналогично рассмотренным ранее директивам.

Как мы уже упоминали, эти директивы удобно применять для проверки фактических аргументов макрокоманд.
К примеру, проверим, какой из регистров — al или ah — передан в макрос в качестве параметра (проверка проводится без учета различия строчных и прописных букв):
show	macro	rg
ifdifi	,
goto	M_al
else
ifdifi	,
goto	M_ah
else
exitm
endif
endif
:M_al
...
:M_ah
...
endm
ENDIF

Вложенность директив условной трансляции

Как мы неоднократно видели в приведенных выше примерах, TASM допускает вложенность условных директив компиляции. Более того, так как вложенность требуется довольно часто, TASM предоставляет набор дополнительных директив формата ELSEIFxxx, которые заменяют последовательность подряд идущих ELSE и IFxxx в структуре:
IFxxx
;
    ELSE
    IFxxx
;...
ENDIF
ENDIF
Эту последовательность условных директив можно заменить эквивалентной последовательностью дополнительных директив:
IFxxx
;...
    ELSEIFxxx
;...
ENDIF

Наличие xxx в ELSExxx говорит о том, что каждая из директив IF, IFB, IFIDN и т. д. имеет аналогичную директиву ELSEIF, ELSEIFB, ELSEIFIDN и т. д.
В конечном итоге это улучшает читаемость кода. В последнем примере фрагмента макроса, проверяющем, имя какого регистра было передано в макрос, наблюдается подобная ситуация. Последовательность ELSE и IFDIFI можно записать так, как в строке 4:
<1>show	macro	rg
<2>ifdifi	,
<3>goto	M_al
<4>elseifdifi	,
<5>	goto	M_ah
<6>else
<7>exitm
<8>endif
<9>:M_al
<10>...
<11>:M_ah
<12>...
<13>endm

Директивы генерации ошибок

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

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

  • безусловные директивы, генерирующие ошибку трансляции без проверки каких-либо условий;
  • условные директивы, генерирующие ошибку трансляции после проверки определенных условий.

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

Безусловная генерация пользовательской ошибки

К безусловным директивам генерации пользовательской ошибки относится только одна директива — это ERR (.ERR).

Данная директива, будучи вставлена в текст программы, безусловно приводит к генерации ошибки на этапе трансляции и удалению объектного модуля. Она очень эффективна при ее использовании с директивами условной компиляции или в теле макрокоманды с целью отладки.
К примеру, эту директиву можно было бы вставить в ту ветвь программы (в последнем рассмотренном нами макроопределении), которая выполняется, если указанный в качестве аргумента регистр отличен от al и ah:
show	macro	rg
ifdifi	,
goto	M_al
else
ifdifi	,
goto	M_ah
else
.Err
endif
endif
...
endm

Если после определенного таким образом макроопределения в сегменте кода вызвать макрокоманду show с фактическим параметром, отличным от имен регистров ah или al, будет сгенерирована ошибка компиляции (с текстом “User error”), сам процесс компиляции прекращен и, естественно, объектный модуль создан не будет.

Остальные директивы являются условными, так как их поведение определяют некоторые условия.

Условная генерация пользовательской ошибки

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

  • .ERRB (ERRIFB) и .ERRNB (ERRIFNB)
  • .ERRDEF (ERRIFDEF) и .ERRNDEF (ERRIFNDEF)
  • .ERRDIF (ERRIFDIF) и .ERRIDN (ERRIFIDN)
  • .ERRE (ERRIFE) и .ERRNZ (ERRIF)

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

Директивы .ERRB (ERRIFB) и .ERRNB (ERRIFNB)

Синтаксис директив:

.ERRB (ERRIFB) <имя_формального_аргумента> — генерация пользовательской ошибки, если <имя_формального_аргумента> пропущено;

.ERRNB (ERRIFNB) <имя_формального_аргумента> — генерация пользовательской ошибки, если <имя_формального_аргумента> присутствует.

Данные директивы применяются для генерации ошибки трансляции в зависимости от того, задан или нет при вызове макрокоманды фактический аргумент, соответствующий формальному аргументу в заголовке макроопределения с именем <имя_формального_аргумента>.
По принципу действия эти директивы полностью аналогичны соответствующим директивам условной компиляции IFB и IFNB. Их обычно используют для проверки задания параметров при вызове макроса.

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

К примеру, определим обязательность задания фактического аргумента, соответствующего формальному аргументу rg, в макросе show:
<1>	show	macro	rg
<2>;если rg в макрокоманде не будет задан,
<3>;то завершить компиляцию
<4>.errb
<5>;текст макроопределения
<6>;...
<7>endm

Директивы .ERRDEF (ERRIFDEF) и .ERRNDEF (ERRIFNDEF)

Синтаксис директив:

.ERRDEF (ERRIFDEF) символическое_имя — если указанное имя определено до выдачи этой директивы в программе, то генерируется пользовательская ошибка.

.ERRNDEF(ERRIFNDEF) символическое_имя — если указанное символическое_имя не определено до момента обработки транслятором данной директивы, то генерируется пользовательская ошибка.

Данные директивы генерируют ошибку трансляции в зависимости от того, определено или нет некоторое символическое_имя в программе.
Не забывайте о том, что компилятор TASM по умолчанию формирует объектный модуль за один проход исходного текста программы. Следовательно, директивы .ERRDEF (ERRIFDEF) и .ERRNDEF (ERRIFNDEF) отслеживают факт определения символического_имени только в той части исходного текста, которая находится до этих директив.

Директивы .ERRDIF (ERRIFDIF) и .ERRIDN (ERRIFIDN)

Синтаксис директив:

.ERRDIF (ERRIFDIF) <строка_1>,<строка_2> — директива, генерирующая пользовательскую ошибку, если две строки посимвольно не совпадают. Строки могут быть символическими именами, числами или выражениями и должны быть заключены в угловые скобки. Аналогично директиве условной компиляции IFDIF, при сравнении учитывается различие прописных и строчных букв.

.ERRIDN (ERRIFIDN) <строка_1>,<строка_2> — директива, генерирующая пользовательскую ошибку, если строки посимвольно идентичны. Строчное и прописное написание одной и той же буквы воспринимается как разные символы.

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

ERRIFDIFI <строка_1>,<строка_2> — то же, что и ERRIFDIF, но игнорируется различие строчных и прописных букв при сравнении <строка_1> и <строка_2>.

ERRIFIDNI <строка_1>,<строка_2> — то же, что и ERRIFIDN, но игнорируется различие строчных и прописных букв при сравнении <строка_1> и <строка_2>.

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

Директивы .ERRE (ERRIFE) и .ERRNZ (ERRIF)

Синтаксис директив:

.ERRE (ERRIFE) константное_выражение — директива вызывает пользовательскую ошибку, если константное_выражение ложно (равно нулю). Вычисление константного_выражения должно приводить к абсолютному значению, и это выражение не может содержать компонентов, являющихся ссылками вперед.

.ERRNZ(ERRIF) константное_выражение — директива вызывает пользовательскую ошибку, если константное_выражение истинно (не равно нулю). Вычисление константного_выражения должно приводить к абсолютному значению и не может содержать компонентов, являющихся ссылками вперед.

Константные выражения в условных директивах

Как вы успели заметить, во многих условных директивах в формировании условия участвуют выражения. Результат вычисления этого выражения обязательно должен быть константой. Хотя его компонентами могут быть и символические параметры, но их сочетание в выражении должно давать абсолютный результат.
К примеру:
.data
mas	db	...
len	dd	...
...
.code
...
.erre	(len-mas) lt 10	;генерация ошибки, если длина
            ;области mas меньше 10 байт
...

Кроме того, выражение не должно содержать компоненты, которые транслятор еще не обработал к тому месту программы, где находится условная директива.
Также мы отметили, что логические результаты “истина” и “ложь” являются условными в том смысле, что ноль соответствует логическому результату “ложь”, а любое ненулевое значение — “истине”.
Но в языке ассемблера существуют операторы, которые позволяют сформировать и “чисто логический” результат. Это так называемые операторы отношений, выражающие отношение двух значений или константных выражений.
В контексте условных директив вместе с операторами отношений можно рассматривать и логические операторы. Результатом работы и тех, и других может быть одно из двух значений:

  • истина — число, которое содержит двоичные единицы во всех разрядах;
  • ложь — число, которое содержит двоичные нули во всех разрядах.

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

Таблица 1. Операторы отношений
Оператор/ | Синтаксис | Результат отношения
EQ (equal) — равно | выражение_1 EQ выражение_2 | истина — если выражение_1 равно выражение_2
NE (not equal) — не равно | Выражение_1 NE выражение_2 | Истина — если выражение_1 не равно выражение_2
LT (less than) — меньше | Выражение_1 LT выражение_2 | Истина — если выражение_1 меньше выражение_2
LE (less or equal) — меньше или равно | Выражение_1 LE выражение_2 | Истина — если выражение_1 меньше или равно выражение_2
GT (greater than) — больше | Выражение_1 GT выражение_2 | Истина — если выражение_1 больше выражение_2
GE (greater or equal) — больше или равно | Выражение_1 GE выражение_2 | Истина — если выражение_1 больше или равно выражение_2

Таблица 2. Логические операторы
Оператор | Синтаксис | Результат
NOT — логическое отрицание | NOT выражение | Истина — если выражение ложно; ложь — если выражение истинно
AND — логическое И | выражение_1 AND выражение_2 | Истина — если выражение_1 и выражение_2 истинны
OR — логическое ИЛИ | выражение_1 OR выражение_2 | Истина — если выражение_1 или выражение_2 истинны
XOR — исключающее ИЛИ | выражение_1 XOR выражение_2 | Истина — если выражение_1 = (NOT выражение_2)

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

TASM предоставляет средства для вывода текстового сообщения во время трансляции программы — директивы DISPLAY и %OUT. С их помощью можно, при необходимости, следить за ходом трансляции.
К примеру:
display	недопустимые аргументы макрокоманды
...
%out	недопустимое имя регистра

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


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

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

Предложения ассемблера бывают четырех типов:

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

Синтаксис ассемблера

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

Рис. 1. Формат предложения ассемблера

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

Рис. 3. Формат команд и макрокоманд

На этих рисунках:

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

Допустимыми символами при написании текста программ являются:

  1. все латинские буквы: A—Z, a—z. При этом заглавные и строчные буквы считаются эквивалентными;
  2. цифры от 0 до 9;
  3. знаки ?, @, $, _, &;
  4. разделители , . [ ] ( ) < > { } + / * % ! ' " ? \ = # ^.
Предложения ассемблера формируются из лексем, представляющих собой синтаксически неразделимые последовательности допустимых символов языка, имеющие смысл для транслятора.

Лексемами являются:

  • идентификаторы — последовательности допустимых символов, использующиеся для обозначения таких объектов программы, как коды операций, имена переменных и названия меток. Правило записи идентификаторов заключается в следующем: идентификатор может состоять из одного или нескольких символов. В качестве символов можно использовать буквы латинского алфавита, цифры и некоторые специальные знаки — _, ?, $, @. Идентификатор не может начинаться символом цифры. Длина идентификатора может быть до 255 символов, хотя транслятор воспринимает лишь первые 32, а остальные игнорирует. Регулировать длину возможных идентификаторов можно с использованием опции командной строки mv. Кроме этого существует возможность указать транслятору на то, чтобы он различал прописные и строчные буквы либо игнорировал их различие (что и делается по умолчанию). Для этого применяются опции командной строки /mu, /ml, /mx;
  • цепочки символов — последовательности символов, заключенные в одинарные или двойные кавычки;
  • целые числа в одной из следующих систем счисления: двоичной, десятичной, шестнадцатеричной. Отождествление чисел при записи их в программах на ассемблере производится по определенным правилам:
    • Десятичные числа не требуют для своего отождествления указания каких-либо дополнительных символов, например 25 или 139.
    • Для отождествления в исходном тексте программы двоичных чисел необходимо после записи нулей и единиц, входящих в их состав, поставить латинское “b”, например 10010101b.
    • Шестнадцатеричные числа имеют больше условностей при своей записи:
      • Во-первых, они состоят из цифр 0...9, строчных и прописных букв латинского алфавита a, b, c, d, e, f или A, B, C, D, E, F.
      • Во-вторых, у транслятора могут возникнуть трудности с распознаванием шестнадцатеричных чисел из-за того, что они могут состоять как из одних цифр 0...9 (например 190845), так и начинаться с буквы латинского алфавита (например ef15). Для того чтобы "объяснить" транслятору, что данная лексема не является десятичным числом или идентификатором, программист должен специальным образом выделять шестнадцатеричное число. Для этого на конце последовательности шестнадцатеричных цифр, составляющих шестнадцатеричное число, записывают латинскую букву “h”. Это обязательное условие. Если шестнадцатеричное число начинается с буквы, то перед ним записывается ведущий ноль: 0ef15h.
Таким образом, мы разобрались с тем, как конструируются предложения программы ассемблера. Но это лишь самый поверхностный взгляд.

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

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

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

  • постоянные, или непосредственные, операнды
  • адресные операнды
  • перемещаемые операнды
  • счетчик адреса
  • регистровый операнд
  • базовый и индексный операнды
  • структурные операнды
  • Записи
Рассмотрим подробнее характеристику операндов из приведенной классификации:
  • Постоянные или непосредственные операнды — число, строка, имя или выражение, имеющие некоторое фиксированное значение. Имя не должно быть перемещаемым, то есть зависеть от адреса загрузки программы в память. К примеру, оно может быть определено операторами equ или =.

num     equ     5
imd = num-2
        mov     al,num  ;эквивалентно mov al,5
;5 здесь непосредственный операнд
        add     [si],imd        ; imd=3 - непосредственный операнд
        mov     al,5            ;5 - непосредственный операнд

    В данном фрагменте определяются две константы, которые затем используются в качестве непосредственных операндов в командах пересылки mov и сложения add.
  • Адресные операнды — задают физическое расположение операнда в памяти с помощью указания двух составляющих адреса: сегмента и смещения (рис. 4).

    Рис. 4. Синтаксис описания адресных операндов

    К примеру:

        mov     ax,0000h
        mov     ds,ax
        mov     ax,ds:0000h     ;записать слово в ax из области памяти по
                                        ;физическому адресу 0000:0000

    Здесь третья команда mov имеет адресный операнд.

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

    К примеру:

data    segment
mas_w   dw      25 dup (0)
…
code    segment
…
        lea     si,mas_w        ;mas_w - перемещаемый операнд

    В этом фрагменте mas_w — символьное имя, значением которого является начальный адрес области памяти размером 25 слов. Полный физический адрес этой области памяти будет известен только после загрузки программы в память для выполнения.

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

 jmp $+3 ;безусловный переход на команду mov
 cld ;длина команды cld составляет 1 байт
 mov al,1

    При использовании подобного выражения для перехода не забывайте о длине самой команды, в которой это выражение используется, так как значение счетчика адреса соответствует смещению в сегменте команд данной, а не следующей за ней команды. В нашем примере команда jmp занимает 2 байта. Но будьте осторожны, длина команды зависит от того, какие в ней используются операнды. Команда с регистровыми операндами будет короче команды, один из операндов которой расположен в памяти. В большинстве случаев эту информацию можно получить, зная формат машинной команды и анализируя колонку листинга с объектным кодом команды.
  • Регистровый операнд — это просто имя регистра. В программе на ассемблере можно использовать имена всех регистров общего назначения и большинства системных регистров.

 mov al,4 ;константу 4 заносим в регистр al
 mov dl,pass+4 ;байт по адресу pass+4 в регистр
dl
 add al,dl ;команда с регистровыми
операндами
  • Базовый и индексный операнды. Этот тип операндов используется для реализации косвенной базовой, косвенной индексной адресации или их комбинаций и расширений.
  • Структурные операнды используются для доступа к конкретному элементу сложного типа данных, называемого структурой.
  • Записи (аналогично структурному типу) используются для доступа к битовому полю некоторой записи.
Операнды являются элементарными компонентами, из которых формируется часть машинной команды, обозначающая объекты, над которыми выполняется операция.
В более общем случае операнды могут входить как составные части в более сложные образования, называемые выражениями.
Выражения представляют собой комбинации операндов и операторов, рассматриваемые как единое целое.

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

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

  • Арифметические операторы
  • Операторы сдвига
  • Операторы сравнения
  • Логические операторы
  • Индексный оператор
  • Оператор переопределения типа
  • Оператор переопределения сегмента
  • Оператор именования типа структуры
  • Оператор получения сегментной составляющей адреса выражения
  • Оператор получения смещения выражения

В табл. 2 приведены поддерживаемые языком ассемблера операторы и перечислены их приоритеты. Дадим краткую характеристику операторов:

  • Арифметические операторы. К ним относятся:
    • унарные “+” и “–”;
    • бинарные “+” и “–”;
    • умножения “*”;
    • целочисленного деления “/”;
    • получения остатка от деления “mod”.
    Эти операторы расположены на уровнях приоритета 6, 7, 8 в табл. 2. Например,

tab_size equ 50 ;размер массива в байтах
size_el equ 2 ;размер элементов
…
;вычисляется число элементов массива и заносится в
регистр cx
 mov cx,tab_size / size_el ;оператор “/”

    Рис. 5. Синтаксис арифметических операций
  • Операторы сдвига выполняют сдвиг выражения на указанное количество разрядов (рис. 6). Например,

mask_b equ 10111011
…
mov al,mask_b shr 3 ;al=00010111

    Рис. 6. Синтаксис операторов сдвига
  • Операторы сравнения (возвращают значение “истина” или “ложь”) предназначены для формирования логических выражений (см. рис. 7 и табл. 1). Логическое значение “истина” соответствует цифровой единице, а “ложь” — нулю. Например,

tab_size equ 30 ;размер таблицы
…
 mov al,tab_size ge 50 ;загрузка размера
таблицы в al
 cmp al,0 ;если tab_size < 50, то
 je m1 ;переход на m1
…
m1: …
    В этом примере если значение tab_size больше или равно 50, то результат в al равен 0ffh, а если tab_size меньше 50, то al равно 00h. Команда cmp сравнивает значение al с нулем и устанавливает соответствующие флаги в flags/eflags. Команда je на основе анализа этих флагов передает или не передает управление на метку m1.

    Рис. 7. Синтаксис операторов сравнения
    Таблица 1. Операторы сравнения
    Оператор | Значение
    eq | ИСТИНА, если выражение_1 равно выражение_2
    ne | ИСТИНА, если выражение_1 не равно выражение_2
    lt | ИСТИНА, если выражение_1 меньше выражение_2>ИСТИНА, если выражение_1 не равно выражение_2
    le | ИСТИНА, если выражение_1 меньше или равно выражение_2
    gt | ИСТИНА, если выражение_1 больше выражение_2
    ge | ИСТИНА, если выражение_1 больше или равно выражение_2
  • Логические операторы выполняют над выражениями побитовые операции (рис. 8). Выражения должны быть абсолютными, то есть такими, численное значение которых может быть вычислено транслятором. Например:

flags   equ     10010011
         mov    al,flags xor 01h        ;al=10010010;пересылка в al поля flags с
                                        ;инвертированным правым битом

    Рис. 8. Синтаксис логических операторов
  • Индексный оператор [ ]. Не удивляйтесь, но скобки тоже являются оператором, и транслятор их наличие воспринимает как указание сложить значение выражение_1 за этими скобками с выражение_2, заключенным в скобки (рис. 9). Например,
            mov     ax,mas[si]      ;пересылка слова по адресу mas+(si) в регистр ax

    Рис. 9. Синтаксис индексного оператора

    Заметим, что в литературе по ассемблеру принято следующее обозначение: когда в тексте речь идет о содержимом регистра, то его название берут в круглые скобки. Мы также будем придерживаться этого обозначения.
    К примеру, в нашем случае запись в комментариях последнего фрагмента программы mas + (si) означает вычисление следующего выражения: значение смещения символического имени mas плюс содержимое регистра si.

  • Оператор переопределения типа ptr применяется для переопределения или уточнения типа метки или переменной, определяемых выражением (рис. 10).
    Тип может принимать одно из следующих значений: byte, word, dword, qword, tbyte, near, far. Например,
    d_wrd   dd      0
...
        mov     al,byte ptr d_wrd+1 ;пересылка второго байта из двойного слова
    Поясним этот фрагмент программы. Переменная d_wrd имеет тип двойного слова. Что делать, если возникнет необходимость обращения не ко всей переменной, а только к одному из входящих в нее байтов (например, ко второму)? Если попытаться сделать это командой
    mov al,d_wrd+1, то транслятор выдаст сообщение о несовпадении типов операндов. Оператор ptr позволяет непосредственно в команде переопределить тип и выполнить команду.

    Рис. 10. Синтаксис оператора переопределения типа
  • Оператор переопределения сегмента : (двоеточие) заставляет вычислять физический адрес относительно конкретно задаваемой сегментной составляющей: “имя сегментного регистра”, “имя сегмента” из соответствующей директивы SEGMENT или “имя группы” (рис. 11). Этот момент очень важен, поэтому поясню его подробнее. При обсуждении сегментации мы говорили о том, что микропроцессор на аппаратном уровне поддерживает три типа сегментов — кода, стека и данных. В чем заключается такая аппаратная поддержка? К примеру, для выборки на выполнение очередной команды микропроцессор должен обязательно посмотреть содержимое сегментного регистра cs и только его. А в этом регистре, как мы знаем, содержится (пока еще не сдвинутый) физический адрес начала сегмента команд. Для получения адреса конкретной команды микропроцессору остается умножить содержимое cs на 16 (что означает сдвиг на четыре разряда) и сложить полученное 20-битное значение с 16-битным содержимым регистра ip. Примерно то же самое происходит и тогда, когда микропроцессор обрабатывает операнды в машинной команде. Если он видит, что операнд — это адрес (эффективный адрес, который является только частью физического адреса), то он знает, в каком сегменте его искать — по умолчанию это сегмент, адрес начала которого записан в сегментном регистре ds.
    А что же с сегментом стека? Посмотрите раздел "Программная модель микропроцессора", там, где мы описывали назначение регистров общего назначения.
    В контексте нашего рассмотрения нас интересуют регистры sp и bp. Если микропроцессор видит в качестве операнда (или его части, если операнд — выражение) один из этих регистров, то по умолчанию он формирует физический адрес операнда используя в качестве его сегментной составляющей содержимое регистра ss. Что подразумевает термин “по умолчанию”? Вспомните “рефлексы”, о которых мы говорили на уроке 1. Это набор микропрограмм в блоке микропрограммного управления, каждая из которых выполняет одну из команд в системе машинных команд микропроцессора. Каждая микропрограмма работает по своему алгоритму. Изменить его, конечно же, нельзя, но можно чуть-чуть подкорректировать. Делается это с помощью необязательного поля префикса машинной команды (см. формат машинной команды). Если мы согласны с тем, как работает команда, то это поле отсутствует. Если же мы хотим внести поправку (если, конечно, она допустима для конкретной команды) в алгоритм работы команды, то необходимо сформировать соответствующий префикс.
    Префикс представляет собой однобайтовую величину, численное значение которой определяет ее назначение. Микропроцессор распознает по указанному значению, что этот байт является префиксом, и дальнейшая работа микропрограммы выполняется с учетом поступившего указания на корректировку ее работы. Сейчас нас интересует один из них - префикс замены (переопределения) сегмента. Его назначение состоит в том, чтобы указать микропроцессору (а по сути, микропрограмме) на то, что мы не хотим использовать сегмент по умолчанию. Возможности для подобного переопределения, конечно, ограничены. Сегмент команд переопределить нельзя, адрес очередной исполняемой команды однозначно определяется парой cs:ip. А вот сегменты стека и данных — можно. Для этого и предназначен оператор “:”. Транслятор ассемблера, обрабатывая этот оператор, формирует соответствующий однобайтовый префикс замены сегмента. Например,
    .code
...
        jmp     met1    ;обход обязателен, иначе поле ind
будет трактоваться ;как очередная команда
ind     db      5       ;описание поля данных в сегменте команд
met1:
...
mov al,cs:ind ;переопределение сегмента позволяет работать с
                        ;данными, определенными внутри сегмента кода

    Рис. 11. Синтаксис оператора переопределения сегмента
  • Оператор именования типа структуры . (точка) также заставляет транслятор производить определенные вычисления, если он встречается в выражении.
  • Оператор получения сегментной составляющей адреса выражения seg возвращает физический адрес сегмента для выражения (рис. 12), в качестве которого могут выступать метка, переменная, имя сегмента, имя группы или некоторое символическое имя.

    Рис. 12. Синтаксис оператора получения сегментной составляющей
  • Оператор получения смещения выражения offset позволяет получить значение смещения выражения (рис. 13) в байтах относительно начала того сегмента, в котором выражение определено.

    Рис. 13. Синтаксис оператора получения смещения

    Например,

.data
pole    dw      5
...
.code
...
        mov     ax,seg  pole
        mov     es,ax
        mov     dx,offset       pole    ;теперь в паре es:dx полный адрес pole

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

Таблица 2. Операторы и их приоритет
Оператор | Приоритет
length, size, width, mask, (, ), [, ], <, > | 1
. | 2
: | 3
ptr, offset, seg, type, this | 4
high, low | 5
+, - (унарные) | 6
*, /, mod, shl, shr | 7
+, -, (бинарные) | 8
eq, ne, lt, le, gt, ge | 9
not | 10
and | 11
or, xor | 12
short, type | 13

Директивы сегментации

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

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

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

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

Рис. 14. Синтаксис описания сегмента

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

  • Атрибут выравнивания сегмента (тип выравнивания) сообщает компоновщику о том, что нужно обеспечить размещение начала сегмента на заданной границе. Это важно, поскольку при правильном выравнивании доступ к данным в процессорах i80х86 выполняется быстрее. Допустимые значения этого атрибута следующие:
    • BYTE — выравнивание не выполняется. Сегмент может начинаться с любого адреса памяти;
    • WORD — сегмент начинается по адресу, кратному двум, то есть последний (младший) значащий бит физического адреса равен 0 (выравнивание на границу слова);
    • DWORD — сегмент начинается по адресу, кратному четырем, то есть два последних (младших) значащих бита равны 0 (выравнивание на границу двойного слова);
    • PARA — сегмент начинается по адресу, кратному 16, то есть последняя шестнадцатеричная цифра адреса должна быть 0h (выравнивание на границу параграфа);
    • PAGE — сегмент начинается по адресу, кратному 256, то есть две последние шестнадцатеричные цифры должны быть 00h (выравнивание на границу 256-байтной страницы);
    • MEMPAGE — сегмент начинается по адресу, кратному 4 Кбайт, то есть три последние шестнадцатеричные цифры должны быть 000h (адрес следующей 4-Кбайтной страницы памяти).

      По умолчанию тип выравнивания имеет значение PARA.

  • Атрибут комбинирования сегментов (комбинаторный тип) сообщает компоновщику, как нужно комбинировать сегменты различных модулей, имеющие одно и то же имя. Значениями атрибута комбинирования сегмента могут быть:
    • PRIVATE — сегмент не будет объединяться с другими сегментами с тем же именем вне данного модуля;
    • PUBLIC — заставляет компоновщик соединить все сегменты с одинаковыми именами. Новый объединенный сегмент будет целым и непрерывным. Все адреса (смещения) объектов, а это могут быть, в зависимости от типа сегмента, команды и данные, будут вычисляться относительно начала этого нового сегмента;
    • COMMON — располагает все сегменты с одним и тем же именем по одному адресу. Все сегменты с данным именем будут перекрываться и совместно использовать память. Размер полученного в результате сегмента будет равен размеру самого большого сегмента;
    • AT xxxx — располагает сегмент по абсолютному адресу параграфа (параграф — объем памяти, кратный 16; поэтому последняя шестнадцатеричная цифра адреса параграфа равна 0). Абсолютный адрес параграфа задается выражением xxx. Компоновщик располагает сегмент по заданному адресу памяти (это можно использовать, например, для доступа к видеопамяти или области ПЗУ), учитывая атрибут комбинирования. Физически это означает, что сегмент при загрузке в память будет расположен, начиная с этого абсолютного адреса параграфа, но для доступа к нему в соответствующий сегментный регистр должно быть загружено заданное в атрибуте значение. Все метки и адреса в определенном таким образом сегменте отсчитываются относительно заданного абсолютного адреса;
    • STACK — определение сегмента стека. Заставляет компоновщик соединить все одноименные сегменты и вычислять адреса в этих сегментах относительно регистра ss. Комбинированный тип STACK (стек) аналогичен комбинированному типу PUBLIC, за исключением того, что регистр ss является стандартным сегментным регистром для сегментов стека. Регистр sp устанавливается на конец объединенного сегмента стека. Если не указано ни одного сегмента стека, компоновщик выдаст предупреждение, что стековый сегмент не найден. Если сегмент стека создан, а комбинированный тип STACK не используется, программист должен явно загрузить в регистр ss адрес сегмента (подобно тому, как это делается для регистра ds).

      По умолчанию атрибут комбинирования принимает значение PRIVATE.

  • Атрибут класса сегмента (тип класса) — это заключенная в кавычки строка, помогающая компоновщику определить соответствующий порядок следования сегментов при собирании программы из сегментов нескольких модулей. Компоновщик объединяет вместе в памяти все сегменты с одним и тем же именем класса (имя класса, в общем случае, может быть любым, но лучше, если оно будет отражать функциональное назначение сегмента). Типичным примером использования имени класса является объединение в группу всех сегментов кода программы (обычно для этого используется класс “code”). С помощью механизма типизации класса можно группировать также сегменты инициализированных и неинициализированных данных;
  • Атрибут размера сегмента. Для процессоров i80386 и выше сегменты могут быть 16 или 32-разрядными. Это влияет, прежде всего, на размер сегмента и порядок формирования физического адреса внутри него. Атрибут может принимать следующие значения:
    • USE16 — это означает, что сегмент допускает 16-разрядную адресацию. При формировании физического адреса может использоваться только 16-разрядное смещение. Соответственно, такой сегмент может содержать до 64 Кбайт кода или данных;
    • USE32 — сегмент будет 32-разрядным. При формирования физического адреса может использоваться 32-разрядное смещение. Поэтому такой сегмент может содержать до 4 Гбайт кода или данных.
Все сегменты сами по себе равноправны, так как директивы SEGMENT и ENDS не содержат информации о функциональном назначении сегментов. Для того чтобы использовать их как сегменты кода, данных или стека, необходимо предварительно сообщить транслятору об этом, для чего используют специальную директиву ASSUME, имеющую формат, показанный на рис. 15. Эта директива сообщает транслятору о том, какой сегмент к какому сегментному регистру привязан. В свою очередь, это позволит транслятору корректно связывать символические имена, определенные в сегментах. Привязка сегментов к сегментным регистрам осуществляется с помощью операндов этой директивы, в которых имя_сегмента должно быть именем сегмента, определенным в исходном тексте программы директивой SEGMENT или ключевым словом nothing. Если в качестве операнда используется только ключевое слово nothing, то предшествующие назначения сегментных регистров аннулируются, причем сразу для всех шести сегментных регистров. Но ключевое слово nothing можно использовать вместо аргумента имя сегмента; в этом случае будет выборочно разрываться связь между сегментом с именем имя сегмента и соответствующим сегментным регистром (см. рис. 15).

Рис. 15. Директива ASSUME

На уроке 3 мы рассматривали пример программы с директивами сегментации. Эти директивы изначально использовались для оформления программы в трансляторах MASM и TASM. Поэтому их называют стандартными директивами сегментации.

Для простых программ, содержащих по одному сегменту для кода, данных и стека, хотелось бы упростить ее описание. Для этого в трансляторы MASM и TASM ввели возможность использования упрощенных директив сегментации. Но здесь возникла проблема, связанная с тем, что необходимо было как-то компенсировать невозможность напрямую управлять размещением и комбинированием сегментов. Для этого совместно с упрощенными директивами сегментации стали использовать директиву указания модели памяти MODEL, которая частично стала управлять размещением сегментов и выполнять функции директивы ASSUME (поэтому при использовании упрощенных директив сегментации директиву ASSUME можно не использовать). Эта директива связывает сегменты, которые в случае использования упрощенных директив сегментации имеют предопределенные имена, с сегментными регистрами (хотя явно инициализировать ds все равно придется).

В листинге 1 приведен пример программы с использованием упрощенных директив сегментации:

        Листинг 1. Использование упрощенных директив сегментации
;---------Prg_3_1.asm-------------------------------
masm                    ;режим работы TASM: ideal или masm
model   small           ;модель памяти
.data                   ;сегмент данных
message db      'Введите две шестнадцатеричные цифры,$'
.stack                  ;сегмент стека
        db      256     dup ('?')       ;сегмент стека
.code                   ;сегмент кода
main    proc            ;начало процедуры main
        mov     ax,@data        ;заносим адрес сегмента данных в регистр ax
        mov     ds,ax   ;ax в ds
;далее текст программы (см. сегмента кода в листинге 3.1 книги)
        mov     ax,4c00h        ;пересылка 4c00h в регистр ax
        int     21h             ;вызов прерывания с номером 21h
main    endp            ;конец процедуры main
end     main            ;конец программы с точкой входа main
Синтаксис директивы MODEL показан на рис. 16.

Рис. 16. Синтаксис директивы MODEL

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

Таблица 3. Упрощенные директивы определения сегмента
Формат директивы (режим MASM) | Формат директивы (режим IDEAL) | Назначение
.CODE [имя] | CODESEG[имя] | Начало или продолжение сегмента кода
.DATA | DATASEG | Начало или продолжение сегмента инициализированных данных. Также используется для определения данных типа near
.CONST | CONST | Начало или продолжение сегмента постоянных данных (констант) модуля
.DATA? | UDATASEG | Начало или продолжение сегмента неинициализированных данных. Также используется для определения данных типа near
.STACK [размер] | STACK [размер] | Начало или продолжение сегмента стека модуля. Параметр [размер] задает размер стека
.FARDATA [имя] | FARDATA [имя] | Начало или продолжение сегмента инициализированных данных типа far
.FARDATA? [имя] | UFARDATA [имя] | Начало или продолжение сегмента неинициализированных данных типа far
Наличие в некоторых директивах параметра [имя] говорит о том, что возможно определение нескольких сегментов этого типа. С другой стороны, наличие нескольких видов сегментов данных обусловлено требованием обеспечить совместимость с некоторыми компиляторами языков высокого уровня, которые создают разные сегменты данных для инициализированных и неинициализированных данных, а также констант.

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

Таблица 4. Идентификаторы, создаваемые директивой MODEL
Имя идентификатора | Значение переменной
@code | Физический адрес сегмента кода
@data | Физический адрес сегмента данных типа near
@fardata | Физический адрес сегмента данных типа far
@fardata? | Физический адрес сегмента неинициализированных данных типа far
@curseg | Физический адрес сегмента неинициализированных данных типа far
@stack | Физический адрес сегмента стека
Если вы посмотрите на текст листинга 1, то увидите пример использования одного из этих идентификаторов. Это @data; с его помощью мы получили значение физического адреса сегмента данных нашей программы.

Теперь можно закончить обсуждение директивы MODEL. Операнды директивы MODEL используют для задания модели памяти, которая определяет набор сегментов программы, размеры сегментов данных и кода, способ связывания сегментов и сегментных регистров. В табл. 5 приведены некоторые значения параметра модель памяти директивы MODEL.

Таблица 5. Модели памяти
Модель | Тип кода | Тип данных | Назначение модели
TINY | near | near | Код и данные объединены в одну группу с именем DGROUP. Используется для создания программ формата .com.
SMALL | near | near | Код занимает один сегмент, данные объединены в одну группу с именем DGROUP. Эту модель обычно используют для большинства программ на ассемблере
MEDIUM | far | near | Код занимает несколько сегментов, по одному на каждый объединяемый программный модуль. Все ссылки на передачу управления — типа far. Данные объединены в одной группе; все ссылки на них — типа near
COMPACT | near | far | Код в одном сегменте; ссылка на данные — типа far
LARGE | far | far | Код в нескольких сегментах, по одному на каждый объединяемый программный модуль
Параметр модификатор директивы MODEL позволяет уточнить некоторые особенности использования выбранной модели памяти (табл. 6).
Таблица 6. Модификаторы модели памяти
Значение модификатора | Назначение
use16 | Сегменты выбранной модели используются как 16-битные (если соответствующей директивой указан процессор i80386 или i80486)
use32 | Сегменты выбранной модели используются как 32-битные (если соответствующей директивой указан процессор i80386 или i80486)
dos | Программа будет работать в MS-DOS
Необязательные параметры язык и модификатор языка определяют некоторые особенности вызова процедур. Необходимость в использовании этих параметров появляется при написании и связывании программ на различных языках программирования.

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

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