Арифметические операции над двоично-десятичными числами

Умножение двоичных чисел

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

Умножение чисел размером 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

Правила деления в Assembler

Почти аналогично реализуется и деление, вот примеры:

  • Если аргументом команды div является 1-байтовый регистр (например ), то значение регистра ax поделится на значение регистра bl, результат от деления запишется в регистр al, а остаток запишется в регистр ah.
  • Если аргументом является регистр из 2 байт(например), то процессор поделит число, старшие биты которого хранит регистр dx, а младшие ax на значение, хранящееся в регистре bx. Результат от деления запишется в регистр ax, а остаток запишется в регистр dx.
  • Если же аргументом является регистр из 4 байт(например), то процессор аналогично предыдущему варианту поделит число, старшие биты которого хранит регистр edx, а младшие eax на значение, хранящееся в регистре ebx. Результат от деления запишется в регистр eax, а остаток запишется в регистр edx.

Подытожим…

Итак, приведу неполный перечень того, в каких случаях используется ассемблер.

  1. Создание загрузчиков, прошивок устройств (комплектующих ПК, встраиваемых систем), элементов ядра ОС.

  2. Низкоуровневая работа с железом, в т.ч. с процессором, памятью.

  3. Внедрение кода в процессы (injection), как с вредоносной целью, так и с целью защиты или добавления функционала. Системный софт.

  4. Блоки распаковки, защиты кода и прочего функционала (с целью изменения поведения программы, добавления новых функций, взлома лицензий), встраиваемые в исполняемые файлы (см. UPX, ASProtect и пр).

  5. Оптимизация кода по скорости, в т.ч. векторизация (SSE, AVX, FMA), математические вычисления, обработка мультимедиа, копирование памяти.

  6. Оптимизация кода по размеру, где нужно контролировать каждый байт. Например, в демосцене.

  7. Вставки в языки высокого уровня, которые не позволяют выполнять необходимую задачу, либо позволяют делать это неоптимальным образом.

  8. При создании компиляторов и трансляторов исходного кода с какого-либо языка на язык ассемблера (например, многие компиляторы C/C++ позволяют выполнять такую трансляцию). При создании отладчиков, дизассемблеров.

  9. Собственно, отладка, дизассемблирование, исследование программ (reverse engineering).

  10. Создание файлов данных с помощью макросов и директив генерации данных.

  11. Вы не поверите, но ассемблер можно использовать и для написания обычного прикладного ПО (консольного или с графическим интерфейсом – GUI), игр, драйверов и библиотек 🙂

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

Все команды этой группы выполняют переход (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

Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:

  • Если аргументом команды mul является 1-байтовый регистр (например ), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
  • Если аргументом является регистр из 2 байт(например), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
  • Если аргументом является регистр из 4 байт(например), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.

Правила деления в 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

Рассмотрим команды ассемблера на практическом примере.

С использованием среды разработки TASMED или любого текстового редактора набираем код. Программа, задаст вопрос на английском языке о половой принадлежности (имеется ввиду ваш биологический пол при рождении). Если вы нажмете m (Man), будет выведено приветствие с мужчиной, если w (Woman), то с женщиной, после этого программа прекратит работу. Если будет нажата любая другая клавиша, то программа предположит, что имеет дело с гоблином, не поверит и будет задавать вам вопросы о половой принадлежности, пока вы не ответите верно.

;goblin.asm
.model tiny ; for СОМ
.code ; code segment start
org 100h ; offset in memory = 100h (for COM)

start: main proc
begin:
mov ah,09h
mov dx,offset prompt
int 21h
inpt:
mov ah,01h
int 21h
cmp al,’m’
je mode_man
cmp al,’w’
je mode_woman
call goblin
jmp begin
mode_man:
mov addrs,offset man; указатель на процедуру в addrs
jmp cont
mode_woman:
mov addrs,offset woman; указатель на процедуру в addrs
cont:
call word ptr addrs; косвенный вызов процедуры
mov ax,4c00h
int 21h
main endp

man proc
mov ah,09h
mov dx,offset mes_man
int 21h
ret
man endp

woman proc
mov ah,09h
mov dx,offset mes_womn
int 21h
ret
woman endp

goblin proc
mov ah,09h
mov dx,offset mes_gobl
int 21h
ret
goblin endp

;DATA
addrs dw 0;for procedure adress
prompt db ‘Are you Man or Woman [m/w]? : $’
mes_man db 0Dh,0Ah,»Hello, Strong Man!»,0Dh,0Ah,’$’ ; строка для вывода. Вместо ASCII смвола ‘$’ можно написать машинный код 24h
mes_womn db 0Dh,0Ah,»Hello, Beautyful Woman!»,0Dh,0Ah,’$’ ; строка для вывода
mes_gobl db 0Dh,0Ah,»Hello, Strong and Beautyful GOBLIN!»,0Dh,0Ah,24h ; строка для вывода. 24h = ‘$’ .
len = $ — mes_gobl
end start

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

;goblin.asm

.modeltiny; for СОМ

.code; code segment start

org100h; offset in memory = 100h (for COM)

startmainproc

begin

movah,09h

movdx,offsetprompt

int21h

inpt

movah,01h

int21h

cmpal,’m’

jemode_man

cmpal,’w’

jemode_woman

callgoblin

jmpbegin

mode_man

movaddrs,offsetman; указатель на процедуру в addrs

jmpcont

mode_woman

movaddrs,offsetwoman; указатель на процедуру в addrs

cont

callwordptraddrs; косвенный вызов процедуры

movax,4c00h

int21h

mainendp

manproc

movah,09h

movdx,offsetmes_man

int21h

ret

manendp

womanproc

movah,09h

movdx,offsetmes_womn

int21h

ret

womanendp

goblinproc

movah,09h

movdx,offsetmes_gobl

int21h

ret

goblinendp

 
;DATA

addrsdw;for procedure adress

promptdb’Are you Man or Woman [m/w]? : $’

mes_mandb0Dh,0Ah,»Hello, Strong Man!»,0Dh,0Ah,’$’; строка для вывода. Вместо ASCII смвола ‘$’ можно написать машинный код 24h

mes_womndb0Dh,0Ah,»Hello, Beautyful Woman!»,0Dh,0Ah,’$’; строка для вывода

mes_gobldb0Dh,0Ah,»Hello, Strong and Beautyful GOBLIN!»,0Dh,0Ah,24h; строка для вывода. 24h = ‘$’ .

len=$-mes_gobl

endstart

Умножение в Assembler

Для умножения чисел без знака предназначена команда MUL. У этой команды только один операнд — второй множитель, который должен находиться в регистре или в памяти. Местоположение первого множителя и результата задаётся неявно и зависит от размера операнда:

Отличие умножения от сложения и вычитания в том, что разрядность результата получается в 2 раза больше, чем разрядность сомножителей. Также и в десятичной системе — например, умножая двухзначное число на двухзначное, мы можем получить в результате максимум четырёхзначное. Запись «DX:AX» означает, что старшее слово результата будет находиться в DX, а младшее — в AX.

Если старшая часть результата равна нулю, то флаги CF и ОF будут иметь нулевое значение. В этом случае старшую часть результата можно отбросить. Это свойство можно использовать в программе, если результат должен быть такого же размера, как множители.

Если аргументом команды MUL является 1-байтовый регистр (например MUL bl), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.

Если аргументом является регистр из 2 байт(например MUL bx), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.

Если аргументом является регистр из 4 байт(например MUL ebx), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.

Непрямая адресация памяти

Обычно для этой цели используются базовые регистры 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

2.5 Константы

NASM знает четыре различных типа констант: числовые, символьные, строковые и с плавающей точкой.

2.5.1 Числовые константы

Числовая константа — это просто число. NASM позволят определять числа в различных системах счисления и различными способами: вы можете использовать суффиксы H, Q или O, и B для шестнадцатеричных, восьмеричных и двоичных чисел соответственно; можете использовать для шестнадцатеричных чисел префикс 0x в стиле С, а также префикс $ в стиле Borland Pascal. Однако имейте в виду, что префикс $ может быть также префиксом идентификаторов (см. параграф ), поэтому первой цифрой шестнадцатеричного числа при использовании этого префикса должна быть обязательно цифра, а не буква.

Некоторые примеры числовых констант:

mov ax,100 ; десятичная
mov ax,0a2h ; шестнадцатеричная
mov ax,$0a2 ; снова шестнадцатеричная: нужен 0
mov ax,0xa2 ; опять шестнадцатеричная
mov ax,777q ; восьмеричная
mov ax
,777o ; снова восьмеричная
mov ax,10010011b ; двоичная

2.5.2 Символьные константы

Символьная константа содержит от одного до четырех символов, заключенных в одиночные или двойные кавычки. Тип кавычек для NASM несущественен, поэтому если используются одинарные кавычки, двойные могут выступать в роли символа и, соответственно, наоборот. Символьная константа, содержащая более одного символа, будет загружаться в обратном порядке следования байт: если вы пишете

mov eax,’abcd’

сгенерированной константой будет не 0x61626364, а 0x64636261, поэтому если сохранить эту константу в память, а затем прочитать, получится снова abcd, но никак не dcba. Это также влияет на инструкцию CPUID Пентиумов.

2.5.3 Строковые константы

Строковые константы допустимы только в некоторых псевдо-инструкциях, а именно в семействе DB и инструкции INCBIN.
Строковые константы похожи на символьные, только длиннее. Они обрабатываются как сцепленные друг с другом символьные константы. Так, например, следующие строки кода эквивалентны.

db ‘hello’ ; строковая константа
db ‘h’,’e’,’l’,’l’,’o’ ; эквивалент из символьных констант

Следующие строки также эквивалентны:

dd ‘ninechars’ ; строковая константа в двойное слово
dd ‘nine’,’char’,’s’ ; три двойных слова
db ‘ninechars’,0,0,0 ; и действительно похоже

Обратите внимание, что когда используется db, константа типа ‘ab’ обрабатывается как строковая, хотя и достаточно коротка, чтобы быть символьной, потому что иначе db ‘ab’ имело бы тот же смысл, какой и db ‘a’, что глупо. Соответственно, трех- или четырехсимвольные константы, являющиеся операндами инструкции dw, обрабатываются также как строки

2.5.4 Константы с плавающей точкой

Константы с плавающей точкой допустимы только в качестве аргументов DW, DD, DQ и DT. Выражаются они традиционно: цифры, затем точка, затем возможно цифры после точки, и наконец, необязательная E с последующей степенью. Точка обязательна, т.к. dd 1 NASM воспримет как объявление целой константы, в то время как dd 1.0 будет воспринята им правильно.

Несколько примеров:

dw -0.5 ;
dd 1.2 ; «простое» число
dq 1.e10 ; 10,000,000,000
dq 1.e+10 ; синоним 1.e10
dq 1.e-10 ; 0.000 000 000 1
dt 3.141592653589793238462 ; число pi

В процессе компиляции NASM не может проводить вычисления над константами с плавающей точкой (это сделано с целью переносимости). Несмотря на то, что NASM генерирует код для х86 процессоров, сам по себе ассемблер может работать на любой системе с ANCI C компилятором. Само собой, ассемблер не может гарантировать присутствия устройства, обрабатывающего числа с плавающей точкой в формате Intel, поэтому стало бы необходимо включить собственный полный набор подпрограмм для работы с такими числами, что неизбежно привело бы к значительному увеличению размера самого ассемблера, хотя польза от этого была бы минимальна.

Системные вызовы Linux

В своих программах на ассемблере вы можете использовать системные вызовы Linux. Для этого нужно:

   поместить номер системного вызова в регистр EAX;

   сохранить аргументы системного вызова в регистрах EBX, ECX и т.д.;

   вызвать соответствующее прерывание (80h);

   результат обычно возвращается в регистр EAX.

Есть шесть регистров, в которых хранятся и используются аргументы необходимого системного вызова:

   EBX

   ECX

   EDX

   ESI

   EDI

   EBP

Эти регистры принимают последовательные аргументы. Если есть более шести аргументов, то ячейка памяти, содержащая первый аргумент, сохраняется в регистре EBX.

В следующем примере мы будем использовать системный вызов :

mov eax,1 ; номер системного вызова (sys_exit)
int 0x80 ; вызов ядра

1
2

moveax,1; номер системного вызова (sys_exit)

int0x80; вызов ядра

А в следующем — :

mov edx,4 ; длина сообщения
mov ecx,msg ; сообщение для вывода на экран
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра

1
2
3
4
5

movedx,4; длина сообщения

movecx,msg; сообщение для вывода на экран

movebx,1; файловый дескриптор (stdout)

moveax,4; номер системного вызова (sys_write)

int0x80; вызов ядра

Все системные вызовы перечислены в вместе с их номерами (значение, которое помещается в EAX перед вызовом ).

В следующей таблице приведены некоторые системные вызовы, которые мы будем использовать:

%eax Название %ebx %ecx %edx %esx %edi
1 sys_exit int
2 sys_fork struct pt_regs
3 sys_read unsigned int char * size_t
4 sys_write unsigned int const char * size_t
5 sys_open const char * int int
6 sys_close unsigned int

В следующей программе мы запрашиваем число, а затем выводим его на экран:

section .data ; сегмент данных
userMsg db ‘Please enter a number: ‘ ; сообщение с просьбой ввести число
lenUserMsg equ $-userMsg ; длина сообщения
dispMsg db ‘You have entered: ‘
lenDispMsg equ $-dispMsg

section .bss ; неинициализированные данные
num resb 5

section .text ; сегмент кода
global _start

_start: ; запрашиваем пользовательский ввод
mov eax, 4
mov ebx, 1
mov ecx, userMsg
mov edx, lenUserMsg
int 80h

; Считываем и сохраняем пользовательский ввод
mov eax, 3
mov ebx, 2
mov ecx, num
mov edx, 5 ; 5 байт информации
int 80h

; Выводим сообщение ‘You have entered: ‘
mov eax, 4
mov ebx, 1
mov ecx, dispMsg
mov edx, lenDispMsg
int 80h

; Выводим число пользователя
mov eax, 4
mov ebx, 1
mov ecx, num
mov edx, 5
int 80h

; Код выхода
mov eax, 1
mov ebx, 0
int 80h

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
39
40
41
42
43
44

section.data; сегмент данных

   userMsgdb’Please enter a number: ‘; сообщение с просьбой ввести число

   lenUserMsgequ$-userMsg; длина сообщения

   dispMsgdb’You have entered: ‘

   lenDispMsgequ$-dispMsg

section.bss; неинициализированные данные

   numresb5

section.text; сегмент кода

   global_start

_start; запрашиваем пользовательский ввод

   moveax,4

   movebx,1

   movecx,userMsg

   movedx,lenUserMsg

   int80h

; Считываем и сохраняем пользовательский ввод

   moveax,3

   movebx,2

   movecx,num

   movedx,5; 5 байт информации

   int80h

; Выводим сообщение ‘You have entered: ‘

   moveax,4

   movebx,1

   movecx,dispMsg

   movedx,lenDispMsg

   int80h

; Выводим число пользователя

   moveax,4

   movebx,1

   movecx,num

   movedx,5

   int80h

; Код выхода

   moveax,1

   movebx,

   int80h

Результат выполнения программы:

Деление в 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.

Режимы адресации

Большинство инструкций на языке ассемблера требуют обработки операндов. Адрес операнда предоставляет место, где хранятся данные, подлежащие обработке. Некоторые инструкции не требуют операнда, в то время как другие могут требовать один, два или три операнда. В тех случаях, когда инструкции требуется два операнда, первый операнд обычно является местом назначения, содержащий данные в регистре или в ячейке памяти, а второй — источником. Источник содержит либо данные для доставки (немедленная адресация), либо адрес (в регистре или в памяти) данных. Как правило, исходные данные остаются неизменными после операции.

Есть три основных режима адресации:

   регистровая адресация;

   прямая (или «непосредственная») адресация;

   адресация памяти.

Правила деления в 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

Адресация памяти

Когда в режиме адресации памяти операнды определены, то требуется прямой доступ к основной памяти (обычно к сегменту данных) — этот вариант является медленным. Чтобы обнаружить точное местоположение данных в памяти, нам нужен начальный адрес сегмента, который обычно находится в регистре DS, и значение смещения. Это значение смещения также называется эффективным адресом.

В режиме прямой адресации значение смещения указывается как часть инструкции (например, имя переменной). Ассемблер вычисляет значение смещения и поддерживает таблицу символов, в которой хранятся значения смещения всех переменных, используемых в программе.

При прямой адресации памяти один из операндов ссылается на ячейку памяти, а другой — на регистр. Например:

ADD BYTE_VALUE, DL ; добавляем регистр в ячейку памяти
MOV BX, WORD_VALUE ; операнд из памяти скопирован в регистр

1
2

ADDBYTE_VALUE,DL; добавляем регистр в ячейку памяти

MOVBX,WORD_VALUE; операнд из памяти скопирован в регистр

Быть или не быть?

Так, нужно ли изучать ассемблер современному программисту? Если вы уже не новичок в программировании, и у вас серьёзные амбиции, то изучение ассемблера, внутреннего устройства операционных систем и функционирования железа (особенно процессоров, памяти), а также использование различных инструментов для дизассемблирования, отладки и анализа кода полезно тем, кто хочет писать действительно эффективные программы. Иначе будет сложно в полной мере понять, что происходит «под капотом» любимого компилятора (хотя бы в общих чертах), как оптимизировать программы на любом языке программирования и какой приём стоит предпочесть. Необязательно погружаться слишком глубоко в эту тему, если вы пишете на Python или JavaScript. А вот если ваш язык – C или C++, хорошенько изучить ассемблер будет полезно.

Вместе с тем, необходимо помнить не только о «тактике», но и о «стратегии» написания кода, поэтому не менее важно изучать и алгоритмы (правильный выбор которых зачастую более важен для создания эффективных программ, нежели низкоуровневая оптимизация), шаблоны проектирования и многие другие технологии, без которых программист не может считать себя современным

Битовые операции

Мнемоника Описание Операция Флаги
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

3.2.5 TIMES: Повторение инструкций или данных

Префикс TIMES заставляет инструкцию ассемблироваться
несколько раз. Данная псевдо-инструкция отчасти представляет NASM-эквивалент
синтаксиса DUP, поддерживающегося MASM-совместимыми
ассемблерами. Вы можете написать, например

или что-то подобное; однако TIMES более разносторонняя
инструкция. Аргумент TIMES не просто числовая
константа, а числовое выражение, поэтому вы можете писать следующие вещи:

При этом будет резервироваться строго определенное пространство, начиная от
метки buffer и длиной 64 байта. Наконец, TIMES
может использоваться в обычных инструкциях, так что вы можете писать тривиальные
развернутые циклы:

Заметим, что нет никакой принципиальной разницы между times
100 resb 1
и resb 100 за исключением того,
что последняя инструкция будет обрабатываться примерно в 100 раз быстрее из-за
внутренней структуры ассемблера.

Операнд псевдо-инструкции TIMES, подобно EQU
и RESB, является критическим выражением ().

Имейте также в виду, что TIMES не применима в
макросах: причиной служит то, что TIMES обрабатывается
после макро-фазы, позволяющей аргументу TIMES содержать
выражение, подобное 64-$+buffer. Для повторения
более одной строки кода или в сложных макросах используйте директиву препроцессора
%rep.

3.3 Эффективные адреса

Эффективный адрес это любой операнд инструкции со ссылкой на память.
Эффективные адреса в NASM имеют очень простой синтаксис: они содержат выражение
(в результате вычислений которого получается нужный адрес), обрамленное квадратными
скобками. Например:

Любая другая ссылка, не соответствующая этой простой системе, для NASM недействительна,
например es:wordvar.

Более сложные эффективные адреса, когда вовлечено более одного регистра, работают
точно также:

NASM способен воспринимать алгебру таких выражений, поэтому он правильно транслирует
вещи, выглядящие на первый взгляд недопустимыми:

Некоторые варианты эффективных адресов имеют более одной ассемблерной формы;
в большинстве таких ситуаций NASM будет генерировать самую короткую из них.
Например, у нас имеются простые ассемблерные инструкции
и . NASM будет генерировать последнюю
из них, т.к. первый вариант требует дополнительно 4 байта для хранения нулевого
смещения.

NASM имеет механизм подсказок, позволяющий создавать из
и разные инструкции; это порой полезно,
т.к. например и
по умолчанию имеют разные сегментные регистры.

Несмотря на это, вы можете заставить NASM генерировать требуемые формы эффективных
адресов при помощи ключевых слов BYTE, WORD, DWORD
и NOSPLIT. Если вам нужно, чтобы
ассемблировалась со смещением в двойное слово, вместо одного байта по умолчанию,
вы можете написать . Точно также при
помощи вы можете заставить NASM
использовать байтовые смещения для небольших значений, не определяемых при первом
проходе (см. пример такого кода в ).
В особых случаях, будет кодироваться
как с нулевым байтовым смещением, а будет кодироваться с нулевым смещением в двойное слово. Обычная
форма, , будет оставлена без смещения.

NASM будет разделять на ,
т.к. это позволяет избежать использования поля смещения и сэкономить некоторое
пространство; соответственно, будет
разделено на . При помощи ключевого
слова NOSPLIT вы можете запретить такое поведение
NASM: будет буквально оттранслировано
в .

Обзор группы арифметических команд и данных

Рис. 1. Классификация
арифметических команд

Группа арифметических целочисленных команд работает с двумя типами чисел:

  • целыми двоичными числами. Числа могут иметь знаковый разряд
    или не иметь такового, то есть быть числами со знаком или без знака;
  • целыми десятичными числами.

Целые двоичные числа

Размерность целого двоичного числа может составлять 8,
16 или 32 бит. Знак двоичного числа определяется тем, как интерпретируется
старший бит в представлении числа. Это 7-й, 15-й или 31-й биты для чисел
соответствующей размерности (см. Типы данных
). При этом интересно то, что среди арифметических команд есть
всего две команды, которые действительно учитывают этот старший разряд
как знаковый, — это команды целочисленного умножения и деления imul и idiv.
В остальных случаях ответственность за действия со знаковыми числами и,
соответственно, со знаковым разрядом ложится на программиста. К этому вопросу
мы вернемся чуть позже. Диапазон значений двоичного числа зависит от его
размера и трактовки старшего бита либо как старшего значащего бита числа,
либо как бита знака числа (табл. 1). 

Таблица 1. Диапазон значений двоичных чисел

Размерность поля Целое без знака Целое со знаком
байт 0…255 –128…+127
слово 0…65 535 –32 768…+32 767
двойное слово 0…4 294 967 295 –2 147 483 648…+2 147 483 647

Это делается с использованием директив
описания данных. К примеру, последовательность описаний двоичных
чисел из сегмента данных листинга 1 (помните о принципе “младший байт по
младшему адресу”) будет выглядеть в памяти так, как показано на рис. 2.

Листинг 1. Числа с фиксированной точкой
;prg_8_1.asm
masm
model   small
stack   256
.data           ;сегмент данных
per_1   db      23
per_2   dw      9856
per_3   dd      9875645
per_4   dw      29857
.code           ;сегмент кода
main:           ;точка входа в программу
        mov     ax,@data        ;связываем регистр dx с сегментом
        mov     ds,ax   ;данных через регистр ax
exit:           ;посмотрите в отладчике дамп сегмента данных
        mov     ax,4c00h        ;стандартный выход
        int     21h
end     main    ;конец программы


Рис. 2. Дамп
памяти для сегмента данных листинга 1

Десятичные числа

  • упакованном формате — в этом формате каждый байт содержит
    две десятичные цифры. Десятичная цифра представляет собой двоичное значение
    в диапазоне от 0 до 9 размером 4 бита. При этом код старшей цифры числа
    занимает старшие 4 бита. Следовательно, диапазон представления десятичного
    упакованного числа в одном байте составляет от 00 до 99;
  • неупакованном формате — в этом формате каждый байт содержит
    одну десятичную цифру в четырех младших битах. Старшие четыре бита имеют
    нулевое значение. Это так называемая зона. Следовательно, диапазон представления
    десятичного неупакованного числа в одном байте составляет от 0 до 9.

Рис. 3. Представление
BCD-чисел

Как описать двоично-десятичные числа в программе?

Для этого можно использовать только две директивы описания
и инициализации данных — db и dt. Возможность применения только этих директив
для описания BCD-чисел обусловлена тем, что к таким числам также применим
принцип “младший байт по младшему адресу”, что, как мы увидим далее, очень
удобно для их обработки. И вообще, при использовании такого типа данных
как BCD-числа, порядок описания этих чисел в программе и алгоритм их обработки
— это дело вкуса и личных пристрастий программиста. Это станет ясно после
того, как мы ниже рассмотрим основы работы с BCD-числами. К примеру, приведенная
в сегменте данных листинга 2 последовательность описаний BCD-чисел будет
выглядеть в памяти так, как показано на рис. 4.

 
 Листинг 2. BCD-числа
;prg_8_2.asm
masm
model   small
stack   256
.data   ;сегмент данных
per_1   db      2,3,4,6,8,2
;неупакованное BCD-число 286432
per_3   dt      9875645 ;упакованное BCD-число 9875645
.code   ;сегмент кода
main:   ;точка входа в программу
        mov     ax,@data        ;связываем регистр dx с сегментом
        mov     ds,ax   ;данных через регистр ax
exit:   ;посмотрите в отладчике дамп сегмента данных
        mov     ax,4c00h        ;стандартный выход
        int     21h
end     main    ;конец программы

Рис. 4. Дамп
памяти для сегмента данных листинга 2

После столь подробного обсуждения объектов, с которыми
работают арифметические операции, можно приступить к рассмотрению средств
их обработки на уровне системы команд микропроцессора.