Организация вызова подпрограммы

Вызов подпрограммы --- это передача управления на адрес начала ее кода. 
Подпрограмма может вызываться из разных мест программы как, 
например, стандартная подпрограмма writeln языка паскаль. После окончания 
исполнения подпрограмма должна передать управление на оператор, следующий 
за ее вызовом, поэтому необходимо при вызове запоминать адрес этого 
оператора. Подпрограммы также могут вызывать сами себе --- быть 
рекурсивными. Идеальной структурой данных для хранения такого адреса 
является стек. 

Если у подпрограммы нет локальных переменных, в частности, параметров, 
то лишь сохранение адреса возврата в стеке гарантирует корректный вызов 
любого числа подпрограмм и прерываний.

Примером такой подпрограммы является следующая процедура на паскале
 
procedure proc0;
  begin
    ...
  end;

или функция без значения на си

void proc0 () {
  ...
}.

В языке ассемблера совместимых с Intel 8086 процессоров (для других 
процессоров существуют те же возможности) существует несколько 
разновидностей двух команд CALL и RET. Первая --- это вызов подпрограммы с 
сохранением адреса следующей команды в стеке, а вторая --- это извлечение 
адреса из стека и передача управления по этому адресу, т.~е. возврат из 
подпрограммы. Адрес вершины стека храниться в регистре SP --- Stack Pointer. 
Например, при вызове proc0 SP уменьшается на размер адреса (на 8, 4 или 2
байта, в зависимости от разновидности CALL и режима работы процессора) и 
в стек по новому адресу в SP помещается адрес следующего оператора.

Работа с прерываниями проходит при помощи похожих средств, но здесь в стеке 
перед адресом сохраняется еще слово состояния процессора. Прерывания могут 
быть аппаратными, например, от клавиатуры, а могут программно генерироваться 
командой INT, возврат из обоих разновидностей прерываний осуществляется 
командой IRET (для завершения аппаратного прерывания нужно еще до выполнения 
IRET сбросить вызвавший его аппаратный сигнал).

Если у подпрограммы есть параметры или локальные переменные, то в стеке 
должно выделяться место для хранения всех этих локальных объектов. Это 
выделение должно происходить на аппаратном уровне автоматически, что и дает 
название этой памяти и хранимым в ней объектам. Задача этого размещения 
решается использованием еще одного регистра для работы со стеком BP --- 
Base Pointer. При вызове подпрограммы с автоматической памятью после 
сохранения адреса возврата сохраняется текущее значение BP, затем BP 
присваивается значение SP и SP уменьшается на размер автоматической памяти.

Например, при каждом вызове 

function proc1(a: word): word;
   var b: word;
   begin
      ...
   end;

или

short proc1 (short a) {
   short b;
      ...
}

в стеке должно выделяться 4 байта для хранения a и b. 
Если до вызова SP=1000 и используется 16-разрядный режим (в частности, 
адрес возврата, SP и BP будут 2-байтными), то после вызова 
SP=992. Сначала после сохранения адреса SP устанавливается в 998, затем 
после сохранения BP в 996, затем BP становится 996 и SP=996-4=992. Таким 
образом, BP становится адресом начала автоматической памяти подпрограммы. 
Адресация локальных объектов происходит относительно адреса BP, например, 
адрес a --- [BP-2], адрес b --- [BP-4].

   1000 --- SP до вызова подпрограммы
    998 --- адрес возврата
    996 --- сохраненный BP, BP в подпрограмме,
    994 --- начало автоматической памяти, адрес a
    992 --- адрес b, SP в подпрограмме

Таким образом, между BP и SP образуется стековый кадр или фрейм подпрограммы.

При выходе из подпрограммы перед RET нужно сначала установить SP 
равным BP (что уничтожит всю автоматическую память) и затем восстановить 
BP.

Рассмотренная схема достаточна для организации вызова подпрограмм в языке 
си, но в паскале подпрограммы могут быть вложенными, что требует 
дополнительных соглашений для организации их вызова. Рассмотрим

procedure proc2;
   var a: word;
   procedure proc21;
     var b: word;
     begin
       ...
     end;
   procedure proc22;
     var c: word;
     begin
       ...
     end;
   begin
     ...
   end;

--- здесь proc21 может использовать автоматические переменные a и b, а 
proc22 --- a и c, т.~е. кадры для proc21 и proc22 должны иметь ссылку 
на кадр для proc2. В общем случае нужно, чтобы каждая подпрограмма имела 
ссылки на кадры всех содержащих ее подпрограмм.

Синтаксическая связь между вложенными подпрограммами называется статической, 
а связь подпрограмм в процессе их вложенных вызовов --- динамической. Для 
корректной работы с подпрограммами необходимо поддерживать оба вида связи.
 
В процессорах, совместимых с Intel 80186, используются две команды для 
организации работы с кадрами подпрограмм. Команда ENTER используется для 
создания кадра, а LEAVE для его уничтожения. Первую используют перед вызовом
подпрограммы, а вторую --- после RET. ENTER имеет два параметра --- 
размер автоматической памяти и уровень синтаксической вложенности 
подпрограммы. У LEAVE параметров нет.

Алгоритм ENTER с параметрами size и level.

  push BP
  t <- SP
  if level > 0 then 
    for i <- 0 to level - 1 do
      BP <- BP - 4  (* или 2 в 16-разрядном режиме *)
      push [BP]     (* содержимое адреса в BP *)
    end for
    push t  
  end if
  BP <- t
  SP <- SP - size

В стеке создается список из ссылок, ENTER поддерживают 
вложенность до 31 подпрограммы, т.~е. максимальное значение level --- 31.
Построение адреса переменной из охватывающей подпрограммы из глубоко 
вложенной подпрограммы не требует прохода по списку ссылок, т.~к. 
в каждом кадре (при level > 0) есть все ссылки от начальной к 
наиболее "далекой" подпрограмме (при level > 1), до конечной "пустой", 
образующие массив, с начальным адресом в BP.

Пример. Перед вызовом кода proc21 нужно поставить ENTER 2,1, а перед вызовом 
proc2 --- ENTER 2,0. В подпрограммах на си второй параметр всегда 0. 

Команда ENTER N,0 эквивалентна трем инструкциям

  push bp
  mov bp,sp
  sub sp,N

ENTER выполняется на современных процессорах в несколько раз медленнее,
чем эти три инструкции, выигрывая только в большей компактности. Поэтому
ENTER практически не используется с непаскалеподобными языками.

Алгоритм LEAVE
  SP <- BP
  pop BP

Пример кода вызова подпрограммы proc1(7).

   push bp     ;enter 4,0
   mov bp,sp
   sub sp,4
   mov [bp-2],7   ;параметр a, локальная переменная b - по адресу [bp-4]
   call proc1
   leave

Можно работать с подпрограммами, используя инструкции PUSH и RET с параметром.
Такая RET после извлечения адреса возврата затем увеличивает SP на заданное
параметром-константой значение. Такой упрощенный способ годится только для
языков без вложенности подпрограмм.

Пример, вызов proc1(7) и структура proc1.

   push 7    ;параметр a
   sub sp,2  ;создаём место для локальных переменных
   call proc1

proc1:
   push bp
   mov bp,sp
      ; к a теперь можно обращаться через [bp+6], к b - через [bp+4]
   pop bp
   ret 4  ;4 - размер памяти для a и b
       
Существует два способа порядка передачи параметров в подпрограмму, 
соответствующие языкам паскаль и си. В паскале параметры передаются в стек 
справо-налево, что делает трудным определение функций с неограниченным
числом аргументов, а в си слева-направо. Например, на паскале вызовом
f(2,3) сначала в стек кладется 3, а затем 2; в си - наоборот.

Возврат значения функции может происходить через резервирование места для 
результата в коде подпрограммы, вызывающей функцию. Вызванная функция 
через сохраненный BP получает доступ к этому коду. Кроме того, для архитектуры
x64 характерно передавать все или часть параметров и возвращать значение через
регистры.