Как долго заживает перелом у собак.
Для работы с подпрограммами в составе системы команд любого процессора есть две команды (см. рисунок ниже):
Кроме упомянутых команд и их групп, в отдельных моделях, как универсальных, так и специализированных процессоров могут быть реализованы специальные команды, предназначенные для выполнения более сложных действий, часто используемых в применениях, для которых разрабатывалась конкретная модель. Приведем несколько примеров:
Для обращения к подпрограмме используется специальная команда вызова подпрограммы. В системе команд х86 она имеет мнемонику call.
Минимальный набор действий, которые выполняет такая команда, это:
Для возврата из ПП в системе команд есть специальная команда (в i*86 ее мнемоника ret). Эта команда загружает ранее сохраненный адрес возврата в счетчик команд.
В х86 имеются две разновидности команды call: ближний near вызов (внутри текущего сегмента кода) и дальний far вызов (в другой сегмент кода). В случае использования дальнего вызова в стеке сохраняется, наряду со счетчиком команд (e)ip, и сегментный регистр cs.
Соответственно имеются и две команды возврата ret near и ret far. Состояние стека при ближнем и дальнем вызовах иллюстрируется следующим рисунком (см. рисунок ниже).
Понятие контекста (вектора состояния): в каждой конкретной точке выполнения программы — содержимое всех переменных элементов (регистров, ячеек памяти), которое требуется установить/восстановить, чтобы стало возможным запустить выполнение программы с этой точки.
Подпрограмма при своей работе использует какие-либо ресурсы процессора (регистры), а, кроме того, меняет состояние отдельных ячеек памяти, флагов и т.п. — т.е. ПП изменяет контекст. Естественный способ исключить влияние изменения контекста — непосредственно перед вызовом ПП или сразу после входа в ПП сохранять текущий контекст, а при выходе из ПП все восстанавливать назад. Было бы удобно, если бы программисту не надо было задумываться о сохранении-восстановлении контекста. Проблема сохранения-восстановления контекста возникает и в других ситуациях (прерывания, переключение задач — это будет позже). Чаще всего для автоматического сохранении-восстановлении контекста используется стек.
Рассмотрим типовую последовательность команд, используемую при вызове подпрограммы. Пусть подпрограмма использует два входных параметра (два аргумента) типа int, возвращает значение тоже типа int и использует две локальные переменные. Две команды mov в вызывающей части помещают в стек параметры, после чего выполняется команда вызова подпрограммы call.
Первые две команды подпрограммы сохраняют старое значение ebp (элемент контекста) и устанавливают ebp на адрес внутри стекового кадра. Следующая команда (sub esp,8) занимает в стеке место под две локальные переменные.
Теперь подпрограмма может, используя адресацию косвенно-регистровую со смещением через регистр ebp, иметь доступ к параметрам, используя положительные значения смещения (в примере для доступа к первому параметру смещение должно быть равно 12), и к локальным переменным, используя отрицательные смещения (в примере смещение -8 позволяет обращаться к локальной переменной 2).
Результат, вычисляемый подпрограммой, можно передавать в вызывающую часть разными способами, но чаще всего это делают, помещая результата в регистр перед возвратом из подпрограммы, как сделано в примере. Для освобождения места, занятого локальными переменными, следует вернуть указателю стека то значение, которое он имел в начале подпрограммы (оно хранится в ebp). После этого выполняется команда возвврата ret, использующая сохраненный в стеке адрес возврата в вызывающую часть программы.
После возврата следует освободить место в стеке, занятое параметрами. В нашем примере это делает команда add esp, 8 , однако в архитектуре х86 это может быть сделано с помощью разновидности команды возварата из подпрограммы ret n, она не только осуществляет возварт, но и извлекает из стека (в "никуда") число байтов, указываемое параметром n.
Для облегчения сохранения-восстановления контекста в разных реализациях процессоров могут быть использованы:
1) Специальные команды:
2) Несколько экземпляров наборов регистров:
При обращении к подпрограмме могут передаваться: сами данные или адрес участка памяти, где находятся данные. Данные могут находиться:
В большинстве процессоров для а) сохранения-восстановления контекста, б) связи по данным (передачи параметров и возврата значений), в) выделения памяти под локальные переменные используется фрагмент стека, называемый стековым кадром.
Пример на языке С.
int Calc (int x, int y) {
int iX, iY;
iX=2*x+y;
iY=3*y-x;
return iX*iY;
}
Эта процедура принимает два параметра x и y и возвращает результат вычисления несложного арифметического выражения в глобальную ёпеременную z. В процедуре объявлены две локальные переменные. Далее приведен результат компиляции этой процедуры после его дизассемблирования полноэкранным отладчиком TurboDebugger (см. таблицу ниже).
Посмотрим теперь структуру стека (см. таблица ниже). Столбец 2) содержит адреса стека как они получились при компиляции рассматриваемого примера, столбец 3) показывает состояние этих адресов до начала помещения параметров в стек перед вызовом процедуры, столбец 4) — состояние стека после входа в процедуру и выделения места под локальные переменные. В столбце 1) отмечен участок стека, составляющий стековый кадр для рассматриваемого примера.
После входа в процедуру в bp заносится базовый адрес стекового кадра. Благодаря этому, к параметрам можно получить доступ косвенно-регистровой адресацией через bp с положительным смещением [bp+disp], а к локальным переменным — с отрицательным смещением [bp-disp].
Рекурсивный вызов (когда ПП вызывает сама себя) иногда бывает полезен. Классический пример — вычисление факториала с использованием соотношения
N ! = N * (N-1) !
Другой пример — решение задачи о "Ханойской башне".
Рекурсия при вызове подпрограммы может быть как прямая (ПП вызывает сама себя) так и цепная (подпрограмма A вызывает подпрограмму B, та, в свою очеред,ь вызывает подпрограмму C, а затем подпрограмма C вызывает подпрограмму A).
Проблем при рекурсивном вызове несколько:
Решение всех этих проблем обеспечивается, когда при вызове ПП в стеке выделяется блок ячеек (стековый кадр) для параметров и временных переменных очередного экземпляра ПП (как в примере, рассмотренном выше). Адресацию внутри стекового кадра удобно делать относительно какого-либо его адреса. Для этого перед вызовом ПП параметры помещаются в стек:
push par1
push par2
call subr
Вначале ПП применяется, как мы уже видели, типовая последовательность команд:
subr: push bp; запоминание старого значения регистра базы bp
mov bp,sp; передача в bp адреса начала стекового кадра
Теперь можно к локальным переменным ПП обращаться, используя адресацию относительно регистра базы:
mov ax,[bp+4]
mov bx,[bp+6]
Для обеспечения возможности использовать параметры и переменные "внешних" экземпляров ПП при рекурсивном вызове стековый кадр должен содержать ссылки на "предыдущие" стековые кадры. В системе команд процессора могут быть команды, облегчающие формирование стекового кадра. Например, в х86 есть команды ENTER и LEAVE:
enter frmsiz, level
Первый параметр команды определяет количество байтов, требуемых в стеке для временных (локальных) переменных. Второй параметр показывает уровень "рекурсивности" (для самого внешнего уровня level=1). Для доступа к элементам стекового кадра используется адресный регистр BP, который указывает на начало (самый старший адрес) стекового кадра.
Структура стекового кадра в х86 включает три компонента (см. рисунок ниже):
Выполнение команды ENTER:
При выполнении команды делаются следующие действия (описание делается в предположении, что происходит рекурсивный вход в подпрограмму):