Регистры процессоров ограничены в разрядности архитектурой процессора (как правило, x86/64), поэтому сложение чисел, как из реального мира, так и из виртуального в ассемблере имеет дополнительные нюансы. В предыдущем Уроке 9. Сложение и вычитание операции сложения и вычитания выполнялись только в границах размеров регистров.
Сложение операндов превышающих размеры регистров приводит к переполнению и установке флага CF. Команда ADC предназначена для сложения с учетом флага CF, а команда SBB вычитает с учетом флага CF. Данные инструкции корректно складывают и вычитают любые целые числа, превышающие вместимость регистров процессора (16 бит в рамках курса).
Алгоритм таких арифметических операции прост:
- в коде программы командами ADD и SUB сначала складываются или вычитаются младшие разряды чисел
- затем командами ADC и SBB складываются или и вычитаются старшие части
Команды ADC и SBB используют значение флага CF, куда записываются перенос из старшего разряда, что позволяет складывать числа любой длины по частям. Представьте эту процедуру как сложение и вычитание чисел столбиком.
Обратите внимание на иллюстрацию, показывающую операцию сложения 16 битных чисел (с использованием ADD):
Обратите внимание, произошел перенос из младшей части результата (старший 7-й бит) в старшую часть (младший 8-й бит). Если бы мы были ограничены разрядностью процессора в 8 бит, то складывая эти числа по частям, сначала младшую, а затем старшую часть, и оба раза командой ADD, мы потеряли бы этот вынесенный бит и получили некорректный результат. Ситуацию исправляет флаг CF, который хранит перенос из старшего разряда. Поэтому нам нужно использовать для сложения старшей части только команду ADC:
Ровно так же обстоит дело с вычитанием чисел превышающих размеры регистров. Для наглядности рассмотрим на примере программы, которая выполнит вычисление формулы x = k + l – m + 1, где операнды x, k, l и m являются беззнаковыми 32-битными целыми числами. Как вы уже поняли, мы будем складывать и вычитать их с учетом флага переноса, то есть в два подхода: первыми выполнив команды над младшими разрядами чисел, и завершив вычисления командами ADC и SBB с учётом флага переноса.
Обратите внимание, что в контексте этой программы нельзя использовать команду INC, поскольку INC не влияет на флаг переноса! К примеру, у нас беззнаковое десятичное число 65535 в регистре AX, то, сделав INC AX, мы получим 0 в AX, а флаг CF не изменится!
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 | use16 ;Генерировать 16-битный код org 100h ;Программа начинается с адреса 100h mov ax,word[k] ;Загружаем младшую часть k в AX mov bx,word[k+2] ;Загружаем старшую часть k в BX add ax,word[l] ;Складываем младшие части k и l adc bx,word[l+2] ;Складываем старшие части k и l sub ax,word[m] sbb bx,word[m+2] ;BX:AX = k+l-m add ax,1 ;Команда INC здесь не подходит! adc bx,0 ;BX:AX = k+l-m+1 mov word[x],ax ;\ mov word[x+2],bx ;/ Сохраняем результат в x mov ax,4C00h ;\ int 21h ;/ Завершение программы ;------------------------------------------------------- k dd 120000 l dd 80500 m dd 2300 x dd ? |
Директива word в коде программы ограничивает размер операнда до 16 бит. Первым в памяти хранится младший разряд, поэтому для чтения старшего разряда нужно сместиться на 2 байта вправо. В нашем случае word[k] обращается к младшему разряду операнда k, а word[k+2] к старшему.
Рассмотрим, как в отладчике представлены переменные k, l, m и x:
Процессоры Intel имеют особенный порядок представления значений в памяти. Первым в памяти (по младшему адресу) идет младший байт числа (little endian). Вправо по старшенству идут остальные байты числа. То есть в окошке дампа значения правильнее читать справа налево. В отличие от дампа памяти значения в регистрах показаны в привычном порядке. Для наглядности приведены значения операнда k в окне дампа и в регистрах (старший разряд в BX, а младший в AX).
«Плюшка» ассемблера в том, что программист не ограничен в длине целых чисел. То есть складывать и вычитать можно сколь угодно длинные числа. В противоположность к языкам высокого уровня, где размеры переменных, как правило, ограничены компилятором. От слов к делу, чтобы наглядно показать сложение двух 7-байтных числа (с использованием только одного регистра процессора):
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 | use16 ;Генерировать 16-битный код org 100h ;Программа начинается с адреса 100h mov ax,word[x] add ax,word[y] mov word[z],ax mov ax,word[x+2] adc ax,word[y+2] mov word[z+2],ax mov ax,word[x+4] adc ax,word[y+4] mov word[z+4],ax mov al,byte[x+6] adc al,byte[y+6] mov byte[z+6],al mov ax,4C00h ;\ int 21h ;/ Завершение программы ;------------------------------------------------------- x dd 0xF1111111 dw 0xF111 db 0x11 y dd 0x22222222 dw 0x2222 db 0x22 z rb 7 |
Чтобы обратиться к самому старшему байту значения используется директива byte[x+6]. Думаю, здесь вы уже поняли, что к чему Использование команды MOV безопасно между командами сложения или вычитания, поскольку команда MOV не затрагивает флаги процессора.
Упражнение
Внимательно прочитали материал? Отлично! Самое время выполнить упражнение к уроку — вычислить формулу x = k — 1 + n — m. Объявите все операнды как 3-х байтные значения без знака. Скомпилируйте программу без ошибок и проверьте в отладчике Tourbo Debugger. Разместите результат в комментариях к этому уроку.