OS‎ > ‎Interrupts‎ > ‎

Interrupts and Handlers

Interrupts and Handlers

       

Abstract

       Study! The sound of this word alone is enough to strike fear into the heart of even the most seasoned and devoted student. This unavoidable and time consuming demon seems to creep out of nowhere and usually hits us in the most unappropriated of times. And if you're forced or the study contents are truly unpleasant, it hits you even harder. So, either you flee and abandon yourself into the bottomless pit of ignorance or allow yourself to be dragged into study hell where your task is to get out as quickly and efficiently as possible. How to accomplish this? Use a study technique. Of course, everyone has its own personal favourite study technique. Mine for instance is leaving everything to the last day and then study to death while cursing my foolishness. Ok, not much of a technique I agree... and unfortunately not very original either. But back in my old high school days I had a friend that had a very peculiar and unique way of studying.

       He thought that since we had so many different disciplines and each demanded some amount of study, why not study them all at once. When he had to study a given subject, for example maths, he proceeded to opening his math book and then reading it for a couple of minutes. So far nothing strange. But, he would then mark his position with a pencil and jump to another book, for example history, which he would read for a few minutes. Then, he returned to the marked position in the maths book and read a little more. He moved on to yet another book, let's say biology, before returning to the newly marked position in the maths book. This ritual continued until he decided study was over. He claimed that this method allowed him to distance himself from the subject at hand (in this case maths) so he could better absorb what he had just read and at the same time learn a little bit of other disciplines in the process, gaining a better understanding of what he was sure he had to study in the near future (for an exam of a different subject).

       Without trying to figure out if this is an efficient technique (although somehow it seemed to work...), let's just say that it's a good thing that computers work in a similar way. You can have a program running that is interrupted somewhere along its execution so it can perform for example a system command. The processor keeps track of where the program stopped, carries out the system command by jumping to the right address and when this is over jumps back to the previous location and resumes program execution. If, at a later stage, it needs to be interrupted again the process is repeated. If aninterrupt wasn't issued and instructions were always carried out in a linear sequential way this kind of operation would be impossible to perform. And the whole idea of a computer system as we conceive it would collapse.

       Of course, a program may be interrupted by a multitude of reasons other than the execution of a system command. For example, interrupts can occur when there is an I/O device request, when a memory protection violation is detected or when there is an hardware malfunction or a power failure. All these events trigger the arrest of program execution and transfer of control to another piece of code, often called an handler or ISR (Interrupt Service Routine). The handler takes care of everything required by the interrupt and then returns to original program.

       Thus, interrupts and handlers are a vital part of any computer system. Unfortunately, the terminology to describe exceptional situations where the normal execution order of instruction is changed varies among machines and authors. Terms like interrupt, fault, trap and exceptions are used, though not in a consistent fashion. So, in the first part of this tutorial I'll start by providing a description of an interrupt and then I'll cover the various types of interrupts. I'll then move on to handlers and the care you must take when designing one. Next, I'll explore the hardware responsible or directly affected by interrupts. Afterwards, I'll take you on a guided tour through the pitfalls of protected mode interrupt design in the second part of this tutorial, giving full coverage to this delicate subject and offering plenty of useful examples. This section is without question the most empirical of all. Finally, I'll discuss some miscellaneous related issues of interest. This HTML document was split in two due to space constraints.

       The third and final part of this tutorial is as if not more important. And it comes in a form of a zipped file. This compressed file contains essential coded elements and examples. And not only that, but it works as a full tutorial of its own. The code is exhaustively commented and accompanies you step by step as you unravel the deepest secrets of interrupt and handler control. So please, download this file so we can get down to serious business...

       I'll be using Djggp (The DOS® version of the GNU gcc compiler) 2.03 and NASM 0.98 in the protected-mode section of this document and in the zipped tutorial files. However, most of the notions can be applied to other DPMI compilers. You can get Djgpp here and NASM here.

       This tutorial would not have been possible without the invaluable help of a few people. Please check the acknowledgements section for further details.

       One final word of warning, this tutorial is quite long. So, grab some hot chocolate and get ready to jump into the fire of hell study...


Legal Stuff

       This section is a bore. Unfortunately, nowadays, it is also a necessity...

       This text is provided "as is", without warranty of any kind or fitness for a particular purpose, either expressed or implied, all of are hereby explicitly disclaimed. In no way can the author of this text be made liable for any damages that are caused by it. You are using this document at your own risk!

       Now for the good news. Nothing that comes in this document ever made my computer crash and I personally think that all information within is absolutely harmless. I wrote this to help out fellow programmers and I sincerely hope it is not a pointless article. Please send some feedback.


Interrupts

       An interrupt is a request of the processor to suspend its current program and transfer control to a new program called the Interrupt Service Routine (ISR). Special hardware mechanisms that are designed for maximum speed force the transfer. The ISR determines the cause of the interrupt, takes the appropriate action, and then returns control to the original process that was suspended.

       Why do you need interrupts? The structure of the processor of any computer is conceived so it can carry out instructions endlessly. As soon as an instruction has been executed, the next one is loaded and executed. Even if it appears the computer is inactive, when it is waiting in the DOS prompt or in Windows for your next action, it does not mean it has stopped working , only to start again when instructed to. No, not at all. In fact, many routines are always running in the background independently of your instructions , such as checking the keyboard to determine whether a character has been typed in. Thus, a program loop is carried out. To interrupt the processor in its never-ending execution of these instructions, a so-called interrupt is issued. That is why it is possible for you to reactivate the CPU whenever you press a key (fortunately...). Another example, this time an internal one, is the timer interrupt, a periodic interrupt, that is used to activate the resident program PRINT regularly for a short time.

       For the 80x86 256 different interrupts (ranging from 0-255) are available in total. Intel has reserved the first 32 interrupts for exclusive use by the processor but this unfortunately hasn't prevented IBM from placing all hardware interrupts and the interrupts of the PC BIOS in exactly this region which can give rise to some strange situations.

       Speaking of hardware interrupts, you can distinguish three types of interrupts:

       - Software Interrupts
       - Hardware Interrupts
       - Exceptions

       I will give a brief description of the previous categories but a detailed analysis is beyond the scope of this document. Please consult a reference manual, like the excellent "The Indispensable PC Hardware Book" by Hans-Peter Messmer and published by Addisson-Wesley (see the reference section), or send me an email if you wish to know further details.


1) Software Interrupts

       Software interrupts are initiated with an INT instruction and, as the name implies, are triggered via software. For example, the instruction INT 33h issues the interrupt with the hex number 33h.

       In the real mode address space of the i386, 1024 (1k) bytes are reserved for the interrupt vector table (IVT). This table contains an interrupt vector for each of the 256 possible interrupts. Every interrupt vector in real mode consists of four bytes and gives the jump address of the ISR (also known as interrupt handler) for the particular interrupt in segment:offset format.

       When an interrupt is issued, the processor automatically transfers the current flags, the code segment CS and the instruction pointer EIP (or IP in 16-bit mode) onto the stack. The interrupt number is internally multiplied by four and then provides the offset in the segment 00h where the interrupt vector for handling the interrupt is located. The processor then loads EIP and CS with the values in the table. That way, CS:EIP of the interrupt vector gives the entry point of the interrupt handler. The return to the original program that launched the interrupt occurs with an IRET instruction.

       Software interrupts are always synchronized with program execution; this means that every time the program gets to a point where there is an INT instruction, an interrupt is issued. This is very different from hardware interrupts and exceptions as you'll soon find out.


2) Hardware Interrupts

       As the name suggests, these interrupts are set by hardware components (like for instance the timer component) or by peripheral devices such as a hard disk. There are two basic types of hardware interrupts: Non Maskable Interrupts (NMI) and (maskable) Interrupt Requests (IRQ).

       An NMI in the PC is, generally, not good news as it is often the result of a serious hardware problem, such as a memory parity error or a erroneous bus arbitration. An NMI cannot be suppressed (or masked as the name suggests). This is quite easy to understand since it normally indicates a serious failure and a computer with incorrectly functioning hardware must be prevented from destroying data.

       Interrupt requests, on the other hand, can be masked with a CLI instruction that ignores all interrupt requests. The opposite STI instruction reactivates these interrupts. Interrupt requests are generally issued by a peripherical device.

       Hardware interrupts (NMI or IRQ) are, contrary to software interrupts, asynchronous to the program execution. This is understandable because, for example, a parity error does not always occur at the same program execution point. This makes the detection of program errors very difficult if they only occur in connection with hardware interrupts.


3) Exceptions

       This particular type of interrupt originates in the processor itself. The production of an exception corresponds to that of a software interrupt. This means that an interrupt whose number is set by the processor itself is issued. When do exceptions occur? Generally, when the processor can't handle alone an internal error caused by system software.

       There are three main classes of exceptions which I will discuss briefly.

       - Fault : A fault issues an exception prior to completing the instruction. The saved EIP value then points to the same instruction that created the exception. Thus, it is possible to reload the EIP (with IRET for instance) and the processor will be able to re-execute the instruction, hopefully without another exception.
       - Trap : A trap issues an exception after completing the instruction execution. The saved EIP points to the instruction immediately following the one that gave rise to the exception. The instruction is therefore not re-executed again. Why would you need this? Traps are useful when, despite the fact the instruction was processed without errors, program execution should be stopped as with the case of debugger breakpoints.
       - Abort : This is not a good omen. Aborts usually translate very serious failures, such as hardware failures or invalid system tables. Because of this, it may happen that the address of the error cannot be found. Therefore, recovering program execution after an abort is not always possible.


Signals

       A signal is an notification to a process that an event has occurred. Signals are sometimes called software interrupts. And this, causes a few problems... Are signals different from the software interrupts we treated above? Or only a different name for the same thing? Before answering to those questions, you should know that the concept of an interrupt (in particular a software interrupt) has expanded in scope over the years. The problem is that this expansion has not been an organized one, but rather a 'I'll do as I please' rampage. The 80x86 family has only added to the confusion surrounding interrupts by introducing the INT (software interrupts) instruction discussed above. The result of all this mess? There is no clear consensus of what terms to use in a given situation and different authors adopted different terms to their own use. So, words like software interruptssignalsexceptionstraps,etc came bouncing around in completely different contexts.

       In order to avoid further confusion, this document will attempt to use the most common meaning for these terms. Also, in order to differentiate between signals and software interrupts, we'll consider that :

       - Software interrupts - Are explicitly triggered with an INT instruction and are therefore synchronous, as discussed previously.
       - Signals - Don't make use of the INT instruction and usually occur asynchronously, that is, the process doesn't know ahead of time exactly when a signal will make its appearance.

       Now that we've cleared the pathway, let's dive into the pool. The concept of signal handling was born (or at least it gained strength) with the Unix platform, a protected mode and multi-threaded system. Therefore, I will start by providing a general overview of signal handling and only afterwards will I explain what changes occur in a real-mode system like MS-DOS using a protected mode compiler like Djgpp. So, get ready for a thrill...

       A signal is said to be generated for (or sent to) a process when the event associated with that signal first occurs. Signals can be sent by one process to another process ( or to itself) or by the OS to a process. And what kind of events can raise a signal? Here are a few examples :

  • A program error such as dividing by zero or issuing an address outside the valid range.
  • A user request to interrupt or terminate the program.
  • The termination of a child process.
  • Expiration of a timer or alarm.
  • A call to kill from another (or the same) process.
  • An attempt to perform an illegal I/O operation like for instance reading from a pipe when the link is broken...

       As you can easily see, the events that generate signals fall into three major categories : Errors, external events and explicit requests.

       An error means that the program performed something invalid. But not all kinds of errors generate signals--in fact, most do not. For example, trying to open a nonexistent file is an error but it does not raise a signal. This error is associated with a specific library call. The errors which raise signals are those that can happen anywhere in the program, not just in library calls. These include division by zero and invalid memory addresses.

       An external event generally has to do with I/O or other processes. These include the arrival of input, the expiration of a timer or the termination of a child process.

       An explicit request means the use of a library function such as kill whose purpose is specifically to generate a signal.

       Signals can be generated synchronously or asynchronously (the latter being more common). If you try to reference an unmapped, protected or bad memory address a SIGSEV or SIGBUS can be issued, a floating point exception can generate a SIGFPE, and the execution of an illegal instruction can generate SIGILL. All the previous events, called errors if you recall, generate synchronous signals.

       Events such as keyboard interrupts generate signals (SIGINT) which are sent to the target process. Such events generate asynchronous signals.

       We now know how signals are generated and how about delivery? Well, when a signal is generated, it becomes pending. Normally, it remains pending for just a short period of time and then is delivered to the process that was signaled. However, if that kind of signal is currently blocked, it may remain pending indefinitely--until signals of that kind are unblocked. Once unblocked, it will be delivered immediately. Once a signal has been delivered, the target program has a choice : Ignore the signal, specify an handler function or accept the default action for that kind of signal. If the first option is selected, any signal that is generated is discarded immediately, even if blocked at the time. Building handler functions will be examined in closer detail later on. Finally, if a signal arrives which the program has neither handled nor ignored, its default action takes place. Each kind of action has its own default action : It can be to terminate the process (the most common one) or for certain "harmless" events, to do nothing.

       There are few other things concerning signals that might interest you, but for our purpose we're done. If you wish to know more, check any Unix reference manual (see the reference section) or give me a ring. Also, I decided not to give a complete listing of standard signals here as it would fill too much space, but I'll cover quite a few that are accepted by Djgpp, the DOS version of the GNU gcc compiler, on the next section of this document.



Signals and MS-DOS

       As with so many other things, signal handling in Djgpp under MS_DOS brings a few additional complications. Therefore, as described in the info docs, due to the subtleties of protected-mode behaviour in MS-DOS programs, signal handlers cannot be safely called from within hardware interrupt handlers. In reality, what happens is that signal handler is only called when the program hits protected-mode and starts messing with its data. So, if the signal is raised while the processor is in real-mode, like when calling DOS services, the signal handler won't be called until the call returns. An example of this is if you try to press 'CTRL-C' in the middle of a gets() instruction, you will need to press 'ENTER' before the signal handler for SIGINT (CTRL-C) is called. Another consequence of this implementation is that when the program isn't touching any of its data (like in very tight loops which only use values in the registers), it can't be interrupted.

       But how do you incorporate signal handling in your programs? This is when the signal() function steps in. Here's its rather complicated prototype :

    #include <signal.h>
    void (*signal (int sig, void (*func)(int)))(int);

       The first argument is the signal number that you want to address. Every signal has a mnemonic that you should use for portable programs, but we'll get back to that in a second. The second argument is the name of the function that will be registered as the signal handler for the signal given by the first argument. After you call signal() and register the function as a signal handler, it will be called when that signal occurs. The execution of the program will then be suspended until the handler returns or calls 'longjmp'.

       Instead of passing a function name as the second argument, you have other options at your disposal. You may pass SIG_DFL as the value of 'func' to reset the signal handling for signal number 'sig' to default., SIG_ERR to force an error when the signal is raised or SIG_IGN to ignore that signal.

       If signal can't honor the request, that is if its first argument is outside valid limits for signal numbers, it returns SIG_ERR instead.

       I promised in the previous section that I would give you a list of supported signals in Djgpp. I also told you that every signal number has a mnemonic associated with it. So, here are the items you should use as signal numbers and their corresponding description :

  • SIGABRT - Abnormal termination. Only used in Djgpp by the assert() function to terminate the program when an assertion fails and by the abort() function.
  • SIGALRM - Alarm clock expiration. Generated after a certain time period has passed after a call to the library function alarm(). Not ANSI.
  • SIGFPE - Floating-Point Exception / Erroneous Arithmetic Operation. Generated in case of a divide by zero exception ( int 00h), overflow exception (int 04h), and any x87 co-processor exception, either generated by the CPU (int 10h) or by the co-processor itself (int 75h).
  • SIGHUP - Hangup on controlling-terminal. Currently unused. Not ANSI.
  • SIGILL - Illegal hardware instruction. Currently only generated for unknown/invalid exceptions.
  • SIGINT - Terminal interrupt signal. One of the most useful signals. Generated when an INTR key ('CTRL-C' by default) or a 'CTRL-BREAK' key (int 1Bh) is hit.
  • SIGKILL - Termination signal (cannot be caught, blocked or ignored). Currently unused. Not ANSI.
  • SIGNOFP - The no co-processor signal. Generated if a co-processor (floating point) instruction is encountered when no co-processor is installed (int 07h). Not ANSI nor POSIX.
  • SIGPIPE - Write on a pipe that has no reading process (broken pipe). Currently unused. Not ANSI.
  • SIGPROF - The profiler signal. Used by the execution profile gathering code in a program compiled with the '-pg' option. Not ANSI nor POSIX.
  • SIGQUIT - Interactive termination. Generated when the QUIT key ( 'CTRL-\' by default) is hit. Not ANSI.
  • SIGSEV - The invalid storage access (segmentation violation) signal. Generated in response to any of the following exceptions : bound range exceeded in BOUND instruction (int 05h), double exception or an exception in the exception handler (int 08h), segment boundary violation by the co-processor (int 09h), invalid TSS (int 0Ah), segment not present (int 0Bh), stack fault (int 0Ch), general protection violation (int 0Dh) or page fault (int 0Eh).
  • SIGTERM - Termination request signal. Currently unused.
  • SIGTIMR - The timer signal. Used by the setitimer() and alarm() functions. Not ANSI nor POSIX.
  • SIGTRAP - Trace/Breakpoint trap. Generated in response to the debugger exception (int 01h) or breakpoint exception (int 03h). Not ANSI nor POSIX.
  • SIGUSR1 - User-defined signal 1. Not ANSI.
  • SIGUSR2 - User-defined signal 2. Not ANSI.


Handlers

       An handler, also known as callback, is in fact the routine that is called by an interrupt, or in other words, it's the ISR itself. So, why a different name? Well, the word handler is normally used for ISRs created by you, the programmer, as opposed to those that are pre-built either in the OS or BIOS.

       The next question is why create an handler if the ISRs are already present? The answer is simple : To have more control and flexibility. Without handlers, your programs would have to abide by strict and rigid rules which would limit their usefulness. Handlers are indispensable in several situations as you will soon find out. Keep on reading.

Creating Handlers

       The creation of interrupt handlers has traditionally been considered one of the most arcane of programming tasks, suitable only for the elite cadre of system hackers. However, writing an interrupt handler in itself is quite straightforward. Let's hope that the following guidelines will help clear the myth...

       A program preparing to handle interrupts must do the following :

  1. Disable interrupts, if they were previously enabled, to prevent them from occurring while interrupt vectors are being modified.
  2. Initialize the vector for the interrupt of interest to point to the program's interrupt handler.
  3. Ensure that, if interrupts were previously disabled, all other vectors point to some valid handler routine.
  4. Enable interrupts once again.

       The interrupt handler must observe the following sequence of steps :

  1. Save the system context (registers, flags and anything else that the handler is suitable of modifying and that wasn't saved automatically by the CPU), normally by pushing the desired elements into the stack or saving them into variables.
  2. Block any interrupts that might cause interference if they are allowed to occur during this handler's processing. Please note that sometimes you'll have to disable all interrupts.
  3. Enable any interrupts that should still be allowed to occur during this handler's execution.
  4. Determine the cause of the interrupt.
  5. Take the appropriate action(s) for the interrupt.
  6. Restore the system context, usually by popping the elements from the stack or by reading the variables.
  7. Reenable all interrupts blocked.
  8. Resume execution of the interrupted process.

       When writing an interrupt handler, take it easy and try to cover all the bases. The main reason interrupt handlers have acquired such a mystical reputation is that they are so difficult to debug when they contain obscure errors. Because interrupts can occur asynchronously - that is, because they can be caused by external events without regard to the state of the currently executing process - bugs can be a real problem to detect and correct. This means that an error can manifest its presence in the program much later than it actually occurs, thus leading to a true quest of the Holy Grail.



Handlers and MS-DOS

       This section is only to inform you of some restrictions and rules that apply to a handler for hardware interrupts under MS-DOS :

  • Because MS-DOS is not reentrant, a hardware interrupt handler should never call any MS-DOS functions during the actual interrupt process.
  • If your program is not the only process in the system that uses this interrupt level, chain back to the previous handler after performing your own processing on an interrupt.
  • Remember to fetch and save the initial contents of the interrupt vector before modifying it and then restore the original contents when your program exits.
  • Try to keep the time that interrupts are disabled and the total length of the service routine to an absolute minimum.


Hardware Zone

       For all of you hardware freaks out there, we'll start by examining in close detail the chip that allows the existence of interrupts : The 8259A PIC.

       Afterwards, we'll explore each of the 8259A PIC input lines, that is, the interrupts that can trigger a reaction from this device. A brief explanation of each input will be given, but since this is a tutorial about interrupts (and quite a big one might I add) everything not directly related with interrupts will only be approached lightly. However, stay tuned for a hardware section on this site that will not avoid such issues.

1) The 8259A PIC

       As explained in the Interrupt Driven I/O vs. Polling section I/O devices can be serviced in two different ways : The CPU polling method and the interrupt based technique. The 8259A ProgrammableInterrupt Controller (PIC) allows for the later. It is designed to permit prioritizing and handling of hardware interrupt requests from peripheral devices, mainly in a PC environment.

       As the processor usually only has a single interrupt input but requires data exchange with several interrupt-driven devices, the 8259A PIC is implemented to manage them. The 8259A PIC acts like a bridge between the processor and the interrupt-requesting components, that is, the interrupt requests are first transferred to the 8259A PIC, which in turn drives the interrupt line to the processor. Thus, the microprocessor is saved the overhead of determining the source and priority of the interrupting device.

       How does it work? The PIC receives an interrupt request from an I/O device and informs the microprocessor. The CPU completes whatever instruction it is currently executing and then fetches a new routine (ISR) that will service the requesting device. Once this peripheral service is completed, the CPU resumes doing exactly what it was doing when the interrupt request occurred (as explained throughout this entire document). The PIC functions as an overall manager of hardware interrupt requests in an interrupt driven system environment.

       In case you're wondering how is accomplished the interrupt acknowledge sequence, here's a quick overview :

  1. Eight interrupt lines IR0-IR7 are connected to the interrupt request register (IRR). The IRR is eight bits wide, where every bit corresponds to one of the lines IR0-IR7. The device requiring service signals the PIC via one of the eight pins IR0-IR7 setting it to a high level. The 8259A then sets the corresponding bit in the IRR.

  2. At the same time, the PIC activates its output INT line to inform the processor about the interrupt request. The INT line is directly connected to the INTR input of the processor. This starts an interrupt acknowledge sequence.

  3. The CPU receives the INT signal, finishes the currently executing instruction and outputs a first interrupt acknowledge (INTA) pulse if the IE flag is set (that is, if the interrupts are not masked at the CPU).

  4. Upon receival of the first INTA pulse from the CPU the highest priority in the IRR register is cleared and the corresponding bit in the in-service register (ISR) register is set.There is no PIC activity on the data bus in this cycle.

  5. The processor initiates a second INTA pulse and thus causes the 8259A to put an 8-bit number onto the data bus. The CPU reads this number as the number of the interrupt handler to call, which is then fetched and executed.

  6. In the Automatic End Of Interrupt (AEOI) Mode the ISR bit is reset at the end of the second INTA pulse. Otherwise, the CPU must issue an End of Interrupt (EOI) to the 8259A PIC when executing the interrupt handler to clear the ISR bit manually.

       The EOI command has two forms, specific and non-specific. The controller responds to a non-specific EOI command by resetting the highest in-service bit of those set. In a mode that uses a fully-nested interrupt structure, the highest in-service bit set is the level that was just acknowledged and serviced. This is the default mode for PCs. In a mode that can use other than the fully-nested interrupt structure, a Specific EOI command is required to define which in-service bit to reset.

       Is this all there is to it? Usually, yes. But things can get a little trickier depending on the environment. For example, as the name indicates the 8259A programmable interrupt controller can be programmed under several different modes and for a defined operation it needs to be initialized first. For instance, it can be programmed to mask certain interrupt request lines. In order to do that the interrupt mask register is implemented. A set bit in this register masks all the interrupt requests of the corresponding peripheral, that is, all requests on the line allocated the set bit are ignored; all others are not affected by the masking.

       And what happens if an interrupt comes when another is being processed, and the EOI for it wasn't issued yet? This really depends on interrupt priorities. If a certain interrupt request is in-service (that is, the corresponding bit in the ISR is set), all interrupts of a lower priority are disabled because the in-service request is serviced first. Only an interrupt of a higher priority pushes its way to the front immediately after the INTA sequence of the serviced interrupt. In this case the current INTA sequence is completed and the new interrupt request is already serviced before the old request has been completed by an EOI. Thus, interrupt requests of a lower priority are serviced once the processor has informed the PIC by an EOI that the request has been serviced. Please note that, under certain circumstances, it is favourable also to enable requests of a lower priority using the PIC programming abilities to set the special mask mode (if you're curious check the reference section for further reading). The next table shows the priority among simultaneous interrupts and exceptions :


Class of interrupts and exceptionsPriority
Faults except debug faultsHighest
Trap instructions INTO, INT n, INT 3 
Debug traps for this instruction 
Debug faults for next instruction 
NMI interrupt 
INTR interruptLowest

Table 1 : Priority Among Simultaneous Interrupts and Exceptions

       Another characteristic of the 8259A PIC is its cascading capability, that is, the possibility to interconnect one master and up to eight slave PIC's in an application. But these subjects could build a tutorial of their own, so I'll forward you to any serious hardware book if you need more details (alternately, you can always mail me).

       For our purposes we only need to know that a typical PC uses two PICs to provide 15 interrupt inputs (7 on the master PIC and 8 on the slave one). The sections following this one will describe the devices connected to each of those inputs. In the meantime, the following table lists the interrupt sources on the PC (sorted in descending order of priority) :


Input on
8259A
Priority80x86
INT
Device
IRQ 0Highest08hTimer Chip
IRQ 1 09hKeyboard
IRQ 2 0AhCascade for controller 2 (IRQ 8-15)
IRQ 9/1 71hCGA vertical retrace (and other IRQ 2 devices)
IRQ 8/0 70hReal-time clock
IRQ 10/2 72hReserved
IRQ 11/3 73hReserved
IRQ 12/4 74hReserved in AT, auxiliary device on PS/2 systems
IRQ 13/5 75hFPU interrupt
IRQ 14/6 76hHard disk controller
IRQ 15/7 77hReserved
IRQ 3 0BhSerial Port 2
IRQ 4 0ChSerial Port 1
IRQ 5 0DhParallel port 2 in AT, reserved in PS/2 systems
IRQ 6 0EhDiskette drive
IRQ 7Lowest0FhParallel Port 1

Table 2 : 8259A Programmable Interrupt Controller Inputs


2) The timer interrupt (int 08h)

       I'll assume you've all played old games, back in the days where gameplay and plot were far more important that fancy graphics and nice box-sets... (although these concepts are all important and shouldn't be mutually exclusive one can't help but notice that priorities shifted to uncanny and greedy grounds). Game programmers of the period, often one-man teams with lots of imagination, were sometimes confronted with the problem of implementing certain delays in the game (damn, that enemy plane is closing in too fast... evasive maneuvers... I'll ne...arghhhhhhh). Often, dummy loops of the following form were employed :

    for(i=0;i<10000;i++);

       This seemed to work. However, this kind of work-around has a significant disadvantage : It relies on processor speed. What a surprise to find out that the powerful hero Kill Them All of our favourite platform game, so deft and gracious on our 20MHZ i386, now, with a 600MHZ Pentium III running a GeForce beast, helplessly dashes against every kind of inoffensive and pitiful obstacle before you even can operate a single key!

       Summarizing, we need a way to generate exactly defined time intervals. And what better way than by hardware? Thus, the PC's designers have implemented one (PC/XT and most ATs) or sometimes two (some new ATs or EISA) Programmable Interval Timers (PITs).

       The PIT 8253/8254 generates programmable time intervals from an external clock signal of a crystal oscillator that are defined independently from the CPU. It's very flexible and has six modes of operation in all (these modes will not be explained in this tutorial, maybe in a future less generic one).

       The 8253/8254 chip comprises three independently and separately programmable counters 0-2, each of which is 16 bits wide. Each counter, or channel, is supplied with its own clock signal (CLK0-CLK2) which serves as the time base for each counter. Each channel is responsible for a different task on the PC :

  • Channel 0 : This channel is responsible for updating the system clock, generating interrupts every 55ms (approximately). This is about once every 1/18.2 seconds. Sometimes this is called the "eighteenth second clock" but we will refer to it as the timer interrupt. We'll discuss this channel in more detail in a little while.

  • Channel 1 : This channel controls DMA memory refreshing, instructing all 18 CLK cycles on a DMA chip to carry out a dummy read cycle. In the course of this dummy cycle, data is read from memory onto the data bus and the address buffers, and address decoders and sense amplifiers in the memory chips are activated to refresh one memory cell row. But the data is not read by any peripheral. Instead, it's discarded in the next bus cycle. This is done because DRAM's memory cells must be periodically refreshed or they quickly lose their charge. One interesting thing that should be noted is that most system designers lay out the memory refresh rather carefully, that is, the memory is refreshed more often that is really necessary. This so-called refresh overhead can reach 10% or more. Reprogramming channel 1 to refresh memory at a slower rate can sometimes speed up system performance, but don't overdue it or data losses might incur (giving rise to parity errors upon reading main memory).

  • Channel 2 : This channel is dedicated to the tone frequency generation for the installed speaker. It's normally programmed to generate a square wave so a continuous tone is heard. Reprogramming it for "Interrupt on Terminal Count" mode is a nifty trick which can be used to play 8-bit samples from the PC speaker. You may generate various frequencies with it. The audible range of tones lies between about 16Hz and 16kHz. Frequencies above and below this range are called infra- or supersonic: Your Pc's amplifier is probably unable to generate such tones.

       The timer interrupt vector (channel 0) is probably the most commonly patched interrupt in the system. However, it turns out there are two of these vectors in the system. The first one, int 08h, is the hardware vector associated with the timer interrupt. Unless you're willing to taunt fate, it's not a good idea to patch this interrupt. If you want to build a timer handler, go for the second interrupt, interrupt 1ch. The BIOS' timer ISR (int 08h) always executes an int 1ch instruction before it returns. Catching it, assuming control and chain back to the old ISR is the best way to design your timer handler. Unless you're willing to duplicate the BIOS and DOS timer code, you should never completely replace the existing timer ISR with one of your own. Twiddling with int 1ch can be very dangerous and misuse can cause your system to crash or otherwise malfunction.

       Finally, without entering into too much detail, I'll leave you with the port addresses of the various 8253/8254 PIT registers (the control register loads the counters and controls the various operation modes) :


Port
(1st PIT)
Port
(2nd PIT)
RegisterAccess Type
040h048hCounter 0Read/Write
041h049hCounter 1Read/Write
042h04ahCounter 2Read/Write
043h04bhControl RegisterWrite-only

Table 3 : 8253/8254 register ports in a PC


3) The Keyboard Interrupt (int 09h)

       The keyboard is the most common and most important input device for PCs (excluding the mouse). Despite the birth and rise of many new "hi-tech" input devices such as scanners and voice input systems, the keyboard still plays the major role if commands are to be issued or data input to a computer.

       Contrary to popular belief, every keyboard has a keyboard chip, even the old "dumb" PC/XT keyboard with the 8048. This chip supervises the detection of key presses or releases. When you press a key, the keyboard generates a so-called make-code interrupt. If, on the other hand, you release a pressed key then the keyboard generates a so-called break-code interrupt. This occurs on IRQ 1 of master 8259A PIC. The BIOS responds to these interrupts by reading the key's scan code (1 byte code that identifies each keyboard key), converting this to an ASCII character , and storing the scan and ASCII codes away in the system type ahead buffer.

       The keyboard really deserves a tutorial of its own and I won't let it down... So, please be patient or send me an email.



4) The Serial Port Interrupts (int 0Bh and 0Ch)

       The serial interface is essential in a PC because of its flexibility. Various devices such as a plotter, modem, mouse and, of course, a printer can be connected to a serial interface. This document will not cover the structure, functioning and programming of the serial interface, but will take a quick look instead at its interrupt driven serial communication capabilities.

       The PC uses two interrupts, IRQ 3 and IRQ 4, to support interrupt driven serial communications, as seen in the following table :


InterfaceBase AddressIRQ
COM 13F8hIRQ 4
COM 22F8hIRQ 3
COM 33E8hIRQ 4
COM 42E8hIRQ 3

Table 4 : COMx base addresses and IRQ channels


       Just like the LPT ports, the base addresses for the COM ports can be read from the BIOS Data Area.


Start AddressFunction
0000:0400COM1's Base Address
0000:0402COM2's Base Address
0000:0404COM3's Base Address
0000:0406COM4's Base Address

Table 5 : COMx Port Addresses in the BIOS Data Area


       The Universal Asynchronous Receiver/Transmitter (UART) 8250 (or compatible) generates an interrupt in one of four situations : a character arriving over the serial line, the UART finished the transmission of a character and is requesting another, an error occurs or a status change is requested. The UART activates the same interrupt line (IRQ 3 or IRQ 4) for all four interrupt sources. This means that the ISR needs to determine the exact nature of the interrupt interrogating the UART.



5) The Parallel Port Interrupts (int 0Dh and 0Fh)

       Every PC is equipped with at least one parallel and one serial interface. Unlike the serial interface, for which a lot of applications exist, the parallel interface ekes out its existence as a wallflower, as it's only used to serve a parallel printer. In a similar way to what was done in the serial port section, we'll only concern ourselves with the basics of interrupt driven parallel communications.

       BIOS and DOS can usually serve up to four parallel interfaces in a PC, denoted LPT1, LPT2, LPT3 and LPT4 (for line printer). The abbreviation PRN (for printer) is a synonym (an alias) for LPT1. When BIOS assigns addresses to your printer devices, it stores the address at specific locations in memory, so we can find them as listed in the following table :


Start AddressFunction
0000:0408LPT1's Base Address
0000:040ALPT2's Base Address
0000:040CLPT3's Base Address
0000:040ELPT4's Base Address

Table 6 : LPT Addresses in the BIOS Data Area


       Now that we've come to parallel ports interrupts we face a little enigma. Why did IBM design the original system to allow two parallel port interrupts and then promptly designed a printer interface card that didn’t support the use of interrupts? As a result, almost no DOS based software today uses the parallel port interrupts (IRQ 5 and IRQ 7). Actually, DOS based software is almost harder to find than diamonds nowadays but we'll not go that way...

       "Great, now that we have some useless interrupts hanging around..." Wait! These interrupts were not dumped into the scrap hill! In fact, many devices make use of them. Examples include SCSI and sound cards. Because of this, many devices today include interrupt jumpers that let you select IRQ 5 or IRQ 7 on installation.



6) The Diskette and Hard Drive Interrupts (int 0Eh and 76h)

       I will not explain what floppy and hard disk drives are or how they work or what their structure is. Although these are all interesting topics to cover in this text, this article is already long enough. I will explore their relationship with interrupts however. But before I do that I need to ask you a little question : Do you think that your hard disk is the most important and valuable part of your PC? You do? Why? What? You haven't made a single data backup and all the past three years's work is on the hard disk? I'll just leave you with a serious piece of advice : Always have a backup handy! You never know...

       The floppy and hard disk drives generate interrupts at the completion of a disk operation. This is a very useful feature for multitasking systems like OS/2, Linux, or Windows. While the disk is reading or writing data, the CPU can go execute instructions for another process. When the disk finishes the read or write operation, it interrupts the CPU so it can resume the original task.



7) The Real-Time Clock Interrupt (int 70h)

       Before IBM made the Real Time Clock (RTC) chip standard equipment on its PC AT in 1984, users were prompted to enter the date manually every time they turned on their computers. Why? Because at every boot process the PC initialized itself to 01.01.1980, 0:00 o'clock. The user had to input the current date and time via the DOS commands DATE and TIME. DOS managed all times relative to this time and date.

       Not very practical, was it? So the RTC was born. The RTC chip is powered by an accumulator or an in-built battery to ensure it can keep time even when the PC is turned off. The RTC is independent of the CPU and all other chips (including the 8253/8254 that deals with internal system clock)and keeps on updating time, day, month, and 2-digit year. It typically contains seven registers that store time and date values. Six of the registers are updated automatically. Each one of them stores a different value: seconds, minutes, hours, days, months, and years. The year register stores the last two digits – "99" in 1999 or "00" in 2000. A seventh one, the century register, stores the first two digits of the 4-digit year. The century register reads either "19" in 1999 or "20" in 2000 and is not updated automatically. It will change only if updated by either the BIOS or the operating system.

       The real-time clock interrupt (int 70h) is called 1024 times per second for periodic and alarm functions. By default, it is disabled. You should only enable this interrupt if you have an int 70h ISR installed.

       One last thing. If you notice that the system clock is not accurate losing a number of minutes each day, or not incrementing the time when the system is turned off, then the problem might be the RTC battery. The power consumption by the CMOS RAM and the RTC is so low that usually it plays no role in the lifetime of the batteries or accumulators. Instead, the life expectancy is determined by the self-discharge time of the accumulator or battery, and is about 3 years (or 10 years for lithium batteries). Also, the quality of components these days is rather questionable sometimes. So, if you have a PC, old or not, that keeps losing track of time, take it for a visit at the local computer store.



8) The FPU Interrupt (int 75h)

       The Floating-Point Unit (FPU), also known as a maths co-processor, provides high-performance floating-point processing capabilities. Floating point operations such as decimals and logarithms can take many instruction steps on the main processor. Such calculations can be handled more efficiently if passed on to a co-processor. The FPU executes instructions from the processor's normal instruction stream and greatly improves its efficiency in handling the types of high-precision floating-point processing operations commonly found in scientific, engineering, and business applications.

       Before the advent of the 80486, the FPU was an optional chip with a reserved slot on the motherboard, close to the CPU. Nowadays, all PCs come with in-built FPU.

       The 80x87 FPU generates an interrupt whenever a floating point exception occurs. On CPUs with built-in FPUs (80486DX and better) there is a bit in one of the control register you can set to simulate a vectored interrupt. BIOS generally initializes such bits for compatibility with existing systems.



9) Nonmaskable Interrupts (int 02h)

       Non-maskable interrupts (NMI) are critical interrupts such as those generated after a power failure (memory parity error) that cannot be blocked by the CPU. This is in contrast to most common device interrupts such as disk and network adapter interrupts, which are considered maskable (you can enable or disable them with sti and cli instructions).

       This interrupt (int 02h) is always enabled by default since it cannot be masked.



10) Reserved Interrupts

       As mentioned in the section on the 8259A PIC, there are several interrupts reserved by IBM. Many systems use the reserved interrupts for the mouse or for other purposes. Since such interrupts are inherently system dependent, we will not describe them here.






Protected-Mode Considerations

       In Real-mode, which is a misnomer by the way since it should be called unprotected mode, your programs can only access the first Mb of memory (also called conventional memory). However, they can do so freely with no restrictions at all as no access checks are carried out for the code and data segments and the I/O address space. What this means is that your program can roam around unchecked and poke at dangerous memory locations which could result in disastrous consequences. Inadvertently, a wild loose pointer could erase everything that you have on your HD...

       All 80x86 CPUs up to Pentiums support the real mode for compatibility reasons. But, with the advent of the 80286 a new and advanced operating mode called protected-mode appeared, where the access of a task to code and data segments and the I/O address space is automatically checked by processor hardware. This allows the OS to run multiple programs at the same time, without letting them interfere with each other. Also, in protected mode you can access all the memory of your computer.

       In protected mode your program has complete and total freedom to roam around its address space, but when it wants to explore outside of its address space, it has to inform the OS that it is not about to do something harmful. Of course, DOS, being a 16-bit OS, adds additional complications since Djgpp programs run as 32-bit applications and the address generation in protected mode is incompatible with that in real mode. Therefore, in order to interact with DOS, a 32-bit programmer must perform a plethora of time-consuming tasks. The tradeoff is that, while in protected mode, programs can run quite a bit faster than in real mode (despite of what some say), manage memory much more efficiently, and allow the programmer to sleep at ease in the knowledge that a stray pointer won't accidentally wipe out his HD.

       I assume that if you are reading this, you're a programmer. And if you use Djgpp, you're a 32-bit programmer. And if you're a 32-bit programmer, then you need to know that interrupt handling and memory access require some special techniques in protected mode. And what are those techniques? Keep on reading.

       Note : All that follows applies to Djgpp, the freeware 32-bit compiler. However, most of the notions can be used with other DPMI compilers.


1) Interrupts in protected mode

       As I told you before, the 80286 and later processors can operate in protected mode (although the 80286 cannot switch back to real mode which is an important feature as we will soon find out), in which case the interrupt handling is somewhat different. First of all, the interrupt table consists of eight-byte descriptors instead of four-byte addresses and need not be located at physical address zero, nor contain the full 256 entries.

       Secondly, working with the interrupts themselves gained a whole new meaning. Let's take a look.

       - Software interrupts : Calling real-mode DOS or BIOS services from protected-mode programs requires a switch to real-mode, so the int86 family of functions should reissue the INT instruction after the mode switch. However, if the interrupt needs pointers to buffers the 'int86()' has to move data between your programs and low memory to transparently support these services. This means, it has to know the layout and the size of the buffers involved. While 'int86' supports most of these services, it doesn't support them all. For those it doesn't support, you'll have to use the '_dpmi_int()' function and set up or use an existing buffer (like the transfer buffer) in conventional memory.

       - Hardware interrupts : Hardware interrupts can occur when the processor is either in real mode (like when your program calls some DOS or BIOS service) or in protected mode. When you program runs under a DPMI host, all hardware interrupts are caught by the DPMI host and passed to protected-mode first; only if unhandled, are they reflected to real mode. How is this accomplished? When there is a switch to protected mode, the dpmi host ignores the normal interrupt vector table used in real mode that lies at address 0000:0000 and sets the 386+ IDT (Interrupt Descriptor Table) register to a vector table somewhere else in memory (and not necessarily at physical address zero). In fact, this is one of the main differences between the IVT and the IDT. Instead of being stuck at physical address 0, the protected-mode IDT can float around in the linear address space with absolute freedom (although it is possible to change the address of the IVT while in real-mode, it is incompatible with the implementation of the 8086 processor). The linear address of the IDT is determined by a value set into the IDTR register using the LIDT instruction. Each entry in the IDT is 8 bytes long (as opposed to the 4 bytes entries of the IVT) and can contain one of three gate descriptors :

  • A task gate - Causes a task switch to occur.
  • An interrupt gate - Control is transferred to the interrupt handler with interrupts disabled.
  • A trap gate - Control is transferred to the interrupt handler (the interrupt flag remains unchanged).

       The DPMI host also keeps the addresses of the various protected-mode handlers in a 'virtual interrupt vector table'. When an interrupt comes in, the DPMI host scans this table and two things can happen:

       1) The interrupt number is claimed by a protected-mode function : The program switches to protected-mode (if need be) and the DPMI host simulates the interrupt.

       2) The interrupt number is not claimed by a protected-mode handler : The program switches to protected-mode (if need be) and the DPMI host's default handler after verifying that no protected-mode handler applies simply switches the CPU into real mode and re-issues the interrupt, so that it can be serviced by the original real mode owner of the interrupt.

       Want an example? Since I'm not feeling very imaginative right now, let's consider the example given by Alaric B. Williams in his document about interrupt handling in Djgpp (see the reference section) that deals with an interrupt that is vital to game programmers : The timer tick interrupt (IRQ 0, INT 8). Imagine that your application (let's say a game, shall we? ) is running in protected-mode and the timer tick interrupt goes off. This is the sequence of events:

  1. The CPU stores the protected mode state and looks up the address to jump in the IDT.
  2. As expected, it jumps to the DPMI host's interrupt wrapper for INT8.
  3. The wrapper checks to see if this interrupt has been claimed by a protected mode routine.
  4. It hasn't, so the wrapper switches to real mode, and jumps to the address stored in the interrupt vector table entry 8, thus executing the real BIOS interrupt handler.
  5. Upon return from the BIOS handler, the wrapper cleans up after itself, returning to protected mode and restoring the machine state so our protected mode application continues to run smoothly.

       Since all hardware interrupts go through the DPMI host first, you can always get away with installing only a protected-mode handler. However, as you can see by the previous example, a lot of switches between protected-mode and real mode can occur and if the interrupts happen at a high frequency (like for instance more than 10Khz), then the overhead of the interrupt reflection from real to protected-mode might be too painful, and you should consider installing a real-mode interrupt handler in addition or in replacement of the protected-mode one. Such a real mode handler will be called BEFORE the interrupt gets to the DPMI host and will treat the interrupt entirely in real-mode.

       How about software interrupts? Most software interrupts always require a switch to real mode. However, 3 software interrupts are special, in that they also get reflected to a protected-mode handler. Those interrupts are : 1Ch (the timer tick interrupt as presented in the previous example), 23h (keyboard break interrupt) and 24h (critical error interrupt). This means, that to catch these interrupts, you need to install a protected-mode handler only. Unlike hardware interrupts, it doesn't make sense to install dual PM and RM handlers for these software interrupts. Therefore, if you wanted to install a timer handler instead of using the BIOS default one as portrayed in the example, you must treat it as a protected-mode handler. Check the example file to see how it's done.



2) Handlers in protected mode

       Operating with handlers in protected-mode is, as you might expect, a bit trickier than in real-mode. First of all, an interrupt handler is always entered with the interrupts disabled (the interrupt gate is activated). This is quite important and often eludes some people. As with interrupts, let's differentiate two different types of handlers :

       - Software interrupt handlers : When you have a real-mode service that needs to call a real-mode function, you may have to build a user-defined software interrupt handler yourself. But since Djgpp runs in a protected-mode environment, you should wrap your protected-mode handler with a real-mode stub that'll be recognized by the previous service. To this end, create a '_go32_dpmi_seginfo' structure, assign its 'pm_offset' field to your handler as well as its 'pm_selector' field to '_my_cs()' and call either '_go32_dpmi_allocate_real_mode_callback_retf()' or '_go32_dpmi_allocate_real_mode_callback_iret()' as required by the real-mode service you want to hook. Now that you've called a small assembly routine to handle the overhead of servicing a real-mode interrupt, you can pass the "segment" (the 'rm_segment' of the 'go32_dpmi_seginfo' structure) and the "offset" ('rm_offset') it returns to the service you want by calling '__dpmi_int()'. Don't forget to restore the original handler on exit. A very useful application of this approach is when building mouse handlers using the mouse driver's 0Ch function. Want some example code? Please refer to my soon-to-be mouse handler tutorial. In the meantime, take a look at this very schematic example and modify it to fill your own needs :

    #include <dpmi.h>
    #include <go32.h>

    static __dpmi_regs callback_regs;
    static _go32_dpmi_seginfo callback_info;

    int install_real_mode_service_handler( void (*func) (__dpmi_regs *) )
    {

    __dpmi_regs r;

    [...]

    callback_info.pm_offset = (long)func;
    callback_info.pm_selector = _my_cs();

    /* Replace the next line with the equivalent *_iret function if your real-mode
        service requires it. */
    if (_go32_dpmi_allocate_real_mode_callback_retf(&callback_info, &callback_regs))
        return -1;

    /* Fill all the required fields of the '__dpmi_regs' with appropriate values including
        the interrupt function number. In the next example the ES:DX register pair
        was used as a pointer to the handler. Feel free to modify as necessary. */
    r.x.es = callback_info.rm_segment;
    r.x.dx = callback_info.rm_offset;

    /* Finally call the required interrupt. */
    __dpmi_int (interrupt_number, &r);

    [...]
    }

       - Hardware interrupt handlers : Even more fun and "hair-consuming" (read as premature boldness...) than the previous. First of all, you'll have to consider how the handler you're about to build will affect the system (and vice-versa) because the optimal setup depends on several factors such as interrupt frequency, amount of processing required, raw desire for speed, and how much time and hard work you're willing to sacrifice. It's up to you to decide what best suits you but I'll be happy to provide a few pointers.

       As you recall, hardware interrupts can occur at any time, be it in real-mode or in protected-mode. We've also seen that this usually originates time-consuming mode switches. 'What?!? How do you expect me to build my ultra-hyper-mega blaster 3D engine that will really beat the crap out of quake3 and alikes if protected-mode is so SLOWWWWWW?' No need to start whining because you've several different solutions at your disposal to compensate that handicap. In the end, your protected-mode programs will run faster than any real-mode ones you've come up with. Do you doubt me? Well, if your program is really hungry for speed you can install a protected-mode interrupt handler, or place a real-mode handler in conventional memory that will be called BEFORE the interrupt gets to the DPMI host, depending on where your application spends the most time. If it's a DOS I/O bound program, the real-mode handler will run faster as no mode switch is necessary. On the other hand, if the application mostly swims in protected-mode waters, you can write a true protected-mode handler that, as you can easily guess, will execute sooner than its real-mode brother. Also, if you're feeling really adventurous or are forced by the circumstances, you can hook an interrupt with both protected-mode and real-mode handlers but be sure to hook the protected-mode one first as otherwise it will modify the real-mode one. Also, you should know that some DPMI hosts don't allow you to hook the real-mode interrupt and some call both handlers no matter what.

       Now for the serious stuff. Let's look at the various ways of installing a protected-mode hardware interrupt handler.


       - Installing a protected-mode hardware interrupt handler : The info docs included in Djgpp state that you should always write your handler in assembly to be bullet-proof. However, I don't quite agree with that. Most of the time, unless you need special control over the stack for instance, you can get away with a handler written entirely in C. That is, if you don't forget to lock all data that can be touched by your handler during interrupt processing. But more on that a little bit later. For now, let's see what steps to follow if you wish to install a C protected-mode handler :

  1. Initialization : Define 'new_handler' and 'old_handler' as '_go32_dpmi_seginfo' structure variables.
  2. Store the original handler you want to replace so it can be restored on program exit : This is done by calling '_go32_dpmi_get_protected_mode_interrupt_vector()'. This function puts the selector and offset of the specified interrupt vector into the 'pm_selector' and 'pm_offset' fields of a '_go32_dpmi_seginfo' structure pointed to by its second argument. You should save this data into the 'old_handler' structure variable so you can restore the original handler on exit.
  3. Create a small assembly routine to handle the overhead of servicing an interrupt : Set the 'pm_offset' and the 'pm_selector' field of the 'new_handler' structure variable to the address of your function and to '_go32_my_cs()' respectively. Pass this structure to '_go32_dpmi_allocate_iret_wrapper()'. The 'pm_offset' field will get replaced with the address of a small assembly function that handles everything an interrupt handle should do on entry and before exit (and what the code Djgpp generates for an ordinary C function doesn't include). The effect is similar to the 'interrupt' or '_interrupt' keyword found in some real-mode compilers.
  4. Rebound ? : If you merely want to chain into the interrupt vector and still return to the previous handler, call '_go32_dpmi_chain_protected_mode_interrupt_vector()' instead of the previous function. This will set up a wrapper function which, when called, will evoke your handler, then jump to the previous handler after your handler returns. Follow the same steps as above putting the address of your handler into the 'pm_offset' field and the value of '_go32_my_cs()' into the 'pm_selector' field of the 'new_handler' structure and pass a pointer to it to this function. Ignore the next step in this case as you've already called your handler and jump right in to step 6.
  5. Set your own protected-mode interrupt handler : If you don't want to chain to the previous handler, you need to call '_go32_dpmi_set_protected_mode_interrupt_vector()' with the address of the 'new_handler' structure variable. And you're ready to do some serious damage...
  6. Clean-up the mess : When you're done with the new handler, restore the old one by calling '_go32_dpmi_set_protected_mode_interrupt_vector()' with the 'old_handler' structure variable as its second argument. Also, remember to free the allocated IRET wrapper with the 'new_handler' structure variable as an argument to '_go32_dpmi_free_iret_wrapper()' if you didn't chain to the previous handler.
    A simple example ( no chaining ) :

    #include <dpmi.h>
    #include <go32.h>

    _go32_dpmi_seginfo old_handler, new_handler;

    [...]

    _go32_dpmi_get_protected_mode_interrupt_vector(interrupt_number, &old_handler);

    new_handler.pm_offset = (int) handler;
    new_handler.pm_selector = _go32_my_cs();

    _go32_dpmi_allocate_iret_wrapper(&new_handler);

    _go32_dpmi_set_protected_mode_interrupt_vector(interrupt_number, &new_handler);

    [...]

       As you've noticed, you can choose to chain back to a previous handler or simply install a new one over the original handler. A good example of chaining would be a timer routine handler since you only want to perform a set of tasks over a predefined period of time. On the other end of the line, you could have a user-defined keyboard handler that we want to take over the original and assume full control at all times.

       There might be situations where C isn't powerful enough to suit your purposes. If you want to play with stacks or want to create true reentrant functions, you have to invoke the language of the Gods : Assembly. To install an assembly protected-mode handler, you should do this :

       In your C code :

  1. Initialization : Define 'new_handler' and 'old_handler' as '__dpmi_paddr' structure variables.
  2. Allocate extra memory if necessary : If your interrupt handler is a real "memory monster eating", you should allocate memory for a user-defined stack.
  3. Retrieve the original handler you want to replace so it can be restored on program exit : Call 'dpmi_get_protected_mode_interrupt_vector()' and save the structure it returns in the 'old_handler' structure variable (passed as the second argument).
  4. Lock all the memory your handler touches and the code of the handler itself (and any functions it calls) : You've several methods at your disposal. Since this is an important issue, I devote an entire sub-chapter to it. Please check the locking section in this document for more information.
  5. Set your own protected-mode assembly interrupt handler : Finally, call '__dpmi_set_protected_mode_interrupt_vector()' passing it a pointer to the 'new_handler' structure variable filled with the value returned by '_my_cs()' in the selector field and the address of your assembly handler wrapper function in the 'offset32' field.
    Do something like this :

    #include <dpmi.h>
    #include <go32.h>

    /* 1024 bytes seem like enough for a stack (counting "fear factor") */
    #define STACK_SIZE 1024 /* a 1k stack should be plenty */

    __dpmi_paddr old_handler, new_handler;
    unsigned char *stack;
    int (*handler)(void);

    [...]

    /* If you need, allocate memory for your own stack */
    stack = (char *)malloc(STACK_SIZE);
    if (stack)
    {
        _go32_dpmi_lock_data(stack, STACK_SIZE);
        _go32_dpmi_lock_data(&stack, sizeof(stack));
        stack += STACK_SIZE - 32; /* Stacks grow downwards */
    }

    __dpmi_get_protected_mode_interrupt_vector(interrupt_number, &old_handler);

    new_handler.selector = _my_cs();
    old_handler.offset32 = (long)handler_wrapper;
    handler = your_handler_function;

    /* Lock everything that the handler might touch (variables, functions, pointers, etc...)
        before setting the new handler. Read the locking section of this document
        for further details. */

    __dpmi_set_protected_mode_interrupt_vector(interrupt_number, &new_handler);

    [...]

       Now that you've set everything you need in your C code to install your handler, it's time to create an assembly wrapper function that will perform all the chores required by the DPMI host and when that is done call the 'true' handler. Please keep in mind that it's the address of this wrapper function you should pass to '__dpmi_set_protected_mode_interrupt_vector()' and not the address of the handler itself.

       In your assembly wrapper code :

  1. Explicitly save all the segment registers : Segment registers and the stack pointer are not passed between modes. This means that the contents of the segment registers after a mode switch are undefined (the single most important exception is the cs register that is ALWAYS loaded with the code segment). This also means that you should push all segment registers (ds, es, fs, gs) into the stack, one by one, before messing with them in the next step. You should also push all other registers with a 'pusha' instruction.
  2. Assign a valid value to the previous registers : We don't want to play with handlers and have our segment registers undefined. We REALLY don't or we're in for some pain... Therefore, we should set each segment register to '__djgpp_ds_alias', a data selector that is always valid. And that's one less thing to worry about.
  3. Play with the stack, if you need or want to : The DPMI host automatically supplies a valid real mode stack but if necessary, you can set up your own stack at [SS:ESP] using the memory allocated in the C code. Just remember to save the original stack and restore it on exit.
  4. Call the assembly handler (at last!) : Finally, party time! Remember that the handler may return with an 'IRET' or a simple 'RET' depending on the type of service it calls. Be prepared to deal with both in your programming career.
  5. Restore the original stack if need be : If you used your own stack, be sure to restore the previous one or else...
  6. Restore the segment registers : Pop them from the stack. They are no longer needed. Ah, always restore the original stack (if you installed one yourself) BEFORE doing this if you want to avoid some unpleasant situations. Also, use the 'popa' instruction to retrieve the other registers.
  7. Explicitly issue the 'STI' instruction before 'IRET' : As in real-mode, hardware interrupt handlers are called with virtual interrupts disabled and the trace flag reset. In systems where the CPU's interrupt flag is virtualized, 'IRET' may not restore the interrupt flag. Therefore, you should execute a 'STI' before executing 'IRET' or else interrupts will remain disabled.
    Here's a typical wrapper function in NASM with a user-defined stack :

    [BITS 32]

    [SECTION .text]

    align 4
    _handler_wrapper :
    push word ds ; Save registers including the segment registers
    push word es
    push word fs
    push word gs
    pusha

    mov word ax, [___djgpp_ds_alias] ; Assign a valid data selector to the
    mov word ds, ax                            ; segment registers
    mov word es, ax
    mov word fs, ax
    mov word gs, ax

    lea long ebx, [_stack] ; Check if we have allocated memory for a stack
    cmp long [ebx], 0
    jz over ; Ops, seems like we forgot our ticket

    mov long ecx, esp ; Store old stack in ecx + dx
    mov long edx, ss

    mov long esp, [ebx] ; Set up our new stack
    mov word ss, eax

    mov long [ebx], 0 ; Flag the stack is in use
    push long edx ; Save the old stack
    push long ecx
    push long ebx

    cld ; Clear direction flag

    mov long eax, _handler
    call [eax] ; Call the handler

    cli ; Clear interrupts

    pop long ebx ; Restore old stack
    pop long ecx
    pop long edx
    mov long [ebx], esp
    mov word ss, edx
    mov long esp, ecx

    over:
    popa ; Restore registers
    pop word gs
    pop word fs
    pop word es
    pop word ds
    sti ; Re-enable interrupts (very important)
    iret

    _wrapper_NASM_end : ; To calculate function size

       In your assembly handler code :

  1. Just do it : Not much to say. All the hard work has already been done. Typically, you should preserve the stack caller's frame, disable interrupts, save all important registers, do what the handler is supposed to do, acknowledge the present interrupt, restore all the necessary registers, reenable interrupts, reset the stack caller's frame and return with either a 'IRET' or 'RET' as required by the service controlling your handler.

       Finally, one last issue needs to be addressed. The interrupt handler or the wrapper function needs to acknowledge the interrupt by sending an EOI. Usually, you only need to send a non-specific EOI to the PIC controller at the end of your routine with something like 'outportb(0x20,0x20)' (C) or 'out 0x20, 0x20' (Intel's asm). However, if priorities are shifted during the execution of the interrupt handler or if you're running in the special mask mode (see the section about the 8259A PIC for more information), you should send a specific EOI instead to set the right bit corresponding to the handler. This doesn't happen very often.

       - Installing a real-mode hardware interrupt handler : Relax. This a lot easier than the previous. There are only two things to consider in this case : If the CPU is running in real mode, your handler will be called before the interrupt gets to the DPMI host so it must be written in assembly and located in conventional memory ( below 1Mb mark ) which is something that hard-core real-mode programmers understand quite well... However, if the CPU is in protected-mode and you want to hook an interrupt with both RM and PM handlers you must hook the PM interrupt first, and then the RM one (because hooking the PM interrupt modifies the RM one). This means that you must make sure the interrupt won't be processed twice if both handlers are installed, using for instance some semaphore variable for mutual exclusion control.

  1. Initialization : Define 'new_handler' and 'old_handler' as '__dpmi_raddr' structure variables.
  2. Retrieve the original handler you want to replace so it can be restored on program exit : Call 'dpmi_get_real_mode_interrupt_vector()' and save the structure it returns in the 'old_handler' structure variable (passed as the second argument).
  3. Allocate some conventional memory : Call '__dpmi_allocate_dos_memory()' and allocate sufficient conventional memory to hold your handler. Save the segment the function returns in a variable.
  4. Set the handler in conventional memory : Put the code of your handler in recently allocated dos memory with the help of the 'dosmemput()' function. You could also call '__dpmi_allocate_real_mode_callback()' instead, but that would cause a mode switch on every interrupt, which is what we're trying to avoid. Otherwise, why bother installing a real-mode interrupt handler?
  5. Set the new handler : Use the variable that contains the address returned by '__dpmi_allocate_dos_memory()' to fill the 'new_handler' structure variable ( the lower 4 bits into 'offset16' field, the rest into 'segment' field), then call '__dpmi_set_real_mode_interrupt_vector()'.
  6. Clean-up the mess : When you're done with the new handler, restore the old one by calling '__dpmi_set_real_mode_interrupt_vector()' with the 'old_handler' structure variable as its second argument. Also, free the conventional memory allocated by calling '__dpmi_free_dos_memory()'.


3) Locking memory to prevent page faults

       With its 32-bit offset registers and 16-bit segment registers the i386 has a logical address space of 64Tbytes per task. Because the base address in the segment descriptors is 32 bits wide, these 64Tbytes are mapped to an address space with 4 Gbytes at most (2^32). The combination of the segment's base address and the offset within the segment leads to a so-called linear address. This means that memory is stored in a linear fashion. Thus, with a larger address you find the memory object higher up in memory. However, the segment selector, or if you prefer the segment number, doesn't indicate linearity. A selector larger in value may directly point to a segment at the lowest possible segment address and vice-versa.

       Because all segments must be present in physical memory, which is usually much smaller than 4Gbytes (unless you happen to own a massively parallel computer like the Connection Machine with its 65536 (!) processors), the base addresses of all the segments are also within the address space of the physical memory. With a main memory of, let's say 64 Mbytes, all base addresses are lower than 64Mbytes, which means that the complete linear address space between 64Mbytes and 4Gbytes has not been used. Actually, more than 99% of the linear address space remains unused. Loads of memory for many, many, many (you get the picture) segments...

       This is where paging comes in. Paging can be seen as the mapping of a very large linear address space onto the much smaller physical address space of main memory, as well as the large address space of an external mass storage unit (usually a hard disk). But mapping the entire i386 virtual address space onto a hard disk would be time-consuming not to mention space forbidding. Therefore, this mapping is carried out in "slices" of a fixed size called pages. In the case of the i386, a page size of 4kbytes is defined (this size is fixed by the processor hardware and cannot be altered). The Pentium chip also allows pages of 4Mbytes in addition to the previous ones. Thus the complete linear address space consists of one million 4kbytes pages (or one thousand 4Mbytes pages). In most cases, only a few are occupied by data. The occupied pages are either in memory or swapped to the hard disk. It's up to the operating system to manage, swap and reload the pages as the need arises as well as intercept the paging exceptions and service them accordingly.

       But what has this to do with locking memory? I was getting to that. But there is still one important concept called demand paging that I need to explain. Demand paging refers to the swapping of pages in main memory onto an external mass storage (usually a hard disk) if the data stored there is currently not needed by the program. If the CPU wants to access the swapped data later on, the whole page is transferred into memory again and another currently unused page is swapped. Therefore, as it was seen earlier on, a much larger virtual address space than that actually present physically can be generated.

       So, what seems to be the problem? Well, the problem can be the demand paging itself. If, during the process, the paging unit determines that the required page table or the page itself is externally stored (swapped), then the i386 issues an interrupt 14 (0eh). Normally, this offers no trouble whatsoever as the operating system (or in our case the DPMI host) can load the respective page or page table into memory and resume its operation. However, this should not be done during the execution of an interrupt handler because DOS is a non-reentrant operating system. If an interrupt handler, any other functions it invokes or any variable it touches is swapped out then a page fault (exception 14) might be issued and your program goes to the little fishes...

       The solution, as you may have guessed, is to 'lock' the memory regions that must be available, telling the DPMI host to keep them in active memory at all times. This can be done with the Djgpp library functions '_go32_dpmi_lock_code()' and '_go32_dpmi_lock_data()'. How do you use them? It depends on the circumstances.

       1) Locking data :

       If you only need to lock a static variable, it's pretty easy. Just use :

    _go32_dpmi_lock_data(&my_var, sizeof(my_var));

       If you want to lock a dynamically allocated memory block, you must know its size and you mustn't forget to lock the pointer variable, as well as the data itself :

    #define BUFFERSIZE 128 char *buffer;
    if(! (buffer = (char *)malloc(BUFFERSIZE)))
        return -1;
    _go32_dpmi_lock_data(&buffer, sizeof(buffer));
    _go32_dpmi_lock_data(buffer, BUFFERSIZE);

       2) Locking code :

       Since we can't 'sizeof()' a function we'll have to use a trick :

    void int_handler()
    {
    }

    void lock_int_handler()
    {
        _go32_dpmi_lock_code(int_handler, (unsigned long) (lock_int_handler - int_handler));
    }

       Just declare a dummy function after your handler routine and use pointer arithmetics to deduce the code size of your handler function.

       It would really be painful to write all this every time you need to lock code and data so I recommend you use the following set of convenient macros present in the Allegro library by Shawn Hargreaves :

    #define END_OF_FUNCTION(x) void x##_end() { }
    #define LOCK_VARIABLE(x) _go32_dpmi_lock_data((void *)&x, sizeof(x))
    #define LOCK_FUNCTION(x) _go32_dpmi_lock_code(x, (long)x##_end - (long)x)

       The first macro is used like so :

    void int_handler()
    {
    }

    END_OF_FUNCTION(int_handler);

       This will create a dummy function named int_handler_end.

       LOCK_VARIABLE is pretty self explanatory.

       LOCK_FUNCTION(int_handler) expands to '_go32_dpmi_lock_code(int_handler, (long)int_handler_end - (long)int_handler);', thus saving you some time.

       Instead of locking the necessary memory regions, you might consider disabling virtual memory to make sure your program doesn't page. To accomplish this, either set the '_CRT0_FLAG_LOCK_MEMORY' bit in the '_crt0_startup_flags' variable, or use CWSDPR0 or PMODE/DJ as your DPMI host. This is a good way to start writing a program that hooks interrupts since it really eases out debugging. After you make sure your basic setup works, you can proceed to an environment where paging might happen. Please note that '_CRT0_FLAG_LOCK_MEMORY' is only recommended for small programs that run on a machine where enough physical memory is always available because the startup code in DJGPP currently doesn't test if memory is indeed locked, and if there is not enough physical memory installed service your program needs, you can end up with unlocked or partially unlocked memory, which will surely crash your program. If you want to make sure all memory is locked, use a DPMI server which disables paging (with all the inconveniences it brings).



Other Stuff

       We covered a lot of ground already but we're not over the hills yet. Some issues still need to be addressed, even if in a light-hearted approach. The first thing we need to consider is that ill-conceived and mythical problem of reentrancy and all the consequences it brings about (and why it's so badly understood...)

       Another thing to take in consideration is efficiency. I've told you before that interrupt handling is really important (read fundamental). But don't take my word for it. We'll study a few situations where alternatives will be presented. You'll find out that in most cases an interrupt driven system is usually superior despite the added complexity. However, not always. For some systems, alternative methods provide better performance.



1) Reentrancy Problems

       What is reentrancy? In order to answer this question I'll dispel a myth. Reentrant functions, AKA "pure code", are often falsely thought to be any code that does not modify itself. Too many programmers feel if they simply avoid self-modifying code, then their routines are guaranteed to be reentrant, and thus interrupt-safe. Nothing could be further from the truth.

       In order to make this easier, think what would happen if you enable interrupts in the middle of an ISR and a second interrupt from that device comes along. Well, this would interrupt the ISR and thenreenter the ISR from the beginning. Many applications do not behave properly under these conditions. Those that do are said to be reentrant. The others are called nonreentrant. Therefore, a function is reentrant if, while it is being executed, it can be re-invoked by itself, or by any other routine, by interrupting the present execution for a while.

       And now enters another deeply ingrowing thorn : "I don't need to worry about reentrancy—I'm not writing multithreaded code". Big mistake! As explained earlier, by reentrant we're considering code thatreenters itself. It's true that reentrancy was invented for mainframes running in multithreaded environments, in the days when memory was a valuable commodity. But it's far from being restricted to that nowadays.

       How do I know if my function is reentrant? A function must satisfy the following two conditions to be reentrant :

       1. It never modifies itself. That is, the instructions of the program are never changed. Period. Under any circumstances. Never is never. Far too many embedded systems still violate this cardinal rule.

       2. Any variables touched by the routine must be allocated to a individual storage place. Thus, if reentrant function XPTO is called by three different functions, then XPTO's data must be stored in three different areas of RAM.

       Ok, by now you must be drooling for an example. Let's pick a certain type of functions that are really asking for reentrancy problems : Recursive functions. Getting past the traditional factorial function (n!), take a look at the following very simple recursive function, taken from an example in the Borland C++ Programmer's Guide :

    double power(double x, int exp)
    {
        if (exp<=0) return(1);
            return(x*power(x, exp-1));
    }

       This function is reentrant. It can be interrupted at will. Now suppose, we'll be using in our program the following equivalent function, where exp is now defined as a public variable, accessible to many other functions :

    double power(double x)
    {
        if (exp<=0)return(1);
        --exp;
        return(x*power(x));
    }

       This function is no longer reentrant. Why? What happened? Imagine if this function was called by, say, main(), and then an interrupt which calls the same function came along while it is executing, it will return an incorrect result. The variable exp is fixed in memory; it is not unique to each call, and is therefore shared between callers, a disaster in an interrupting environment.

       "But a part of my program cannot absolutely, positively be interrupted!" Fear not. Here comes a critical region or critical section to the rescue. What is a critical region? A critical region is that section of the code where the program must not be reentered while executing. Actually, this is a rather simplistic definition, especially when it comes to mutual exclusion and semaphores in multitasking systems, but one that suits our immediate needs. How to avoid reentry in these regions? Simple, just disable interrupts (with a cli instruction for instance). "Wait a minute! Doesn't this mean that my program is no longer reentrant?" Not necessarily. Most programs, even those that are reentrant, have various critical regions. The key is to prevent an interrupt that could cause a critical region to be reentered while in that critical region. The easiest way to prevent such an occurrence is to turn off the interrupts while executing code in a critical section. Overall, a program, if reentrant, will retain its status throughout most of its code and in the critical regions, since interrupts are disabled, there's no fear of errors creeping in.



2) Interrupt Driven I/O vs Polling

       In a polling system the CPU continually test the status of an I/O device in order to check whether an I/O operation is complete or necessary. In a direct contrast, with an interrupt-driven system the CPU can continue to process instructions and is notified when some I/O activity occurs. This is generally much more efficient than wasting CPU cycles polling a device while it is not ready.

       I would not like to dwell much longer on this subject but rather point you to two other documents I wrote that approach these issues in detail : The mouse poll and the mouse handler tutorials (this last one will be available soon).



3) Interrupt Latency

       As a start, some of the terminology which will be necessary for the next couple of sections will be explained :

       - Interrupt latency : Time that elapses between the point a device signals that it needs service and the point where the ISR provides the needed service.

       - Interrupt response time : Time that elapses between the occurrence of an interrupt and the execution of the first instruction of that Interrupt Service Routine (ISR) by the CPU.

       - Interrupt service time : Time it takes the ISR to service the interrupt.

       - Interrupt frequency : How many times per second (or any other time measurement) a particular interrupt occurs.

       Now that we're cleared on this, let's dirty our hands. In response to an interrupt, the following operations take place in a microprocessor system while in Real Mode as indicated below. However, a very similar procedure would be followed in protected mode except that the Task switch may be necessary and the privilege levels may cause the instruction executions to take longer to execute than in normal real mode operation.

  1. An interrupt request (INTR) is issued (start of interrupt latency & interrupt response time).
  2. Recognition of the INTR by the CPU (end of interrupt latency).
  3. The processing of the current instruction is completed.
  4. Micro-code of the CPU Core executes the INTR.
  5. Get INTR vector from the 8259A PIC.
  6. Branching to the ISR while saving the microprocessor state.
  7. First instruction of the ISR executed (end of interrupt response time & start of interrupt service time).
  8. Continue with interrupt service routine.
  9. Restore the saved status of the microprocessor and return to the instruction that follows the interrupted instruction (end of interrupt service time).

       Sometimes, interrupt latency is more important than interrupt service time. For example, consider a device that interrupts the CPU in 5 seconds intervals but can only hold data on its input port for about a millisecond. In theory, 5 seconds is more than enough for interrupt service time; but the incoming data must be read within 1 millisecond to avoid loss.

       Therefore, having a quick recognition of the interrupt by the CPU with an interrupt acknowledge cycle (that is, a low interrupt latency) is very important if not even critical in many applications. Indeed, in some applications the latency requirements are so strict that you have to use a very fast CPU or you have to abandon interrupts altogether and go back to polling with all its inconveniences.

       You can easily measure the best case interrupt latency using our friend the 8254 timer chip. Doing this is beyond the scope of this article, so consult a reference manual (see the reference section for further details) or give me a ring.



4) Interrupt Service Time

       Let's finish what we started. It's now time to brief you on interrupt frequency and interrupt service time (if you're unsure of these two definitions, please refer to the previous section). Usually, these two factors control most of the ISR's impact on system performance.

       Of course, the frequency is completely dependent on the source of the interrupt. For instance, the timer chip (8253 or its successor 8254) generates evenly spaced interrupts about 18 times per second. A serial port at 9600 bps generates more than 100 interrupts per second. When compared to the keyboard that generates at most 25 interrupts per second at an irregular rate, you can easily see there is no clear frequency pattern.

       And now we focus on the core of the issue : Interrupt service time. Since this represents the time it takes for the ISR to execute its instructions and since quite often it's up to the programmer to create those instructions (see the Handler sections of this document), it becomes quite a critical area of interest. One thing above all others affects the interrupt service time : The number of instructions being processed by the ISR (obviously). But the interrupt service time is also dependent on the particular CPU and clock frequency. This is quite logical since a powerful PC usually executes the same ISR faster than its slower brother.

       In order to measure the true impact an interrupt will have on system's performance you'll need to multiply the interrupt service time by its frequency. Remember, every CPU cycle spent in an ISR is one less cycle available for your application programs.

       So, what's your goal as a programmer? Yes, you guessed it. You'll want to build solid and fast ISRs that have minimum influence on system performance. This is one of the main reasons why handlers for DOS are still being written in Assembly.

       There's one last thing that has been left unsaid. Imagine that you've been assigned to an important software project that required the implementation of an interrupt handler. After careful deliberation and calculation you've come to the conclusion that no more than 15% could be spent on ISRs. You get to work and after many months of hard labour you produce a program where your handler consumes only 10% of the overall CPU cycles. A big and wild party follows and in the morning, while trying to survive a terrible hangover, you find yourself penniless, bewildered and without a job. What did I do to deserve this????? Well, you forgot one essential rule : Your ISR is probably not the only one running on the system. While your ISR is consuming only 10% of the CPU cycles, there may be another ISR that is doing the same thing; and another, and another, and… There goes the overall 15%! Of course, if you are designing an embedded system, one in which the PC runs only your application, you don't really give a damn whether your handler must coexist with other ISRs and applications.

       Ah... yes, the story was a bit far-stretched but you get the picture... :)



Conclusion

      We've gone through quite a while indeed. We covered interrupts and their meaning and with this knowledge in our baggage we proceeded to handler land, where ISRs could hide their secrets no longer. Tired with so much software information, we turned our minds to the more earth to earth components of our PC and their direct connection with interrupts. With this extra motivation, we ventured into the almost arcane mysteries of protected-mode interrupt programming and resisted the onslaught of endless examples. Finally, we discussed such important concepts as interrupt latency and reentrancy. Not too bad, don't you agree?

      This was a quite generic tutorial and we covered a lot of ground here. You can be sure that at least some of this knowledge will come in handy if you spend enough time programming. Interrupts and handlers will keep on confusing programmers for quite some time, mostly because of the debugging difficulty they originate (you can never be too sure where the error occurred). So, if you must fight them, take some time to study your enemy and victory shall be yours!

      This is a very long document, and, despite careful proofreading, errors are eagerly waiting to show their ugly little faces. Great care has been taken to prevent them but if you spot any mistakes, or wish to provide some feedback, please email me. Thanks in advance.



Examples

      The following zipped file contains practical and useful examples of how to manipulate interrupts and handlers. In fact, this download represents the third part of this article, and if you've read this far, you must get it as soon as possible. I'll even throw in an added bonus : An entire section covering memory handling in Djgpp. All examples require Djggp and/or NASM and were compiled using Djgpp version 2.03 and NASM version 0.98.

      Just download the zip file and extract all files to a directory of your choice. Then, follow the instructions present in the readme.txt file.

Examples V1.00


Acknowledgements

      I would like to thank the Djgpp and Allegro source code, the Info docs and the Djgpp faq for all the insight they provided. Gratitude is also expressed to Peter Johnson for graciously allowing me to inspire from some of his code, to Shawn Hargreaves for building such a great programming library (Allegro) and for answering my various questions and to Eli Zaretskii for maintaining an amazing Djgpp faq list and for providing precious help and feedback.



References


  • DUNCAN, RAY [1988]. Advanced MSDOS programming. Microsoft press, second edition.

  • FREE SOFTWARE FOUNDATION, INC [1998]. "Info docs". Version 2.18.

  • GANSSLE, JACK [1993]. "Reentrancy". Embedded Systems Programming, February 1993.

  • HENNESY, JOHN L. AND DAVID A. PATTERSON [1996]. Computer architecture a quantitative approach. Morgan Kaufmann Publishers, second edition.

  • HYDE, RANDALL [1996]. Art of assembly language programming.

  • INTEL CORPORATION [2000]. Intel's developer site.

  • LAMOTHE, RATCLIFF, SEMINATORE AND TYLER [1994]. Tricks of the game programming gurus. Sams Publishing, first edition.

  • MANO, M. MORRIS [1988]. Computer engineering : hardware design. Prentice Hall, first edition.

  • MESSNER, HANS-PETER [1997]. The indispensable PC hardware book. Addison-Wesley, third edition.

  • SCHILDT, HERBERT [1995]. C++ : The complete reference. Osborne, second edition.

  • STEVENS, W. RICHARD [1998]. Unix network programming - Networking APIs : sockets and XTI (Volume 1). Prentice Hall, second edition.

  • UFFENBECK, JOHN [1987]. The 8086/8088 family : design, programming, and interfacing. Prentice Hall, first edition.

  • WILLIAMS, ALARIC B. [?]. "The Dark Art of writing DJGPP Hardware Interrupt Handlers". Version 1.0.

  • ZARETSKII, ELI AND OTHERS [1994-1998]. "Djgpp faq list". Version 2.11.

Comments