В.Г. Баула - Введение в архитектуру ЭВМ и системы программирования (975817), страница 16
Текст из файла (страница 16)
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]; {так в Паскале нельзя} 25
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.
Однако для того, чтобы так же хорошо писать на Ассемблере, нам понадобятся другие соглашения о связях между процедурой и основной программой. Вспомним, что хорошо написанная процедура в языке Паскаль получает все свои аргументы как фактические параметры и не использует имён глобальных переменных. При программировании процедур на языке Ассемблера мы будем использовать так называемые стандартные соглашения о связях.
7.9.1. Стандартные соглашения о связях
Сначала поймём необходимость существования некоторых стандартных соглашений о связях между процедурой и основной программой. Действительно, иногда программист просто не сможет "договориться", например, с процедурой, как она должна принимать свои параметры. В качестве первого примера можно привести так называемые библиотеки стандартных процедур. В этих библиотеках собраны готовые процедуры, реализующие алгоритмы для некоторой предметной области (например, для работы с матрицами). Такие библиотеки обычно поставляется в виде набора так называемых объектных модулей, что исключает возможность вносить изменения в исходный текст этих процедур (с объектными модулями мы познакомимся далее в нашем курсе).
Другим примером является написание частей программы на нескольких языках программирования, при этом чаще всего основная программа пишется на некотором языке высокого уровня (Фортране, Паскале, С и т.д.), а процедура – на Ассемблере. Вспомним, что когда мы говорили об областях применения Ассемблера, то одной из таких областей и было написание процедур, которые вызываются из программ на языках высокого уровня. Например, для языка Турбо-Паскаль такая, как говорят, внешняя, функция может быть описана следующим образом:
Function Summa(Var A: Mas, N: integer): integer;
External;
Служебное слово External является указанием на то, что эта функция описана не в данной программе и Паскаль-машина должна вызвать эту внешнюю функцию, как-то передать ей параметры и получить результат работы функции. Если программист пишет эту функцию на Ассемблере, то он конечно никак не может "договориться" с Паскаль-машиной, как он хочет получать параметры и возвращать результат работы своей функции.
Именно для таких случаев и разработаны стандартные соглашения о связях. При этом если процедура или функция, написанная на Ассемблере, соблюдает эти стандартные соглашения о связях, то это гарантирует, что эту процедуру или функцию можно будет вызывать из программы, написанной на другом языке программирования, если в нём тоже соблюдаются такие же стандартные соглашения о связях.
Рассмотрим типичные стандартные соглашения о связях, обычно они включают следующие пункты.
-
Фактические параметры перед вызовом процедуры или функции записываются в стек.26 При передаче параметра по значению в стек записывается это значение, а в случае передачи параметра по ссылке в стек записывается адрес начала фактического параметра.27 Порядок записи фактических параметров в стек может быть прямым (сначала записывается первый параметр, потом второй и т.д.) или обратным (когда, наоборот, сначала записывается последний параметр, потом предпоследний и т.д.). В разных языках программирования этот порядок различный. Так, в языке С это обратный порядок, а в большинстве других языков программирования высокого уровня – прямой. 28
-
Если в процедуре или функции необходимы локальные переменные, то место им отводится в стеке. Обычно это делается путём увеличения размера стека, для чего, как мы уже знаем, надо уменьшить значение регистра SP на число байт, которые занимают эти локальные переменные.
-
Функция возвращает своё значение в регистрах al, ax или в паре регистров <dx,ax>, в зависимости от величины этого значения. Для возврата значений, превышающих двойное слово, устанавливаются специальные соглашения.
-
Если в процедуре или функции изменяются регистры, то в начале работы необходимо запомнить значения этих регистров в локальных переменных, а перед возвратом – восстановить эти значения (для функции, естественно, не запоминаются и не восстанавливаются регистр(ы), на котором(ых) возвращается результат её работы). Обычно также не запоминаются и не восстанавливаются регистры для работы с вещественными числами.
-
Перед возвратом из процедуры и функции стек очищается от всех локальных переменных, в том числе и от фактических параметров (вспомним, что в языке Паскаль формальные параметры, в которые передаются соответствующие им фактические параметры, тоже являются локальными переменными процедур и функций!).
Участок стека, в котором процедура или функция размещает свои локальные переменные (в частности, фактические параметры) называется стековым кадром (stack frame). Стековый кадр начинает строить основная программа перед вызовом процедуры или функции, помещая туда фактические параметры. Затем команда передачи управления с возвратом call помещает в стек адрес возврата (это одно слово для близкой процедуры и два – для дальней). Далее уже сама процедура или функция продолжает построение стекового кадра, размещая в нём свои локальные переменные.
Заметим, что если построением стекового кадра занимаются как основная программа, так и процедура (функция), то полностью разрушить стековый кадр должна процедура (функция), так что при возврате в основную программу стековый кадр будет уже уничтожен.29
Перепишем теперь нашу последнюю программу с использованием стандартного соглашения о связях. Будем предполагать, что передаваемый по ссылке адрес фактического параметра-массива занимает одно слово (т.е. является смещением в сегменте данных). Для хранения стекового кадра (локальных переменных функции) зарезервируем в стеке 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,
а не по адресу