Первая программа
Следующая программа на языке ассемблера выведет строку на экран:
section .text
global _start ; необходимо для линкера (ld)
_start: ; сообщает линкеру стартовую точку
mov edx,len ; длина строки
mov ecx,msg ; строка
mov ebx,1 ; дескриптор файла (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov eax,1 ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра
section .data
msg db ‘Hello, world!’, 0xa ; содержимое строки для вывода
len equ $ — msg ; длина строки
1 |
section.text global_start;необходимодлялинкера(ld) _start;сообщаетлинкерустартовуюточку mov edx,len;длинастроки mov ecx,msg;строка mov ebx,1;дескрипторфайла(stdout) mov eax,4;номерсистемноговызова(sys_write) int0x80;вызовядра mov eax,1;номерсистемноговызова(sys_exit) int0x80;вызовядра section.data msg db’Hello, world!’,0xa;содержимоестрокидлявывода len equ$-msg;длинастроки |
Результат выполнения программы:
Битовые операции
Мнемоника | Описание | Операция | Флаги |
---|---|---|---|
CBR Rd, K | Очистка разрядов регистра | Rd ← Rd and (0FFH – K) | Z, N, V |
SBR Rd, K | Установка разрядов регистра | Rd ← Rd or K | Z, N, V |
CBI P, b | Сброс разряда I/O-регистра | P.b ← 0 | — |
SBI P, b | Установка разряда I/O-регистра | P.b ← 1 | — |
BCLR s | Сброс флага SREG | SREG.s ← 0 | SREG.s |
BSET s | Установка флага SREG | SREG.s ← 1 | SREG.s |
BLD Rd, b | Загрузка разряда регистра из флага T | Rd.b ← T | — |
BST Rr, b | Запись разряда регистра во флаг T | T ← Rd.b | T |
CLC | Сброс флага переноса | C ← 0 | C |
SEC | Установка флага переноса | C ← 1 | C |
CLN | Сброс флага отрицательного числа | N ← 0 | N |
SEN | Установка флага отрицательного числа | N ← 1 | N |
CLZ | Сброс флага нуля | Z ← 0 | Z |
SEZ | Установка флага нуля | Z ← 1 | Z |
CLI | Общий запрет прерываний | I ← 0 | I |
SEI | Общее разрешение прерываний | I ← 1 | I |
CLS | Сброс флага знака | S ← 0 | S |
SES | Установка флага знака | S ← 1 | S |
CLV | Сброс флага переполнения дополнительного кода | V ← 0 | V |
SEV | Установка флага переполнения дополнительного кода | V ← 1 | V |
CLT | Сброс пользовательского флага T | T ← 0 | T |
SET | Установка пользовательского флага T | T ← 1 | T |
CLH | Сброс флага половинного переноса | H ← 0 | H |
SEH | Установка флага половинного переноса | H ← 1 | H |
Использование программы
Этот раздел описывает
использование компилятора и
встроенного редактора
Открытие
файлов
В WAVRASM могут быть открыты как
новые так и существующие файлы.
Количество открытых файлов
ограничено размером памяти, однако
объём одного файла не может
превышать 28 килобайт (в связи с
ограничением MS-Windows). Компиляция
файлов большего размера возможна,
но они не могут быть редактируемы
встроенным редактором. Каждый файл
открывается в отдельном окне.
Сообщения об
ошибках
После компиляции программы
появляется окно сообщений. Все
обнаруженные компилятором ошибки
будут перечислены в этом окне. При
выборе строки с сообщением о
ошибке, строка исходного файла, в
которой найдена ошибка, становится
красной. Если же ошибка находится
во вложенном файле, то этого
подсвечивания не произойдёт.
Если по строке в окне сообщений
клацнуть дважды, то окно файла с
указанной ошибкой становится
активным, и курсор помещается в
начале строки содержащей ошибку.
Если же файл с ошибкой не открыт
(например это вложенный файл) то он
будет автоматически открыт.
Учтите, что если вы внесли
изменения в исходные тексты
(добавили или удалили строки), то
информация о номерах строк в окне
сообщений не является корректной.
Опции
Некоторые установки программы
могут быть изменены через пункт
меню «Options».
В поле ввода, озаглавленном
«List-file extension», вводится
расширение, используемое для файла
листинга, а в поле «Output-file
extension» находится расширение для
файлов с результатом компиляции
программы. В прямоугольнике «Output
file format» можно выбрать формат
выходного файла (как правило
используется интеловский). Однако
это не влияет на объектный файл
(используемый AVR Studio), который
всегда имеет один и тот же формат, и
расширение OBJ. Если в исходном файле
присутствует сегмент EEPROM то будет
также создан файл с расширением EEP.
Установки заданные в данном окне
запоминаются на постоянно, и при
следующем запуске программы, их нет
необходимости переустанавливать.
Опция «Wrap relative jumps» даёт
возможность «заворачивать»
адреса. Эта опция может быть
использована только на чипах с
объёмом программной памяти 4К слов
(8К байт), при этом становится
возможным делать относительные
переходы (rjmp) и вызовы подпрограмм
(rcall) по всей памяти.
Опция «Save before assemble» указывает
программе на необходимость
автоматического сохранения
активного окна (и только его) перед
компиляцией.
Если вы хотите, чтобы при закрытии
программы закрывались все
открытые окна, то поставьте галочку
в поле «Close all windows before exit».
Atmel, AVR являются
зарегистрированными товарными
знаками фирмы Atmel Corporation
Перевод выполнил Руслан
Шимкевич, ruslansh@i.com.ua
Условные переходы
Все команды этой группы выполняют переход (PC ← PC + A + 1) при разных условиях.
Мнемоника | Описание | Условие | Флаги |
---|---|---|---|
BRBC s, A | Переход если флаг S сброшен | Если SREG(S) = 0 | — |
BRBS s, A | Переход если флаг S установлен | Если SREG(S) = 1 | — |
BRCS A | Переход по переносу | Если C = 1 | — |
BRCC A | Переход если нет переноса | Если C = 0 | — |
BREQ A | Переход если равно | Если Z = 1 | — |
BRNE A | Переход если не равно | Если Z = 0 | — |
BRSH A | Переход если больше или равно | Если C = 0 | — |
BRLO A | Переход если меньше | Если C = 1 | — |
BRMI A | Переход если отрицательное значение | Если N = 1 | — |
BRPL A | Переход если положительное значение | Если N = 0 | — |
BRGE A | Переход если больше или равно (со знаком) | Если (N и V) = 0 | — |
BRLT A | Переход если меньше (со знаком) | Если (N или V) = 1 | — |
BRHS A | Переход по половинному переносу | Если H = 1 | — |
BRHC A | Переход если нет половинного переноса | Если H = 0 | — |
BRTS A | Переход если флаг T установлен | Если T = 1 | — |
BRTC A | Переход если флаг T сброшен | Если T = 0 | — |
BRVS A | Переход по переполнению дополнительного кода | Если V = 1 | — |
BRVC A | Переход если нет переполнения дополнительного кода | Если V = 0 | — |
BRID A | Переход если прерывания запрещены | Если I = 0 | — |
BRIE A | Переход если прерывания разрешены | Если I = 1 | — |
SBRC Rd, K | Пропустить следующую команду если бит в регистре очищен | Если Rd = 0 | — |
SBRS Rd, K | Пропустить следующую команду если бит в регистре установлен | Если Rd = 1 | — |
SBIC A, b | Пропустить еследующую команду если бит в регистре ввода/вывода очищен | Если I/O(A, b) = 0 | — |
SBIS A, b | Пропустить следующую команду если бит в регистре ввода/вывода установлен | Если I/O(A, b) = 1 | — |
Код сложения чисел на Assembler
Ну и собственно небольшой код программы:
.386 .model flat,stdcall .data .code start: mov eax,3 add eax,2 ret end start
Ну что ж, вот так вот выглядит код на Assembler. Первые 2 строчки являются обязательными для MASM, поэтому их мы будем писать в каждой программе. Они обозначают тип процессора и модель памяти с которой мы работаем.
Третья строчка — это раздел переменные( после этой строки, должны объявляться переменные), как вы видите, у нас в этой программе не будет переменных.
Четвертая — раздел кода. В нашей программе, мы помещаем в регистр eax значение 3, а затем с помощью add(прибавить) добавляем 2, логично, что теперь в этом регистре будет храниться значение 5. Кто не знает, что такое регистр, то вам лучше почитать об этом здесь. Затем идет команда ret, которая говорит о выходе из программы и сам выход end start.
Инструкции DIV и IDIV
Операция деления генерирует два элемента: частное и остаток. В случае умножения переполнения не происходит, так как для хранения результата используются регистры двойной длины. Однако в случае деления, переполнение может произойти. Если это произошло, то процессор генерирует прерывание.
Инструкция DIV (от англ. «DIVIDE») используется с данными unsigned, а инструкция IDIV (от англ. «INTEGER DIVIDE») используется с данными signed.
Синтаксис инструкций DIVIDIV:
Делимое находится в аккумуляторе. Обе инструкции могут работать с 8-битными, 16-битными или 32-битными операндами. Данная операция влияет на все 6 флагов состояния.
Рассмотрим следующие 3 сценария:
Сценарий №1: Делителем является значение типа byte. Предполагается, что делимое находится в регистре AX (16 бит). После деления частное переходит в регистр AL, а остаток переходит в регистр AH:
Сценарий №2: Делителем является значение типа word. Предполагается, что делимое имеет длину 32 бита и находится в регистрах DX и AX. Старшие 16 бит находятся в DX, а младшие 16 бит — в AX. После деления 16-битное частное попадает в регистр AX, а 16-битный остаток — в регистр DX:
Сценарий №3: Делителем является значение типа doubleword. Предполагается, что размер делимого составляет 64 бита и оно размещено в регистрах EDX и EAX. Старшие 32 бита находятся в EDX, а младшие 32 бита — в EAX. После деления 32-битное частное попадает в регистр EAX, а 32-битный остаток — в регистр EDX:
В следующем примере мы делим 8 на 2. Делимое 8 сохраняется в 16-битном регистре AX, а делитель 2 — в 8-битном регистре BL:
section .text
global _start ; должно быть объявлено для использования gcc
_start: ; сообщаем линкеру входную точку
mov ax,’8′
sub ax, ‘0’
mov bl, ‘2’
sub bl, ‘0’
div bl
add ax, ‘0’
mov , ax
mov ecx,msg
mov edx, len
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov ecx,res
mov edx, 1
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov eax,1 ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра
section .data
msg db «The result is:», 0xA,0xD
len equ $- msg
segment .bss
res resb 1
1 |
section.text global_start; должно быть объявлено для использования gcc _start; сообщаем линкеру входную точку movax,’8′ subax,’0′ movbl,’2′ subbl,’0′ divbl addax,’0′ movres,ax movecx,msg movedx,len movebx,1; файловый дескриптор (stdout) moveax,4; номер системного вызова (sys_write) int0x80; вызов ядра movecx,res movedx,1 movebx,1; файловый дескриптор (stdout) moveax,4; номер системного вызова (sys_write) int0x80; вызов ядра moveax,1; номер системного вызова (sys_exit) int0x80; вызов ядра section.data msgdb»The result is:»,0xA,0xD lenequ$-msg segment.bss resresb1 |
Результат выполнения программы:
Правила умножения в Assembler
Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:
- Если аргументом команды mul является 1-байтовый регистр (например ), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
- Если аргументом является регистр из 2 байт(например), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
- Если аргументом является регистр из 4 байт(например), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.
Простые логические операторы
Известные всем логические операторы и в Assembler выполняют практически такие же функции. Мы рассмотрим самые основные и наиболее используемые:
Логическое побитовое И
Логическое побитовое ИЛИ
Исключающее побитовое ИЛИ
Логическое побитовое И
В Assembler этот оператор сравнивает два регистра по одному биту. Он обозначается как and, и вот пример синтаксиса: Очевидно, что результатом этой операции будет 01000001, но возникает один вопрос: куда записывается результат выполнения оператора?
Так вот, результат записывается в регистр, который стоит первым после оператора and, в нашем случае — это регистр bx, то есть теперь его значение поменялось на 01000001.
Логическая инструкция test
Во многих случаях нам бы не хотелось, чтобы число переписывалось и теряло своего первоначального значения. Именно для этого предусмотрели логическую инструкцию test. Она как и and производит побитовое умножение, но не записывает результат в какой либо регистр, а всего лишь поднимает флаги для каждого бита, то есть она имитирует выполнение инструкции and. И для лучшего восприятия мы рассмотрим пример на проверку четности числа с помощью логических операций в Assembler:
.386 .model flat,stdcall option casemap:none include ..\INCLUDE\kernel32.inc include ..\INCLUDE\user32.inc includelib ..\LIB\kernel32.lib includelib ..\LIB\user32.lib .data yes db "Chetnoe" no db "Ne chetnoe" stdout dd ? cWritten dd ? .code start: invoke GetStdHandle, -11 ; дескриптор вывода mov stdout,eax ; по умолчанию помещается в eax mov ah, 23 test ah, 00000001b ; сравниваем последний бит числа jz evn invoke WriteConsoleA, stdout, ADDR no, sizeof no, ADDR cWritten, 0 jmp exit evn: invoke WriteConsoleA, stdout, ADDR yes, sizeof yes, ADDR cWritten, 0 exit: invoke ExitProcess,0 end start
Число, которое мы проверяем на четность, помещаем в регистр ah, затем сравниваем последний бит числа с единицей, и если вернется единица, то число нечетное, а если ноль — четное, что соответственно выводится на экран. Напомню, что команда jz отвечает за условный переход на метку (метка evn), а команда jmp — за безусловный переход.
Логическое побитовое ИЛИ
В Assembler логическое побитовое ИЛИ обозначается or, и синтаксис идентичен синтаксису команды and, по своей сути представляет побитовое сложение.
Выполнение этой инструкции вернет 01101111 и поместит это двоичное число в регистр bx.
Логическое исключающее ИЛИ
Также помимо логического ИЛИ, часто используют исключающее ИЛИ в Assembler. Оно обозначается командой xor и выделяет различия в регистрах, то есть, если в одном бите содержится 1, а в другом 0, то xor вернет 1, если же в битах содержатся одинаковые значения, то xor вернет 0.
Разберем на примере, за основу возьмем предыдущий пример проверки на четность:
.386 .model flat,stdcall option casemap:none include ..\INCLUDE\kernel32.inc include ..\INCLUDE\user32.inc includelib ..\LIB\kernel32.lib includelib ..\LIB\user32.lib .data yes db "Chetnoe" no db "Ne chetnoe" stdout dd ? cWritten dd ? .code start: invoke GetStdHandle, -11 ; дескриптор вывода mov stdout,eax ; по умолчанию помещается в eax xor ah, ah ; обнуление регистра ah xor al, al or ah, 21 ; помещаем в регистр число 21 or al, 20 xor ah, al xor al, ah xor ah, al ; конструкция для смены значений регистров test ah, 00000001b jz evn invoke WriteConsoleA, stdout, ADDR no, sizeof no, ADDR cWritten, 0 jmp exit evn: invoke WriteConsoleA, stdout, ADDR yes, sizeof yes, ADDR cWritten, 0 exit: invoke ExitProcess,0 end start
Конструкция из 3 xor позволяет поменять значения в регистрах, и по окончании в регистре ah будет содержаться число 20. Затем выведется сообщение о том, что число четное.
Также отметим конструкцию — она позволяет обнулить регистр. По сути это аналог команды , но программисты любят использовать именно эту конструкцию, так как она занимает всего 2 байта, а команда mov — 5 байт.
Заключение
В этой статье мы поговорили и разобрали основные моменты работы с логическими инструкциями и операциями в Assembler, не забывайте скачивать и просматривать исходники, а также оставляйте ваши комментарии.
Скачать исходник 1 Скачать исходник 2
Деление в Assembler
Для умножения чисел без знака предназначена команда DIV, которая относится к группе команд целочисленной арифметики и производит целочисленное деление с остатком беззнаковых целочисленных операндов.
Делимое, частное и остаток задаются неявно. Делимое является переменной в регистре (или регистровой паре) AX, DX:AX или EDX:EAX в зависимости от кода команды и размера операнда (что также определяет и разрядность делителя). Единственный явный операнд команды — операнд-источник (SRC), задающий делитель — может быть переменной в регистре или в памяти.
Целая часть частного помещается в регистр AL, AX или EAX в зависимости от заданного размера делителя (8, 16 или 32 бита). При этом остаток от целочисленного деления помещается в регистр AH, DX или EDX соответственно.
Действие команды DIV зависит от размера операнда-источника следующим образом:
Если частное, получаемое в результате деления, оказывается слишком велико, чтобы поместиться в целевом регистре-назначении (то есть имеет место переполнение), или если делитель равен нулю, то генерируется особая ситуация #DE.
Если аргументом команды div является 1-байтовый регистр (например DIV bl), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah.
Если аргументом является регистр из 2 байт(например DIV bx), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx.
Если же аргументом является регистр из 4 байт(например DIV ebx), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx.
Директива EQU
Директива EQU используется для определения констант. Её синтаксис следующий:
Например:
TOTAL_STUDENTS equ 50
1 | TOTAL_STUDENTS equ50 |
Затем вы можете использовать эту константу в программе:
mov ecx, TOTAL_STUDENTS
cmp eax, TOTAL_STUDENTS
1 |
mov ecx,TOTAL_STUDENTS cmp eax,TOTAL_STUDENTS |
Операндом стейтмента EQU может быть выражение:
LENGTH equ 20
WIDTH equ 10
AREA equ length * width
1 |
LENGTH equ20 WIDTH equ10 AREA equ length*width |
Вышеприведенный фрагмент кода определит как .
Еще один пример:
SYS_EXIT equ 1
SYS_WRITE equ 4
STDIN equ 0
STDOUT equ 1
section .text
global _start ; должно быть объявлено для линкера (gcc)
_start: ; сообщаем линкеру входную точку
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg1
mov edx, len1
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg2
mov edx, len2
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg3
mov edx, len3
int 0x80
mov eax,SYS_EXIT ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра
section .data
msg1 db ‘Hello, programmers!’,0xA,0xD
len1 equ $ — msg1
msg2 db ‘Welcome to the world of,’, 0xA,0xD
len2 equ $ — msg2
msg3 db ‘Linux assembly programming! ‘
len3 equ $- msg3
1 |
SYS_EXITequ1 SYS_WRITEequ4 STDINequ STDOUTequ1 section.text global_start; должно быть объявлено для линкера (gcc) _start; сообщаем линкеру входную точку moveax,SYS_WRITE movebx,STDOUT movecx,msg1 movedx,len1 int0x80 moveax,SYS_WRITE movebx,STDOUT movecx,msg2 movedx,len2 int0x80 moveax,SYS_WRITE movebx,STDOUT movecx,msg3 movedx,len3 int0x80 moveax,SYS_EXIT; номер системного вызова (sys_exit) int0x80; вызов ядра section.data msg1db’Hello, programmers!’,0xA,0xD len1equ$-msg1 msg2db’Welcome to the world of,’,0xA,0xD len2equ$-msg2 msg3db’Linux assembly programming! ‘ len3equ$-msg3 |
Результат выполнения программы:
Инструкции MIPS
Примечание.MARSотсюда
Типы инструкций
- тип R (register). В роли операндов используются три регистра – регистр назначения (сокр. $rd), первый аргумент ($rs), и второй аргумент ($rt). Пример такой инструкции – сложение трёх регистров: В данном случае в $t2 будет помещён результат сложения значений в $t0 и $t1.
- тип I (immediate). Операнды – два регистра и число. Пример инструкции типа I: После выполнения в регистр $t3 будет помещён результат сложения $t2 и числа 12.
- Тип J (jump). Единственный операнд – 26-битный адрес, куда нужно перейти. Инструкция перейдёт на адрес 128 в .
Арифметические инструкции
- сумма rs и rt записывается в регистр rd. Аккуратно, может вызвать переполнение.
- rd = rs — rt. Также можно получить переполнение.
- почти то же самое, что и предыдущая инструкция, но эта не может вызвать переполнение. Для арифметических вычислений предпочтительно использовать именно эту инструкцию.
- rd = rs — rt. Также без переполнения, и поэтому рекомендуется к использованию.
- rt = rs + 16-битное целое число. Как и , может вызывать переполнение.
- то же самое, но без возможности переполнения. Use it.
отсюда
Оговорочки
Хочу сразу оговориться, что правильно говорить не «ассемблер» (assembler), а «язык ассемблера» (assembly language), потому как ассемблер – это транслятор кода на языке ассемблера (т.е. по сути, программа MASM, TASM, fasm, NASM, UASM, GAS и пр., которая компилирует исходный текст на языке ассемблера в объектный или исполняемый файл). Тем не менее, из соображения краткости многие, говоря «ассемблер» (асм, asm), подразумевают именно «язык ассемблера».
Синтаксис директив, стандартных макросов и пр. структурных элементов различных диалектов (к примеру, MASM, fasm, NASM, GAS), могут отличаться довольно существенно. Мнемоники (имена) инструкций (команд) и регистров, а также синтаксис их написания для одного и того же процессора примерно одинаковы почти во всех диалектах (заметным исключением среди популярных ассемблеров является разве что GAS (GNU Assembler) в режиме синтаксиса AT&T для x86, где к именам инструкций могут добавляться суффиксы, обозначающие размер обрабатываемых ими данных, что бывает довольно удобно, но там есть и другие нюансы, сбивающие с толку программиста, привыкшего к классическому ассемблеру, к примеру, иной порядок указания операндов, хотя всё это лечится специальной директивой переключения в режим классического синтаксиса Intel).
Поскольку ассемблер – самый низкоуровневый язык программирования, довольно проблематично написать код, который корректно компилировался бы для разных архитектур процессоров (например, x86 и ARM), для разных режимов одного и того же процессора (16-битный реальный режим, 32-битный защищённый режим, 64-битный long mode; а ещё код может быть написан как с использованием различных технологий вроде SSE, AVX, FMA, BMI и AES-NI, так и без них) и для разных операционных систем (Windows, Linux, MS-DOS). Хоть иногда и можно встретить «универсальный» код (например, отдельные библиотеки), скажем, для 32- и 64-битного кода ОС Windows (или даже для Windows и Linux), но это бывает нечасто. Ведь каждая строка кода на ассемблере (не считая управляющих директив, макросов и тому подобного) – это отдельная инструкция, которая пишется для конкретного процессора и ОС, и сделать кроссплатформенный вариант можно только с помощью макросов и условных директив препроцессора, получая в итоге порой весьма нетривиальные конструкции, сложные для понимания.
Команды ассемблера и команды процессора.
Стоит пояснить, что если к вопросу подойти формально строго, то команды процессора и команды ассемблера — это не одно и то же. Ассеммблер — хоть и низкоуровневый язык программирования, но иногда он без спроса программиста «корректирует код под себя». Причём у каждого ассемблера (masm, tasm, fasm) это может быть по-разному. Самый яркий пример — команда ret. В ассемблерном коде мы запишем ret, а реальный ассемблер ассемблирует её как retf или retn 8. Может также изменяться код, добавлением в качестве выравнивания кода команды процессора nop (об этом ниже в статье) и т.п. Чтобы не усложнять суть вопроса, под понятиями команды процессора и команды ассемблера мы будем подразумевать одно и то же.
Команды процессора (команды ассемблера) в большинстве своём работают с аргументами, которые в ассемблере называются операндами. Система машинного кода процессоров Intel содержит более 300 команд (команды процессора, сопроцессора, MMX-расширения, XMM-расширения). С каждым новым процессором их количество растёт. Для того, чтобы профессионально программировать, не надо зубрить и разбирать все команды процессора. При необходимости можно воспользоваться справочником. В процессе чтения статей, вы поймёте, что основная суть знания ассемблера состоит не в доскональном знании всех команд, а в понимании работы системы.
Не следует забывать, что команды процессор видит в виде цифр, которые можно рассматривать как данные. Например, команда NOP занимает один байт и её машинный код — 90h.
Начиная изучать язык низкого уровня, мы будем иметь дело с ограниченным набором старых-добрых команд процессора. Иные команды ассемблера понадобятся специалистам, заинтересованным в оптимизацией кода, связанного со сложными математическими расчетами данных большого объёма.
Основные (т.н. целочисленные) команды ассемблера позволяют написать практически любую программу для операционных систем MS-DOS и Windows. Количество команд ассемблера, которыми вы будете пользоваться будет расти со временем прохождения курса. Для более детального понимания, в последствии можете обратиться к справочнику команд.
Пример на практике
Посмотрите на следующую простую программу, чтобы понять, как используются регистры в программировании на ассемблере. Эта программа выводит 9 звёздочек с простым сообщением:
section .text
global _start ; должно быть объявлено для линкера (gcc)
_start: ; сообщаем линкеру точку входа
mov edx,len ; длина сообщения
mov ecx,msg ; сообщение для вывода на экран
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov edx,9 ; длина сообщения
mov ecx,s2 ; сообщение для написания
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov eax,1 ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра
section .data
msg db ‘Displaying 9 stars’,0xa ; наше сообщение
len equ $ — msg ; длина нашего сообщения
s2 times 9 db ‘*’
1 |
section.text global_start;должнобытьобъявленодлялинкера(gcc) _start;сообщаемлинкеруточкувхода mov edx,len;длинасообщения mov ecx,msg;сообщениедлявыводанаэкран mov ebx,1;файловыйдескриптор(stdout) mov eax,4;номерсистемноговызова(sys_write) int0x80;вызовядра mov edx,9;длинасообщения mov ecx,s2;сообщениедлянаписания mov ebx,1;файловыйдескриптор(stdout) mov eax,4;номерсистемноговызова(sys_write) int0x80;вызовядра mov eax,1;номерсистемноговызова(sys_exit) int0x80;вызовядра section.data msg db’Displaying 9 stars’,0xa;нашесообщение len equ$-msg;длинанашегосообщения s2 times9db’*’ |
Результат выполнения программы:
Непрямая адресация памяти
Обычно для этой цели используются базовые регистры EBX, EBP (или BX, BP) и индексные регистры (DI, SI), используемые в квадратных скобках для ссылок на память.
Непрямая адресация обычно используется для переменных, содержащих несколько элементов, таких как массивы. Начальный адрес массива хранится, например, в регистре EBX.
В следующем примере мы получаем доступ к разным элементам переменной:
MY_TABLE TIMES 10 DW 0 ; выделяем 10 слов (2 байта), каждое из которых инициализируем значением 0
MOV EBX, ; помещаем эффективный адрес MY_TABLE в EBX
MOV , 110 ; MY_TABLE = 110
ADD EBX, 2 ; EBX = EBX +2
MOV , 123 ; MY_TABLE = 123
1 |
MY_TABLETIMES10DW; выделяем 10 слов (2 байта), каждое из которых инициализируем значением 0 MOVEBX,MY_TABLE; помещаем эффективный адрес MY_TABLE в EBX MOVEBX,110; MY_TABLE = 110 ADDEBX,2; EBX = EBX +2 MOVEBX,123; MY_TABLE = 123 |
Правила деления в Assembler
Почти аналогично реализуется и деление, вот примеры:
- Если аргументом команды div является 1-байтовый регистр (например div bl ), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah. ax/bl = al, ah
- Если аргументом является регистр из 2 байт(например div bx ), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx. (dx,ax)/bx = ax, dx
- Если же аргументом является регистр из 4 байт(например div ebx ), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx. (edx,eax)/ebx = eax, edx
Умножение двоичных чисел
В отличие от сложения и вычитания операция умножения реализуется двумя типами команд – учитывающими и не учитывающими знаки операндов.
Умножение чисел размером 1 байт без учета знака
--------------------------------------------------------------------- :mul_unsign.asm – программа умножения чисел размером 1 байт без учета знака. ;Вход: multiplier], и multiplied – множители размером 1 байт. ;Выход: product – значение произведения. --------------------------------------------------------------------- .data :значения в multiplier], и multiplied нужно внести product label word productj label byte multiplied db?:множитель 1 (младшая часть произведения) product_h db 0;старшая часть произведения multiplied db?;множитель 2 .code mul_unsign proc mov al.multiplierl mul multiplier2:оценить результат: jnc по_саrrу;нет переполнения – на no_carry обрабатываем ситуацию переполнения mov product_h.ah:старшая часть результата no_carry: mov product_l.al;младшая часть результата ret mul_unsign endp main: call mul_unsign end main
Здесь все достаточно просто и реализуется средствами самого процессора. Проблема состоит лишь в правильном определении размера результата. Произведение чисел большей размерности (2/4 байта) выполняется аналогично. Необходимо заменить директивы DB на DW/DD, регистр AL на АХ/ЕАХ, регистр АН на DX/EDX.
Умножение чисел размером N и М байт без учета знака
Для умножения чисел размером N и М байт, существует несколько стандартных алгоритмов, описанных в литературе. В этом разделе мы рассмотрим только один из них. В его основе лежит алгоритм умножения неотрицательных целых чисел, предложенный Кнутом.
Умножение N-байтного числа на число размером М байт
ПРОГРАММА mul_unsign_NM --------------------------------------------------------------------- //mul_unsign_NM – программа на псевдоязыке умножения N-байтного числа //на число размером М байт //(порядок – старший байт по младшему адресу (не Intel)) //Вход: U и V – множители размерностью N и М байт соответственно : b=256 – размерность машинного слова. //Выход: W – произведение размерностью N+M байт. --------------------------------------------------------------------- ПЕРЕМЕННЫЕ INT_BYTE u; v; w: k=0: INT_WORD b=256: temp_word НАЧ_ПРОГ ДЛЯ j: = M-l ДО 0 //J изменяется в диапазоне М-1..0 НАЧ_БЛОК_1 //проверка на равенство нулю очередного элемента множителя (не обязательно) ЕСЛИ v==0 TO ПЕРЕЙТИ_НА тб k: = 0: i: = n-l ll\ изменяется в диапазоне N-1..0 ДЛЯ 1: = N-1 ДО О НАЧ_БЛ0К_2 //перемножаем очередные элементы множителей temp_word: = u*v+w+k w: = temp_word MOD b //остаток от деления temp_word\b › w k: = temp_word\b //целая часть частного temp_word\b > k К0Н_БЛ0К_2 w: = k шб: КОН БЛОК_1 КОН_ПРОГ :inul_unsign_NM.asm – программа на ассемблере умножения N-байтного числа на число :размером М байт (порядок – старший байт по младшему адресу (не Intel)). .data:значения в U и V нужно внести U db?;U-un.i".UiU() – множитель_1 размерностью N байт 1-S-U:i=N V db?; V"Vm.i_ViV(| – множитель_2 размерностью М байт j=$-V:j=M len_product=$-U ;w – результат умножения, длина N+M W db len_product dup (0);1en_product=N+M k db 0:перенос 0 < k < 255 b dw lOOh: размер машинного слова .code