8086 Assembly: Handling the External Timer Interrupt (INT 08h)

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:

  1. Pushes the flags register onto the stack.
  2. Pushes CS (current code segment).
  3. Pushes IP (next instruction offset).
  4. Loads CS:IP from the vector table at 0000:0020.
  5. 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

  1. Data segment variablestick_count is a word initialised to 0. old_int08 is a double-word (4 bytes) that will hold the far pointer to the original BIOS handler. marker holds the two bytes written to video RAM: the character 'T' and its display attribute.
  2. 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_int08 before installing our own handler, so we can restore the system to its original state on exit.
  3. 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_isr lives) and load DX with its offset. Interrupts must be disabled during this operation; DOS 25h does this internally.
  4. 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_int08 pair simulates an INT instruction: the BIOS handler expects Flags on the stack (as if it were reached by an INT), will execute, and will IRET back to our code at the instruction after the call.
  5. Wait loop — The main program spins on tick_count until 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 use HLT inside the loop to yield the CPU between ticks.
  6. 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.
  7. IRET vs RET — The ISR ends with IRET (Interrupt Return), not RET. IRET pops three words from the stack: IP, CS, and Flags. RET only pops IP (near) or IP + CS (far). Using RET instead of IRET would 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

MistakeConsequenceFix
Forgetting to restore DS inside the ISRAll data-segment accesses read/write random memoryAlways push ds / mov ax, data / mov ds, ax at the top of the ISR
Using RET instead of IRETFlags are not popped — SP is wrong, system crashesAlways end an ISR with IRET
Not chaining to original INT 08hBIOS time-of-day clock freezes; keyboard BIOS may malfunctionpushf / call dword ptr old_int08 before returning
Not saving registers before modifying themInterrupted program sees corrupted register valuesPUSH every register at ISR entry; POP in reverse order before IRET
Not restoring the vector on program exitSystem timer broken after your program exitsCall DOS 25h with saved address before int 21h / ah=4Ch

See Also

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.

Leave a Reply

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