Деление в ассемблере примеры

Содержание

Первая программа

Следующая программа на языке ассемблера выведет строку на экран:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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
2

mov  ecx,TOTAL_STUDENTS

cmp  eax,TOTAL_STUDENTS

Операндом стейтмента EQU может быть выражение:

LENGTH equ 20
WIDTH equ 10
AREA equ length * width

1
2
3

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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
2
3
4
5

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