Программирование на языке Ассемблер. Учебное пособие
.pdf8. ЦИКЛ
Выполнение цикла в Ассемблере можно организовать с помощью нескольких команд. Одна из таких команд – это команда LOOP. Команда цикла в Ассемблере всегда уменьшает значение счётчика на единицу. Это значение находится в регистре СХ (или ECX). Отличия между командами цикла заключаются только в условиях, при которых выполняется переход к метке или цикл завершается.
Команда LOOP выполняет переход к метке во всех случаях, когда значение в регистре СХ не равно нулю. Чтобы организовать цикл с помощью этой команды, сначала необходимо в регистр СХ записать число итераций цикла (т. е. сколько раз цикл должен быть выполнен), затем вставить в код метку, а затем написать команды, которые должны быть выполнены в цикле. А уже в конце списка этих команд записать команду
LOOP.
8.1. Инструкция LOOP
Инструкция LOOP в Ассемблере уменьшает значение в регистре СХ в реальном режиме или ECX в защищённом. Если после этого значение в СХ не равно нулю, то команда LOOP выполняет переход на МЕТКУ. Синтаксис команды:
LOOP МЕТКА
Алгоритм работы команды LOOP:
CX = CX – 1;
если CX не равен 0, то выполнить переход;
иначе не выполнять переход, продолжить цикл.
То есть команда LOOP выполняется в два этапа. Сначала из регистра СХ вычитается единица и его значение сравнивается с нулём. Если регистр не равен нулю, то выполняется переход к указанной МЕТКЕ. Иначе переход не выполняется,
41
и управление передаётся команде, которая следует сразу после команды LOOP.
Более понятно это будет в примере программы, представленном далее.
Пример программы:
.model tiny |
|
|
.code |
|
|
ORG 100h |
|
|
start: |
|
|
MOV CX, 26 |
; цикл будет выполнен 26 раз |
|
MOV DL, 'A' |
; |
CL=41h (ASCII-код) – первая буква |
MOV BX, 676h |
; |
позиция первой буквы на экране |
MOV AX, 0B800h |
; установить AX=B800h (память VGA) |
|
MOV DS, AX |
; копировать значение из AX в DS |
|
MOV DH, 01001110b ; CH=атрибуты цвета |
||
abcde: |
|
|
MOV [BX], DX |
; записать символ в видеопамять |
|
INC DL ; увеличить ASCII-код (для следующего сим- |
||
вола) |
|
|
ADD BX, 2 |
; сместить координаты |
|
LOOP abcde |
; повторить цикл |
|
END start |
|
|
42
9. ПРОЦЕДУРЫ
Процедура – это часть кода, которая может быть вызвана из программы для выполнения какой-либо определенной задачи. Процедуры делают программу более структурной и доступной для понимания. В общем случае процедура возвращает программу к той же самой точке, откуда она была вызвана.
Синтаксис для объявления процедуры:
имя PROC
;здесь находится
;код процедуры ...
RET
имя ENDP
имя – это имя процедуры. Одно и то же имя должно быть в верхней и нижней части, совпадение имен используется для проверки правильности закрытия процедур.
Обычно команда RET используется для возвращения в операционную систему. Эта же команда используется для возвращения из процедуры (фактически операционная система воспринимает программу как специальную процедуру).
Команды PROC и ENDP – это директивы компилятора, поэтому они не ассемблируются в какой-либо реальный машинный код. Компилятор только запоминает адрес процедуры.
Команда CALL используется для вызова процедуры. Пример:
ORG 100h CALL m1 MOV AX, 2
RET ; вернуться в операционную систему m1 PROC
MOV BX, 5
RET ; вернуться в программу, из которой была вызвана m1 ENDP
END
43
Вышеописанный пример вызывает процедуру m1, которая выполняет команду MOV BX, 5. После окончания процедуры программа выполняет команду, следующую после команды CALL, т. е. команду: MOV AX, 2.
Есть несколько способов передачи параметров процедуре. Самый простой из них – использование регистров. Здесь представлен пример процедуры, которая принимает два параметра в регистрах AL и BL, умножает их и возвращает результат в регистр AX:
ORG 100h
MOV AL, 1
MOV BL, 2
CALL m2
CALL m2 |
|
CALL m2 |
|
CALL m2 |
|
RET |
; вернуться в операционную систему |
m2 PROC |
|
MUL BL |
; AX=AL*BL |
RET |
; вернуться в вызвавшую программу |
m2 ENDP |
|
END |
|
В этом примере значение регистра AL изменяется каждый раз при вызове процедуры, а состояние регистра BL не изменяется. Таким образом, был получен алгоритм вычисления числа 2 в 4 степени. В результате в регистре AX будет число 16 (или 10h).
Далее представлен пример, в котором используется процедура для вывода на экран сообщения «Hello World!»:
ORG 100h
LEA SI, msg ; загрузить адрес msg в регистр SI CALL print_me
RET ; вернуться в операционную систему
44
;эта процедура печатает строку, строка должна
;оканчиваться нулем (иметь ноль в конце),
;адрес строки должен быть в регистре SI: print_me PROC
next_char:
CMP b.[SI], 0 ; проверить регистр SI, если он
JE stop |
|
; |
равен 0, перейти к метке stop |
MOV AL, |
[SI] |
; |
иначе получить ASCII-символ |
MOV AH, |
0Eh |
; номер функции для печати символа |
|
INT 10h |
; |
используем прерывание для печати |
|
|
; |
символа из AL |
|
ADD SI, |
1 ; увеличить индекс строкового массива |
||
JMP next_char ; |
вернуться и напечатать |
||
stop: |
|
; |
другой символ |
|
|
|
|
RET |
|
; |
вернуться в программу |
print_me |
ENDP |
|
|
;msg DB 'Hello World!', 0
;строка с нулевым окончанием
END
Префикс «b.» перед [SI] означает, что необходимо сравнивать байты, а не слова. Если необходимо сравнить слова, то нужно добавить префикс «w.» вместо «b.». Если один из сравниваемых операторов – регистр, то вставлять префиксы не требуется, так как компилятор знает размер каждого регистра.
45
10. СТЕК
Стек – это область памяти для хранения временных данных. Стек используется командой CALL для хранения адреса, чтобы программа могла вернуться к тому месту, откуда была вызвана процедура. Команда RET получает этот адрес из стека и возвращает управление по этому смещению. То же самое происходит, когда команда INT вызывает прерывание, она записывает в стек регистр флагов, сегмент и смещение кода. Команда IRET используется для возвращения после вызова прерывания.
Стек можно использовать для хранения любых данных. Для работы со стеком имеются две команды:
PUSH – записывает 16-битовое значение в стек;
POP – получает 16-битовое значение из стека.
Синтаксис для команды PUSH:
PUSH REG
PUSH SREG
PUSH memory
PUSH immediate
Здесь:
REG: AX, BX, CX, DX, DI, SI, BP, SP; SREG: DS, ES, SS, CS;
memory: [BX], [BX+SI+7], 16-битовая переменная и т. п.; immediate: 5, -24, 3Fh, 10001101b, и т. п.
Синтаксис для команды POP:
POP REG
POP SREG
POP memory
Здесь:
REG: AX, BX, CX, DX, DI, SI, BP, SP; SREG: DS, ES, SS, (кроме CS).
memory: [BX], [BX+SI+7], 16-битовая переменная и т. п.
46
Команды PUSH и POP работают только с 16-битовыми значениями.
PUSH immediate работает только на процессорах 80186 и выше.
Стек использует алгоритм LIFO (Last In First Out – последним пришел – первым ушел), это значит, что если поместить значения одно за другим в стек: 1, 2, 3, 4, 5, то первое значение, которое можно будет получить из стека, будет 5, затем 4, 3, 2 и только потом 1 (рис. 8).
Рис. 8. Запись и получение значений из стека
Очень важно применять равное количество команд PUSH и POP, иначе стек может быть нарушен и невозможно будет вернуться в операционную систему. Для возвращения в операционную систему используется команда RET. Когда программа запускается, ее адрес записывается в стек (обычно это
0000h).
Команды PUSH и POP чрезвычайно полезны, т. к. для хранения данных обычно недостаточно только регистров. Вот выход из ситуации:
47
записать значение регистра в стек (используя PUSH);
использовать этот регистр в своих целях;
восстановить предыдущее значение регистра из стека (используя POP).
Пример:
ORG 100h
MOV AX, 1234h
PUSH AX ; записать значение из AX в стек
MOV AX, 5678h ; изменить значение регистра AX POP AX ; восстановить первоначальное значение AX RET
END
Стек можно также использовать для того, чтобы поменять местами значения в регистрах:
ORG 100h
MOV AX, 1212h ; записать в АХ число 1212h. MOV BX, 3434h ; записать в ВХ число 3434h. PUSH AX ; записать значение AX в стек.
PUSH BX ; записать значение ВX в стек. POP AX ; установить в AX значение BX. POP BX ; установить в ВX значение АX. RET
END
Обмен данными происходит потому, что стек использует алгоритм LIFO (последним пришел – первым вышел), поэтому когда в стек помещаем число 1212h, а затем – 3434h, то при обращении к стеку сначала будет получено число 3434h, и только потом – 1212h.
Область памяти стека устанавливается при помощи ре-
гистров SS (Stack Segment – сегмент стека) и SP (Stack Pointer
– указатель стека). Обычно операционная система устанавливает значения этих регистров на начало программы.
Команда PUSH источник делает следующее:
48
–вычитает 2 из регистра SP;
–записывает значение источника по адресу SS:SP. Команда POP приемник делает следующее:
записывает данные, размещенные по адресу SS:SP
вприемник;
увеличивает на 2 значение регистра SP.
Текущий адрес указателя в SS:SP называется вершиной стека.
Для COM-файлов сегмент стека – это обычно и сегмент кода, а указатель стека установлен в значение 0FFFEh. По адресу SS:0FFFEh записывается адрес возврата для команды RET, которая выполняется в конце программы.
Вы можете наблюдать за работой стека, щелкнув по кнопке [Stack] в окне эмулятора. Вершина стека отмечена знаком «<».
49
11. МАКРОСЫ
Макросы – это те же процедуры, только виртуальные. Макросы подобны процедурам, но они выполняются только во время компиляции. После компиляции все макросы заменяются реальными командами. Если макрос будет объявлен, и никогда не будет использоваться в коде, компилятор просто проигнорирует его. Emu8086.inc содержит хорошие примеры использования макросов. Этот файл содержит несколько макросов, которые делают создание кода более легким.
Определение макроса:
имя MACRO [параметры,...] <команды>
ENDM
В отличие от процедур, макрос должен быть определен перед участком кода, где он будет использоваться, например:
MyMacro MACRO p1, p2, p3 MOV AX, p1
MOV BX, p2 MOV CX, p3 ENDM
ORG 100h MyMacro 1, 2, 3 MyMacro 4, 5, DX RET
Вышеописанный код эквивалентен следующему набору команд:
MOV AX, 00001h
MOV BX, 00002h
MOV CX, 00003h
MOV AX, 00004h
MOV BX, 00005h MOV CX, DX
50
