В.Г. Баула - Введение в архитектуру ЭВМ и системы программирования (1110549), страница 17
Текст из файла (страница 17)
External;
Служебное слово External является указанием на то, что эта функция описана не в данной программе и Паскаль-машина должна вызвать эту внешнюю функцию, как-то передать ей параметры и получить результат работы функции. Если программист пишет эту функцию на Ассемблере, то он конечно никак не может "договориться" с Паскаль-машиной, как он хочет получать параметры и возвращать результат работы своей функции.
Именно для таких случаев и разработаны стандартные соглашения о связях. При этом если процедура или функция, написанная на Ассемблере, соблюдает эти стандартные соглашения о связях, то это гарантирует, что эту процедуру или функцию можно будет вызывать из программы, написанной на другом языке программирования, если в нём тоже соблюдаются такие же стандартные соглашения о связях.
Рассмотрим типичные стандартные соглашения о связях, обычно они включают следующие пункты.
-
Фактические параметры перед вызовом процедуры или функции записываются в стек.1 При передаче параметра по значению в стек записывается это значение, а в случае передачи параметра по ссылке в стек записывается адрес начала фактического параметра.2 Порядок записи фактических параметров в стек может быть прямым (сначала записывается первый параметр, потом второй и т.д.) или обратным (когда, наоборот, сначала записывается последний параметр, потом предпоследний и т.д.). В разных языках программирования этот порядок различный. Так, в языке С это обратный порядок, а в большинстве других языков программирования высокого уровня – прямой. 3
-
Если в процедуре или функции необходимы локальные переменные, то место им отводится в стеке. Обычно это делается путём увеличения размера стека, для чего, как мы уже знаем, надо уменьшить значение регистра SP на число байт, которые занимают эти локальные переменные.
-
Функция возвращает своё значение в регистрах al, ax или в паре регистров <dx,ax>, в зависимости от величины этого значения. Для возврата значений, превышающих двойное слово, устанавливаются специальные соглашения.
-
Если в процедуре или функции изменяются регистры, то в начале работы необходимо запомнить значения этих регистров в локальных переменных, а перед возвратом – восстановить эти значения (для функции, естественно, не запоминаются и не восстанавливаются регистр(ы), на котором(ых) возвращается результат её работы). Обычно также не запоминаются и не восстанавливаются регистры для работы с вещественными числами.
-
Перед возвратом из процедуры и функции стек очищается от всех локальных переменных, в том числе и от фактических параметров (вспомним, что в языке Паскаль формальные параметры, в которые передаются соответствующие им фактические параметры, тоже являются локальными переменными процедур и функций!).
Участок стека, в котором процедура или функция размещает свои локальные переменные (в частности, фактические параметры) называется стековым кадром (stack frame). Стековый кадр начинает строить основная программа перед вызовом процедуры или функции, помещая туда фактические параметры. Затем команда передачи управления с возвратом call помещает в стек адрес возврата (это одно слово для близкой процедуры и два – для дальней). Далее уже сама процедура или функция продолжает построение стекового кадра, размещая в нём свои локальные переменные.
Заметим, что если построением стекового кадра занимаются как основная программа, так и процедура (функция), то полностью разрушить стековый кадр должна процедура (функция), так что при возврате в основную программу стековый кадр будет уже уничтожен.1
Перепишем теперь нашу последнюю программу с использованием стандартного соглашения о связях. Будем предполагать, что передаваемый по ссылке адрес фактического параметра-массива занимает одно слово (т.е. является смещением в сегменте данных). Для хранения стекового кадра (локальных переменных функции) зарезервируем в стеке 32 слова. Ниже показано возможное решение этой задачи.
include io.asm
data segment
X dw 100 dup(?)
Y dw 200 dup(?)
Sum dw ?
data ends
stack segment stack
dw 64 dup (?); для системных нужд
dw 32 dup (?); для стекового кадра
stack ends
code segment
assume cs:code,ds:data,ss:stack
Summa proc near
; стандартные соглашение о связях
push bp
mov bp,sp; база стекового кадра
push bx
push ax
push cx; запоминание регистров
sub sp,2; порождение локальной переменной
S equ word ptr [bp-8]
; имя S будет эквивалентным адресу локальной переменной
mov cx,[bp+4]; cx:=длина массива
mov bx,[bp+6]; bx:=адрес первого элемента
mov S,0; сумма:=0
L: mov ax,[bx];сложение двумя командами,
add S,ax; так как нет формета память-память
add bx,2
loop L
mov ax,S; результат функции
add sp,2; уничтожение локальной переменной
pop cx
pop ax
pop bx
pop bp; восстановление регистров cx, bx и bp
ret 2*2
; возврат с очисткой стека от фактических параметров
Summa endp
start:mov ax,data
mov ds,ax
; здесь команды для ввода массивов X и У
mov ax, offset X; адрес начала X
push ax; первый фактический параметр
mov ax,100
push ax; второй фактический параметр
call Summa
mov Sum,ax; сумма массива X
mov ax, offset Y; адрес начала Y
push ax; первый фактический параметр
mov ax,200
push ax; второй фактический параметр
call Summa
add Sum,ax; сумма массивов X и Y
outint Sum
newline
finish
code ends
end start
Подробно прокомментируем эту программу. Первый параметр функции у нас передаётся по ссылке, а второй – по значению. После выполнения команды вызова процедуры call Summa стековый кадр имеет вид, показанный на рис. 7.2. После полного формирования стековый кадр будет иметь вид, показанный на рис. 7.3.
Начало стека SS | |
Вершина стека SP Начало стекового кадра | Адрес возврата |
Число элементов N | |
Адрес начала массива | |
Рис. 7.2. Вид стекового кадра при входе в функцию Summa. |
Начало стека SS | ||
Вершина стека SP База стекового кадра bp Начало стекового кадра | Локальная переменная S | bp-8 |
Значение регистра cx | bp-6 | |
Значение регистра ax | bp-4 | |
Значение регистра bx | bp-2 | |
Значение регистра bp | bp+0 | |
Адрес возврата | bp+2 | |
Число элементов N | bp+4 | |
Адрес начала массива | bp+6 | |
Рис. 7.3. Вид полного стекового кадра (справа показаны смещения слов кадра относительно значения регистра bp). |
Сначала отметим особое значение, которое имеет индексный регистр bp при работе со стеком. В архитектуре нашего компьютера это единственный индексный регистр, который предписывает по умолчанию осуществлять запись и чтение данных из сегмента стека. Так команда нашей программы
mov cx,[bp+4]; cx:=длина массива
читает в регистр cx слово, которое расположено по физическому адресу
Афиз = (SS*16 + (4 + <bp>)mod 216)mod 220,
а не по адресу
Афиз = (DS*16 + (4 + <bp>)mod 216)mod 220,
как происходит при использовании на месте bp любого другого индексного регистра, т.е. bx, si или di.
Таким образом, если установить регистр bp внутрь стекового кадра, то его легко использовать для доступа к локальным переменным процедуры или функции. Так мы и поступили в нашей программе, поставив регистр bp примерно на средину стекового кадра. Теперь, отсчитывая смещения от регистра bp вниз, например [bp+4], мы получаем доступ к фактическим параметрам, а, отсчитывая смещение вверх – доступ к сохранённым значениям регистров и локальной переменной, например [bp-8]это адрес локальной переменной, которую в программе на Паскале мы назвали именем S (см. рис. 7.3).
Обратите внимание, что локальные переменные в стековом кадре не имеют имён, что может быть не совсем удобно. В нашем примере мы присвоили локальной переменной имя S при помощи директивы эквивалентности
S equ word ptr [bp-8]
И теперь всюду вместо имени S Ассемблер будет подставлять выражение word ptr [bp-8], которое имеет, как нам и нужно, тип слова. Для порождение этой локальной переменной мы отвели ей место в стеке с помощью команды
sub sp,2; порождение локальной переменной
т.е. просто уменьшили значение регистра-указателя вершины стека на два байта. Этой же цели можно было бы достичь, например, более короткой (но менее понятной для нашей цели) командой
push ax; порождение локальной переменной
Перед возвратом из функции мы начали разрушение стекового кадра, как этого требуют стандартные соглашения о связях. Сначала командой
add sp,2; уничтожение локальной переменной
мы уничтожили локальную переменную, затем восстановили из стека старые значения регистров cx, bx и bp (заметьте, что регистр bp нам больше не понадобится в нашей функции). И, наконец, команда возврата
ret 2*2; возврат с очисткой стека
удаляет из стека адрес возврата и значение двух слов – фактических параметров функции. Уничтожение стекового кадра завершено.
Продолжение изучения стандартных соглашений о связях мы начнём со следующего замечания. В различных системах программирования стандартные соглашения о связях могут несколько различаться. Например, результат значения функции может возвращаться не на регистре ax, как в нашем последнем примере, а на вершине стека. В этом случае функция должна запоминать и восстанавливать регистр ax наравне с другими регистрами, а полное разрушение стекового кадра будет производить основная программа путем чтения результата работы функции из стека.
Важной особенностью использования стандартных соглашений о связях является и то, что они позволяют производить рекурсивный вызов процедур и функций, причём рекурсивный и не рекурсивный вызовы "по внешнему виду" не отличаются друг от друга. В качестве примера рассмотрим реализацию функции вычисления факториала от неотрицательного целого числа, при этом будем предполагать, что значение факториала поместится в слово. На языке Турбо-Паскаль эта функция имеет следующий вид:
Function Factorial(N: word): word;
Begin
if N<=1 then Factorial:=1
else Factorial:=N*Factorial(N-1)
End;
Реализуем теперь эту функцию в виде близкой процедуры на Ассемблере:
Factorial proc near; стандартные соглашение о связях
push bp
mov bp,sp; база стекового кадра
push dx
N equ word ptr [bp+4]; фактический параметр N
mov ax,1; Factorial(N<=1)
cmp N,1
jbe Vozv
mov ax,N
dec ax; N-1
push ax
call Factorial; Рекурсия
mul N; Factorial(N-1)*N
Vozv: pop dx
pop bp
ret 2
Factorial endp
Начало стека SS | |
Вершина стека SP Начало второго кадра Конец первого кадра Начало первого кадра | Значение регистра dx |
Значение регистра bp | |
Адрес возврата | |
N=4 | |
Значение регистра dx | |
Значение регистра bp | |
Адрес возврата | |
N=5 | |
Рис. 7.4. Два стековых кадра функции Factorial. |
Рассмотрим вызов этой функции Factorial для вычисления факториала числа 5. Такой вызов можно в основной программе сделать, например, следующими командами:
mov ax,5
push ax
call Factorial