It's a feeling you probably
know all too well. You've just
banged out a bunch of new code and run it for the first
time when bam! Another pesky access violation rears its ugly head.
You've probably also seen the dreaded number 0xC0000005, AKA
STATUS_ACCESS_VIOLATION. What's not so well known is how the number
0xC0000005 came to represent "Something bad just happened," or the Win32®
support for different exception types. In this month's column, I'll dig
into Win32 exceptions and how they correlate to hardware exceptions. On
the hardware side of things I'll focus on the Intel x86 architecture.
(Alas, the Alpha I was using was reclaimed by Digital.)
If you've ever programmed for Windows® 3.x or used an MS-DOS®
extender, you've no doubt encountered exception 0xD (General Protection
Fault, or GPF for short). There's a good chance you've seen other
faults, such as the invalid opcode fault (exception 6). These numbers
aren't made up; any Intel manual shows you that these exception codes
are what the CPU uses to signal various problems or events. You won't
see these exception codes in Win32 because Windows NT®,
the flagship Win32 operating system, is designed to run on multiple
hardware platforms. It simply wouldn't do to have an Alpha or MIPS
version of Windows NT using exception codes from an Intel CPU.
Instead,
Win32 uses its own numbering system to represent the various kinds of
exceptions. On any given Win32-based platform, the system maps each of
the CPU's exception codes to one or more generic Win32 exception codes.
For example, on Intel CPUs exception 0xD may become a
STATUS_ACCESS_VIOLATION (0xC0000005). Alternatively, exception 0xD may
become a Win32 STATUS_
PRIVILEGED_INSTRUCTION (0xC0000096) exception. The underlying cause for
the hardware exception determines which Win32 exception it maps to.
To
begin the journey into Win32 exceptions, let's start at the beginning:
CPU exceptions and interrupts. Exceptions and interrupts are a means by
which the CPU's execution switches to a completely different code path
to handle some external stimulus or condition in the executing code. An
interrupt is typically caused by an external stimulus—for example, a
keystroke being hit. An exception is caused by an internal condition in
the code or data that causes the processor to generate an exception. A
classic example of an exception is the CPU attempting to read from an
address that doesn't have physical RAM mapped to it.
Intel CPUs reserve 32 interrupt/exception codes to handle various conditions. Figure 1
shows some of the more common numbers. It's pretty obvious what some of
the numbers mean, but there's a good chance you haven't encountered
others (at least not before running this column's sample program).
Veterans of the MS-DOS wars may be surprised that INT 5H isn't listed as
a print-screen, while INT 8H isn't listed as the timer interrupt.
What's up with this? The descriptions in Figure 1
are Intel's definitions for the exceptions and interrupts.
Unfortunately, before the Intel CPU architecture had fully evolved, the
authors of MS-DOS used some of these interrupt numbers for other
purposes. The wisdom of this decision became apparent when programmers
using the BOUND instruction unexpectedly received printouts of their
screens.
To
keep things simple, for the rest of the column I'll use the word
"exception" to mean exception or interrupt. As I indicated earlier,
interrupts and exceptions are technically different. In addition,
exceptions can be further subdivided into faults, traps, and aborts.
I'll skip over a bunch of geek-speak about what these things mean and
simply say that for now you can consider all of these terms to mean
effectively the same thing.
When
an exception occurs, the CPU suspends the current path of execution in
preparation for transferring control to the exception handler. The CPU
saves the current executing state by pushing the flags register
(EFLAGS), the code segment register (CS), and the instruction pointer
register (EIP) onto the stack. Next, the exception code is used to look
up and transfer control to the address where the designated handler for
this exception resides. At the most fundamental level, the exception
code is merely an index into the Interrupt Descriptor Table (IDT), which
indicates where the exception should be handled.
The
IDT is a fundamental data structure used by Intel CPUs that's comprised
of an array of up to 256 interrupt descriptors, each eight bytes in
length. The IDT is created and maintained by the operating system and is
thus considered a CPU data structure, but it also falls under the
control of the operating system. If the operating system messes up the
IDT, the whole system comes down in a hurry.
 On
most operating systems, including those based on Win32, the IDT is kept
in privileged memory, away from access by lowly application programs.
This is quite different from real-mode MS-DOS programming, where
application programs commonly usurp slots in the interrupt table (a
real-mode version of the IDT). The lack of coordination between multiple
MS-DOS-based programs, drivers, and TSRs is what caused a great deal of
the legendary instability of systems running MS-DOS and 16-bit Windows.
With the newer 32-bit operating systems, the CPU restricts access to
the IDT, with a corresponding increase in overall stability.
Nonetheless, Win32 supervisor-level device drivers have access to the
IDT and can modify its entries.
|