День 19. Основные команды языка ассемблера и оконное приложение
Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!
Оглавление цикла заметок о языке ассемблера
Вчера я начал знакомство с языком Ассемблер и с основами структуры процессоров. Продолжаю.
Существует две основные архитектуры процессоров. Первая называется RISC – архитектура с уменьшенным набором команд. Архитектура позволяет использовать меньшее число команд (относительно второй), но выполняет их более быстро.
Вторая называется CISC – компьютер со сложной системой команд. Все х86-совместимые процессоры принадлежат к этой архитектуре.
Очень хорошо расписано про выполнение команд в третей главе книги Рудольфа Марека «Ассемблер на примерах».
Данные, которые обрабатываются командами, называются операндами.
Адрес, как и команда – число. Чтобы не запоминать все эти числа, им присваиваются символические обозначения (переменные или еще называют их указателями).
При использовании косвенного операнда адрес в памяти записывается в квадратных скобках.
Команда mov
Прежде чем работать с данными в регистрах, их необходимо туда поместить. Команда MOV
(move), хоть и переводится как «перемещать», но выполняет другое, а именно, копирует из источника в приемник.
mov ax, 1
mov bx, [number]
и так далее
и так далее
Недопустимо использовать два косвенных аргумента в одной команде:
mov [num_1], [num_2] ; так нельзя!
Чтобы реализовать неправильный выше пример, необходимо использовать промежуточный аргумент:
mov ax, [num_1]
mov [num_2], ax
Оба операнда команды MOV
должны быть одинакового размера. Запустим следующий код:
use16
org 100h
mov ax, 0x1234
mov [counter], ax
mov ax,4C00h ;\
int 21h ;/
;-------------------------------------------------------
counter dw ?
Интересно вот что. Кроме того, что counter в дебаге заменяется на реальный адрес, если посмотреть дамп, то можно заменить следующее. Запись в память происходит в обратном порядке, т.е. записывается сначала 34, а потом 12. Это связано с тем, что х86-процессоры хранят байты слова или двойного слова младшим байтом вперед.
В языке ассемблер есть инструкции сложения ADD
и вычитания SUB
. Например:
add ax, cx
sub ax, cx
Есть важный нюанс. Значение ax
(в обоих случаях) будет перезаписано на результат, поэтому необходимо использовать промежуточный регистр, чтобы не потерять данные.
Теперь попробуем записать в регистр AL
число 255 и прибавить к нему число 8. То есть мы намерено выйдем за диапазон 8-битного регистра:
use16
org 100h
mov al, 255
add al, 8
mov ax,4C00h
int 21h
После выполнения суммирования, регистр AL
будет равен 0x07
. Куда же потерялся бит? Он записался в флаг CF
(признак переноса). Данный флаг используется для работы с бОльшим диапазоном чисел, чем могут поддерживать регистры.
Более интересно обстоят дела с инкрементированием. Запишем тот же пример:
use16
org 100h
mov al, 255
inc al
mov ax,4C00h
int 21h
В данном случае в флаг CF
не будет записана единица (флаг ZF
станет равен 1, так как в AL
теперь 0). То есть мы потеряли перенос.
Стало интересно, на что «реагирует» флаг ZF
, на весь регистр или на выполненный результат. Запишем следующий код:
use16
org 100h
mov ax, 0xFFFF
inc al
mov ax,4C00h
int 21h
Прибавляем единицу только к младшим битам. В результате в флаге ZF
единица.
Про отрицательные числа
«Процесс отображения отрицательных чисел в дополнительный код иногда называют маппингом (mapping)». Один байт может содержать от 0 до 255. Код дополнения может заменять этот диапазон другим – от -128 до 127. Аналогично для двух байт и четырех байт. Вырисовываются типы данных для высокоуровневых языков программирования. Ноль, кстати, относится к положительным числам.
Шестнадцатеричное число FFFA равно -6 или 65530. Если сложить это с любым числом, например, с числом 7:
use16
org 100h
mov ax, 0xFFFA
add ax, 7
mov ax,4C00h
int 21h
Мы получили, что в регистре AX
теперь значение 0x0001
, а флаг переноса равен 1. Если проигнорировать флаг переноса, то мы получим правильный результат сложения отрицательного числа -6 и числа 7.
Чтобы не заморачиваться с представлением отрицательного числа через дополнительный код, можно записать его сразу отрицательным:
use16
org 100h
mov ax, -6
add ax, 7
mov ax,4C00h
int 21h
Управляющие конструкции
Программа любой сложности и на любом языке программирования может быть написана с использованием всего лишь трех структур: линейной, условной и цикла.
Попробуем сравнить два числа:
use16
org 100h
mov ax, 0x00F0
mov bx, 0x00F0
cmp ax, bx
mov ax,4C00h
int 21h
Обычное вычитание? Повлияло на флаги ZF
и PF
(четность). А, не совсем вычитание. В регистрах изменения не происходят.
Теперь перейдем к команде безусловного перехода – JMP
. Аналог goto высокоуровневого языка. Команде необходимо передать адрес в памяти, куда необходимо перейти. У меня появилось желание написать бесконечный цикл (потому что пока не знаю, как делать ветвление):
use16
org 100h
my_loop:
mov ax, 0x00F0
mov bx, 0x00FF
cmp ax, bx
jmp my_loop
mov ax,4C00h
int 21h
Условные переходы
Наиболее популярные команды следующие:
jz
операнд – если флаг ZF
= 1 (ноль);jc
операнд – если флаг CF
= 1 (перенос);js
операнд – если флаг SF
= 1 (результат отрицательный);jo
операнд – если флаг OF
= 1 (переполнение).
Так же после буквы J
может быть добавлена буква N
, что говорить об инвертировании условия. Написал программу, которая сравнивает два регистра и, если они не равны, то вычитает из BX
число 0x01
до тех пор, пока регистры не будут равны. Бесполезно, но зато теперь более-менее понятно, как работает ветвление в языке ассемблер. Такая простая программа и такой сложный код.
use16
org 100h
mov ax, 0x00F0
mov bx, 0x00FF
my_loop:
cmp ax, bx
jz result_is_true
jnz result_is_false
result_is_true:
jmp finish
result_is_false:
sub bx, 0x01
jmp my_loop
finish:
mov ax,4C00h
int 21h
Такие прыжки (JMP
) ограничены по «дальности». Чтобы выполнить «дальний» прыжок, необходимо использовать промежуточную метку для прыжка.
Так как мы работает на архитектуре CISK
, то можем использовать специальную команду LOOP
, которая принимает метку. В регистр CX
запишем число 10 – это будет счетчиком, который будет уменьшаться командой LOOP
:
use16
org 100h
mov ax, 0x00F0
mov cx, 10
for_loop:
sub ax, 0x02
loop for_loop
mov ax,4C00h
int 21h
Так же можно добавлять к команде LOOP
буквы, например, LOOPZ
, LOOPNZ
и так далее. Уже похоже на цикл while
.
use16
org 100h
mov ax, 0x00F0
mov bx, 0x00F5
mov cx, 10
for_loop:
sub bx, 0x01
cmp ax, bx
loopnz for_loop
mov ax,4C00h
int 21h
Команды обработки стека
Часто возникает необходимость временно сохранять содержимое регистров процессора, чтобы в дальнейшем воспользоваться им.
Вспомним про FIFO
и LIFO
. Стек работает по принципу LIFO
. Здесь мы вспомним регистры BP
и SP
. Для работы со стеком есть две команды PUSH
и POP
.
mov ax, 0x00F0
push ax
pop bx
Так же можно поместить в стек сразу все общие регистры. Это делается при помощи команды PUSHA
и POPA
без параметров:
mov ax, 0x00F0
pusha
popa
Так же можно сохранять 32-битные регистры при помощи команды PUSHAD
и извлекать при помощи POPAD
(но мы работаем с 16-битными регистрами).
При помощи команд PUSHF
и POPF
(для 32-разрядных PUSHFD
и POPFD
) можно работать с регистрами признаков.
Программные прерывания
Еще раз про прерывания, но уже подробнее. Команда int 21h
вызывает DOS
. Т.е. вызывается 21 прерывание. Возврат из обработчика прерывания выполняется при помощи команды IRET
.
Программы под Win64 на языке ассемблер
Следующий раздел книги Рудольфа Марека «Ассемблер на примерах» повествует о прочих командах для процессора х86. Поэтому пора установить ассемблер, который способен компилировать под i386 и AMD64.
Первое, что я сделал, скачал Nasm с официального сайта (https://www.nasm.us/pub/nasm/releasebuilds/?C=M;O=D) для 64-разрядной системы. Если ввести nasm –h, то можно узнать какие форматы доступны. Нам нужен «Microsoft extended COFF for Win64 (x86-64)». Код будет сохранен в файле myfile.asm
. Для создания объектного файла, необходимо выполнить следующую команду:
nasm -f win64 myfile.asm
Теперь появился файл myfile.obj
. Так как Nasm кроссплатформенная программа, то далее начинаются танцы с бубном. Сначала скачал линкер Alink. Как понял в дальнейшем, при помощи его нельзя собирать исполняемые файлы под 64-разрядные системы. Поэтому воспользовался gcc
(нужно было сразу с него начинать).
gcc myfile.obj -o myfile.exe
(Примечание: вернулся сюда из 21 дня, когда дошел до раздела 9.6. книги «Ассемблер на примерах», в котором частично повествуется о компиляции программ под разные процессоры).
Далее в интернете для теста нашел программу:
global start
extern MessageBoxA ;from user32
extern ExitProcess ;from kernel32
section .text
start: push rbp ;sub rsp,40
xor r9d,r9d ;mov r9,MB_OK
mov r8d,title
mov edx,hello
xor ecx,ecx ;mov rcx,NULL
call MessageBoxA
xor ecx,ecx
call ExitProcess
hello: db 'Hello, Windows!',0
title: db 'My First Win64',0
Но она оказалась нерабочей (я долго думал, что как-то неправильно собирал ассемблеровский код). Кстати, ошибка была следующая:
... undefined reference to `WinMain' collect2.exe: error: ld returned 1 exit status
Как оказалось, необходимо использовать стандартное название main
(а не start
или _main
– два таких примера почему-то приводятся на разных сайтах).
global main
extern MessageBoxA ;from user32
extern ExitProcess ;from kernel32
section .text
main: push rbp ;sub rsp,40
xor r9d,r9d ;mov r9,MB_OK
mov r8d,title
mov edx,hello
xor ecx,ecx ;mov rcx,NULL
call MessageBoxA
xor ecx,ecx
call ExitProcess
hello: db 'Hello, Windows!',0
title: db 'My First Win64',0
После компоновки можно запустить myfile.exe
и увидеть всплывающее окно. Теперь можно приступить … т.е. продолжить изучение команд. Но уже завтра.