Chapter_07 (1110559), страница 12
Текст из файла (страница 12)
Для функции, естественно, не запоминаются и невосстанавливаются регистр(ы), на котором(ых) возвращается результат её работы. Обычнотакже принято не запоминать и не восстанавливать регистры для работы с вещественнымичислами (в нашей книге эти регистры не рассматриваются).• Перед возвратом из процедуры и функции стек очищается от всех локальных переменных, втом числе и от фактических параметров (вспомним, что в языке Паскаль формальные параметры, в которые передаются соответствующие им фактические параметры, тоже являются локальными переменными процедур и функций).Участок стека, в котором для процедуры или функции размещаются их локальные переменные(в частности, фактические параметры и адрес возврата) называется стековым кадром (stack frame).Стековый кадр начинает строить основная программа перед вызовом процедуры или функции, по1Большинство современных компьютеров имеют аппаратно реализованный стек, если же это не так, то встандартных соглашениях о связях для таких ЭВМ устанавливаются какие-нибудь другие способы передачипараметров, этот случай мы рассматривать не будем.2В некоторых языках программирования одного из этих способов может и не быть.
Так, в языке С (неС++) параметры передаются только по значению, а во многих версиях языка Фортран – только по ссылке.3Является ли этот адрес для нашей архитектуры близким (т.е. смещением в текущем сегменте данных)или же дальним (это значения сегментного регистра и смещение) иногда может специфицироваться при написании процедуры на языке высокого уровня. Ясно, что, скорее всего во внешнюю процедуру будут передаваться дальние адреса.4Обратный порядок позволяет более легко реализовывать процедуры и функции с переменным числомпараметров (в языке Паскаль, как Вы должны знать, для процедур пользователя это запрещено), в то же времяобратный порядок менее эффективен, чем прямой, так как труднее удалить из стека фактические параметрыпосле окончания процедуры.
В связи с этим, например, во многих реализациях языка С при описании внешнейпроцедуры программист может явно указывать порядок записи фактических параметров в стек.5В различных системах программирования стандартные соглашения о связях могут несколько различаться. Например, результат значения функции может возвращаться не на регистре ax, как у нас, а на вершинестека. Понятно, что всё это должно учитываться при программировании.37мещая туда (т.е. записывая в стек) фактические параметры. Затем команда call помещает в стек адрес возврата (это одно слово для близкой процедуры и два – для дальней) и передаёт управление наначало процедуры.
Далее уже сама процедура или функция продолжает построение стекового кадра,размещая в нём свои локальные переменные, в частности, для хранения старых значений изменяемых регистров.Заметим, что если построением стекового кадра занимаются как основная программа, так и процедура (функция), то полностью разрушить (очистить) стековый кадр должна сама процедура (функция), так что при возврате в основную программу стековый кадр будет уже уничтожен.1Перепишем теперь нашу последнюю "хорошую" программу с языка Паскаль на Ассемблер с использованием стандартного соглашения о связях. Будем предполагать, что передаваемый по ссылкеадрес фактического параметра-массива занимает одно слово (т.е. является близким адресом – смещением в сегменте данных).
Для хранения стекового кадра (локальных переменных функции) дополнительно зарезервируем в стеке, например, 32 слова. Ниже показано возможное решение этойзадачи.include io.asmdata segmentXdw100 dup(?)Ydw200 dup(?)Sumdw?data endsstack segment stackdw64 dup (?); для системных нуждdw32 dup (?); для стекового кадраstack endscode segmentassume cs:code,ds:data,ss:stackSumma proc near; стандартные соглашение о связяхpush bpmovbp,sp; база стекового кадраpush bxpush cx;запоминание используемых регистровsubsp,2; порождение локальной переменнойSequword ptr [bp-6]; S будет именем локальной переменнойmovcx,[bp+4]; cx:=длина массиваmovbx,[bp+6]; bx:=адрес первого элементаmovS,0; сумма:=0L:movax,[bx];нельзя add S,[bx]addS,ax;так как нет формета память-памятьaddbx,2loop Lmovax,S; результат функции на axaddsp,2; уничтожение локальной переменной Spopcxpopbxpopbp; восстановление регистров cx, bx и bpret2*2; возврат с очисткой стека от фактических параметровSumma endpstart:movax,data1При обратном порядке записи фактических параметров в стек (как в языке С) удаление из стека фактических параметров обычно делает не процедура, а основная программа, т.к.
это легче реализовать для переменного числа параметров (основной программе, в отличие от процедуры, легче определить, сколько параметровзаписано в стек для конкретного вызова). Плохо в этом случае то, что такое удаление приходится выполнять нев одном месте (при выходе из процедуры), а в каждой точке возврата из процедуры, что существенно увеличивает размер кода (в программе может быть очень много вызовов одной и той же процедуры).38movds,ax; здесь команды для ввода массивов X и Уmovax, offset X; адрес начала Xpush ax; первый фактический параметрmovax,100push ax; второй фактический параметрcall SummamovSum,ax; сумма массива Xmovax, offset Y; адрес начала Ypush ax; первый фактический параметрmovax,200push ax; второй фактический параметрcall SummaaddSum,ax; сумма массивов X и Youtint Sumnewlinefinishcode endsend startПодробно прокомментируем эту программу.
Первый параметр функции у нас передаётся поссылке, а второй – по значению, именно так мы и записываем эти параметры в стек. После выполнения команды вызова процедуры call Summa стековый кадр имеет вид, показанный на рис. 7.4.После полного формирования стековый кадр будет иметь вид, показанный на рис. 7.5. (справа наэтом рисунке показаны смещения слов в стеке относительно значения регистра bp).Начало стека SS →Вершина стека SP →Начало стекового кадра →Адрес возврата IPЧисло элементов NАдрес начала массиваРис.
7.4. Вид стекового кадра при входе в функцию Summa.Отметим далее то особое значение, которое имеет индексный регистр 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-6]являетсяадресом локальной переменной, которую в нашей программе на Ассемблере мы, повторяя программу на Паскале, назвали именем S (см. рис. 7.5). Теперь понятно, почему регистр bp часто называютбазой стекового кадра (base pointer).39Начало стека SS →Вершина стека SP →База стекового кадра bp →Начало стекового кадра →Локальная переменная SЗначение регистра cxЗначение регистра bxЗначение регистра bpАдрес возврата IPЧисло элементов NАдрес начала массиваbp-6bp-4bp-2bp+0bp+2bp+4bp+6Рис.
7.5. Вид полного стекового кадра (справа показаны смещенияслов кадра относительно значения регистра bp).Обратите внимание, что локальные переменные в стековом кадре не имеют имён, что можетбыть не совсем удобно для программиста. В нашем примере мы присвоили локальной переменнойимя S при помощи директивы эквивалентностиS equ word ptr [bp-6]И теперь всюду вместо имени S Ассемблер будет подставлять выражение word ptr [bp-6], которое имеет, как нам и нужно, тип слова, расположенного в стеке. Для порождения этой локальнойпеременной мы отвели ей место в стеке с помощью командыsub sp,2; порождение локальной переменнойт.е.
просто уменьшили значение регистра-указателя вершины стека на два байта. Эта переменная порождается, как мы и привыкли в языке Паскаль, с неопределённым начальным значением. Этой жецели можно было бы достичь, например, более короткой (но менее понятной для читающего программу человека) командойpush ax; порождение локальной переменнойЗаметьте, что теперь переменная S порождается уже с начальным значением, равным значениюрегистра ax, что нам, вообще говоря, было не нужно.Перед возвратом из функции мы начали разрушение стекового кадра, как этого требуют стандартные соглашения о связях.
Сначала командойadd sp,2; уничтожение локальной переменноймы уничтожили локальную переменную S (т.е. удалили её из стека), затем восстановили из стекастарые значения регистров cx, bx и bp (заметьте, что регистр bp нам больше не понадобится в нашей функции). И, наконец, команда возвратаret 2*2; возврат с очисткой стекаудаляет из стека адрес возврата и значение двух слов – значений фактических параметров функции.Уничтожение стекового кадра завершено.Важной особенностью использования стандартных соглашений о связях является и то, что онипозволяют производить рекурсивный вызов процедур и функций, причём рекурсивный и не рекурсивный вызовы "по внешнему виду" не отличаются друг от друга.
В качестве примера рассмотримреализацию функции вычисления факториала от неотрицательного (т.е. беззнакового) целого числа,при этом будем предполагать, что значение факториала поместится в слово (иначе пусть мы выдадим неправильный результат без диагностики об ошибке). На языке Турбо-Паскаль эта функцияимеет следующий вид:Function Factorial(N: word): word;Beginif N<=1 then Factorial:=1else Factorial:=N*Factorial(N-1)End;40Будем надеяться, что язык Паскаль Вы все хорошо знаете, и без пояснений понимаете, как работает рекурсивная функция ☺. Реализуем теперь эту функцию в виде близкой процедуры на Ассемблере. Сначала заметим, что условный оператор Паскаляif N<=1 then Factorial:=1else Factorial:=N*Factorial(N-1)для программирования на Ассемблере неудобен, так как содержит две ветви (then и else), которые придётся размещать в линейной структуре машинной программы, что повлечёт использованиемежду этими ветвями команды безусловного перехода.