Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Баула.doc
Скачиваний:
112
Добавлен:
28.05.2015
Размер:
1.82 Mб
Скачать

7.8. Команды вызова процедуры и возврата из процедуры

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

call op1

где op1 может иметь следующие форматы: i16, r16, m16, m32 и i32. Как видим, по сравнению с командой безусловного перехода здесь не реализован только близкий короткий относительный переход сall i8 , он практически бесполезен в практике программирования, так как почти всегда тело процедуры находится достаточно далеко от точки вызова этой процедуры. Таким образом, как и команды безусловного перехода, команды вызова процедуры бывают близкими (внутрисегментными) и дальними (межсегментными). Близкий вызов процедуры выполняется по следующей схеме:

Встек(IP); jmp op1

Здесь запись Встек(IP)обозначает действие "записать значение регистра IP в стек". Заметим, что отдельной команды push IP в языке машины нет. Дальний вызов процедуры выполняется по схеме:

Встек(CS); Встек(IP); jmp op1

Для возврата на команду программы, адрес которой находится на вершине стека, предназначена команда возврата из процедуры, по сути, это тоже команда безусловного перехода. Команда возврата из процедуры имеет следующий формат:

ret [i16]; Параметр может быть опущен

На языке машины у этой команды есть две модификации, отличающиеся кодами операций: близкий и дальний возврат из процедуры. Нужный код операции выбирается программой Ассемблера автоматически, по контексту использования команды возврата, о чём мы будем говорить далее. Если программист опускает параметр этой команды i16, то Ассемблер автоматически полагает i16=0.

Команда близкого возврата из процедуры выполняется по схеме:

Изстека(IP); SP:=(SP+i16)mod 216

Здесь, по аналогии с командой вызова процедуры, запись Изстека(IP)обозначает операцию "считать из стека слово и записать его в регистр IP".

Команда дальнего возврата из процедуры выполняется по схеме:

Изстека(IP); Изстека(CS); SP:=(SP+i16)mod 216

Действие SP:=(SP+i16)mod 216 приводит к тому, что указатель вершины стека SP устанавливается на некоторое другое место в стеке. В большинстве случаев этот операнд имеет смысл только для чётных i16>0 и SP+i16<=K, где K – размер стека. В этом случае из стека удаляются i16 div 2 слов, что можно трактовать как очистку стека от данного количества слов (уничтожение соответствующего числа локальных переменных). Возможность очистки стека, как мы увидим, будет весьма полезной при программировании процедур на Ассемблере.

7.9. Программирование процедур на Ассемблере

В языке Ассемблера есть понятие процедуры – это участок программы, который начинается директивой

<имя процедуры> proc [<спецификация процедуры>]

и заканчивается директивой

<имя процедуры> endp

Вложенность процедур, в отличие от языка Паскаль, не допускается. Имя процедуры имеет тип метки, хотя за ним и не стоит двоеточие. Вызов процедуры обычно производится командой call, а возврат из процедуры – командой ret.

Спецификация процедуры – это константа –2 (этой служебной константе в Ассемблере присвоено имя far) или –1 (этой служебной константе в Ассемблере присвоено имя near).1 Если спецификация опущена, то имеется в виду ближняя (near) процедура. Спецификация процедуры – это единственный способ повлиять на выбор Ассемблером конкретного кода операции для команды возврата ret внутри этой процедуры: для близкой процедуры это близкий возврат, а для дальней – дальний возврат. Отметим, что для команды ret, расположенной вне процедуры Ассемблером выбирается ближний возврат.

Изучение программирования процедур на Ассемблере начнём со следующей простой задачи: пусть надо ввести массивы X и У знаковых целых чисел, массив X содержит 100 чисел, а массив Y содержит 200 чисел. Затем необходимо вычислить величину

Будем предполагать, что массивы находятся в одном сегменте данных, а переполнение результата при сложении будем для простоты игнорировать (не выдавать диагностику). Для данной программы естественно реализовать процедуру суммирования элементов массива и дважды вызывать эту процедуру для массивов X и Y. Текст нашей процедуры мы, как и в Паскале, будем располагать перед текстом основной программы (начало программы, как мы знаем, помечено меткой, указанной в директиве end нашего модуля).

Заметим, что, вообще говоря, мы будем писать функцию, но в языке Ассемблера различия между процедурой и функцией не синтаксическое, а чисто семантическое, т.е. о том, что это функция, а не процедура, знает программист, но не программа Ассемблера, поэтому далее мы часто будем использовать обобщённый термин процедура.

Перед тем, как писать процедуру, необходимо составить соглашение о связях между основной программой и процедурой.2 Это соглашение включает в себя способ передачи параметров, возврата результата работы и некоторую другую информацию. Так, мы "договоримся" с процедурой, что суммируемый массив слов будет располагаться в сегменте данных, адрес первого элемента перед вызовом процедуры будет записан в регистр bx, а количество элементов – в регистр cx. Сумма элементов массива при возврате из процедуры должна находится в регистре ax. При этих соглашениях о связях у нас получится следующая программа (для простоты вместо команд для ввода массивов вы указали только комментарий).

include io.asm

data segment

X dw 100 dup(?)

Y dw 200 dup(?)

Sum dw ?

data ends

stack segment stack

dw 64 dup (?)

stack ends

code segment

assume cs:code,ds:data,ss:stack

Summa proc

; соглашение о связях: bx – адрес первого элемента

; cx=количество элементов, ax – ответ (сумма)

sub ax,ax; сумма:=0

L: add ax,[bx]

add bx,2

loop L

ret

Summa endp

start:mov ax,data

mov ds,ax

; здесь команды для ввода массивов X и У

mov bx, offset X; адрес начала X

mov cx,100; число элементов в X

call Summa

mov Sum,ax; сумма массива X

mov bx, offset Y; адрес начала Y

mov cx,200; число элементов в Y

call Summa

add Sum,ax; сумма массивов X и Y

outint Sum

newline

finish

code ends

end start

Если попытаться один к одному переписать эту программу на Турбо-Паскале, то получится примерно следующее:

Program S(input,output);

Var X: array[1..100] of integer;

Y: array[1..200] of integer;

bx: integer; Sum,cx,ax: integer;

Procedure Summa;

Label L;

Begin

ax:=0;

L: ax := ax + bx; bx:=bx+2; {так в Паскале нельзя}

dec(cx); if cx<>0 then goto L

End;

Begin {Ввод массивов X и Y}

cx:=100; bx:=X[1]; {так в Паскале нельзя} 1

Summa; Sum:=ax;

cx:=200; bx:=Y[1]; {так в Паскале нельзя}

Summa; Sum:=Sum+ax; Writeln(Sum)

End.

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

Program S(input,output);

Type Mas= array[1..N] of integer;

{так в Паскале нельзя, N – не константа}

Var X,Y: Mas;

Sum: integer;

Function Summa(Var A: Mas, N: integer): integer;

Var i,S: integer;

Begin S:=0; for i:=1 to N do S:=S+A[i]; Summa:=S End;

Begin {Ввод массивов X и Y}

Sum:=Summa(X,100); Sum:=Sum+Summa(Y,200); Writeln(Sum)

End.

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