Условный переход в Ассемблере: флаги процессора и команда JMP

Содержание

  1. 1.Как процессор принимает решения
  2. 2.Четыре ключевых флага EFLAGS
    1. 2.1.ZF — флаг нуля
    2. 2.2.CF — флаг переноса
    3. 2.3.SF — флаг знака
    4. 2.4.OF — флаг переполнения
  3. 3.Команда JMP: безусловный переход
  4. 4.Условный переход в Ассемблере: как это работает
  5. 5.Дальние переходы и архитектурные ограничения
  6. 6.CMP и TEST: подготовка к ветвлению
    1. 6.1.Команда CMP
    2. 6.2.Команда TEST
  7. 7.Частые ошибки при работе с переходами
  8. 8.Итоги
Хотите заказать настройку и доработку сайта?
Эксперты Ворк24 помогут!

Написать программу без разветвления логики почти невозможно — даже простейший калькулятор проверяет, не равен ли делитель нулю. В низкоуровневом программировании эту задачу решают переходы: безусловные и условные. Именно о последних пойдёт речь.

Материал будет полезен тем, кто изучает архитектуру 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 передаёт управление по указанному адресу без каких-либо проверок — всегда и в любом случае. Флаги при этом не читаются и не изменяются.

Синтаксис:

пример 1 синтаксис.png

Главное отличие JMP от команды CALL: JMP не сохраняет адрес возврата в стек. Это простой «прыжок», без возможности вернуться назад автоматически. CALL/RET используются для подпрограмм, JMP — для переходов внутри одного потока выполнения.

📌Обратите внимание!

JMP умеет прыгать как вперёд, так и назад. Именно это делает возможными циклы — после тела цикла ставится JMP на начало, а выход организуется через условный переход.

По дальности прыжка JMP бывает трёх видов: короткий (SHORT, ±127 байт), ближний (NEAR, в пределах текущего сегмента) и дальний (FAR, между сегментами). В 32/64-битном защищённом режиме FAR-переходы практически не используются — вся программа работает в едином адресном пространстве.

Условный переход в Ассемблере: как это работает

Любая инструкция условного перехода в Ассемблере работает по одной схеме: проверить состояние одного или нескольких флагов и, если условие выполнено, перейти на метку. Если нет — перейти к следующей инструкции.

Алгоритм:

  1. Выполнить операцию, которая установит нужные флаги (например, CMP, SUB, TEST или ADD).
  2. Поставить инструкцию условного перехода сразу после неё.
  3. Указать метку, на которую нужно перейти при выполнении условия.

Простейший пример на NASM (x86-64):

пример 2.png

Команда 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:

дополнительный.png

Такой приём называют «прыжком через прыжок». Он добавляет одну лишнюю инструкцию, но решает задачу без изменения логики.

В 64-битном режиме ближние переходы используют 32-битное смещение со знаком — диапазон расширяется до ±2 ГБ относительно текущей позиции. Для подавляющего большинства реальных программ этого более чем достаточно, и дальние FAR-переходы между сегментами практически вышли из употребления.

Понимание этих ограничений важно не только при написании кода вручную. Компиляторы C/C++ учитывают дальность переходов при генерации машинного кода и иногда меняют порядок блоков в памяти, чтобы горячие ветки оставались в коротком диапазоне — это ускоряет выборку инструкций и снижает давление на кэш.

CMP и TEST: подготовка к ветвлению

Большинство команд условного перехода опираются на флаги, которые устанавливают именно CMP и TEST. Понять разницу между ними — значит правильно строить логику ветвлений.

Команда CMP

CMP вычитает второй операнд из первого, но не записывает результат — только обновляет флаги. Типичный сценарий: проверить, равны ли два значения, больше или меньше одно другого.

Пример — знаковое сравнение двух значений в регистрах:

пример 3.png

Здесь одна команда CMP «кормит» сразу три инструкции условного перехода — флаги не сбрасываются между ними.

Команда TEST

TEST выполняет побитовое AND и тоже не записывает результат. Её применяют, когда нужно проверить конкретный бит или убедиться, что значение не равно нулю:

пример 4 команда тест.png

Такой приём эффективнее, чем 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.

Вам нужна биржа фриланса для новичков или требуются разработчики сайтов?

Комментарии

Нет комментариев
Не можешь разобраться в этой теме?
Обратись за помощью к фрилансерам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 1 дня
Безопасная сделка
Прямой эфир