ЯЗЫК МАКРОАССЕМБЛЕРА IBM PC (Справочное пособие)
Составитель: В.Н.Пильщиков (МГУ, ВМК) (январь 1992 г.)
В пособии рассматривается язык макроассеблера для персональных ЭВМ типа IBM PC (язык MASM, версия 4.0).
Пособие состоит из 4 глав. В главе 1 рассмотрены особенности персональных компьютеров типа IBM PC и приведены начальные сведения о языке MASM. В главе 2 описывается система команд этих компьютеров. Глава 3 посвящена посвящена собственно языку MASM. В главе 4 приведены примеры фрагментов программ и полных программ на MASM для решения различных задач.
В пособии не рассматриваются вопросы, связанные с обработкой двоично-десятичных чисел и работой арифметического сопроцессора 8087 или 80287.
Под термином "ПК" в пособии понимается персональный компьютер типа IBM PC c микропроцессором 8088/8086, 80186 или 80286.
ГЛАВА 1. ОСОБЕННОСТИ ПК. ВВЕДЕНИЕ В MASM.
1.1. ОПЕРАТИВНАЯ ПАМЯТЬ. РЕГИСТРЫ.
1.1.1 Оперативная память
Объем оперативной памяти ПК - 2^20 байтов (1 Мб). Байты нумеруются начиная с 0, номер байта называется его адресом. Для ссылок на байты памяти используются 20-разрядные адреса: от 00000 до FFFFF (в 16-ричной системе).
Байт содержит 8 разрядов (битов), каждый из которых может принимать значение 1 или 0. Разряды нумеруются справа налево от 0 до 7:
-----------------
| | | | | | | | |
-----------------
7 6 5 4 3 2 1 0
Байт - это наименьшая адресуемая ячейка памяти. В ПК используются и более крупные ячейки - слова и двойные слова. Слово - это два соседних байта, размер слова - 16 битов (они нумеруются справа налево от 0 до 15). Адресом слова считается адрес его первого байта (с меньшим адресом); этот адрес может быть четным и нечетным. Двойное слово - это любые четыре соседних байта (два соседних слова), размер такой ячейки - 32 бита; адресом двойного слова считается адрес его первого байта.
Байты используются для хранения небольших целых чисел и символов,
слова - для хранения целых чисел и адресов, двойные слова - для хранения "длинных" целых чисел и т.н. адресных пар (сегмент:смещение).
1.1.2 Регистры
Помимо ячеек оперативной памяти для хранения данных (правда, кратковременного) можно использовать и регистры - ячейки, входящие в состав процессора и доступные из машинной программы. Доступ к регистрам осуществляется значительно быстрее, чем к ячейкам памяти, поэтому использование регистров заметно уменьшает время выполнения программ.
Все регистры имеют размер слова (16 битов), за каждым из них закреплено определенное имя (AX, SP и т.п.). По назначению и способу использования регистры можно разбить на следующие группы:
- регистры общего назначения (AX, BX, CX, DX, BP, SI, DI, SP);
- сегментные регистры (CS, DS, SS, ES);
- счетчик команд (IP);
- регистр флагов (Flags).
(Расшифровка этих названий: A - accumulator, аккумулятор; B - base, база; C - counter, счетчик; D - data, данные; BP - 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, счетчик команд.)
Регистры общего назначения можно использовать во всех арифметических и логических командах. В то же время каждый их них имеет определенную специализацию (некоторые команды "работают" только с определенными регистрами). Например, команды умножения и деления требуют, чтобы один из операндов находился в регистре AX или в регистрах AX и DX (в зависимости от размера операнда), а команды управления циклом используют регистр CX в качестве счетчика цикла. Регистры BX и BP очень часто используются как базовые регистры, а SI и DI - как индексные. Регистр SP обычно указывает на вершину стека, аппаратно поддерживаемого в ПК.
Регистры AX, BX, CX и DX конструктивно устроены так, что возможен независимый доступ к их старшей и младшей половинам; можно сказать, что каждый из этих регистров состоит из двух байтовых регистров, обозначаемых AH, AL, BH и т.д. (H - high, старший; L - low, младший):
----------- ----------- ----------- -----------
AX | AH | AL | BX | BH | BL | CX | CH | CL | DX | DH | DL |
----------- ----------- ----------- -----------
15 8 7 0
Таким образом, с каждым из этих регистров можно работать как с единым целым, а можно работать и с его "половинками". Например, можно записать слово в AX, а затем считать только часть слова из регистра AH или заменить только часть в регистре AL и т.д. Такое устройство регистров позволяет использовать их для работы и с числами, и с символами.
Все остальные регистры не делятся на "половинки", поэтому считать или записать их содержимое (16 битов) можно только целиком.
Сегментные регистры CS, DS, SS и ES не могут быть операндами никаких команд, кроме команд пересылки и стековых команд. Эти регистры используются только для сегментирования адресов (см. 1.4).
Счетчик команд IP всегда содержит адрес (смещение от начала программы) той команды, которая должна быть выполнена следующей (начало программы хранится в регистре CS). Содержимое регистра IP можно изменить только командами перехода.
1.1.3 Флаги
И, наконец, в ПК имеется особый регистр флагов. Флаг - это бит, принимающий значение 1 ("флаг установлен"), если выполнено некоторое условие, и значение 0 ("флаг сброшен") в противном случае. В ПК ис-
пользуется 9 флагов, каждому из них присвоено определенное имя (ZF, CF и т.д.). Все они собраны в регистре флагов (каждый флаг - это один из разрядов регистра, часть его разрядов не используется):
-------------------------------------------------
Flags | 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
Некоторые флаги принято называть флагами условий; они автоматически меняются при выполнении команд и фиксируют те или иные свойства их результата (например, равен ли он нулю). Другие флаги называются флагами состояний; они меняются из программы и оказывают влияние на дальнейшее поведение процессора (например, блокируют прерывания).
Флаги условий:
CF (carry flag) - флаг переноса. Принимает значение 1, если при сложении целых чисел появилась единица переноса, не "влезающая" в разрядную сетку, или если при вычитании чисел без знака первое из них было меньше второго. В командах сдвига в CF заносится бит, вышедший за разрядную сетку. CF фиксирует также особенности команды умножения.
OF (overflow flag) - флаг переполнения. Устанавливается в 1, если при сложении или вычитании целых чисел со знаком получился результат, по модулю превосходящий допустимую величину (произошло переполнение мантиссы и она "залезла" в знаковый разряд).
ZF (zero flag) - флаг нуля. Устанавливается в 1, если результат команды оказался равным 0.
SF (sign flag) - флаг знака. Устанавливается в 1, если в операции над знаковыми числами получился отрицательный результат.
PF (parity flag) - флаг четности. Равен 1, если результат очередной команды содержит четное количество двоичных единиц. Учитывается обычно только при операциях ввода-вывода.
AF (auxiliary carry flag) - флаг дополнительного переноса. Фиксирует особенности выполнения операций над двоично-десятичными числами.
Флаги состояний:
DF (direction flag) - флаг направления. Устанавливает направление просмотра строк в строковых командах: при DF=0 строки просматриваются "вперед" (от начала к концу), при DF=1 - в обратном направлении.
IF (interrupt flag) - флаг прерываний. При IF=0 процессор перестает реагировать на поступающие к нему прерывания, при IF=1 блокировка прерываний снимается.
TF (trap flag) - флаг трассировки. При TF=1 после выполнения каждой команды процессор делает прерывание (с номером 1), чем можно воспользоваться при отладке программы для ее трассировки.
1.2. ПРЕДСТАВЛЕНИЕ ДАННЫХ. АРИФМЕТИЧЕСКИЕ ОПЕРАЦИИ
Здесь рассматривается машинное представление целых чисел, строк и адресов. Представление двоично-десятичных чисел, используемых достаточно редко, не рассматривается. Что касается вещественных чисел, то в ПК нет команд вещественной арифметики (операции над этими числами реализуются программным путем или выполняются сопроцессором) и потому нет стандартного представления вещественных чисел. Кроме того, рассматриваются некоторые особенности выполнения арифметических операций.
Шестнадцатиричные числа записываются с буквой h на конце, двоичные
числа - с буквой b (так принято в MASM).
1.2.1 Представление целых чисел.
В общем случае под целое число можно отвести любое число байтов, однако система команд ПК поддерживает только числа размером в байт и слово и частично поддерживает числа размером в двойное слово. Именно эти форматы и будут рассмотрены.
В ПК делается различие между целыми числами без знака (неотрицательными) и со знаком. Это объясняется тем, что в ячейках одного и того же размера можно представить больший диапазон беззнаковых чисел, чем неотрицательных знаковых чисел, и если известно заранее, что некоторая числовая величина является неотрицательной, то выгоднее рассматривать ее как беззнаковую, чем как знаковую.
Целые числа без знака.
Эти числа могут быть представлены в виде байта, слова или двойного слова - в зависимости от их размера. В виде байта представляются целые от 0 до 255 (=2^8-1), в виде слова - целые от 0 до 65535 (=2^16-1), в виде двойного слова - целые от 0 до 4 294 967 295 (=2^32-1). Числа записываются в двоичной системе счисления, занимая все разряды ячейки. Например, число 130 записывается в виде байта 10000010b (82h).
Числа размером в слово хранятся в памяти в "перевернутом" виде: младщие (правые) 8 битов числа размещаются в первом байте слова, а старшие 8 битов - во втором байте (в 16-ричной системе: две правые цифры - в первом байте, две левые цифры - во втором байте). Например, число 130 (=0082h) в виде слова хранится в памяти так:
-----------
| 82 | 00 |
-----------
(Отметим, однако, что в регистрах числа хранятся в нормальном виде: -----------
AX | 00 | 82 |
-----------
AH AL )
"Перевернутое" представление используется и при хранении в памяти целых чисел размером в двойное слово: в первом его байте размещаются младшие 8 битов числа, во втором байте - предыдущие 8 битов и т.д. Например, число 12345678h хранится в памяти так:
---------------------
| 78 | 56 | 34 | 12 |
---------------------
Другими словами, в первом слове двойного слова размещаются младшие (правые) 16 битов числа, а во втором слове - старшие 16 битов, причем в каждом из этих двух слов в свою очередь используется "перевернутое" представление.
Такое необычное представление чисел объясняется тем, что в первых моделях ПК за раз можно было считать из памяти только один байт и что все арифметические операции над многозначными числами начинаются с действий над младшими цифрами, поэтому из памяти в первую очередь надо считывать младшие цифры, если сразу нельзя считать все цифры. Учитывая это, в первых ПК и стали размещать младшие цифры числа перед старшими цифрамми, а ради преемственности такое представление чисел сохранили в последующих моделях ПК.
Конечно, "перевернутое" представление неудобно для людей, однако при использовании языка ассемблера это неудобство не чувствуется: в MASM все числа записываются в нормальном, неперевернутом виде (см. ниже).
Целые числа со знаком.
Эти числа также представляются в виде байта, слова и двойного слова. В виде байта записываются числа от -128 до 127, в виде слова числа от -32768 до 32767, а в виде двойного слова - числа от -2147483648 до 2147483647. При этом числа записываются в дополнительном коде: неотрицательное число записывается так же, как и беззнаковое число (т.е. в прямом коде), а отрицательное число -x (x>0) представляется беззнаковым числом 2^8-x (для байтов), 2^16-x (для слов) или 2^32-x (для двойных слов). Например, дополнительным кодом числа -6 является байт FAh (=256-6), слово FFFAh или двойное слово FFFFFFFAh. При этом байт 10000000b (=80h) трактуется как -128, а не как +128 (слово 8000h понимается как -32678), поэтому левый бит дополнительного кода всегда играет роль знакового: для неотрицательных чисел он равен 0, для отрицательных - 1.
Знаковые числа размером в слово и двойное слово записываются в памяти в "перевернутом" виде (при этом знаковый бит оказывается в последнем байте ячейки). Но в MASM эти числа, как и беззнаковые, записываются в нормальной форме.
Иногда число-байт необходимо расширить до слова, т.е. нужно получить такое же по величине число, но размером в слово. Существует два способа такого расширения - без знака и со знаком. В любом случае исходное число-байт попадает во второй (до "переворачивания") байт слова, а вот первый байт заполняется по-разному: при расширении без знака в него записываются нулевые биты (12h -> 0012h), а при расширении со знаком в первый байт записываются нули, если число-байт было неотрицательным, и записывается восемь двоичных единиц в противном случае (81h -> FF81h). Другими словами, при расширении со знаком в первом байте слова копируется знаковый разряд числа-байта.
Аналогично происходит расширение числа-слова до двойного слова.
1.2.2 Особенности выполнения арифметических опреаций
В ПК имеются команды сложения и вычитания целых чисел размером в слово и байт. Специальных команд для сложения и вычитания двойных слов нет, эти операции реализуются через команды сложения и вычитания слов.
Сложение и вычитание беззнаковаых чисел производится по модулю 2^8
для байтов и 2^16 для слов. Это означает, что если в результате сложения появилась единица переноса, не вмещающаяся в разрядную сетку, то она отбрасывается. Например, при сложении байтов 128 и 130 получается число 258 = 100000010b, поэтому левая двоичная единица отбрасывается и остается число 2 = 10b, которое и объявляется результатом сложения. Ошибка здесь не фиксируется, но в флаг переноса CF записывается 1 (если переноса не было, в CF заносится 0). "Поймать" такое искажение суммы можно только последующим анализом флага CF.
Искажение результата происходит и при вычитание из меньшего числа большего. И здесь не фиксируется ошибка, однако первому числу дается "заем единицы" (в случае байтов это число увеличивается на 256, для
слов - на 2^16), после чего и производится вычитание. Например, вычитание байтов 2 и 3 сводится к вычитанию чисел 256+2=258 и 3, в результате чего получается неправильная разность 255 (а не -1). Для того чтобы можно было обнаружить такую ситуацию, в флаг переноса CF заносится 1 (если заема не было, в CF записывается 0).
Сложение и вычитание знаковых целых чисел производится по тем же алгоритмам, что и для беззнаковых чисел (в этом одно из достоинств дополнительного кода): знаковые числа рассматриваются как соответствующие беззнаковые числа, произодится операция над этими беззнаковыми числами и полученный результат интерпретируется как знаковое число. Например, сложение байтовых чисел 1 и -2 происходит так: берутся их дополнительные коды 1 и (256-2)=254, вычисляется сумма этих величин 1+254=255 и она трактуется как знаковое число -1 (255=256-1). Если при таком сложении возникла единица переноса, то она, как обычно, отбрасывается, а флаг CF получает значение 1. Однако в данном случае это отсечение не представляет интерес - результат операции будет правильным, например: 3+(-2) => 3+254(mod 256) = 257(mod 256) = 1. Зато здесь возможна иная неприятность: модуль суммы (ее мантисса) может превзойти допустимую границу и "залезть" в знаковый разряд, испортив его. Например, при сложении байтовых чисел 127 и 2 получается величина 129 = = 100001001b, представляющая дополнительный код числа -127 (=256-129).
Хотя результат здесь получился и неправильным, процессор не фиксирует ошибку, но зато заносит 1 в флаг переполнения OF (если "переполнения мантиссы" не было, в OF записывается 0). Анализируя затем этот флаг, можно "поймать" такую ошибку.
Таким образом, сложение (вычитание) знаковых и беззнаковых чисел производится по одному и тому же алгоритму. При этом ПК не "знает", какие числа (со знаком или без) он складывает; в любом случае он складывает их как беззнаковые числа и в любом случае формирует флаги CF и OF. А вот как интерпретировать слагаемые и сумму, на какой из этих флагов обращать внимание - это личное дело автора программы.
Что касается умножения и деления знаковых и беззнаковых чисел, то они выполняются по разным алгоритмам, разными машинными командами. Однако и у этих операций есть ряд особенностей. При умножении байтов (слов) первый сомножитель обязан находиться в регистре AL (AX), результатом же умножения является слово (двойное слово), которое заносится в регистр AX (регистры DX и AX). Тем самым при умножении сохраняются все цифры произведения. При делении байтов (слов) первый операнд (делимое) должен быть словом (двойным словом) и обязан находиться в регистре AX (регистрах DX и AX). Результатом деления являются две величины размером в байт (слово) - неполное частное (div) и остаток от деления (mod); неполное частное записывается в регистр AL (AX), а остаток - в регистр AH (DX).
1.2.3 Представление символов и строк
На символ отводится один байт памяти, в который записывается код символа - целое от 0 до 255. В ПК используется система кодировки ASCII (American Standard Code for Information Interchange). Она, естественно, не содержит кодов русских букв, поэтому в нашей стране применяется некоторый вариант этой системы с русскими буквами (обычно это альтернативная кодировка ГОСТа).
Некоторые особенности этих систем кодировки:
- код пробела меньше кода любой буквы, цифры и вообще любого графи-
чески представимого символа;
- коды цифр упорядочены по величине цифр и не содержат пропусков,
т.е. из неравенства код('0')<=код(c)<=код('9') следует, что c - цифра; - коды больших латинских букв упорядочены согласно алфавиту и не со-
держат пропусков; аналогично с малыми латинскими буквами;
- (в альтернативной кодировке ГОСТа) коды русских букв (как больших, так и малых) упорядочены согласно алфавиту, но между ними есть коды других символов.
Строка (последовательность символов) размещается в соседних байтах памяти (в неперевернутом виде): код первого символа строки записывается в первом байте, код второго символа - во втором байте и т.п. Адресом строки считается адрес ее первого байта.
В ПК строкой считается также и последовательность слов (обычно это
последовательность целых чисел). Элементы таких строк располагаются в последовательных ячейках памяти, но каждый элемент представлен в "перевернутом" виде.
1.2.4 Представление адресов
Адрес - это порядковый номер ячейки памяти, т.е. неотрицательное целое число, поэтому в общем случае адреса представляются так же, как и беззнаковые числа. Однако в ПК есть ряд особенностей в представлении адресов.
Дело в том, что в ПК термином "адрес" обозначают разные вещи. Часто под адресом понимается 16-битовое смещение (offset) - адрес ячейки, отсчитанный от начала сегмента (области) памяти, которому принадлежит эта ячейка. В этом случае под адрес отводится слово памяти, причем адрес записывается в "перевернутом" виде (как и числа-слова вообще).
В другом случае под "адресом" понимается 20-битовый абсолютный адрес некоторой ячейки памяти. В силу ряда причин в ПК такой адрес задается не как 20-битовое число, а как пара "сегмент:смещение", где "сегмент" (segment) - это первые 16 битов начального адреса сегмента памяти, которому принадлежит ячейка, а "смещение" - 16-битовый адрес этой ячейки, отсчитанный от начала данного сегмента памяти (величина 16*сегмент+смещение даетабсолютный адрес ячейки). Такая пара записывается в виде двойного слова, причем (как и для чисел) в "перевернутом" виде: в первом слове размещается смещение, а во втором - сегмент, причем каждое из этих слов в свою очередь представлено в "перевернутом" виде. Например, пара 1234h:5678h будет записана так:
---------------------
| 78 | 56 | 34 | 12 |
---------------------
смещение сегмент
1.2.5 Директивы определения данных
Для того чтобы в программе на MASM зарезервировать ячейки памяти под константы и переменные, необходимо воспользоваться директивами определения данных - с названиями DB (описывает данные размером в байт), DW (размером в слово) и DD (размером в двойное слово). (Директивы, или команды ассемблеру, - это предложения программы, которыми ее автор сообщает какую-то информацию ассемблеру или просит что-то сделать дополнительно, помимо перевода символьных команд на машинный язык.)
В простейшем случае в директиве DB, DW или DD описывается одна константа, которой дается имя для последующих ссылок на нее. По этой директиве ассемблер формирует машинное представление константы (в частности, если надо, "переворачивает" ее) и записывает в очередную ячейку памяти. Адрес этой ячейки становится значением имени: все вхождения имени в программу ассемблер будет заменять на этот адрес. Имена, указанные в директивах DB, DW и DD, называются именами переменных (в отличие от меток - имен команд).
В MASM числа записываются в нормальном (неперевернутом) виде в cистемах счисления с основанием 10, 16, 8 или 2. Десятичные числа записываются как обычно, за шестнадцатиричным числом ставится буква h (если число начинается с "цифры" A, B, ..., F, то вначале обязателен 0), за восьмиричным числом - буква q или o, за двоичным числом - буква b.
Примеры:
A DB 162 ;описать константу-байт 162 и дать ей имя A
B DB 0A2h ;такая же константа, но с именем B
С DW -1 ;константа-слово -1 с именем С
D DW 0FFFFh ;такая же константа-слово, но с именем D
E DD -1 ;-1 как двойное слово
Константы-символы описываются в директиве DB двояко: указывается либо код символа (целое от 0 до 255), либо сам символ в кавычках (одинарных или двойных); в последнем случае ассемблер сам заменит символ на его код. Например, следующие директивы эквивалентны (2A - код звездочки в ASCII):
CH DB 02Ah
CH DB '*' CH DB "*"
Константы-адреса, как правило, задаются именами. Так, по директиве
ADR DW CH
будет отведено слово памяти, которому дается имя ADR и в которое запишется адрес (смещение), соответствующий имени CH. Если такое же имя описать в директиве DD, то ассемблер автоматически добавит к смещению имени его сегмент и запишет смещение в первую половину двойного слова, а сегмент - во вторую половину.
По любой из директив DB, DW и DD можно описать переменную, т.е. отвести ячейку, не дав ей начального значения. В этом случае в правой части директивы указывается вопросительный знак:
F DW ? ;отвести слово и дать ему имя F, ничего в этот байт не ;записывать
В одной директиве можно описать сразу несколько констант и/или переменных одного и того же размера, для чего их надо перечислить через запятую. Они размещаются в соседних ячейках памяти. Пример:
G DB 200, -5, 10h, ?, 'F'
Имя, указанное в директиве, считается именующим первую из констант. Для ссылок на остальные в MASM используются выражения вида <имя>+<целое>; например, для доступа к байту с числом -5 надо указать выражение G+1, для доступа к байту с 10h - выражение G+2 и т.д.
Если в директиве DB перечислены только символы, например:
S DB 'a','+','b'
тогда эту директиву можно записать короче, заключив все эти символы в одни кавычки:
S DB 'a+b'
И, наконец, если в директиве описывается несколько одинаковых констант (переменных), то можно воспользоваться конструкцией повторения
k DUP(a,b,...,c)
которая эквивалентна повторенной k раз последовательности a,b,...,c. Например, директивы
V1 DB 0,0,0,0,0
V2 DW ?,?,?,?,?,?,?,?,?,'a',1,2,1,2,1,2,1,2
можно записать более коротко таким образом:
V1 DB 5 DUP(0)
V2 DW 9 DUP(?), 'a', 4 DUP(1,2)
1.3. ПРЕДСТАВЛЕНИЕ КОМАНД. МОДИФИКАЦИЯ АДРЕСОВ.
1.3.1 Структура команд. Исполнительные адреса
Машинные команды ПК занимают от 1 до 6 байтов.
Код операции (КОП) занимает один или два первых байта команды. В ПК столь много различных операций, что для них не хватает 256 различных КОПов, которые можно представить в одном байте. Поэтому некоторые операции объединяются в группу и им дается один и тот же КОП, во втором же байте этот КОП уточняется. Кроме того, во втором байте указываются типы и способ адресации операндов. Остальные байты команды указывают на операнды.
Команды могут иметь от 0 до 3 операндов, у большинства команд один или два операнда. Размер операндов - байт или слово (редко двойное слово). Операнд может быть указан в самой команде (это т.н. непосредственный операнд), либо может находиться в одном из регистров ПК и тогда в команде указывается этот регистр, либо может находиться в ячейке памяти и тогда в команде тем или иным способом указывается адрес этой ячейки. Некоторые команды требуют, чтобы операнд находился в фиксированном месте (например, в регистре AX), тогда операнд явно не указывается в команде. Результат выполнения команды помещается в регистр или ячейку памяти, из которого (которой), как правило, берется первый операнд. Например, большинство команд с двумя операндами реализуют действие
op1 := op1 _ op2
где op1 - регистр или ячейка, а op2 - непосредственный операнд, регистр или ячейка.
Адрес операнда разрешено модифицировать по одному или двум регистрам. В первом случае в качестве регистра-модификатора разрешено использовать регистр BX, BP, SI или DI (и никакой иной). Во втором случае один из модификаторов обязан быть регистром BX или BP, а другой -
регистром SI или DI; одновременная модификация по BX и BP или SI и DI недопустима. Регистры BX и BP обычно используются для хранения базы (начального адреса) некоторого участка памяти (скажем, массива) и потому называются базовыми регистрами, а регистры SI и DI часто содержат индексы элементов массива и потому называются индексными регистрами. Однако такое распределение ролей необязательно, и, например, в SI может находиться база массива, а в BX - индекс элемента массива.
В MASM адреса в командах записываются в виде одной из следующих конструкции:
A, A[M] или A[M1][M2],
где A - адрес, M - регистр BX, BP, SI или DI, M1 - регистр BX или BP, а M2 - регистр SI или DI. Во второрм и третьем варианте A может отсутствовать, в этом случае считается, что A=0.
При выполнении команды процессор прежде всего вычисляет т.н. исполнительный (эффективный) адрес - как сумму адреса, заданного в команде, и текущих значений указанных регистров-модификаторов, причем все эти величины рассматриваются как неотрицательные и суммирование
ведется по модулю 2^16 ([r] означает содержимое регистра r):
A : Aисп = A
A[M] : Aисп = A+[M] (mod 2^16)
A[M1][M2]: Aисп = A+[M1]+[M2] (mod 2^16)
Полученный таким образом 16-разрядный адрес определяет т.н. смещение - адрес, отсчитанный от начала некоторого сегмента (области) памяти. Перед обращением к памяти процессор еще добавляет к смещению начальный адрес этого сегмента (он хранится в некотором сегментном регистре), в результате чего получается окончательный 20-разрядный адрес, по которому и происходит реальное обращение к памяти (см. 1.4).
1.3.2 Форматы команд
В ПК форматы машинных команд достаточно разнообразны. Для примера приведем лишь основные форматы команд с двумя операндами.
1) Формат "регистр-регистр" (2байта):
------------- ----------------
| КОП |d|w| | 11 |reg1|reg2|
------------- ----------------
7 2 1 0 7 6 5 3 2 0
Команды этого формата описывают обычно действие reg1:=reg1_reg2 или
reg2:=reg2_reg1. Поле КОП первого байта указывает на операцию (_), которую надо выполнить. Бит w определяет размер операндов, а бит d указывает, в какой из регистров записывается результат:
w = 1 - слова d = 1 - reg1:=reg1_reg2
= 0 - байты = 0 - reg2:=reg2_reg1
Во втором байте два левых бита фиксированы (для данного формата), а трехбитовые поля reg1 и reg2 указывают на регистры, участвующие в операции, согласно следующей таблице:
reg w=1 w=0 reg w=1 w=0 ----------------- ----------------
000 AX AL 100 SP AH
001 CX CL 101 BP CH
010 DX DL 110 SI DH
011 BX BL 111 DI BH
2) Формат "регистр-память" (2-4 байта):
------------- ------------- -------------------
| КОП |d|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 (a8 - адрес размером в байт, a16
- адрес размером в слово):
mem \ mod | 00 01 10
------------------------------------------------------000 | [BX]+[SI] [BX]+[SI]+a8 [BX]+[SI]+a16
001 | [BX]+[DI] [BX]+[DI]+a8 [BX]+[DI]+a16
010 | [BP]+[SI] [BP]+[SI]+a8 [BP]+[SI]+a16
011 | [BP]+[DI] [BP]+[DI]+a8 [BP]+[DI]+a16
100 | [SI] [SI]+a8 [SI]+a16
101 | [DI] [DI]+a8 [DI]+a16
110 | a16 [BP]+a8 [BP]+a16
111 | [BX] [BX]+a8 [BX]+a16
Замечания. Если в команде не задан адрес, то он считается нулевым. Если адрес задан в виде байта (a8), то он автоматически расширяется со знаком до слова (a16). Случай mod=00 и mem=110 указывает на отсутствие регистров-модификаторов, при этом адрес должет иметь размер слова (адресное выражение [BP] ассемблер транслирует в mod=01 и mem=110 при a8=0). Случай mod=11 соответствует формату "регистр-регистр".
3) Формат "регистр-непосредственный операнд" (3-4 байта): ----------- ------------- --------------------------
| КОП |s|w| |11|КОП"|reg| |непосред.операнд (1-2 б)|
----------- ------------- --------------------------
Команды этого формата описывают операции reg:=reg_immed (immed - непосредственный операнд). Бит w указывает на размер операндов, а поле reg - на регистр-операнд (см. выше). Поле КОП в первом байте определяет лишь класс операции (например, класс сложения), уточняет же операцию поле КОП" из второго байта. Непосредственный операнд может занимать 1 или 2 байта - в зависимости от значения бита w, при этом операнд-слово записывается в команде в "перевернутом" виде. Ради экономии памяти в ПК предусмотрен случай, когда в операции над словами непосредственный операнд может быть задан байтом (на этот случай указывает 1 в бите s при w=1), и тогда перед выполнением операции байт автоматически расширяется (со знаком) до слова.
4) Формат "память-непосредственный операнд" (3-6 байтов):
----------- -------------- -------------- ------------------
| КОП |s|w| |mod|КОП"|mem| |адрес (0-2б)| |непоср.оп (1-2б)|
----------- -------------- -------------- ------------------
Команды этого формата описывают операции типа mem:=mem_immed. Смысл всех полей - тот же, что и в предыдущих форматах.
Помимо рассмотренных в ПК используются и другие форматы команды с двумя операндами; так, предусмотрен специальный формат для команд, один из операндов которых фиксирован (обычно это регистр AX). Имеют свои форматы и команды с другим числом операндов.
1.3.3 Запись команд в MASM
Из сказанного ясно, что одна и та же операция в зависимости от типов операдов записывается в виде различных машинных команд: например, в ПК имеется 28 команд пересылки байтов и слов. В то же время в MASM
все эти "родственные" команды записываются единообразно: например, все команды пересылки имеют одну и ту же символьную форму записи:
MOV op1,op2 (op1:=op2)
Анализируя типы операндов, ассемблер сам выбирает подходящую машинную команду.
В общем случае команды записываются в MASM следующим образом:
метка: мнемокод операнды ;комментарий
Метка с двоеточием, а также точка с запятой и комментарий могут отсутствовать. Метка играет роль имени команды, ее можно использовать в командах перехода на данную команду. Комментарий не влияет на смысл команды, а лишь поясняет ее.
Мнемонические названия операций полностью перечислены в главе 2. Операнды, если есть, перечисляются через запятую. Основные правила
записи операндов следующие.
Регистры указываются своими именами, например:
MOV AX,SI ;оба операнда - регистры
Непосредственные операнды задаются константными выражениями (их значениями являются константы-числа), например:
MOV BH,5 ;5 - непосредственный операнд
MOV DI,SIZE X ;SIZE X (число байтов, занимаемых перемен;ной X) - непосредственный операнд
Адреса описываются адресными выражениями (например, именами переменных), которые могут быть модифицированы по одному или двум регистрам; например, в следующих командах первые операнды задают адреса:
MOV X,AH
MOV X[BX][DI],5
MOV [BX],CL
При записи команд в символьной форме необходимо внимательно следить за правильным указанием типа (размера) операндов, чтобы не было ошибок. Тип обычно определяется по внешнему виду одного из них, например:
MOV AH,5 ;пересылка байта, т.к. AH - байтовый регистр
MOV AX,5 ;пересылка слова, т.к. AX - 16-битовый регистр
;(операнд 5 может быть байтом и словом, по нему ;нельзя определить размер пересылаемой величины)
MOV [BX],300 ;пересылка слова, т.к. число 300 не может быть ;байтом
Если по внешнему виду можно однозначно определить тип обоих операндов, тогда эти типы должны совпадать, иначе ассемблер зафиксирует ошибку. Примеры:
MOV DS,AX ;оба операнда имеют размер слова
MOV CX,BH ;ошибка: регистры CX и BH имеют разные размеры
MOV DL,300 ;ошибка: DL - байтовый регистр, а число 300 не ;может быть байтом
Возможны ситуации, когда по внешнему виду операндов нельзя определить тип ни одного из них, как, например, в команде
MOV [BX],5
Здесь число 5 может быть и байтом, и словом, а адрес из регистра BX может указывать и на байт памяти, и на слово. В подобных ситуациях ассемблер фиксирует ошибку. Чтобы избежать ее, надо уточнить тип одного из операндов с помощью оператора с названием PTR:
MOV BYTE PTR [BX],5 ;пересылка байта
MOV WORD PTR [BX],5 ;пересылка слова
(Операторы - это разновидность выражений языка MASM, аналогичные функциям.)
Оператор PTR необходим и в том случае, когда надо изменить тип, предписанный имени при его описании. Если, например, X описано как имя переменной размером в слово:
X DW 999
и если надо записать в байтовый регистр AH значение только первого байта этого слова, тогда воспользоваться командой
MOV AH,X
нельзя, т.к. ее операнды имеют разный размер. Эту команду следует записать несколько иначе:
MOV AH,BYTE PTR X
Здесь конструкция BYTE PTR X означает адрес X, но уже рассматриваемый не как адрес слова, а как адрес байта. (Напомним, что с одного и того же адреса может начинаться байт, слово и двойное слово; оператор PTR
уточняет, ячейку какого размера мы имеем в виду.)
И еще одно замечание. Если в символьной команде, оперирующей со словами, указан непосредственный операнд размером в байт, как, например, в команде
MOV AX,80h
то возникает некоторая неоднозначность: что будет записано в регистр AX - число 0080h (+128) или 0FF80h (-128)? В подобных ситуациях ассемблер формирует машинную команду, где операнд-байт расширен до слова, причем расширение происходит со знаком, если операнд был записан как отрицательное число, и без знака в остальных случаях. Например:
MOV AX,-128 ; => MOV AX,0FF80h (A:=-128)
MOV AX,128 ; => MOV AX,0080h (A:=+128) MOV AX,80h ; => MOV AX,0080h (A:=+128)
1.4. СЕГМЕНТИРОВНИЕ
1.4.1 Сегменты памяти. Сегментные регистры.
Первые модели ПК имели оперативную память объемом 2^16 байтов (64Кб) и потому использовали 16-битовые адреса. В последующих моделях память была увеличена до 2^20 байтов (1Мб=1000Кб), для чего уже необходимы 20-битовые адреса. Однако в этих ПК ради сохранения преемственности были сохранены 16-битовые адреса: именно такие адреса хранятся в регистрах и указываются в командах, именно такие адреса получаются в результате модмфикации по базовым и индексным регистрам. Как же удается 16-битовыми адресами ссылаться на 1Мб памяти?
Эта проблема решается с помощью сегментирования адресов (неявного базирования адресов). В ПК вводится понятие "сегмент памяти". Так называется любой участок памяти размером до 64Кб и с начальным адресом, кратным 16. Абсолютный (20-битовый) адрес A любой ячейки памяти можно
представить как сумму 20-битового начального адреса (базы) B сегмента, которому принадлежит ячейка, и 16-битового смещения D - адреса этой ячейки, отсчитанного от начала сегмента: A=B+D. (Неоднозначность выбора сегмента не играет существенной роли, главное - чтобы сумма B и D давала нужный адрес.) Адрес B заносится в некоторый регистр S, а в команде, где должен быть указан адрес A, вместо него записывается пара из регистра S и смещения D (в MASM такая пара, называемая адресной парой или указателем, записывается как S:D). Процессор же устроен так, что при выполнении команды он прежде всего по паре S:D вычисляет абсолютный адрес A как сумму содержимого регистра S и смещения D и только затем обращается к памяти по этому адресу A. Вот так, заменяя в командах абсолютные адреса на адресные пары, и удается адресовать всю память 16-битовыми адресами (смещениями).
В качестве регистра S разрешается использовать не любой регистр, а только один из 4 регистров, называемых сегментными: CS, DS, SS и ES. В связи с этим одновременно можно работать с 4 сегментами памяти: начало одного из них загружается в регистр CS и все ссылки на ячейки этого сегмента указываются в виде пар CS:D, начало другого заносится в DS и все ссылки на его ячейки задаются в виде пар DS:D и т.д. Если одновременно надо работать с большим числом сегментов, тогда нужно своевременно спасать содержимое сегментных регистров и записывать в них начальные адреса пятого, шестого и т.д. сегментов.
Отметим, что используемые сегменты могут быть расположены в памяти произвольным образом: они могут не пересекаться, а могут пересекаться и даже совпадать. Какие сегменты памяти использовать, в каких сегментных регистрах хранить их начальные адреса - все это личное дело автора машинной программы.
Как и все регистры ПК, сегментные регистры имеют размер слова. Поэтому возникает вопрос: как удается разместить в них 20-битовые начальные адреса сегментов памяти? Ответ такой. Поскольку все эти адреса кратны 16 (см. выше), то в них младшие 4 бита (последняя 16-ричная цифра) всегда нулевые, а потому эти биты можно не хранить явно, а лишь подразумевать. Именно так и делается: в сегментном регистре всегда хранятся только первые 16 битов (первые четыре 16-ричные цифры) начального адреса сегмента (эта величина называется номером сегмента или просто сегментом). При вычислении же абсолютного адреса A по паре S:D процессор сначала приписывает справа к содержимому регистра S четыре нулевых бита (другими словами, умножает на 16) и лишь затем прибавляет смещение D, причем суммирование ведется по модулю 2^20:
Aабс = 16*[S]+D (mod 2^20)
Если, например, в регистре CS хранится величина 1234h, тогда адресная пара 1234h:507h определяет абсолютный адрес, равный 16*1234h+507h = 12340h+507h = 12847h.
1.4.2 Сегментные регистры по умолчанию
Согласно описанной схеме сегментирования адресов, замену абсолютных адресов на адресные пары надо производить во всех командах, имеющих операнд-адрес. Однако разработчики ПК придумали способ, позволяющий избежать выписывания таких пар в большинстве команд. Суть его в том, что заранее договариваются о том, какой сегментный регистр на ка-
кой сегмент памяти будет указывать, и что в командах задается только смещение: не указанный явно сегментный регистр автоматически восстанавливается согласно этой договоренности. И только при необходимости нарушить эту договоренность надо полностью указывать адресную пару.
Что это за договоренность?
Считается, что регистр CS всегда указывает на начало области памяти, в которой размещены команды программы (эта область называется сегментом команд или сегментом кодов), и потому при ссылках на ячейки этой области регистр CS можно не указывать явно, он подразумевается по умолчанию. (Отметим попутно, что абсолютный адрес очередной команды, подлежащей выполнению, всегда задается парой CS:IP: в счетчике команд IP всегда находится смещение этой команды относительно адреса из регистра CS.) Аналогично предполагается, что регистр DS указывает на сегмент данных (область памяти с константами, переменными и другими величинами программы), и потому во всех ссылках на этот сегмент регистр DS можно явно не указывать, т.к. он подразумевается по умолчанию. Регистр SS, считается, указывает на стек - область памяти, доступ к которой осуществляется по принципу "последним записан - первым считан" (см. 1.7), и потому все ссылки на стек, в которых явно не указан сегментный регистр, по умолчанию сегментируются по регистру SS. Регистр ES считается свободным, он не привязан ни к какому сегменту памяти и его можно использовать по своему усмотрению; чаще всего он применяется для доступа к данным, которые не поместились или сознательно не были размещены в сегменте данных.
С учетом такого распределения ролей сегментных регистров машинные программы обычно строятся так: все команды программы размещаются в одном сегменте памяти, начало которого заносится в регистр CS, а все данные размещаются в другом сегменте, начало которого заносится в регистр DS; если нужен стек, то под него отводится третий сегмент памяти, начало которого записывается в регистр SS. После этого практически во всех командах можно указывать не полные адресные пары, а лишь смещения, т.к. сегментные регистры в этих парах будут восстанавливаться автоматически.
Здесь, правда, возникает такой вопрос: как по смещению определить, на какой сегмент памяти оно указывает? Точный ответ приведен ниже (см. 1.4.3), а в общих чертах он такой: ссылки на сегмент команд могут быть только в командах перехода, а ссылки практически во всех других командах (кроме строковых и стековых) - это ссылки на сегмент данных. Например, в команде пересылки
MOV AX,X
имя X воспринимается как ссылка на данное, а потому автоматически восстанавливается до адресной пары DS:X. В команде же безусловного перехода по адресу, находящемуся в регистре BX,
JMP BX
абсолютный адрес перехода определяется парой CS:[BX].
Итак, если в ссылке на какую-то ячейку памяти не указан явно сегментный регистр, то этот регистр берется по умолчанию. Явно же сегментные регистры надо указывать, только если по каким-то причинам регистр по умолчанию не подходит. Если, например, в команде пересылки нам надо сослаться на стек (скажем, надо записать в регистр AH байт стека, помеченный именем X), тогда нас уже не будет устраивать договоренность о том, что по умолчанию операнд команды MOV сегментируется по регистру DS, и потому мы обязаны явно указать иной регистр - в нашем случае регистр SS, т.к. именно он указывает на стек:
MOV AH,SS:X
Однако такие случаи встречаются редко и потому в командах, как правило, указываются только смещения.
Отметим, что в MASM сегментный регистр записывается в самой команде непосредственно перед смещением (именем переменной, меткой и т.п.), однако на уровне машинного языка ситуация несколько иная. Имеется 4 специальные однобайтовые команды, называемые префиксами замены сегмента (обозначаемые как CS:, DS:, SS: и ES:). Они ставятся перед командой, операнд-адрес которой должен быть просегментирован по регистру, отличному от регистра, подразумеваемому по умолчанию. Например, приведенная выше символическая команда пересылки - это на самом деле две машинные команды:
SS:
MOV AH,X
1.4.3 Сегментирование, базирование и индексирование адресов
Поскольку сегментирование адресов - это разновидность модификации
адресов, то в ПК адрес, указываемый в команде, в общем случае модифицируется по трех регистрам - сегментному, базовому и индексному. В целом, модификация адреса производится в два этапа. Сначала учитываются только базовый и индексный регистры (если они, конечно, указаны в команде), причем вычисление здесь происходит в области 16-битовых адресов; полученный в результате 16-битовый адрес называется исполнительным (эффективным) адресом. Если в команде не предусмотрено обращение к памяти (например, она загружает адрес в регистр), то на этом модификация адреса заканчивается и используется именно исполнительный адрес (он загружается в регистр). Если же нужен доступ к памяти, тогда на втором этапе исполнительный адрес рассматривается как смещение и к нему прибавляется (умноженное на 16) содержимое сегментного регистра, указанного явно или взятого по умолчанию, в результате чего получается абсолютный (физический) 20-битовый адрес, по которому реально и происходит обращение к памяти.
Отметим, что сегментный регистр учитывается только в "последний" момент, непосредственно перед обращением к памяти, а до этого работа ведется только с 16-битовыми адресами. Если учесть к тому же, что сегментные регистры, как правило, не указываются в командах, то можно в общем-то считать, что ПК работает с 16-битовыми адресами.
Как уже сказано, если в ссылке на ячейку памяти не указан сегментный регистр, то он определяется по умолчанию. Это делается по следующим правилам.
1) В командах перехода адрес перехода сегментируется по регистру CS и только по нему, т.к. абсолютный адрес команды, которая должна быть выполнена следующей, всегда определяется парой CS:IP (попытка изменить в таких командах сегментный регистр будет безуспешной).
Отметим, что сегментиорвание по регистру CS касается именно адреса
перехода, а не адреса той ячейки, где он может находиться. Например, в команде безусловного перехода по адресу, находящемуся в ячейке X:
JMP X
имя X сегментируется по регистру DS, а вот адрес перехода, взятый из ячейки X, уже сегментируется по регистру CS.
2) Адреса во всех других командах, кроме строковых (STOS, MOVS, SCAS и CMPS), по умолчанию сегментируются:
- по регистру DS, если среди указанных регистров-модификаторов нет регистра BP;
- по регистру SS, если один из модификаторов - регистр BP.
Таким образом, адреса вида A, A[BX], A[SI], A[DI], A[BX][SI] и A[BX][DI] сегментируются по регистру DS, а адреса A[BP], A[BP][SI] и A[BP][DI] - по регистру SS, т.е. адреса трех последних видов используются для доступа к ячейкам стека.
3) В строковых командах STOS, MOVS, SCAS и CMPS, имеющих два операнда-адреса, на которые указывают индексные регистры SI и DI, один из операндов (на который указывает SI) сегментируется по регистру DS, а другой (на него указывает DI) - по регистру ES.
1.4.4 Программные сегменты. Директива ASSUME
Рассмотрим, как сегментирование проявляется в программах на MASM.
Для того чтобы указать, что некоторая группа предложений программы на MASM образуют единый сегмент памяти, они оформляются как программный сегмент: перед ними ставится директива SEGMENT, после них - директива ENDS, причем в начале обеих этих директив должно быть указано одно и то же имя, играющее роль имени сегмента. Программа же в целом представляет собой последовательность таких программных сегментов, в конце которой указывается директива конца программы END, например:
DT1 SEGMENT ;программный сегмент с именем DT1 A DB 0
B DW ? DT1 ENDS
;
DT2 SEGMENT ;программный сегмент DT2
C DB 'hello'
DT2 ENDS
;
CODE SEGMENT ;программный сегмент CODE
ASSUME CS:CODE, DS:DT1, ES:DT2
BEG: MOV AX,DT2
MOV DS,AX
MOV BH,C
...
CODE ENDS
END BEG ;конец текста программы
Предложения программного сегмента ассемблер размещает в одном сегменте памяти (в совокупности они не должны занимать более 64Кб) начиная с ближайшего свободного адреса, кратного 16. Номер (первые 16 битов начального адреса) этого сегмента становится значением имени сегмента. В MASM это имя относится к константным выражениям, а не адрес-
ным, в связи с чем в команде
MOV AX,DT2
второй операнд является непосредственным, поэтому в регистр AX будет записано начало (номер) сегмента DT2, а не содержимое начальной ячейки этого сегмента.
Имена же переменных (A, B, C) и метки (BEG) относятся к адресным выражениям, и им ставится в соответствие адрес их ячейки относительно "своего" сегмента: имени A соответствует адрес 0, имени B - адрес 1, имени C - адрес 0, а метке BEG - адрес 0.
Все ссылки на предложения одного программного сегмента ассемблер сегментирует по умолчанию по одному и тому же сегментному регистру. По какому именно - устанавливается специальной директивой ASSUME. В нашем примере эта директива определяет, что все ссылки на сегмент CODE должны, если явно не указан сегментный регистр, сегментироваться по регистру CS, все ссылки на DT1 - по регистру DS, а все ссылки на DT2 - по регистру ES.
Встретив в тексте программы ссылку на какое-либо имя (например, на имя C в команде MOV AX,C), ассемблер определяет, в каком программном сегменте оно описано (у нас - в DT2), затем по информации из директивы ASSUME узнает, какой сегментный регистр поставлен в соответствие этому сегменту (у нас - это ES), и далее образует адресную пару иэ данного регистра и смещения имени (у нас - ES:0), которую и записывает в формируемую машинную команду. При этом ассемблер учитывает используемое в ПК соглашение о сегментных регистрах по умолчанию: если в адресной паре, построенной им самим или явно заданной в программе, сегментный регистр совпадает с регистром по умолчанию, то в машинную команду заносится лишь смещение. Если, скажем, в нашем примере встретится команда MOV CX,B, тогда по имени В ассемблер построит пару DS:1, но раз операнд-адрес команды MOV по умолчанию сегментируется по регистру DS, то записывать этот регистр в машинную команду излишне и ассемблер записывает в нее только смещение 1.
Таким образом, директива ASSUME избавляет программистов от необходимости выписывать полные адресные пары не только тогда, когда используются сегментные регистры по умолчанию (как в случае с именем B), но тогда, когда в машинной команде нужно было бы явно указать сегментный регистр (как в случае с именем C). В MASM сегментный регистр в ссылке на имя требуется указывать лишь тогда, когда имя должно по каким-либо причинам сегментироваться по регистру, отличному от того, что поставлен в соответствие всему сегменту, в котором это имя описано.
Однако все это справедливо только при соблюдении следующих условий. Во-первых, директива ASSUME должна быть указана перед первой командой программы. В противном случае ассемблер, просматривающий текст программы сверху вниз, не будет знать, как сегментировать имена из команд, расположенных до этой директивы, и потому зафиксирует ошибку. Во-вторых, в директиве ASSUME следует каждому сегменту ставить в соответствие сегментный регистр: если ассемблеру встретится ссылка на имя из сегмента, которому не соответствует никакой сегментный регистр, то он зафиксирует ошибку. Правда, в обоих случаях можно избежать ошибки, но для этого в ссылках необходимо явно указывать сегментный регистр.
1.4.5 Начальная загрузка сегментных регистров
Директива ASSUME сообщает ассмеблеру о том, по каким регистрам он должен сегментировать имена из каких сегментов, и "обещает", что в этих регистрах будут находиться начальные адреса этих сегментов. Однако загрузку этих адресов в регистры сама директива не осуществляет. Сделать такую загрузку - обязанность самой программы, с загрузки сегментных регистров и должно начинаться выполнение программы. Делается это так.
Поскольку в ПК нет команды пересылки непосредственного операнда в сегментный регистр (а имя, т.е. начало, сегмента - это непосредственный операнд), то такую загрузку приходится делать через какой-то другой, несегментный, регистр (например, AX):
MOV AX,DT1 ;AX:=начало сегмента DT1
MOV DS,AX ;DS:=AX
Аналогично загружается и регистр ES.
Загружать регистр CS в начале программы не надо: он, как и счетчик команд IP, загружается операционной системой перед тем, как начинается выполнение программы (иначе нельзя было бы начать ее выполнение). Что же касается регистра SS, используемого для работы со стеком, то он может быть загружен так же, как и регистры DS и ES, однако в MASM предусмотрена возможность загрузки этого регистра еще до выполнения программы (см. 1.7).
1.4.6 Ссылки вперед
Встречая в символьной команде ссылку назад - имя, которое описано в тексте программы до этой команды, ассемблер уже имеет необходимую информацию об имени и потому может правильно оттранслировать эту команду. Но если в команде встретится ссылка вперед, т.е. имя, которое не было описано до команды и которое, наверное, будет описано позже, то ассемблер в большинстве случаев не сможет правильно оттранслировать эту команду. Например, не зная, в каком программном сегменте будет описано это имя, ассемблер не может определить, по какому сегментному регистру надо сегментировать имя, и потому не может определить, надо или нет размещать перед соответствующей машинной командой префикс замены сегмента и, если надо, то какой именно.
В подобной ситуации ассемблер действует следующим образом: если в команде встретилась ссылка вперед, то он делает некоторое предположение относительно этого имени и уже на основе этого предположения формирует машинную команду. Если затем (когда встретится описание имени) окажется, что данное предположение было неверным, тогда ассемблер пытается исправить сформированнную им ранее машинную команду. Однако это не всегда удается: если правильная машинная команда должна занимать больше места, чем машинная команда, построенная на основе предположения (например, перед командой надо на самом деле вставить префикс замены сегмента), тогда ассемблер фиксирует ошибку (как правило, это ошибка номер 6: Phase error between passes.)
Какие же предположения делает ассемблер, встречая ссылку вперед? Во всех командах, кроме команд перехода (о них см. 1.5), ассемблер предполагает, что имя будет описано в сегменте данных и потому сегментируется по регистру DS. Это следует учитывать при составлении программы: если в команде встречается ссылка вперед на имя, которое описано в сегменте, на начало которого указывает сегментный регистр, отличный от DS, то перед таким именем автор программы должен написать соотвествующмй префикс. Пример:
code segment
assume cs:code
x dw ?
beg: mov ax,x ;здесь вместо cs:x можно записать просто x mov cs:y,ax ;здесь обязательно надо записать cs:y
...
y dw ?
code ends
1.5. ПЕРЕХОДЫ
В систему команд ПК входит обычный для ЭВМ набор команд перехода: безусловные и условные переходы, переходы с возвратами и др. Однако в ПК эти команды имеют некоторые особенности, которые здесь и рассматриваются.
Абсолютный адрес команды, которая должна быть выполнена следующей, определяется парой CS:IP, поэтому выполнение перехода означает изменение этих регистров, обоих или только одного (IP). Если изменяется только счетчик команд IP, то такой переход называется внутрисегментным или близким (управление остается в том же сегменте команд), а если меняются оба регистра CS и IP, то это межсегментный или дальний переход (начинают выполняться команды из другого сегмента команд). По способу изменения счетчика команд переходы делятся на абсолютные и относительные. Если в команде перехода указан адрес (смещение) той команды, которой надо передать управление, то это абсолютный переход. Однако в команде может быть указана величина (сдвиг), которую надо добавить к текущему значению регистра IP, чтобы получился адрес перехода, и тогда это будет относительный переход; при этом сдвиг может быть положительным и отрицательным, так что возможен переход вперед и назад. По величине сдвига относительные переходы делятся на короткие (сдвиг задается байтом) и длинные (сдвиг - слово). Абсолютные же переходы делятся на прямые и косвенные: при прямом переходе адрес перехода задается в самой команде, а при косвенном - в команде указывается регистр или ячейка памяти, в котором (которой) находится адрес перехода.
1.5.1 Безусловные переходы.
В MASM все команды безусловного перехода обозначаются одинаково:
JMP op
но в зависимости от типа операнда, ассемблер формирует разные машинные команды.
1) Внутрисегментный относительный короткий переход.
JMP i8 (IP:=IP+i8)
Здесь i8 обозначает непосредственный операнд размеров в байт, который интерпретируется как знаковое целое от -128 до 127. Команда прибавляет это число к текущему значению регистра IP, получая в нем адрес (смещение) той команды, которая должна быть выполнена следующей. Регистр CS при этом не меняется.
Необходимо учитывать следующую особенность регистра IP. Выполнение любой команды начинается с того, что в IP заносится адрес следующей за ней команды, и только затем выполняется собственно команда. Для команды относительного перехода это означает, что операнд i8 прибавляется не к адресу этой команды, а к адресу команды, следующей за ней, поэтому, к примеру, команда JMP 0 - это переход на следующую команду программы.
При написании машинной программы сдвиги для относительных переходов приходится вычислять вручную, однако MASM избавляет от этого неприятного занятия: в MASM в командах относительного перехода всегда указывается метка той команды, на которую надо передать управление, и ассемблер сам вычисляет сдвиг, который он и записывает в машинную команду. Отсюда следует, что в MASM команда перехода по метке воспринимается не как абсолютный переход, а как относительный.
По короткому переходу можно передать управление только на ближайшие команды программы - отстоящие от команды, следующей за командой перехода, до 128 байтов назад или до 127 байтов вперед. Для перехода на более дальние команды используется
2) Внутрисегментный относительный длинный переход.
JMP i16 (IP:=IP+i16)
Здесь i16 обозначает непосредственный операнд размером в слово, который рассматривается как знаковое целое от -32768 до 32767. Этот переход аналогичен короткому переходу.
Отметим, что, встретив команду перехода с меткой, которой была помечена одна из предыдущих (по тексту) команд программы, ассемблер вычисляет разность между адресом этой метки и адресом команды перехода и по этому сдвигу определяет, какую машинную команду относительного перехода - короткую или длинную - надо сформировать. Но если метка еще
не встречалась в тексте программы, т.е. делается переход вперед, тогда ассемблер, не зная еще адреса метки, не может определить, какую именно машинную команду относительного перехода формировать, поэтому он на всякий случай выбирает команду длинного перехода. Однако эта машинная команда занимает 3 байта, тогда как команда короткого перехода - 2 байта, и если автор программы на MASM стремится к экономии памяти и знает заранее, что переход вперед будет на близкую метку, то он должен сообщить об этом ассемблеру, чтобы тот сформировал команду короткого перехода. Такое указание делается с помощью оператора SHORT:
JMP SHORT L
Для переходов назад оператор SHORT не нужен: уже зная адрес метки, ассемблер сам определит вид команды относительного перехода.
3) Внутрисегментный абсолютный косвенный переход.
JMP r16 (IP:=[r])
или JMP m16 (IP:=[m16])
Здесь r16 обозначает любой 16-битовый регистр общего назначения, а m16 - адрес слова памяти. В этом регистре (слове памяти) должен находиться адрес, по которому и будет произведен переход. Например, по команде JMP BX осушествляется переход по адресу, находящемуся в регистре BX.
4) Межсегментный абсолютный прямой переход.
JMP seg:ofs (CS:=seg, IP:=ofs)
Здесь seg - начало (первые 16 битов начального адреса) некоторого сегмента памяти, а ofs - смещение в этом сегменте. Пара seg:ofs определяет абсолютный адрес, по которому делается переход. В MASM эта пара всегда задается конструкцией FAR PTR <метка>, которая "говорит", что надо сделать переход по указанной метке, причем эта метка - "дальняя", из другого сегмента. Отметим, что ассемблер сам определяет, какой это сегмент, и сам подставляет в машинную команду его начало, т.е. seg.
5) Межсегментный абсолютный косвенный переход.
JMP m32 (CS:=[m32+2], IP:=[m32])
Здесь под m32 понимается адрес двойного слова памяти, в котором находится пара seg:ofs, задающая абсолютный адрес, по которому данная команда должна выполнить переход. Напомним, что в ПК величины размером в двойное слово хранятся в "перевернутом" виде, поэтому смещение ofs находится в первом слове двойного слова m32, а смещение seg - во втором слове (по адресу m32+2).
Команды межсегментного перехода используются тогда, когда команды программы размещены не в одном сегменте памяти, а в нескольких (например, команд столь много, что в совокупности они занимают более 64Кб, т.е. более максимального размера сегмента памяти). При переходе из одного такого сегмента в другой необходимо менять не только счетчик команд IP, но и содержимое регистра CS, загружая в последний начальный адрес второго сегмента. Такое одновременное изменение обоих этих регистров и делают команды межсегментного перехода.
При записи в MASM команд перехода следует учитывать, что они могут восприниматься неоднозначно. Скажем, как воспринимать команду
JMP A
- как переход по метке A или как переход по адресу, хранящемуся в ячейке с именем A? Кроме того, какой это переход - внутрисегментный или межсегментный? Ответ зависит от того, как описано имя A, и от того, когда описано имя A - до или после команды перехода.
Пусть A описано до команды перехода ("ссылка назад"). Если именем A помечена некоторая команда текущего сегмента команда (т.е. A - метка), тогда ассемблер формирует машинную команду внутрисегментного относительного перехода. Если же A - имя переменной, тогда ассемблер формирует машинную команду косвенного перехода - внутрисегментного, если A описано в директиве DW, или межсегментного, если A описано в директиве DD.
В случае же, если имя A описано после команды перехода ("ссылка вперед"), ассемблер всегда формирует машинную команду внутрисегментного относительного длинного перехода. С учетом этого имя A обязательно должно метить команду из текущего сегмента команд, иначе будет зафиксирована ошибка. Если такая трактовка ссылки вперед не удовлетворяет автора программы, тогда он обязан с помощью оператора SHORT или PTR уточнить тип имени A:
JMP SHORT A ;внутрисегментный короткий переход по метке
JMP WORD PTR A ;внутрисегментный косвенный переход
JMP DWORD PTE A ;межсегментный косвенный переход
Отметим, что переход по метке A из другого сегмента команд всегда должен указываться с помощью FAR PTR (независимо от того, описана метка A до или после команды перехода):
JMP FAR PTR A ;межсегментный переход по метке
1.5.2 Условные переходы.
Практически во всех командах условного перехода проверяется значение того или иного флага (например, флага нуля ZF) и, если он имеет определенное значение, выполняется переход по адресу, указанному в команде. Значение флага должно быть установлено предыдущей командой, например, командой сравнения
CMP op1,op2
которая вычисляет разность op1-op2, однако результат никуда не записывает, а только меняет флаги, на которые и будет реагировать команда
условного перехода.
В MASM команды условного перехода имеют следующую форму:
Jxx op
где xx - одна или несколько букв, в сокращенном виде отражающие проверяемое условие (обычно в предположении, что перед этой командой находится команда сравнения). Примеры некоторых мнемоник:
JE - переход "по равно" (jump if equal)
JL - переход "по меньше" (jump if less)
JNL - переход "по неменьше" (jump if not less)
Особеностью всех машинных команд условного перехода является то, что они реализуют внутрисегментный относительный короткий переход, т.е. добавляют к счетчику команд IP свой операнд, рассматриваемый как знаковое число от -128 до 127. В MASM этот операнд всегда должен записываться как метка, которую ассемблер заменит на соответствующий сдвиг (см. выше).
Такая особенность команд условного перехода вызывает неудобство при переходах на "дальние" команды. Например, если надо сделать переход при A<B на команду, помеченную меткой L и расположенную далеко от команды перехода, то приходится использовать команду длинного безусловного перехода:
MOV AX,A
CMP AX,B ;сравнение A и B
JNL M ;не меньше --> M (обход команды JMP)
JMP L ;меньше --> L (длинный переход)
M: ...
1.5.3 Команды управление циклом
В ПК есть несколько команд, упрощающих программирование циклов с заранее известным числом повторений. Применение этих команд требует, чтобы к началу цикла в регистр CX было занесено число шагов цикла. Сами команды размещаются в конце цикла, они уменьшают значение CX на 1 и, если CX еще не равно 0, передают управление на начало цикла. Например, найти S - сумму элементов массива X из 10 чисел-слов можно так:
MOV AX,0 ;начальное значение суммы (накапливается в AX)
MOV SI,0 ;начальное значение индексного регистра
MOV CX,10 ;число повторений цикла
L: ADD AX,X[SI] ;AX:=AX+X[i]
ADD SI,2 ;SI:=SI+2
LOOP L ;CX:=CX-1; if CX<>0 then goto L
MOV S,AX ;S:=AX
Помимо команды LOOP есть еще две "циклические" команды - LOOPZ и LOOPNZ (они имеют синонимичные названия LOOPE и LOOPNE), которых кроме регистра CX проверяют еще и флаг нуля ZF; например, команда LOOPZ "выходит" из цикла, если CX=0 или ZF=1. Эту команду можно, например, использовать при поиске в массиве первого нулевого элемента, где должно быть предусмотрено два условия выхода из цикла: либо будет найден ну-
левой элемент (ZF=1, если перед LOOPZ поставить команду сравнения очередного элемента с 0), либо будет исчерпан весь мсассив (CX=0)
Отметим, что все эти "циклические" команды реализуют короткий относительный переход, как и команды условного перехода, поэтому их можно использовать только для циклов с небольшим числом команд.
В MASM есть еще две команды перехода - CALL (переход с возвратом) и RET (возврат из подпрограммы), они рассматриваются в 1.7.
1.6. СТРОКОВЫЕ ОПЕРАЦИИ
В ПК под строкой понимается последовательность соседних байтов или слов. В связи с этим все строковые команды имеют две разновидности для работы со строками из байтов (в мнемонику операций входит буква B) и для работы со строками из слов (в мнемонику входит W).
Имеются следующие операции над строками:
· пересылка элементов строк (в память, из памяти, память-память);
· сравнение двух строк;
· просмотр строки с целью поиска элемента, равного заданному.
Каждая из этих операций выполняется только над одним элементом строки, однако одновременно происходит автоматическая настройка на следующий или предыдущий элемент строки. Имеются специальные команды повторения (REP и др.), которые заставляют следующую за ними строковую команду многократно повторяться (до 2^16 раз), в связи с чем такая пара команд позволяет обработать всю строку, причем намного быстрее, чем запрограммированный цикл.
Кроме того, строки можно просматривать вперед (от их начала к концу) и назад. Направление просмотра зависит от флага направления 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,m32
которая в регистр SI заносит первое слово, а в регистр DS - второе слово из двойного слова, имеющего адреc m32 (таким образом, по адресу m32+2 должен храниться сегмент, а по адресу m32 - смещение начального или конечного элемента строки). Начальную загрузку регистров ES и DI обычно осуществляют одной командой
LES DI,m32
которая действует аналогично команде LDS.
Перечислим вкратце строковые команды ПК.
Команда загрузки элемента строки в аккумулятор (LODSB или LODSW) пересылает в регистр AL или AX очередной элемент строки, на который указывает пара DS:SI, после чего увеличивает (при DF=0) или уменьшает (при DF=1) регистр SI на 1 или 2.
Команда записи аккумулятора в строку (STOSB или STOSW) заносит содержимое регистра AL или AX в тот элемент строки, на который указывает пара 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 или AX и результат сравнения фиксирует в флагах, после чего меняет содержимое регистра DI.
Перед любой строковой командой можно поставить одну из двух команд, называемых "префиксами повторения", которая заставит многократно повториться эту строковую команду. Число повторений (обычно это длина строки) должно быть указано в регистре CX. Префикс повторения REPZ (синонимы - REPE, REP) сначала заносит 1 в флаг нуля ZF, после чего, постоянно уменьшая CX на 1, заставляет повторяться следующую за ним строковую команду до тех пор, пока в CX не окажется 0 или пока флаг ZF не изменит свое значение на 0. Другой префикс повторения REPNZ (синоним - REPNE) действует аналогично, но только вначале устанавливает флаг ZF в 0, а при при изменении его на 1 прекращает повторение строковой команды.
Пример. Пусть надо переписать 10000 байтов начиная с адреса A в другое место памяти начиная с адреса B. Если оба этих имени относятся к сегменту данных, на начало которого указывает регистр DS, тогда эту пересылку можно сделать так:
CLD ;DF:=0 (просмотр строки вперед)
MOV CX,1000 ;CX - число повторений
MOV AX,DS
MOV ES,AX ;ES:=DS
LEA SI,A ;ES:SI - "откуда"
LEA DI,B ;DS:DI - "куда"
REP MOVSB ;пересылка CX байтов
1.7. СТЕК. ПОДПРОГРАММЫ.
1.7.1 Стек
В ПК имеются специальные команды работы со стеком, т.е. областью памяти, доступ к элементам которой осуществляется по принципу "последним записан - первым считан". Но для того, чтобы можно было воспользоваться этими командами, необходимо соблюдение ряда условий.
Под стек можно отвести область в любом месте памяти. Размер ее может быть любым, но не должен превосходить 64Кб, а ее начальный адрес должен быть кратным 16. Другими словами, эта область должна быть сегментом памяти; он называется сегментом стека. Начало этого сегмента (первые 16 битов начального адреса) должно обязательно храниться в сегментном регистре SS.
Хранимые в стеке элементы могут иметь любой размер, однако следует учитывать, что в ПК имеются команды записи в стек и чтения из него только слов. Поэтому для записи байта в стек его надо предварительно расширить до слова, а запись или чтение двойных слов осуществляются парой команд.
В ПК принято заполнять стек снизу вверх, от больших адресов к меньшим: первый элемент записывается в конец области, отведенной под стек, второй элемент - в предыдущую ячейку области и т.д. Считывается всегда элемент, записанный в стек последним. В связи с этим нижняя граница стека всегда фиксирована, а верхняя - меняется. Слово памяти, в котором находится элемент стека, записанный последним, называется вершиной стека. Адрес вершины, отсчитанный от начала сегмента стека, обязан находиться в указателе стека - регистре SP. Таким образом, абсолютный адрес вершины стека определяется парой SS:SP.
----- ----- -----
SS:SP | | SS:SP | | SS:SP | |
| ----- запись | ----- чтение | -----
| | | =======> ---->| b | =======> | | |
| ----- в стек ----- из стека | -----
----->| a | | a | ---->| a |
----- ----- -----
Значение 0 в регистре SP свидетельствует о том, что стек полностью заполнен (его вершина "дошла" до начала области стека). Поэтому для
контроля за переполнением стека надо перед новой записью в стек проверять условие SP=0 (сам ПК этого не делает). Для пустого стека значение SP должно равняться размеру стека, т.е. пара SS:SP должна указывать на байт, следующий за последним байтом области стека. Контроль за чтением из пустого стека, если надо, обязана делать сама программа.
Начальная установка регистров SS и SP может быть произведена в самой программе, однако в MASM предусмотрена возможность автоматической загрузки этих регистров. Если в директиве SEGMENT, начинающей описание сегмента стека, указать параметр STACK, тогда ассемблер (точнее, загрузчик) перед тем, как передать управление на первую команду машинной программы, загрузит в регистры SS и SP нужные значения. Например, если в программе сегмент стека описан следующим образом:
ST SEGMENT STACK
DB 256 DUP(?) ;размер стека - 256 байтов
ST ENDS
и если под этот сегмент была выделена область памяти начиная с абсолютного адреса 12340h, тогда к началу выполнения программы в регистре SS окажется величина 1234h, а в регистре SP - величина 100h (=256). Отметим, что эти значения соответствуют пустому стеку.
1.7.2 Основные стековые команды
При соблюдении указанных требований в программе можно использовать команды, предназначенные для работы со стеком. Основными из них являются следующие.
Запись слова в стек: PUSH op
Здесь op обозначает любой 16-битовый регистр (в том числе и сегментный) или адрес слова памяти. По этой команде значение регистра SP уменьшается на 2 (вычитание происходит по модулю 2^16), после чего указанное операндом слово записывается в cтек по адресу SS:SP.
Чтение слова из стека: POP op
Слово, считанное из вершины стека, присваивается операнду op (регистру, в том числе сегментному, но не CS, или слову памяти), после чего значение SP увеличивается на 2.
Переход с возвратом: CALL op
Эта команда записывает адрес следующей за ней команды в стек и затем делает переход по адресу, определяемому операндом op. Она используется для переходов на подпрограммы с запоминанием в стеке адреса возврата.
Имеются следующие разновидности этой команды (они аналогичны вари-
антам команды безусловного перехода JMP):
- внутрисегментный относительный длинный переход (op - непосредственный операнд размером в слово, а в MASM - это метка из текущего сегмента команд или имя близкой процедуры (см. ниже)); в этом случае в стек заносится только текущее значение счетчика команд IP, т.е. смещение следующей команды;
- внутрисегментный абсолютный косвенный переход (op - адрес слова памяти, в которой находится адрес (смещение) той команды, на которую и будет сделан переход); и здесь в стек записывается только смещение адреса возврата;
- межсегментный абсолютный прямой переход (op - непосредственный операнд вида seg:ofs, а в MASM - это FAR PTR <метка> или имя дальней процедуры (см. ниже)); здесь в стек заносится текущие значение регистров CS и IP (первым в стек записывается содержимое CS), т.е. абсолютный адрес возврата, после чего меняются регистры CS и IP;
- межсегментный абсолютный косвенный переход (op - адрес двойного слова, в котором находится пара seg:ofs, задающая абсолютный адрес перехода); и здесь в стеке спасается содержимое регистров CS и IP.
Переход (возврат) по адресу из стека: RET op
Из стека считывается адрес и по нему производится переход. Если указан операнд (а это должно быть неотрицательное число), то после чтения адреса стек еще очищается на это число байтов (к SP добавляется это число). Команда используется для возврата из подпрограммы по адресу, записанному в стек по команде CALL при вызове подпрограммы, и одновременной очистки стека от параметров, которые основная программа занесла
в стек перед обращением к подпрограмме.
Команда RET имеет две разновидности (хотя в MASM они записываются и одинаково): в одном случае из стека считывается только одно слово смещение адреса возврата, а во втором - из стека считывается пара seg: ofs, указывающая абсолютный адрес возврата. Как ассемблер определяет, какой из этих двух случаев имеет место, объяснено ниже.
В ПК стек в основном используется для организации подпрограмм и прерываний. Подпрограммы рассматриваются ниже, а прерывания - в главе 3. Однако, даже если программе не нужен стек, она все равно должна отвести под него место. Дело в том, что стеком будет неявно пользоваться операционная система при обработке прерываний, которые возникают (например, при нажатии клавиш на клавиатуре) в то время, когда выполняется программа. Для нужд ОС рекомендуется выделять в стеке 64 байта.
1.7.3 Подпрограммы
Типичная схема огранизации подпрограмм, обычно используемая трансляторами с языков высокого уровня для реализации процедур и функций (в частности, рекурсивных), следующая.
При обращении к подпрограмме в стек заносятся параметры для нее и адрес возрата, после чего делается переход на ее начало:
PUSH param1 ;запись 1-го параметра в стек
...
PUSH paramk ;запись последнего (k-го) параметра в стек
CALL subr ;переход в возратом на подпрограмму
(Замечание: если необходимо вычислить параметр или если его размер отличен от слова, тогда для записи параметра в стек нужно, конечно, несколько команд, а не одна.) Состояние стека после выполнения этих команд обращения к подпрограмме показано на рис. a
| | |--------------|
| | | лок.величины |<-SP
| | -2| (m байтов) |
| | |--------------|
| | 0| BP стар |<-BP
|адрес возврата|<-SP +2|адрес возврата|
| 1-й параметр | +4| 1-й параметр |
| ... | | ... |
| k-й параметр | | k-й параметр |
|//////////////| |//////////////|
|//////////////|<-BP |//////////////|
рис. а рис. б
Первыми командами подпрограммы обычно являются следующие:
PUSH BP ;спасти в стеке старое значение BP
MOV SP,BP ;установить BP на вершину стека
SUB SP,m ;отвести в стеке место (m байтов) под локальные
;величины подпрограммы (состояние стека в этот ;момент показано на рис. б)
Поясним эти "входные" команды. В подпрограмме для обращения к ячейкам стека, занятых параметрами, используется (как базовый) регистр BP: если в BP занести адрес вершины стека, то для доступа к этим ячейкам следует использовать адресные выражения вида i[BP] или, что то же самое, [BP+i]. (Отметим, что применять здесь регистры-модификаторы BX, SI и DI нельзя, т.к. формируемые по ним исполнительные адреса будут сегментироваться по умолчанию по регистру DS, а в данном случае нужно сегментирование по SS.) Однако данная подпрограмма может быть вызвана из другой, также использующей регистр BP, поэтому прежде, чем установить BP на вершину стека, надо спасти в стеке старое значение этого регистра, что и делает первая из "входных" команд. Вторая же команда устанавливает BP на вершину стека. Если предположить, что каждый параметр и адрес возврата занимают по слову памяти, тогда доступ к первому параметру обеспечивается адресным выражением [BP+4], ко второму - выражением [BP+6] и т.д. (см. рис. б).
Подпрограмме может потребоваться место для ее локальных величин.
Такое место обычно отводится в стеке (а для рекурсивных подпрограмм только в стеке) "над" ячейкой, занимаемой старым значением BP. Если под эти величины нужно m байтов, то такой "захват" места можно реализовать простым уменьшением значения регистра SP на m, что и делает 3-я "входная" команда. Доступ к локальным величинам обеспечивается адресными выражениями вида [BP-i]. Если подпрограмме не нужно место под локальные величины, тогда третью из "входных" команд следует опустить.
Выход из подпрограммы реализуется следующими командами:
MOV SP,BP ;очистить стек от локальных величин
POP BP ;восстановить старое значение BP
RET 2*k ;возврат из подпрограммы и очистка стека от
;параметров (считаем, что они занимают 2*k байтов) Первая из этих "выходных" команд заносит в регистр SP адрес той ячейки стека, где хранится старое значение регистра BP, т.е. происходит очистка стека от локальных величин (если их не было, то данную команду надо опустить). Вторая команда восстанавливает в BP это старое значение, одновременно удаляя его из стека. В этот момент состояние стека будет таким же, как и перед входом в подпрограмму (см. рис а). Третья команда считывает из стека адрес возврата (в результате чего SP "опускается" на 2 байта), затем добавляет к SP число, которое должно равняться числу байтов, занимаемых всеми параметрами подпрограммы, и затем осуществляет переход по адресу возврата. В этот момент состояние стека будет таким же, каким оно было перед обращением к подпрограмме.
Здесь описана универсальная схема организации работы подпрограмм.
В кокретных же случаях можно использовать более простые схемы. Например, параметры можно передавать не через стек, а через регистры, место под локальные величины можно отводить не в стеке, а в сегменте данных и т.п.
1.7.4 Процедуры в языке ассемблера
При составлении и вызове подпрограмм необходимо следить за тем, чтобы команды CALL и RET действовали согласовано - были одновременно близкими или дальними. В MASM эта проблема снимается, если подпрограмму описать как процедуру. Процедуры имеют следующий вид:
имя_процедуры PROC [NEAR или FAR]
...
имя_процедуры ENDP
Хотя в директиве PROC после имени процедуры не ставится двоеточие, это имя относится к меткам и его можно указывать в командах перехода, в частности в команде CALL, когда надо вызвать процедуру. Это же имя должно быть повторено в директиве ENDP, заканчивающей описание процедуры. Предложения между этими двумя директивами образуют тело процедуры (подпрограмму). Имя процедуры является фактически меткой первой из команд тела, поэтому данную команду не надо специально метить.
Если в директиве PROC указан параметр NEAR или он вообще не указан, то такая процедура считается "близкой" и обращаться к ней можно только из того сегмента команд, где она описана. Дело в том, что ассемблер будет заменять все команды CALL, где указано имя данной процедуры, на машинные команды близкого перехода с возвратом, а все команды RET внутри процедуры - на близкие возвраты. Если же в директиве PROC указан параметр FAR, то это "дальняя" процедура: все обращения к ней и все команды RET внутри нее рассматриваются ассемблером как дальние переходы. Обращаться к этой процедуре можно из любых сегментов команд. Таким образом, достаточно лишь указать тип процедуры (близкая она или дальняя), всю же остальную работу возьмет на себя ассемблер: переходы на нее и возвраты из нее будут автоматически согласованы с этим типом. В этом главное (и единственное) достоинство описания подпрограмм в виде процедур. (Отметим, что метки и имена, описанные в процедуре, не локализуются в ней.)
Например, вычисление ax:=sign(ax) можно описать в виде процедуры следующим образом:
sing proc far ;дальняя процедура
cmp ax,0
je sgn1 ;ax=0 - перейти к sgn1
mov ax,1 ;ax:=1 (флаги не изменились!)
jg sgn1 ;ax>0 - перейти к sgn1
mov ax,-1 ;ax:=-1
sgn1: ret ;дальний возврат
sign endp
...
Возможный пример обращения к этой процедуре:
;cx:=sign(var)
mov ax,var
call sign ;дальний вызов
mov cx,ax
|