- FasmWorld - https://fasmworld.ru -

Учебный курс. Часть 21. Простые процедуры

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

Команды 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-битной переменной без знака. Напишите процедуру для вычисления среднего арифметического массива чисел. В качестве параметров ей будет передаваться адрес массива и количество элементов, а возвращать она будет вычисленное значение. С помощью процедуры вычислите среднее арифметическое каждого массива и сохраните где-нибудь в памяти. Выводить числа на экран не нужно, этим мы займемся в следующей части 🙂 Результаты можете писать в комментариях.

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