Calling Interrupts

Atrevida Game Programming Tutorial #5
Copyright 1997, Kevin Matz, All Rights Reserved.

"Prerequisites:"

  • Chapter 1: Introduction to Binary and Hexadecimal
  • Chapter 2: Binary Operations
  • Chapter 3: Binary Manipulations
  • Chapter 4: Memory in the PC

  •        
    In this chapter, we will learn about the PC's interrupts: what they are and how they work, the different types of interrupts, the BIOS and DOS services, and how to call interrupt services in your C or C++ programs.

    What is an interrupt?

    An interrupt is a special type of signal that is sent to the processor. Interrupts can be produced by hardware devices, or specific interrupts can be generated by a program. The processor itself can even generate an interrupt. When the processor receives an interrupt signal, it essentially stops what it is doing, executes a certain section of code called an interrupt handler, and then goes back to what it was doing before the interrupt was received. (That's why they are called interrupts.)

    Whenever you press a key on the keyboard, the keyboard (or actually, a chip connected to the keyboard) issues an interrupt to the processor. If you have a mouse driver installed, moving the mouse or pressing the mouse buttons will cause the mouse driver to generate interrupts. A timer chip in the computer generates a certain interrupt roughly 18.2 times per second. If your program tries to divide by zero, the processor generates a divide-by-zero interrupt (interrupts generated by the processor are often called exceptions).

    Our programs can also generate interrupts. Because you normally generate interrupts to initiate certain DOS or BIOS services, we often say that we are calling an interrupt. There are services available for accessing many devices, such as the video display, keyboard, printers, and disk drives.

    Interrupts can have different priorities assigned to them. If two interrupts were to arrive at the processor at about the same time, the processor would deal with the interrupt with the higher priority first, and then deal with the lower priority interrupt afterwards.

    The highest-priority interrupt signal is the non-maskable interrupt, or NMI. The NMI is used for warning the processor about a serious hardware failure, perhaps a memory parity error or some other critical situation. It is possible to instruct the processor to temporarily ignore interrupt signals, but the NMI cannot be disabled in this fashion -- it's non-maskable.

    There are 256 possible interrupts, 0 hex through FF hex (interrupts are almost always referred to in hexadecimal). Most assembly language books refer to interrupts with a syntax like "INT 33h" for interrupt 33 hex, so that's the syntax we'll use here. (This is just a minor syntax issue, but normally, I'd write "033h" instead of "33h". Actually, the preceeding zero is not strictly necessary for hex numbers that start with a digit in the range 0..9. But it's a good idea to keep in the habit of using the preceeding zero for the remaining hex numbers, because you want to prevent confusion between, say, D9 hex, the number, and D9, a variable. This is more of an issue in assembler than in C.)

    Some interrupts may have more than one service associated with it. For example, one interrupt might be designated for dealing with video services, and it might contain separate services for writing characters, reading characters, changing screen modes, and so on. The services are usually numbered in hexadecimal, starting at 0 hex.

    BIOS and DOS services, in general

    The BIOS, or basic input/output system, provides simple, low-level functions that access the system's hardware. Calling a BIOS service should have the same effect on all PC's. If I were to call a disk-accessing service of the BIOS, it would perform the same task on all compatible computers, whether the disk in question was a hard disk or a 1.44MB diskette, or a 1.2MB floppy, or a 360K floppy for that matter, or any other disk media. Even if two computers had different types or brands of disk drives or controller cards, the BIOS service that is called should perform the same task on both computers. The actual machine-language code for each BIOS function might be different on different computers (imagine that company X's disk drive requires different commands than company Y's disk drive), but the effects (what the function does) are the same.

    As another example, we'll be using the BIOS' video display services later in a sample program. The same BIOS functions do the same task (eg. displaying characters on the screen) no matter what video card is installed.

    DOS (or some other operating system) essentially uses the BIOS as a "foundation" for its functions. DOS provides operating system services that are more sophisticated and provide more functionality than the BIOS services. The DOS services often call the BIOS services to get their work done:

                      calls                calls                accesses
    Compiled Program  ---->  DOS Services  ---->  BIOS Services  ---->  Hardware
    

    DOS, because it is a Disk Operating System, provides file-handling capabilities such as opening and closing files, reading and writing data, creating and deleting directories, reading lists of files from a directory, and so on. BIOS' disk-oriented services are much less "civilized": it provides functions for simpler services such as reading and writing disk sectors or formatting specific tracks of a disk.

    For the best compatibility, you should call DOS functions if you are given the choice. Often, however, the BIOS functions are slightly faster, because they are generally simpler. If you're absolutely concerned about speed, you should write your own functions in assembler that directly control the hardware. The problem is, PC compatible computers often aren't as compatible as we'd like to think. So, if you write assembler code, you get more speed, but you give up compatibility (your assembler code will only function with the correct hardware). At least the BIOS and DOS functions are more or less guaranteed to function correctly on compatible computers. (We'll get started in assembler in another chapter.)

    A quick overview of the 8088's registers

    The processor, the 8088, has a set of "built-in variables" or "built-in memory locations" called registers. That is, these variables are not held in memory. The registers are our primary method of communication with the processor. Each register in the 8088 has a width of 16 bits (one word), but some of the registers can be split into two bytes, and each byte can be accessed separately. The following is a diagram showing the 8088's register set. Notice that the registers are not "little endian"; numbers in a register are "continuous" or "big endian".

          |<------------------- Word -------------------->|
          |<---- Byte (high) ---->|<---- Byte (low) ----->|
    
    bits:  15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    AX    |          AH           |          AL           |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    BX    |          BH           |          BL           |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    CX    |          CH           |          CL           |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    DX    |          DH           |          DL           |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    CS    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    DS    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    ES    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    SS    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    IP    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    SI    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    DI    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    SP    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    BP    |                                               |
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    FLAGS |  |  |  |  |OF|DF|IF|TF|SF|ZF|  |AF|  |PF|  |CF|
          +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    

    The first set of registers, AX through DX, are called the scratch-pad registers or general purpose registers. You can use them for almost any purpose you like. You can temporarily store data here for use by the processor. These registers can be broken up into bytes; the most-significant byte of AX is called AH, and the least-significant byte of AX is called AL. You can still access all of AX as one word. (The letters A through D supposedly stand for Accumulator, Base, Count, and Data. AX often holds results from mathematical operations. BX is sometimes used for storing a base address (an offset). CX is often used in loops, as a kind of a counter variable. DX sometimes holds miscellaneous data for certain uses.)

    The second set of registers are called the segment registers. These registers hold the segment portions of segmented addresses. The third set of registers are the offset registers. The segment and offset registers are usually used together in these pairs:

    CS:IP (code segment:instruction pointer) -- the address stored in these registers points to the next machine-language instruction in memory that is to be executed by the processor.

    DS:SI (data segment:source index) -- the address stored in these registers points to a byte or word in the data segment. It can be used to reference a variable. It is often used to specify the start of a string of bytes or words that is to be copied to a position specified by ES:DI (described next).

    ES:DI (extra segment:destination index) -- this address can point anywhere in memory, although it usually points somewhere in the data segment (so that DS = ES). It is often used to specify the destination address to which a string of bytes or words will be copied from DS:SI.

    SS:SP and SS:BP (stack segment: and stack pointer or base pointer) -- point to certain elements on the system's stack.

    The FLAGS register holds many flag bits which store information that is important to the processor. These flags can also be read (and changed) by your programs. For example, CF, the carry flag, is set when a mathematical operation experiences a carry in the last bit. As another example, if the interrupt flag (IF) is set, the processor recognizes interrupt signals; if it is cleared, the processor ignores interrupt signals (except for the NMI). Here's the list of the flags:

    • CF: carry flag
    • PF: parity flag
    • AF: auxiliary carry flag
    • ZF: zero flag
    • SF: sign flag
    • TF: trap flag
    • IF: interrupt flag
    • DF: direction flag
    • OF: overflow flag

    Later processors, such as the 80286, 80386, and up, have more registers (and flag bits) available for use.

    It's not terribly important to understand all of these registers at this point. We will gain lots of experience with them in the chapters on assembler. In those chapters, we will also discuss topics such as the stack and the individual flags. For now, just be aware that byte- and word-sized data are normally stored in the scratch-pad registers, and addresses are stored in the segment and address registers.

    How to call an interrupt in C and C++

    The commands used in this section work with Turbo/Borland C and C++, but are not guaranteed with other compilers. Check your manual!

    You first need a reasonably good interrupt listing, so you know how to set up the processor's registers for each interrupt. If you were lucky enough to receive a technical reference manual when you purchased your computer, it will most likely have such a listing. (Just try to get any manual with a computer these days...) Many assembler and DOS books contain interrupt listings in an appendix. I happen to use a listing from a fairly old book on BASIC (stop laughing!). Or, here's an excellent, free interrupt listing that's huge: Ralf Brown's interrupt list! There are WWW-based versions of this list on the Internet; the two I'm currently aware of are at http://ctyme.com/rbrown.htm and http://www.delorie.com/djgpp/doc/rbinter/. Alternately, do an FTP search for "inter*.zip"; the current version as of this writing has files inter53a.zip through to inter53g.zip. Of course, by the time you read this, these addresses and filenames may no longer be valid. Try searching for "interrupt" and "Ralf Brown".

    Say we want to write a character to the screen using a BIOS function. Scanning through my list, I find INT 10h, service 0Ah. The listing looks like this (slightly re-written to try to avoid copyright violations):

    INT 10h, Service 0Ah
    Write Character Only at Cursor Position
    
    Input:   AH = 0Ah
             AL = ASCII code of character to write
             BH = Screen page number
             CX = Count of characters to write
    
    Output:  The character is written CX times to the screen, starting at
             the current cursor position.
    

    So, to call this service, we need to set up the registers, and then call the interrupt. Here's the easy way to set registers in Turbo/Borland C and C++: write an underline, and then the name of the register (in capital letters). This can now be treated as a variable. We can write "_AX = 0x78B0;" to put the value 78B0 hex into register AX. We could also write "_DL = 55;" to put 55 hex into the byte-sized register DL. And we could also say "x = _SS;" to store in x the segment part of the address of the start of the stack segment. (Generally, it's best to avoid setting segment and offset registers -- doing this without the proper precautions can cause some nasty problems.) Then, once the registers are set up, you can call a function called geninterrupt() (remember to "#include <dos.h>").

    Here's an example using the above listed BIOS service. Let's write ten asterisks ("*"'s) to the screen at the current cursor position. The screen page number is almost always zero (unless you call another service to change the video screen page).

    #include <dos.h>
    
    int main ()
    {
        _AH = 0x0A;             /* Specify that we want service number 0Ah */
        _AL = '*';              /* (ASCII 42 dec = '*') */
        _BH = 0;                /* Screen page 0 */
        _CX = 10;               /* Repeat the character ten times */
        geninterrupt (0x10);    /* Call interrupt 10h. */
    
        return 0;
    }
    

    Notice that we don't need to change any registers that aren't mentioned. Also notice that we only need to specify the interrupt number with geninterrupt(), and not the service number. The service number typically is placed in register AH before calling an interrupt, although it's more of a custom than a rule: you could possibly see some interrupts using another scheme for specifying particular services.

    If an interrupt service was to return something in a register, we could write, say, "x = _CL;", after the geninterrupt() call, to retrieve the returned value (if the returned value was placed in register CL). We must do this immediately after the geninterrupt() call, however, as calling other functions (including functions in C or C++'s standard libraries) or using any operators can invalidate the values stored in the registers. (These functions and operations must use the registers to get their work done).

    Now, earlier, I warned against setting the segment and offset registers using the geninterrupt() method described above. Again, this is because the machine code that the C/C++ compiler generates needs these registers to keep track of its data, stack, code, and so on. It doesn't expect to have them altered, and altering some of them could potentially bring disastrous results (a system crash or reboot). Normally, we use the stack to save registers first, call the interrupt, retrieve any returned values from the registers, and then restore the registers from the stack, but I'd rather save the full discussion of the stack for a later chapter.

    It turns out that very few interrupt services actually use registers other than the scratch-pad registers. So if you're not particularly interested, skip the next paragraph.

    To use the other registers with interrupt services, we can actually cheat a little. We can change the ES register quite freely, as long as we set it back to its original value after the interrupt is called. We can change the DS register, as long as we set it back to its original value later, but we have to be sure not to access any (global) variables between the setting and re-setting of DS. (Depending on the memory model and the compiler, you should be able to get away with accessing a local variable, and just avoid using global variables, but I prefer not to risk it. Changing DS for the purposes of an interrupt is fairly rare, anyways.) DI and SI can be changed at will. SS, BP, and SP should never need to be changed for an interrupt service. CS and IP should never be changed either, and in fact, you are prevented from doing so if you try. (You have to use indirect methods to do this.)

    Turbo/Borland C and C++ provide alternate methods for calling interrupt services. There are functions defined in dos.h such as int86() and int86x(); these functions make use of a union structure called REGS. The advantage this provides is that register values are saved in an instantiation of REGS for later use. I'm not going to go into using these functions here, as I think the geninterrupt() method is simpler (and I am certain that that method is faster than using the int86(), et al, functions).

    In the chapters on assembler, I'll introduce Turbo/Borland C/C++'s inline assembler capability, which gives us another way to call interrupts.

    Let's try another example of calling an interrupt. We'll call a DOS service, INT 21h, Service 09h. This service prints out a string that is terminated with a dollar sign. (It's unfortunate that this service is so limited. The dollar sign is a legitimate character! C/C++'s null-terminated, or "ASCIIZ", strings are much more sensible.) Here's the listing:

    INT 21h, Service 09h
    "$"-Terminated String Print
    
    Input:   AH = 09h
             DS:DX points to a string that ends with "$"
    
    Output:  The string is written to the screen (actually, stdout) at the
             current cursor position.
    
    Here's one way to use this service:

    #include <dos.h>
    
    /* Note: global variables are normally stored in the data segment (DS). */
    char my_string[] = "This is my test message.$";
    
    int main ()
    {
        /* my_string[] is in the data segment, so DS does not need to
           be changed. */
        _DX = FP_OFF(my_string);        /* Offset address of the string */
        _AH = 0x09;                     /* INT 21h, Service 09h */
        geninterrupt (0x21);            /* Call INT 21h */
    
        return 0;
    }
    

    Notice that it was possible here to avoid manipulating DS. Global variables, such as my_string[], are placed by the compiler in the data segment, so it is unnecessary to change DS before and after calling the interrupt; it already equals the correct value.

    Summary

    We've covered plenty of material in this chapter. We've learned what interrupts are, and we've learned some basic information about BIOS and DOS and the services they provide. We've been introduced to the 8088 processor's registers. Finally, we saw a method for calling interrupts from our C and C++ programs.

    It would be a good idea to get an interrupt list, whether in a book or in on-line form, as soon as possible. I'd suggest trying out a few interrupts that sound interesting -- there are DOS and BIOS services to control the video display, the printer, the disk drives (be careful!), and so on. For example, try DOS interrupt 21h, service 02Ah; it will return the current date in certain registers.

      

    Copyright 1997, Kevin Matz, All Rights Reserved. Last revision date: Sun, Jul. 06, 1997.

    Go back

    A project