В.Г. Баула - Введение в архитектуру ЭВМ и системы программирования (1110549), страница 16
Текст из файла (страница 16)
mov dx, offset T2
outstr
L1: pop ax
outword ax,10; ширина поля вывода=10
loop L1
Kon: finish
Err: mov dx,T3
outstr
finish
code ends
end program_start
Заметим, что в нашей программе нет собственно переменных, а только строковые константы, поэтому мы не описали отдельный сегмент данных, а разместили эти строковые константы в кодовом сегменте. Можно считать, что сегменты данных и кода в нашей программе совмещены. Мы разместили строковые константы в начале сегмента кода, перед входной точкой программы, но с таким же успехом можно разместить эти строки и в конце кодового сегмента после последней макрокоманды finish.
Обратите внимание, как мы выбрали размер стека: 128 байт мы зарезервировали для системных нужд (как уже упоминалось, стеком будут пользоваться и другие программы, подробнее об этом будет рассказано далее) и 300 слов мы отвели для хранения введённых нами чисел. При реализации этой программы может возникнуть желание определять, что введено слишком много чисел, анализируя переполнение стека. Другими словами, вместо проверки
cmp cx,300; в стеке уже 300 чисел ?
je Err
казалось бы, можно было поставить проверку исчерпания стека
cmp SP,2; стек уже полон ?
jb Err
Это, однако, может повлечь за собой тяжёлую ошибку. Дело в том, что в стеке может остаться совсем мало места, а, как мы знаем, стек использует не только наша, но и другие программы, которые в этом случае будут работать неправильно.
Теперь, после того, как мы научились работать со стеком, вернёмся к дальнейшему рассмотрению команд перехода.
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.
Однако для того, чтобы так же хорошо писать на Ассемблере, нам понадобятся другие соглашения о связях между процедурой и основной программой. Вспомним, что хорошо написанная процедура в языке Паскаль получает все свои аргументы как фактические параметры и не использует имён глобальных переменных. При программировании процедур на языке Ассемблера мы будем использовать так называемые стандартные соглашения о связях.
7.9.1. Стандартные соглашения о связях
Сначала поймём необходимость существования некоторых стандартных соглашений о связях между процедурой и основной программой. Действительно, иногда программист просто не сможет "договориться", например, с процедурой, как она должна принимать свои параметры. В качестве первого примера можно привести так называемые библиотеки стандартных процедур. В этих библиотеках собраны готовые процедуры, реализующие алгоритмы для некоторой предметной области (например, для работы с матрицами). Такие библиотеки обычно поставляется в виде набора так называемых объектных модулей, что исключает возможность вносить изменения в исходный текст этих процедур (с объектными модулями мы познакомимся далее в нашем курсе).
Другим примером является написание частей программы на нескольких языках программирования, при этом чаще всего основная программа пишется на некотором языке высокого уровня (Фортране, Паскале, С и т.д.), а процедура – на Ассемблере. Вспомним, что когда мы говорили об областях применения Ассемблера, то одной из таких областей и было написание процедур, которые вызываются из программ на языках высокого уровня. Например, для языка Турбо-Паскаль такая, как говорят, внешняя, функция может быть описана следующим образом:
Function Summa(Var A: Mas, N: integer): integer;