Программирование игр для Windows. Советы профессионала

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


Языки Си и ассемблер похожи на дальних родственников, живущих в одном доме - они вынуждены придерживаться сложных взаимных условностей. Однако ассемблер значительно более примитивен. Поэтому при передаче параметров ассемблерной процедуре нам приходится сочинять множество дополнительных строк кода, обеспечивающих доступ к ним. Вначале необходимо оформить фрейм стека, как показано в Листинге 2.1. Далее необходимо получить доступ к переданным параметрам, основываясь на новом значении регистра базы (ВР). Для обеспечения доступа к параметрам вы должны четко представлять себе, как именно передаваемые параметры размещаются в стеке. К примеру, вы хотите написать процедуру, вычисляющую сумму двух чисел и возвращающую результат в регистре АХ. На языке Си, описание этой функции выглядит так:

int Add_Int(int number_1, int number_2);

При выполнении этой процедуры компилятор языка Си создаст фрейм стека и поместит туда параметры. Иными словами, значения number_1 и number_2 будут расположены в стеке. Вы можете подумать, что сначала в стек будет помещено значение number 1, а затем - number_2. Однако компилятор Си думает несколько иначе. Он помещает параметры в стек в обратном порядке, что облегчает доступ к ним. За счет применения обратного порядка размещения параметров, адрес каждого из них будет задаваться некоторым положительным смещением относительно регистра ВР, что делает жизнь намного легче. В частности, именно благодаря такому механизму, некоторые функции (например, printf) могут получать переменное число параметров. Таким образом, при вызове функции Add_Int фрейм стека будет выглядеть, как показано па рисунке 2.1 или 2.2, в зависимости от используемой модели памяти. Причина, по которой вид фрейма стека зависит от модели памяти, состоит в следующем: при вызове процедуры в стек помещается адрес команды, следующей непосредственно за командой вызова. Если мы применили модель памяти SMALL, все процедуры по определению находятся внутри одного кодового сегмента. Следовательно, для доступа из программы к любой из них нам необходимо знать только смещение.


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

Как видно из рисунков 2.1 и 2.2, параметры помещаются в стек в том порядке, который обеспечивает их адресацию положительными смещениями относительно значения регистра базы (ВР). Следовательно, для доступа к параметру number 1 вы должны использовать [ВР+4] или [ВР+6], в зависимости от установленной модели памяти. В качестве примера рассмотрим полный текст функции Add_Int. Она вычисляет сумму двух передаваемых в качестве аргументов чисел. Результат возвращается в регистре АХ, который, в соответствии с соглашениями языка Си, используется для возврата 16-битных значений.

Листинг 2.2. Простая процедура сложения.

; Секция констант

integer_1 EQU [ВР+6]        ; задает адрес первого аргумента

integer_2 EQU [BP+8]        ; задает адрес второго аргумента

.MODEL medium                ; указываем компилятору, что он должен

; использовать модель памяти MEDIUM



.CODE                        ; начало кодового сегмента

PUBLIC _Add_Int              ; эта функция - общедоступна

_Add_Int PROC FAR            ; имя функции и ее тип (дальняя)

push BP                      ; эти две инструкции инициализируют

; фрейм стека

mov ВР, SP

mov AX,integer_1             ; помещаем первое слагаемое

; в аккумулятор (регистр АХ)

add AX,integer_2             ; добавляем второе, слагаемое

; к содержимому АХ

pop ВР                       ; ликвидируем фрейм стека

_Add_Int  ENDP               ; конец процедуры

END                          ; конец кодового сегмента

Единственное, что мы изменили по сравнению с Листингом 2.1, это добавили несколько строк кода и ввели определения для адресов параметров. Теперь давайте проанализируем то, что у нас получилось.

§          Как и в предыдущем листинге, здесь были использованы директивы ассемблера для указания модели памяти, способа вызова, начала и конца функции;

§          EQU — это простая директива, заменяющая одну строку на другую. Я прибег к ней потому, что мне не хотелось в тексте самой функций использовать синтаксические конструкции [ВР+6] и [BP+8]. Строки, задающие выражения, которые будут подставлены при компиляции, это:

integer_l EQU [ВР+6]

integer_2 EQU [BP+8]

В общем, использование таких подстановок позволяет сделать ассемблерную программу более читабельной. Единственной альтернативой такому подходу является написание команды индексирования относительно содержимого одного из регистров (типа [ВР+6]).


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