Деление на ассемблере: Деление и умножение в Assembler
Содержание
Деление и умножение в Assembler
Здравствуйте, уважаемые друзья! Продолжаем изучать нашу рубрику, на очереди тема умножения и деления в Assembler. Разберемся со всеми тонкостями этих операций, конечно же, на практическом примере.
Основные команды
- Для умножения в Assembler используют команду
mul
- Для деления в Assembler используют команду
div
Правила умножения в Assembler
Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:
- Если аргументом команды mul является 1-байтовый регистр (например
mul bl
), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.bl*al = ax
- Если аргументом является регистр из 2 байт(например
mul bx
), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.bx*ax = eax
- Если аргументом является регистр из 4 байт(например
mul ebx
), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.ebx*eax = 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
Программа
Далее перейдем к примеру: он не должен вызвать у вас каких либо затруднений, если вы читали наши предыдущие статьи, особенно важна статья про вывод на экран, советую вам с ней ознакомиться. Ну а мы начнем:
.386 .model flat,stdcall option casemap:none include ..\INCLUDE\kernel32.inc include ..\INCLUDE\user32.inc includelib ..\LIB\kernel32.lib includelib ..\LIB\user32.lib BSIZE equ 15 .data ifmt db "%d", 0 ;строка формата stdout dd ? cWritten dd ? CRLF WORD ? .data? buf db BSIZE dup(?) ;буфер
Стандартное начало, в котором мы подключаем нужные нам библиотеки и объявляем переменные для вывода чисел на экран. Единственное о чем нужно сказать: новый для нас раздел .data? Знак вопроса говорит о том, что память будет выделяться на этапе компилирования и не будет выделяться в самом исполняемом файле с расширением .exe (представьте если бы буфер был большего размера) . Такое объявление — грамотное с точки зрения программирования.
.code start: invoke GetStdHandle, -11 mov stdout,eax mov CRLF, 0d0ah ;-------------------------деление mov eax, 99 mov edx, 0 mov ebx, 3 div ebx invoke wsprintf, ADDR buf, ADDR ifmt, eax invoke WriteConsoleA, stdout, ADDR buf, BSIZE, ADDR cWritten, 0 invoke WriteConsoleA, stdout, ADDR CRLF, 2, ADDR cWritten,0
В разделе кода, уже по традиции, считываем дескриптор экрана для вывода и задаем значения для перевода каретки. Затем помещаем в регистры соответствующие значения и выполняем деление регистра ebx, как оно реализуется описано чуть выше. Думаю, тут понятно, что мы просто делим число 99 на 3, что получилось в итоге выводим на экран консоли.
;-------------------------умножение mov bx, 4 mov ax, 3 mul bx invoke wsprintf, ADDR buf, ADDR ifmt, eax invoke WriteConsoleA, stdout, ADDR buf, BSIZE, ADDR cWritten, 0 invoke ExitProcess,0 end start
Думаю, что здесь тоже все понятно и без комментариев. Как производиться умножение в Assembler вы тоже можете прочитать чуть выше, ну и результат выводим на экран.
Просмотр консоли
Этот код я поместил в файл seventh.asm, сам файл поместил в папку BIN (она появляется при установке MASM32). Далее открыл консоль, как и всегда, с помощью команды cd
перешел в эту папку и прописал amake.bat seventh
. Скомпилировалось, затем запускаю исполняемый файл и в консоли получаются такие числа:
Как видите, мы правильно посчитали эти операции.
На этом сегодня все! Надеюсь вы научились выполнять деление и умножение на Assembler.
Скачать исходники
Поделиться ссылкой:
Похожее
Assembler: div & mul. Приветствуем всех! С вами команда IT… | by ITRoot Corp
Деление в 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.
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
Команда DIV
|
ТВ онлайн: более 100 каналов в HD-качестве 1) Большинство каналов бесплатные. 2) Можно смотреть как на сайте, так и в приложениях для iOS, Android и Smart TV. 3) Множество телесериалов и фильмов бесплатно или за 1 рубль. 4) Несколько платных подписок, в том числе 18+ и телеканал «Дождь». 5) А также записи телепередач, новости, ТВ-шоу и др.
|
Инструкция DIV в Ассемблере выполняет деление без знака. Использование этой инструкции
похоже на работу команды MUL, хотя, конечно, имеет некоторые особенности, потому что деление — это не умножение )))
Итак, синтаксис команды DIV такой:
DIV ЧИСЛО
ЧИСЛОМ может быть один из следующих:
- Область памяти (MEM)
- Регистр общего назначения (REG)
Эта команда не работает с сегментными регистрами, а также не работает непосредственно с числами. То есть вот так
DIV 200 ; неправильно
делать нельзя.
А теперь алгоритм работы команды DIV:
- Если ЧИСЛО — это БАЙТ, то AL = AX / ЧИСЛО
- Если ЧИСЛО — это СЛОВО, то AX = (DX AX) / ЧИСЛО
Если вы уже изучили инструкцию MUL, то ничего особо нового для вас здесь нет. Ну а если не изучали, то немного напомню.
Обратите внимание, что инструкция DIV работает либо с регистром АХ, либо с парой
регистров DX AX. То есть перед выполнением этой команды нам надо записать в регистр АХ
или пару регистров DX AX значение, которое требуется разделить. Сделать это можно,
например, с помощью уже известной нам команды MOV.
Затем надо в область памяти или в регистр общего назначения записать делитель — то есть число, на которое будем делить.
Далее мы выполняем деление, и получаем результат либо в регистр АL (если ЧИСЛО — это байт), либо в регистр AX (если ЧИСЛО — это слово).
Остаток от деления
Как вы понимаете, инструкция DIV выполняет целочисленное деление. При этом остаток от деления, если таковой имеется, будет записан:
- В регистр АН, если ЧИСЛО — это байт
- В регистр DX, если ЧИСЛО — это слово
Никакие флаги при этом не изменяются. А если и меняются, то об этом ничего не сказано в документации, следовательно, проверять флаги нет необходимости.
Просто если есть сомнения, что деление выполнено без остатка, надо проверить содержимое регистров AL или DX в зависимости от того, какой размер имеет ЧИСЛО.
Пример деления в Ассемблере
Итак, например, нам надо 250 разделить на 150. Тогда мы делаем так:
MOV AX, 250 ; Делимое в регистр AX MOV BL, 150 ; Делитель в регистр BL DIV BL ; Теперь АL = 250 / 150 = 1, AH = 100
Обратите внимание, что нам приходится два раза использовать команду MOV, так как команда DIV не работает непосредственно с числами, а только с регистрами общего назначения или с памятью.
После выполнения этого кода в регистре АL будет результат целочисленного деления
числа 250 на число 150, то есть число 1, а в регистре АН будет остаток от
деления — число 100 (64 в шестнадцатеричной системе).
Теперь попробуем число 50000000 разделить на 60000.
MOV DX, 762 ; Делимое - в пару регистров DX AX MOV AX, 61568 ; (DX AX) = 50000000 MOV BX, 60000 ; Делитель в регистр BX DIV BX ; Теперь АХ = 50000000 / 60000 = 833 (341h) ; DX = 20000 (4E20h)
Для записи делителя в пару регистров DX и AX используются две команды MOV. В нашем
примере в регистр DX будет записано число 762 (02FA — в шестнадцатеричной системе),
а в регистр АХ — число 61568 (F080 — в шестнадцатеричной системе). А если рассматривать
их как одно число (двойное слово), где в старшем слове 762, а в младшем — 61568, то это и
будет 50000000 (2FAF080 — в шестнадцатеричной системе).
Затем в регистр BX мы записываем число 60000 и выполняем команду деления. В результате в регистре АХ будет число 833 (или 341 в шестнадцатеричной системе), в регистре DX — остаток от деления, который в нашем случае будет равен 20000 (или 4E20 в шестнадцатеричной системе).
В конце как обычно расскажу, почему эта команда ассемблера называется DIV.
Это сокращение от английского слова DIVIDE, которое можно перевести как “разделить”.
|
Первые шаги в программирование Главный вопрос начинающего программиста – с чего начать? Вроде бы есть желание, но иногда «не знаешь, как начать думать, чтобы до такого додуматься».
|
Деление в ассемблере примеры | Gadget-apple.ru
Что такое JavaScript |
Если вы интересуетесь программированием вообще, и сайтостроением в частности, то вы наверняка слышали слово JavaScript. И, если вы до сих пор не узнали толком, что же это такое, то пришло время сделать это. Подробнее.
Инструкция DIV в Ассемблере выполняет деление без знака. Использование этой инструкции похоже на работу команды MUL, хотя, конечно, имеет некоторые особенности, потому что деление — это не умножение )))
Итак, синтаксис команды DIV такой:
ЧИСЛОМ может быть один из следующих:
- Область памяти (MEM)
- Регистр общего назначения (REG)
Эта команда не работает с сегментными регистрами, а также не работает непосредственно с числами. То есть вот так
DIV 200 ; неправильно
А теперь алгоритм работы команды DIV:
- Если ЧИСЛО — это БАЙТ, то AL = AX / ЧИСЛО
- Если ЧИСЛО — это СЛОВО, то AX = (DX AX) / ЧИСЛО
Если вы уже изучили инструкцию MUL, то ничего особо нового для вас здесь нет. Ну а если не изучали, то немного напомню.
Обратите внимание, что инструкция DIV работает либо с регистром АХ, либо с парой регистров DX AX. То есть перед выполнением этой команды нам надо записать в регистр АХ или пару регистров DX AX значение, которое требуется разделить. Сделать это можно, например, с помощью уже известной нам команды MOV.
Затем надо в область памяти или в регистр общего назначения записать делитель — то есть число, на которое будем делить.
Далее мы выполняем деление, и получаем результат либо в регистр АL (если ЧИСЛО — это байт), либо в регистр AX (если ЧИСЛО — это слово).
Остаток от деления
Как вы понимаете, инструкция DIV выполняет целочисленное деление. При этом остаток от деления, если таковой имеется, будет записан:
- В регистр АН, если ЧИСЛО — это байт
- В регистр DX, если ЧИСЛО — это слово
Никакие флаги при этом не изменяются. А если и меняются, то об этом ничего не сказано в документации, следовательно, проверять флаги нет необходимости.
Просто если есть сомнения, что деление выполнено без остатка, надо проверить содержимое регистров AL или DX в зависимости от того, какой размер имеет ЧИСЛО.
Пример деления в Ассемблере
Итак, например, нам надо 250 разделить на 150. Тогда мы делаем так:
Обратите внимание, что нам приходится два раза использовать команду MOV, так как команда DIV не работает непосредственно с числами, а только с регистрами общего назначения или с памятью.
После выполнения этого кода в регистре АL будет результат целочисленного деления числа 250 на число 150, то есть число 1, а в регистре АН будет остаток от деления — число 100 (64 в шестнадцатеричной системе).
Теперь попробуем число 50000000 разделить на 60000.
Для записи делителя в пару регистров DX и AX используются две команды MOV. В нашем примере в регистр DX будет записано число 762 (02FA — в шестнадцатеричной системе), а в регистр АХ — число 61568 (F080 — в шестнадцатеричной системе). А если рассматривать их как одно число (двойное слово), где в старшем слове 762, а в младшем — 61568, то это и будет 50000000 (2FAF080 — в шестнадцатеричной системе).
Затем в регистр BX мы записываем число 60000 и выполняем команду деления. В результате в регистре АХ будет число 833 (или 341 в шестнадцатеричной системе), в регистре DX — остаток от деления, который в нашем случае будет равен 20000 (или 4E20 в шестнадцатеричной системе).
В конце как обычно расскажу, почему эта команда ассемблера называется DIV. Это сокращение от английского слова DIVIDE, которое можно перевести как «разделить».
Здравствуйте, уважаемые друзья! Продолжаем изучать нашу рубрику, на очереди тема умножения и деления в Assembler. Разберемся со всеми тонкостями этих операций, конечно же, на практическом примере.
Основные команды
- Для умножения в Assembler используют команду mul
- Для деления в Assembler используют команду div
Правила умножения в Assembler
Итак, как мы уже сказали, при умножении и делении в Assembler есть некоторые тонкости, о которых дальше и пойдет речь. Тонкости эти состоят в том, что от того, какой размерности регистр мы делим или умножаем многое зависит. Вот примеры:
- Если аргументом команды mul является 1-байтовый регистр (например mul bl ), то значение этого регистра bl умножится на значение регистра al, а результат запишется в регистр ax, и так будет всегда, независимо от того, какой 1-байтовый регистр мы возьмем.
bl*al = ax - Если аргументом является регистр из 2 байт(например mul bx ), то значение в регистре bx умножится на значение, хранящееся в регистре ax, а результат умножения запишется в регистр eax.
bx*ax = eax - Если аргументом является регистр из 4 байт(например mul ebx ), то значение в регистре ebx умножится на значение, хранящееся в регистре eax, а результат умножения запишется в 2 регистра: edx и eax.
ebx*eax = 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
Программа
Далее перейдем к примеру: он не должен вызвать у вас каких либо затруднений, если вы читали наши предыдущие статьи, особенно важна статья про вывод на экран, советую вам с ней ознакомиться. Ну а мы начнем:
Стандартное начало, в котором мы подключаем нужные нам библиотеки и объявляем переменные для вывода чисел на экран. Единственное о чем нужно сказать: новый для нас раздел .data? Знак вопроса говорит о том, что память будет выделяться на этапе компилирования и не будет выделяться в самом исполняемом файле с расширением .exe (представьте если бы буфер был большего размера) . Такое объявление — грамотное с точки зрения программирования.
В разделе кода, уже по традиции, считываем дескриптор экрана для вывода и задаем значения для перевода каретки. Затем помещаем в регистры соответствующие значения и выполняем деление регистра ebx, как оно реализуется описано чуть выше. Думаю, тут понятно, что мы просто делим число 99 на 3, что получилось в итоге выводим на экран консоли.
Думаю, что здесь тоже все понятно и без комментариев. Как производиться умножение в Assembler вы тоже можете прочитать чуть выше, ну и результат выводим на экран.
Просмотр консоли
Этот код я поместил в файл seventh.asm, сам файл поместил в папку BIN (она появляется при установке MASM32). Далее открыл консоль, как и всегда, с помощью команды cd перешел в эту папку и прописал amake.bat seventh . Скомпилировалось, затем запускаю исполняемый файл и в консоли получаются такие числа:
Как видите, мы правильно посчитали эти операции.
На этом сегодня все! Надеюсь вы научились выполнять деление и умножение на Assembler.
Мнемониками DIV , IDIV записывают команды процессору, обозначающие соответственно беззнаковое деление и деление чисел со знаком.
DIV делитель IDIV делитель
где делитель — это 8− , 16− , 32-битный регистр или 8− , 16− , 32-битная переменная.
Местоположение делимого в командах DIV , IDIV не указывается, оно жестко определено и зависит от размерности (байт, слово, двойное слово) делителя. По этой причине делителем не может быть явно заданное (в команде) число, так как явно заданное число не имеет размерности.
Делимое должно быть помещено:
в AX — если делитель имеет размерность байт, тогда после деления частное находим в регистре AL , остаток от деления — в регистре AH ;
в DX:AX — если делитель имеет размерность слово (2 байта), тогда после деления частное находим в регистре AX , остаток от деления — в регистре DX ;;
в EDX:EAX — если делитель имеет размерность двойное слово (4 байта), тогда после деления частное находим в регистре EAX , остаток от деления — в регистре EDX .
При делении на слово или двойное слово делимое должно быть «расписано» на два регистра. Запись DX:AX означает делимое в виде двойного слова, два старших байта которого помещены в DX , два младших байта — в AX . Запись EDX:EAX означает делимое в виде учетверенного слова (8 байт), четыре старших байта которого помещены в EDX , четыре младших байта — в EAX .
В беззнаковом делении, если делимое не имеет разрядов в той части, которая должна быть помещена в DX или EDX , регистр DX или EDX просто обнуляют. В знаковом делении используют команды cbw , cwd , cdq , при выполнении которых все старшие биты заполняются нулями — для положительных чисел, или единицами — для отрицательных чисел. Команда cbw знаково преобразует (расширяет) содержимое регистра AL до AX , команда cwd преобразует AX до DX:AX , команда cwde преобразует AX до EAX , команда cdq преобразует EAX до EDX:EAX .
В случае превышения (overflow) результатом деления размера того регистра, куда должен быть помещен этот результат, дальнейшее выполнение программы прерывается операционной системой. При беззнаковом делении на байт максимально допустимое частное составляет 255, при делении на слово — 65535. Ввиду малого диапазона обрабатываемых чисел и самого результата, деление на байт или слово, как операция, поддерживаемая современными процессорами Intel, имеет значение, в основном, для обеспечения совместимости с предыдущими версиями процессоров и соответствующими (устаревшими) программами. Однако, чем меньше размерность делителя, тем быстрее выполняется команда, и деление на байт и слово могут и теперь (при подходящих условиях) сослужить пользу в алгоритмах с большими объемами вычислений и критичных ко времени исполнения. Так, для 386-процессоров выполнение деления на двойное слово требует 38 тактов процессора, на слово — 22 такта, на байт — 14 тактов.
Примеры приведены ниже. О функциях PrintLine, PrintText, PrintDec см. VKDEBUG
НАШ САЙТ РЕКОМЕНДУЕТ:
Делись, рыбка, быстро и нацело / Хабр
Деление — одна из самых дорогих операций в современных процессорах. За доказательством далеко ходить не нужно: Agner Fog[
1
] вещает, что на процессорах Intel / AMD мы легко можем получить Latency в 25-119 clock cycles, а reciprocal throughput — 25-120. В переводе на Русский —
МЕДЛЕННО
! Тем не менее, возможность избежать инструкции деления в вашем коде — есть. И в этой статье, я расскажу как это работает, в частности в современных компиляторах(они то, умеют так уже лет 20 как), а также, расскажу как полученное знание можно использовать для того чтобы сделать код лучше, быстрее, мощнее.
Собственно, я о чем: если делитель известен на этапе компиляции, есть возможность заменить целочисленное деление умножением и логическим сдвигом вправо (а иногда, можно обойтись и без него вовсе — я конечно про реализацию в Языке Программирования). Звучит весьма обнадеживающе: операция целочисленного умножения и сдвиг вправо на, например, Intel Haswell займут не более 5 clock cycles. Осталось лишь понять, как, например, выполняя целочисленное деление на 10, получить тот же результат целочисленным умножением и логическим сдвигом вправо? Ответ на этот вопрос лежит через понимание… Fixed Point Arithmetic (далее FPA). Чуть-чуть основ.
При использовании FP, экспоненту (показатель степени 2 => положение точки в двоичном представлении числа) в числе не сохраняют (в отличие от арифметики с плавающей запятой, см. IEE754), а полагают ее некой оговоренной, известной программистам величиной. Сохраняют же, только мантиссу (то, что идёт после запятой). Пример:
0.1 — в двоичной записи имеет ‘бесконечное представление’, что в примере выше отмечено круглыми скобками — именно эта часть будет повторяться от раза к разу, следуя друг за другом в двоичной FP записи числа 0.1.
В примере выше, если мы используем для хранения FP чисел 16-битные регистры, мы не сможем уместить FP представление числа 0.1 в такой регистр не потеряв в точности, а это в свою очередь скажется на результате всех дальнейших вычислений в которых значение этого регистра участвует.
Пусть дано 16-битное целое число A и 16-битная Fraction часть числа B. Произведение A на B результатом дает число с 16 битами в целой части и 16-тью битами в дробной части. Чтобы получить только целую часть, очевидно, нужно сдвинуть результат на 16 бит вправо.
Поздравляю, вводная часть в FPA окончена.
Формируем следующую гипотезу: для выполнения целочисленного деления на 10, нам нужно выполнить умножение Числа Делимого на FP представление числа 0.1, взять целую часть и дело в шля… минуточку… А будет ли полученный результат точным, точнее его целая часть? — Ведь, как мы помним, в памяти у нас хранится лишь приближенная версия числа 0.1. Ниже я выписал три различных представления числа 0.1: бесконечно точное представление числа 0.1, обрезанное после 16-ого бита без округления представление числа 0.1 и обрезанное после 16 ого бита с округлением вверх представление числа 0.1.
Оценим погрешности truncating представлений числа 0.1:
Чтобы результат умножения целого числа A, на Аппроксимацию числа 0.1 давал точную целую часть, нам нужно чтобы:
, либо
Удобнее использовать первое выражение: при
мы всегда получим тождество (но, заметь, не все решения, что в рамках данной задачи — более чем достаточно). Решая, получаем
. То есть, умножая любое 14-битное число A на truncating with rounding up представление числа 0.1, мы всегда получим точную целую часть, которую бы получили умножая бесконечно точно 0.1 на A. Но, по условию у нас умножается 16-битные число, а значит, в нашем случае ответ будет неточным и довериться простому умножению на truncating with rounding up 0.1 мы не можем. Вот если бы мы могли сохранить в FP представлении числа 0.1 не 16 бит, а, скажем 19, 20 — то все было бы ОК. И ведь можем же!
Внимательно смотрим на двоичное представление — truncating with rounding up 0.1: старшие три бита — нулевые, а значит, и никакого вклада в результат умножения не вносят (новых бит).
Следовательно, мы можем сдвинуть наше число влево на три бита, выполнить округление вверх и, выполнив умножение и логический сдвиг вправо сначала на 16, а затем на 3 (то есть, за один раз на 19 вообще говоря) — получим нужную, точную целую часть. Доказательство корректности такого ’19’ битного умножения аналогично предыдущему, с той лишь разницей, что для 16-битных чисел оно работает правильно. Аналогичные рассуждения верны и для чисел большей разрядности, да и не только для деления на 10.
Ранее я писал, что вообще говоря, можно обойтись и без какого-либо сдвига вовсе, ограничившись лишь умножением. Как? Ассемблер x86 / x64 на барабане:
В современных процессорах, есть команда MUL (есть еще аналоги IMUL, MULX — BMI2), которая принимая один, скажем 32 / 64 -битный параметр, способна выполнять 64 / 128 битное умножение, сохраняя результат частями в два регистра (старшие 32 / 64 бита и младшие, соответственно):
MUL RCX ; умножить RCX на RAX, а результат (128 бит) сохранить в RDX:RAX
В регистре RCX пусть хранится некое целое 62-битное A, а в регистре RAX пускай хранится 64 битное FA представление truncating with rounding up числа 0.1 (заметь, никаких сдвигов влево нету). Выполнив 64-битное умножение получим, что в регистре RDX сохранятся старшие 64 бита результата, или, точнее говоря — целая часть, которая для 62 битных чисел, будет точной. То есть, сдвиг вправо (SHR, SHRX) не нужен. Наличие же такого сдвига нагружает Pipeline процессора, вне зависимости поддерживает ли он OOOE или нет: как минимум появляется лишняя зависимость в, скорее всего и без того длинной цепочке таких зависимостей (aka Dependency Chain). И вот тут, очень важно упомянуть о том, что современные компиляторы, видя выражение вида some_integer / 10 — автоматически генерируют ассемблерный код для всего диапазона чисел Делимого. То есть, если вам известно что числа у вас всегда 53-ех битные (в моей задаче именно так и было), то лишнюю инструкцию сдвига вы получите все равно. Но, теперь, когда вы понимаете как это работает, можете сами с легкостью заменить деление — умножением, не полагаясь на милость компилятору. К слову сказать, получение старших битов 64 битного произведения в C++ коде реализуется интринсиком (something like mulh), что по Asm коду, должно быть равносильно строчкам инструкции {I}MUL{X} выше.
Возможно, с появлением контрактов (в С++ 20 не ждем) ситуация улучшится, и в каких-то кейсах, мы сможем довериться машине! Хотя, это же С++, тут за все отвечает программист — не иначе.
Рассуждения описанные выше — применимы к любым делителям константам, ну а ниже список полезных ссылок:
[1] https://www.agner.org/optimize/instruction_tables.pdf
[2] Круче чем Агнер Фог
[3] Телеграмм канал, с полезной информацией о оптимизациях под Intel / AMD / ARM
[4] Про деление нацело, но по Английски
Лекция 7. Арифметические команды языка Ассемблер: аддитивные и мультипликативные команды целочисленных операций.
7.1. Сложение и вычитание.
7.1.1. ADD – команда для сложения двух
чисел. Она работает как с числами со знаком, так и без знака.
ADD
Приемник,
Источник
Логика
работы команды:
<Приемник> = <Приемник> + <Источник>
Возможные
сочетания операндов для этой команды аналогичны команде MOV.
По
сути дела, это – команда сложения с присвоением, аналогичная принятой в языке C/C++:
Приемник += Источник;
Операнды
должны иметь одинаковый размер. Результат помещается на место первого операнда.
После
выполнения команды изменяются флаги, по которым можно определить характеристики
результата:
- Флаг CF устанавливается, если
при сложении произошёл перенос из старшего разряда. Для беззнаковых чисел
это будет означать, что произошло переполнение и результат получился
некорректным. - Флаг OF обозначает
переполнение для чисел со знаком. - Флаг SF равен знаковому биту
результата (естественно, для чисел со знаком, а для беззнаковых он равен
старшему биту и особо смысла не имеет). - Флаг ZF устанавливается, если
результат равен 0. - Флаг PF — признак чётности,
равен 1, если результат содержит нечётное число единиц.
Примеры:
add ax,5 ;AX = AX + 5
add dx,cx ;DX = DX + CX
add dx,cl
;Ошибка: разный размер
операндов.
7.1.2. SUB — команда для вычитания одного
числа из другого. Она работает как с числами со знаком, так и без знака.
SUB
Приемник,
Источник
Логика
работы команды:
<Приемник> = <Приемник> — <Источник>
Возможные
сочетания операндов для этой команды аналогичны команде MOV.
По
сути дела, это – команда вычитания с присвоением, аналогичная принятой в языке C/C++:
Приемник -= Источник;
Операнды должны
иметь одинаковый размер. Результат помещается на место первого операнда.
На самом деле
вычитание в процессоре реализовано с помощью сложения. Процессор меняет знак
второго операнда на противоположный, а затем складывает два числа.
Примеры:
sub ax,13 ;AX = AX — 13
sub ax,bx ;AX = AX + BX
sub bx,cl ;Ошибка: разный размер операндов.
7.1.3. Инкремент и декремент. Очень часто в программах используется операция прибавления
или вычитания единицы. Прибавление единицы называется инкрементом, а вычитание
— декрементом. Для этих операций существуют специальные команды процессора: INC
и DEC. Эти команды не изменяют значение флага CF.
Эти команды
содержит один операнд и имеет следующий синтаксис:
INC
Операнд
DEC
Операнд
Логика работы
команд:
INC: <Операнд>
= < Операнд > + 1
DEC: <Операнд>
= < Операнд > — 1
В качестве
инкремента допустимы регистры и память: reg, mem.
Примеры:
inc ax
;AX = AX + 1
dec ax
;AX = AX — 1
7.1.4. NEG – команда для изменения знака
операнда.
Синтаксис:
NEG
Операнд
Логика работы
команды:
<Операнд> = – < Операнд >
В качестве
декремента допустимы регистры и память: reg, mem.
Примеры:
neg ax ;AX = -AX
7.2. Сложение и вычитание с
переносом.
В системе команд
процессоров x86 имеются специальные команды сложения и вычитания с учётом флага
переноса (CF). Для сложения с учётом переноса предназначена команда ADC, а для вычитания — SBB. В общем, эти команды работают
почти так же, как ADD и SUB, единственное отличие в том, что к младшему разряду
первого операнда прибавляется или вычитается дополнительно значение флага CF.
Они позволяют
выполнять сложение и вычитание многобайтных целых чисел, длина которых больше,
чем разрядность регистров процессора (в нашем случае 16 бит). Принцип
программирования таких операций очень прост — длинные числа складываются
(вычитаются) по частям. Младшие разряды складываются(вычитаются) с помощью
обычных команд ADD и SUB, а затем последовательно складываются(вычитаются)
более старшие части с помощью команд ADC и SBB. Так как эти команды учитывают
перенос из старшего разряда, то мы можем быть уверены, что ни один бит не
потеряется. Этот способ похож на сложение(вычитание) десятичных чисел в
столбик.
На следующем
рисунке показано сложение двух двоичных чисел командой ADD:
При сложении
происходит перенос из 7-го разряда в 8-й, как раз на границе между байтами.
Если мы будем складывать эти числа по частям командой ADD, то перенесённый бит
потеряется и в результате мы получим ошибку. К счастью, перенос из старшего
разряда всегда сохраняется в флаге CF. Чтобы прибавить этот перенесённый бит,
достаточно применить команду ADC:
Пример:
#include <iostream.h>
#include <iomanip.h>
void main()
{
//Сложение двух чисел с учетом переноса: FFFFFFAA + FFFF
int a, b;
asm {
mov eax, 0FFFFFFAAh
mov ebx, 0FFFFh
mov edx, 0
mov ecx, 0
add eax, ebx
adc edx, ecx
mov a, edx
mov b, eax
}
cout << hex << a
<< setw(8) << setfill(‘0’) << b; //10000ffa9
}
7.3. Умножение и деление.
7.3.1. MUL – команда умножения чисел без
знака. У этой команды только один операнд — второй множитель, который должен
находиться в регистре или в памяти. Местоположение первого множителя и
результата задаётся неявно и зависит от размера операнда:
Размер операнда | Множитель | Результат |
1 байт | AL | AX |
2 байта | AX | DX:AX |
4 байта | EAX | EDX:EAX |
Отличие умножения
от сложения и вычитания в том, что разрядность результата получается в 2 раза
больше, чем разрядность сомножителей.
Примеры:
mul bl
;AX = AL * BL
mul ax
;DX:AX = AX * AX
Если
старшая часть результата равна нулю, то флаги CF и ОF будут иметь нулевое
значение. В этом случае старшую часть результата можно отбросить.
7.3.2. IMUL – команда умножения чисел со
знаком. Эта команда имеет три формы, различающиеся количеством операндов:
1. С одним операндом — форма,
аналогичная команде MUL. В качестве операнда указывается множитель.
Местоположение другого множителя и результата определяется по таблице.
2. С двумя операндами — указываются
два множителя. Результат записывается на место первого множителя. Старшая часть
результата в этом случае игнорируется. Кстати, эта форма команды не работает с
операндами размером 1 байт.
3. С тремя операндами — указывается
положение результата, первого и второго множителя. Второй множитель должен быть
непосредственным значением. Результат имеет такой же размер, как первый
множитель, старшая часть результата игнорируется. Это форма тоже не работает с
однобайтными множителями.
Примеры:
imul cl ;AX = AL * CL
imul bx,ax ;BX = BX * AX
imul cx,-5 ;CX = CX * (-5)
imul dx,bx,134h ;DX = BX * 134h
CF
= OF = 0, если произведение помещается в младшей половине результата, иначе CF
= OF = 1. Для второй и третьей формы команды CF = OF = 1 означает, что
произошло переполнение.
7.3.3. DIV
– команда деления чисел без знака. У этой команды один операнд — делитель,
который должен находиться в регистре или в памяти. Местоположение делимого,
частного и остатка задаётся неявно и зависит от размера операнда:
Размер | Делимое | Частное | Остаток |
1 байт | AX | AL | AH |
2 байта | DX:AX | AX | DX |
4 байта | EDX:EAX | EAX | EDX |
При
выполнении команды DIV может возникнуть прерывание (в данном курсе
прерывания мы рассматривать не будем поэтому старайтесь избегать таких
случаев):
- если делитель равен нулю;
- если частное не помещается в
отведённую под него разрядную сетку (например, если при делении слова на
байт частное больше 255).
Примеры:
div cl ;AL = AX / CL, остаток в AH
div di ;AX = DX:AX / DI, остаток в
DX
7.3.4. IDIV
– команда деления чисел со знаком. Единственным операндом является делитель.
Местоположение делимого и частного определяется также, как для команды DIV. Эта
команда тоже генерирует прерывание при делении на ноль или слишком большом
частном.
7.3.5. NOP
– ничего не делающая команда.
Синтаксис:
NOP
Примеры:
nop
Пример. (5 + 8) / (2 * 3)
#include <iostream.h>
void main()
{
asm {
mov bx, 5 //BL = 5
add bx, 8
//BL = BL + 8 | 13
sub bx, 1
//BL = BL — 1 | 12
mov al, 2
//AL = 2
mov cl, 3
//CL = 3
mul
cl //AX = AL * CL
| 6
//AX = 6,
BL = 12
xchg bx,
ax //AX = 12, BX = 6
mov dx, 0
div bx
}
}
Ассемблер. Арифметические инструкции | Уроки Ассемблера
Обновл. 9 Мар 2021 |
На этом уроке мы будем разбираться с арифметическими инструкциями в ассемблере на примере INC, DEC, ADD, SUB и пр.
Инструкция INC
Инструкция INC (от англ. «INCREMENT») используется для увеличения операнда на единицу. Она работает с одним операндом, который может находиться либо в регистре, либо в памяти.
Синтаксис инструкции INC:
INC место_назначения
Операндом место_назначения
может быть 8-битный, 16-битный или 32-битный операнд.
Пример:
INC EBX ; выполняем инкремент 32-битного регистра
INC DL ; выполняем инкремент 8-битного регистра
INC [count] ; выполняем инкремент переменной count
INC EBX ; выполняем инкремент 32-битного регистра INC DL ; выполняем инкремент 8-битного регистра INC [count] ; выполняем инкремент переменной count |
Инструкция DEC
Инструкция DEC (от англ. «DECREMENT») используется для уменьшения операнда на единицу. Она работает с одним операндом, который может находиться либо в регистре, либо в памяти.
Синтаксис инструкции DEC:
DEC место_назначения
Операндом место_назначения
может быть 8-битный, 16-битный или 32-битный операнд.
Пример:
segment .data
count dw 0
value db 15
segment .text
inc [count]
dec [value]
mov ebx, count
inc word [ebx]
mov esi, value
dec byte [esi]
segment .data count dw 0 value db 15 segment .text inc [count] dec [value] mov ebx, count inc word [ebx] mov esi, value dec byte [esi] |
Инструкции ADD и SUB
Инструкции ADD и SUB используются для выполнения простого сложения/вычитания двоичных данных размером один byte, word или doubleword, то есть для сложения или вычитания 8-битных, 16-битных или 32-битных операндов, соответственно.
Синтаксис инструкций ADD и SUB:
ADD/SUB место_назначения, источник
Инструкции ADD/SUB могут выполняться между:
регистром и регистром;
памятью и регистром;
регистром и памятью;
регистром и константами;
памятью и константами.
Однако, как и для других инструкций, операции типа память-в-память невозможны с использованием инструкций ADD/SUB. Операции ADD или SUB устанавливают или сбрасывают флаги переполнения и переноса.
В следующем примере мы спрашиваем у пользователя два числа, сохраняем их в регистрах EAX и EBX, затем выполняем операцию сложения, сохраняем результат в ячейке памяти res
и выводим его на экран:
SYS_EXIT equ 1
SYS_READ equ 3
SYS_WRITE equ 4
STDIN equ 0
STDOUT equ 1
segment .data
msg1 db «Enter a digit «, 0xA,0xD
len1 equ $- msg1
msg2 db «Please enter a second digit», 0xA,0xD
len2 equ $- msg2
msg3 db «The sum is: «
len3 equ $- msg3
segment .bss
num1 resb 2
num2 resb 2
res resb 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_READ
mov ebx, STDIN
mov ecx, num1
mov edx, 2
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg2
mov edx, len2
int 0x80
mov eax, SYS_READ
mov ebx, STDIN
mov ecx, num2
mov edx, 2
int 0x80
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, msg3
mov edx, len3
int 0x80
; перемещаем первое число в регистр EAX, а второе число — в регистр EBX
; и вычитаем ASCII ‘0’ для конвертации в десятичное число
mov eax, [num1]
sub eax, ‘0’
mov ebx, [num2]
sub ebx, ‘0’
; складываем eax и ebx
add eax, ebx
; добавляем ‘0’ для конвертации суммы из десятичной системы в ASCII
add eax, ‘0’
; сохраняем сумму в ячейке памяти res
mov [res], eax
; выводим сумму
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, res
mov edx, 1
int 0x80
exit:
mov eax, SYS_EXIT
xor ebx, ebx
int 0x80
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | SYS_EXIT equ 1 SYS_READ equ 3 SYS_WRITE equ 4 STDIN equ 0 STDOUT equ 1
segment .data
msg1 db «Enter a digit «, 0xA,0xD len1 equ $- msg1
msg2 db «Please enter a second digit», 0xA,0xD len2 equ $- msg2
msg3 db «The sum is: « len3 equ $- msg3
segment .bss
num1 resb 2 num2 resb 2 res resb 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_READ mov ebx, STDIN mov ecx, num1 mov edx, 2 int 0x80
mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg2 mov edx, len2 int 0x80
mov eax, SYS_READ mov ebx, STDIN mov ecx, num2 mov edx, 2 int 0x80
mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, msg3 mov edx, len3 int 0x80
; перемещаем первое число в регистр EAX, а второе число — в регистр EBX ; и вычитаем ASCII ‘0’ для конвертации в десятичное число mov eax, [num1] sub eax, ‘0’ mov ebx, [num2] sub ebx, ‘0’
; складываем eax и ebx add eax, ebx
; добавляем ‘0’ для конвертации суммы из десятичной системы в ASCII add eax, ‘0’
; сохраняем сумму в ячейке памяти res mov [res], eax
; выводим сумму mov eax, SYS_WRITE mov ebx, STDOUT mov ecx, res mov edx, 1 int 0x80
exit:
mov eax, SYS_EXIT xor ebx, ebx int 0x80 |
Результат выполнения программы:
Enter a digit:
3
Please enter a second digit:
4
The sum is:
7
Ниже рассмотрен пример, в котором за счет того, что значения переменных для арифметических выражений прописаны в самом коде программы, можно получить код программы короче и проще:
section .text
global _start ; должно быть объявлено для использования gcc
_start: ; сообщаем линкеру входную точку
mov eax,’3’
sub eax, ‘0’
mov ebx, ‘4’
sub ebx, ‘0’
add eax, ebx
add eax, ‘0’
mov [sum], eax
mov ecx,msg
mov edx, len
mov ebx,1 ; файловый дескриптор (stdout)
mov eax,4 ; номер системного вызова (sys_write)
int 0x80 ; вызов ядра
mov ecx,sum
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 sum is:», 0xA,0xD
len equ $ — msg
segment .bss
sum 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: ; сообщаем линкеру входную точку mov eax,’3′ sub eax, ‘0’ mov ebx, ‘4’ sub ebx, ‘0’ add eax, ebx add eax, ‘0’ mov [sum], eax mov ecx,msg mov edx, len mov ebx,1 ; файловый дескриптор (stdout) mov eax,4 ; номер системного вызова (sys_write) int 0x80 ; вызов ядра mov ecx,sum 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 sum is:», 0xA,0xD len equ $ — msg segment .bss sum resb 1 |
Результат выполнения программы:
The sum is:
7
Инструкции MUL и IMUL
Есть 2 инструкции для умножения двоичных данных:
инструкция MUL (от англ. «MULTIPLY») обрабатывает данные unsigned;
инструкция IMUL (от англ. «INTEGER MULTIPLY») обрабатывает данные signed.
Обе инструкции влияют на флаги переноса и переполнения.
Синтаксис инструкций MUL/IMUL:
MUL/IMUL множитель
Множимое в обоих случаях будет находиться в аккумуляторе, в зависимости от размера множимого и множителя, и результат умножения также сохраняется в двух регистрах, в зависимости от размера операндов.
Рассмотрим 3 разных сценария:
Сценарий №1: Перемножаются 2 значения типа byte. Множимое находится в регистре AL, а множителем является значение типа byte в памяти или в другом регистре. Результат произведения находится в AX. Старшие 8 бит произведения хранятся в AH, а младшие 8 бит хранятся в AL:
Сценарий №2: Перемножаются 2 значения типа word. Множимое должно находиться в регистре AX, а множителем является значение типа word в памяти или в другом регистре. Например, для такой инструкции, как MUL DX
, вы должны сохранить множитель в DX, а множимое — в AX. В результате получится значение типа doubleword, для которого понадобятся два регистра. Часть высшего порядка (крайняя слева) сохраняется в DX, а часть нижнего порядка (крайняя справа) сохраняется в AX:
Сценарий №3: Перемножаются 2 значения типа doubleword. Множимое должно находиться в EAX, а множителем является значение типа doubleword, хранящееся в памяти или в другом регистре. Результат умножения сохраняется в регистрах EDX и EAX. Биты старшего порядка сохраняются в регистре EDX, а биты младшего порядка сохраняются в регистре EAX:
Пример:
MOV AL, 10
MOV DL, 25
MUL DL
…
MOV DL, 0FFH ; DL= -1
MOV AL, 0BEH ; AL = -66
IMUL DL
MOV AL, 10 MOV DL, 25 MUL DL … MOV DL, 0FFH ; DL= -1 MOV AL, 0BEH ; AL = -66 IMUL DL |
А в следующем примере мы 3 умножаем на 2 и выводим результат:
section .text
global _start ; должно быть объявлено для использования gcc
_start: ; сообщаем линкеру входную точку
mov al,’3’
sub al, ‘0’
mov bl, ‘2’
sub bl, ‘0’
mul bl
add al, ‘0’
mov [res], al
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 34 | section .text global _start ; должно быть объявлено для использования gcc _start: ; сообщаем линкеру входную точку
mov al,’3′ sub al, ‘0’ mov bl, ‘2’ sub bl, ‘0’ mul bl add al, ‘0’ mov [res], al 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 |
Результат выполнения программы:
The result is:
6
Инструкции DIV и IDIV
Операция деления генерирует два элемента: частное и остаток. В случае умножения переполнения не происходит, так как для хранения результата используются регистры двойной длины. Однако в случае деления, переполнение может произойти. Если это произошло, то процессор генерирует прерывание.
Инструкция DIV (от англ. «DIVIDE») используется с данными unsigned, а инструкция IDIV (от англ. «INTEGER DIVIDE») используется с данными signed.
Синтаксис инструкций DIV/IDIV:
DIV/IDIV делитель
Делимое находится в аккумуляторе. Обе инструкции могут работать с 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 [res], 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: ; сообщаем линкеру входную точку mov ax,’8′ sub ax, ‘0’ mov bl, ‘2’ sub bl, ‘0’ div bl add ax, ‘0’ mov [res], 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 |
Результат выполнения программы:
The result is:
4
Оценить статью:
Загрузка…
Поделиться в социальных сетях:
Деление в сборке x86 ГАЗ
Инструкция idiv
принимает 2 аргумента в 3 регистрах.
Первый неявный аргумент — это делимое, 64-битный аргумент в edx: eax
Младшие 32 бита — в eax
, старшие 32 бита — в edx
.
Второй явный аргумент — это делитель, 32-битный аргумент в одном регистре.
По очевидным причинам делитель , а не , должен быть edx
или eax
.
Результат возвращается в формате eax = Quotient, edx = Remainder.
Зная это, правильная установка должна быть:
синтаксис Intel синтаксис pdp-11
-------------------------------------------------- ------
.intel_syntax noprefix
mov ebx, дивиденд
mov ecx, делитель
mov eax, ebx movl% ebx,% eax
cdq cdq // edx: eax = 32-битное делимое
idiv ecx idivl% ecx // разделить edx: eax на ecx
imul eax, ecx imull% ecx,% eax // умножить результат на делимое
cmp ebx, eax cmpl% eax,% ebx
je.равный je. равный
добавить eax, edx addl% edx,% eax // добавить остаток
cmp ebx, eax cmpl% eax,% ebx // теперь должно быть равно
je .equal2 je .equal2
Следует иметь в виду, что div / idiv
выполняет целочисленное деление!
Результатом всегда является целое число с остатком (который может быть равен нулю).
Не выполняет никаких операций с плавающей запятой. Он генерирует исключение только в том случае, если результат слишком велик, чтобы уместиться в 32 бита, или если вы делите на ноль, и в этом случае вы получаете ошибку #DE Division.
Причина, по которой вы получаете ошибку целочисленного деления , заключается в том, что вы ошибочно используете edx
в качестве делителя и поскольку ваш дивиденд 32-битный, более высокие 32 бита (хранящиеся в edx
) всегда равны нулю, и, следовательно, у вас есть деление на ноль.
Никогда не используйте одни и те же регистры для деления и делителя!
Вы получите ту же ошибку, если edx: eax idiv ecx
не помещается в 32 бита (т.е. если edx: eax
слишком велик по сравнению с ecx
).
См .: http://www.felixcloutier.com/x86/IDIV.html и http://www.felixcloutier.com/x86/DIV.html
Я горячо ненавижу синтаксис ATT или PDP, который бессмысленен и сломан.
Вы можете восстановить работоспособность и использовать синтаксис Intel в Gas с помощью псевдо-инструкции .intel_syntax noprefix
.
См .: Могу ли я использовать синтаксис Intel сборки x86 с GCC?
О языке ассемблера — подраздел
Язык ассемблера — Подразделение
Сокращенный набор команд для всех микросхем в семействе ARM — от ARM2 до StrongARM —
включает в себя странные и замечательные инструкции, такие как MLA (Умножение на Накопление: умножение на два
регистров и добавить к результату содержимое трети) и ASL (арифметический сдвиг влево:
полностью идентична инструкции Logical Shift Left).Более поздние фишки содержат несколько
Дополнительные указания. Однако, насколько мне известно, места в
набор инструкций для чего-то, что было бы очень полезно — инструкции DIV.
Принципы деления
Деление в машинном коде точно такое же, как деление любым другим методом — это просто
дело повторного вычитания. Когда мы делим двенадцать на четыре, мы действительно хотим знать
сколько раз число 4 уместится в число 12, другими словами, сколько раз мы можем
вычтите меньшее число из большего числа, пока большее число не достигнет нуля.В
ответ в этом случае, конечно, будет трижды:
12 - 4 = 8 8 - 4 = 4 4-4 = 0
Когда я был совсем маленьким, мне разрешили играть со старомодными механическими счетчиками.
машина. На передней панели машины было множество вертикальных циферблатов, подобных тем, что на
кодовый замок, на котором вы устанавливаете числа, которые хотите вычислить, и был
ручка на одной стороне, которая была намотана от вас, чтобы прибавить текущий номер к
итоговое значение на дисплее или по направлению к вам, чтобы вычесть его.Чтобы осуществить разделение,
нужно было установить первое число, а затем вычесть из него второе число
несколько раз (считая, сколько раз вы поворачивали ручку!), как и в приведенной выше сумме.
Очевидно, однако, что это могло бы быть очень медленным, если бы вы делали сумму вроде 128 ÷ 4.
поскольку ответ — 32, это количество раз, которое вам придется повернуть ручку!
Было бы вполне возможно осуществить разделение в коде ARM, используя эту простую технику,
построив такой цикл:
MOV R1, # 128; разделить R1 MOV R2, # 4; от R2 MOV R0, # 0; инициализировать счетчик .вычесть SUBS R1, R1, R2; вычесть R2 из ; R1 и магазин ; результат обратно в ; R1 установка флагов ADD R0, R0, # 1; добавить 1 к счетчику, ; НЕ устанавливаем флаги BHI вычесть; перейти к началу ; цикл по условию ; Выше, т.е. R1 равно ; еще больше, чем ; R2. Ответьте сейчас в R0
Поскольку даже самый медленный процессор ARM намного быстрее, чем ребенок, поворачивающий ручку, прикрепленную к
жесткий набор шестерен, это может быть даже приемлемым решением.Но есть уловка
обычно используется для сохранения работы на вычислительной машине — и вы также можете использовать ее на компьютере.
Перемещение по
На практике, разделив большое число на гораздо меньшее, вы сместите
меньшее число на один или несколько мест слева, установив его на дисках слева от
там, где он обычно был бы, оставив правые циферблаты на нуле. Степень, в которой вы
сдвиг будет продиктован здравым смыслом и мысленной арифметикой — если вы не можете вычесть это
хоть раз с другого номера, значит, вы зашли слишком далеко! Затем вы вычтите это
сдвинул число столько раз, сколько возможно, чтобы получить первую цифру результата, сбросить все
ваши циферблаты, чтобы сдвинуть его обратно вправо, несколько раз вычитайте снова, чтобы найти следующую цифру,
и продолжайте этот процесс, пока снова не достигнете исходного числа и больше не сможете вычитать
даже то, что.
Используя наш пример 128 ÷ 4, вы в конечном итоге сделаете следующее:
- Сдвинуть 4 на одну позицию влево — станет 40.
- Shift 4 другое место влево — становится 400. Это больше 128, так что мы зашли слишком далеко. Начнем с предыдущего результата — 40. Итак:
- 128-40 = 88 (поворот ручки один раз)
- 88-40 = 48 (дважды повернув)
- 48-40 = 8 (трижды повернув)
- Мы больше не можем вычесть 40, поэтому теперь мы обнаружили, что первая цифра ответа — 3.
- Теперь сдвигаем наше число назад вправо, чтобы оно снова стало 4, и снова начинаем считать обороты ручки.
- 8 — 4 = 4 (однократный поворот ручки)
- 4-4 = 0 (дважды повернув)
Мы больше не можем вычесть 4 (наше исходное число), поэтому мы нашли последнюю цифру числа
ответ будет 2. Другими словами, ответ 32, результат, который мы получили раньше, но у нас было
обернуть ручку только пять раз, чтобы получить ее, а не тридцать два раза!
Вы могли заметить, что это почти тот же метод, который используется при делении с использованием
ручка и бумага… «четыре в один не пойдут … четыре в двенадцать входят три раза … четыре в
восемь идет дважды … ответ тридцать два «. Разница в том, что мы знаем по опыту,
что 12 — это 4 × 3, а 8 — это 4 × 2. Машины, даже электронные, такие как компьютеры,
не имеют этого преимущества — они не содержат справочного набора таблиц умножения, поэтому
они должны делать это трудным путем.
Двоичное деление
Компьютеры используют двоичные числа. Вы не поверите, но это на самом деле делает жизнь намного проще, когда
приходит к написанию процедуры машинного кода для выполнения деления одного регистра на другой!
Поскольку значение каждой двоичной цифры ответа может быть только 0 или 1, мы можем избежать
упомянутая выше «проблема таблицы умножения».
Каждый раз, когда мы сдвигаем нашу цифру 1 вправо, чтобы получить следующую цифру ответа,
мы знаем, что сможем вычесть его либо ровно один раз, либо вообще не сможем. Нам только нужно
выполнять одно вычитание за смену; недостатком является то, что, поскольку двоичные числа имеют много
больше цифр, чем их десятичные эквиваленты, мы должны выполнить гораздо больше сдвигов.
Деление на два
Здесь я воспользуюсь новым примером. Причина этого в том, что в двоичной системе деление на
четыре или любая другая степень двойки чрезвычайно проста; ни один здравомыслящий программист не напишет
сложная процедура деления, когда все, что ему нужно сделать, это использовать одну инструкцию для
сдвинуть соответствующий регистр вправо, не больше, чем он достанет ручку и бумагу, чтобы
вычислить ответ на сумму «460 ÷ 10» вместо того, чтобы мысленно сбивать
ноль, чтобы получить правильный ответ.Разумная программа для деления 128 на 4 выглядела бы так:
MOV R1, # 128 MOV R0, R1, LSR # 2; смена R1 2 разряда ;Направо & ; хранить в R0 ; ответь сейчас в R0
Поскольку 4 равно 2 × 2, все, что нам нужно сделать, чтобы разделить на 4 в двоичном формате, — это сдвинуть регистр на два.
местами вправо, так же как все, что нам нужно сделать, чтобы разделить на 100 (10 × 10) в десятичной дроби, это
сдвинуть на два места вправо — например, от 600 пенсов получаем 6 фунтов.
Разделение на другие числа
Однако этот подход работает только тогда, когда мы хотим разделить на фиксированное значение, которое, как нам известно, является
кратно двум.Для более общего использования нам нужно использовать подход, основанный на вычитании, описанный в общих чертах.
выше.
Чтобы разделить 50 (% 110010) на 10 (% 1010) в двоичном формате:
- Мы сдвигаем 10 насколько возможно (на два места) влево, пока оно не станет% 101000, и вычтем это из% 110010, чтобы получить первая цифра.
- % 110010-% 101000 =% 1010 (первая цифра 1)
- Теперь мы сдвигаем% 101000 назад на одну позицию вправо и пытаемся вычесть% 10100 из того, что осталось от 25, с которых мы начали.
- % 1010 -% 10100 (Следующая цифра 0 — не пойдет!)
- Сдвиньте вправо, получите% 1010 и попробуйте еще раз:
- % 1010 -% 1010 =% 0 (Успешное вычитание — следующая цифра 1)
Наша цифра «10» теперь сдвинута на две позиции вправо, возвращая ее к исходному значению, которое является нашим сигналом к остановке и подсчету цифр в нашем ответе -% 101 в двоичном формате или «5» в десятичном, w
ich, конечно, правильный ответ.
Реализация процедуры в машинном коде
Показав, что у нас есть рабочий алгоритм двоичного деления, теперь нам нужно перевести его
в настоящие инструкции ассемблера.Я собираюсь разделить R1 на R2; нам также нужно будет использовать
регистры R0 и R4. Прежде чем мы начнем, необходимо выполнить всего одну обязательную проверку .
сделал….
CMP R2, # 0 BEQ Diviv_end ; проверьте деление на ноль!
Настройка дивизиона
Чтобы разделить R1 на R2, первое, что нам нужно сделать, это сдвинуть R2 влево на необходимое
количество мест. Самый простой способ сделать это — просто методом проб и ошибок — сдвигать до тех пор, пока
мы обнаруживаем, что R2 стал слишком большим, и останавливаемся.
MOV R0, # 0; очистить R0 для накопления результата MOV R3, # 1; установить бит 0 в R3, который будет ; сдвинут влево, затем вправо .Начните CMP R2, R1 MOVLS R2, R2, LSL №1 MOVLS R3, R3, LSL # 1 BLS начало ; сдвиньте R2 влево, пока ; быть больше R1 ; сдвиньте R3 влево параллельно по порядку ; чтобы отметить, как далеко нам нужно пройти
R0 будет использоваться для хранения результата. Роль R3 более сложная.
Фактически, мы используем R3, чтобы отметить, где должен находиться правый конец R2 — если мы сдвинем R2
осталось три места, это будет обозначено значением% 1000 в R3.Однако мы также добавляем его в
R0 каждый раз, когда нам удается успешно вычитать, так как он отмечает позицию цифры
в настоящее время рассчитывается в ответе. В приведенном выше двоичном примере (50 ÷ 10) мы
сдвинул «10» на два места влево, поэтому во время первого вычитания R3 был бы
% 100, во время второго (который потерпел неудачу) это было бы% 10, а во время
третий% 1. Добавление его к R0 после каждого успешного вычитания снова дало бы нам
ответ% 101!
Код ARM не имеет конкретных инструкций сдвига и поворота, присутствующих в не-RISC
наборы инструкций.Вместо этого у него есть концепция «барреля-переключателя», который можно использовать для
изменить значение, указанное в правом регистре для почти любой инструкции ,
без изменения самого регистра. Например, инструкция ADD R0, R1, R2, LSL # 2 будет
сложите R1 и (R2 << 2) и загрузите результат в R0, не влияя на значение
R2 никак. Это может быть очень полезно, но это означает, что если мы действительно хотим изменить
значение R2, сдвигая его, как мы это делаем здесь, мы должны прибегнуть к перемещению его в себя через
Переключатель: MOV R2, R2, LSL # 1.
Петля вычитания
Теперь о цикле, который действительно выполняет работу:
.следующий CMP R1, R2; перенос установлен, если R1> R2 (не спрашивайте почему) SUBCS R1, R1, R2; вычтите R2 из R1, если это ; дать положительный ответ ADDCS R0, R0, R3; и добавить текущий бит в R3 к ; накапливающийся ответ в R0
При вычитании кода ARM (инструкция CMP имитирует вычитание для установки флагов),
если R1 — R2 дает положительный ответ и «заимствование» не требуется, флаг переноса — , установленный .Это необходимо для правильной работы SBC (вычитание с переносом) при использовании для переноски.
получается 64-битное вычитание, но это сбивает с толку!
В данном случае мы используем это в своих интересах. Флаг переноса устанавливается, чтобы указать, что
возможно успешное вычитание, т. е. такое, которое не дает отрицательного результата, и
две следующие инструкции выполняются только при выполнении условия Carry Set. Обратите внимание, что
‘S’ в конце этих инструкций является частью кода условия ‘CS’ и означает , а не .
значит, что они устанавливают флаги!
MOVS R3, R3, LSR # 1; Сдвинуть R3 вправо во флаг переноса MOVCC R2, R2, LSR # 1; и если бит 0 R3 был равен нулю, также ; сдвинуть R2 вправо BCC следующий; если перенос не очищен, R3 сдвинулся ; назад туда, где все началось, и мы ; может закончиться .Divide_end MOV R25, R24; выход из программы
Следующие две инструкции сдвигают вправо R3, регистр «счетчик», и R2, который содержит
число, на которое мы делим. Мы специально устанавливаем флаги, используя суффикс ‘S’ при переключении
R3, поскольку мы хотим знать, когда бит, хранящийся в этом регистре, достигает правой части. В течение
сдвиг вправо, бит 0 передается во флаг переноса, в то время как остальные биты перемещаются
вместе. Поскольку установлен только один бит R3 (изначально он был загружен с% 1 перед сдвигом)
влево, а затем вправо), когда установлен флаг переноса, он указывает, что перед сдвигом значение
R3 было% 1, т.е.е. мы вернулись туда, откуда начали, и теперь R0 должен держать правую
отвечать.
Сводка
В конце подпрограммы R1 сохраняет остаток, если таковой имеется, R2 вернул значение, которое он удерживал.
при входе в процедуру R0 сохраняет результат, а R3 сохраняет ноль. И нулевой, и переносящий флаги
установленный. Эта процедура не работает для отрицательных значений R1 или R2.
Как и в случае с результатами целочисленного деления в Basic, значение в R0 всегда будет округляться до
следующий наименьшее целое число , а не до ближайшего числа.Например, 1156 ÷
19 дает результат «60 остатков 16», который на самом деле ближе к 61, чем к 60.
Главная / Руководство пользователя Assembler 4,23 Целое число без знака |
Руководства по AVR — 8-битное деление
В этом руководстве мы рассмотрим процедуры 8-битного деления в сборке AVR.AVR не имеет возможности выполнять аппаратное разделение (т.е. нет инструкций по разделению), поэтому подпрограммы должны быть написаны с использованием других инструкций для выполнения этой задачи.
Примечание по применению Atmel AVR200 предоставляет полный список подпрограмм разделения — вместо того, чтобы изобретать велосипед, мы рассмотрим, как они работают.
8-битное беззнаковое деление
Подпрограмма 8-битного беззнакового деления от Atmel показана ниже
; ********************************************** ****************************
; *
; * "div8u" - 8/8 битное беззнаковое деление
; *
; * Эта подпрограмма делит две регистровые переменные "dd8u" (делимое) и
; * "dv8u" (делитель).Результат помещается в "dres8u", а остаток в
; * "drem8u".
; *
; * Количество слов: 14
; * Количество циклов: 97
; * Используемые младшие регистры: 1 (drem8u)
; * Использовано старших регистров: 3 (dres8u / dd8u, dv8u, dcnt8u)
; *
; *********************************************** **************************
; ***** Переменные регистра подпрограммы
.def drem8u = r15; остаток
.def dres8u = r16; результат
.def dd8u = r16; дивиденд
.def dv8u = r17; делитель
.def dcnt8u = r18; счетчик циклов
; ***** Код
div8u: sub drem8u, drem8u; очистить остаток и перенести
ldi dcnt8u, 9; счетчик цикла инициализации
d8u_1: rol dd8u; сдвинуть дивиденд влево
dec dcnt8u; счетчик уменьшения
brne d8u_2; если сделано
ret; возвращение
d8u_2: rol drem8u; сдвинуть делимое в остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_3; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_1; иначе
d8u_3: сек; установить перенос, который будет перемещен в результат
rjmp d8u_1
В этой подпрограмме делитель вводится в регистр dd8u (r16), а делитель помещается в регистр dv8u (r17).По завершении результат будет сохранен в dd8u (r16), а остаток будет в drem8u (r15).
Подпрограмма начинается с очистки регистра, в котором будет храниться остаток от деления, drem8u, с использованием инструкции sub.
div8u: sub drem8u, drem8u; очистить остаток и перенести
Вы можете спросить, а почему бы просто не использовать вместо этого инструкцию clr? Что ж, этой подпрограмме необходимо очистить Carry Flag перед выполнением, и clr оставит его неизмененным.Если бы вместо этого использовался clr, clc должен был бы следовать за ним, например
div8u: clr drem8u; очистить остаток
clc; чистый перенос
Вместо этого использование sub очищает регистр и флаг переноса в одной инструкции. Довольно хитро!
Затем dcnt8u инициализируется как счетчик цикла со значением 9.
ldi dcnt8u, 9; счетчик цикла инициализации
A For Loop устанавливается на этикетке d8u_1
d8u_1: rol dd8u; сдвинуть дивиденд влево
dec dcnt8u; счетчик уменьшения
brne d8u_2; если сделано
ret; возврат
В каждом цикле dcnt8u будет уменьшаться, давая 8 итераций, по одной для каждого бита делимого.Подпрограмма вернется, когда она достигнет нуля.
Затем старший значащий бит делителя сдвигается в младший значащий бит остатка. Мы проверяем, переходит ли делитель в остаток, используя вспомогательную инструкцию. Мы вычитаем делитель из этого значения, и если он отрицательный (проверяется инструкцией brcc), мы знаем, что делитель входит в остаток меньше одного раза. В противном случае мы знаем, что он входит хотя бы один раз.
d8u_2: rol drem8u; сдвинуть делимое в остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_3; если результат отрицательный
Если делитель , а не входит в остаток, нам нужно восстановить его (так как он был изменен с помощью вспомогательной инструкции) и сдвинуть ноль в наш результат.
drem8u восстанавливается путем добавления к нему dv8u, флаг переноса очищается инструкцией clc, а rol используется для сдвига нуля (значение флага переноса) в результат. Этот поток показан ниже с удаленными альтернативными ветвями.
d8u_2: rol drem8u; сдвинуть делимое в остаток
sub drem8u, dv8u; остаток = остаток - делитель
...
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
↓
d8u_1: rol dd8u; сдвинуть ноль в делимое
Обратите внимание, что dd8u является как входным делимым , так и результатом, поэтому флаг переноса сдвигается в него, а не в другой регистр.
В качестве альтернативы, если делитель не входит в остаток , нам не нужно его восстанавливать. Вместо этого нам нужно сместить единицу в результат, установив флаг переноса.
d8u_2: rol drem8u; сдвинуть делимое в остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_3; если результат отрицательный
...
d8u_3: сек; установить перенос, который будет перемещен в результат
↓
d8u_1: rol dd8u; сдвинуть единицу на делимое
Для любого результата используется rjmp для возврата к началу цикла, где другой бит будет сдвинут из делимого, и цикл будет повторяться для всех 8 битов.
d8u_1: rol dd8u; сдвинуть дивиденд влево
...
rjmp d8u_1
Использовать эту подпрограмму просто. Просто нужно загрузить дивиденд в r16, а делитель — в r17, прежде чем он будет вызван.
LDI R16,101; загрузить 101 в r16
ldi r17,10; загрузить 10 в r17
rcall div8u; вычислить 101/10
В приведенном выше примере r16 будет содержать значение 10 по завершении подпрограммы — результат деления, а r15 будет содержать 1, остаток.
Время выполнения в зависимости от размера кода
В сборке вы часто будете сталкиваться с выбором между временем выполнения и размером кода. Приведенный выше пример довольно компактен из-за использования ветвей , но это вводит несколько дополнительных циклов накладных расходов на каждой итерации цикла. Вместо этого ветвление можно было бы уменьшить, имея отдельный блок кода для каждого цикла цикла, что ускоряет работу, но увеличивает размер кода.
Вышеупомянутое является «оптимизированным по размеру», но рассмотрим вместо этого «оптимизированную по скорости» версию подпрограммы.
; ********************************************** ****************************
; *
; * "div8u" - 8/8 битное беззнаковое деление
; *
; * Эта подпрограмма делит две регистровые переменные "dd8u" (делимое) и
; * "dv8u" (делитель). Результат помещается в "dres8u", а остаток в
; * "drem8u".
; *
; * Количество слов: 66 + возврат
; * Количество циклов: 50/58/66 (мин / среднее / макс) + возврат
; * Используемые младшие регистры: 1 (drem8u)
; * Использовано старших регистров: 2 (dres8u / dd8u, dv8u)
; *
; *********************************************** **************************
; ***** Переменные регистра подпрограммы
.def drem8u = r15; остаток
.def dres8u = r16; результат
.def dd8u = r16; дивиденд
.def dv8u = r17; делитель
; ***** Код
div8u: sub drem8u, drem8u; очистить остаток и перенести
rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_1; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_2; иначе
d8u_1: сек; установить перенос, который будет перемещен в результат
d8u_2: rol dd8u; сдвинуть делимое влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_3; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_4; иначе
d8u_3: сек; установить перенос, который будет перемещен в результат
d8u_4: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_5; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_6; иначе
d8u_5: сек; установить перенос, который будет перемещен в результат
d8u_6: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_7; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_8; иначе
d8u_7: сек; установить перенос, который будет перемещен в результат
d8u_8: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_9; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_10; иначе
d8u_9: сек; установить перенос, который будет перемещен в результат
d8u_10: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_11; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_12; иначе
d8u_11: сек; установить перенос, который будет перемещен в результат
d8u_12: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_13; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_14; иначе
d8u_13: сек; установить перенос, который будет перемещен в результат
d8u_14: rol dd8u; сдвинуть дивиденд влево
rol drem8u; сдвинуть дивиденды на остаток
sub drem8u, dv8u; остаток = остаток - делитель
brcc d8u_15; если результат отрицательный
добавить drem8u, dv8u; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8u_16; иначе
d8u_15: сек; установить перенос, который будет перемещен в результат
d8u_16: rol dd8u; сдвинуть делимое влево
Ret
Версия с «оптимизацией по скорости» значительно длиннее — 66 слов вместо 14.Однако он может выполняться за 58 циклов по сравнению с 97 для версии с оптимизированным размером.
Вам нужно будет принять решение на основе компромисса между скоростью и размером кода для вашего конкретного приложения. Но самое замечательное в сборке то, что теперь у вас есть выбор!
8-битное деление со знаком
Знаковое 8-битное деление на самом деле довольно просто, если вы знаете, как делать беззнаковое деление. Вам просто нужно проверить, являются ли делитель и делимое отрицательными, преобразовать их значение Two’s Complement в величину, если они есть, и продолжить беззнаковое деление, как и раньше.В конце, если один или другой был отрицательным (но не оба!), Вам нужно преобразовать результат обратно в значение со знаком. Это показано ниже
; ********************************************** ****************************
; *
; * "div8s" - 8/8 битное знаковое деление
; *
; * Эта подпрограмма делит две регистровые переменные "dd8s" (делимое) и
; * "dv8s" (делитель). Результат помещается в "dres8s", а остаток в
; * "drem8s".
; *
; * Количество слов: 22
; * Количество циклов: 103
; * Используемые младшие регистры: 2 (d8s, drem8s)
; * Использовано старших регистров: 3 (dres8s / dd8s, dv8s, dcnt8s)
; *
; *********************************************** **************************
; ***** Переменные регистра подпрограммы
.def d8s = r14; регистр знака
.def drem8s = r15; остаток
.def dres8s = r16; результат
.def dd8s = r16; дивиденд
.def dv8s = r17; делитель
.def dcnt8s = r18; счетчик циклов
; ***** Код
div8s: mov d8s, dd8s; переместить делимое в регистр знака
eor d8s, dv8s; знак xor с делителем
sbrc dv8s, 7; если установлен старший бит делителя
neg dv8s; изменить знак делителя
sbrc dd8s, 7; если установлен старший бит дивиденда
neg dd8s; изменить знак делителя
sub drem8s, drem8s; очистить остаток и перенести
ldi dcnt8s, 9; счетчик цикла инициализации
d8s_1: rol dd8s; сдвинуть дивиденд влево
dec dcnt8s; счетчик уменьшения
brne d8s_2; если сделано
sbrc d8s, 7; если установлен MSB знакового регистра
neg dres8s; изменить знак результата
ret; возвращение
d8s_2: rol drem8s; сдвинуть делимое в остаток
sub drem8s, dv8s; остаток = остаток - делитель
brcc d8s_3; если результат отрицательный
добавить drem8s, dv8s; восстановить остаток
clc; чистый перенос, который нужно преобразовать в результат
rjmp d8s_1; иначе
d8s_3: сек; установить перенос, который будет перемещен в результат
rjmp d8s_1
Эта подпрограмма деления со знаком требует на один регистр больше, чем раньше — d8s (r14).d8s используется для отслеживания знаков операндов и результата.
Подпрограмма начинается с копирования делимого dd8s в знаковый регистр d8s и выполнения исключающего ИЛИ между ним и делителем dv8s. Если делимое или делитель отрицательное, будет установлен бит 7 знакового регистра — в противном случае он будет очищен.
div8s: mov d8s, dd8s; переместить делимое в регистр знака
eor d8s, dv8s; знак xor с делителем
Далее по отдельности проверяется знак делимого и делителя.Если они отрицательны, они преобразуются в беззнаковую величину с помощью инструкции neg.
sbrc dv8s, 7; если установлен старший бит делителя
neg dv8s; изменить знак делителя
sbrc dd8s, 7; если установлен старший бит дивиденда
neg dd8s; изменить знак делителя
Затем процедура деления может продолжаться точно так же, как и раньше, поскольку мы знаем, что используем два беззнаковых значения. Единственное отличие в том, что перед возвратом мы проверяем знаковый регистр d8s. Если установлен его самый старший бит, один из операндов был отрицательным, поэтому мы знаем, что результат должен быть отрицательным и преобразован обратно в знаковое значение с использованием neg
.
div8s:...
sbrc d8s, 7; если установлен MSB знакового регистра
neg dres8s; изменить знак результата
ret; возвращение
...
Конечно, если вы предпочитаете скорость размеру кода, эту подпрограмму можно изменить точно так же, как указанную выше, чтобы уменьшить ветвление. Посмотрим, удастся ли тебе написать это самостоятельно!
Заключение
Вот и все. 8-битное деление в сборке AVR. Не так уж и плохо, правда? Надеюсь, вы понимаете, насколько это дороже аппаратных процедур.Избегайте этого, если можете, но если вам нужно … теперь вы знаете, как это сделать.
Микроконтроллер
— Как выполнить деление двух чисел в PIC16F877A на языке ассемблера?
Это другой способ:
;
; Файл: main.asm
; Цель: PIC16F877A
; IDE: MPLABX v5.25
; Ассемблер: MPASM v5.84
;
; Описание:
;
; Показать реализацию и тест целочисленного деления.
;
;
; Примечания:
;
; Сообщение на форуме: https://electronics.stackexchange.com/questions/463950/how-do-i-perform-division-of-two-numbers-in-pic16f877a-in-assembly-language
;
;
список r = dec, n = 0, c = 132
errorlevel -302, -312
;
# включить "p16F877A.inc "
__CONFIG _FOSC_XT & _WDTE_OFF & _PWRTE_OFF & _BOREN_OFF & _LVP_OFF & _CPD_OFF & _WRT_OFF & _CP_OFF
#define FOSC (4000000)
#define FCYC (FOSC / 4)
;
; Определите макросы, которые помогут
; с выбором банка
;
#define BANK0 (h'000 ')
#define BANK1 (h'080 ')
#define BANK2 (h'100 ')
#define BANK3 (h'180 ')
;
; *********************************************** *********************
RESET_VECTOR код 0x000; вектор сброса процессора
нет
перейти к началу; начать инициализацию PIC
; ------------------------------------------------- -----------------------
Начните:
clrf INTCON; Отключить все источники прерываний
clrf TMR0
banksel BANK1
clrf PIE1
clrf PIE2
movlw b'11000000 '; Подтягивания отключены, INT край от низкого к высокому, предварительный масштаб WDT 1: 1
movwf OPTION_REG; Граница тактового сигнала TMR0 от низкого к высокому, тактовый сигнал TMR0 = FCY, предварительное масштабирование TMR0 1: 2
movlw b'11111111 ';
movwf TRISA
movlw b'01111111 ';
movwf TRISB
movlw b'11111111 ';
movwf TRISC
movlw b'11111111 ';
movwf TRISD
; отключить компараторы
movlw b'00000111 '
movwf CMCON
; Установите все входы АЦП для цифрового ввода / вывода
movlw b'00000110 '
movwf ADCON1
banksel BANK0
pagesel главная
перейти на главную
;
; Межжелковое деление 8x8 с остатком
;
; Частное = делитель / делитель с остатком
;
; Ввод: Dividen
; Делитель
;
; Выход: частное в WREG.
; Остаток
;
Div8x8_DATA udata 0x20
Делитель res 1
Дивиден
Коэффициент разрешения 1
Остаток 1
BitCount res 1
Код Div8x8_CODE
Div8x8:
banksel BitCount
movlw 8
movwf BitCount
clrf остаток
Div8x8Loop:
clrc
rlf Dividen, F
rlf Остаток, F
movf Делитель, Вт
subwf остаток, Вт
скпнз
goto BigEnough
skpc
goto TooSmall
Достаточно большой:
movwf остаток
bsf Dividen, 0
Слишком маленький:
decfsz BitCount, F
goto Div8x8Loop
movf Коэффициент, Вт
возвращение
;
; Основной цикл приложения
;
MAIN_DATA udata
Значение1 разрешение 1
Значение2 разрешение 1
MAIN_CODE код
главный:
;
; Цикл здесь вечно тестирует Dividen и Divisors
;
ProcessLoop:
;
;
banksel Value1
clrf Value1
decf Value1, F
clrf Value2
decf Value2, F
TestLoop:
banksel Value1
movf Value1, W
Banksel Divisor
movwf Divisor
banksel Value2
movf Value2, W
Banksel Dividen
movwf Dividen
позвонить в Div8x8
banksel Value1
decfsz Значение1, F
goto TestLoop
decf Value1, F
banksel Value2
decfsz Значение2, F
goto TestLoop
decf Value2, F
goto ProcessLoop
конец
Если вы собираетесь записать это как домашнее задание, по крайней мере, вы можете добавить комментарии.
8086 Инструкции по целочисленному делению, объясненные с помощью программ сборки
В этом руководстве мы увидим различные инструкции целочисленного деления, поддерживаемые микропроцессорами 8086. Мы также предоставим примеры программ на ассемблере для каждой инструкции деления.
В этой серии руководств по микропроцессору 8086, которые мы обсуждали ранее; Режимы адресации микропроцессора 8086, инструкции передачи данных 8086, инструкции целочисленной арифметики 8086, инструкции умножения целых чисел 8086.
8086 Microprocessor Division Instructions
Следующие инструкции поддерживаются микропроцессором 8086, который используется для выполнения операции деления:
- DIV (числа без знака)
- IDIV (числа со знаком)
- AAD
8086 Инструкция DIV (беззнаковые операнды)
Команда DIV выполняет деление двух беззнаковых операндов. Знаменатель находится в исходном операнде, и он не должен быть немедленным.Однако это может быть регистр или ячейка памяти. Есть четыре случая деления в зависимости от количества битов. Деление может быть:
- Байт с байтом
- Слово со словом
- Слово с байтом
- Двойное слово со словом
Байт с байтовым делением
В этом случае операнды знаменателя и знаменателя являются байтами. Номинатор находится в регистре AL, а AH устанавливается в ноль. После деления инструкция сохраняет частное в AL, а остаток в регистре AH.
Код примера сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DB 0F2H
NUM_2 DB 4H
.КОД
MOV BH, NUM_2; Загрузить числитель в BH
MOV AL, NUM_1; Загрузить знаменатель в AL
DIV BH; Разделить BH на AL
RET
Выход
Инструкция DIV делит BH на AL. F2, деленное на 04, дает частное 3C и дает 02 в качестве остатка. AL сохраняет частное, а остаток хранится в регистре AH.
Слово со словом Деление
В этом случае регистр AX содержит знаменатель. После деления частное сохраняется в регистре AX, а остаток поступает в регистр DX.
Код примера сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DW 0F213H
NUM_2 DW 41A8H
.КОД
MOV AX, NUM_1; Загрузить числитель в AX
DIV NUM_2; разделить AX на NUM_2
RET
Выход
Окно вывода показывает, что деление F213H на 41A8 дает остаток 2D1B и 03 как частное.
Слово с байтом
Числитель — это 16-битное слово, хранящееся в AX, которое делится с 8-битным знаменателем. После деления AL содержит частное, а AH будет содержать остаток.
Код примера сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DW 1B25H
NUM_2 DB 24ч
.КОД
MOV AX, NUM_1; Загрузить знаменатель в AX
DIV NUM_2; разделить AX на NUM_2
RET
Выход
ЧИСЛО_1 делится на ЧИСЛО_2, что дает частное C1 и остаток 01.Из содержимого регистра AX видно, что AH содержит остаток, а AL — частное.
Двойное слово за словом Divsion
Это последний случай деления, в котором числитель представляет собой 32-битное число, а знаменатель — 16-битное число. В этом случае AX и DX хранят числитель. Наиболее значимая часть находится в регистре DX, а младшие значащие биты числителя находятся в регистре AX.
После выполнения инструкции DIV остаток переходит в регистр DX, а частное находится в регистре AX.
Код примера сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DW 2413H
.КОД
MOV AX, 5670; Загрузить младшие байты числителя в AX
MOV DX, 4531; Загрузить старшие байты числителя в DX
DIV NUM_1; разделить AX на NUM_1
RET
Выход
Здесь числитель в DX и AX, который равен 45315670. Он делится на 4531, чтобы получить частное от 7D9A, которое находится в регистре AX.
Ошибка разделения или ошибка переполнения
Эмулятор сгенерирует прерывание типа 0, если:
- В случае байта с байтовым делением и слова с байтовым делением частное больше, чем FFH.
- В случае слова с разделением на слово и двойное слово на слово, если частное больше, чем FFFFH.
- Когда знаменатель равен 0.
Предположим, что числитель равен 5BBDH, а знаменатель — 21H.
Пример кода сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DW 5BBDH
NUM_2 DB 21H
.КОД
MOV AX, NUM_1; Загрузить знаменатель в AX
DIV NUM_2; разделить AX на NUM_2
RET
Выход
В этом случае окно вывода генерирует ошибку, потому что частное равно 2C7, что превышает максимальный предел регистра AL.Регистр AL может хранить данные только до FFH.
8086 Инструкция IDIV (подписанные операнды)
IDIV — это арифметическая инструкция, которая выполняет операцию деления между двумя числами со знаком. Исходный операнд в инструкции — делитель со знаком. Его можно указать с помощью любого режима адресации, кроме немедленного режима адресации. Как и инструкция DIV, инструкция IDIV также имеет четыре случая:
- Байт с байтом
- Слово со словом
- Слово с байтом
- Двойное слово со словом
1.Байт с байтом :
Для деления байта со знаком на байт со знаком числитель сохраняется в регистре AL, а затем знак расширяется до регистра AH. После деления частное переходит в регистр AL, а остаток — в регистр AH. Значение частного должно находиться в диапазоне от +127 до 127, иначе будет генерироваться прерывание ошибки деления.
2. Слово со словом:
В этом случае числитель находится в регистре AX, а оставшиеся старшие значащие биты заполняются копиями битов со знаком.Деление создает частное в AX и остаток в DX. Здесь диапазон частного составляет от -32767 до +32767.
3. Слово с байтом:
Если числитель — 16-битное слово, а знаменатель — байт, то регистры AL и AH сохранят частное и остаток. Но теперь в этом случае весь регистр AX будет хранить числитель.
4. Двойное слово за словом:
В последнем случае двойное слово со знаком делится на одиночное слово со знаком.Поскольку двойное слово имеет 32 бита, 16-битный регистр AX не может его хранить. Таким образом, регистр DX будет хранить наиболее значимые биты числителя, а наименее значимые биты находятся в регистре AX. После деления AX сохранит частное, а DX — остаток.
Пример кода сборки
Этот пример показывает, как все эти инструкции изменяют данные регистров.
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.ДАННЫЕ
NUM_1 DD 0C250A91H
NUM_2 DW -0B25H
NUM_3 DB 24H
.КОД
; Байт за байтовым знаковым делением
MOV AL, NUM_3
МОВ БЛ, 3
IDIV BL
; Пословно подписанный раздел
MOV AX, -265H
IDIV NUM_2
; Слово за байтом Знаковое деление
MOV AX, NUM_2
IDIV NUM_3
; Двойное слово по однословному делению со знаком
MOV AX, NUM_1
MOV DX, NUM_1 + 2
MOV BX, 0A234H
IDIV BX
RET
Выход
8086 Инструкция AAD
AAD — это мнемоника для «ASCII Adjust for Division».Эта инструкция используется перед делением двух распакованных чисел BCD, так что после деления полученное частное и остаток будут в распакованной форме BCD. Команда AAD выполняет и умножает AH на 10. Затем прибавляет AH к AL и после этого устанавливает AH в 0. Инструкция AAD может изменять флаги PF, SF и ZF.
Пример кода сборки
ORG 100ч
.МОДЕЛЬ МАЛЫЙ
.КОД
MOV AX, 3730H; ASCII для 70
MOV BL, 03H; Загрузить 03H в BL
AAD
DIV BL
RET
Команда «MOV AX, 3730H» сохраняет 37 в регистре AH и 30 в регистре AL.Инструкция AAD умножит AH на 10 и прибавит к AL. AL становится:
AL = (10) (37) + 1C = 3
И AH сбрасывается в 0. После этого инструкция деления выполняется для генерации частного и остатка в распакованных формах BCD.
6502 математика сборки: деление — простой алгоритм, использующий степени двойки
В сети доступно несколько подпрограмм, выполняющих целочисленное деление на языке ассемблера 6502. Тем не менее, я хотел найти собственное решение.Мне нужна была процедура деления для вычисления уклона м для моей программы рисования линий.
Я начал думать о чем-то очень простом, что я мог бы реализовать на BASIC в целях тестирования, а затем перевести на машинный язык.
Самый простой способ выполнить деление — использовать вычитание. Например, если нам нужно оценить:
10/2
Мы можем просто вычесть число 2 из числа еще 10 раз, пока делимое (10) не станет равным нулю или меньше делителя (2).Итак:
10-2 = 8 (1 раз) 8-2 = 6 (2 раза) 6 - 2 = 4 (3 раза) 4-2 = 2 (4 раза) 2-2 = 0 (5 раз)
Итак, 2 может содержаться в 10 пять раз. Вот что мы искали: 10/2 = 5, это просто.
Такой алгоритм довольно неэффективен, так как требуется много вычитаний. Мы можем улучшить этот алгоритм, используя степень двойки.
Принцип такой. Предположим, мы хотим выполнить A / B. Если мы удвоим B и если он все еще может содержаться в A, то мы знаем, что B может содержаться в A как минимум два раза.Если мы снова удвоим B и он все еще может содержаться в A, мы узнаем, что B может содержаться в A четыре раза… и так далее. В какой-то момент мы больше не можем умножать на два: A не может содержать такое кратное B. Итак, мы вычитаем наибольшее кратное B, которое может удерживать A, от самого A, и повторяем процесс с разницей, которая теперь является новый дивиденд. Такой подход резко снижает количество необходимых вычитаний.
Попробуем применить этот метод:
Надо выполнить: 10/2 Установите счетчик (CT) на 0: CT = 0 Установите частичный дивиденд на 0: K = 0 Удвойте делитель: 4, положите K = 4 4 может содержаться в 10? да.CT = 1 Теперь сложим два частных, чтобы получить: Итоговое частное = Q1 + Q2 = 4 + 1 = 5 Это окончательный результат, так как 10/2 = 5.
Следующая программа Commodore 64 BASIC демонстрирует описанный выше метод.
СКАЧАТЬ: алгоритм целочисленного деления (BASIC реализация)
Вы можете найти реализацию этого алгоритма на машинном языке Commodore 64 в исходном коде программы рисования линий.Я также создал тестовую версию, смешанную с программой BASIC. Код BASIC используется для ввода / вывода и показывает частное, вычисленное математическими подпрограммами BASIC.
ЗАГРУЗИТЬ: 31-битная на 15-битная ассемблерная программа с целочисленным делением (тестовая версия)
СКАЧАТЬ: 31-битная на 15-битная ассемблерная программа с целочисленным делением (исходный код)
Обратите внимание, что максимальное значение для B — 32767, так как делитель равен 15 битам. Этого было достаточно, чтобы вычислить наклон линий, но процедуру можно изменить, чтобы поддерживать более высокие значения для B.
Версия сборки использует таблицы для вычисления степеней двойки. Как видите, это прямой перевод программы BASIC, показанной ранее. Вы даже можете найти метки, соответствующие номерам строк BASIC. Обычно вам не советуют этого делать, но я обнаружил, что программу довольно сложно реализовать на языке ассемблера, поэтому я остался близок к версии BASIC, даже если полученный код очень далек от оптимального.