Распределение пространства стека. Общий случай распределения пространства стека при выполнении процедуры показан в табл. В.2.
Таблица В.2. Распределение пространства стека в порядке увеличения адресов
Общедоступная область стека |
Промежуточные переменные подпрограммы |
Исходное содержимое регистра bр или ebp |
Адрес возврата из подпрограммы |
Параметры подпрограммы |
Недоступная часть стека |
Общедоступная область расположена в начале стекового сегмента, ее минимальный адрес (смещение) равен нулю, а максимальный хранится в указателе стека (в регистре sp). Обычно она используется для хранения содержимого регистров и передачи параметров вызываемым подпрограммам.
Место для промежуточных переменных резервирует подпрограмма, если в этом есть необходимость. Она же сохраняет в стеке исходное содержимое регистра bpли ebp при работе в 32-разрядном режиме. Во время выполнения подпрограммы адрес, в котором сохранено исходное значение регистра bp(или ebp), используется в качестве базы для доступа команд к параметрам или промежуточным переменным.
Адрес возврата и параметры размещает в стеке основная задача, вызывающая данную подпрограмму. При входе в подпрограмму указатель стека содержит адрес первого свободного слова, в которое обычно помещается исходное значение регистра bpли ebp.
Недоступная часть стека названа так не потому, что она физически недоступна, а потому, что подпрограмма не должна изменять хранящиеся там данные. Если перед размещением параметров стек был полностью очищен, то недоступной области просто не существует.
B зависимости от конкретных особенностей подпрограммы при ее выполнении может возникнуть необходимость в использовании переменных для хранения промежуточных результатов вычислений в оперативной памяти. Назовем такие переменные "промежуточными".
До сих пор, в приводимых примерах переменные располагались в разделе данных задачи. Только в примере В.5 переменная dten хранится в сегменте кодов. Как правило, у внешних подпрограмм нет собственного сегмента данных, исключения возможны, но они встречаются редко. Размещать же промежуточные результаты в сегменте данных основной задачи не целесообразно, поскольку они используются только во время выполнения подпрограммы и не нужны в других случаях.
Пространство для размещения промежуточных переменных лучше всего выделять в стеке при входе в подпрограмму и освобождать его перед выходом из нее. Для резервирования требуемого пространства после сохранения содержимого регистра bpнадо просто уменьшить текущее значение указателя стека на суммарный размер промежуточных переменных, выраженный в байтах. В примере В.6 показано, как это обычно делается.
Пример В.6. Варианты оформления начала подпрограммы
; Вариант 1 — использование
трех команд
push bp ; сохранение содержимого bp
mov bp, sp ; запись в bpдреса верхушки стека
sub sp, N ; резервирование N байтов в стеке
; Вариант 2 — специальная команда enter
enter N, 0 ; заменяет три команды варианта 1
В первом варианте примера В.6 показано, как резервируется пространство размером N байтов с помощью обычных команд. Начиная с модели Intel 80286, у микропроцессоров появилась специальная команда enter. Она сохраняет в стеке содержимое регистра bp, копирует в bpдрес верхушки стека и уменьшает на N содержимое sp, т. е. по результату эквивалентна трем командам варианта 1. При использовании во внешних процедурах второй параметр команды enter равен нулю.
Важно
Ни при каких обстоятельствах значение указателя стека не может быть нечетным
числом. Поэтому количество байтов, отводимых для размещения промежуточных
переменных, обязательно должно быть четным. Однако это не означает, что
в пространстве стека нельзя размещать байты и работать с ними.
После выполнения любого из вариантов примера В.6 регистр bpспользуется для прямого доступа к параметрам и промежуточным переменным. Параметры расположены выше, а промежуточные переменные — ниже находящейся в bpточки отсчета. Обозначим смещение параметра или переменной как хх. В таком случае, при обращении к параметрам содержимое в bpувеличивается на величину смещения ([bp+хх]), а при обращении к промежуточным переменным оно уменьшается на величину смещения ([bp-хх]).
Прежде чем использовать переменные в командах, надо вычислить смещение каждой из них относительно регистра bp. Оно зависит от размеров предыдущих и данной переменной и не может быть равно нулю. Например, первая по порядку переменная может быть байтом, словом или двойным словом, ее адрес будет соответственно равен [bp-i], [bp-2] или [bp-4].
Имена параметров и переменных
Запись адреса в явном виде вполне корректна, но не наглядна.
Для того чтобы текст подпрограммы был более понятен, при визуальном анализе
лучше использовать имена.
Особенность данного случая в том, что переменные и параметры распределяются
не статическим, а динамическим способом. Это не позволяет использовать
для их описания обычные директивы db, dw и пр. Вместо этого используется
директива EQU (эквивалентно). Перед ней указывается имя параметра или
переменной, а после нее описание адреса и размера.
Предположим, что внешней подпрограмме передаются два параметра, каждый из которых имеет размер слова. Один их них задает ширину строки на экране в точках, а второй — количество строк на экране. В главе 2 для обозначения этих величин были введены имена Horsize и versize. Для присвоения этих имен параметрам в текст модуля подпрограммы надо включить две следующие директивы:
Horsize EQU word ptr [bp + 6] ; количество точек в строке
Versize EQU
word ptr [bp + 8] ; количество строк на экране
В этом примере предполагается, что перед вызовом внешней процедуры в стек сначада было записано значение параметра versize, а затем Horsize, например, так:
@Invoke имя_подпрограммы <Versize, Horsize>
Только в таком случае описание параметров соответствует их реальному расположению в стеке.
Обычно директивы EQU размещаются в начале тела модуля, вне сегмента (или вне сегментов). Имена, присвоенные в подпрограмме, являются локальными, поэтому вполне допустимо их совпадение с именами, описанными в основной программе или в других подпрограммах.
При указанном описании параметров для вычисления количества точек на экране в тексте подпрограммы выполняются следующие две команды:
mov ах, Horsize ; ах = количество точек в строке mui Versize
; dx:ax = Horsize * Versize
Описание промежуточных переменных отличается от описания параметров только указанием отрицательного смещения относительно bp, пример:
Address EQO word ptr [bp — 2] ; описание переменной Address
Следует отметить, что последовательность директив EQU является своеобразным описанием формальных параметров. Если такие директивы включены в текст модуля, то по его распечатке можно определить тип и последовательность указания параметров при вызове процедуры.
Контроль пространства стека
Контроль состояния стека нужен в тех случаях, когда задача использует вложенные процедуры и уровень вложенности достаточно велик. Вложенными называются процедуры последовательно вызывающие друг друга. При этом пространство стека, доступное каждой последующей процедуре, сокращается. При неудачном стечении обстоятельств оно может быть просто исчерпано. Особенно активно используют стек рекурсивные процедуры, способные многократно вызывать самих себя, правда, в графических задачах они обычно не используются.
Если проверка свободного пространства стека предусмотрена, то она выполняется в начале процедуры. При корректной работе со стеком размер свободного пространства в байтах равен значению указателя стека (содержимому регистра sp). Для выполнения контроля надо сравнить текущее содержимое sp с той величиной, которая нужна для выполнения процедуры. Если содержимое sp больше требуемого значения, то процедура может быть выполнена, в противном случае обычно выводится аварийное сообщение и выполнение задачи прекращается.
Процедура может использовать стек для размещения промежуточных переменных, хранения содержимого регистров и для вызова вложенных процедур. Определить требуемый размер пространства в стеке можно только на основании анализа исходного текста конкретной процедуры. Это должен делать ее разработчик.
Следует отметить, что проверка доступного пространства в стеке не является обязательной. Ее можно использовать на стадии отладки задачи, а затем исключить из подпрограмм. Напоминаем, что размер стекового сегмента указывается при его описании в тексте основной программы, поэтому всегда можно выбрать оптимальное значение.
Очистка стека является обязательным действием, выполняемым перед возвратом из процедуры. На стадии отладки задачи многие ошибки могут быть связаны с некорректными действиями при очистке стека.
В общем случае подпрограмма использует три разные области стека (см. табл. В.2) и их очистка производится разными способами.
Прежде всего, очищается общедоступная часть стека. Для этого количество использованных в подпрограмме команд push и pop должно совпадать.
Следующим шагом является освобождение пространства, выделенного для хранения промежуточных переменных, разумеется, если оно выделялось. В примере В.6 показаны два варианта резервирования пространства в стеке. В зависимости от используемого варианта выбирается способ освобождения стека.
Если применялся первый вариант примера В.6, то возможны два способа освобождения зарезервированного пространства. Первый способ заключается в увеличении содержимого регистра зр на величину м, соответствующую размеру выделенного пространства в байтах (add sp, N). Второй способ состоит в том, что в регистр sp копируется содержимое bp, при условии, что оно не изменялось в процессе выполнения подпрограммы (mov sp, bp).
Если для резервирования пространства в стеке использовался
второй вариант примера В.6, т. е. команда enter, то для его освобождения
применяется специальная команда leave, не имеющая параметров. При ее выполнении
содержимое регистра bР копируется в sp, поэтому оно не должно изменяться
при выполнении подпрограммы.
После выполнения описанных действий в верхушке стека должно находиться
исходное содержимое регистра bp. Оно выталкивается командой pop bp, после
которой можно выполнить ret для завершения подпрограммы.
Таким образом, если процедура ориентирована на передачу параметров в стеке, то ее выполнение завершают следующие две команды:
pop bp ; восстановление содержимого bpret ; возврат из подпрограммы
Удаление параметров. При возврате таким способом в основную задачу или в вызывавшую процедуру в стеке остаются параметры, которые надо удалить. Это можно сделать либо при выполнении команды ret, либо в вызывающем модуле сразу после возврата из подпрограммы.
Если в качестве параметра команды ret указать число к, то после выборки адреса возврата оно будет прибавлено к указателю стека. Такая модификация команды ret введена специально для удаления параметров при возврате из подпрограммы. Число к задает размер освобождаемой области стека в байтах, оно всегда четное. Например, если при вызове процедуры в стек было записано м параметров, каждый из которых имел размер слова, то к = 2м.
Для удаления параметров в вызывавшей процедуре сразу после возврата из подпрограммы выполняется команда add sp, к, где к задает размер освобождаемой области стека в байтах, о чем мы только что говорили.
Какой из двух способов лучше? Однозначного ответа на этот вопрос не существует, на практике применяются оба варианта. Удаление параметров при выполнении команды ret проще, но оно не допустимо, если в стеке находятся выходные параметры подпрограммы, предназначенные для вызывающего модуля. Во избежание подобных случаев при вызове подпрограммы в стеке можно указывать адреса выходных параметров, а в подпрограмме записывать результаты вычислений не в стек, а по указанным адресам. По окончании выполнения подпрограммы адреса параметров не нужны, и их можно удалять командой ret.
Таким образом, вы можете выбрать любой из двух вариантов удаления параметров, но желательно остановиться на одном варианте и использовать его во всех случаях.