Стек

Стек - это такая структура данных в памяти, которая используется для временного хранения информации. Программа может поместить информацию в стек (команда PUSH) или извлечь ее из стека (команда POP). Данные в стеке упорядочиваются специальным образом. Извлекаемый из стека элемент данных - это всегда тот элемент, который был записан туда последним. Такая организация хранения данных сокращенно обозначается LIFO (Last In, First Out - последний поступивший удаляется первым). Если мы поместим в стек два элемента: сначала A, а затем B, то при первом обращении к стеку извлекается элемент B, а при следующем - A. Информация выбирается из стека в обратном по отношению к записи порядке.

Стек можно противопоставить очереди, похожей на ту, что обычно образуется в почтовом отделении или в магазине. Очередь - это структура данных, организованная по принципу "первый поступивший удаляется первым" (First In, First Out - FIFO). Первый человек, вставший в очередь, первым ее и покинет. Очередь и стек полностью противоположны.

В ЭВМ за стеком резервируется блок памяти и указатель, называемый указателем стека SP (Stack Pointer). Указатель стека используется программой для того, чтобы фиксировать самый последний записанный в стек элемент данных. В почтовом отделении члены очереди перемещаются вперед по мере продвижения очереди. В ЭВМ для фиксации местонахождения элемента данных гораздо проще использовать указатель и перемещать его только по мере записи и считывания данных из стека. При выполнении команды POP или PUSH значение указателя стека соответственно увеличивается или уменьшается на 1.

На рис.2.19 показан пример стека. Рисунок 2.19а иллюстрирует состояние стека после того, как туда были последовательно записаны значения A, B и C. Указатель стека указывает на содержимое вершины стека TOS, в данном случае на C. После этого в стек записывается еще один элемент - D (рис.2.19б). Команда PUSH уменьшает на 1 значение указателя стека SP, который теперь указывает на значение содержимого вершины TOS, равное D. Как всегда, указатель стека указывает на последний записанный в стек элемент.

Рисунок 2.19в иллюстрирует состояние стека после выполнения команды POP. Эта команда считала из стека элемент D. Считанное из стека значение помещается командой POP в специальную область. Если в данном случае выполнялась команда POP AX, то микропроцессор запишет значение D в регистр AX (это уже дополнительный аспект, который будет подробно рассмотрен в следующей главе). Указатель стека увеличивается этой командой на 1 и будет указывать теперь на новое значение содержимого вершины TOS, равное С. Обратите внимание на то, что выборка элементов из стека производится по описанному выше принципу LIFO. Последним помещенным в стек элементом был D и он же был первым, считанным из стека.

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

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

В рассмотренных выше примерах подразумевалась определенная организация стека, реализованная в микропроцессоре 8088. При этом указатель стека всегда указывал на текущее значение содержимого вершины стека. Команда PUSH уменьшает, а команда POP увеличивает значение указателя стека на 1. Стек растет в направлении меньших значений адресов памяти. Основание стека имеет адрес паямти больший, чем его вершина. Если, как и на рис.2.19, изображать ячейки стека с меньшими адресами сверху, то вершина стека будет располагаться в верхней части рисунка.

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

Каждая команда CALL вызывает запись в стек - записывает в стек адрес возврата. Команда RET, подобно команде POP, считывает из стека адрес возврата и помещает его в указатель команд. Таким образом, в микропроцессоре 8088 стек используется для хранения адресов возврата, что позволяет осуществлять вложение подпрограмм. Что такое вложение подпрограмм? Рисунок 2.20 иллюстрирует пример вложения подпрограмм.

Приведенная на рис.2.20 программа не имеет реального смысла и будет использоваться как пример вложения подпрограмм. На рис.2.20а показано состояние стека перед выполнением программы. По мере выполнения основной программы, начинающейся с метки MAIN, происходит вызов подпрограммы SUBROUTINE_A. При этом, как видно из рис.2.20б, микропроцессор записывает в стек адрес возврата, равный 103. В процессе своего выполнения подпрограмма SUBROUTINE_A вызывает подпрограмму SUBROUTINE_B. Команда вызова записывает в стек соответствующий адрес возрвата, равный 108, в подпрограмму SUBROUTINE_A, как это показано на рис.2.20в. В момент завершения подпрограммы SUBROUTINE_B команда возврата считывает из стека значение 108 (рис.2.20г). Процессор в соответствии с командой возврата помещает это значение в указатель команд. Как видно из приведенного примера, команда в ячейке с адресом 108 относится к подпрограмме SUBROUTINE_A и является следующей после команды вызова подпрограммы SUBROUTINE_B. При этом работа подпрограммы SUBROUTINE_A завершается. Команда возврата перезаписывает значение 103 из стека в указатель команд. Команда в ячейке с адресом 103 относится к головной программе инепосредственно следует за командой вызова подпрограммы SUBROUTINE_A.

В приведенном на рис.2.20 примере основным является вложение подпрограмм. Одна подпрограмма может вызывать другую, а команды возврата всегда обеспечивают возвращение в нужное место. Единственное, что ограничивает глубину вложения подпрограмм (число подпрограмм, вызывающих другие подпрограммы) - это объем стека. До тех пор, пока в стеке имеется место для хранения еще одного адреса возврата, могут осуществляться вызов и вложение еще одной подпрограммы. Организация стека по принципу LIFO обеспечивает правильное упорядочение адресов возврата.

Приведенная на рис.2.20 программа иллюстрирует также использование псевдокоманды языка ассемблера PROC. Оператор PROC используется ассемблером для идентификации подпрограмм. Как будет показано в дальнейшем, ассемблеру нужна информация о том, как далеко располагается подпрограмма и как должен осуществляться возврат в точку вызова подпрограммы. Параметр NEAR идентифицирует такую подпрограмму, которая расположена недалеко от точки ее вызова. В дальнейшем мы еще вернемся к оператору PROC, когда будем подробно рассматривать выполнение команд CALL и JMP.

Хостинг от uCoz