Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Stalking General Protection Faults: Part I


JAN90: STALKING GENERAL PROTECTION FAULTS: PART I

Andrew is a software engineer in Cambridge, Mass., where he is writing a network CD-ROM server. Andrew can be reached at 32 Andrew (really!). St., Cambridge, MA 02139.


"All protection violations that do not cause another exception cause a general protection exception."

-- Intel 80386 Programmer's Reference Manual

"'Good' exceptions are the ones you expect to occur."

-- Edmund Strauss, Inside the 80286

In porting applications to the Protected Virtual Address Mode (protected mode for short) of Intel's 286 and 386 processors, many developers have become familiar with the General Protection (GP) fault.

The code in Example 1 causes a GP fault in protected mode. When the Intel processor detects a protection violation (writing into a code segment, peeking past the end of a segment, trying to execute data, or using an illegal segment selector), it raises an exception. A protected-mode operating environment such as OS/2 or a DOS extender catches this exception and, almost always, terminates the offending application.

Example 1: This code will cause a GP fault in protected mode

main ()
{

     int far *fp = (int far *) main;
     *fp = rand ();
     main();

}

In contrast to real mode, which allows a program such as this to execute, protected mode makes it possible for an operating system to halt buggy applications. Whereas the real-mode version of this program behaves nondeterministically (or would, if the random number generator were initialized), the protected-mode version behaves the same every time. This makes protected mode terrific for software development.

But, what if a protected-mode environment catches a GP fault from the program in Example 2? That would not be an application problem, but a problem in user nput. This program is an interpreter of sorts, and if it GP faults, it's because of an error by the user, not by the application itself.

Example 2: If this program GP faults, it's because of an error by the user, not by the application itself

main (int argc, char *argv[])
{

     int far *fp = (int far *) atol (argv[1]);
     *fp = atoi (argv [2]);

}

Now isn't this rather silly program responsible for verifying its input? Perhaps, but these two lines of code represent a large class of programs that allow address manipulation by the user, and in many such extensible systems, it will not be possible to completely verify "user code." It's often better to let the Intel processor handle the verification; your application is then responsible for catching the GP fault signals that the CPU sends.

The GP fault is just an interrupt: INT OD, in fact. Normally, the operating system installs an interrupt handler for GP faults; this handler halts buggy programs. Clearly, this is not the proper response when the GP fault lies in the user's input rather than your code.

In these cases -- when it's not your fault -- your application needs to install its own GP fault handler. Depending on the system, catching GP faults may be no more difficult than catching ^C signals, or may be as complicated as running oneself under a pseudodebug process.

Catching and recovering from GP faults is important to a far wider class of applications than one might guess. Aside from the programmer's interpreters and debuggers, more and more applications include some form of embedded language that allows the user (or, more likely, a consultant) to customize the application. When ported to protected mode, these applications will have to recover gracefully from GP faults. This means building software that is fault-tolerant.

This two-part article discusses catching GP faults in several different 286 and 386 protected-mode DOS extenders and in OS/2. Part I explains why you would want your protected-mode programs to catch GP faults, and then shows how to do so using a 286-based DOS extender. Part II, scheduled to run in the next issue of DDJ, shows how to catch GP faults in Phar Lap's 386-based DOS extender, and in IBM and Microsoft's 16-bit OS/2 operating system. Catching GP faults is more difficult in OS/2 than in the other protected-mode environments, and requires use of the interesting DosPTrace()debugging function.

To a programmer familiar with the ANSI C standard library or with the Unix operating system, all of this will at first seem to be much ado about nothing, because the solution is "obviously" to install a SIGSEGV (segmentation violation) handler with signal(). But none of these environments turns an INT 0D into a SIGSEGV signal. For portability with Unix, we could write a SIGSEGV signal handler, and then make our INT 0D interrupt handler raise (SIGSEGV), but that still leaves the question of how to catch INT 0D in the first place.

Because this article focuses on writing code in C to catch GP faults, it also discusses general issues involved with writing interrupt handlers in C, and with the setjmp( ) and signal( )facilities. And, while focusing on one seemingly obscure aspect of 286/386 programming, this article also looks at a lot of different protected-mode tools.

The Protected-Mode PEEK/POKE Problem

"A GP fault is evidence that the program's logic is incorrect, and therefore it cannot be expected to fix itself or trusted to notify the user of its ill health."

Gordon Letwin Inside OS/2, 1988

Similar statements can be found in the documentation for almost any system based on protected mode, but Letwin is wrong.

How could it be otherwise? Are there any real programs for which a GP fault is not indicative of the program's ill health? How can a program violate the protection model and still deserve to continue executing?

Take the example of Digitalk's superb Smalltalk/V286, which runs in protected mode. Normally Smalltalk/V286 is quite resilient to user errors. But type in this line of code: Dos new peekFrom: 0 @ 0. Select it and evaluate it by choosing "Show It" from the menu. (0 @ 0 is a point in two-dimensional memory space.) The result is that Smalltalk bombs back out to DOS with the message:

Exception 0D: General protection fault   Error code/selector: 0000

All we did was ask to peek at memory location 0000:0000. We weren't even trying to change it! How could peeking at it cause Smalltalk to crash? Still crazy after all those years of real-mode programming, we expected that 0000:0000 would point to the address of the divide-by-zero interrupt vector. In fact, in protected mode dereferencing the address 0000:0000 is guaranteed to be illegal.

Okay, so we're not allowed to peek at 0:0 in protected mode. But the bug was in our code, not in the Smalltalk interpreter. The interpreter was shut down because of a bug in our code? This doesn't sound very protected. In this case, the assumption "a program that GP faults should be terminated"was wrong. It's our "user code" that should be squelched, not the Smalltalk interpreter itself.

To see that this problem is not inherent in protected mode, let's execute the same instruction using another great protected-mode product, Laboratory Microsystems's UR/Forth 386, a 32-bit Forth that runs under Phar Lap's 386| DOS-Extender and virtual-memory manager.

When we execute this Forth statement 00c@L (fetch from 0:0) and then continue on with the Forth interpreter itself responds with the message: General protection fault! This is what Smalltalk/V286 should have done. Instead of crashing back out to DOS, the interpreter stays in control.

That this is easier said than done, though, becomes clear if we lastly look at the OS/2 version of UR/Forth. True, this isn't supposed to be identical to UR/F 386, because that's a completely 32-bit application while the OS/2 version remains, alas, 16-bit. But if we evaluate the same line of code: 0 0 C@L which, again, means "peek at 0000:0000," OS/2 halts the Forth interpreter and displays a GP fault dump as shown in Figure 1.

Figure 1: A GP fault dump Session title: UR/Forth

SYS1943: A program caused a protection violation.
TRAP 000D
AX=2092 BX=0000 CX=FFFF DX=3FC2 BP=FFFA
SI=05EA DI=05E4 DS=00C7 ES=0000 FLG=2206
CS=0227 IP=2093 SS=00C7 SP=FBFC MSW=FFED
CSLIM=FFFE SSLIM=FFFF DSLIM=FFFF ESLIM=****
CSACC=DF SSACC=F3 DSACC=F3 ESACC=**
ERRCD=0000 ERLIM=**** ERACC=**
End the program

Now, unless we're writing a 32-bit protected-mode HastyBasic, few of us care about PEEK and POKE as such. But any other form of memory access can be reduced to PEEK and POKE. The question when writing a protected mode interpreter is what do PEEK and POKE mean in the context of protected mode?

The answer is simple: The user must be allowed to execute any command without fear of killing the interpreter. If the Intel processor detects a GP fault, the interpreter should handle it by printing out a message such as "Illegal selector" or "Can't write to code segment" or "Peeking past end of segment," or perhaps simply "General protection fault!" The interpreter should then return to its top-level input loop.

The Real-Mode GP Fault

Actually, this isn't purely a protected-mode problem. There is also a limited form of GP fault in real mode on 286 and 386 machines. Attempt to dereference more than one byte from offset FFFF:

  int *p = (int *) -1;
  printf("%d \ n", *p);

This is an excellent way to crash an IBM AT. The CPU generates an INT 0D, but it isn't caught properly. On an AT, the real-mode GP fault (segment overrun) is treated as though it were a hardware interrupt coming in on IRQ5. IRQ5 can be used by anything from LPT2 to the Novell network to an interrupt-driven SCSI board, so exactly what this code will do depends on your setup.

FFFF.C (Listing One, page 120) is a program that generates this real-mode GP fault on 286 and 386 machines. But it also includes a handler for INT 0D. The listing shows how easy it is to write a real-mode interrupt handler in C. The only difficulty is that, while both Turbo C and Microsoft C push all registers to a function defined with the interrupt keyword, they differ in the order in which the registers are pushed.

FFFF.C uses a REG_PARAMS structure in which the fields appear in a different order depending on the compiler used. REG_PARAMS is defined in GPFAULT.H (Listing Two , page 120). The interrupt handler expects one of these structures (not a pointer to it!) to be on the stack. Using 286 instructions, the Microsoft C compiler does a PUSHA; Turbo C pushes each register individually. The processor itself pushes FLAGS and CS:IP before calling the interrupt handler. In addition to distinguishing between the Borland and Microsoft compilers, REG_PARAMS also handles the extra parameter that is pushed on the stack of protected-mode handlers for INT 08-0D, and the extra FS and GS registers on the 386. GPFAULT.H is #included by most of the programs in this article.

The real-mode GP fault handler is installed using MS-DOS function 25, just like any other interrupt handler.

How does the INT 0D handler know whether it's been invoked because of a GP fault or because of a hardware interrupt on IRQ5? An INT 0D handler can check whether the CS:IP pushed on its stack corresponds to any of the known places in the program where a GP fault is expected or allowed to occur. If it doesn't, the handler can just pass the interrupt along using the MSC 5.1 chain_intr( ) function.

This program will not work properly on a 386 machine running in Virtual 86 mode, however. If you run Qualitas'386 ^ Max, for example, it's as if you hadn't installed the interrupt handler at all:

A Privileged operation exception at address 0ED3:003A.
Press any key to restart your computer.

(In fact, pressing any key has no effect, and you have to reach around the back for the power switch.)

Running FFFF.EXE under Windows/ 386 doesn't hang your machine like 386 ^ Max, but it does terminate the application, and the message box it displays sounds ominous for a program that simply wants to peek at two bytes starting at offset FFFF (on an 8088, memory would wrap around):

This application has violated system integrity and will be terminated. This may result in the system becoming unstable. It is strongly recommended that you close all applications, exit Windows, and then reboot your machine.

Our exception handler is ignored in Virtual 86 mode because exceptions in V86 mode are vectored through the protected-mode interrupt descriptor table (IDT), not through the 8086-style interrupt vector table at memory location zero. In fact, it is important that Virtual 86 control programs keep control over INT 0D, because GP fault is precisely the mechanism that programs such as Windows/386 use to virtualize I/O. It would be nice, though, if V86 control programs would not scare us with dire warnings of hellfire, brimstone, and data loss.

Protected-Mode MS-DOS

Stalking the real-mode GP fault is a good introduction to handling GP faults in programs running under a DOS extender. DOS extenders run in protected mode, but switch into real mode to use the INT 21 facilities provided by MS-DOS, to use other INT facilities (for example, the mouse or NetBIOS), or to call functions in real mode (for example, some graphics libraries).

Installing a GP fault handler in a DOS extender is similar to using the MS-DOS Set Vector function, and most DOS extenders use the same INT 21 function 25h interface as real-mode MS-DOS. But, as in V86 mode, Set Vector for protected mode manipulates the protected-mode IDT rather than the low-memory interrupt vector table. Also, Set Vector in protected mode must be able to install interrupt handlers located in high memory (above one Mbyte). Set Vector for a 32-bit environment such as Phar Lap's 386|DOS-Extender must be able to use 32-bit offsets (which, added to 16-bit segment selectors, make for 48-bit far addresses). And, finally, because these environments switch back into real mode, DOS extenders also must provide a way to install real-mode interrupt handlers.

GPFAULT.C (Listing Three, page 120)is a program that runs under two 286-based DOS extenders: DOS/16M from Rational Systems, and OS/286 from Eclipse Computing (formerly AI Architects). The phrase 286-based means that these DOS extenders can run on any IBM-compatible equipped with an Intel 80286 and up. Developers will presumably be using a 386, but aren't frozen out of the much larger AT market. This 16-bit program can be compiled with Microsoft C 5.1, and then run through a post-processor (DOS/16M MAKEPM or OS/286 EXPRESS). The resulting program, GPFAULT.EXP, can then be spliced with a protected-mode loader to form GPFAULT.EXE.

The GPFAULT program is a mock interpreter that allows the user to try to poke at arbitrary locations in memory, as shown in Figure 2, the session with the DOS/16M version.

Figure 2: A GPFAULT.EXP session

     C:\DOS16M>gpfault
     DOS/16M Protected Mode Run-Time Version 3.25
     Copyright (C) 1987, 1988, 1989 by Rational Systems, Inc.
     'Q' to quit, '!' to reinstall default GP fault handler
     00A0:04C6 is a legal address to poke
     00A0:04C4 is not a legal address to poke
     $ 1234:5678 666
     Protection violation at 0088:00C5!
     Error code 1234
     <ES 00A0> <DS 00A0> <DI 1AC0> <S10082>
     <AX 0015> <BX 0BF8> <CX 0015> <DX 0000>
     $ 00A0:04C4 666
     poked 00A0:04C4 with 666
     $ 00A0:04C2 1
     poked 00A0:04C2 with 1
     $ 0088:00C5 666
     Protection violation at 0088:00CB!
     <ES 0088> <DS 00A0> <DI 1AC0> <SI 0082>
     <AX.029A> <BX 00CB> <CS 0015> <DX 0000>
     $ 0:0 0
     Protection violation at 0088:00CB!
     <ES 0000> <DS 00A0> <DI 1AC0> <SI 0082>
     <AX 0000> <BX 0000> <CX 0015> <DX 0000> $!
     $!
     $ 0:0 0
     DOS/16M: Unexpected Interrupt=000D at 0088:00CB
     code=0000 ss=00A0 ds=00A0 es=0000
     ax=0000 bx=0000 cx=0015 dx=0000 sp=1982 bp=1A92 si=0082 di=1AC0
     C:\DOS16M>

It is interesting that while poking at 1234:5678, 0088:00CB; and 0000:0000, all caused GP faults that happened in different ways. In fact, we can see why this is called the general protection fault: It is a catchall for everything that doesn't fit into some other exception.

In the example session, 1234:5678 was a completely bogus pointer, and the processor refused even to load 1234 into a segment register (note that ES still contains the selector 00A0). We didn't get anywhere near trying to poke this address: There was no such address in this session. In contrast to real mode, in which every segment:offset combo points somewhere (in this sense, real mode is magic), memory in protected mode is a sparse matrix: Most addresses you can form don't point anywhere.

Because the processor refuses to load the number, the GP fault handler can't find it in the registers and, consequently, has no way of knowing what caused the fault, unless the processor also gives it some additional information.

Fortunately, the Intel processor provides this information by passing an extra parameter on the stack of a GP fault handler. When GPFAULT.H is included in a protected-mode program, the REG_PARAMS structure contains a field for this error code. In a C interrupt handler, this ext a parameter goes between the CS:IP and FLAGS pushed by the processor and the PUSHA supplied by the C compiler.

The handler could use the Intel LAR and LSL instructions to figure out why a GP fault took place (did we try to write into code, did the offset overrun the segment limit, and so on), so that it could provide the user with a more informative message or perhaps correct the error.

Trying to poke 0088:00C5 did not produce an error code (the processor just pushes a 0 on the handler's stack). This is because 0088 was a valid segment selector; in fact, it was GPFAULT.EXP's code segment. This means that loading it into ES was a legal operation (as we can see from the register dump), but trying to poke it caused a GP fault.

This explains why the CS:IP where the fault took place was a few bytes further down from where we tried poking the totally bogus address. In GPFAULT.C, the crucial line of code

  *fp = data;

becomes three instructions:

   less bx, dword ptr _fp
   mov ax, word ptr _data
   mov word ptr es:[bx], ax

Loading a bogus segment selector faults on the first instruction, whereas doing something illegal with a genuine segment selector faults on the third.

Trying to poke memory location zero is a special case. While segment selector 0 is bogus in the sense that it is guaranteed not to correspond to an actual segment descriptor, loading 0 into a segment register is not illegal: It's using any resulting pointer that's illegal. In this way, the Intel processor in protected mode lends support to high-level languages that treat memory location zero as the NIL pointer.

While on the subject of 0:0, remember that even trying to peek at this location caused Smalltalk/V286 and UR/Forth OS/2 to blow up. Here we tried to poke it, but because we installed an INT 0D handler, the mock-interpreter stayed in control. At the end of the session, I told GPFAULT to reinstall DOS/16M's default GP fault handler, and then did another POKE 0:0 0. The result this time was that DOS/16M spewed out some diagnostics and the interpreter halted. This is the sort of behavior we've avoided by installing our own handler.

The IRQ5 Problem

Microsoft C 5.1 provides the function dos_setvect( ) to install an interrupt handler. Turbo C provides setvect( ). These functions merely call INT 21 function 25. Can these functions he used from a protected-mode DOS extender to install a GP fault handler?

All DOS extenders provide an INT 21 interface for protected-mode programs (that, in fact, is a pretty good definition of a DOS extender), and that includes INT 21 function 25. This means that a lot of even low-level code, including a call to _dos_setvect( ), works transparently under a DOS extender.

However, the GP fault is a little unusual. INT 0D lies in the range of Intel processor exceptions that are also IBM AT-compatible hardware interrupts. On the 286 and above, Intel reserved INT 0D and other slots for processor exceptions like the GP fault, but IBM wasn't thinking about protected mode at the time and overloaded INT 08 through 0F for use by the AT hardware interrupts IRQ0 through IRQ7. Thus, INT 0D is multiplexed between the GP fault and IRQ5 (which, in turn, may be used by LPT2 or by an Ethernet board or by an interrupt-driven SCSI board or by ...).

The 386-based DOS extender from Phar Lap and Quarterdeck's DESQview 386 both solve this problem by reconfiguring the 8259 programmable interrupt controller (PIC) so that IRQ0 through IRQ7 are relocated to INT 78 through 7F (see Ray Duncan, "Power Programming," PC Magazine, October 17, 1989).

But 286-based DOS extenders happen not to reprogram the 8259 PIC. So, when you install an INT 0D handler with INT 21 function 25, are you installing a protected-mode GP fault handler or a real-mode IRQ5 handler? The 286-based DOS extenders try to "Do the Right Thing" (to trivialize the title of a recent movie). And alternatives to INT 21 function 25 are provided when the extender's default behavior isn't what you need.

Returning to GPFAULT.C in Listing Three, for DOS/16M, I used the functions D16pmInstall( )and D16pmGetVector( ). The "pm" in the function names stands for protected mode. These functions, along with many other C interface routines for protected-mode programming, appear in the file DOS-16LIB.C that comes with DOS/16M and with Rational Systems's protected-mode development environment, Instant-C.

The Limits of Protection

In addition to illustrating how to catch the GP fault, Listing Three also shows its limitations. Having committed one GP fault and seeing the register dump, the user now knows the selector number for DS, and an error prone or malicious user can freely poke at the program's internal data.

Doing so doesn't cause a GP fault. Instead, the more places the user clobbers, the more erratically the program behaves. Generally, the interpreter's strings are clobbered first. Because they take up more room in memory than other variables, there's a good chance that random pokes will first hit strings. Soon, crucial program data is poked, and the program itself causes a GP fault. This is somewhat like the game Core Wars (which first appeared in the "Computer Recreations" column of Scientific American), in which two assembly-language programs try to make each other execute an illegal opcode.

A GP fault handler must be able to distinguish its own "internal" GP faults from those caused directly by user code. GPFAULT.C keeps a "whereami" flag. The GP fault handler checks whether (whereami==IN_USER_CODE) which indicates whether INT 0D occurred in any known places in the code where a GP fault is expected or allowed to occur. If it didn't, then the INT 0D was caused by an internal error (or by a real mode IRQ5, though in that case the processor won't have pushed the additional error code, and our interrupt handler expecting a REG_PARAMS on the stack will view CS and IP as garbage). If the fault took place while directly executing code on behalf of the user, then we print out some diagnostics and longjump( ) back to the interpreter's main input loop. longjmp( )is a non-local goto. The jmp_buf holds the state of the program's registers at the time we called setjmp( ), and calling longjmp( ) restores this state. The program's thread of execution resurfaces as if it had just "returned" from setjmp( ).

Signal handlers that execute longjmp( ) are notoriously unreliable. So you might have some doubts about taking a longjmp( ) from within an interrupt handler. For example, what about all the junk that was pushed on the stack at entry to the interrupt handler? If we longjmp( ) out of the interrupt handler, we never execute the IRET instruction and, it seems, we never clean up the stack. The jmp_buf, however, includes the stack pointer from when setjmp( ) was first called, so longjmp( ), by loading this saved stack pointer into SP, will effectively cut back the stack.

There's still a problem with an interrupt handler that doesn't return: It can leave the program in an incomplete state. By itself, longjmp( ) is insufficient for true exception handling. In a real program, the jmp_buf should probably be enclosed within a larger application-defined structure that includes other fields necessary for cleanup (such as open file handles, data base indexes, and so on). For a good discussion of longjmp( )'s problems as an exception-handling mechanism, see William M. Miller's paper, "Exception Handling without Language Extensions," Usenix Proceedings C++ Conference, Denver, Colo., 17 - 21 October, 1988.

What does the fault handler do if the flag indicates the program wasn't in user code? The GP fault must then have been caused by our own code (perhaps because the user tromped on some of our data, including possibly the whereami flag itself). In this program, I print out a message and then use chain_intr( ) to call the default GP fault handler, which terminates the application. In a real program, you might want to try to give users a chance to save their work, perhaps by disabling all menu items except "File Save..." and displaying a "The End Is Near" message.

Because the whereami flag itself is so easy to tromp on, it holds "magic"numbers rather than simple enum values. And because the jmp_buf top level is so crucial, the program maintains two of them. In a real program, you might want to make such essentials read only while executing user code. Even better, you might want to try to run user code using a different local descriptor table (LDT) from the one you use for the interpreter itself.

Any protection scheme has similar limitations. Who protects the protector? Another protector, of course, almost (but not quite) as vulnerable. Ultimately, the protection problem is the same as the halting problem.

This concludes Part I of the epic saga, "Stalking GP Faults." Tune in again next month, when our hero gets caught inside 32-bit code running under a 386-based DOS extender, and inside 16-bit code running under the OS/2 operating system. Until then, remember: To commit a GP fault is human, but to catch it and recover, divine.

_STALKING GENERAL PROTECTION FAULTS: PART I_ by Andrew Schulman

[LISTING ONE]

<a name="0045_000e">

/* FFFF.C
    -- causes GP fault in real mode on 286/386, and in Virtual 86 mode
    -- catch it in real mode
    -- can't catch it in Virtual 86 mode

Turbo C: tcc ffff.c
Microsoft C: cl ffff.c
*/

#include <stdio.h>
#include <dos.h>
#include "gpfault.h"


void (interrupt far *old)();

void fini(char *msg, int exit_code)
{
    puts(msg);
    _dos_setvect(INT_GPFAULT, old);
    exit(exit_code);
}

void far my_exit(void) { fini("Bye!", 1); }

void interrupt far handler(REG_PARAMS r)
{
    printf("\nProtection violation at %04X:%04X\n", r.cs, r.ip);
    /* change CS:IP on stack so control is "returned" to my_exit */
    /* this is an alternative to using longjmp() */
    r.cs = FP_SEG(my_exit);
    r.ip = FP_OFF(my_exit);
}

main()
{
    int *p = (int *) -1;
    old = _dos_getvect(INT_GPFAULT);
#ifndef CRASH_AT
    _dos_setvect(INT_GPFAULT, handler);
#endif
    printf("int at %p is ", p);
    printf("%04X\n", *p);
    /*NOTREACHED on 286/386 */
    fini("Done!", 0);
}



<a name="0045_000f"><a name="0045_000f">
<a name="0045_0010">
[LISTING TWO]
<a name="0045_0010">

/* GPFAULT.H
    -- REG_PARAM structure represents stack at entry to interrupt handler
    -- CPU pushes flags, CS:IP, and, for protected-mode INT 08-0D,
       an error code
    -- Compiler pushes all other registers at entry to interrupt function
    -- Turbo C pushes registers in a strange order
    -- Watcom C 386 7.0 also pushes FS and GS registers (unfortunately
       MetaWare High C for MS-DOS 386 1.5 does not)

    -- replace Microsoft C 5.1 FP_SEG, FP_OFF macros with ones that don't
       requires lvalues
    -- keep Watcom C 386 7.0 FP_SEG, etc. -- these work for 48-bit pointers
    -- for MetaWare High C for 386 MS-DOS, need our own 48-bit FP_SEG, etc.
*/

#ifdef InstantC_16M
/* Rational Systems Instant-C/16M protected-mode C interpreter */
#define DOS16M
#define PROT_MODE
#endif

typedef struct {
#if defined(__WATCOMC__) && defined(__386__)
    unsigned gs,fs;
#endif
#ifdef __TURBOC__
    unsigned bp,di,si,ds,es,dx,cx,bx,ax;
#else
    unsigned es,ds,di,si,bp,sp,bx,dx,cx,ax;     /* same as PUSHA */
#endif
#ifdef PROT_MODE
    unsigned err_code;                          /* for pmode INT 08-0D */
#endif
    unsigned ip,cs,flags;
    } REG_PARAMS;

#ifdef __TURBOC__
#define _dos_setvect(x,y)   setvect(x,y)
#define _dos_getvect(x)     getvect(x)
#endif

#define INT_GPFAULT         0x0D

/* 386 protected-mode: far pointer is 48 bits; near pointer is 32 bits */
/* thus, 386 pmode near pointer can hold a real-mode far pointer */
#ifdef __HIGHC__
#define real_far            _near
#define prot_far            _far
#define far                 _far
#else
#if defined(__WATCOMC__) && defined(__386__)
#define real_far            near
#define prot_far            far
#endif
#endif

#ifdef __HIGHC__
/* use overlay struct: no High C support for 48-bit immediate values */
/* remember that unsigned is 32 bits, short is 16 bits */
typedef struct { unsigned off; short seg; } overlay;

#define FP_SEG(fp)          ((overlay prot_far *) &(fp))->seg
#define FP_OFF(fp)          ((overlay prot_far *) &(fp))->off
#else
#if (!(defined(__WATCOMC__) && defined(__386__)))
/* Microsoft C FP_SEG() and FP_OFF() require an lvalue:  yuk! */
#ifdef FP_SEG
#undef FP_SEG
#undef MK_FP
#undef FP_OFF
#endif
#define FP_SEG(fp)          (((UL)(fp)) >> 16)
#define MK_FP(seg,off)      ((FP)(UL)(((UL)(seg) << 16) | (off)))
#define FP_OFF(fp)          ((unsigned)(fp))
#endif
#endif

typedef unsigned long UL;
typedef void far *FP;
typedef enum { FALSE, TRUE } BOOL;

#ifdef __HIGHC__
#pragma Calling_convention(C_interrupt | _FAR_CALL);
typedef void (*IPROC)();
#pragma Calling_convention();
#else
typedef void (interrupt far *IPROC)();
#endif



<a name="0045_0011"><a name="0045_0011">
<a name="0045_0012">
[LISTING THREE]
<a name="0045_0012">

/* GPFAULT.C -- for AI Architects OS/286 or Rational Systems DOS/16M

for AI Architects:
    cl -AL -Ox -Gs2 -c -DPROT_MODE gpfault.c
    link gpfault,gpfault,gpfault/map,\os286\llibce;
    \os286\express gpfault
    cp gpfault

for DOS16M:
    if not exist dos16lib.obj cl -AL -Ox -Gs2 -c source\dos16lib.c
    cl -AL -Ox -Gs2 -c -DPROT_MODE -DDOS16M -Zi gpfault.c
    link /co preload crt0_16m pml gpfault dos16lib /noe,gpfault;
    makepm gpfault
    splice gpfault gpfault
    d gpfault
*/

#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <string.h>
#include <dos.h>
#ifdef DOS16M
#include "dos16.h"
#endif
#include "gpfault.h"

#define IN_MY_CODE          11593
#define IN_USER_CODE        16843
#define IN_HANDLER          40311

unsigned whereami = IN_MY_CODE;     /* need our own protection for this */
jmp_buf toplevel;
jmp_buf toplevel_copy = {0};        /* initialized, in a different segment */
unsigned legal = 0;                 /* just a legal address to bang on */
void (interrupt far *old_int13handler)();

void interrupt far int13handler(REG_PARAMS r);  /* GP fault handler */
void goto_toplevel(void);       /* longjump out of handler */
void revert(void);              /* restore default handler */
void fail(char *msg, FP fp);    /* fail by calling default handler */

main(int argc, char *argv[])
{
    char buf[255];
    unsigned far *fp;
    unsigned data;

    old_int13handler = _dos_getvect(INT_GPFAULT);
    _dos_setvect(INT_GPFAULT, int13handler);

    printf("'Q' to quit, '!' to reinstall default GP Fault handler\n");
    printf("%Fp is a legal address to poke\n", &legal);
    /* next line helps illustrate limitations of protection */
    printf("%Fp is not a legal address to poke\n", &legal-1);

    setjmp(toplevel);
    whereami = IN_MY_CODE;
    memcpy(toplevel_copy, toplevel, sizeof(jmp_buf));

    for (;;)
    {
        printf("$ ");
        *buf = '\0';
        gets(buf);

        if (toupper(*buf) == 'Q')
            break;
        else if (*buf == '!')
        {
            revert();
            continue;
        }

        sscanf(buf, "%Fp %u", &fp, &data);
        whereami = IN_USER_CODE;
        *fp = data;     /* the crucial line of code */
        printf("poked %Fp with %u\n", fp, *fp);
        whereami = IN_MY_CODE;
    }

    revert();
    puts("Bye");
    return 0;
}

void revert(void)
{
    _dos_setvect(INT_GPFAULT, old_int13handler);
}

void fail(char *msg, FP fp)
{
    (fp) ? printf(msg, fp) : puts(msg);
    revert();
    _chain_intr(old_int13handler);
}

void goto_toplevel(void)
{
    if (memcmp(toplevel, toplevel_copy, sizeof(jmp_buf)) == 0)
        longjmp(toplevel, -1);
    else
        fail("Toplevel context has been trompled", 0);
}

void interrupt far int13handler(REG_PARAMS r)
{
    switch (whereami)
    {
        case IN_HANDLER:
            fail("\nDouble fault at %Fp\n", MK_FP(r.cs, r.ip));
            /*NOTREACHED*/
        case IN_MY_CODE:
            fail("\nInternal error at %Fp\n", MK_FP(r.cs, r.ip));
            /*NOTREACHED*/
        case IN_USER_CODE:
            whereami = IN_HANDLER;
            _enable();  /* reenable interrupts */
            /* we could use Intel LAR and LSL instructions here to
            figure out why GP fault took place:  did we try to write
            into code?  or did offset overrun segment limit? */
            printf("\nProtection violation at %04X:%04X\n", r.cs, r.ip);
            if (r.err_code)
                printf("Error code %04X\n", r.err_code);
            printf("<ES %04X> <DS %04X> <DI %04X> <SI %04X>\n",
                r.es, r.ds, r.di, r.si);
            printf("<AX %04X> <BX %04X> <CX %04X> <DX %04X>\n",
                r.ax, r.bx, r.cx, r.dx);
            goto_toplevel();
            /*NOTREACHED*/
        default:
            whereami = IN_HANDLER;
            _enable();
            puts("whereami flag got trompled");
            goto_toplevel();
            /*NOTREACHED*/
    }
}

#ifdef DOS16M
void _dos_setvect(unsigned intno, IPROC isr)
{
    D16pmInstall(intno, FP_SEG(isr), FP_OFF(isr), NULL);
}

IPROC _dos_getvect(unsigned intno)
{
    IPROC isr;
    D16pmGetVector(intno, (INTVECT *) &isr);
    return isr;
}
#endif


Example 1: This code will cause a GP fault in protected mode

        main()
        {
            int far *fp = (int far *) main;
            *fp = rand();
            main();
        }

Example 2: If this program GP faults, it's because of an error by
the user, not by the application itself.

        main(int argc, char *argv[])
        {
            int far *fp = (int far *) atol(argv[1]);
            *fp = atoi(argv[2]);
        }



Figure 1: A GP fault dump



        Session Title: UR/Forth
            SYS1943: A program caused a protection violation.
            TRAP 000D
            AX=2092 BX=0000 CX=FFFF DX=3FC2 BP=FFFA
            SI=05EA DI=05E4 DS=00C7 ES=0000 FLG=2206
            CS=0227 IP=2093 SS=00C7 SP=FBFC MSW=FFED
            CSLIM=FFFE SSLIM=FFFF DSLIM=FFFF ESLIM=****
            CSACC=DF SSACC=F3 DSACC=F3 ESACC=**
            ERRCD=0000 ERLIM=**** ERACC=**
                End the program


Figure 2: A GPFAULT.EXP session




        C:\DOS16M>gpfault
        DOS/16M Protected Mode Run-Time     Version 3.25
        Copyright (C) 1987,1988,1989 by Rational Systems, Inc.
        'Q' to quit, '!' to reinstall default GP fault handler
        00A0:04C6 is a legal address to poke
        00A0:04C4 is not a legal address to poke
        $ 1234:5678 666
        Protection violation at 0088:00C5!
        Error code 1234
        <ES 00A0> <DS 00A0> <DI 1AC0> <SI 0082>
        <AX 0015> <BX 0BF8> <CX 0015> <DX 0000>
        $ 00A0:04C4 666
        poked 00A0:04C4 with 666
        $ 00A0:04C2 1
        poked 00A0:04C2 with 1
        $ 0088:00C5 666
        Protection violation at 0088:00CB!
        <ES 0088> <DS 00A0> <DI 1AC0> <SI 0082>
        <AX 029A> <BX 00CB> <CX 0015> <DX 0000>
        $ 0:0 0
        Protection violation at 0088:00CB!
        <ES 0000> <DS 00A0> <DI 1AC0> <SI 0082>
        <AX 0000> <BX 0000> <CX 0015> <DX 0000>
        $ !

        $ 0:0 0
        DOS/16M: Unexpected Interrupt=000D  at 0088:00CB
        code=0000 ss=00A0 ds=00A0 es=0000
        ax=0000 bx=0000 cx=0015 dx=0000 sp=1982 bp=1A92 si=0082 di=1AC0
        C:\DOS16M>










Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.