Учебный курс. Часть 29. Макросы PROC и ENDP

Автор: xrnd | Рубрика: Учебный курс | 06-12-2010 | Распечатать запись Распечатать запись

Наверно, вы заметили, что довольно неудобно обращаться к параметрам и локальным переменным, указывая смещения относительно регистра BP. Такой способ подходит только для совсем простых и маленьких процедур. Поэтому в таких ассемблерах, как TASM и MASM, существуют специальные директивы, позволяющие создавать процедуры быстро и удобно. В FASM таких директив нет! Но они и не нужны — то же самое можно сделать с помощью макросов.

Для начала, нам потребуется заголовочный файл с макросами. Стандартный пакет FASM для Windows, к сожалению, не включает в себя макросы для 16-битных процедур. Однако такие макросы можно найти на официальном форуме FASM или скачать здесь: PROC16.INC. Это переделанная версия файла PROC32.INC с точно таким же синтаксисом.

Заголовочный файл необходимо будет включить в программу с помощью директивы include:

1
include 'proc16.inc'

Базовый синтаксис объявления процедуры

Для создания процедуры используется следующий синтаксис:

proc <имя_процедуры>[,][<список_параметров>]
    ...
    ret
endp

После proc указывается имя процедуры. Далее через запятую список параметров. Между именем процедуры и списком параметров запятую ставить не обязательно (можно просто поставить пробел). Для возврата из процедуры следует использовать команду RET без операндов. Завершается процедура макросом endp. Например, объявим процедуру с тремя параметрами:

proc myproc,a,b,c
    mov ax,[b]
    ...
    ret
endp

Внутри процедуры обращаться к параметрам можно как к простым переменным — с помощью квадратных скобок! При вызове процедуры параметры должны помещаться в стек, начиная с последнего. Если запустить программу в отладчике, то можно увидеть сгенерированный макросами код (вместо 3-х точек я поставил команду NOP):

Макросы создали нужный пролог и эпилог процедуры. Команда RET дополнительно выталкивает 6 байт из стека (это как раз 3 параметра-слова).

Указание размеров параметров

По умолчанию размер параметров считается равным ширине стека, то есть в нашем случае 16-бит. Если требуется передавать процедуре байт или двойное слово, то нужно дополнительно указать размер. Это можно сделать, поставив двоеточие после имени параметра. Возможные операторы размера перечислены в таблице:

Оператор Размер в байтах
BYTE 1
WORD 2
DWORD 4
PWORD 6
QWORD 8
TBYTE 10
DQWORD 16

Пусть первый параметр процедуры будет иметь размер байт, второй — слово, третий — двойное слово:

proc myproc,a:BYTE,b:WORD,c:DWORD
    mov cl,[a]
    mov bx,[b]
    mov ax,word[c]
    mov dx,word[c+2]
    ...
    ret
endp

В результате будет сгенерирован такой код:

Первый параметр будет занимать в стеке одну ячейку, хотя использоваться будет только младший байт. Третий параметр займёт 2 смежные ячейки стека. Для избежания путаницы, я рекомендую вам всегда использовать только параметры размером слово (или двойное слово в 32-битном ассемблере). Дело в том, что поместить отдельный байт в стек всё равно не получится 🙂 А DWORD придётся заталкивать двумя командами, поэтому лучше разделить его на два слова, явно указав младшую и старшую часть.

Указание соглашений вызова

Дополнительно для процедуры можно указать соглашения вызова. Чтобы это сделать, надо добавить специальное ключевое слово после имени процедуры. Доступно всего 2 варианта:

Ключевое слово/
соглашения вызова
Порядок помещения
параметров в стек
Очищает стек
stdcall обратный вызываемая процедура
c обратный вызывающий код

Буква c здесь обозначает соглашения вызова для языка Си. По умолчанию используется stdcall. Например, пусть наша процедура не очищает стек:

proc myproc c,a:BYTE,b:WORD,c:DWORD
    mov cl,[a]
    mov bx,[b]
    mov ax,word[c]
    mov dx,word[c+2]
    ...
    ret
endp

В результате получится следующий код (сравните с предыдущим примером и найдите одно отличие!):

Сохранение и восстановление используемых регистров

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

proc myproc c uses ax bx cx dx,a:BYTE,b:WORD,c:DWORD
    mov cl,[a]
    mov bx,[b]
    mov ax,word[c]
    mov dx,word[c+2]
    ...
    ret
endp

Если объявление процедуры получается слишком длинным, можно продолжить его на следующей строке, добавив символ \ в конец первой строки (это работает и с любыми другими макросами):

proc myproc4 c uses ax bx cx dx,\
            a:BYTE,b:WORD,c:DWORD

Результат:

Объявление локальных переменных

Для объявления локальных переменных существует 3 варианта синтаксиса:

1. local + директивы объявления данных

Макрос local предназначен для создания локальных переменных. После слова local локальные переменные объявляются обычными директивами. Можно использовать как инициализированные, так и неинициализированные переменные. Можно объявлять несколько переменных в одной строке через запятую (однако, не получится объявить массив, перечислив значения). Например:

proc myproc c uses ax bx cx dx,\
            a:BYTE,b:WORD,c:DWORD
local i db 5
local j dw ?, k rd 1
    mov cl,[a]
    mov bx,[b]
    mov ax,word[c]
    mov dx,word[c+2]
    add cl,[i]
    mov [j],bx
    mov word[k],ax
    mov word[k+2],dx
    ret
endp

После объявления обращаться к локальным переменным можно также, как к параметрам и глобальным переменным. Теперь посмотрим на код:

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

2. Альтернативный синтаксис local

Альтернативный синтаксис похож на синтаксис размеров параметров. После имени переменной ставится двоеточие и оператор размера. Вместо оператора размера может быть также имя структуры (это удобно при программировании для Windows).

local i:BYTE
local j:WORD, k:DWORD

Инициализация переменных в этом варианте синтаксиса не предусмотрена, её придётся делать вручную. Зато можно легко объявлять массивы — обозначение очень напоминает языки высокого уровня:

local buffer[256]:BYTE      ;Локальный буфер размером 256 байт

3. locals и endl

Третий вариант — объявление локальных переменных в виде блока localsendl. Используются обычные директивы объявления данных:

proc myproc c uses ax bx cx dx,\
            a:BYTE,b:WORD,c:DWORD
locals
  i db 5
  j dw ?
  k rd 1
endl
    mov cl,[a]
    mov bx,[b]
    mov ax,word[c]
    mov dx,word[c+2]
    add cl,[i]
    mov [j],bx
    mov word[k],ax
    mov word[k+2],dx
    ret
endp

Этот способ хорошо подходит, если в процедуре много локальных переменных.

Макросы для вызова процедур

В файле PROC16.INC объявлены также макросы для удобного вызова процедур. Эти макросы избавляют от необходимости писать несколько команд PUSH для помещения параметров в стек. Вместо этого достаточно написать одну строку, в которой указывается имя процедуры и список параметров через запятую. Например:

    stdcall <имя_процедуры>[,<список_параметров>]

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

Макрос Описание
stdcall Вызов процедуры с соглашениями вызова stdcall
ccall Вызов процедуры с соглашениями вызова c
invoke То же самое, что stdcall [<имя_переменной>]
cinvoke То же самое, что ccall [<имя_переменной>]

Так как наша процедура использует соглашения вызова c, то вызывать её надо макросом ccall:

    ccall myproc,5,0,ax,dx      ;3-й параметр находится в DX:AX

Макрос дополнительно генерирует код для восстановления указателя стека:

Важная особенность

Если вы объявили процедуру, но ни разу не вызывали, то её код не будет добавлен в исполняемый файл! Это позволяет создать свою библиотеку полезных процедур, сохранить их в отдельном файле и добавлять во все проекты, где они нужны. При этом в исполняемом файле окажутся только те процедуры, которые использует ваша программа.

Упражнение

В этот раз упражнение на дизассемблирование 🙂 Проанализируйте процедуру и напишите для неё ассемблерный код с использованием макросов PROC16. Что делает эта процедура? Подумайте, какие недостатки у неё есть и как их можно исправить.

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

Комментарии: