The 8086 processor does not poll a hardware timer on each clock tick — it reacts to one. The system timer fires INT 08h approximately 18.2 times per second, and the CPU responds by suspending whatever it is currently executing, saving its state, and running the interrupt service routine (ISR) stored at the corresponding vector. Understanding how to intercept this interrupt, execute custom logic, and then chain back to the original BIOS handler is a foundational skill in low-level 8086 programming.
This post builds a working INT 08h handler step by step. The ISR increments a counter in memory on every timer tick, displays a visible marker, and correctly chains to the original BIOS routine before returning — the safe and production-correct pattern for timer-interrupt programming.
Background: The 8086 Interrupt Vector Table
The 8086 stores 256 interrupt vectors in the first 1 KB of memory, starting at address 0000:0000. Each vector is a 4-byte far pointer: two bytes for the offset and two bytes for the segment, stored in little-endian order. INT 08h therefore lives at physical address 0000:0020 (interrupt number 8 × 4 bytes = offset 32 = 0x20).
When an INT 08h occurs the CPU automatically:
- Pushes the flags register onto the stack.
- Pushes CS (current code segment).
- Pushes IP (next instruction offset).
- Loads CS:IP from the vector table at
0000:0020. - Clears the Interrupt-Enable flag (IF), preventing nested hardware interrupts.
The ISR must end with IRET, which pops IP, CS, and Flags back, restoring the interrupted context exactly.
The Program
; ============================================================
; 8086 Assembly: INT 08h External Timer Interrupt Handler
; Assembler : TASM / MASM
; Purpose : Install a custom ISR for the system timer tick
; (INT 08h, ~18.2 Hz). The ISR increments a memory
; counter, prints a marker to the screen, then
; chains to the original BIOS INT 08h handler.
; ============================================================
data segment
tick_count dw 0 ; incremented on every timer tick
marker db 'T', 07h ; character 'T' + BEL attribute for display
old_int08 dd ? ; 4-byte far pointer: saves original INT 08h vector
data ends
stack segment stack
db 256 dup (0) ; 256-byte stack (required — avoids LINK L4021 warning)
stack ends
code segment
assume cs:code, ds:data, ss:stack
; ============================================================
; new_timer_isr
; Called automatically by the CPU on every INT 08h tick.
; Must preserve ALL registers it modifies (8086 convention).
; Must end with IRET, not RET.
; ============================================================
new_timer_isr proc far
push ax ; save every register we touch
push bx
push ds
; Re-establish DS — interrupt context may arrive with any DS value
mov ax, data
mov ds, ax
; --- Increment the tick counter ---
inc tick_count ; tick_count = tick_count + 1
; --- Print marker 'T' to video memory (direct VRAM write) ---
; B800:0000 is the start of CGA/EGA text VRAM.
; Each character cell is 2 bytes: [char][attribute].
; We write to cell (0,79) — top-right corner — to avoid scrolling.
push es
mov ax, 0B800h
mov es, ax ; ES -> video RAM segment
mov bx, 79 * 2 ; offset of top-right cell (column 79, row 0)
mov ax, word ptr marker ; AH = attribute (07h), AL = 'T'
mov word ptr es:[bx], ax ; write char + attribute to VRAM
pop es
; --- Chain to original BIOS INT 08h handler ---
; We must call it so the BIOS time-of-day clock still updates.
; A far call through the saved 4-byte pointer in old_int08:
pushf ; simulate INT (CPU pushes Flags before vectoring)
call dword ptr old_int08 ; far call to original ISR; it will IRET back here
pop ds ; restore registers in reverse PUSH order
pop bx
pop ax
iret ; restore IP, CS, Flags — return to interrupted code
new_timer_isr endp
; ============================================================
; start (main program)
; Installs the ISR, waits for 36 ticks (~2 seconds), then
; restores the original vector before exiting to DOS.
; ============================================================
start:
mov ax, data
mov ds, ax
; --- Save the original INT 08h vector via DOS service 35h ---
mov ah, 35h ; DOS: Get Interrupt Vector
mov al, 08h ; interrupt number
int 21h ; returns ES:BX = current INT 08h handler address
mov word ptr old_int08, bx ; save offset
mov word ptr old_int08 + 2, es ; save segment
; --- Install our new ISR via DOS service 25h ---
push ds ; 25h uses DS:DX as the new vector
mov ax, cs ; our ISR is in the code segment
mov ds, ax
mov dx, offset new_timer_isr
mov ah, 25h ; DOS: Set Interrupt Vector
mov al, 08h
int 21h
pop ds ; restore DS to our data segment
; --- Enable interrupts and wait for ~36 ticks (~2 s) ---
sti ; set interrupt-enable flag
wait_loop:
cmp tick_count, 36 ; wait until 36 ticks have fired
jl wait_loop ; loop while count < 36
; --- Restore the original INT 08h vector ---
push ds
mov dx, word ptr old_int08
mov ax, word ptr old_int08 + 2
mov ds, ax
mov ah, 25h
mov al, 08h
int 21h
pop ds
; --- Exit to DOS ---
mov ah, 4Ch ; DOS: Terminate process
mov al, 00h ; exit code 0
int 21h
code ends
end start
How the Code Works
- Data segment variables —
tick_countis a word initialised to 0.old_int08is a double-word (4 bytes) that will hold the far pointer to the original BIOS handler.markerholds the two bytes written to video RAM: the character'T'and its display attribute. - Saving the original vector (DOS 35h) — INT 21h service 35h returns the current handler address for interrupt AL in ES:BX. We store both words into
old_int08before installing our own handler, so we can restore the system to its original state on exit. - Installing our ISR (DOS 25h) — Service 25h writes DS:DX into the interrupt vector table. We temporarily point DS at the code segment (where
new_timer_isrlives) and load DX with its offset. Interrupts must be disabled during this operation; DOS 25h does this internally. - Inside new_timer_isr — We re-establish DS immediately because the ISR can be called from any context and DS may not point to our data segment. After incrementing the counter and updating video RAM, we chain to the original BIOS handler. The
pushf / call dword ptr old_int08pair simulates an INT instruction: the BIOS handler expects Flags on the stack (as if it were reached by an INT), will execute, and willIRETback to our code at the instruction after thecall. - Wait loop — The main program spins on
tick_countuntil 36 ticks have been counted. At ~18.2 ticks per second that is roughly two seconds. This is a busy-wait for simplicity; production code would useHLTinside the loop to yield the CPU between ticks. - Restoring the vector — DOS 25h is called again with the saved address to put the original BIOS handler back. Failing to do this would leave the system without a working timer after the program exits, causing the BIOS time-of-day clock to freeze.
- IRET vs RET — The ISR ends with
IRET(Interrupt Return), notRET.IRETpops three words from the stack: IP, CS, and Flags.RETonly pops IP (near) or IP + CS (far). UsingRETinstead ofIRETwould leave the Flags on the stack, corrupting SP and causing a crash.
Sample Output
C:TASM> masm timer08.asm
Microsoft (R) Macro Assembler Version 5.00
0 Warning Errors
0 Severe Errors
C:TASM> link timer08.obj
Microsoft (R) Overlay Linker Version 3.60
(no L4021 warning — we defined a proper stack segment)
C:TASM> timer08.exe
[T appears in top-right corner of screen; flickers ~18 times per second]
[Program exits cleanly after ~2 seconds]
Common Mistakes to Avoid
| Mistake | Consequence | Fix |
|---|---|---|
| Forgetting to restore DS inside the ISR | All data-segment accesses read/write random memory | Always push ds / mov ax, data / mov ds, ax at the top of the ISR |
| Using RET instead of IRET | Flags are not popped — SP is wrong, system crashes | Always end an ISR with IRET |
| Not chaining to original INT 08h | BIOS time-of-day clock freezes; keyboard BIOS may malfunction | pushf / call dword ptr old_int08 before returning |
| Not saving registers before modifying them | Interrupted program sees corrupted register values | PUSH every register at ISR entry; POP in reverse order before IRET |
| Not restoring the vector on program exit | System timer broken after your program exits | Call DOS 25h with saved address before int 21h / ah=4Ch |
See Also
- 8086 Assembly: PUSH, POP, CALL, and RET – Stack Operations Explained
- Implementing FCFS Scheduling Algorithm in C++
- Implementing Round Robin Scheduling Algorithm in C++
Conclusion
Intercepting INT 08h requires four things: save the original vector, install your own far-procedure ISR, chain to the original handler inside the ISR, and restore the vector on exit. Miss any one of them and the system becomes unstable. The key rule is that an ISR is not a normal procedure — it must preserve every register it touches, re-establish its own segment registers, and always end with IRET rather than RET. Get those four rules right and you can attach custom logic to any of the 8086’s 256 interrupt vectors using exactly the same pattern shown here.