- •1. Неявная адресация
- •2. Непосредственная адресация
- •3. Абсолютная прямая адресация
- •4. Относительная прямая адресация
- •5. Регистровая адресация
- •6. Косвенная регистровая (базовая) адресация
- •7. Косвенная регистровая (базовая) адресация со смещением
- •8. Косвенная базовая индексная адресация
- •9. Косвенная базовая индексная адресация со смещением
- •2. Альтернативный синтаксис local
- •3. Locals и endl
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-битной переменной в памяти. Команда работает следующим образом:
значение в регистре SP уменьшается на 2 (так как ширина стека — 16 бит или 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-битной переменной в памяти. Команда работает следующим образом:
операнд читается из памяти по адресу в SP;
значение в регистре 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 байт памяти. Первая переменная находится по самому младшему адресу (ближе к вершине стека). Сгенерировалась также команда, присваивающая значение первой переменной (сразу после сохранения регистров).
