Turbo Assembler 3.0. Руководство пользователя

         

Передача параметров


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?

На Рис. 18.2 показано, как выглядит стек перед выполнением первой инструкции в функции Test:

i = 25; j = 4; Test(1, j, 1);


. . . . . . ¦ ¦ +-----------------------+ ¦ ¦ +-----------------------+ SP -- ¦ Адрес возврата ¦ +-----------------------+ SP + 2 ¦ 25 (i) ¦ +-----------------------+ SP + 4 ¦ 4 (j) ¦ +-----------------------+ SP + 6 ¦ 1 ¦ +-----------------------+ ¦ ¦ +-----------------------+ ¦ ¦ . . . . . .



Рис. 18. 2 Состояние стека перед выполнением первой инструк- ции функции Test

Параметры функции Test представляют собой фиксированные ад- реса относительно SP, начиная с ячейки, на два байта старше адре- са, по которому хранится адрес возврата, занесенный туда при вы- зове. После загрузки регистра BP значением SP вы можете обращать- ся к параметрам относительно BP. Однако, вы должны сначала сохра- нить BP, так как в вызывающей программе предполагается, что при возврате BP изменен не будет. Занесение в стек BP изменяет все смещения в стеке. На Рис. 18.3 показано состояние стека после вы- полнения следующих строк кода:

. . . push bp mov bp,sp . . .

. . . . . . ¦ ¦ +-----------------------+ SP -- ¦ BP вызывающей прогр. ¦ -- BP +-----------------------+ SP + 2 ¦ Адрес возврата ¦ BP + 2 +-----------------------+ SP + 4 ¦ 25 (i) ¦ BP + 4 +-----------------------+ SP + 6 ¦ 4 (j) ¦ BP + 6 +-----------------------+ SP + 8 ¦ 1 ¦ BP + 8 +-----------------------+ ¦ ¦ +-----------------------+ ¦ ¦ . . . . . .

Рис. 18.3 Состояние стека после инструкций PUSH и MOVE

Организация передачи параметров функции через стек и исполь- зование его для динамических локальных переменных - это стандарт- ный прием в языке С++. Как можно заметить, неважно, сколько пара- метров имеет программа на языке С++: самый левый параметр всегда хранится в стеке по адресу, непосредственно следующим за сохра- ненным в стеке адресом возврата, следующий возвращаемый параметр хранится непосредственно после самого левого параметра и т.д. Поскольку порядок и тип передаваемых параметров известны, их всегда можно найти в стеке.

Пространство для динамических локальных переменных можно за- резервировать, вычитая из SP требуемое число байт. Например, пространство для динамического локального массива размером в 100 байт можно зарезервировать, если начать функцию Test с инструк- ций:



. . . push bp mov bp,sp sub sp,100 . . .

как показано на Рис. 18.4

. . . . . . ¦ ¦ +-----------------------+ SP -- ¦ ¦ - BP - 100 +-----------------------+ ¦ ¦ +-----------------------+ . . . . . . . . ¦ ¦ +-----------------------+ SP + 100 -- ¦ BP вызывающей прогр. ¦ -- BP +-----------------------+ SP + 102 ¦ Адрес возврата ¦ BP + 2 +-----------------------+ SP + 104 ¦ 25 (i) ¦ BP + 4 +-----------------------+ SP + 106 ¦ 4 (j) ¦ BP + 6 +-----------------------+ SP + 108 ¦ 1 ¦ BP + 8 +-----------------------+ ¦ ¦ +-----------------------+ ¦ ¦ . . . . . .

Рис. 18. 4 Состояние стека после инструкций PUSH, MOVE и SUB

Поскольку та часть стека, где хранятся динамические локаль- ные переменные, представляет собой более младшие адреса, чем 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 автоматически учитывает раз- личные размеры возвратов ближнего и дальнего типа.


Содержание раздела