8086 Interrupt System: IVT, ISR Writing, and Hardware Interrupts

Every keypress, timer tick, and disk operation on an 8086 system is ultimately driven by interrupts. When hardware raises a signal, the CPU stops what it’s doing, saves its position, runs a handler routine, and returns to exactly the instruction it left — all in hardware, with zero polling overhead. The mechanism that makes this work is a 1 KB lookup table at the very bottom of memory, and a precise six-step acknowledgment sequence that every interrupt triggers. Understanding both is fundamental to writing system-level 8086 code.

The Interrupt Vector Table

The IVT sits at physical addresses 00000h–003FFh — the first 1 KB of memory. It holds 256 entries, one for each possible interrupt number (0–255). Each entry is a 4-byte far pointer: first the 16-bit IP offset (little-endian), then the 16-bit CS segment. Together they tell the CPU exactly where to jump when that interrupt fires.

To find the entry for interrupt N: multiply N by 4. INT 0 lives at 00000h, INT 1 at 00004h, INT 21h (the DOS API) at 00084h, INT FFh at 003FCh.

8086 Interrupt Vector Table layout showing 256 entries at 00000h-003FFh and the hardware interrupt acknowledgment sequence
The IVT layout at 00000h–003FFh and the hardware interrupt acknowledgment sequence.

In protected mode (80286+), the IVT is replaced by the Interrupt Descriptor Table (IDT). Unlike the IVT, which is always fixed at 00000h, the IDT can reside anywhere in memory — its base address and size limit are stored in the IDTR register, loaded with the LIDT instruction. Each IDT entry is 8 bytes (an interrupt gate, trap gate, or task gate descriptor) rather than the IVT’s 4-byte far pointer. Hardware and software interrupts in protected mode also pass through privilege-level checks: a user-mode program at ring 3 cannot invoke a ring-0 handler directly unless the gate’s DPL explicitly permits it, making it impossible for application code to hook system interrupts without OS cooperation.

Three Sources of Interrupts

SourceMechanismBlocked by CLI?Example
Hardware INTR pin8259 PIC asserts INTR; CPU reads vector on /INTAYes — IF must be 1Timer, keyboard, disk
Hardware NMI pinRising edge on NMI pin; always vector 2No — always firesRAM parity error
Software INT nInstruction in codeNo — always firesINT 21h, INT 10h
CPU exceptionInternal CPU faultNo — always firesINT 0 divide error
⚡ Key Takeaway — CLI Only Blocks Maskable Hardware Interrupts

CLI sets IF=0 and stops the CPU from acknowledging the INTR pin — but it cannot block NMI, software INT n, or CPU exceptions. All three fire regardless of IF. This means a critical section protected by CLI is still vulnerable to divide-by-zero or a RAM parity NMI. Keep CLI sections as short as possible: they delay every timer tick and keyboard scan for the entire duration.

Writing a Correct Hardware ISR

A hardware ISR fires at any point in your program’s execution — between any two instructions, with the program in an unknown register state. Your ISR must leave every register exactly as it found it. Four non-negotiable rules: save every register you touch, reload DS to your own segment, send EOI to the 8259 before returning, and use IRET (not RET) to exit.

my_isr PROC FAR
    PUSH AX
    PUSH BX
    PUSH DS
    MOV  AX, SEG my_data   ; reload DS -- interrupted code had its own DS
    MOV  DS, AX

    ; --- ISR work here ---
    INC  WORD PTR [tick_count]

    ; Chain to original handler (it sends EOI and updates system time)
    PUSHF
    CALL DWORD PTR [old_handler]  ; original BIOS timer ISR

    POP  DS
    POP  BX
    POP  AX
    IRET                    ; pops IP, CS, FLAGS -- not RET, not RETF
my_isr ENDP
⚡ Key Takeaway — The Four Non-Negotiable ISR Rules

Every hardware ISR must follow these four rules without exception: (1) PUSH every register you modify — the interrupted program doesn’t know you ran. (2) Reload DS to your own data segment — the interrupted code’s DS is not yours. (3) Send EOI (MOV AL,20h / OUT 20h,AL) to the 8259 before returning — skipping it freezes all further hardware interrupts. (4) Exit with IRET, never RET — RET pops only IP and leaves FLAGS and CS on the stack, corrupting SP immediately.

Installing and Restoring Vectors

Always use DOS INT 21h functions 35h (get vector) and 25h (set vector) rather than directly patching the IVT. DOS manages the IVT correctly across program loads and unloads. And always restore the original vector before your program exits — DOS does not do this for you.

.data
    old_timer  DD 0     ; storage for original INT 08h far pointer
    tick_count DW 0

.code
; --- Install ---
    ; 1. Save original vector
    MOV AH, 35h
    MOV AL, 08h         ; INT 08h = timer
    INT 21h             ; ES:BX = current handler
    MOV WORD PTR [old_timer],   BX
    MOV WORD PTR [old_timer+2], ES

    ; 2. Install new handler
    MOV AH, 25h
    MOV AL, 08h
    PUSH CS
    POP  DS
    MOV  DX, OFFSET my_isr
    INT  21h

; --- Main loop ---
    MOV AX, @data
    MOV DS, AX
wait_loop:
    CMP [tick_count], 182
    JB  wait_loop           ; wait ~10 seconds (182 ticks at 18.2 Hz)

; --- Restore ---
    MOV AH, 25h
    MOV AL, 08h
    LDS DX, [old_timer]     ; DS:DX = original handler
    INT 21h

    MOV AH, 4Ch
    INT 21h

Key DOS and BIOS Services

INTAHFunctionKey registers
21h09hPrint $-terminated stringDS:DX = string
21h08hRead key (no echo)AL = ASCII
21h2ChGet timeCH=hr CL=min DH=sec
21h35hGet interrupt vectorAL=vector; ES:BX=handler
21h25hSet interrupt vectorAL=vector; DS:DX=handler
21h4ChTerminate programAL = exit code
10h00hSet video modeAL=mode (03h=80×25 text)
10h02hSet cursor positionBH=page DH=row DL=col
16h00hWait for keystrokeAH=scan AL=ASCII

Common ISR Mistakes

MistakeWhat happensFix
RET instead of IRETFLAGS left on stack, SP wrong, immediate crashAlways IRET
No EOI to 8259No further IRQs delivered; system freezesMOV AL,20h / OUT 20h,AL
DS not reloadedISR reads/writes wrong memory segmentPUSH DS; load your segment; POP DS
No CLI when patching IVT directlyInterrupt fires with half-written vector; crashCLI before write; STI after (or use INT 21h)
Vector not restored on exitNext IRQ jumps to deallocated code; crashAlways restore with INT 21h AH=25h

Expanded BIOS Services

INT 10h — Video Services

AHFunctionKey input registersOutput
00hSet video modeAL = mode (03h = 80×25 text, 13h = 320×200 256-color)
02hSet cursor positionBH = page, DH = row (0-24), DL = col (0-79)
03hGet cursor positionBH = pageDH = row, DL = col, CX = cursor shape
06hScroll window upAL = lines (0 = clear), BH = attr, CH/CL = top-left, DH/DL = bottom-right
09hWrite character + attributeAL = char, BH = page, BL = attribute, CX = repeat count
0EhWrite char (teletype mode)AL = char, BL = color (graphics mode)Advances cursor automatically
0FhGet current video modeAL = mode, AH = cols, BH = page
; Clear screen and print 'A' at row 5, col 10 in bright red on black
    MOV AH, 06h     ; scroll up (clear)
    MOV AL, 00h     ; 0 lines = clear entire window
    MOV BH, 07h     ; fill attribute: white on black
    MOV CX, 0000h   ; top-left corner (row 0, col 0)
    MOV DX, 184Fh   ; bottom-right (row 24, col 79)
    INT 10h

    MOV AH, 02h     ; set cursor position
    MOV BH, 00h     ; page 0
    MOV DH, 05h     ; row 5
    MOV DL, 0Ah     ; col 10
    INT 10h

    MOV AH, 09h     ; write character + attribute
    MOV AL, 'A'
    MOV BH, 00h     ; page 0
    MOV BL, 0Ch     ; attribute: bright red on black
    MOV CX, 1       ; repeat count
    INT 10h

INT 13h — Disk Services

AHFunctionKey registersOutput / Error
00hReset disk systemDL = drive (00h=A, 80h=HDD0)CF=1 on error
02hRead sectorsAL=count, CH=cyl, CL=sector, DH=head, DL=drive, ES:BX=bufferCF=1 + AH=error code on failure
03hWrite sectorsSame as 02hCF=1 on error
08hGet drive parametersDL = driveCH=max cyl, CL=max sector, DH=max head, BL=drive type
; Read boot sector (sector 1, head 0, cylinder 0) of floppy A: into buffer
.data
    disk_buffer DB 512 DUP(0)

.code
    MOV AH, 02h         ; read sectors
    MOV AL, 01h         ; read 1 sector
    MOV CH, 00h         ; cylinder 0
    MOV CL, 01h         ; sector 1 (sectors start at 1)
    MOV DH, 00h         ; head 0
    MOV DL, 00h         ; drive A:
    MOV BX, OFFSET disk_buffer
    INT 13h
    JC  disk_error      ; CF=1: read failed; AH = error code

INT 16h — Keyboard Services

AHFunctionBlocks?Output
00hRead next keystrokeYes — waits until key pressedAH = scan code, AL = ASCII (0 for extended keys)
01hCheck if keystroke availableNo — returns immediatelyZF=1: no key waiting. ZF=0: AH=scan, AL=ASCII (key stays in buffer)
02hGet shift stateNoAL = shift flags (bit 0=RShift, 1=LShift, 2=Ctrl, 3=Alt)
; Non-blocking key poll: process keys while doing other work
poll_loop:
    MOV AH, 01h         ; check keyboard buffer (non-blocking)
    INT 16h
    JZ  no_key          ; ZF=1: buffer empty, continue main work

    MOV AH, 00h         ; consume the key
    INT 16h             ; AH = scan code, AL = ASCII
    CMP AL, 1Bh         ; ESC key?
    JE  exit_loop
    ; process AL here
no_key:
    ; main work continues here
    JMP poll_loop

TSR Programs — Terminate and Stay Resident

A TSR (Terminate and Stay Resident) is a program that hooks one or more interrupt vectors, then exits in a way that leaves its code and data permanently in memory. The next time that interrupt fires — hardware timer tick, keyboard press, any software INT — the TSR’s handler runs before (or instead of) the original handler. DOS provides two ways to go resident: INT 27h (legacy, limited to 64 KB) and INT 21h AH=31h (modern, supports any size and sets an exit code).

The critical rule: the memory the TSR occupies must be declared to DOS as used. If you exit normally with INT 21h AH=4Ch, DOS marks your segment free and will overwrite it the moment another program loads. Using AH=31h tells DOS to keep that memory allocated permanently — until the next reboot.

.MODEL SMALL
.STACK 100h
.DATA
    old_timer   DD 0        ; storage for original INT 08h vector
    tick_count  DW 0

.CODE
; ---- TSR handler: intercepts timer tick (INT 08h, IRQ0) ----
new_timer PROC FAR
    PUSH AX
    PUSH DS
    MOV  AX, @data
    MOV  DS, AX

    INC  tick_count         ; count ticks

    POP  DS
    POP  AX

    PUSHF
    CALL DWORD PTR [old_timer]  ; chain to original BIOS timer (sends EOI)
    IRET
new_timer ENDP

; ---- Initialization: runs once, then goes resident ----
main PROC NEAR
    MOV  AX, @data
    MOV  DS, AX

    ; 1. Save original INT 08h vector
    MOV  AH, 35h
    MOV  AL, 08h
    INT  21h                    ; ES:BX = current handler
    MOV  WORD PTR [old_timer],   BX
    MOV  WORD PTR [old_timer+2], ES

    ; 2. Install our handler
    MOV  AH, 25h
    MOV  AL, 08h
    PUSH CS
    POP  DS                     ; DS:DX = our handler
    MOV  DX, OFFSET new_timer
    INT  21h

    ; 3. Go resident: keep (paragraphs of code + data + stack)
    ;    DX = number of paragraphs to keep resident
    ;    Calculate: (end_of_program - start) / 16 + 1
    MOV  AH, 31h                ; Terminate and Stay Resident
    MOV  AL, 00h                ; exit code
    MOV  DX, (OFFSET main - OFFSET new_timer + 15) / 16 + 10h
    INT  21h                    ; DOS: stay resident, keep DX paragraphs
main ENDP

END main

Read Next & Related Articles

📚 Recommended Reading

FAQs

Q: Can software INT n be blocked by CLI?
No. CLI only blocks maskable hardware interrupts on the INTR pin. Software INT n, NMI, and CPU exceptions always fire regardless of IF.

Q: Why is INT 3 encoded as one byte (CCh) while INT 03h is two bytes?
Debuggers need to replace exactly one byte at any instruction address to set a breakpoint. The one-byte CCh encoding lets them do this without disturbing adjacent code. No other interrupt has this one-byte form.

Q: Does DOS restore interrupt vectors when my program exits?
No. If your ISR code is deallocated but your vector still points to it, the next interrupt crashes the system. Always restore with INT 21h AH=25h before calling INT 21h AH=4Ch to terminate.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.