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 число, которое должно равняться числу байтов, занимаемых всеми параметрами подпрограммы, и затем осуществляет переход по адресу возврата. В этот момент состояние стека будет таким же, каким оно было перед обращением к подпрограмме.