×
Главная   »   Заметки   »   День 19. Основные команды языка ассемблера и оконное приложение
День 19. Основные команды языка ассемблера и оконное приложение
Метки:      ,   ,   ,   ,   

Продолжаю знакомиться с языком ассемблера. Изучаю основные команды и компилирую под AMD64

Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!

Оглавление цикла заметок о языке ассемблера

Вчера я начал знакомство с языком Ассемблер и с основами структуры процессоров. Продолжаю.

Существует две основные архитектуры процессоров. Первая называется 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    

Условные переходы

Наиболее популярные команды следующие:

  1. jz операнд – если флаг ZF = 1 (ноль);
  2. jc операнд – если флаг CF = 1 (перенос);
  3. js операнд – если флаг SF = 1 (результат отрицательный);
  4. 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 и увидеть всплывающее окно. Теперь можно приступить … т.е. продолжить изучение команд. Но уже завтра.

434 просмотра
04.06.2022
Автор