Article by Ayman Alheraki on January 11 2026 10:37 AM
Control flow instructions direct the execution sequence of a program by altering the normal linear flow of instructions. They enable decision-making, looping, subroutine calls, and interrupt handling, which are fundamental for implementing algorithms and managing program state. The x86-64 architecture provides a rich set of control flow instructions, encompassing unconditional jumps, conditional jumps, procedure calls and returns, loop control, and interrupt handling.
Proper support and understanding of these instructions are vital for designing an assembler that generates efficient and correct machine code.
Unconditional jumps cause the processor to immediately continue execution at a specified target address, without testing any condition.
JMP
Description: Transfers execution to a target address.
Forms: Near jump (within the current code segment), far jump (to a different segment), relative or absolute addressing.
Encoding: Supports short (8-bit relative displacement), near (32-bit relative), and far jumps.
Usage: Implement loop exits, unconditional branches, infinite loops.
Example:
JMP label causes execution to continue at label.
Conditional jumps transfer control depending on the state of CPU flags, which are affected by previous arithmetic, logic, or comparison instructions. This mechanism forms the basis for decision-making in assembly programs.
Common conditional jump mnemonics and their conditions:
JE / JZ: Jump if equal / zero flag set
JNE / JNZ: Jump if not equal / zero flag clear
JG / JNLE: Jump if greater (signed)
JGE / JNL: Jump if greater or equal (signed)
JL / JNGE: Jump if less (signed)
JLE / JNG: Jump if less or equal (signed)
JA / JNBE: Jump if above (unsigned)
JAE / JNB: Jump if above or equal (unsigned)
JB / JNAE: Jump if below (unsigned)
JBE / JNA: Jump if below or equal (unsigned)
JC: Jump if carry flag set
JNC: Jump if carry flag clear
JO: Jump if overflow flag set
JNO: Jump if overflow flag clear
JS: Jump if sign flag set
JNS: Jump if sign flag clear
JP / JPE: Jump if parity even
JNP / JPO: Jump if parity odd
Forms: Conditional jumps use relative displacements, generally 8-bit or 32-bit, allowing both short-range and long-range branching.
Flag dependency: Conditional jumps depend on flags such as Zero (ZF), Sign (SF), Overflow (OF), Carry (CF), and Parity (PF), reflecting results of previous instructions.
Usage: Fundamental in implementing if-else constructs, loops with conditions, and switch-case logic.
The x86-64 ISA provides specialized instructions to implement count-controlled loops efficiently.
LOOP
Decrements the RCX register (or CX/ECX depending on operand size) and jumps to the target if the counter is not zero.
Useful for simple counted loops without explicit decrement and compare instructions.
Encoded with short relative displacement (8-bit).
LOOPE / LOOPZ
Like LOOP but continues looping only if ZF is set (zero).
LOOPNE / LOOPNZ
Continues looping only if ZF is clear (not zero).
These instructions allow compact loop implementation but are less commonly used in modern compilers which prefer explicit decrement and conditional jump for clarity and performance.
Function calls and returns are handled through the stack and control transfer instructions that allow structured programming and recursion.
CALL
Transfers control to a procedure/subroutine.
Pushes the return address onto the stack automatically.
Supports near (within segment) and far (to different segments) calls.
Can be relative (short or near) or absolute.
Example:
CALL my_function
RET (Return)
Pops return address from the stack and jumps back.
Can have an optional immediate operand to adjust the stack pointer for cleaning up parameters in some calling conventions.
Example:
RET
RET 8 adjusts stack pointer by 8 bytes after returning.
Interrupt Return (IRET / IRETQ)
Returns from interrupt or exception handlers.
Restores flags, code segment, and instruction pointer from the stack.
Essential for system-level control flow in protected and long modes.
Indirect control flow instructions transfer control to an address held in a register or memory, supporting function pointers, virtual dispatch, and dynamic jumps.
Indirect JMP
JMP [RAX] or JMP RAX jumps to the address stored in RAX or memory location pointed by RAX.
Used for jump tables and function pointers.
Indirect CALL
Similar to indirect JMP but pushes return address.
Used for dynamic subroutine calls.
These instructions enable more flexible and dynamic control flow constructs required in object-oriented and dynamic programming paradigms.
Control flow related to hardware and software interrupts is critical for OS and low-level programming.
INT
Generates a software interrupt, invoking the corresponding interrupt handler.
Used for system calls and exception simulation.
Typical usage: INT 0x80 or INT 0x2E for system services in legacy systems.
IRET / IRETQ
Returns from interrupt by restoring processor state.
HLT
Halts CPU execution until the next external interrupt.
Used in power management and idle loops.
Modern x86-64 processors implement advanced branch prediction mechanisms to minimize performance penalties from control flow changes. Although this is a hardware detail, understanding the cost of mispredicted branches influences assembler design for optimization:
Minimizing conditional jumps inside tight loops improves pipeline efficiency.
Combining tests and jumps efficiently reduces instruction count and latency.
Aligning targets and frequently executed branches improves cache usage.
Jump instructions encode relative offsets with varying operand sizes:
Short jumps: 8-bit signed displacement (range ±128 bytes).
Near jumps: 32-bit signed displacement (range ±2GB).
Far jumps: segment selector plus offset for protected mode transitions.
Call and return instructions have similar operand encoding to jumps but involve stack operations.
Conditional jumps use opcodes ranging from 0x70–0x7F for short jumps and 0x0F 0x80–0x8F for near jumps.
Indirect jumps and calls use ModR/M byte addressing modes to specify register or memory operands.
Control flow instructions in the x86-64 ISA empower programmers to implement decision structures, loops, subroutines, and interrupt handling with precision and flexibility. Mastering these instructions is essential for creating correct, efficient, and optimized assembly code. An assembler must generate precise encodings, respect operand constraints, and ensure correct management of program flow and stack discipline.
This foundational knowledge prepares the reader to handle advanced program structures and lays the groundwork for system programming, OS development, and performance-critical applications.