Передача параметров
Borland C++ передает функциям параметры через стек. Перед вызовом функции С++ сначала заносит передаваемые этой функции параметры, начиная с самого правого параметра и кончая левым, в стек. В С++ вызов функции:
. . . Test(i, j, 1); . . .
компилируется в инструкции:
mov ax,1 push ax push word ptr DGROUP:_j push word ptr DGROUP:_i call near ptr _Test add sp,6
где видно, что правый параметр (значение 1), заносится в стек первым, затем туда заносится параметр j и, наконец, i.
При возврате из функции занесенные в стек параметры все еще находятся там, но они больше не используются. Поэтому непосредственно после каждого вызова функции Borland C++ настраивает указатель стека обратно в соответствии со значением, которое он имел перед занесением в стек параметров (параметры, таким образом, отбрасываются). В предыдущем примере три параметра (по два байта каждый) занимают в стеке вместе 6 байт, поэтому Borland C++ добавляет значение 6 к указателю стека, чтобы отбросить параметры после обращения к функции Test. Важный момент здесь заключается в том, что в соответствии с используемыми по умолчанию соглашениями Си/C++ за удаление параметров из стека отвечает вызывающая программа.
Функции Ассемблера могут обращаться к параметрам, передаваемым в стеке, относительно регистра BP. Например, предположим, что функция Test в предыдущем примере представляет собой следующую функцию на Ассемблере (PRMSTACK.ASM):
.MODEL SMALL .CODE PUBLIC _Test _Test PROC push bp mov bp,sp mov ax,[bp+4] ; получить параметр 1 add ax,[bp+6] ; прибавить параметр 2 ; к параметру 1 sub ax,[bp+8] ; вычесть из суммы 3 pop bp ret _Test ENDP
Как можно видеть, функция Test получает передаваемые из программы на языке Си параметры через стек, относительно регистра BP. (Если вы помните, BP адресуется к сегменту стека.) Но откуда она знает, где найти параметры относительно BP?
На Рисунке показано, как выглядит стек перед выполнением первой инструкции в функции Test:
Параметры функции Test представляют собой фиксированные адреса относительно SP, начиная с ячейки, на два байта старше адреса, по которому хранится адрес возврата, занесенный туда при вызове. После загрузки регистра BP значением SP вы можете обращаться к параметрам относительно BP. Однако, вы должны сначала сохранить BP, так как в вызывающей программе предполагается, что при возврате BP изменен не будет. Занесение в стек BP изменяет все смещения в стеке. На Рис. 18.3 показано состояние стека после выполнения следующих строк кода:
Организация передачи параметров функции через стек и использование его для динамических локальных переменных - это стандартный прием в языке С++. Как можно заметить, неважно, сколько параметров имеет программа на языке С++: самый левый параметр всегда хранится в стеке по адресу, непосредственно следующим за сохраненным в стеке адресом возврата, следующий возвращаемый параметр хранится непосредственно после самого левого параметра и т.д. Поскольку порядок и тип передаваемых параметров известны, их всегда можно найти в стеке.
Пространство для динамических локальных переменных можно зарезервировать, вычитая из SP требуемое число байт. Например, пространство для динамического локального массива размером в 100 байт можно зарезервировать, если начать функцию Test с инструкций:
. . . push bp mov bp,sp sub sp,100 . . .
как показано на рисунке
Поскольку та часть стека, где хранятся динамические локальные переменные, представляет собой более младшие адреса, чем BP, для обращения к динамическим локальным переменным используется отрицательное смещение. Например, инструкция:
mov byte ptr [bp-100]
даст значение первого байта ранее зарезервированного 100-байтового массива. При передаче параметров всегда используется положительная адресация относительно регистра BP.
Хотя можно выделять пространство для динамических локальных переменных описанным выше способом, в Турбо Ассемблере предусмотрена специальная версия директивы LOCAL, которая существенно упрощает выделение памяти и присваивание имен для динамических локальных переменных. Когда в процедуре встречается директива LOCAL, то подразумевается, что она определяет для данной процедуры динамические локальные переменные. Например, директива:
LOCAL LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE
определяет динамические переменные LocalArray и LocalCount. LocalArray на самом деле представляет собой метку, приравненную к [BP-100], а LocalCount - это метка, приравненная к [BP-102]. Однако вы можете использовать их, как имена переменных. При этом вам даже не нужно будет знать их значения. AUTO_SIZE - это общее число байт (объем памяти), необходимых для хранения динамических локальных переменных. Чтобы выделить пространство для динамических локальных переменных, это значение нужно вычесть из SP.
Приведем пример того, как нужно использовать директиву LOCAL:
. . . _TestSub PROC LOCAL LocalArray:BYTE:100,LocalCount:WORD=AUTO_SIZE push bp ; сохранить указатель стека вызывающей программы mov bp,sp ; установить собственный; указатель стека sub sp,AUTO_SIZE ; выделить пространство для динамических локальных переменных mov [LocalCount],10 ; установить переменную LocalCount в значение 10 (LocalCount это [BP-102]) . . . mov cx,[LocalCount] ; получить значение (счетчик) из локальной переменной mov al,"A" ; заполним символом "A" lea bx,[LocalArray] ; ссылка на локальный массив LocalArray (LocalArray это [BP-100]) FillLoop: mov [bx],al ; заполнить следующий байт inc bx ; ссылка на следующий байт loop FillLoop ; обработать следующий байт, если он имеется mov sp,bp ; освободить память, выделенную для динамических локальных переменных ; (можно также использовать add sp,AUTO_SIZE) pop bp ; восстановить указатель стека вызывающей программы
ret _TestSub ENDP . . .
В данном примере следует обратить внимание не то, что первое поле после определения данной динамической локальной переменной представляет собой тип данных для этой переменной: BYTE, WORD, DWORD, NEAR и т.д. Второе поле после определения данной динамической локальной переменной - это число элементов указанного типа, резервируемых для данной переменной. Это поле является необязательным и определяет используемый динамический локальный массив (если он используется). Если данное поле пропущено, то резервируется один элемент указанного типа. В итоге LocalArray состоит из 100 элементов размером в 1 байт, а LocalCount - из одного элемента размером в слово (см. пример).
Отметим также, что строка с директивой LOCAL в данном примере завершается полем =AUTO_SIZE. Это поле, начинающееся со знака равенства, необязательно. Если оно присутствует, то метка, следующая за знаком равенства, устанавливается в значение числа байт требуемой динамической локальной памяти. Вы должны затем использовать данную метку для выделения и освобождения памяти для динамических локальных переменных, так как директива LABEL только генерирует метки и не генерирует никакого кода или памяти для данных. Иначе говоря, директива LOCAL не выделяет память для динамических локальных переменных, а просто генерирует метки, которые вы можете использовать как для выделения памяти, так и для доступа к динамическим локальным переменным.
Очень удобное свойство директивы LOCAL заключается в том, что область действия меток динамических локальных переменных и общего размера динамических локальных переменных ограничена той процедурой, в которой они используются, поэтому вы можете свободно использовать имя динамической локальной переменной в другой процедуре.
Как можно заметить, с помощью директивы LOCAL определять и использовать автоматические переменные намного легче. Отметим, что при использовании в макрокомандах директива LOCAL имеет совершенно другое значение.
Кстати, Borland C++ работает с границами стека так же, как мы здесь описали. Вы можете скомпилировать несколько модулей Borland C++ с параметром -S и посмотреть, какой код Ассемблера генерирует Borland C++ и как там создаются и используются границы стека.
Все это прекрасно, но здесь есть некоторые трудности. Во-первых, такой способ доступа к параметрам, при котором используется постоянное смещение относительно BP достаточно неприятен: при этом не только легко ошибиться, но если вы добавите другой параметр, все другие смещения указателя стека в функции должны измениться. Предположим, например, что функция Test воспринимает три параметра:
Test(Flag, i, j, 1);
Тогда i находится по смещению 6, а не по смещению 4, j - по смещению 8, а не 6 и т.д. Для смещений параметров можно использовать директиву EQU:
. . . Flag EQU 4 AddParm1 EQU 6 AddParm2 EQU 8 SubParm1 EQU 10
mov ax[bp+AddParm1] add ax,[bp+AddParm1] sub ax,[bp+SubParm1] . . .
но вычислять смещения и работать с ними довольно сложно. Однако здесь могут возникнуть и более серьезные проблемы: в моделях памяти с дальним кодом размер занесенного в стек адреса возврата увеличивается на два байта, как и размеры передаваемых указателей на код и данные в моделях памяти с дальним кодом и дальними данными, соответственно. Разработка функции, которая с равным успехом будет ассемблироваться и правильно работать с указателем стека при использовании любой модели памяти было бы весьма непростой задачей.
Однако в Турбо Ассемблере предусмотрена директива ARG, с помощью которой можно легко выполнять передачу параметров в программах на Ассемблере.
Директива ARG автоматически генерирует правильные смещения в стеке для заданных вами переменных. Например:
ARG FillArray:WORD, Count:WORD, FillValue:BYTE
Здесь задается три параметра: FillArray, параметр размером в слово, Count, также параметр размером в слово и FillValue - параметр размером в байт. Директива ARG устанавливает метку FillArray в значение [BP+4] (подразумевается, что код находится в процедуре ближнего типа), метку Count - в значение [BP+6], а метку FillValue - в значение [BP+8]. Однако особенно ценна директива ARG тем, что вы можете использовать определенные с ее помощью метки не заботясь о тех значениях, в которые они установлены.
Например, предположим, что у вас есть функция FillSub которая вызывается из С++ следующим образом:
extern "C" { void FillSub( char *FillArray, int Count, char FillValue); }
main() { #define ARRAY_LENGTH 100 char TestArray[ARRAY_LENGTH]; FillSub(TestArray,ARRAY_LENGTH,"*"); }
В FillSub директиву ARG для работы с параметрами можно использовать следующим образом:
_FillSub PROC NEAR ARG FillArray:WORD, Count:WORD, FillValue:BYTE push bp ; сохранить указатель стека вызывающей программы mov bp,sp ; установить свой собственный указатель стека mov bx,[FillArray] ; получить указатель на заполняемый массив mov cx,[Count] ; получить заполняемую длину mov al,[FillValue] ; получить значение-заполнитель FillLoop: mov [bx],al ; заполнить символ inc bx ; ссылка на следующий символ loop FillLoop ; обработать следующий символ pop bp ; восстановить указатель стека вызывающей программы ret _FillSub ENDP
Не правда ли, удобно работать с параметрами с помощью директивы ARG? Кроме того, директива ARG автоматически учитывает различные размеры возвратов ближнего и дальнего типа.
1 2 3 4 5 6 7 8 9
8 8 8
| |