Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Шпоры.docx
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
158 Кб
Скачать
☆

9. Косвенная базовая индексная адресация со смещением

Адрес операнда вычисляется как сумма содержимого одного из базовых регистров BX или BP, одного из индексных регистров SI или DI и 8- или 16-разрядного смещения. Примеры:

mov al,[bp+di+5]

mov bl,[array2+bx+si]

Логическое И

Если оба бита равны 1, то результат равен 1, иначе результат равен 0.

AND

0

1

0

0

0

1

0

1

Для выполнения операции логического И предназначена команда AND. У этой команды 2 операнда, результат помещается на место первого операнда. Часто эта команда используется для обнуления определённых битов числа. При этом второй операнд называют маской. Обнуляются те биты операнда, которые в маске равны 0, значения остальных битов сохраняются. Примеры:

and ax,bx ;AX = AX & BX

and cl,11111110b ;Обнуление младшего бита CL

and dl,00001111b ;Обнуление старшей тетрады DL

Ещё одно использование этой команды — быстрое вычисление остатка от деления на степень 2. Например, так можно вычислить остаток от деления на 8:

and ax,111b ;AX = остаток от деления AX на 8

Логическое ИЛИ

Если хотя бы один из битов равен 1, то результат равен 1, иначе результат равен 0.

OR

0

1

0

0

1

1

1

1

Логическое ИЛИ вычисляется с помощью команды OR. У этой команды тоже 2 операнда, и результат помещается на место первого. Часто это команда используется для установки в 1 определённых битов числа. Если бит маски равен 1, то бит результата будет равен 1, остальные биты сохранят свои значения. Примеры:

or al,dl ;AL = AL | DL

or bl,10000000b ;Установить знаковый бит BL

or cl,00100101b ;Включить биты 0,2,5 CL

Логическое НЕ (инверсия)

Каждый бит операнда меняет своё значение на противоположное (0 → 1, 1 → 0). Операция выполняется с помощью команды NOT. У этой команды только один операнд. Результат помещается на место операнда. Эта команда не изменяет значения флагов. Пример:

not byte[bx] ;Инверсия байта по адресу в BX

Логическое исключающее ИЛИ (сумма по модулю два)

Если биты имеют одинаковое значение, то результат равен 0, иначе результат равен 1.

XOR

0

1

0

0

1

1

1

0

Исключающим ИЛИ эта операция называется потому, что результат равен 1, если один бит равен 1 или другой равен 1, а случай, когда оба равны 1, исключается. Ещё эта операция напоминает сложение, но в пределах одного бита, без переноса. 1+1=10, но перенос в другой разряд игнорируется и получается 0, отсюда название «сумма по модулю 2». Для выполнения этой операции предназначена команда XOR. У команды два операнда, результат помещается на место первого. Команду можно использовать для инверсии определённых битов операнда. Инвертируются те биты, которые в маске равны 1, остальные сохраняют своё значение. Примеры:

xor si,di ;SI = SI ^ DI

xor al,11110000b ;Инверсия старшей тетрады AL

xor bp,8000h ;Инверсия знакового бита BP

Обозначение операции в комментарии к первой строке используется во многих языках высокого уровня (например C, C++, Java и т.д.). Часто XOR используют для обнуления регистров. Если операнды равны, то результат операции всегда равен 0. Такой способ обнуления работает быстрее и, в отличие от командыMOV, не содержит непосредственного операнда, поэтому команда получается короче (и не содержит нулевых байтов, что особенно нравится хакерам):

mov bx,0 ;Эта команда занимает 3 байта

xor bx,bx ;А эта - всего 2

Команда

Переход, если

Условие перехода

JZ/JE

нуль или равно

ZF=1

JNZ/JNE

не нуль или не равно

ZF=0

JC/JNAE/JB

есть переполнение/не выше и не равно/ниже

CF=1

JNC/JAE/JNB

нет переполнения/выше или равно/не ниже

CF=0

JP

число единичных бит чётное

PF=1

JNP

число единичных бит нечётное

PF=0

JS

знак равен 1

SF=1

JNS

знак равен 0

SF=0

JO

есть переполнение

OF=1

JNO

нет переполнения

OF=0

JA/JNBE

выше/не ниже и не равно

CF=0 и ZF=0

JNA/JBE

не выше/ниже или равно

CF=1 или ZF=1

JG/JNLE

больше/не меньше и не равно

ZF=0 и SF=OF

JGE/JNL

больше или равно/не меньше

SF=OF

JL/JNGE

меньше/не больше и не равно

SF≠OF

JLE/JNG

меньше или равно/не больше

ZF=1 или SF≠OF

JCXZ

содержимое CX равно нулю

CX=0

Команды CMP и TEST

Часто для формирования условий переходов используются команды CMP и TEST. Команда CMPпредназначена для сравнения чисел. Она выполняется аналогично команде SUB: из первого операнда вычитается второй, но результат не записывается на место первого операнда, изменяются только значения флагов. Например:

cmp al,5 ;Сравнение AL и 5

jl c1 ;Переход, если AL < 5 (числа со знаком)

cmp al,5 ;Сравнение AL и 5

jb c1 ;Переход, если AL < 5 (числа без знака)

Команда TEST работает аналогично команде AND, но также результат не сохраняется, изменяются только флаги. С помощью этой команды можно проверить состояние различных битов операнда. Например:

test bl,00000100b ;Проверить состояние 2-го бита BL

jz c2 ;Переход, если 2-й бит равен 0

Логический сдвиг вправо

Логический сдвиг всегда выполняется без учёта знакового бита. Для логического сдвига вправо предназначена команда SHR. У этой команды два операнда. Первый операнд представляет собой сдвигаемое значение и на его место записывается результат операции. Второй операнд указывает, на сколько бит нужно осуществить сдвиг. Этим операндом может быть либо непосредственное значение, либо регистр CL.

shr ax,1 ;Логический сдвиг AX на 1 бит вправо

shr byte[bx],cl ;Лог. сдвиг байта по адресу BX на СL бит вправо

shr cl,4 ;CL = CL / 16 (для числа без знака)

Арифметический сдвиг вправо

Арифметический сдвиг отличается от логического тем, что он не изменяет значение старшего бита, и предназначен для чисел со знаком. Арифметический сдвиг вправо выполняется командой SAR. У этой команды тоже 2 операнда, аналогично команде SHR.

sar bx,1 ;Арифметический сдвиг BX на 1 бит вправо

sar di,cl ;Арифметический сдвиг DI на CL бит вправо

sar [x],3 ;x = x / 8 (x - 8-битное значение со знаком)

Логический и арифметический сдвиг влево

Логический сдвиг влево выполняется командой SHL, а арифметический — командой SAL. Однако, на самом деле это просто синонимы для одной и той же машинной команды. Сдвиг влево одинаков для чисел со знаком и чисел без знака. У команды 2 операнда, аналогично командам SHR и SAR

shl dx,1 ;Сдвиг DX на 1 бит влево

sal dx,1 ;То же самое

shl ax,cl ;Сдвиг AX на CL бит влево

sal [x],2 ;x = x * 4

Стеком называется структура данных,организованная по принципу LIFO («Last In — First Out» или «последним пришёл — первым ушёл»). Стек является неотъемлемой частью архитектуры процессора и поддерживается на аппаратном уровне: в процессоре есть специальные регистры (SS, BP, SP) и команды для работы со стеком.

Обычно стек используется для сохранения адресов возврата и передачи аргументов при вызове процедур (о процедурах в следующей части), также в нём выделяется память для локальных переменных. Кроме того, в стеке можно временно сохранять значения регистров.

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

  • добавление элемента на вершину стека (PUSH);

  • извлечение элемента с вершины стека (POP);

Добавление элемента в стек

Выполняется командой PUSH. У этой команды один операнд, который может быть непосредственным значением, 16-битным регистром (в том числе сегментым) или 16-битной переменной в памяти. Команда работает следующим образом:

  1. значение в регистре SP уменьшается на 2 (так как ширина стека — 16 бит или 2 байта);

  2. операнд помещается в память по адресу в SP.

push -5 ;Поместить -5 в стек

push ax ;Поместить AX в стек

push ds ;Поместить DS в стек

push [x] ;Поместить x в стек (x объявлен как слово)

push word [bx] ;Поместить в стек слово по адресу в BX

Существуют ещё 2 команды для добавления в стек. Команда PUSHF помещает в стек содержимое регистра флагов. Команда PUSHA помещает в стек содержимое всех регистров общего назначения в следующем порядке: АХ, СХ, DX, ВХ, SP, BP, SI, DI (значение DI будет на вершине стека). Значение SP помещается то, которое было до выполнения команды. Обе эти команды не имеют операндов.

Извлечение элемента из стека

Выполняется командой POP. У этой команды также один операнд, который может быть 16-битным регистром (в том числе сегментым, но кроме CS) или 16-битной переменной в памяти. Команда работает следующим образом:

  1. операнд читается из памяти по адресу в SP;

  2. значение в регистре SP увеличивается на 2.

Обратите внимание, что извлеченный из стека элемент не обнуляется и не затирается в памяти, а просто остаётся как мусор. Он будет перезаписан при помещении нового значения в стек.

pop cx ;Поместить значение из стека в CX

pop es ;Поместить значение из стека в ES

pop [x] ;Поместить значение из стека в переменную x

pop word [di] ;Поместить значение из стека в слово по адресу в DI

Соответственно, есть ещё 2 команды. POPF помещает значение с вершины стека в регистр флагов. POPAвосстанавливает из стека все регистры общего назначения (но при этом значение для SP игнорируется).

Команды CALL и RET

Для работы с процедурами предназначены команды CALL и RET. С помощью команды CALL выполняетсявызов процедуры. Эта команда работает почти также, как команда безусловного перехода (JMP), но с одним отличием — одновременно в стек сохраняется текущее значение регистра IP. Это позволяет потом вернуться к тому месту в коде, откуда была вызвана процедура. В качестве операнда указывается адрес перехода, который может быть непосредственным значением (меткой), 16-разрядным регистром (кроме сегментных) или ячейкой памяти, содержащей адрес.

Возврат из процедуры выполняется командой RET. Эта команда восстанавливает значение из вершины стека в регистр IP. Таким образом, выполнение программы продолжается с команды, следующей сразу после команды CALL. Обычно код процедуры заканчивается этой командой. Команды CALL и RET не изменяют значения флагов (кроме некоторых особых случаев в защищенном режиме). Небольшой пример разных способов вызова процедуры:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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 ;Переменная с адресом процедуры

Сохранение регистров

Хорошим приёмом является сохранение регистров, которые процедура изменяет в ходе своего выполнения. Это позволяет вызывать процедуру из любой части кода и не беспокоиться, что значения в регистрах будут испорчены. Обычно регистры сохраняются в стеке с помощью команды PUSH, а перед возвратом из процедуры восстанавливаются командой POP. Естественно, восстанавливать их надо в обратном порядке. Примерно вот так:

myproc:

push bx ;Сохранение регистров

push cx

push si

... ;Код процедуры

pop si ;Восстановление регистров

pop cx

pop bx

ret ;Возврат из процедуры

Для начала рассмотрим две полезные процедуры, которые будут использоваться в дальнейшем. Чтобы постоянно не обращаться в коде в функции DOS 09h, удобно написать маленькую процедуру для вывода строки:

;Процедура вывода строки на консоль

; DI - адрес строки

print_str:

push ax

mov ah,9 ;Функция DOS 09h - вывод строки

xchg dx,di ;Обмен значениями DX и DI

int 21h ;Обращение к функции DOS

xchg dx,di ;Обмен значениями DX и DI

pop ax

ret

В качестве параметра ей передаётся адрес строки в регистре DI. Строка должна оканчиваться символом ‘$’. Здесь используется команда XCHG, которая выполняет обмен значениями двух операндов.

Вторая полезная процедура — вывод конца строки. Она вызывает первую процедуру для вывода двух символов CR(13) и LF(10). Вызывается без параметров и не изменяет регистры.

;Процедура вывода конца строки (CR+LF)

print_endline:

push di

mov di,endline ;DI = адрес строки с символами CR,LF

call print_str ;Вывод строки на консоль

pop di

ret

...

endline db 13,10,'$'

Вывод чисел в двоичном виде

Алгоритм вывода в двоичном виде очень прост. Нужно проанализировать все биты числа и поместить в строку символы ’0′ или ’1′ в зависимости от значения соответствующего бита. Удобно делать это с помощью циклического сдвига в цикле. Сдвинутый бит оказывается в флаге CF, а после завершения цикла в регистре то же значение, что и в начале. Процедура byte_to_bin_str преобразует байт в регистре AL в строку. Адрес буфера для строки передаётся в регистре DI. Процедура всегда записывает в буфер 8 символов, так как в байте 8 бит.

;Процедура преобразования байта в строку в двоичном виде

; AL - байт.

; DI - буфер для строки (8 символов). Значение регистра не сохраняется.

byte_to_bin_str:

push cx ;Сохранение CX

mov cx,8 ;Счётчик цикла

 

btbs_lp:

rol al,1 ;Циклический сдвиг AL влево на 1 бит

jc btbs_1 ;Если выдвинутый бит = 1, то переход

mov byte[di],'0' ;Добавление символа '0' в строку

jmp btbs_end

btbs_1:

mov byte[di],'1' ;Добавление символа '1' в строку

btbs_end:

inc di ;Инкремент DI

loop btbs_lp ;Команда цикла

 

pop cx ;Восстановление CX

ret ;Возврат из процедуры

Используя эту процедуру, легко написать ещё одну для вывода слова в двоичном виде:

;Процедура преобразования слова в строку в двоичном виде

; AX - слово

; DI - буфер для строки (16 символов). Значение регистра не сохраняется.

word_to_bin_str:

xchg ah,al ;Обмен AH и AL

call byte_to_bin_str ;Преобразование старшего байта в строку

xchg ah,al ;Обмен AH и AL

call byte_to_bin_str ;Преобразование младшего байта в строку

ret

И наконец вот две процедуры, которые делают то, что нужно   Буфер имеет размер 17 символов, так как в слове 16 бит + символ ‘$’, обозначающий конец строки.

;Процедура вывода байта на консоль в двоичном виде

; AL - байт

print_byte_bin:

push di

mov di,buffer ;DI = адрес буфера

call byte_to_bin_str ;Преобразование байта в AL в строку

mov byte[di],'$' ;Добавление символа конца строки

sub di,8 ;DI = адрес начала строки

call print_str ;Вывод строки на консоль

pop di

ret

 

;Процедура вывода слова на консоль в двоичном виде

; AX - слово

print_word_bin:

push di

mov di,buffer ;DI = адрес буфера

call word_to_bin_str ;Преобразование слова в AX в строку

mov byte[di],'$' ;Добавление символа конца строки

sub di,16 ;DI = адрес начала строки

call print_str ;Вывод строки на консоль

pop di

ret

...

buffer rb 17

Полный исходный код примера вы можете скачать отсюда: printbin.asm. Результат работы программы выглядит вот так:

Вывод чисел в шестнадцатеричном виде

Этот пример по структуре похож на предыдущий, поэтому для краткости я рассмотрю только сами процедуры преобразования числа в строку. Преобразование в шестнадцатеричный вид удобно выполнять группами по 4 бита, то есть по тетрадам. Каждая тетрада будет представлять собой одну шестнадцатеричную цифру. Я написал отдельную процедуру для преобразования тетрады в символ цифры:

;Процедура преобразования числа (0-15) в шестнадцатеричную цифру

; вход : AL - число (0-15)

; выход: AL - шестнадцатеричная цифра ('0'-'F')

to_hex_digit:

add al,'0' ;Прибавляем символ '0' (код 0x30)

cmp al,'9' ;Сравнение с символом '9' (код 0x39)

jle thd_end ;Если получилось '0'-'9', то выход

add al,7 ;Прибавляем ещё 7 для символов 'A'-'F'

thd_end:

ret

Если значение тетрады от 0 до 9, то достаточно только прибавить код символа ’0′ (0×30). А если значение больше 9, то надо прибавить ещё 7, чтобы получилась буква ‘A’-'F’.

Теперь легко можно преобразовать байт в шестнадцатеричную строку, достаточно каждую из его тетрад заменить соответствующей цифрой:

;Процедура преобразования байта в строку в шестнадцатеричном виде

; AL - байт.

; DI - буфер для строки (2 символа). Значение регистра не сохраняется.

byte_to_hex_str:

push ax

mov ah,al ;Сохранение значения AL в AH

shr al,4 ;Выделение старшей тетрады

call to_hex_digit ;Преобразование в шестнадцатеричную цифру

mov [di],al ;Добавление символа в строку

inc di ;Инкремент DI

mov al,ah ;Восстановление AL

and al,0Fh ;Выделение младшей тетрады

call to_hex_digit ;Преобразование в шестнадцатеричную цифру

mov [di],al ;Добавление символа в строку

inc di ;Инкремент DI

pop ax

ret

Преобразование слова также не представляет трудности — сначала преобразуем старший байт, затем младший:

;Процедура преобразования слова в строку в шестнадцатеричном виде

; AX - слово

; DI - буфер для строки (4 символа). Значение регистра не сохраняется.

word_to_hex_str:

xchg ah,al ;Обмен AH и AL

call byte_to_hex_str ;Преобразование старшего байта в строку

xchg ah,al ;Обмен AH и AL

call byte_to_hex_str ;Преобразование младшего байта в строку

ret

Полный исходный код примера: printhex.asm. Результат работы программы выглядит вот так:

Вывод чисел в десятичном виде

С десятичными числами немного сложнее. Для начала займёмся числами без знака. Чтобы преобразовать число в десятичную строку необходимо в цикле делить его на 10 (это основание системы счисления). Остатки от деления дают нам значения десятичных цифр. Первый остаток — младшая цифра, последний — старшая. Деление продолжается пока частное не равно нулю.

Например, если есть число 125. Делим его на десять: получаем 12, 5 в остатке. Потом делим 12 на десять: получаем 1, 2 в остатке. Наконец, 1 делим на 10: получаем 0, 1 в остатке. Цифры числа, начиная с младшей: 5, 2, 1. Так как обычно десятичные числа пишут, начиная со старшей цифры, то необходимо переставить их наоборот   Я для этого использовал стек.

В первом цикле производится деление, полученные остатки преобразуются в цифры и помещаются в стек. Во втором цикле символы извлекаются из стека (в обратном порядке) и помещаются в строку. Так как максимальное значение слова без знака 65536 (5 цифр), то в буфер записывается максимум 5 символов.

;Процедура преобразования слова в строку в десятичном виде (без знака)

; AX - слово

; DI - буфер для строки (5 символов). Значение регистра не сохраняется.

word_to_udec_str:

push ax

push cx

push dx

push bx

xor cx,cx ;Обнуление CX

mov bx,10 ;В BX делитель (10 для десятичной системы)

 

wtuds_lp1: ;Цикл получения остатков от деления

xor dx,dx ;Обнуление старшей части двойного слова

div bx ;Деление AX=(DX:AX)/BX, остаток в DX

add dl,'0' ;Преобразование остатка в код символа

push dx ;Сохранение в стеке

inc cx ;Увеличение счетчика символов

test ax,ax ;Проверка AX

jnz wtuds_lp1 ;Переход к началу цикла, если частное не 0.

 

wtuds_lp2: ;Цикл извлечения символов из стека

pop dx ;Восстановление символа из стека

mov [di],dl ;Сохранение символа в буфере

inc di ;Инкремент адреса буфера

loop wtuds_lp2 ;Команда цикла

 

pop bx

pop dx

pop cx

pop ax

ret

Для вывода байта можно преобразовать его в слово и воспользоваться той же процедурой:

;Процедура преобразования байта в строку в десятичном виде (без знака)

; AL - байт.

; DI - буфер для строки (3 символа). Значение регистра не сохраняется.

byte_to_udec_str:

push ax

xor ah,ah ;Преобразование байта в слово (без знака)

call word_to_udec_str ;Вызов процедуры для слова без знака

pop ax

ret

Теперь разберёмся с числами со знаком. Сначала нужно проверить старший бит числа. Если число положительное, то его можно преобразовать также как число без знака. Если число отрицательное, то добавляем в строку символ ‘-’, а затем инвертируем число и преобразуем как беззнаковое.

;Процедура преобразования слова в строку в десятичном виде (со знаком)

; AX - слово

; DI - буфер для строки (6 символов). Значение регистра не сохраняется.

word_to_sdec_str:

push ax

test ax,ax ;Проверка знака AX

jns wtsds_no_sign ;Если >= 0, преобразуем как беззнаковое

mov byte[di],'-' ;Добавление знака в начало строки

inc di ;Инкремент DI

neg ax ;Изменение знака значения AX

wtsds_no_sign:

call word_to_udec_str ;Преобразование беззнакового значения

pop ax

ret

;Процедура преобразования байта в строку в десятичном виде (со знаком)

; AL - байт.

; DI - буфер для строки (4 символа). Значение регистра не сохраняется.

byte_to_sdec_str:

push ax

movsx ax,al ;Преобразование байта в слово (со знаком)

call word_to_sdec_str ;Вызов процедуры для слова со знаком

pop ax

ret

Полный исходный код примера: printdec.asm. Результат работы программы выглядит вот так:

Как вывести на консоль в десятичном виде очень большое число (> 32 бит) читайте здесь.

Вывод чисел в восьмеричном виде

Выводить числа в восьмеричном виде приходится достаточно редко, поэтому подробно описывать не буду. Можно либо делить число последовательно на 8, либо преобразовывать в цифры группы по 3 бита. Я использовал второй вариант. Смотрите код примера: printoct.asm. Результат работы программы:

Вывод чисел в других системах счисления

Реализуется также, как вывод в десятичном виде — с помощью алгоритма последовательного деления на основание системы счисления. Например, если вам нужно вывести число в пятеричной системе счисления, делить надо на 5, а не на 10.

Упражнение

Напишите программу для вывода на консоль массива слов со знаком в десятичном виде (например, через запятую). Для вывода чисел можете воспользоваться моим примером или написать свою собственную процедуру. Результаты можете писать в комментариях или на форуме.

Флаг переноса CF

Команда CLC сбрасывает флаг CF.

Команда STC устанавливает флаг CF в единицу.

Команда CMC инвертирует значение флага CF.

Флаг направления DF

Этот флаг определяет направление обработки данных цепочечными командами (о них подробно расскажу в отдельной статье). Он должен устанавливаться или сбрасываться перед использованием этих команд.

Команда CLD сбрасывает флаг DF.

Команда STD устанавливает флаг DF в единицу.

Флаг прерывания IF

Этот флаг определяет, разрешены в данный момент прерывания или нет (о прерываниях тоже будет отдельная статья).

Команда CLI сбрасывает флаг IF (запрещает прерывания).

Команда STI устанавливает флаг IF в единицу (разрешает прерывания).

Команды LAHF и SAHF

Команда LAHF загружает младший байт регистра флагов в AH. Её удобно использовать, когда нужно получить значения сразу нескольких флагов. Порядок расположения флагов представлен на рисунке:

Команда SAHF выполняет обратную операцию — загружает содержимое AH в младший байт регистра флагов. Это позволяет одновременно изменить значения нескольких флагов. При этом биты 1, 3, 5 регистра AH игнорируются.

Помещение параметров в стек

Перед вызовом процедуры параметры необходимо поместить в стек с помощью команды PUSH. Здесь существует два варианта: параметры могут помещаться в стек в прямом или в обратном порядке. Обычно используется обратный порядок и его я буду использовать в примерах. Параметры помещаются в стек, начиная с последнего, так что перед вызовом процедуры на вершине стека оказывается первый параметр:

; Данные

arg0 dw 0

arg1 dw 12

argN dw 345

;---------------------------------------------------------------------

; Код

push [argN]

push ...

push [arg1]

push [arg0]

call myproc

Обращение к параметрам внутри процедуры

Для обращения к параметрам внутри процедуры обычно используют регистр BP. В самом начале процедуры содержимое регистра BP сохраняется в стеке и в него копируется значение регистра SP. Это позволяет «запомнить» положение вершины стека и адресовать параметры относительно регистра BP.

;Процедура

myproc:

push bp

mov bp,sp

...

Извлечение параметров из стека

После того, как процедура выполнилась, необходимо очистить стек, вытолкнув из него параметры. Тут тоже существует 2 способа: стек может быть очищен самой процедурой или кодом, который эту процедуру вызывал. Для первого способа используется команда RET с одним операндом, который должен быть равен количеству байтов, выталкиваемых из стека. В нашем случае он должен быть равен количеству параметров, умноженному на 2.

Команды ENTER и LEAVE

В системе команд процессоров x86 существуют также специальные команды для работы с кадром стека процедуры: ENTER и LEAVE. Команда ENTER обычно размещается в начале процедуры. У неё два непосредственных операнда: первый операнд — размер памяти, выделяемой под локальные переменные, второй операнд — уровень вложенности. в нашем случае второй операнд будет равен 0. Тогда по действию команда будет аналогична трём следующим командам:

push bp ;\

mov bp,sp ; > или enter locals_size,0

sub sp,locals_size ;/

Второй операнд этой команды позволяет организовывать вложенные области видимости, как в некоторых языках высокого уровня. В ассемблере эти возможности используются редко.

Команда LEAVE не имеет операндов и аналогична по действию двум командам:

mov sp,bp ;\

pop bp ;/ или leave

Синтаксис создания макроса

Для создания макроса используется директива macro. Эта директива имеет следующий синтаксис:

macro <название_макроса> [<список_параметров>]

{

<тело_макроса>

}

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

macro <название_макроса> [<список_параметров>] { <тело_макроса> }

Тело макроса — это код, который подставляется в то место, где макрос будет вызван. Создание макроса является по сути лишь его объявлением, в этом месте программы никакого кода сгенерировано не будет! Поэтому объявления макросов обычно размещают в самом начале программы или в отдельном файле.

Примеры макросов

В качестве примера рассмотрим простой макрос без параметров, который предназначен для завершения программы:

macro exit_app

{

mov ax,4C00h

int 21h

}

После того, как макрос объявлен, в нужном месте программы достаточно написать exit_app. Туда препроцессор FASM автоматически подставит 2 команды, записанные в теле макроса. Создадим ещё один полезный макрос, предназначенный для такой часто используемой операции, как вывод строки:

macro print_str str

{

mov ah,9

mov dx,str

int 21h

}

У этого макроса есть один параметр — адрес строки. При генерации кода вместо str будет подставлен тот параметр, который указан при вызове макроса. Обратите внимание, что код будет генерироваться в месте каждого вызова макроса! В этом главное отличие макроса от процедуры. Код процедуры содержится в программе только в одном экземпляре, а вызывается она с помощью передачи управления командойCALL.

Расширение системы команд

Макросы можно использовать для расширения системы команд. Например, часто в программе приходится обнулять регистры. Создадим специальный макрос для этой цели:

; Макрос - команда обнуления регистра

macro clr reg { xor reg,reg }

Теперь обнулять регистры в программе можно так:

clr ax ;AX=0

clr si ;SI=0

clr bl ;BL=0

Макросы с переменным количеством параметров

Возможно также создавать макросы с переменным количеством параметров. В этом случае имя параметра записывается в квадратных скобках. Для генерации кода макрос вызывается столько раз, сколько параметров ему было передано. Например, можно написать специальные макросы для улучшения командPUSH и POP:

; Макрос - улучшенная команда push

macro push [arg] { push arg }

; Макрос - улучшенная команда pop

macro pop [arg] { pop arg }

Несмотря на то, что название макроса совпадает с именем команды, в теле макроса это имя считается именем команды (иначе получилась бы бесконечная рекурсия   ). Данные макросы позволяют помещать в стек и извлекать из стека сразу несколько операндов, что упрощает код программы. Пример использования:

push ax,word[si],5

pop dx,cx,ax

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

push ax

push word[si]

push 5

pop dx

pop cx

pop ax

Директива include

Возможно, вам захочется написать собственный набор макросов и использовать их в своих программах. В этом случае удобно поместить макросы в отдельный файл и воспользоваться директивой включения файлаinclude. Синтаксис директивы include очень прост:

include 'путь/к/файлу'

Путь к файлу указывается в одинарных кавычках и может быть относительным (по отношению к компилируемому файлу) или полным (начиная от буквы диска или корневого каталога системы). Если включаемый файл находится в той же папке, то достаточно указать только имя файла. Расширение файла может быть любым, но обычно используют «inc» или «asm».

Препроцессор FASM читает указанный файл и подставляет код из него вместо директивы include. В отдельный файл можно также вынести часто используемые процедуры, отдельные функциональные блоки программы или даже объявления данных.

Заголовочный файл необходимо будет включить в программу с помощью директивы 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 байт памяти. Первая переменная находится по самому младшему адресу (ближе к вершине стека). Сгенерировалась также команда, присваивающая значение первой переменной (сразу после сохранения регистров).

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]