- FasmWorld - https://fasmworld.ru -

Учебный курс. Часть 25. Передача параметров через стек

В предыдущих частях учебного курса все параметры передавались процедурам через регистры. В этой статье мы рассмотрим другой способ — передачу параметров через стек. Часто этот способ оказывается удобнее. Через регистры можно передать максимум 6-7 параметров, а через стек — сколько угодно. Кроме того можно написать процедуру с переменным количеством параметров. (Подробнее о таких процедурах вы можете прочитать здесь [1].)

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

Помещение параметров в стек

Перед вызовом процедуры параметры необходимо поместить в стек с помощью команды PUSH [2]. Здесь существует два варианта: параметры могут помещаться в стек в прямом или в обратном порядке. Обычно используется обратный порядок и его я буду использовать в примерах. Параметры помещаются в стек, начиная с последнего, так что перед вызовом процедуры на вершине стека оказывается первый параметр:

; Данные
arg0     dw 0
arg1     dw 12
argN     dw 345
;---------------------------------------------------------------------
; Код
    push [argN]
    push ...
    push [arg1]
    push [arg0]
    call myproc

Перед выполнением команды CALL [3] стек будет иметь следующий вид:

Обращение к параметрам внутри процедуры

Для обращения к параметрам внутри процедуры обычно используют регистр BP. В самом начале процедуры содержимое регистра BP сохраняется в стеке и в него копируется значение регистра SP. Это позволяет «запомнить» положение вершины стека и адресовать параметры относительно регистра BP.

;Процедура
myproc:
    push bp
    mov bp,sp
    ...

При выполнении кода процедуры стек будет иметь следующую структуру:

Здесь ret_addr обозначает адрес возврата, помещаемый в стек командой вызова процедуры, а bp — сохранённое значение регистра BP. В нашем случае стек имеет ширину 16 бит, поэтому первый параметр будет доступен как word[bp+4], второй как word[bp+6] и так далее.

    mov ax,[bp+4]       ;AX = arg0
    mov bx,[bp+6]       ;BX = arg1
    add ax,[bp+8]       ;AX = AX + arg2

Полезно представлять себе стек, чтобы правильно указывать смещения относительно регистра BP. Не забудьте перед возвратом из процедуры восстановить значение BP из стека.

Извлечение параметров из стека

После того, как процедура выполнилась, необходимо очистить стек, вытолкнув из него параметры. Тут тоже существует 2 способа: стек может быть очищен самой процедурой или кодом, который эту процедуру вызывал. Для первого способа используется команда RET [4] с одним операндом, который должен быть равен количеству байтов, выталкиваемых из стека. В нашем случае он должен быть равен количеству параметров, умноженному на 2.

    push [arg1]
    push [arg0]
    call myproc
    ...
;----------------------------------------------------------------------
;Процедура c двумя параметрами
myproc:
    push bp
    mov bp,sp
    ...
    pop bp
    ret 4        ;Из стека дополнительно извлекается 4 байта

Для второго способа нужно использовать команду RET [4] без операндов. Стек восстанавливается после выполнения процедуры путём прибавления значения к SP. С помощью такого способа программируются процедуры с переменным количеством параметров. Процедура не знает, сколько ей будет передано параметров, поэтому очистка стека должна выполняться вызывающим кодом.

    push [arg1]
    push [arg0]
    call myproc2
    add sp,4                ;Восстановление указателя стека
    ...
;----------------------------------------------------------------------
;Процедура с двумя параметрами (не очищает стек)
myproc2:
    push bp
    mov bp,sp
    ...
    pop bp
    ret

Соглашения вызова

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

Пример

В качестве примера рассмотрим процедуру с тремя параметрами: a, b и c. Процедура вычисляет значение выражения (a+b)/c. Параметры передаются через стек в обратном порядке, результат возвращается в регистре AX, стек восстанавливается вызываемой процедурой. Все числа — 16-битные целые со знаком.

use16                       ;Генерировать 16-битный код
org 100h                    ;Программа начинается с адреса 100h
    jmp start               ;Переход на метку start
;---------------------------------------------------------------------
; Данные
a     dw 81
b     dw 273
x     dw ?
;---------------------------------------------------------------------
start:
    push 3                  ;c=3
    push [b]                ;b
    push [a]                ;a
    call primer             ;Вызов процедуры
    mov [x],ax              ;x=(a+b)/c

    mov ax,4C00h            ;\
    int 21h                 ;/ Завершение программы

;---------------------------------------------------------------------
;Процедура c тремя параметрами: a, b, c.
;Вычисляет значение выражения (a+b)/c. Результат возвращается в AX.
primer:
    push bp                 ;Сохранение регистра BP
    mov bp,sp               ;BP=SP
    push dx

    mov ax,[bp+4]           ;AX=a
    add ax,[bp+6]           ;AX=(a+b)
    cwd                     ;DX:AX=(a+b)
    idiv word[bp+8]         ;AX=(a+b)/c

    pop dx
    pop bp                  ;Восстановление регистра BP
    ret 6                   ;Возврат с извлечением параметров из стека

Как видите, всё довольно просто. Главное — не запутаться со смещениями относительно BP.

Упражнение

Напишите любую процедуру с 4-5 параметрами, передаваемыми через стек. Вызовите процедуру в своей программе. Запустите программу в Turbo Debugger и посмотрите, как происходит работа со стеком. Результаты можете писать в комментариях.

Следующая часть » [5]