Interrupt quick start

Overview

(In QB, numbers preceded by "&H" are hexadecimal; it's like C's "0x" prefix or the "h" sigil used in some doc. Also, backslash represents integer division.)

In short, interrupts are groups of system functions; they belong to various services and include functions for many aspects of your computer, from file system management to controller support and other graphic stuff. For instance, the DOS API (interrupt &H21) regroups a little more than a hundred functions that can be invoked any time (well, most of them at least) and will "interrupt" the execution of the current program to execute its own handler, just like any other QB procedure (SUB or FUNCTION) would. Interrupts are also used by some programs and hardware to signal when they're done doing something or need something from you... more or less like a tap on your shoulder to get your attention (and like a tap on your shoulder, it's considered rude not to respond.)

Getting started

First stop, the CALL INTERRUPTX procedure. QBASIC 1.1 (the free, home version of QuickBASIC) doesn't support CALL INTERRUPTX. Hopefully, Hans Lunsing wrote a replacement routine for that procedure in assembly (download the original here,) and it looks like this:

'
' Define the type needed for INTERRUPTX
'
TYPE RegTypeX
  AX AS INTEGER
  BX AS INTEGER
  CX AS INTEGER
  DX AS INTEGER
  BP AS INTEGER
  SI AS INTEGER
  DI AS INTEGER
  FLAGS AS INTEGER
  DS AS INTEGER
  ES AS INTEGER
END TYPE

'
' Generate a software interrupt, loading all registers
'
DECLARE SUB INTERRUPTX (intnum AS INTEGER, inreg AS RegTypeX, outreg AS RegTypeX)

'
' Call InterruptX assembly code by Hans Lunsing
'
SUB INTERRUPTX (intnum AS INTEGER, inreg AS RegTypeX, outreg AS RegTypeX) STATIC
  DIM aASM(94) AS INTEGER, offset AS INTEGER, sASM AS STRING

  DEF SEG = VARSEG(aASM(0))
  offset = VARPTR(aASM(0))

  IF (LEN(sASM) = 0) THEN
    sASM =        "558BEC8B5E0C8B170AF67407C707FFFF"
    sASM = sASM + "E9A7008B5E068B1F2E8897770032C080"
    sASM = sASM + "FA25740580FA2675020C02501E065657"
    sASM = sASM + "9C8B760A80FA207C0580FA307C0A817C"
    sASM = sASM + "08FFFF74038B6C088B440E25D50F508B"
    sASM = sASM + "048B5C028B4C048B54068B7C0CFF740A"
    sASM = sASM + "817C12FFFF74038E4412817C10FFFF74"
    sASM = sASM + "038E5C105E9DCD00558BEC9C83C50EF6"
    sASM = sASM + "46FE02740245451E568E5EFC8B760889"
    sASM = sASM + "04895C02894C048954068F440A897C0C"
    sASM = sASM + "8F44108C44128F440E8F4408F646FE02"
    sASM = sASM + "740244449D5F5E071F585DCA0800"

    FOR i% = 0 TO LEN(sASM) \ 2 - 1
      POKE offset + i%, VAL("&H" + MID$(sASM, 1 + i% * 2, 2))
    NEXT i%
  END IF

  CALL ABSOLUTE(intnum, inreg, outreg, offset, offset)

  DEF SEG
END SUB

QuickBASIC 4.5 and PDS 7.1 both support CALL INTERRUPTX via their default library. Run QB.EXE (or QBX.EXE) with the /L switch and add the line #INCLUDE: 'QB.BI' (or #INCLUDE: 'QBX.BI') to your source code and you're set. If you don't want to include header files (or are unable to,) you may declare INTERRUPTX and define the RegTypeX User-Defined Type (UDT) on your own, just copy the declaration of the code above.

With the inclusion of the library, we now have access to INTERRUPTX and RegTypeX, a UDT featuring 10 INTEGERs: AX, BX, CX, DX, BP, SI, DI, FLAGS, DS and ES. Each INTEGER represents a processor register (think of them as built-in processor variables.) What these registers mean is of little importance because there are no strict rules for their use, except that AX is used to invoke a function.

Some documents out there will refer to registers that do not seem to exist in the RegTypeX UDT, but rest assured you've got everything you need: if a register ends with H or L, it refers to either the high or low byte of a 16-bit register. Indeed, each register is 2-byte long but they are often broken into smaller 1-byte registers. For instance, AX can be broken into AH (high byte of AX) and AL (low byte of AX;) in other words, assuming we're working with UNSIGNED INTEGERS:

AX = AH * &H100 + AL
AH = AX \ &H100
AL = AX AND &HFF

Additionally, the FLAGS member contains status flags (CF, PF, AF, ZF, SF, OF) and control flags (TF, IF, DF.) So when you see something like: "CF clear if successful, set on error," you just have to test FLAGS against &H01 and see if the result yields anything. Here's the full list of these flags:

Calling an interrupt

Now that we have all this set up and working, we need to obtain interrupt and function numbers. Ralf Brown's amazing interrupt list is the place to go. Most of the time, I search for information in the interrupt list by Interrupt Number (if I suspect a function exists in a specific group) or by Command Category (if I only have a vague idea of what I'm looking for.) Searching is one part of the problem, understanding how to use the information is another. I'll hold your hand only to ease you into it, so please consent and do not call the cops on me, you socially-dysfunctional millennial. And now, for something a tad more exciting...

Open the interrupt list by Interrupt Number and then click 21 in the table to see the list of functions stored in interrupt &H21. Search "GET SYSTEM DATE" (the gig is up, it's not that exciting, sorry) and you should find: "Int 21/AH=2Ah - DOS 1+ - GET SYSTEM DATE." "Int 21" is obviously the interrupt number. "AH=2Ah" means that to access function &H2A, we only need to use the high byte of register AX. Right after the dash, we learn that this function applies to "DOS 1+" (DOS version 1 or more recent) and we learn that the function is "GET SYSTEM DATE." Let's click the link for more detail. The first part of the page lists the registers we need to set up and send to the function. The second part shows the information the function will return after execution: CX = year, DH = month, DL = day and AL = day of the week. Therefore, we can obtain the system date as so:

'$INCLUDE: 'QB.BI'

DIM regsIn AS RegTypeX  ' Registers sent
DIM regsOut AS RegTypeX ' Registers returned

' Get routine &H2A (set AH to &H2A and AL to &H00)
regsIn.AX = &H2A00

' Call interrupt &H21, send regsIn, wait for regsOut
CALL INTERRUPTX(&H21, regsIn, regsOut)

' Decode values obtained in regsOut
PRINT " Year:"; (regsOut.CX)
PRINT "Month:"; (regsOut.DX \ &H100)
PRINT "  Day:"; (regsOut.DX AND &HFF)

Neat, what else can we do?

The interrupt vector table...

When an interrupt is invoked, your computer looks up the address of the handler (a routine) and executes it right away. Terminate and Stay Resident (TSR) programs such as Creative Lab's SBMIDI and Z-Soft's FRIEZE may "register new interrupts" or add functionalities to existing interrupts. To make sure they are properly installed, we may have to read the interrupt handler's code. The question is, where could it be?

Interrupts are not whimsical spells, they are assembly code stored somewhere in memory. The segment and offset of each code snippet is stored in the "interrupt vector table" located at 0000:0000. Each entry is 4-byte long (two bytes for the offset and two bytes for the segment, in that order.) So, if we PEEK four bytes at offset &H21 (interrupt number) * 4 (length in bytes of one vector,) we should obtain the vector for interrupt &H21:

DIM vecOfs AS INTEGER ' Vector offset
DIM intNum AS INTEGER ' Interrupt number
DIM intSeg AS LONG    ' Interrupt code segment
DIM intOfs AS LONG    ' Interrupt code offset

' Get interrupt &H21 vector
intNum = &H21
vecOfs = intNum * 4

' Go to interrupt vector table and PEEK 4 bytes
DEF SEG = 0
intOfs = PEEK(vecOfs) + PEEK(vecOfs + 1) * 256&
intSeg = PEEK(vecOfs + 2) + PEEK(vecOfs + 3) * 256&
DEF SEG

PRINT "Interrupt &H"; HEX$(intNum); " address: "; HEX$(intSeg); ":"; HEX$(intOfs)

If you don't feel comfortable getting dirty, you may also use DOS interrupt &H21 function &H35 to obtain the interrupt vector (ES will return the interrupt handler segment, while EX will return the interrupt handler offset.)

' $INCLUDE: 'QB.BI'

DIM intSeg AS INTEGER, intOfs AS INTEGER, intNum AS INTEGER

intNum = &H21
intGet intNum, intSeg, intOfs
PRINT "Interrupt &H"; HEX$(intNum); " address: "; HEX$(intSeg); ":"; HEX$(intOfs)

'' Get interrupt vector (requires DOS2+)
SUB intGet(intNum AS INTEGER, intSeg AS INTEGER, intOfs AS INTEGER)
  DIM regs AS regTypeX

  regs.AX = &H3500 + intNum           ' AH = function &H35, AL = intNum
  CALL INTERRUPTX (&H21, regs, regs)  ' Call DOS interrupt &H21
  intSeg = regs.ES                    ' Return code memory segment
  intOfs = regs.BX                    ' Return code memory offset
END SUB

Thanks to the vector table, it is possible to "replace" an existing handler by our own without having to overwrite it entirely: all we have to do is replace the entry in the vector table by the address of our own routine and we're good to go. The following routine uses function &H25 to do just that (DS is used to pass the new address' segment and DX passes the offset:)

' $INCLUDE: 'QB.BI'

'' Set interrupt vector (requires DOS1+)
SUB intSet(intNum AS INTEGER, intSeg AS INTEGER, intOfs AS INTEGER)
  DIM regs AS regTypeX

  regs.AX = &H2500 + intNum           ' AH = function &H25, AL = intNum
  regs.DS = intSeg                    ' Provide code memory segment
  regs.DX = intOfs                    ' Provide code memory offset
  CALL INTERRUPTX (&H21, regs, regs)  ' Call DOS interrupt &H21
END SUB

Now we can add or modify interrupts if we need to by creating or replacing existing entries in the interrupt vector table. ALWAYS remember to restore the interrupt vector table the way you found it (save the initial vector somewhere so you can write it back later) and be EXTREMELY careful what you replace because you can seriously screw things up; BIOS interrupts are stored in ROM and DOS interrupts are loaded into memory when the machine is booted, so unless you manage to break the Read-Only Memory (somehow) or destroy DOS, a full reboot should fix it (as long as you did not cook your hardware to a crisp, but that's a given.)