Полная версия

Главная arrow Информатика arrow Архитектура ЭВМ и вычислительных систем

  • Увеличить шрифт
  • Уменьшить шрифт


<<   СОДЕРЖАНИЕ   >>

Системы команд х86. Макроассемблер

Рассмотрим несколько подробнее некоторые элементы набора команд х86 и программирования в этой среде (см. также Приложение 3). Система команд, обобщенно именуемая «х86», включает, как об этом уже упоминалось ранее, целочисленные операции, операции с ПЗ и БПУЮ-команды. Первоначальный набор команд 1А-32 был расширен через какое-то время путем дополнения мультимедийных команд, однако коренное развитие 1А-32 заключалось в переходе к 64 битам.

IA-32

IA-32 (иногда именуется х86-32) представляет собой набор команд-«долгожителей» ЦП Intel и определяет набор инструкций для микропроцессоров, работающих сегодня в подавляющем большинстве персональных компьютеров мира.

Этот набор команд впервые был представлен в микропроцессоре Intel 80386 (1985 г.) и остается основой большинства микропроцессоров ПК 20 лет спустя (процессоры, конечно, выполняют эти команды сегодня гораздо быстрее). В списках различных директив языков программирования IA-32 именуется еще иногда как «архитектура i386».

Аббревиатура означает «Intel Architecture, 32-bit» (Архитектура Intel, 32 бита), что отличает ее от предыдущих х86 процессоров (16 бит) и более поздней архитектуры на 64 бита — IA-64, известной также как «архитектура Itanium». «Живучесть» IA-32 частично объясняется полной обратной программной совместимостью.

Набор команд IA-32 обычно относится к типу CISC, хотя эта рубрика стала менее определенной по мере усовершенствований в микроархитектуре процессоров. Современные процессоры х86 (К7/8, Net Burst и более новые) часто упоминаются как процессоры «post-RISC».

Следующие поколения наборов команд на 64 бита. Преемниками IA-32 на 64 разряда являются два различных набора команд. Один из них действительно основывается на IA-32, но имеет отличающееся название (AMD64/EM64T), в то время как другой отказывается от IA-32 полностью, но имеет похожее название (IA-64).

Модели управления памятью

Известны две модели доступа к памяти в системе команд IA-32:

  • • реальный режим (Real mode) — процессор ограничен доступом не более чем к 1 Мбайт памяти;
  • • защищенный режим (Protected mode) — можно обратиться ко всей памяти (до 4 Гбайт).

Реальный режим. Ранняя ОС — MS-DOS — работала в реальном режиме, в то время как более поздние (OS/2, Windows, Linux и др.) обычно требуют защищенного режима. После включения питания (в процессе загрузки) процессор стартует в реальном режиме и затем начинает загружать программы в оперативную память из ПЗУ и с диска. Для того чтобы переключить процессор в защищенный режим, соответствующая программа может быть вставлена где-либо в последовательности команд начальной загрузки.

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

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

Размер памяти в защищенном режиме обычно ограничивается 4 Гбайт, что, однако, не окончательный предел размера памяти в процессорах IA-32. Используя такие приемы, как страничная память и управление сегментами памяти, ОС IA-32 может обратиться более чем к 32-битовому адресному пространству, даже без переключения к 64 битам (один из этих приемов известен как физическое расширение адреса — Physical Address Extension).

Кроме того, можно использовать виртуальный режим 8 0 8 6 — это подрежим работы в защищенном режиме — некий гибрид, который позволяет старым программам DOS выполняться под управлением супервизора защищенного режима, а также поддерживает одновременное выполнение программ в Protected mode и DOS. В то время как защищенный режим существовал уже в версиях 80286 на 16 битов, виртуальный появляется только в версии IA-32 защищенного режима.

Рассмотрим основные принципы функционирования процессоров i80x86 в реальном режиме.

Реальный режим процессоров І80х86

Оперативную память при работе в этом режиме можно разбить на логические блоки по 64 Кбайт, называемые сегментами, причем каждый сегмент может начинаться с адреса, кратного 16 байт. Таким образом, первый сегмент имеет начальный адрес 0, второй находится по адресу 16 (или 1016) и т. д. Несколько близко расположенных сегментов могут перекрываться. Это удобно при организации совместного доступа к командам, данным разными программами. Доступ к каждой ячейке в памяти происходит путем указания значения регистра сегмента и смещения — адреса внутри этого блока.

Например, команда, подлежащая исполнению процессором в каждый данный момент времени, определяется из значений двух регистров — регистра CS, значение которого, будучи умноженное на 16, дает адрес начала сегмента команд, и регистра указателя команд ip (instruction Pointer, СчАК или PC), указывающего положение соответствующей команды относительно начала сегмента команд.

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

Оперативная память

Объем оперативной памяти І80х86 (здесь — 8086) — 220 байт (1 Мбайт). Байты нумеруются, начиная с 0, и номер байта называется его адресом. Для ссылок на байты памяти используются 20-разрядные адреса — от 00 0 0016 до еееее16.

Байт содержит 8 разрядов (битов), каждый из которых может принимать значение 1 или 0. Разряды нумеруются справа налево от 0 до 7:

7

б

5

4

3

2

t—і

0

Байт — наименьшая адресуемая ячейка памяти — используется для хранения небольших целых чисел и символов. В І80х86 используются и более крупные ячейки — слова и двойные слова.

Слово — два соседних байта, размер — 16 бит (они нумеруются справа налево от 0 до 15). Используется для хранения целых чисел и адресов. Адресом слова считается адрес его первого байта (с меньшим адресом); этот адрес может быть четным и нечетным.

Двойное слово — четыре соседних байта (два соседних слова), размер — 32 бита. Адресом двойного слова считается адрес его первого байта. Используется для хранения «длинных» целых чисел и так называемых а д р е с н ы х пар (сегмент: смещение).

Регистры

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

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

  • • регистры общего назначения (ах, вх, сх, dx, bp, si, di, sp);
  • • сегментные регистры (CS, DS, SS, ES);
  • • счетчик команд (Iр);
  • • регистр флагов (Flags).

Таблица 3.13. Регистры (полурегистры) процессора i80x86

Аббревиатура

Исходное название

Русскоязычный

эквивалент

Старший полурегистр (разряды 15—8)

Младший полурегистр (разряды 7—0)

АХ

Accumulator

Сумматор

АН

АЬ

ВХ

Base

База

ВН

ВВ

СХ

Counter

Счетчик

СН

СВ

DX

Data

Данные

ин

ОВ

ВР

Base pointer

Указатель базы

Нет

Нет

SI

Source index

Индекс источника

Нет

Нет

DI

Destination

index

Индекс приемника

Нет

Нет

SP

Stack pointer

Указатель стека

Нет

Нет

CS

Code segment

Сегмент команд

Нет

Нет

DS

Data segment

Сегмент данных

Нет

Нет

SS

Stack segment

Сегмент стека

Нет

Нет

ES

Extra segment

Дополнительный сегмент

Нет

Нет

IP

Instruction

pointer

Счетчик команд (счАК)

Нет

Нет

Регистры общего назначения можно использовать во всех арифметических и логических командах. В то же время каждый из них имеет определенную специализацию (некоторые команды «работают» только с определенными регистрами). Например, команды умножения и деления требуют, чтобы один из операндов находился в регистре АХ (ах и DX, в зависимости от размера операнда), а команды управления циклом используют регистр сх в качестве счетчика цикла. Регистры вх и ВР очень часто используются как базовые регистры, a si и di — как индексные. Регистр SP обычно указывает на вершину стека, аппаратно поддерживаемого в i80x86.

Регистры ах, вх, сх и DX конструктивно устроены так, что возможен независимый доступ к их старшей и младшей половинам; можно сказать, что каждый из этих регистров состоит из двух байтовых регистров, обозначаемых АН, AL, вн и т. д. (н — high, старший; L — low, младший) — табл. 3.13.

Таким образом, с каждым из этих регистров можно работать как с единым целым, так и с его частями, например — записать слово в ах, а затем считать только часть слова из регистра АН или заменить только часть в регистре AL и т. д. Такое устройство регистров позволяет использовать их как для работы с числами, так и со строками.

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

Сегментные регистры CS, DS, SS И ES не могут быть операндами никаких команд, кроме команд пересылки и стековых команд. Эти регистры используются только для сегментирования адресов.

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

Флаги. Имеется особый регистр флагов (табл. 3.14). Флаг — это бит, принимающий значение «1» (флаг установлен) или «О» (флаг сброшен):

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

В 180x86 используются 9 флагов, каждому из них присвоено определенное ИМЯ СЕ И Т. Д.).

Таблица 3.14. Регистр флагов

Флаги

X

X

X

X

OF

DF

IF

TF

SF

ZF

X

AF

X

PF

X

CF

Разряды

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

Таблица 3.15. Содержание регистра флагов

Символ

Расшифровка

Содержание

Комментарий

Флаги условий

CF

Carry flag

Флаг

переноса

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

OF

Overflow

flag

Флаг

переполнения

Устанавливается в 1, если при сложении или вычитании целых чисел со знаком получился результат, по модулю превосходящий допустимую величину (произошло переполнение мантиссы и она «залезла» в знаковый разряд)

ZF

Zero flag

Флаг нуля

Устанавливается в 1, если результат команды оказался равным 0

SF

Sign flag

Флаг знака

Устанавливается в 1, если в операции над числами со знаками получился отрицательный результат

PF

Parity

flag

Флаг четности

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

AF

Auxiliary carry flag

Флаг дополнительного

переноса

Фиксирует особенности выполнения операций над двоично-десятичными числами

Флаги состояний

DF

Direction

flag

Флаг направления

Устанавливает направление просмотра строк в строковых командах: при ое = 0 строки просматриваются «вперед» (от начала к концу), при эе = 1 — в обратном направлении

Окончание табл. 3.15

Символ

Расшифровка

Содержание

Комментарий

IF

Interrupt

flag

Флаг прерываний

При гг = о процессор перестает реагировать на поступающие к нему прерывания, при тт = 1 блокировка прерываний снимается

TF

Trap flag

Флаг трассировки

При TF = 1 после выполнения каждой команды процессор делает прерывание (с номером 1), чем можно воспользоваться при отладке программы для ее трассировки

Регистры IA-32

В 32-разрядных (и выше) ЦП картина регистров изменяется. Как 386, так и все другие процессоры IA-32 имеют восемь 32-раз-рядных РОН (GPR) для использования прикладными программами. В процессорах AMD64 это число удваивается до 16. Есть 8 регистров стека с плавающей запятой (число также удвоено в AMD64). В более поздних процессорах были добавлены новые регистры для обеспечения выполнения различных наборов команд SIMD — типа ММХ, 3DNow!, SSE, SSE2, SSE3 и SSSE3.

Есть также системные регистры, которые используются главным образом операционными системами, но не приложениями. Это — сегментные (segment), контрольные (control), отладочные (debug) и тестовые (test). Шесть сегментных регистров используются главным образом для управления памятью. Число контрольных, отладочных и тестовых регистров изменяется от модели к модели ЦП.

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

8- и 16-битовые участки регистров. Для программистов доступны также именуемые 8- и 16-битовые части регистров. Например, к младшим битам еах можно обратиться, вызывая регистр ах. Некоторые из регистров на 16 бит могут быть далее подразделены на участки по 8 бит, например, старшие 8 бит АХ могут быть вызваны под именем АН, а младшие биты — как al. Точно так же в евх можно выделить регистр ВХ (16 бит), который в свою очередь содержит вн и вь (каждый по 8 бит).

Описание регистров приводится в табл. 3.16 (ср. с табл. 3.13).

Таблица 3.16. Регистры IA-32

Имя

Номер

Назначение регистра

РОН данных

ЕАХ

000

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

ЕСХ

001

Универсальный счетчик циклов, который имеет специальную интерпретацию для циклов

EDX

010

Регистр данных, который дополняет сумматор и хранит данные, необходимые для операции, которая выполняется сумматором

ЕВХ

011

Используется как буферное ЗУ, но в режиме на 16 бит использовался как указатель (pointer)

Адресные РОН

ESP

100

Указатель стека, используется, чтобы хранить высший адрес стека

EBP

101

Указатель базы, используется, чтобы хранить адрес текущего фрейма стека. Может иногда применяться как буферное ЗУ

ESI

110

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

EDI

111

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

El P

Указатель адреса команды

  • * Могут использоваться как РОН, однако каждый имеет некоторую специализацию. Каждый из этих регистров также позволяет обращаться к именуемым участкам по 8 или 16 бит.
  • ** Используются только для указания адреса. Они имеют именуемые 16-битовые участки (но не по 8 бит).

Представление чисел

Рассмотрим машинное представление целых чисел, строк и адресов. Представление двоично-десятичных чисел, используемых достаточно редко, не рассматривается. Что касается вещественных чисел, то в І80х86 нет команд вещественной арифметики (операции над этими числами реализуются программным путем или выполняются сопроцессором) и потому нет стандартного представления вещественных чисел. Кроме того, рассматриваются некоторые особенности выполнения арифметических операций.

Как это принято в МА8М, шестнадцатеричные числа записываются с буквой й в суффиксе («на конце»), двоичные — с буквой Ь, восьмеричные — с буквой о или q, десятичные — без каких-либо «дополнений».

Представление целых чисел. В общем случае под целое число можно отвести любое число байтов, однако система команд І80х86 поддерживает только числа размером в байт и слово и частично — размером в двойное слово.

В І80х86 делается различие между целыми числами без знака (неотрицательными) и числами со знаком. Это объясняется тем, что в ячейках одного и того же размера можно представить больший диапазон чисел без знаков, чем чисел со знаком, и если известно заранее, что некоторая числовая величина является неотрицательной, то выгоднее рассматривать ее как величину без знака, чем как величину со знаком.

Целые числа без знака в зависимости от их размера могут быть представлены в виде байта, слова или двойного слова. В виде байта представляются целые от 0 до 255 (28 - 1), в виде слова — целые от 0 до 65 535 (216 - 1), в виде двойного слова — целые от 0 до 4 294 967 295 (232 - 1). Числа записываются в двоичной системе счисления, занимая все разряды ячейки.

Например, число 130 записывается в виде байта 10000010Ь (82й).

Числа размером в слово хранятся в памяти в «перевернутом» виде:

  • • младшие (правые) 8 битов числа размещаются в первом байте слова;
  • • старшие 8 битов — во втором байте (в шестнадцатеричной системе: две правые цифры — в первом байте, две левые цифры — во втором байте). Например, число 130 (0082Ь) в виде слова хранится в памяти так:
    • 00

Однако в регистры числа загружаются в нормальном виде:

АХ

00

82

АН

АЬ

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

78

56

34

12

Другими словами, в первом слове двойного слова размещаются младшие (правые) 16 битов числа, а во втором слове — старшие 16 битов, причем в каждом из этих двух слов в свою очередь используется «перевернутое» представление.

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

Целые числа со знаком. Эти числа также представляются в виде байта, слова и двойного слова. В виде байта записываются числа от -128 до 127, слова — от -32 768 до 32 767, а двойные слова — от -2 147 483 648 до 2 147 483 647. При этом числа записываются в дополнительном коде.

Числа со знаком размером в слово и двойное слово записываются в памяти в «перевернутом» виде (при этом знаковый бит оказывается в последнем байте ячейки). Но в тексте на языке МА8М эти числа, как и беззнаковые, записываются в нормальной форме.

Иногда число-байт необходимо расширить до слова, т. е. получить такое же по величине число, но размером в слово. Существует два способа такого расширения — без знака и со знаком. В любом случае исходное число-байт попадает во второй (до «переворачивания») байт слова, а первый байт заполняется по-разному:

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

Аналогично происходит расширение числа-слова до двойного слова.

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

В І80х86 имеются команды сложения и вычитания целых чисел размером в слово и байт (см. табл. П3.1). Специальных команд для сложения и вычитания двойных слов нет, эти операции реализуются через команды сложения и вычитания слов. Сложение и вычитание беззнаковых чисел производится по модулю 28 для байтов и 216 для слов (см. рис. 1.4, а).

Это означает, что если в результате сложения появилась единица переноса, не вмещающаяся в разрядную сетку, то она отбрасывается. Например, при сложении байтов 128 и 130 получается число 258 (ЮОООООЮЬ) длиной 9 бит, поэтому левая двоичная единица отбрасывается и остается число 2 (10Ь), которое и является результатом сложения.

Ошибка здесь не фиксируется, но флаг переноса се устанавливается в «1» (если переноса не было, в се заносится «0»). Установить такое искажение суммы можно только последующим анализом флага СЕ.

Искажение результата происходит и при вычитании из меньшего числа большего. Здесь также не фиксируется ошибка, однако первому числу дается «заем единицы» (в случае байтов это число увеличивается на 256, для слов — на 216), после этого и производится вычитание. Например, вычитание байтов 2 и 3 сводится к вычитанию чисел 256 + 2 = 258 и 3, в результате этого получается неправильная разность — 255 (а не -1). Для того чтобы можно было обнаружить такую ситуацию, флаг переноса СЕ переключается на «1» (если же «заема» не было, в СЕ записывается «0»).

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

Например, сложение байтовых чисел 1 и -2 происходит так: берутся их дополнительные коды 1 и (256 - 2) = 254, вычисляется сумма этих величин 1 + 254 = 255, и она трактуется как число со знаком -1 (255 = 256 - 1).

Если при таком сложении возникла единица переноса, то она, как обычно, отбрасывается, а флаг СЕ получает значение «1». Однако в данном случае это отсечение не представляет интереса — результат операции будет правильным, например: 3 + (-2) = 3 + 254(шос1 256) = 257(шос1 256) = 1. Тем не менее здесь возможна иная неприятность — модуль суммы (ее мантисса) может превзойти допустимую границу и переместиться в знаковый разряд, «испортив» его. Например, при сложении байтовых чисел 127 и 2 получается значение 129 (1000010016), представляющее дополнительный код числа -127 (256 - 129).

Хотя результат здесь получился и неправильным, процессор не фиксирует ошибку, но заносит «1» в флаг переполнения ОЕ (если «переполнения мантиссы» не было, в ОЕ записывается «0»). Анализируя затем этот флаг, можно зафиксировать такую ошибку.

Что касается умножения и деления знаковых и чисел без знака, то они выполняются по разным алгоритмам, разными машинными командами. Однако и у этих операций есть ряд особенностей. При умножении байтов (слов) первый сомножитель должен находиться в регистре аъ (ах), результатом же умножения является слово (двойное слово), которое заносится в регистр ах (регистры эх и ах). Тем самым при умножении сохраняются все цифры произведения. При делении байтов (слов) первый операнд (делимое) должен быть словом (двойным словом) и обязан находиться в регистре ах (регистрах эх и ах). Результатом деления являются две величины размером в байт (слово) — неполное частное (<±^) и остаток от деления (тос1); неполное частное записывается в регистр аъ (ах), а остаток — в регистр АН (ох).

Представление символов и строк

На символ отводится один байт памяти, в который записывается код символа — целое от 0 до 255. В 180x86 используется система кодировки АБСII (см. Приложение 3).

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

В 180x86 строкой считается также и последовательность слов (обычно это последовательность целых чисел). Элементы таких строк располагаются в последовательных ячейках памяти, но каждый элемент представлен в «перевернутом» виде.

Представление адресов

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

Дело в том, что в І80х86 термином «адрес» могут обозначаться разные понятия:

  • • 16-битовое смещение (offset) — адрес ячейки, отсчитанный от начала сегмента (области) памяти, которому принадлежит эта ячейка. В этом случае под адрес отводится слово памяти, причем адрес записывается в «перевернутом» виде (как и числа-слова вообще);
  • • 20-битовый абсолютный адрес некоторой ячейки памяти. В силу ряда причин в І80х86 такой адрес задается не как 20-битовое число, а как пара сегмент: смещение, где сегмент (segment) — это первые 16 битов начального адреса сегмента памяти, которому принадлежит ячейка, а смещение (offset) — 16-битовый адрес этой ячейки, отсчитанный от начала данного сегмента памяти (величина 16 х сегмент + смещение дает абсолютный адрес ячейки).

Такая пара записывается в виде двойного слова: в первом слове размещается смещение, а во втором — сегмент, причем каждое из этих слов в свою очередь представлено в «перевернутом» виде. Например, пара 1234h:5678h будет записана так:

78

56

34

12

Смещение

Сегмент

Директивы определения данных

Для того чтобы в программе на МА8М зарезервировать ячейки памяти под константы и переменные, необходимо воспользоваться директивами определения данных — с названиями эв (описывает данные размером в байт), эдо (слово) и оэ (двойное слово).

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

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

Имена, указанные в директивах эв, и оэ, называются именами переменных (в отличие от меток — имен команд).

В МА8М числа записываются в нормальном (непереверну-том) виде в системах счисления с основанием 10, 16, 8 или 2.

Примеры:

А

ОВ

162

В

ОВ

0А2Ь

С

ОИ

-1

0

ОИ

оргт-!

Е

00

описать константу-байт 162 и дать ей имя А такая же константа, но с именем в константа-слово -1 с именем с такая же константа-слово, но с именем Б -1 как двойное слово

Константы-символы описываются в директиве эв двояко: указывается либо код символа (целое от 0 до 255), либо сам символ в кавычках (одинарных или двойных); в последнем случае ассемблер сам заменит символ на его код. Например, следующие директивы эквивалентны (2А — код звездочки в А8СП):

СН ОВ 02АЙ СН ОВ '*'

СН ОВ "*"

Константы-адреса, как правило, задаются именами. Так, по директиве

АРИ РИ СН

будет выделено слово в памяти, которому дается ИМЯ АРИ и в которое будет помещен адрес (смещение), соответствующий имени СН. Если такое же имя описать в директиве рр, то ассемблер автоматически добавит к смещению имени его сегмент и запишет смещение в первую половину двойного слова, а сегмент — во вторую половину.

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

и РИ ? ; отвести слово и дать ему имя г, ничего в этот байт не записывать.

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

С РВ 200, -5, ЮЬ, ?, Т'

Имя, указанное в директиве, считается именующим первую из констант.

Для ссылок на остальные в МА8М используются выражения вида <имя> + <целое>; например, для доступа к байту с числом -5 надо указать выражение в + 1, для доступа к байту с 10й — выражение с + 2 и т. д.

Если в директиве рв перечислены только символы, например:

Э РВ 'а'Ь',

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

Э РВ 'а+Ь'

И наконец, если в директиве описывается несколько одинаковых констант (переменных), то можно воспользоваться конструкцией повторения

k DUP(а,Ь, ...,с) ,

которая эквивалентна повторенной к раз последовательности а,

Ъ С

Например, директивы

VI DB 0,0,0,0,0

V2 DW ?,?,?,?,?,?,?,?,?, 'а', 1,2,1,2,1,2,1/2,

можно записать более коротко таким образом:

VI DB 5 DUP(0)

V2 DW 9 DUP(?), 'а', 4 DUP(1,2).

Представление команд. Модификация адресов

Структура команд. Исполнительные адреса. Машинные команды І80х86 занимают от 1 до 6 байтов. Код операции (КОП) занимает один или два первых байта команды. В І80х86 достаточно много различных операций, поэтому для них не хватает 256 различных КОП, которые можно представить в одном байте. В связи с этим некоторые операции объединяются в группу и им дается один и тот же КОП, во втором же байте команды этот КОП уточняется. Кроме того, во втором байте указываются типы и способ адресации операндов. Остальные байты команды указывают на операнды.

Команды могут иметь от 0 до трех операндов, у большинства команд — один или два операнда. Размер операндов — байт или слово (редко — двойное слово). Операнд может быть указан в самой команде (это так называемый непосредственный операнд) либо может находиться в одном из регистров І80х86 и тогда в команде указывается этот регистр, либо может находиться в ячейке памяти и тогда в команде тем или иным способом указывается адрес этой ячейки. Некоторые команды требуют, чтобы операнд находился в фиксированном месте (например, в регистре ах), тогда операнд явно не указывается в команде. Результат выполнения команды помещается в регистр или ячейку памяти, из которого (которой), как правило, берется первый операнд. Например, большинство команд с двумя операндами реализуют действие

ор1 := ор1 - ор2, где ор 1 — регистр или ячейка; а ор2 — непосредственный операнд, регистр или ячейка.

Адрес операнда разрешено модифицировать по одному или двум регистрам. В первом случае в качестве регистра-модификатора разрешено использовать регистр вх, ВР, Б1 или 01 (и никакой иной). Во втором случае один из модификаторов обязан быть регистром вх или ВР, а другой — регистром Б1 или 01; одновременная модификация по вх и вр или Б1 и VI недопустима.

Регистры вх и вр обычно используются для хранения базы (или базового, начального адреса) некоторого участка памяти (например, массива) и потому называются базовыми регистрами, а регистры Б1 и Э1 часто содержат индексы элементов массива и потому называются индексными регистрами.

Однако такое «распределение ролей» необязательно, и, например, в Б1 может находиться база массива, а в вх — индекс элемента массива.

В МА5М адреса в командах записываются в виде одной из следующих конструкций:

А, А [М] или А [М1 ] [М2] ,

где А — адрес; м — регистр вх, вр, Б1 или Р1; М1 — регистр вх или вр, а М2 — регистр Б1 или 01. Во втором и третьем варианте А может отсутствовать, в этом случае считается, что А = 0.

При выполнении команды процессор прежде всего вычисляет так называемый исполнительный (эффективный) адрес — сумму адреса, заданного в команде, и текущих значений указанных регистров-модификаторов, причем все эти величины рассматриваются как неотрицательные, и суммирование ведется по модулю 216 ([г] означает содержимое регистра г):

А : Аисп = А

А[М] : Аисп = А+[М] (тос! 216)

А [М1 ] [М2 ] : Аисп = А+[М1] + [М2] (тос! 216) .

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

Форматы команд

В 180x86 форматы машинных команд достаточно разнообразны. Для примера приведем лишь основные форматы команд с двумя операндами (см. также табл. 3.1).

Формат «регистр—регистр» (2 байта):

КОП

d

W

11

r?gi

reg2

7 2

1

0

16

53

20

Команды этого формата описывают обычно действие r?gi : = r?gi - reg2 или reg2 := reg2 - r?gi. Поле КОП первого байта указывает на операцию (-), которую надо выполнить.

Бит w определяет размер операндов, а бит d указывает, в какой из регистров записывается результат:

W = 1

— слова

d = 1

r?gi

: = r?gi - reg2

= 0

— байты

= 0

reg2

:= reg2 - r?gi

Во втором байте два левых бита фиксированы (для данного формата), а трехбитовые поля r?gi и reg2 указывают на регистры, участвующие в операции, согласно следующим правилам:

reg

w = 1

w = 0

000

AX

AL

001

CX

CL

010

DX

DL

011

BX

BL

100

SP

AH

101

BP

CH

110

SI

DH

111

DI

BH

Формат «регистр—память» (2—4 байта):

КОП

W

mod

reg

mem

Адрес (0—2 байта)

Эти команды описывают операции reg : = reg - mem или mem := mem — reg. Бит w первого байта определяет размер операндов (см. ранее), а бит d указывает, куда записывается результат: в регистр (d = 1) или в ячейку памяти (d = 0). Трехбитовое поле reg второго байта указывает операнд-регистр (см. выше), двухбитовое поле mod определяет, сколько байт в команде занимает операнд-адрес (00 — 0 байтов, 01 — 1 байт, 10 — 2 байта), а трехбитовое поле mem указывает способ модификации этого адреса. В следующей таблице указаны правила вычисления исполнительного адреса в зависимости от значений полей mod и mem (а8 — адрес размером в байт, а1б — адрес размером в слово):

mod

mem

00

01

10

000

[BX]+[SI]

[BX]+[SI]+a8

[BX]+[SI]+al6

001

[BX]+[DI]

[BX]+[DI]+a8

[BX]+[DI]+al6

010

[BP]+[SI]

[BP]+[SI]+a8

[BP]+[SI]+al6

Oil

[BP] + [DI]

[BP]+[DI]+a8

[BP]+[DI]+al6

100

[SI]

[SI]+a8

[SI]+al6

101

[DI]

[DI]+a8

[DI]+al6

110

al 6

[BP]+a8

[BP]+al6

111

[BX]

[BX]+a8

[BX]+al6

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

Если адрес задан в виде байта (а8), то он автоматически расширяется со знаком до слова (а16). Случай mod = 00 и mem = 110 указывает на отсутствие регистров-модификаторов, при этом адрес должен иметь размер слова (адресное выражение [вр] ассемблер транслирует в mod = 01 и mem = 110 при а8 = 0). Случай mod = 11 соответствует формату «регистр—регистр».

Формат «регистр—непосредственный операнд» (3—4 байта):

КОП

s

w

11

КОП '

reg

Непосред. операнд (1—2 б.)

Команды этого формата описывают операции reg := reg -- immed (immed — непосредственный операнд). Бит w указывает на размер операндов, а поле reg - на регистр-операнд (см. ранее). Поле коп в первом байте определяет лишь класс операции (например, класс сложения), уточняет же операцию поле коп' из второго байта. Непосредственный операнд может занимать 1 или 2 байта в зависимости от значения бита при этом операнд-слово записывается в команде в «перевернутом» виде. Ради экономии памяти в І80х86 предусмотрен случай, когда в операции над словами непосредственный операнд может быть задан байтом (на этот случай указывает «1» в бите э при л/ = 1), и тогда перед выполнением операции байт автоматически расширяется (со знаком) до слова.

Формат «память—непосредственный операнд» (3—6 байтов):

КОП

S

W

mod

КОП”

mem

Адрес (0—2 б.)

Непоср. оп (1—2 б.)

Команды этого формата описывают операции типа mem : = mem - immed. Смысл всех полей — тот же, что и в предыдущих форматах.

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

Запись команд в МАБМ

Из сказанного ясно, что одна и та же операция в зависимости от типов операндов записывается в виде различных машинных команд, например, в 180x86 имеется 28 команд пересылки байтов и слов. В то же время в МА8М все эти «родственные» команды записываются единообразно — например, все команды пересылки имеют одну и ту же символьную форму записи:

МОУ ор!,ор2 (ор!:=ор2)

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

В общем случае команды записываются в МАБМ следующим образом:

МЕТКА: МНЕМОКОД ОПЕРАНДЫ; КОММЕНТАРИЙ

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

Регистры указываются своими именами, например:

MOV АХ,SI ;оба операнда — регистры.

Непосредственные операнды задаются константными выражениями (их значениями являются константы-числа), например:

MOV ВН,5 5 — непосредственный операнд

MOV DI,SIZE X SIZE X (число байтов, занимаемых

переменной X) — непосредственный операнд

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

МОУ X,АН

МОУ X[ВХ][БІ],5

МОУ [ВХ],СБ.

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

МСЛ/ АН, 5 Пересылка байта, так как АН — байтовый

регистр

МСЛ/ АХ, 5 Пересылка слова, так как АХ — 16-битовый

регистр-операнд (5 может быть байтом и словом, по нему нельзя определить размер пересылаемой величины)

МСЛ/ [ВХ] , 300 Пересылка слова, так как число 300 не

может быть байтом

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

MOV DS,AX Оба операнда имеют размер слова

MOV СХ,ВН Ошибка: регистры СХ и ВН имеют разные

размеры

MOV DL,300 Ошибка: DL — байтовый регистр, а число 300

не может быть байтом

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

MOV [ВХ] , 5.

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

MOV BYTE PTR [ВХ],5 Пересылка байта

MOV WORD PTR [ВХ],5 Пересылка слова

Операторы

Это разновидность выражений языка МА8М, аналогичных функциям.

Оператор рти (вычисляет адрес аргумента) может использоваться для изменения типа переменной, заданной при ее описании. Пусть, например, х есть имя переменной размером в слово

X DW 999,

тогда при необходимости записать в байтовый регистр АН значение только первого байта этого слова, нельзя воспользоваться командой вида

MOV АН, X,

так как ее операнды имеют разный размер. Эту команду следует записать несколько иначе:

MOV АН,BYTE PTR X.

Здесь конструкция byte ptr х означает адрес х, но уже рассматриваемый не как адрес слова, а как адрес байта. (Напомним, что с одного и того же адреса может начинаться байт, слово и двойное слово; оператор PTR уточняет, ячейка какого именно размера имеется в виду.)

Если в символьной команде, оперирующей со словами, указан непосредственный операнд размером в байт, как, например, в команде

MOV АХ, 8Oh,

то возникает некоторая неоднозначность — неизвестно, что именно будет записано в регистр ах — число 0 0 80h (+128) или 0FF80h (-128). В подобных ситуациях ассемблер формирует машинную команду, где операнд-байт расширен до слова, причем расширение происходит со знаком, если операнд был записан как отрицательное число, и без знака — в остальных случаях. Например:

MOV АХ,-128 ; => MOV AX,0FF80h (А:=-128)

MOV АХ,128 ; => MOV AX,0080h (А:=+128)

MOV АХ,8Oh ; => MOV AX,0080h (A:=+128).

Сегментирование

Сегменты памяти. Сегментные регистры. Первые модели 180x86 имели оперативную память объемом 216 байтов (64 Кбайт) и потому использовали 16-битовые адреса. В последующих моделях память была увеличена до 220 байт (1 Мбайт = 1000 Кбайт), что потребовало уже 20-битовые адреса. Однако в этих 180x86 ради преемственности были сохранены 16-битовые адреса — именно такие адреса хранятся в регистрах, указываются в командах и образуются в результате модификации по базовым и индексным регистрам.

Проблема совместимости решается с помощью сегментирования (неявного базирования) адресов (рис. 3.43). В 180x86 абсолютный (20-битовый) адрес А любой ячейки памяти можно представить как сумму 20-битового начального адреса (базы) в сегмента, которому принадлежит ячейка, и 16-битового смещения о — адреса этой ячейки, отсчитанного от начала сегмента: о

оооо

Исполнительный адрес (СчАК)

15

0

Сегмент (базовый регистр)

0000

і

J и J

Сумматор

0

Физический адрес

Рис. 3.43. Модификация адресов при сегментной адресации

А = в + э. (Неоднозначность выбора сегмента не играет существенной роли, главное — чтобы сумма в и Б давала нужный адрес). Адрес в заносится в некоторый регистр Б, а в команде, где должен быть указан адрес а, вместо него записывается пара из регистра Б и смещения в (в МА8М такая пара, называемая адресной парой или указателем, записывается как Б: о). Процессор же устроен так, что при выполнении команды он прежде всего по паре Б: э вычисляет абсолютный адрес А как сумму содержимого регистра Б и смещения в и только затем обращается к памяти по адресу а. Заменяя в командах абсолютные адреса на адресные пары, удается адресовать всю память 16-битовыми адресами (смещениями).

В качестве регистра Б разрешается использовать не любой регистр, а только один из четырех регистров, называемых сегментными: СБ, ОБ, ББ И ЕБ. В СВЯЗИ С ЭТИМ Одновременно МОЖНО работать с четырьмя сегментами памяти: начало одного из них загружается в регистр СБ и все ссылки на ячейки этого сегмента указываются в виде пар СБ: в, начало другого заносится в ОБ и все ссылки на его ячейки задаются в виде пар ОБ: о и т. д. Если одновременно надо работать с большим числом сегментов, тогда следует своевременно сохранять содержимое сегментных регистров и записывать в них начальные адреса пятого, шестого и других сегментов.

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

Как и все регистры i80x86, сегментные регистры имеют размер слова. Поэтому возникает проблема — как удается разместить в них 20-битовые начальные адреса сегментов памяти? Решение следующее — поскольку все эти адреса кратны 16 (см. ранее), то в них младшие 4 бита (последняя шестнадцатеричная цифра) всегда нулевые, а потому эти биты можно не хранить явно, а лишь подразумевать. Именно так и происходит — в сегментном регистре всегда хранятся только первые 16 битов (первые четыре шестнадцатеричные цифры) начального адреса сегмента (эта величина называется номером сегмента или просто сегментом). При вычислении же абсолютного адреса Аабс по паре S: D процессор сначала приписывает справа к содержимому регистра s четыре нулевых бита (другими словами, умножает на 16) и лишь затем прибавляет смещение D, причем суммирование ведется по модулю 220:

Аабс = 16 х [ S ] +D (mod 220).

Если, например, в регистре CS хранится величина 1234h, тогда адресная пара 1234h:507h определяет абсолютный адрес, равный 16х1234h + 507h = 12340h + 507h = 12847h.

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

С учетом такого распределения ролей сегментных регистров машинные программы обычно строятся так: все команды программы размещаются в одном сегменте памяти, начало которого заносится в регистр cs, а все данные размещаются в другом сегменте, начало которого заносится в регистр DS; если нужен стек, то под него отводится третий сегмент памяти, начало которого

Таблица 3.17. Сегментные регистры по умолчанию

Регистр

Умолчания

Комментарий

СБ

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

Абсолютный адрес очередной команды, подлежащей выполнению, всегда задается парой СБ:1Р; в счетчике команд 1Р всегда находится смещение этой команды относительно адреса из регистра СБ

ЭБ

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

Во всех ссылках на этот сегмент регистр ОБ можно явно не указывать, так как он подразумевается по умолчанию

ББ

Указывает на стек — область памяти, доступ к которой осуществляется по принципу «последним записан — первым считан»

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

ЕБ

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

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

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

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

МОУ АХ, X,

имя х воспринимается как ссылка на данное, а потому автоматически восстанавливается до адресной пары ОБ:Х. В команде же безусловного перехода по адресу, находящемуся в регистре вх,

ДМР вх,

абсолютный адрес перехода определяется парой СБ: [ВХ].

Если в ссылке на какую-то ячейку памяти не указан явно сегментный регистр, то этот регистр берется по умолчанию.

Явно же сегментные регистры надо указывать, только если по каким-то причинам регистр по умолчанию не подходит. Если, например, в команде пересылки нам надо сослаться на стек (скажем, надо записать в регистр АН байт стека, помеченный именем х), тогда уже будет недостаточно предположения о том, что по умолчанию операнд команды моу будет сегментироваться по регистру ББ, и потому следует явно указать иной регистр — в данном случае регистр бэ, так как именно он указывает на стек:

МОУ АН, ББ : X.

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

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

ББ:

МОУ АН,X.

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

Если же нужен доступ к памяти, тогда на втором этапе исполнительный адрес рассматривается как смещение и к нему прибавляется (умноженное на 16) содержимое сегментного регистра, указанного явно или взятого по умолчанию и в результате вычисляется абсолютный (физический) 20-битовый адрес, по которому фактически происходит обращение к памяти (см. рис. 3.22).

Отметим, что сегментный регистр учитывается только непосредственно перед обращением к памяти, а до этого работа ведется только с 16-битовыми адресами. Если учесть к тому же, что сегментные регистры, как правило, не указываются в командах, то можно считать, что І80х86 работает с 16-битовыми адресами.

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

• в командах перехода адрес перехода сегментируется по регистру СБ и только по нему, так как абсолютный адрес команды, которая должна быть выполнена следующей, всегда определяется парой СБ:1Р (попытка изменить в таких командах сегментный регистр будет безуспешной). Сегментирование по регистру СБ касается именно адреса перехода, а не адреса той ячейки, где он может находиться. Например, в команде безусловного перехода по адресу, находящемуся в ячейке х:

ДМР X,

имя х сегментируется по регистру об, а вот адрес перехода, взятый из ячейки х, уже сегментируется по регистру сб;

  • • адреса во всех других командах, кроме строковых (бтоб, моуб, бсаб и смрб), сегментируются по умолчанию, а именно:
  • — по регистру об, если среди указанных регистров-модификаторов нет регистра вр;
  • — по регистру ББ, если один из модификаторов — регистр ВР.

Таким образом, адреса вида а, а[вх], а [бі], а[оі], а [вх] [БI] и А[вх] [01] сегментируются по регистру ОБ, а адреса а[вр] , а[вр] [ б і ] и а[вр] [оі] — по регистру бб, т. е. адреса трех последних видов используются для доступа к ячейкам стека;

• в строковых командах stos, movs, scas и cmps, имеющих два операнда-адреса, на которые указывают индексные регистры SI и DI, один из операндов (на который указывает si) сегментируется по регистру DS, а другой (на него указывает DI) — ПО регистру ES.

Программные сегменты. Директива ASSUME. Рассмотрим, как сегментирование проявляется в программах на MASM. Для того чтобы указать, что некоторая группа операторов программы образует единый сегмент памяти, они оформляются как программный сегмент: перед ними ставится директива segment, после них — директива ends, причем в начале обеих этих директив должно быть указано одно и то же имя, играющее роль имени сегмента. Программа же в целом представляет собой последовательность таких программных сегментов, в конце которой указывается директива конца программы end, например:

DTI SEGMENT; Программный СЄГМЄНТ С ИМЄНЄМ DT1 A DB О В DW ?

DTI ENDS

г

DT2 SEGMENT; Программный СЄГМЄНТ DT2 С DB 'hello'

DT2 ENDS

f

CODE SEGMENT; программный СЄГМЄНТ CODE ASSUME CS:CODE, DS:DT1, ES:DT2 PROG: MOV AX,DT2 MOV DS,AX MOV BH,C • • •

CODE ENDS

end PROG; конец текста программы.

Предложения программного сегмента ассемблер размещает в одном сегменте памяти (в совокупности они не должны занимать более 64 Кбайт), начиная с ближайшего свободного адреса, кратного 16. Номер (первые 16 битов начального адреса) этого сегмента становится значением имени сегмента. В MASM это имя относится к константным выражениям, а не адресным, поэтому в команде

МОУ АХ,БТ2, второй операнд является непосредственным, поэтому в регистр ах будет записано начало (номер) сегмента DT2, а не содержимое начальной ячейки этого сегмента.

Имена же переменных (а, в, с) и метки (prog) относятся к адресным выражениям, и им ставится в соответствие адрес их ячейки относительно «своего» сегмента:

  • • имени а соответствует адрес 0;
  • • имени в — адрес 1;
  • • имени с — адрес 0, а метке prog — адрес 0.

Все ссылки на предложения одного программного сегмента ассемблер сегментирует по умолчанию по одному и тому же сегментному регистру, по какому именно — устанавливается специальной директивой assume. В приведенном примере эта директива определяет, что все ссылки на сегмент CODE, если явно не указан сегментный регистр, должны сегментироваться по регистру CS, все ссылки на DT1 — по регистру ds, а все ссылки на DT2 — ПО регистру ES.

Встретив в тексте программы ссылку на какое-либо имя (например, на имя с в команде mov ах, с), ассемблер определяет, в каком именно программном сегменте оно описано (здесь — в DT2), затем по информации из директивы assume «узнает», какой сегментный регистр поставлен в соответствие этому сегменту (в данном случае — это ES), и далее образует адресную пару из данного регистра и смещения имени (здесь — ES: 0), которую и записывает в формируемую машинную команду. При этом ассемблер учитывает используемое в i80x86 соглашение о сегментных регистрах по умолчанию — если в адресной паре, построенной им самим или явно заданной в программе, сегментный регистр совпадает с регистром по умолчанию, то в машинную команду заносится лишь смещение. Если, скажем, встретится команда

MOV сх,в,

тогда по имени в ассемблер построит пару DS:1, но если операнд — адрес команды mov — по умолчанию сегментируется по регистру DS, то записывать этот регистр в машинную команду излишне, и ассемблер записывает в нее только смещение 1.

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

Однако все это справедливо только при соблюдении следующих условий:

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

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

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

MOV АХ,DT1 ;АХ := начало сегмента DT1

MOV DS,АХ ;DS := АХ.

Аналогично загружается регистр es.

Загружать регистр CS в начале программы не надо: он, как и счетчик команд IP, загружается операционной системой перед тем, как начинается выполнение программы (иначе нельзя было бы начать ее выполнение). Что же касается регистра SS, используемого для работы со стеком, то он может быть загружен так же, как и регистры DS и ES, однако в MASM предусмотрена возможность загрузки этого регистра еще до выполнения программы.

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

В подобной ситуации ассемблер действует следующим образом:

  • • если в команде встретилась ссылка вперед, то он делает некоторое предположение относительно этого имени и уже на основе этого предположения формирует машинную команду;
  • • если затем (когда встретится описание имени) окажется, что данное предположение было неверным, тогда ассемблер пытается исправить сформированную ранее машинную команду. Однако это не всегда удается: если правильная машинная команда должна занимать больше места, чем машинная команда, построенная на основе предположения (например, перед командой надо на самом деле вставить префикс замены сегмента), тогда ассемблер фиксирует ошибку (как правило, это ошибка номер 6: Phase error between passes).

Какие же «предположения» делает ассемблер, встречая ссылку вперед?

Во всех командах, кроме команд перехода, ассемблер «предполагает», что имя будет описано в сегменте данных и потому сегментируется по регистру DS. Это следует учитывать при составлении программы: если в команде встречается ссылка вперед на имя, которое описано в сегменте, на начало которого указывает сегментный регистр, ОТЛИЧНЫЙ ОТ DS, то перед таким именем автор программы должен написать соответствующий префикс. Пример:

CODE SEGMENT ASSUME CS:CODE X DW ?

beg: mov ax,x ;здесь вместо cs:x можно записать просто X MOV CS: Y, АХ ;здесь обязательно надо записать CS: Y

• • •

Y DW ?

CODE ENDS.

Команды перехода (ветвления программы)

В систему команд 180x86 входит обычный для ЭВМ набор команд перехода — безусловные и условные переходы, переходы с возвратами и др. Однако в 180x86 эти команды имеют некоторые особенности, которые здесь и рассматриваются.

Абсолютный адрес команды, которая должна быть выполнена следующей, определяется парой СБ:1Р, поэтому выполнение перехода означает изменение этих регистров, обоих или только одного (1Р):

  • • если изменяется только счетчик команд 1Р, то такой переход называется внутрисегментным или ближним (управление остается в том же сегменте команд);
  • • если меняются оба регистра СБ и 1Р, то это межсегментный или дальний переход (начинают выполняться команды из другого сегмента команд).

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

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

По величине сдвига относительные переходы делятся на короткие (сдвиг задается байтом) и длинные (сдвиг-слово).

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

  • • при прямом переходе адрес перехода задается в самой команде;
  • • при косвенном — в команде указывается регистр (ячейка памяти), в котором (которой) находится адрес перехода.

Безусловные переходы. В МА8М все команды безусловного перехода обозначаются одинаково:

ДМР ор,

но в зависимости от типа операнда ассемблер формирует разные машинные команды.

Внутрисегментный относительный короткий переход.

ДМР 18 (IР: =1Р+з.8) .

Здесь 18 обозначает непосредственный операнд размером в 1 байт, который интерпретируется как знаковое целое от -128 до 127. Команда прибавляет это число к текущему значению регистра 1Р, получая в нем адрес (смещение) той команды, которая должна быть выполнена следующей. Регистр СБ при этом не меняется.

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

При написании машинной программы сдвиги для относительных переходов приходится вычислять вручную, однако МАБМ избавляет от этого неприятного занятия: в МАБМ в командах относительного перехода всегда указывается метка той команды, на которую надо передать управление, и ассемблер сам вычисляет сдвиг, который он и записывает в машинную команду. Отсюда следует, что в МА8М команда перехода по метке воспринимается не как абсолютный переход, а как относительный.

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

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

JMP i1 б (IP:=IP+il6).

Здесь Л.16 обозначает непосредственный операнд размером в слово, который рассматривается как знаковое целое от -32 768 до 32 767. Этот переход аналогичен короткому переходу.

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

JMP SHORT L.

Для переходов назад оператор short не нужен — «зная» адрес метки, ассемблер сам определит вид команды относительного перехода.

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

JMP rl6 (IP:=[г])

ИЛИ

JMP гп1б (IP : = [ml б ] ) .

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

Межсегментный абсолютный прямой переход.

ДМР БедгоТэ (СБ := эед, 1Р := оТэ).

Здесь вед — начало (первые 16 битов начального адреса) некоторого сегмента памяти, а — смещение в этом сегменте. Пара эедгс^э определяет абсолютный адрес, по которому делается переход. В МАБМ эта пара всегда задается конструкцией рак рти <метка>, которая указывает, что надо сделать переход по указанной метке, причем эта метка — «дальняя», из другого сегмента. Отметим, что ассемблер сам определяет, какой это сегмент, и сам подставляет в машинную команду его начало, т. е. Бед.

Межсегментный абсолютный косвенный переход.

ДМР ш32 (СБ := [т32 +2], 1Р := [т32]).

Здесь под т32 понимается адрес двойного слова памяти, в котором находится пара зедгс^Б, задающая абсолютный адрес, по которому данная команда должна выполнить переход. Напомним, что в 180x86 величины размером в двойное слово хранятся в «перевернутом» виде, поэтому смещение о?б находится в первом слове двойного слова ш32, а смещение эед — во втором слове (по адресу т32+2).

Команды межсегментного перехода используются тогда, когда команды программы размещены не в одном сегменте памяти, а в нескольких (например, если команд так много, что в совокупности они занимают более 64 Кбайт, т. е. больше максимального размера сегмента памяти). При переходе из одного такого сегмента в другой необходимо менять не только счетчик команд 1Р, но и содержимое регистра СБ, загружая в последний начальный адрес второго сегмента. Такое одновременное изменение обоих этих регистров и делают команды межсегментного перехода.

Пр облемы команд перехода. При записи в МАБМ команд перехода следует учитывать, что они могут восприниматься неоднозначно.

Вопрос — как воспринимать команду jmp а:

  • • как переход по метке а;
  • • как переход по адресу, хранящемуся в ячейке с именем А?

Кроме того, какой это переход — внутрисегментный или

межсегментный? Ответ зависит от того, как описано имя А, и от того, когда описано имя А — до команды перехода или после нее.

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

В случае же, если имя А описано после команды перехода (<ссылка вперед), ассемблер всегда формирует машинную команду внутрисегментного относительного длинного перехода. С учетом этого имя А обязательно должно метить команду из текущего сегмента команд, иначе будет зафиксирована ошибка. Если такая трактовка ссылки вперед не удовлетворяет автора программы, тогда он обязан с помощью оператора short или ptr уточнить тип имени а:

JMP SHORT А ; внутрисегментный короткий переход по метке;

JMP WORD PTR А ; внутрисегментный косвенный переход;

JMP DWORD РТЕ А; межсегментный КОСВеННЫЙ Переход.

Отметим, что переход по метке А из другого сегмента команд всегда должен указываться с помощью far ptr (независимо от того, описана метка а до команды перехода или после нее):

JMP FAR PTR А ; межсегментный Переход ПО Метке.

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

CMP ор1,ор2, которая вычисляет разность opl -ор2, однако результат никуда не записывает, а только меняет флаги, на которые будет реагировать команда условного перехода.

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

Jxx op,

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

Примеры некоторых мнемоник:

JE - переход «по равно» (jump if equal);

JNL — переход «по не меньше» (jump if not less).

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

Такая особенность команд условного перехода вызывает неудобство при переходах на «дальние» команды. Например, если надо сделать переход при А < в на команду, помеченную меткой L и расположенную далеко от команды перехода, то приходится использовать команду длинного безусловного перехода:

MOV АХ,А

СМР АХ, в ; сравнение А И В

jnl м ; не меньше —> м (обход команды JMP)

JMP L ; меньше --> L (длинный Переход)

М: ...

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

В 180x86 есть несколько команд, упрощающих программирование циклов с заранее известным числом повторений. Применение этих команд требует, чтобы к началу цикла в регистр сх было занесено число шагов цикла. Сами команды размещаются в конце цикла, они уменьшают значение сх на 1 и, если сх еще не равно 0, передают управление на начало цикла. Например, найти s — сумму элементов массива х из 10 чисел-слов можно так:

MOV АХ, 0 ; начальное значение суммы (накапливается в АХ)

MOV SI, 0 ; начальное значение индексного регистра

MOV сх,10 ; число повторений цикла

L: ADD АХ,X[SI] ; AX:=AX+X[i]

ADD SI,2 ; SI:=SI+2

LOOP L ; CX: =CX-1 ; if CXoO then goto L

MOV S,AX ; S:=AX.

Помимо команды loop есть еще две «циклические» команды — loopz и loopnz (они имеют синонимичные названия LOOPE и loopne), которые, кроме регистра сх, проверяют еще и флаг нуля ZF; например, команда loopz «выходит» из цикла, если сх = 0 или ZF = 1. Эту команду можно, например, использовать при поиске в массиве первого нулевого элемента, где должно быть предусмотрено два условия выхода из цикла: либо будет найден нулевой элемент (ZF = 1, если перед LOOPZ поставить команду сравнения очередного элемента с 0), либо будет исчерпан весь массив (сх = 0).

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

В MASM есть еще две команды перехода — call (переход с возвратом) и ret (возврат из подпрограммы).

Строковые операции

В 180x86 под строкой понимается последовательность соседних байтов или слов. В связи с этим все строковые команды имеют две разновидности:

  • • для работы со строками из байтов (в мнемонику операций входит буква в);
  • • для работы со строками из слов (в мнемонику входит до). Имеются следующие операции над строками:
  • • пересылка элементов строк (в память, из памяти, память-память);
  • • сравнение двух строк;
  • • просмотр строки с целью поиска элемента, равного заданному.

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

Кроме того, строки можно просматривать вперед (от их начала к концу) и назад. Направление просмотра зависит от флага направления DF, значение которого можно менять с помощью команд STD (DF := 1) И CLD (DF := 0). При DF = 0 все последующие строковые команды программы просматривают строки вперед, а при df = 1 — назад.

В строковых командах операнды явно не указываются, а подразумеваются. Если команда работает с одной строкой, то адрес очередного, обрабатываемого сейчас элемента строки задается парой регистров DS и si или парой ES и DI, а если команда работает с двумя строками, то адрес элемента одной из них определяется парой ds :Si, а адрес элемента другой — парой ES: DI. После выполнения операции значение регистра si и/или DI увеличивается (при DF = 0) или уменьшается (при DF = 1) на 1 (для байтовых строк) или на 2 (для строк из слов).

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

LEA SI,<начальный/конечный адрес строкиХ

Если же надо загрузить сразу оба регистра DS и si, тогда можно воспользоваться командой

LDS SI,гп32,

которая в регистр si заносит первое слово, а в регистр DS — второе слово из двойного слова, имеющего адрес m32 (таким образом, по адресу т32+2 должен храниться сегмент, а по адресу m32 — смещение начального или конечного элемента строки).

Начальную загрузку регистров es и di обычно осуществляют одной командой:

LES DI,m32, которая действует аналогично команде LDS.

Перечислим вкратце строковые команды І80х86:

  • • команда загрузки элемента строки в аккумулятор (lodsb или lodsw) пересылает в регистр AL или ах очередной элемент строки, на который указывает пара DS:SI, после этого увеличивает (при DF = 0) или уменьшает (при DF = 1) регистр si на 1 или 2;
  • • запись аккумулятора в строку (stosb или stosw); содержимое регистра AL или АХ заносится в тот элемент строки, на который указывает пара ES:DI, затем изменяет регистр DI на 1 или 2;
  • • пересылка строк (movsb или movsw); элемент первой строки, определяемый парой DS:Si, заносится в элемент второй строки, определяемый парой ES:DI, затем одновременно меняет регистры SI и DI;
  • • сравнение строк (cmpsb или cmpsw); сравниваются очередные элементы строк, указываемые парами DS:SI и ES:DI, и результат сравнения (равно, меньше и т. п.) фиксирует в флагах, после этого меняется содержание регистров si и di;
  • • сканирование строки (scasb или scasw); сравнивается элемент строки, адрес которого задается парой ES:DI, со значением регистра al или ах и результат сравнения фиксирует в флагах, затем изменяется содержимое регистра DI.

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

Префикс повторения REPZ (синонимы — repe, rep) сначала заносит 1 в флаг нуля ZF, после этого, постоянно уменьшая сх на 1, заставляет повторяться следующую за ним строковую команду до тех пор, пока в сх не окажется «0» или пока флаг zf не изменит свое значение на «0».

Другой префикс повторения REPNZ (синоним — repne) действует аналогично, но только вначале устанавливает флаг zf

в «О», а при изменении его на «1» прекращает повторение строковой команды.

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

CLD

MOV СХ,1000 MOV АХ,DS MOV ES,АХ LEA SI,А LEA DI,В REP MOVSB

; ЭЕ: =0 (просмотр строки вперед) ; сх — число повторений

; ЕЭ:=ОЭ

; ЕБ : Э1 — «Откуда»

; 03:01 — «куда»

; пересылка СХ байтов.

Стек. Подпрограммы

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

Во многих случаях программе требуется временно запомнить информацию, а затем считывать ее в обратном порядке. Эта проблема в ПК решена посредством реализации стека LIFO («последний пришел — первый ушел»), называемого также стеком включения/извлечения (stack — кипа, например бумаг). Наиболее важное использование стека связано с процедурами. Стек обычно рассчитан на косвенную адресацию через регистр SP — указатель стека. При включении элементов в стек производится автоматический декремент указателя стека, а при извлечении — инкремент, т. е. стек всегда «растет» в сторону меньших адресов памяти. Адрес последнего включенного в стек элемента называется вершиной стека (TOS).

Под стек можно отвести область в любом месте памяти. Размер ее может быть любым, но не должен превосходить 64 Кбайт, а ее начальный адрес должен быть кратным 16. Другими словами, эта область должна быть сегментом памяти; он называется сегментом стека. Начало этого сегмента (первые 16 битов начального адреса) должно обязательно храниться в сегментном регистре ss.

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

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

Значение «О» в регистре SP свидетельствует о том, что стек полностью заполнен (его вершина «дошла» до начала области стека). Поэтому для контроля за переполнением стека надо перед новой записью в стек проверять условие SP = 0 (i80x86 этого не делает). Для пустого стека значение SP должно равняться размеру стека, т. е. пара SS:SP должна указывать на байт, следующий за последним байтом области стека. Контроль за чтением из пустого стека, если надо, обязана делать сама программа.

Начальная установка регистров ss и sp может быть произведена в самой программе, однако в MASM предусмотрена возможность автоматической загрузки этих регистров. Если в директиве segment, начинающей описание сегмента стека, указать параметр stack, тогда ассемблер (точнее, загрузчик) перед тем, как передать управление на первую команду машинной программы, загрузит в регистры ss и SP нужные значения. Например, если в программе сегмент стека описан следующим образом:

ST SEGMENT STACK

db 256 dup (?) ; размер стека — 256 байтов

ST ENDS,

и если под этот сегмент была выделена область памяти, начиная с абсолютного адреса 1234Oh, тогда к началу выполнения программы в регистре SS окажется величина 1234h, а в регистре SP — величина 10Oh (=256).

Отметим, что эти значения соответствуют пустому стеку.

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

Запись слова в стек:

PUSH op

Здесь op обозначает любой 16-битовый регистр (в том числе и сегментный) или адрес слова памяти. По этой команде значение регистра SP уменьшается на 2 (вычитание происходит по модулю 216), затем указанное операндом слово записывается в стек по адресу SS: SP.

Чтение слова из стека:

POP op

Слово, считанное из вершины стека, присваивается операнду ор (регистру, в том числе сегментному, но не CS, или слову памяти), затем значение SP увеличивается на 2.

Переход с возвратом:

CALL ор

Эта команда записывает адрес следующей за ней команды в стек и затем делает переход по адресу, определяемому операндом ор. Она используется для переходов на подпрограммы с запоминанием в стеке адреса возврата. Имеются следующие разновидности этой команды (они аналогичны вариантам команды безусловного перехода jmp):

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

рес (смещение) той команды, на которую и будет сделан переход); и здесь в стек записывается только смещение адреса возврата;

  • межсегментный абсолютный прямой переход (ор — непосредственный операнд вида SEG:OFS, а в MASM — это far ptr <метка> или имя дальней процедуры); здесь в стек заносятся текущие значения регистров CS и IP (первым в стек записывается содержимое CS), т. е. абсолютный адрес возврата, затем изменяются регистры CS и ip;
  • межсегментный абсолютный косвенный переход (о р — адрес двойного слова, в котором находится пара seg : of s, задающая абсолютный адрес перехода); и здесь в стеке спасается содержимое регистров с s и IP.

Переход (возврат) по адресу из стека:

RET ор

Из стека считывается адрес и по нему производится переход. Если указан операнд (а это должно быть неотрицательное число), то после чтения адреса стек еще очищается на это число байтов (к SP добавляется это число). Команда используется для возврата из подпрограммы по адресу, записанному в стек по команде call при вызове подпрограммы, и одновременной очистки стека от параметров, которые основная программа занесла в стек перед обращением к подпрограмме.

Команда ret имеет две разновидности (хотя в MASM они записываются одинаково):

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

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

Прерывания и подпрограммы

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

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

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

Прерывание вынуждает процессор прекратить выполнение одной последовательности команд и начать выполнение другой, при этом адрес очередной команды, которая должна была бы выполняться (содержимое регистра 1Р), если бы не было прерывания, запоминается. Адрес команды, которая должна выполняться после возникновения прерывания, выбирается из таблицы, хранящейся в начальной области памяти. Эта таблица называется таблицей векторов прерываний. В таблице записано 256 адресов. Когда устройство вызывает прерывание процессора, оно сообщает ему, какой адрес из таблицы следует использовать для перехода к новой последовательности команд.

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

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

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

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

PUSH paraml ; запись 1-го параметра в стек

• • •

push paramk ; запись последнего (к-то) параметра в стек

CALL subr ; переход с возвратом на подпрограмму.

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

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

push вр ; сохранить в стеке старое значение вр

MOV SP, ВР ; установить ВР на вершину стека

SUB SP,m ; отвести в стеке место байтов) под локальные

величины подпрограммы.

Поясним эти «входные» команды. В подпрограмме для обращения к ячейкам стека, занятым параметрами, используется (как базовый) регистр вр: если в вр занести адрес вершины стека, то для доступа к этим ячейкам следует использовать адресные выражения вида i [вр] или, что то же самое, [ВР+i]. (Отметим, что применять здесь регистры-модификаторы ВХ, Б1 И 01 нельзя, так как формируемые по ним исполнительные адреса будут сегментироваться по умолчанию по регистру ОБ, а в данном случае следует сегментировать по ББ.) Однако данная подпрограмма может быть вызвана из другой, также использующей регистр вр, поэтому прежде чем установить ВР на «вершину стека», надо сохранить в стеке старое значение этого регистра, что и делает первая из «входных» команд.

Вторая же команда устанавливает ВР на «вершину стека». Если предположить, что каждый параметр и адрес возврата занимают по слову памяти, тогда доступ к первому параметру обеспечивается адресным выражением [вр+4], ко второму — выражением [ВР+б] и т. д.

Подпрограмме может потребоваться место для ее локальных величин. Такое место обычно отводится в стеке (а для рекурсивных подпрограмм только в стеке) «над» ячейкой, занимаемой старым значением ВР. Если под эти величины нужно т байтов, то такой «захват» места можно реализовать простым уменьшением значения регистра БР на т, что и делает 3-я «входная» команда. Доступ к локальным величинам обеспечивается адресными выражениями вида [ВР-л.]. Если подпрограмме не нужно место под локальные величины, тогда третью из «входных» команд следует опустить.

Выход из подпрограммы реализуется следующими командами:

МОУ БР, ВР ; очистить стек от локальных величин

рор вр ; восстановить старое значение вр

ИЕТ 2хк ; возврат из подпрограммы и очистка стека

от параметров (предполагается, что они занимают 2хк байтов).

Первая из этих «выходных» команд заносит в регистр БР адрес той ячейки стека, которая содержит «старое» значение регистра ВР, т. е. происходит очистка стека от локальных величин (если их не было, то данную команду следует опустить). Вторая команда восстанавливает в ВР это старое значение, одновременно удаляя его из стека. В этот момент состояние стека будет таким же, как и перед входом в подпрограмму. Третья команда считывает из стека адрес возврата (в результате бр «опускается» на 2 байта), затем добавляет к БР число, которое должно равняться числу байтов, занимаемых всеми параметрами подпрограммы, и затем осуществляет переход по адресу возврата. В этот момент состояние стека будет таким же, каким оно было перед обращением к подпрограмме.

Контрольные вопросы

  • 1. Что такое система команд? Что такое команда? Что описывает команда?
  • 2. Какого рода информацию может содержать адресная часть команды?
  • 3. Приведите примеры команд одно-, двух-, трехадресных.
  • 4. Каким образом процессор при выполнении программы осуществляет выбор очередной команды?
  • 5. Опишите основной цикл процесса обработки команд.
  • 6. В чем различие функций СчАК и РК?
  • 7. Что такое индексирование и базирование?
  • 8. Что такое регистровая память?
  • 9. Какие типы процессоров вам известны?
  • 10. Что такое суперскаляризация?
  • 11. В чем заключается технология конвейерной обработки?
  • 12. Охарактеризуйте технологию динамического исполнения.
  • 13. Что такое 3D Now?
  • 14. Перечислите основные этапы развития процессоров Intel, AMD, Cyrix.
 
<<   СОДЕРЖАНИЕ   >>