Процедуры или подпрограммы очень важны в Ассемблере, так как большинство программ на ассемблере имеют большой размер. Процедуры идентифицируются по имени, после которого, собственно, и следует тело процедуры. Конец процедуры указывается стейтментом возврата.
Процедуры в Ассемблере
Ниже приведен синтаксис определения процедуры:
имя_процедуры:
тело_процедуры
...
ret
Процедура вызывается из другой функции с помощью инструкции CALL, которая содержит имя вызываемой процедуры в качестве аргумента:
CALL имя_процедуры
Вызываемая процедура возвращает управление вызывающей процедуре с помощью инструкции RET.
Например, давайте напишем очень простую процедуру с именем sum
, которая будет складывать переменные, хранящиеся в регистрах ECX и EDX, и возвращать сумму в регистр EAX:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
section .text global _start ; объявляем для использования gcc _start: ; сообщаем линкеру входную точку mov ecx,'4' sub ecx, '0' mov edx, '5' sub edx, '0' call sum ; вызываем процедуру sum mov [res], eax mov ecx, msg mov edx, len mov ebx,1 ; файловый дескриптор (stdout) mov eax,4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov ecx, res mov edx, 1 mov ebx, 1 ; файловый дескриптор (stdout) mov eax, 4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov eax,1 ; номер системного вызова (sys_exit) int 0x80 ; вызов ядра sum: mov eax, ecx add eax, edx add eax, '0' ret section .data msg db "The sum is:", 0xA,0xD len equ $- msg segment .bss res resb 1 |
Результат выполнения программы:
The sum is:
9
Стек как структура данных
Стек — это структура данных в памяти в виде массива, в котором эти данные могут храниться и удаляться из места, называемого «вершиной» стека. Данные, которые должны быть сохранены, «помещаются» в стек, а данные, которые должны быть извлечены — «выталкиваются» из стека. Стек — это структура данных типа LIFO (англ. «Last In, First Out» = «Последним пришёл, первым ушёл»), детально об этом читайте здесь.
Язык ассемблера предоставляет две инструкции для операций со стеком: PUSH
и POP
. Эти инструкции имеют следующий синтаксис:
PUSH операнд
POP адрес/регистр
Пространство памяти, зарезервированное в сегменте стека, используется для реализации стека, а именно — регистры SS и ESP (или SP). На вершину стека, которая указывает на последний добавленный элемент, указывает регистр SS:ESP
, где регистр SS указывает на начало сегмента стека, а SP (или ESP) показывает смещение в сегменте стека.
Сохранять значения регистров в стеке перед их использованием можно следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
; Сохраняем регистры AX и BX в стеке PUSH AX PUSH BX ; Используем регистры для других целей MOV AX, VALUE1 MOV BX, VALUE2 ... MOV VALUE1, AX MOV VALUE2, BX ; Восстанавливаем исходные значения POP BX POP AX |
Например, давайте напишем программу, которая будет выводить весь набор ASCII-символов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
section .text global _start ; объявляем для использования gcc _start: ; сообщаем линкеру входную точку call display mov eax,1 ; номер системного вызова (sys_exit) int 0x80 ; вызов ядра display: mov ecx, 256 next: push ecx mov eax, 4 mov ebx, 1 mov ecx, achar mov edx, 1 int 80h pop ecx mov dx, [achar] cmp byte [achar], 0dh inc byte [achar] loop next ret section .data achar db '0' |
Результат выполнения программы:
0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}
...
...
Для чего в последнем примере мы помещаем 256 в ECX? Также не совсем понятен момент с работой display. Не должны ли мы после выполнения 10-й строки выходить из процедуры и возвращаться в блок _start, а там и вовсе выходить из программы?
Регистр ECX — это счётчик (counter). Каждый loop уменьшает его на единицу и делает переход, а как дойдет до нуля —Продолжит выполнение следующей инструкции. Соответсвенно цикл выполнится 256 раз. Столько символов в одном байте (от 0 до 255).
После 10 строки идет переход дальше, там нет никаких переходов и скачков, метки они условные для программиста и на код не влияют. Возврат произойдет по ret когда в ECX будет ноль. И тогда уже как раз выход.