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:
SP ← SP - 2
word [SS:SP] ← value
A pop performs the reverse:
value ← word [SS:SP]
SP ← SP + 2
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:
- push the return
IP(the address afterCALL); - 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:
- push the old
CS; - push the return
IP; - 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:
push bp
mov bp, sp
sub sp, local_bytesBP 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:
address 4 × type + 0 handler IP
address 4 × type + 2 handler CS
On accepted interrupt entry, the 8086 builds a reversible frame:
- push
FLAGS; - clear
IFandTFso maskable interrupts and single-step do not immediately recur; - push the current
CS; - push the return
IP; - load new
IPandCSfrom vectortype × 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, notRET.
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.