Когда программист впервые открывает листинг на ассемблере, первая реакция — растерянность: где привычные «+», «−», «/»? Вместо них — лаконичные мнемоники ADD, SUB, MUL, DIV. Но за этой лаконичностью — полный контроль над тем, как именно процессор выполняет вычисления.
Этот материал пригодится тем, кто изучает низкоуровневое программирование, пишет код для встраиваемых систем или хочет разобраться, как работает процессор под капотом языков высокого уровня.
По данным Института инженеров электротехники и электроники, ассемблер по-прежнему входит в пятёрку языков, знание которых критично при разработке системного ПО и встраиваемых решений.
Как устроены команды вычислений
Каждая инструкция ассемблера — это символьная запись машинной команды. Она состоит из мнемоники (например, ADD) и операндов: источника и приёмника.
Операнды задаются через регистры процессора или ячейки памяти. Прямая передача данных между двумя ячейками памяти в одной команде невозможна — сначала значение загружается в регистр, затем записывается по нужному адресу.
Синтаксис выглядит так:
ADD <приёмник>, <источник> ; приёмник ← приёмник + источник
Приёмником может быть регистр или ячейка памяти, источником — регистр, ячейка памяти или непосредственное значение (константа). Результат команды всегда помещается на место первого операнда.
Флаги процессора (CF, ZF, SF, OF) изменяются после каждой арифметической команды. Они сигнализируют о переносе, нулевом результате, знаке и переполнении — и используются в условных переходах.
Понимание механизма флагов особенно важно при написании циклов и ветвлений: если не учесть флаг переноса CF после сложения многобайтовых чисел, результат окажется неверным без единого видимого сообщения об ошибке.
Арифметические операции в ассемблере: обзор команд
Ниже — сводная таблица основных команд. Она поможет быстро ориентироваться при написании кода.
| Инструкция | Операция | Пример | Результат в регистре |
|---|---|---|---|
| ADD | Сложение | ADD AX, BX | AX = AX + BX |
| SUB | Вычитание | SUB AX, CX | AX = AX − CX |
| MUL | Умножение беззнаковое | MUL BX | DX:AX = AX × BX |
| DIV | Деление беззнаковое | DIV BX | AX = DX:AX / BX |
| INC/DEC | Инкремент/декремент | INC AX | AX = AX + 1 |
Сложение и вычитание: ADD, SUB, ADC, SBB
ADD складывает два операнда и записывает сумму в приёмник. SUB вычитает источник из приёмника. Оба изменяют флаги AF, OF, PF, SF, ZF, CF.
MOV AX, 10 ; AX = 10
ADD AX, 5 ; AX = 15
SUB AX, 3 ; AX = 12
Для многобайтового сложения используют пару ADD + ADC (Add with Carry). ADC учитывает флаг переноса CF от предыдущей операции. Аналогично, SBB (Subtract with Borrow) выполняет вычитание с учётом заёма.
INC и DEC — компактная замена «ADD op, 1» и «SUB op, 1». Они занимают меньше байт в машинном коде, но не изменяют флаг CF, что важно при многоразрядных вычислениях.
Умножение: MUL и IMUL
MUL выполняет беззнаковое умножение. Один операнд всегда берётся из аккумулятора (AL, AX или EAX), второй задаётся явно.
Результат помещается в регистровую пару расширенной длины:
- байт × байт → AX;
- слово × слово → DX:AX;
- 32-бит × 32-бит → EDX:EAX.
IMUL работает с числами со знаком. В современных процессорах x86 он поддерживает форму с двумя и тремя операндами, что удобнее при генерации кода компиляторами.
Деление: DIV и IDIV
DIV — беззнаковое деление; IDIV — знаковое. Делимое задаётся парой регистров (DX:AX для 16-битной операции), делитель — явным операндом.
Результат делится на две части:
- частное → AX (или AL для байтового деления);
- остаток → DX (или AH).
Если частное не помещается в регистр-приёмник, процессор генерирует исключение деления (#DE, прерывание 0). Перед командой DIV необходимо обнулить DX командой XOR DX, DX, иначе в делимое попадёт мусор из регистра.
Регистры и память при вычислениях
Процессор x86 не работает с данными напрямую в памяти — перед любым вычислением значение нужно загрузить в регистр. Это не ограничение, а архитектурный принцип: регистры расположены внутри кристалла процессора и доступны на порядки быстрее, чем оперативная память.
Для арифметики в 32-битном режиме чаще всего используют EAX, EBX, ECX и EDX. EAX исторически называют аккумулятором. Именно в него MUL и DIV помещают результат, именно его проверяют первым при возврате значения из функции. ECX традиционно задействуют как счётчик в циклах, хотя жёсткого запрета на другое использование нет.
Обращение к памяти записывается в квадратных скобках. Запись [EBX] означает: взять число, которое находится по адресу из EBX, а не сам EBX. Такой способ называют косвенной адресацией. Он незаменим при работе с массивами: смещая регистр-указатель на размер элемента, можно последовательно перебирать ячейки без изменения кода.

Прямая передача данных между двумя адресами памяти одной инструкцией невозможна — это аппаратное ограничение x86. Промежуточный регистр обязателен всегда: сначала загрузить значение из источника, затем записать в приёмник отдельной командой.
В 64-битном режиме (x86-64) те же операции выполняются над регистрами RAX, RBX, RCX, RDX и т. д. Мнемоники команд не меняются — ADD, SUB, MUL, DIV — увеличивается лишь разрядность операндов. Переход с 32 на 64 бита не требует изучения нового набора инструкций, только понимания расширенных регистров.
Работа с отрицательными числами
Отрицательные числа в ассемблере хранятся в дополнительном коде (two’s complement). Это не соглашение — это аппаратный стандарт архитектуры x86. Чтобы получить отрицательное число, процессор инвертирует все биты и прибавляет 1.
Практически это означает: −1 в 16-битном регистре выглядит как FFFFh, −128 как FF80h. Никакого отдельного бита «минус» нет — знак определяется старшим битом числа.
Для смены знака операнда используют команду NEG:

NEG изменяет флаги AF, CF, OF, PF, SF, ZF. Если операнд равен нулю, флаг CF сбрасывается в 0; во всех остальных случаях CF устанавливается в 1.
Для знакового умножения и деления используют IMUL и IDIV. Перед IDIV необходимо корректно расширить знак делимого — иначе результат будет неверным.
Для этого применяют специальные команды расширения знака:
- CBW — расширяет AL в AX (байт → слово);
- CWD — расширяет AX в DX:AX (слово → двойное слово);
- CDQ — расширяет EAX в EDX:EAX (32 бит → 64 бит).
📍 Пример знакового деления −20 на 4:

Без CWD в DX оказался бы произвольный мусор, и результат деления был бы некорректным.
❗ Частая ошибка: использовать XOR DX, DX перед IDIV вместо CWD. Обнуление DX корректно только для беззнакового DIV. При делении отрицательных чисел через IDIV нужно именно расширение знака — иначе делимое DX:AX окажется положительным большим числом, а не ожидаемым отрицательным.
Типичные ошибки при написании кода
Анализ учебных программ показывает: большинство ошибок при написании арифметического кода на ассемблере носят системный характер и повторяются из проекта в проект.
1. Забыли обнулить DX перед DIV. Результат делимого (DX:AX) оказывается непредсказуемым. Правило: перед каждым DIV выполнять XOR DX, DX.
2. Операция память–память. Нельзя написать ADD [addr1], [addr2]. Нужен промежуточный регистр: MOV AX, [addr2] → ADD [addr1], AX.
3. Несоответствие размеров операндов. Складывать AX и BL в одной команде ADD нельзя — операнды должны быть одного размера. Используйте MOVZX или MOVSX для расширения.
4. Игнорирование флага CF при многобайтовых вычислениях. Для корректного сложения чисел, не вмещающихся в один регистр, нужна пара ADD + ADC.
5. Путаница MUL / IMUL. MUL трактует оба операнда как беззнаковые. Если одно из чисел отрицательное — результат будет неверным. Для чисел со знаком используйте IMUL.
✅ Контрольный вопрос перед каждой командой деления: «Что сейчас в DX?» Если неизвестно — обнулить. Это правило устраняет ~40% ошибок в новичковых листингах.
Практические примеры на ассемблере
Теория становится понятнее на конкретных листингах. Рассмотрим три задачи, которые встречаются в учебных программах и реальных проектах.
Вычисление суммы двух чисел
Задача: сложить два числа, введённых пользователем, и вывести сумму. Алгоритм: вычисления выполняются через регистры EAX и EBX.

Результат хранится в переменной result в памяти. Для вывода его нужно преобразовать из двоичного представления в ASCII-строку.
Деление с остатком
Задача: разделить 100 на 7, получить частное и остаток.

После выполнения DIV: AX = 14, DX = 2. Команда XOR DX, DX обнуляет DX без использования MOV — и это стандартный приём, часто встречающийся в оптимизированном коде.
Для знакового деления используют IDIV, а перед ним — инструкцию CDQ, которая расширяет знак EAX на EDX. Это корректная замена XOR DX, DX при работе с отрицательными числами.
Выводы: что нужно запомнить
Базовый набор арифметических команд ассемблера невелик, но каждая из них требует понимания контекста: какой регистр выступает аккумулятором, какие флаги устанавливаются, куда попадает результат.
Команды сложения и вычитания (ADD, SUB, ADC, SBB) работают симметрично и меняют флаги одинаково. Умножение и деление — асимметричны: результат хранится в паре регистров, что обязывает программиста следить за содержимым DX (или EDX) до и после каждой операции в ассемблере.
Правильная работа с регистрами и памятью — фундамент любого корректного листинга:
- косвенная адресация открывает возможности для работы с массивами;
- знание размеров операндов исключает ошибки переполнения.
Хорошая практика — после написания каждого арифметического блока кода явно документировать, что находится в каждом регистре. Это упрощает отладку и делает программу понятной при повторном чтении. Изучив арифметику, можно уверенно переходить к логическим командам и управлению потоком выполнения.
Бонус: ассемблер и языки высокого уровня
Когда компилятор C или C++ обрабатывает выражение a = b + c * d, он генерирует последовательность инструкций ассемблера: сначала умножение через IMUL, затем сложение через ADD, результат — в регистр, оттуда в память. Зная ассемблер, можно открыть дизассемблированный листинг и точно понять, что именно делает код под капотом — без догадок.
Это особенно важно при оптимизации узких мест. Компилятор иногда генерирует избыточные пересылки между регистрами или лишние обращения к памяти. Разработчик, владеющий ассемблером, видит это и может либо переписать критичный участок вручную, либо подсказать компилятору нужное поведение через встроенные директивы (inline asm).
Обратная связь работает в обе стороны: изучение ассемблера делает программиста точнее в языках высокого уровня. Понимание того, во что превращаются вычисления с плавающей точкой, как дорого обходится деление по сравнению со сдвигом, почему порядок полей в структуре влияет на скорость доступа — всё это приходит именно через знание низкоуровневых операций.
Вам нужна биржа фриланса для новичков или требуются разработчики сайтов?


Комментарии