В этой части учебного курса мы рассмотрим основы создания процедур. Процедура представляет собой код, который может выполняться многократно и к которому можно обращаться из разных частей программы. Обычно процедуры предназначены для выполнения каких-то отдельных, законченных действий программы и поэтому их иногда называют подпрограммами. В других языках программирования процедуры могут называться функциями или методами, но по сути это всё одно и то же 🙂
Команды CALL и RET
Для работы с процедурами предназначены команды CALL [1] и RET [2]. С помощью команды CALL [1] выполняется вызов процедуры. Эта команда работает почти также, как команда безусловного перехода (JMP [3]), но с одним отличием — одновременно в стек сохраняется текущее значение регистра IP. Это позволяет потом вернуться к тому месту в коде, откуда была вызвана процедура. В качестве операнда указывается адрес перехода, который может быть непосредственным значением (меткой), 16-разрядным регистром (кроме сегментных) или ячейкой памяти, содержащей адрес.
Возврат из процедуры выполняется командой RET [2]. Эта команда восстанавливает значение из вершины стека в регистр IP. Таким образом, выполнение программы продолжается с команды, следующей сразу после команды CALL [1]. Обычно код процедуры заканчивается этой командой. Команды CALL [1] и RET [2] не изменяют значения флагов (кроме некоторых особых случаев в защищенном режиме). Небольшой пример разных способов вызова процедуры:
use16 ;Генерировать 16-битный код
org 100h ;Программа начинается с адреса 100h
mov ax,myproc
mov bx,myproc_addr
xor si,si
call myproc ;Вызов процедуры (адрес перехода - myproc)
call ax ;Вызов процедуры по адресу в AX
call [myproc_addr] ;Вызов процедуры по адресу в переменной
call word [bx+si] ;Более сложный способ задания адреса ;)
mov ax,4C00h ;\
int 21h ;/ Завершение программы
;----------------------------------------------------------------------
;Процедура, которая ничего не делает
myproc:
nop ;Код процедуры
ret ;Возврат из процедуры
;----------------------------------------------------------------------
myproc_addr dw myproc ;Переменная с адресом процедуры
Ближние и дальние вызовы процедур
Существует 2 типа вызовов процедур. Ближним называется вызов процедуры, которая находится в текущем сегменте кода. Дальний вызов — это вызов процедуры в другом сегменте. Соответственно существуют 2 вида команды RET [2] — для ближнего и дальнего возврата. Компилятор FASM автоматически определяет нужный тип машинной команды, поэтому в большинстве случаев не нужно об этом беспокоиться.
В учебном курсе мы будем использовать только ближние вызовы процедур.
Передача параметров
Очень часто возникает необходимость передать процедуре какие-либо параметры. Например, если вы пишете процедуру для вычисления суммы элементов массива, удобно в качестве параметров передавать ей адрес массива и его размер. В таком случае одну и ту же процедуру можно будет использовать для разных массивов в вашей программе. Самый простой способ передать параметры — это поместить их в регистры перед вызовом процедуры.
Возвращаемое значение
Кроме передачи параметров часто нужно получить какое-то значение из процедуры. Например, если процедура что-то вычисляет, хотелось бы получить результат вычисления 🙂 А если процедура что-то делает, то полезно узнать, завершилось действие успешно или возникла ошибка. Существуют разные способы возврата значения из процедуры, но самый часто используемый — это поместить значение в один из регистров. Обычно для этой цели используют регистры AL и AX. Хотя вы можете делать так, как вам больше нравится.
Сохранение регистров
Хорошим приёмом является сохранение регистров, которые процедура изменяет в ходе своего выполнения. Это позволяет вызывать процедуру из любой части кода и не беспокоиться, что значения в регистрах будут испорчены. Обычно регистры сохраняются в стеке с помощью команды PUSH [4], а перед возвратом из процедуры восстанавливаются командой POP [5]. Естественно, восстанавливать их надо в обратном порядке. Примерно вот так:
myproc:
push bx ;Сохранение регистров
push cx
push si
... ;Код процедуры
pop si ;Восстановление регистров
pop cx
pop bx
ret ;Возврат из процедуры
Пример
Для примера напишем процедуру для вывода собщения в рамке и протестируем её работу, выведя несколько сообщений. В качестве параметра ей будет передаватся адрес строки в регистре BX. Строка должна заканчиваться символом ‘$’. Для упрощения процедуры можно разбить задачу на подзадачи и написать соответствующие процедуры. Прежде всего нужно вычислить длину строки, чтобы знать ширину рамки. Процедура get_length вычисляет длину строки (адрес передаётся также в BX) и возвращает её в регистре AX.
Для рисования горизонтальной линии из символов предназначена процедура draw_line. В DL передаётся код символа, а в CX — количество символов, которое необходимо вывести на экран. Эта процедура не возвращает никакого значения. Для вывода 2-х символов конца строки написана процедура print_endline. Она вызывается без параметров и тоже не возвращает никакого значения. Коды символов для рисования рамок можно узнать с помощью таблицы символов кодировки 866 или можно воспользоваться стандартной программой Windows «Таблица символов», выбрав шрифт Terminal.
use16 ;Генерировать 16-битный код
org 100h ;Программа начинается с адреса 100h
jmp start ;Переход на метку start
;----------------------------------------------------------------------
msg1 db 'Hello!$'
msg2 db 'asmworld.ru$'
msg3 db 'Press any key...$'
;----------------------------------------------------------------------
start:
mov bx,msg1
call print_message ;Вывод первого сообщения
mov bx,msg2
call print_message ;Вывод второго сообщения
mov bx,msg3
call print_message ;Вывод третьего сообщения
mov ah,8 ;Ввод символа без эха
int 21h
mov ax,4C00h ;\
int 21h ;/ Завершение программы
;----------------------------------------------------------------------
;Процедура вывода сообщения в рамке
;В BX передаётся адрес строки
print_message:
push ax ;Сохранение регистров
push cx
push dx
call get_length ;Вызов процедуры вычисления длины строки
mov cx,ax ;Копируем длину строки в CX
mov ah,2 ;Функция DOS 02h - вывод символа
mov dl,0xDA ;Левый верхний угол
int 21h
mov dl,0xC4 ;Горизонтальная линия
call draw_line ;Вызов процедуры рисования линии
mov dl,0xBF ;Правый верхний угол
int 21h
call print_endline ;Вызов процедуры вывода конца строки
mov dl,0xB3 ;Вертикальная линия
int 21h
mov ah,9 ;Функция DOS 09h - вывод строки
mov dx,bx ;Адрес строки в DX
int 21h
mov ah,2 ;Функция DOS 02h - вывод символа
mov dl,0xB3 ;Вертикальная линия
int 21h
call print_endline ;Вызов процедуры вывода конца строки
mov dl,0xC0 ;Левый нижний угол
int 21h
mov dl,0xC4 ;Горизонтальная линия
call draw_line
mov dl,0xD9 ;Правый нижний угол
int 21h
call print_endline ;Вызов процедуры вывода конца строки
pop dx ;Восстановление регистров
pop cx
pop ax
ret ;Возврат из процедуры
;----------------------------------------------------------------------
;Процедура вычисления длины строки (конец строки - символ '$').
;В BX передаётся адрес строки.
;Возвращает длину строки в регистре AX.
get_length:
push bx ;Сохранение регистра BX
xor ax,ax ;Обнуление AX
str_loop:
cmp byte[bx],'$' ;Проверка конца строки
je str_end ;Если конец строки, то выход из процедуры
inc ax ;Инкремент длины строки
inc bx ;Инкремент адреса
jmp str_loop ;Переход к началу цикла
str_end:
pop bx ;Восстановление регистра BX
ret ;Возврат из процедуры
;----------------------------------------------------------------------
;Процедура рисования линии из символов.
;В DL - символ, в CX - длина линии (кол-во символов)
draw_line:
push ax ;Сохранение регистров
push cx
mov ah,2 ;Функция DOS 02h - вывод символа
drl_loop:
int 21h ;Обращение к функции DOS
loop drl_loop ;Команда цикла
pop cx ;Восстановление регистров
pop ax
ret ;Возврат из процедуры
;----------------------------------------------------------------------
;Процедура вывода конца строки (CR+LF)
print_endline:
push ax ;Сохранение регистров
push dx
mov ah,2 ;Функция DOS 02h - вывод символа
mov dl,13 ;Символ CR
int 21h
mov dl,10 ;Символ LF
int 21h
pop dx ;Восстановление регистров
pop ax
ret ;Возврат из процедуры
Результат работы программы выглядит вот так:
Отладчик Turbo Debugger
Небольшое замечание по поводу использования отладчика. В Turbo Debugger нажимайте F7 («Trace into»), чтобы перейти к коду вызываемой процедуры. При нажатии F8 («Step over») процедура будет выполнена сразу целиком.
Упражнение
Объявите в программе 2-3 массива слов без знака. Количество элементов каждого массива должно быть разным и храниться в отдельной 16-битной переменной без знака. Напишите процедуру для вычисления среднего арифметического массива чисел. В качестве параметров ей будет передаваться адрес массива и количество элементов, а возвращать она будет вычисленное значение. С помощью процедуры вычислите среднее арифметическое каждого массива и сохраните где-нибудь в памяти. Выводить числа на экран не нужно, этим мы займемся в следующей части 🙂 Результаты можете писать в комментариях.