Написать программу без разветвления логики почти невозможно — даже простейший калькулятор проверяет, не равен ли делитель нулю. В низкоуровневом программировании эту задачу решают переходы: безусловные и условные. Именно о последних пойдёт речь.
Материал будет полезен тем, кто изучает архитектуру x86, разбирает дизассемблированный код или хочет понять, как высокоуровневые конструкции if/else и циклы превращаются в инструкции процессора.
По данным Intel Software Developer’s Manual, архитектура x86 насчитывает более 20 различных команд условного перехода в Ассемблере. Каждая проверяет один или несколько битов регистра флагов EFLAGS/RFLAGS.
Как процессор принимает решения
Центральный процессор не «думает» — он механически проверяет биты. Для хранения результатов арифметических и логических операций в x86 есть специальный регистр: EFLAGS (в 32-битном режиме) или RFLAGS (в 64-битном). Каждый бит этого регистра отвечает за конкретный признак результата.
После выполнения любой арифметической или логической операции один или несколько флагов устанавливаются автоматически. Команды условного перехода затем читают эти биты и решают, куда передать управление.
Сами инструкции условного перехода флаги не изменяют. Они только читают их состояние и передают или не передают управление метке.
Флагов в EFLAGS больше десятка, но для ветвления чаще задействуют четыре: ZF, CF, SF и OF. Остальные (TF, IF, DF и другие) управляют режимами работы процессора и в пользовательском коде применяются редко.
Четыре ключевых флага EFLAGS
Разобраться в том, какой флаг за что отвечает, проще через конкретные сценарии, а не абстрактные определения. Ниже — таблица с четырьмя флагами, условиями их установки и соответствующими инструкциями перехода.
| Флаг | Название | Условие установки | Команды перехода |
|---|---|---|---|
| ZF | Флаг нуля | Результат операции равен нулю | JZ / JE / JNZ / JNE |
| CF | Флаг переноса | Беззнаковое переполнение / заём | JC / JNC / JB / JAE |
| SF | Флаг знака | Результат отрицателен (MSB = 1) | JS / JNS / JL / JGE |
| OF | Флаг переполнения | Переполнение при знаковой арифметике | JO / JNO / JG / JLE |
Разберём каждый флаг подробнее.
ZF — флаг нуля
Устанавливается в 1, когда результат последней операции равен нулю. Именно его проверяет пара JZ/JE (Jump if Zero / Jump if Equal). После команды CMP AL, BL флаг ZF = 1 означает, что AL и BL равны: вычитание дало ноль.
CF — флаг переноса
Сигнализирует о беззнаковом переполнении или заимствовании. Если сложить два однобайтовых числа и получить результат больше 255 (0xFF), процессор перенесёт старший бит и установит CF = 1. Это сигнал для JC (Jump if Carry).
SF — флаг знака
Копирует старший бит результата (MSB). Если результат отрицателен в знаковой арифметике — SF = 1. Используется в JL (Jump if Less) и JGE (Jump if Greater or Equal) при работе со знаковыми числами.
OF — флаг переполнения
Отличается от CF: OF = 1 при знаковом переполнении, когда результат выходит за допустимый диапазон со знаком.
Пример: сложение двух положительных чисел даёт отрицательный результат из-за переполнения. Проверяется командами JO и JNO.
Команда JMP: безусловный переход
Прежде чем говорить об условиях, нужно понять базу. Команда JMP передаёт управление по указанному адресу без каких-либо проверок — всегда и в любом случае. Флаги при этом не читаются и не изменяются.
Синтаксис:

Главное отличие JMP от команды CALL: JMP не сохраняет адрес возврата в стек. Это простой «прыжок», без возможности вернуться назад автоматически. CALL/RET используются для подпрограмм, JMP — для переходов внутри одного потока выполнения.
JMP умеет прыгать как вперёд, так и назад. Именно это делает возможными циклы — после тела цикла ставится JMP на начало, а выход организуется через условный переход.
По дальности прыжка JMP бывает трёх видов: короткий (SHORT, ±127 байт), ближний (NEAR, в пределах текущего сегмента) и дальний (FAR, между сегментами). В 32/64-битном защищённом режиме FAR-переходы практически не используются — вся программа работает в едином адресном пространстве.
Условный переход в Ассемблере: как это работает
Любая инструкция условного перехода в Ассемблере работает по одной схеме: проверить состояние одного или нескольких флагов и, если условие выполнено, перейти на метку. Если нет — перейти к следующей инструкции.
Алгоритм:
- Выполнить операцию, которая установит нужные флаги (например, CMP, SUB, TEST или ADD).
- Поставить инструкцию условного перехода сразу после неё.
- Указать метку, на которую нужно перейти при выполнении условия.
Простейший пример на NASM (x86-64):

Команда CMP — это вычитание без записи результата: она только устанавливает флаги. Именно поэтому сравнение в ассемблере и условный переход всегда идут в паре: сначала CMP или TEST, затем Jxx.
Все инструкции условного перехода (JZ, JNZ, JG, JL и другие) могут прыгать только в диапазоне ±127 байт от следующей команды (в формате SHORT). Для дальних переходов нужен дополнительный JMP.
Команды группируются по типу чисел. Для беззнаковой арифметики используют JA / JB / JAE / JBE (Above / Below). Для знаковой — JG / JL / JGE / JLE (Greater / Less). Путать их нельзя: для одних и тех же битов в регистре результаты будут разными.
Дальние переходы и архитектурные ограничения
Условные переходы в x86 имеют жёсткое ограничение по дальности: в коротком формате (SHORT) команда может прыгнуть не дальше чем на 127 байт вперёд или 128 байт назад от следующей инструкции. Это не случайность — такой диапазон помещается в один байт смещения, что делает инструкцию компактной: всего 2 байта против 6 у дальнего варианта.
Проблема возникает, когда тело условного блока разрастается. Если между инструкцией перехода и целевой меткой оказывается слишком много кода, ассемблер либо выдаст ошибку, либо автоматически переключится на ближний формат (NEAR) с 32-битным смещением — в зависимости от синтаксиса и версии транслятора. В NASM, например, можно явно указать jz near метка, чтобы сразу использовать длинную форму.
Когда нужно перешагнуть за пределы ближнего перехода, применяют инверсию условия с промежуточным JMP:

Такой приём называют «прыжком через прыжок». Он добавляет одну лишнюю инструкцию, но решает задачу без изменения логики.
В 64-битном режиме ближние переходы используют 32-битное смещение со знаком — диапазон расширяется до ±2 ГБ относительно текущей позиции. Для подавляющего большинства реальных программ этого более чем достаточно, и дальние FAR-переходы между сегментами практически вышли из употребления.
Понимание этих ограничений важно не только при написании кода вручную. Компиляторы C/C++ учитывают дальность переходов при генерации машинного кода и иногда меняют порядок блоков в памяти, чтобы горячие ветки оставались в коротком диапазоне — это ускоряет выборку инструкций и снижает давление на кэш.
CMP и TEST: подготовка к ветвлению
Большинство команд условного перехода опираются на флаги, которые устанавливают именно CMP и TEST. Понять разницу между ними — значит правильно строить логику ветвлений.
Команда CMP
CMP вычитает второй операнд из первого, но не записывает результат — только обновляет флаги. Типичный сценарий: проверить, равны ли два значения, больше или меньше одно другого.
Пример — знаковое сравнение двух значений в регистрах:

Здесь одна команда CMP «кормит» сразу три инструкции условного перехода — флаги не сбрасываются между ними.
Команда TEST
TEST выполняет побитовое AND и тоже не записывает результат. Её применяют, когда нужно проверить конкретный бит или убедиться, что значение не равно нулю:

Такой приём эффективнее, чем cmp eax, 0, потому что test eax, eax короче кодируется и выполняется за одну машинную операцию.
✅Типичный паттерн: test регистр, регистр + jz/jnz — быстрая проверка на ноль. Применяется повсеместно в системном коде и компиляторных оптимизациях.
Ещё одна тонкость: команды ветвления не сбрасывают флаги, поэтому после одного CMP или TEST можно поставить несколько условных переходов подряд. Это позволяет реализовывать конструкции с несколькими ветками без повторных инструкций сравнения.
Частые ошибки при работе с переходами
Большинство проблем при программировании условных переходов — это не синтаксические ошибки, а логические. Ассемблер скомпилируется, программа запустится, но поведёт себя не так, как ожидалось.
Итак, семь самых распространённых ошибок:
1. Смешение знаковых и беззнаковых команд. JG/JL — для знаковых чисел, JA/JB — для беззнаковых. Если поставить JA для знаковых, результат будет непредсказуемым при отрицательных значениях.
2. Инструкция между CMP и переходом изменяет флаги. Любая арифметическая или логическая операция после CMP перезапишет флаги. Переход нужно ставить сразу за CMP.
3. Выход за диапазон SHORT-перехода. Если метка дальше 127 байт, ассемблер либо выдаст ошибку, либо неверно рассчитает смещение. Решение — дополнительная команда JMP через промежуточную метку.
4. Бесконечный цикл из-за неверной логики флагов. Классика: условие выхода из цикла никогда не выполняется, потому что в теле цикла флаг сбрасывается раньше, чем дойти до проверки.
5. Путаница JZ и JE, JNZ и JNE. Это синонимы — они обрабатываются одинаково. Но читабельность важна: JE логичнее после CMP, JZ — после TEST.
6. Модификация флага CF при сдвигах. Команды SHL/SHR изменяют CF. Если после них стоит JC, результат может удивить.
7. Отсутствие JMP после первой ветки. В конструкции if/else без JMP в конце первой ветки выполнение «провалится» во вторую ветку.
Большинство этих ошибок легко обнаруживаются пошаговой отладкой с просмотром регистра флагов — в любом отладчике (GDB, x64dbg, OllyDbg) состояние EFLAGS отображается в реальном времени.
Итоги
Механизм переходов — фундамент управляющей логики в любой программе на ассемблере. Безусловная команда JMP обеспечивает базовое перенаправление потока. Условные инструкции — JZ, JNZ, JG, JL, JC и их синонимы — дают возможность строить полноценные ветвления и циклы, опираясь на биты регистра EFLAGS.
Четыре флага процессора — ZF, CF, SF и OF — покрывают большинство реальных задач: проверку равенства, сравнение знаковых и беззнаковых чисел, обнаружение переполнения. Подготовить нужные флаги помогают инструкции CMP и TEST — без них корректное ветвление в коде просто невозможно.
Понимание того, как логика условного перехода в ассемблере связана с флагами, — ключевой шаг от «читаю листинги» к «понимаю дизассемблированный код». Это пригодится при разработке системного ПО, анализе бинарников и оптимизации горячих участков кода.
Дальнейший шаг — изучить циклические команды LOOP/LOOPE/LOOPNE и понять, как компилятор C/C++ превращает for и while в конкретные последовательности CMP + J-инструкций. Это даст ещё более глубокое понимание архитектуры x86.
Вам нужна биржа фриланса для новичков или требуются разработчики сайтов?


Комментарии