Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
flor_apparato-orientirovnnoe_prog.doc
Скачиваний:
89
Добавлен:
15.06.2014
Размер:
926.72 Кб
Скачать

5.3. Организация стекового кадра подпрограммы

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

Принципиальным решением по передаче аргументов стало использование для этого стека. Пусть некоторая подпрограмма с именем funa описана на некотором языке высоко уровня, например Паскаль, использует при вызове фактические аргументы arg1, arg2, arg3 и пусть все они для простоты картины имеют целочисленный тип. Вызов этой функции, который на языке высоко уровня запишется в виде funa(arg1, arg2, arg3), реализуется с помощью стека следующей последовательностью команд:

push значение arg1

push значение arg2

push значение arg3

call funa

В результате все значения фактических аргументов окажутся в стеке, и вызванной подпрограмме останется только извлекать их оттуда. С этим извлечением возникают, казалось бы, некоторые проблемы. Извлекать их непосредственно командами POP нельзя, так как над значениями этих аргументов в стеке оказывается значение адреса возврата, записываемое туда командой CALL. Снимая с вершины стека этот адрес возврата, подпрограмма лишила бы себя возможности вернуть управление в вызвавшую ее программу (команде RET нужен адрес возврата на вершине стека). Какие-то манипуляции с стеком, отличные от классических операций PUSH, POP теоретически возможны, но такое решение не удобно.

С учетом динамического характера заполнения стека (в свое время) было принято дальновидное решение связывать участок заполнения стека для процедуры со специальным регистром - указателем. Этот регистр называют указателем фрейма или указателем кадра, а в архитектуре Intel его назвали просто базовым указателем, подразумевая базовый указатель фрейма в стеке. Символическое обозначение этого регистра здесь - EBP (от сочетания слов - Base Pointer). Обратим внимание, что использование этого регистра специализированно назначением, хотя временно его можно использовать и как регистр общего назначения (но не рекомендуется, так как при этом нужно учитывать его определенные особенности).

Специализация регистра EBP заключается прежде всего в том, что подпрограммы стандартной структуры должны в качестве первых двух своих командах выполнить следующие:

PUSH EPB

MOV EBP, ESP

Первая из этих команд просто сохраняет предыдущее значение регистра EBP в стеке, чтобы при выходе из процедуры восстановить исходное значение, которое этот регистр имел в вызывающей процедуре. Вторая команда запоминает в регистре EBP адрес текущей верхушки стека - напомним, что специализированный регистр ESP всегда используется как место хранения вершины стека. Далее в ходе выполнения программы процедуры содержимое регистра EBP не должно меняться. Поэтому, какие бы осмысленные манипуляции со стеком не делались в процедуре, регистр EBP своим содержимым постоянно указывается в ней на фиксированное место в стеке. А именно, указывает место запомненного старого содержимого регистра EBP, которое, в свою очередь, записано сразу за адресом возврата. Адрес же возврата был записан командой CALL сразу после записи значений аргументов процедуры в стек.

Посмотрим с помощью рис. 5.3.1, к какому строению участка стека привело выполнение указанных выше двух первых команд процедуры.

Рис. 5.3.1. Строение кадра стека после стандартного пролога в подпрограмме

Из этого рисунка, как и из внимательного рассмотрения действий заполнения стека, следует, что содержимое четырехбайтового поля стека, где размещено значение аргумента arg3, может быть обозначено с помощью базового способа адресации в виде [EBP+8]. (Старое значение регистра EBP обозначается как [EBP+0], значение адреса возврата как [EBP+4], а далее следует как раз значение аргумента arg3.) Содержимое аналогичного поля стека, где содержится значение аргумента arg2, обозначится как [EBP+12], а содержимое поля стека, где находится значение аргумента arg1, - как [EBP+16].

Следует особенно подчеркнуть, что такое обозначение позволяет через содержимое служебного регистра всегда "дотягиваться" до значений аргументов процедур вне зависимости, на какое конкретное место от начала стека они помещаются при вызове. В частности, когда процедура вызывается многократно, не завершив действий предыдущих вызовов, - в так называемых рекурсивных вызовах.

Заметим, что в общем случае могут использоваться более экзотические вызовы процедур, так называемые дальние (FAR), при которых адрес возврата занимает не одно четырехбайтовое поле в стеке, а в два раза больше. В таких случаях указанные рассуждения приводят к обозначению [EBP+12] уже для самого верхнего аргумента в стеке.

Используя рассмотренные содержательные связи, вычисление суммы первых двух аргументов процедуры можно выполнить с помощью двух команд:

MOV eax, [ebp+16] ; arg1

ADD eax, [ebp+12] ; arg2

Для дополнительных объяснений, не связанных непосредственно с проблемой организации кадра процедуры, но использующих приведенную информацию и полезных для понимания действительных операций с аргументами в процедуре, рассмотрим следующее уточнение примера. Пусть третий аргумент данной процедуры представляет собой не просто числовое значение, а является адресом значения третьего аргумента, например, будучи описан в исходной программе на Паскале в составе определения заголовка

PROCEDURE funa(arg1: INTEGER; arg2: INTEGER; VAR arg3 : INTEGER);

Для данной процедуры аргументы arg1 и arg2 передаются по значению, а аргумент arg3 - по ссылке (иначе говоря, по адресу). Поэтому обращение к процедуре funa записывается машинными командами, записанными на ассемблере NASM, как

PUSH DWORD [arg1]

PUSH DWORD [arg2]

PUSH DWORD arg3

Запись того же фрагмента на ассемблере TASM/MASM будет иметь несколько иной вид, а именно

PUSH arg1

PUSH arg2

PUSH OFFSET arg3

(Здесь используется возможность неявного задания атрибутов данных, определенных при их описании в области данных, что позволило отказаться от уточнителя-модификатора DWORD и использования вспомогательного служебного слова OFFSET для указания адреса именованного объекта вместо его самого.)

Сложение оператора Паскаля

arg3:=arg1+arg2;

выполнится в этом примере командами

MOV eax, [ebp+16] ; arg1

ADD eax, [ebp+12] ; arg2

MOV ebx, [ebp+10] ; адрес собственно arg3

MOV [ebx], eax

причем в реальной подпрограмме необходимо было предварительно сохранить (а перед выходом из подпрограммы восстановить) значения регистров eax и ebx.

Перед выходом из процедуры, если она использует стандартное строение кадра процедуры, следует дополнительно выполнять команду POP EBP - для восстановления старого значения регистра EBP, которое нужно будет после возврата в вызывавшую программу. С учетом необходимого сохранения рабочих регистров для расширенного примера собственно содержимое запишется фрагментом

funa: ;; процедура funa

push ebp

mov ebp, esp

push eax

push ebx

mov eax, [ebp+16] ; arg1

add eax, [ebp+12] ; arg2

mov ebx, [ebp+10] ; адрес собственно arg3

mov [ebx], eax

pop ebx

pop eax

pop ebp

ret ; end procedure

Таким образом, как мы видим, в кадре процедуры хранятся аргументы ее вызова, адрес возврата и сохраняемые значения регистров. В действительности часть кадра процедуры еще систематически выделяется для хранения локальных переменных. Если переменные, определенные на языках высокого уровня вне подпрограмм, размещаются в сегменте данных, статические данные (данные, описанные со служебным словом static) в языке Си хранятся также в сегменте данных, то локальные переменные (определенные внутри подпрограммы) размещаются обязательно внутри кадра процедуры.

С этой целью непосредственно после команды MOV EBP,ESP в начале машинного кода процедуры выполняется команда

SUB ESP, размер_области_локальных_переменных

Например, если в подпрограмме определены две локальные переменные i,j целочисленного типа для 32-битной архитектуры (т.е. по 4 байта каждая), то под них необходимо зарезервировать 8 байтов, задав размер области, равным восьми. В результате изменения значения регистра ESP для указателя вершины стека новая вершина будет располагаться на 8 байтов ближе к началу сегмента стека. Причем зарезервированные таким образом четырехбайтовые поля можно обозначить в операндах команд как [EBP-4] и [EBP-8]. На рис. 5.3.2 изображено строение кадра процедуры для данного частного примера.

Рис. 5.3.2. Строение кадра стека после выделения области

для локальных переменных

Резервирование области для локальных переменных в стеке требует определенных обратных действий перед выходом их подпрограммы (иначе эти поля, оставшись на вершине стека, не позволят автоматически сделать вершиной стека то место в нем, где хранится адрес возврата (и запомненное "старое" значение EBP). Такие обратные восстановительные действия могут быть выполнены одним из двух следующих вариантов. Следует либо выполнить команду

ADD ESP, размер_области_локальных_переменных

в том месте завершающих действий, где область локальных переменных окажется на вершине стека, либо выполнить команду

MOV ESP, EBP

после которой регистр ESP будет указывать на то место в стеке, на которое указывал до тех пор регистр EBP (после этого можно выполнять команду POP EBP и команду RET).

Если задумываться о правильном поддержании структуры стека не только в процессе выполнения процедуры, но и после возврата из нее, то возникает вопрос, какая часть программы и как должна позаботиться об извлечении из стека аргументов процедуры, положенных в нее. В примере эта задача сводится к удалению из стека полей arg1, arg2, arg3. Оставить все как есть нельзя, во-первых, потому что остающиеся аргументы рано или поздно переполнят стек, во-вторых, вызывающая программа, как правило, также является подпрограммой и она не сможет автоматически снять адрес возврата для перехода в программу, вызвавшую ее (если в стеке над ней остаются неснятыми данные).

Теоретически для последней проблемы возможны два решения: очисткой кадра процедуры от аргументов занимаются команды процедуры и очисткой кадра процедуры от аргументов занимаются команды вызвавшей программы. Для реализации первого варианта нужно было освобождать стек после снятия адреса возврата, но до возвращения в вызвавшую процедуру. Но так как снятие адреса возврата выполняет команда RET, а к концу выполнения этой команды последовательность управления выборкой следующей команды должна вернуться в вызывающую программу (что называется просто возвратом управлением ), то такая реализация может быть вложена только внутрь выполнения самой команды RET. Именно так и поступили разработчики архитектуры и дополнительно к обычной команде RET ввели в набор команд еще и команду, записываемую в мнемонике как

RET число

где операнд число задает, сколько байтов нужно удалить из стека в ходе выполнения команды RET после снятия из стека адреса возврата. С учетом того, что из стека может быть извлечено только четное число байтов (слово или двойное слово), то непосредственный операнд число должен обязательно задаваться четным числовым значением. В нашем демонстрационном примере следовало выполнить команду RET 12. (Размер области аргументов процедуры funa есть 12 байтов.)

Второе из упомянутых выше решений заключается в выполнении команды

ADD ESP, размер_области_аргументов_процедуры

после команды CALL. В частности, по этому варианту в приведенном примере следует выполнить команду ADD ESP, 12.

Для разработчика программного обеспечения встает естественный вопрос, какой вариант из рассмотренных использовать в конкретной ситуации. Для очистки стека от аргументов процедуры предпочтительным является первый вариант, так как вызовы подпрограммы используются многократно и по второму варианту за каждой командой вызова CALL должна помещаться дополнительная команда ADD ESP, размер, а команда RET число будет присутствовать в процедуре в единственном экземпляре.

В то же время существуют ситуации, когда приходится использовать второй вариант очистки стека от аргументов. Он используется, когда предполагается применение подпрограммы с переменным числом аргументов. Напомним, что в языке С++ допускаются подпрограммы, которые можно использовать, задав только часть аргументов, а именно опустив сколько-то последних из них. Более того, даже в обычном языке Си неявно допускаются такие подпрограммы с переменным числом аргументов, среди которых можно назвать обычные системные функции типа printf.

Рассмотрим, что получится, если предложенную выше подпрограмму примера funa вызвать не с тремя, а только с двумя аргументами. Обращаясь к последовательности укладки аргументов в стек и модифицированной этим допущением схеме, аналогичной рис. 5.3.1, получим, что arg1 должен теперь внутри команд процедуры обозначаться как [EBP+12], а arg2 - как [EBP+8]. Но разработчик программы, а следовательно и машинный код никак не могут знать, с каким числом аргументов в действительности вызвана подпрограмма, и поэтому выбрать правильное обозначение для доступа к аргументом нет никакой возможности.

(Теоретически есть одно возможное решение, не применяемое в настоящее время. Оно заключается в соглашении, по которому в качестве дополнительного аргумента передается число аргументов подпрограммы. Это число как обязательный аргумент, помещаемый сверху всех других аргументов подпрограммы, анализирует машинный код процедуры и, в зависимости от его значения, принимает то или иное решение. Отказ от использования этого решения, несмотря на его универсальность, обусловлен дополнительными расходами памяти на лишний аргумент, его анализ и использование конкретного варианта, а также дополнительными затратами времени при его реализации.)

Выход был найден с помощью простого и достаточно оригинального решения - помещать аргументы в стек не с начала их перечня в списке заголовка подпрограммы на языке высокого уровня, а с конца такого списка. Для нового варианта, называемого вариантом языка Си, вызов подпрограммы funa в машинных командах запишется как

push значение arg3

push значение arg2

push значение arg1

call funa

В результате такой укладки аргументов на верху области аргументов в стеке окажется как раз аргумент arg1, а последний из списка аргументов - в низу области внутри стека. Поэтому теперь аргументу arg1 будет соответствовать обозначение операнда [EBP+8], аргументу arg2 - обозначение [EBP+12], а аргументу arg1 - обозначение [EBP+16]. В результате неукладки последнего аргумента последовательность этих обозначений нисколько не изменится, за исключением того, что неуложенный аргумент ни обозначать, ни использовать будет нельзя (но это и не является неожиданностью или неудобством).

Поэтому, соглашение по укладке аргументов в стек, по которому аргументы укладываются в порядке, обратном их размещению внутри заголовка подпрограммы на языке высокого уровня, называют соглашением типа языка Си. Противоположное же соглашение по укладке аргументов, когда они укладываются в естественном порядке относительно описания, называют соглашением языка Паскаль. Соответственно и соглашение по организации на уровне команд выхода из подпрограммы с коррекцией кадра процедуры, чтобы освободить его от аргументов, называют соглашением языка Си (при использовании добавочной команды сложения для ESP после команды CALL) и соглашением языка Паскаль (при использовании команды RET число).

Последнее, менее значимое соглашение касается возвращаемой информации из подпрограммы. В тех случаях, когда заголовок подпрограммы на языке высокого уровня предполагает возврат в качестве собственного значения ее (как подпрограммы-функции) простого значения, размещаемого в одном, двух, четырех байтах или восьми байтах, то для передачи этого значения используются регистры AL, AX, EAX или пара регистров EAX, EDX, соответственно.

Соседние файлы в предмете Системное программное обеспечение