ГЛАВА 10
АРИФМЕТИЧЕСКИЕ КОМАНДЫ
(часть 1/7)
Основной операцией при выполнении арифметических действий в микропроцессоре является сложение, результаты которого используются для вычитания, умножения, деления и ряда других операций.
Двоичное сложение. Теоретическая часть
Результат сложения двух одноразрядных двоичных чисел содержит 2 бита: разряд суммы (sum) и разряд переноса (carry). Таблица разряда переноса идентична таблице логического И (and), а таблица разряда суммы идентична таблице сложения по модулю 2 (xor).
| A | B | C=A+B | Carry=A and B | Sum=A xor B |
|---|
| 0 | 0 | 00 | 0 | 0 |
| 0 | 1 | 01 | 0 | 1 |
| 1 | 0 | 01 | 0 | 1 |
| 1 | 1 | 10 | 1 | 0 |
Для сложения двух двоичных чисел

и

объединяем два вентиля.
Чтобы не рисовать многократно вентили AND и XOR заменим эту схему обозначением:
Теперь мы складываем уже два четырехразрядных двоичных числа:
Полусумматор можно использовать только для сложения крайнего правого столбца (нулевой разряд): 1+1=0, и 1 идет в перенос. Для правильного поразрядного сложения во втором столбце (первый разряд) нам потребуется складывать три цифры – два разряда слагаемых и перенос из предыдущего разряда. Это же относится и к остальным столбцам. Чтобы сложить три двоичные цифры модифицируем нашу схему следующим образом:
Такая схема называется полным сумматором и обозначается вот так:
Для получения суммы двух 8-разрядных чисел нам потребуются восемь полных сумматоров. На вход для переноса в нулевом разряде подадим логический 0.
Обозначим входы 8-разрядного сумматора как

и

, а выходы как

:
Соединив два 8-разрядных сумматора можно складывать 16-битные числа.
Чтобы использовать наш сумматор еще и для вычитания – входы

дополняем следующей схемой:
Если сигнал
Инверсия = 0, все восемь выходов вентилей XOR повторяют сигнал на входах. Если сигнал
Инверсия = 1, все восемь выходов вентилей XOR инвертируют входной сигнал.
Для общего управления дополняем схему нашего сумматора управляющим сигналом
Сложение/
Вычитание. Если на управляющем входе будет 0 – будет происходить сложение, если 1 – вычитание. Различие кодировок инструкций ADD/SUB и INC/DEC всего в одном бите.
Мы создали устройство для сложения/вычитания двух чисел.
Обобщаем – суммирование двоичных многоразрядных чисел производится путем сложения одноразрядных двоичных чисел

,

и переноса из младшего разряда

, образуя на выходе значения суммы

и переноса в старший разряд

. Функции

и

имеют вид

и

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

выходного сигнала

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

. В результате

, а задержка образования
n-ого разряда суммы равна:

, где

– задержка суммы одноразрядного сумматора.
При большом числе разрядов

времена

,

окажутся весьма значительными, поэтому для повышения быстродействия используют схемы с
ускоренным формированием переноса.
Сложение чисел A и B в один этап
Для этого необходимо получить функцию

для каждого разряда, используя формулу:
для первого разряда:

для второго разряда:

для третьего разряда:

для четвертого разряда:

и так далее.
Одновременное формирование значения переноса во всех разрядах приводит к усложнению формирования схемы переноса с каждым следующим разрядом.
Сложение чисел
и
в несколько этапов Получение частичной суммы

и разрядов переноса

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

и

равны, то мы получим удвоение числа

на первом шаге.
Вычитание чисел

и

произойдет аналогично сложению с той только разницей, что значение

будет заменено на дополнительный к нему код.
Команды сложения и вычитания
Команды ADD и SUB
(Сложение = “Integer ADDITION”)
(Вычитание = “Integer SUBTRACTION”)
Синтаксис команд:
ADD <DEST>,<SRC>
SUB <DEST>,<SRC>
Возможные варианты команды
ADD:
add reg,reg/mem
add reg/mem,reg
add reg/mem,imm
Возможные варианты команды
SUB:
sub reg/mem,reg
sub reg,reg/mem
sub reg/mem,imm
Семантика команды ADD: команда
ADD складывает операнд
DEST и операнд
SRC размерностью байт, слово или двойное слово и записывает результат сложения в операнд
DEST.
Псевдокод команды ADD:
Семантика команды SUB: по команде
SUB из операнда
DEST вычитается операнд
SRC и полученная разность записывается вместо операнда
DEST. Операнд
DEST и операнд
SRC имеют размерность байт, слово или двойное слово.
Псевдокод команды SUB:
Что произойдет, если полученная сумма окажется больше размерности операнда
DEST? Например: при размере операнда
DEST в 8 бит мы складываем 250 и 10, полученное число 260 (=104h=100000100b) не влезает в 8 бит, процессор отбросит самую левую единицу, а в качестве ответа будет записана искаженная сумма 04(=00000100b), но во флаг переноса
CF будет записана 1.
Это сигнал о том, что получилась неправильная сумма (если бы переноса не было, тогда
CF = 0). При сложении и вычитании меняются также флаг нуля
ZF и флаг знака
SF. Флаг нуля
ZF получает значение 1, если результат оказался нулевым, и значение 0, если результат ненулевой. Во флаг знака
SF заносится знаковый (самый левый) бит результата, он получает значение 1, если результат оказался отрицательным, и значение 0 при положительном результате. При выполнении сложения и вычитания могут также меняться флаг переполнения
OF и флаги
AF и
PF.
Пример:
| Code |
1
2
3
| 9 – 9 = 0 = 00000000b ZF=1 SF=0
8 – 9 = –1 = 11111111b ZF=0 SF=1
9 – 8 = 1 = 00000001b ZF=0 SF=0 |
|
Обратите внимание скольким кодировкам соответствует команда ADD EAX/AX/AL,2
| кодировка | команда | длина в байтах |
|---|
| 0402 | add al,2 | 2 |
| 66050200 | add ax,2 | 3 |
| 0502000000 | add eax,2 | 5 |
| 80C002 | add al,2 | 3 |
| 6681C00200 | add ax,2 | 4 |
| 81C002000000 | add eax,2 | 6 |
| 6683C00200 | add ax,2 | 5 |
| 83C002 | add eax,2 | 3 |
Подробно разберем инструкцию «ADD ECX,EBX». Эта инструкция состоит из 2 байт. Первый байт содержит базовый опкод инструкции ADD r32,r/m32 он равен 03/r – его мы находим в первоисточнике «Intel 64 and IA-32 Architectures Software Developer's Manual. Volume 2A: Instruction Set Reference, A-M». В следующем байте биты 7-6 называются Mod, биты 5-3 – Reg/Opcode, биты 2-0 – R/M. Составим код команды – в поле Reg помещаем номер регистра ECX, в поле R/M – номер EBX. У нас регистровая адресация, поэтому поле Mod равно 3 (смотри таблицу 1 «Зависимость режима адресации от поля Mod»). Получаем следующий код:
| | опкод | d | w | Mod | Reg | R/M |
|---|
| bin | 000000 | 1 | 1 | 11 | 001=ECX | 011=EBX |
| hex | 3 | 0СBh |
Пишем в файл test.asm “db 3,0CBh”, компилируем и, просматривая получившийся файл test.exe программой hiew32.exe, убеждаемся в своей правоте – на экране команда add ecx,ebx:
А теперь вспомним про бит направления d, сделаем d=0 и поменяем между собой содержимое полей Reg и R/M: получим ту же команду add ecx,ebx но в другой кодировке:
| | опкод | d | w | Mod | Reg | R/M |
|---|
| bin | 000000 | 0 | 1 | 11 | 011=EBX | 001=ECX |
| hex | 1 | 0D9h |
Маленькие хитростиДля того чтобы «спрятать» сумму

и

от посторонних глаз или сократить размеры программы, помните, что эквивалентами операции

являются:
Аналогично, чтобы «спрятать» разность

и

от посторонних глаз или сократить размеры программы, запомните эквиваленты операции

:
Команды INC и DEC
(Увеличение на 1 =
INCREMENT BY 1)
(Уменьшение на 1 =
DECREMENT BY 1)
Следующая пара команд сложения и вычитания:
Синтаксис команд:
INC <DEST>
DEC <DEST>
Возможные варианты команды
INC:
inc reg/mem
Возможные варианты команды
DEC:
dec reg/mem
Псевдокод команды INC:
Псевдокод команды DEC:
Семантика команд: команда
INC аналогична команде
ADD DEST,1, то есть увеличение значения операнда
DEST в памяти или регистре на 1, а команда
DEC аналогична команде
SUB DEST,1, то есть уменьшение значения операнда
DEST в памяти или регистре на 1 (команды
INC и
DEC не меняют значение флага переноса
CF, эти команды занимают меньше места в памяти, но так как реализованы на другой схемотехнической базе, выполняются медленнее
SUB и
ADD).
Команда DEC сохраняет состояние флага CF. Это позволяет использовать команду DEC для обновления счетчиков циклов без воздействия на состояние флагов, изменяемых под воздействием арифметических операций, используемых для управления циклом. Использование команды SUB с непосредственным значением 1 в качестве операнда выполняет уменьшение, которое обновляет значение флага CF.
Если вы используете DEC для уменьшения счетчика и должны проверять исчерпался ли счетчик (то есть, не стало ли число в цикле меньше нуля?), используйте для этого флаг SF:
| a1: ;тело цикла sub count,1 jnc a1;count<0? | a1: ;тело цикла dec count jns a1;count<0? |
Примеры:
| Assembler |
1
2
3
4
| DEC WORD PTR A; увеличить значение в переменной A на 1
INC EAX; увеличить значение в EAX на 1
MOV AL,9; поместить в регистр AL число 9
DEC AL; в результате команды DEC содержимое AL равно 8 |
|
Таблица кодировки инструкции INC на уровне битов:
| Инструкция | Код | Код | |
|---|
| увеличить значение в регистре reg на 1 | 1111 111w:11 000 reg | FEC0+rb | inc r8 |
| | | FFC0+rw | inc r16 |
| | | FFC0+rd | inc r32 |
| увеличить значение в ячейке памяти mem на 1 | 1111 111w:mod 000 r/m | FE /0 | inc m8 |
| | | FF /0 | inc m16 inc m32 |
увеличить значение в регистре reg на 1 (альтернативная кодировка) | 0100 0reg | 40+ rw | inc r16 |
| | | 40+ rd | inc r32 |
А теперь рассмотрите совмещенную таблицу кодировок инструкций INC и DEC. Не правда ли, очень похоже? Различие всего в одном бите!
| 40+rd | FEC0+rb | FFC0+rd |
|---|
| db 40h;inc eax | db 0FEh, 0C0h;inc al | db 0FFh, 0C0h;inc eax |
| db 41h;inc ecx | db 0FEh, 0C1h;inc cl | db 0FFh, 0C1h;inc ecx |
| db 42h;inc edx | db 0FEh, 0C2h;inc dl | db 0FFh, 0C2h;inc edx |
| db 43h;inc ebx | db 0FEh, 0C3h;inc bl | db 0FFh, 0C3h;inc ebx |
| db 44h;inc esp | db 0FEh, 0C4h;inc ah | db 0FFh, 0C4h;inc esp |
| db 45h;inc ebp | db 0FEh, 0C5h;inc ch | db 0FFh, 0C5h;inc ebp |
| db 46h;inc esi | db 0FEh, 0C6h;inc dh | db 0FFh, 0C6h;inc esi |
| db 47h;inc edi | db 0FEh, 0C7h;inc bh | db 0FFh, 0C7h;inc edi |
| 48+rd | FEC8+rb | FFC8+rd |
|---|
| db 48h;dec eax | db 0FEh, 0C8h;dec al | db 0FFh, 0C8h;dec eax |
| db 49h;dec ecx | db 0FEh, 0C9h;dec cl | db 0FFh, 0C9h;dec ecx |
| db 4Ah;dec edx | db 0FEh, 0CAh;dec dl | db 0FFh, 0CAh;dec edx |
| db 4Bh;dec ebx | db 0FEh, 0CBh;dec bl | db 0FFh, 0CBh;dec ebx |
| db 4Ch;dec esp | db 0FEh, 0CCh;dec ah | db 0FFh, 0CCh;dec esp |
| db 4Dh;dec ebp | db 0FEh, 0CDh;dec ch | db 0FFh, 0CDh;dec ebp |
| db 4Eh;dec esi | db 0FEh, 0CEh;dec dh | db 0FFh, 0CEh;dec esi |
| db 4Fh;dec edi | db 0FEh, 0CFh;dec bh | db 0FFh, 0CFh;dec edi |
Мы увеличивали содержание регистра общего назначения на единицу с помощью опкода 40h, к которому прибавлялся номер регистра:
40+ rd INC r32
«+rd» означает, что номер регистра прибавляется к байту справа от знака «+». Это соответствует формуле «базовый опкод + номер регистра», которую мы использовали в главе «Команды передачи данных». Если нужно увеличить на 1 не регистр, а ячейку памяти, на которую этот регистр указывает (например [eax]), тогда ищем нужную строчку в описании инструкции INC и находим:
FF /0 INC r/m32;
Increment r/m double word by 1
Зависимость режима адресации от поля Mod
| Mod | Адрес |
|---|
| 0 | [Reg] |
| 1 | [Reg+8-битное смещение] |
| 2 | [Reg+32-битное смещение] |
| 3 | Reg |
Таблица 10.2.1«/0» говорит о том, что поле Reg в ModR/M равно 0. «r/m32» задаёт либо регистр, либо ячейку памяти размером в двойное слово, используя поля ModR/M и, возможно, SIB. Зная это, несложно составить код:
| | опкод | w | Mod | Reg/Opcode | R/M |
|---|
| bin | 1111111 | 1 | 00 | 000 | 000=[EAX] |
| hex | FF | 00 |
«INC» можно кодировать также и с помощью опкода «FF /0» :
inc eax
| | опкод | w | Mod | Reg/Opcode | R/M |
|---|
| bin | 1111111 | 1 | 11 | 000 | 000=EAX |
| hex | FF | C0 |
| Эквивалент inc eax | Эквивалент dec eax |
|---|
| add eax,1 | sub eax,1 |
| sub eax,0FFFFFFFFh | add eax,0FFFFFFFFh |
| lea eax,[eax+1] | lea eax,[eax+0FFFFFFFFh] |
По размеру команда
INC REG занимает 1 байт, а команда
ADD REG,imm8 там, где imm имеет значение от –128 до 127 (то есть там, где imm может получить знаковое расширение) – 3 байта, поэтому, с точки зрения размера,
INC REG/INC REG (2 байта) предпочтительнее
ADD REG,2 (3 байта), а команды
DEC REG/DEC REG (2 байта) предпочтительнее
SUB REG,2 (3 байта).
Последовательное применение команд
NEG X и
NOT X эквивалентно команде
DEC X. Последовательное применение
NOT X и
NEG X эквивалентно команде
INC X. Таким образом, повторно используя
NOT и
NEG, можно скрытно добавить или вычесть из
X некоторую константу.
Для скрытного увеличения/уменьшения содержимого регистра DI/EDI используйте команды SCASB/SCASW/SCASD или STOSB/STOSW/STOSD. Для скрытного увеличения/уменьшения содержимого регистра SI/ESI используйте команду LODSB/LODSW/LODSD. Для одновременного увеличения/уменьшения SI/ESI и DI/EDI – команды MOVSB/MOVSW/MOVSD и CMPSB/CMPSW/CMPSD.
Для скрытного увеличения/уменьшения содержимого регистра SP/ESP используйте команды PUSH/POP PUSHF/POPF PUSHA/POPA.
Команда изменение знака NEG
(Отрицательный = «NEGATIVE»)
Еще одна команда из группы сложения и вычитания.
Синтаксис команды:
NEG <DEST>
Возможные варианты команды:
neg reg/mem
Команда NEG рассматривает операнд DEST как число со знаком и меняет этот знак на противоположный.
Псевдокод команды NEG:
| Assembler |
1
2
| mov ah,2
neg ah ; ah=-2 (0FEh) |
|
Четыре особых случая для команды NEG:
- если операнд равен –128=–28 (80h), в этом случае операнд не меняется, так как нет знакового числа +128 величиною в байт. CF=1, ZF=0, OF=1, SF=1. В этом случае флаг OF получает значение 1 (при других операндах OF=0).
- для числа размером в слово. Если значение операнда равно минимальному отрицательному числу -32768=–216 (8000h), то команда не меняет операнд. CF=1, ZF=0, OF=1, SF=1. В этом случае флаг OF получает значение 1 (при других операндах OF=0).
- для числа размером в двойное слово. Если значение операнда равно минимальному отрицательному числу -2147483648=–232 (80000000h), то команда не меняет операнд. CF=1, ZF=0, OF=1, SF=1. В этом случае флаг OF получает значение 1 (при других операндах OF=0).
- если операнд равен 0 команда NEG не меняет операнд. Флаг CF=0, ZF=1, OF=0, SF=0, PF=0. В этом особом случае флаг CF получает значение 0, а флаг ZF значение 1 (при других операндах CF=1 ZF=0).
| | OF | SF | ZF | AF | PF | CF | Комментарий |
|---|
mov al,80h neg al | 1 | 1 | 0 | 0 | 0 | 1 | (1)00h-80h=80h (займ) 0-(-128)=-128 (не верно) |
mov al,0 neg al | 0 | 0 | 1 | 0 | 1 | 0 | 0-0=0 0-0=0 (верно) |
mov al,-5 neg al | 0 | 0 | 0 | 1 | 1 | 1 | (1)00h-FBh=5(займ) 0-(-5)=5 (верно) |
mov al,50h neg al | 0 | 1 | 0 | 0 | 0 | 1 | (1)00h-50h=0B0h(займ) 0-80 =-80 (верно) |
При ненулевых значениях операнда после действия команды NEG ZF=0, при вычитании любого числа из 0 возникнет займ – CF=1, если число не кратно 10h – AF=1. Флаг SF меняется как обычно – если операнд был положительным, то после действия команды NEG SF=1, если операнд был отрицательным – SF=0.
Последовательность команд эквивалентная команде
NEG EAX
| imul eax,–1 | xor eax,–1 inc eax | not eax inc eax | xor ebx,ebx sub ebx,eax xchg eax,ebx | dec eax not eax |
Получение модуля числа:
| вариант 1 | вариант 2 | вариант 3 | вариант 4 | вариант 5 | вариант 6 | вариант 7 | вариант 8 |
|---|
| a1: neg eax | cdq | cdq | ;если нельзя использовать edx | cdq | xor edx,edx | mov ecx,eax | cdq |
| js a1 | xor eax,edx | mov ebx,eax | mov ecx,eax | or edx,1 | sub edx,eax | neg ecx | add eax,edx |
| | sub eax,edx | add ebx,ebx | sar ecx,31 | imul edx | cmovns eax,edx | cmovs eax,ecx | xor eax,edx |
| | | and ebx,edx | xor eax,ecx | | | | |
| | | sub eax,ebx | sub eax,ecx | | | | |
Небольшой подводный камень, который нужно учитывать при получении модуля - если при работе с байтами/словами/двойными словами вам попадет число 80h/8000h/80000000h - тогда все вышеперечисленные варианты получения модуля, кроме варианта с умножением, где произойдет знаковое расширение числа на два регистра (

,

,

), приведут к появлению в качестве модуля числа 80h/8000h/ 80000000h с установкой флагов SF=1 и OF=1, что в варианте
neg/js приведет к зацикливанию, а остальные варианты выдадут неправильный модуль. Отсюда мораль - либо перед вычислением модуля делать проверку на наличие такого числа, либо предварительно делать знаковое расширение, либо обрабатывать появление флага OF, либо, если точность вычисления позволяет, а знаковое расширение сделать нельзя - увеличьте число 80h/8000h/80000000h на единицу.
| Assembler |
1
2
3
4
5
| cdq
xor eax,edx;если eax<0 тогда edx=0FFFFFFFFh иначе edx:=0
sub eax,edx;если edx=0FFFFFFFFh тогда eax:=-eax
cdq; если eax=80000000h и только в этом случае
xor eax,edx; eax:=7FFFFFFFh |
|
Если проверка не делается, тогда замените сочетание neg/js на neg/jl ? команда jl осуществит переход только при SF=1 и OF=0. Хотя команда jl может сработать при сочетании SF=0 и OF=1, но после neg такой комбинации флагов быть не может.