8086 Stack, Control Flow & Interrupts

8086 control transfers either replace IP within a code segment or replace CS:IP across segments. Calls and interrupts make that transfer reversible by building precise word frames on SS:SP; RET, RETF, and IRET consume different frames.

Control flow changes the next CS:IP. A jump makes the change without recording a return path. A call records enough state for RET or RETF. An interrupt records the return address plus flags so IRET can restore both location and control state. Those frames look similar but are not interchangeable.

The stack is an address convention with exact ordering #

The 8086 stack occupies memory addressed through SS:SP and grows toward lower offsets. For a word push:

text
SP ← SP - 2
word [SS:SP] ← value

A pop performs the reverse:

text
value ← word [SS:SP]
SP ← SP + 2
Stack grows toward lower offsets lower address near CALL return IP ← SP after CALL · RET pops far CALL return IP ← SP after CALL · RETF pops first old CS pushed before IP interrupt return IP ← SP after entry · IRET pops first old CS saved FLAGS Real-mode vector entry type × 4 + 0 → new IP type × 4 + 2 → new CS load handler CS:IP after the interrupt frame is complete
The word at the lowest address is popped first. RET, RETF, and IRET require different frame shapes.

The stack has no type metadata. A word is “a saved register,” “a return IP,” or “an argument” only because producer and consumer agree on its position. A single unmatched push changes the meaning of every following pop or return.

Jumps choose a destination #

An unconditional JMP always transfers control. A conditional jump interprets flags from an earlier instruction. Short and near relative forms add a signed displacement to the IP of the following instruction; an intersegment far jump loads both CS and IP.

The machine does not remember why a flag has its value. A Jcc is correct only if the intended producer is still the most recent instruction to define every tested flag and if signed/unsigned interpretation matches the data.

LOOP decrements CX and branches while the result is nonzero. LOOPE/LOOPNE also inspect ZF. This implicit counter can be convenient, but it also hides a dependency that a nested loop or called routine may destroy.

Near and far procedures #

A near call stays in the current code segment:

  1. push the return IP (the address after CALL);
  2. load the target offset into IP.

RET pops that return IP. An optional immediate operand additionally discards a fixed number of argument bytes after the return address is consumed.

A far call changes segments:

  1. push the old CS;
  2. push the return IP;
  3. load the target CS:IP.

Because the stack grows downward, return IP is at the lowest address and is popped first. RETF pops IP and then CS.

The CPU supplies only this control-transfer frame. Which registers a callee preserves, where arguments live, whether BP becomes a frame pointer, and who removes arguments are calling-convention decisions. Mixing conventions can execute correct instructions and still corrupt the caller.

A frame-pointer convention #

A common 16-bit prologue is:

asm
push bp
mov  bp, sp
sub  sp, local_bytes

BP then provides stable access to caller data while SP moves for locals and temporary pushes. The matching epilogue restores the old stack position and BP before RET.

This is a convention, not a mandatory property of CALL. Handwritten assembly can omit it, and optimized code often does. A debugger needs unwind information or a recognized convention; scanning memory for plausible return addresses is only a heuristic.

Real-mode interrupt entry #

The 8086 supports 256 interrupt types. The interrupt vector table begins at physical address zero. Each four-byte entry contains:

text
address 4 × type + 0   handler IP
address 4 × type + 2   handler CS

On accepted interrupt entry, the 8086 builds a reversible frame:

  1. push FLAGS;
  2. clear IF and TF so maskable interrupts and single-step do not immediately recur;
  3. push the current CS;
  4. push the return IP;
  5. load new IP and CS from vector type × 4.

IRET pops IP, then CS, then FLAGS. A normal RET would consume only IP; RETF would consume IP:CS but leave the saved flags on the stack. Either mismatch destroys stack balance and interrupt state.

Interrupt sources are not equivalent #

Source Trigger Masking/selection
software INT n explicit instruction vector encoded by software
internal exception current instruction or trap state architecture assigns type and restart semantics
NMI pin external non-maskable request not controlled by IF
INTR pin external maskable request recognized when IF=1; vector supplied during interrupt acknowledge
single step TF=1 trap after instruction execution

CLI and STI control IF; they do not mask NMI or erase a pending device condition. An external interrupt controller prioritizes sources and supplies a vector, but the device's own status still has to be acknowledged in the correct order.

Handler responsibilities #

A robust real-mode handler must know:

  • which registers and segment registers it changes;
  • whether it may re-enable maskable interrupts and at what point;
  • how the device condition is cleared or acknowledged;
  • whether shared state can change in foreground code;
  • which stack is active and how much worst-case nesting it can hold;
  • whether it must chain to another handler;
  • that exit uses IRET, not RET.

Saving every register is safe but costs stack and latency. Saving too little leaks handler implementation into interrupted code. The contract should be explicit.

Stack replacement is a critical section #

Changing SS and SP is a two-register state transition. If an interrupt used the new SS with the old SP, it would write its frame into an unintended location. The 8086 defines an interrupt-inhibition window around loading SS so the immediately following stack-pointer update can complete; code must keep the paired update adjacent.

This mechanism is narrow. It is not a general critical-section primitive and does not protect arbitrary shared data.

Current x86 boundary #

Current Intel processors still enter real-address mode after reset and retain real-mode interrupt behavior for compatibility. Protected and long modes use descriptor-based interrupt tables, can change privilege and stacks, and may push additional state or error codes. Their return rules are defined by those modes, not by blindly extending the 8086 four-byte-vector model.

Failure signatures #

Symptom Check first
return jumps to data call/return kind mismatch, overwritten return IP, wrong SP
handler runs once, never again IF restoration, device acknowledgement, controller mask/end-of-interrupt
handler recurses until overflow source remains asserted or interrupts re-enabled too early
registers change after unrelated event handler save/restore contract
debugger cannot unwind missing/corrupt frame convention or wrong code/stack segment
failure begins after changing stacks SS:SP pairing and interrupt window

Start from the actual words at SS:SP, assign each one a producer, and match them to the exact consumer instruction. Stack corruption is a lifecycle mismatch, not a mysterious memory failure.

Sources

  1. docsThe 8086 Family User's Manualpdfnotes
  2. docsIntel 64 and IA-32 Architectures Software Developer's Manualpdf

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close