GP-TRICKS Logo

Scalar's GP-TRICKS Homepage

Site MapTutorial Section386 Protected Mode Programming


DPMI Saves The Day

386 Protected Mode Programming

by M. G. Ricken



Borland Pascal provides a powerful programming platform, especially for beginners: an excellent IDE, a mighty language, and in Version 7.0 even a lot of memory it can access with its DPMI support. In short, almost anything that can be done with C/C++ can also be done with Borland Pascal (except for 32-bit Windows programming, but for that, there is Borland Delphi).

There are, however, a few small problems that arise when you want to take Borland Pascal to the bleeding edge of programming. Mainly the fixed and in a way very limited memory model of Borland Pascal may cause you to have a few sleepless nights, for example if you have to code Network routines, a Super VGA library, or just about anything that requires you to call a Real Mode driver. Let's have a look why calling Real Mode routines is so difficult.


Protected Mode and Real Mode

With today's computers it would be a shame if the programmer could not access the megabytes of memory that get shipped even with entry level PCs. Borland Pascal 7.0 (or BP, how I am going to call it from now on) comes with DPMI support to enable you to program in Protected Mode and use up to 16Mb of RAM. All this is packaged into BP's easy-to-use memory manager, and since you have enough to do when programming a game you will also want to use it. So what you really need is a way to keep the comfortable memory interface of Borland Pascal and still have the possibility of low-level hardware and driver access.

The Protected Mode of the Intel 386 and newer machines is one of the best things that Intel came up with. It makes it possible to execute multiple programs at the same time without any unwanted interference between them. The CPU employs some powerful mechanisms to protect memory and applications from outside influences, hence the name Protected Mode.

When doing normal programming in BP, most likely you will not discover any differences between Real and Protected Mode. If you have to dive deeper, you certainly will. Since this is not an introduction to Protected Mode programming, I will not explain every detail, just as much as needed to understand how to interface a Real Mode driver. If you need to know more, please refer to a hardware book or do a web search, because this topic is worth another full-sized article.

When you use BP's Intr procedure to call the IPX driver, for example, you will get a Runtime Error 216 or some other mysterious error message. This is because the program is running in Protected Mode and uses selectors as values inside the segment registers. Selectors can be thought of as indexes in a large table that stores segment lengths and locations.

When you call a Real Mode driver, however, the driver will use the segment registers as in Real Mode, and in most cases, this will lead to a General Protection Fault (GPF).


Calling a Real Mode Driver

To avoid a GPF, you have to find a different way to call the driver. Maybe it is possible to switch back to Real Mode for the time the driver function executes, you think. You are right, it is possible, and the DPMI interface shows the way.

The first thing to do on your way to do Real Mode driver calls is to replace BP's Intr procedure with your own, custom-made one. This procedure has to pass the address of a record containing register values to the DPMI function 300h. Other parameters are the number of the interrupt to call and the number of stack words to copy from your Protected Mode stack to the Real Mode stack of the interrupt driver. The latter value will almost always be zero. A procedure doing all this might look like this:

Listing 1
Borland Pascal 7.0
function  Intr386(const int_number:byte; const stack_words:word;
                  var regs:DPMIRegisters) : word; assembler;
(* Make Real Mode interrupt while in Protected Mode.         *)
asm
   xor bx,bx                                       { clear bx }
   mov bl,int_number                  { load interrupt number }
   mov cx,stack_words                       { get stack words }
   les di,regs               { load effective address of regs }
   mov ax,0300h                        { function number 300h }
   int 31h                                   { call interrupt }
   jc @pexit              { if error jump to end of procedure }
   xor ax,ax                           { clear ax; successful }
@pexit:                                    { end of procedure }
end;

Prior to calling this function, a variable of the type DPMIRegisters has to be filled with the required values. On return, the variable will contain the values passed back from the interrupt function. Here is the declaration of the DPMIRegisters structure:

Listing 2
Borland Pascal 7.0
type DPMIRegisters = record    { registers for DPMI int calls }
      case byte of
           1 : (EDI,ESI,EBP,EReserved,
                EBX,EDX,ECX,EAX:longint;
                Flags,ES,DS,FS,GS,
                IP,CS,SP,SS:word);
           2 : (DI,XDI,SI,XSI,BP,XBP,Reserved,
                XReserved,BX,XBX,DX,XDX,CX,XCX,
                AX,XAX:word);
           3 : (DIL,DIH,XDIL,XDIH,
                SIL,SIH,XSIL,XSIH,
                BPL,BPH,XBPL,XBPH,
                ReservedL,ReservedH,
                XReservedL,XReservedH,
                BL,BH,XBL,XBH,
                DL,DH,XDL,XDH,
                CL,CH,XCL,XCH,
                AL,AH,XAL,XAH,
                FlagsL,FlagsH,
                ESL,ESH,DSL,DSH,FSL,FSH,
                GSL,GSH,IPL,IPH,CSL,CSH,
                SPL,SPH,SSL,SSH:byte);
        end;

As you can tell, I have used a variant record which will let me access all parts of the registers, standard ones as EAX, BX, and BL as well as obscure ones like XBPL, which maps to the low byte of the high word of the BP register. Basically, everything you do with this structure is straight-forward, but take your time to experiment: Often things like writing to the low word of a register and then shifting it into the extended part can now be done much easier.

Ok, now you can call simple functions like the multiplex interrupt that do not require further access to memory. But what happens if you have to supply an address to a memory buffer that the interrupt will fill with data? The program will abort with a GPF, since the interrupt driver will again use the values given in the segment registers as immediate segment addresses instead of as selectors. But again, DPMI provides the work-around.


Real Mode Memory

In order to have a memory block that Real and Protected Mode programs can share, it has to be created below 1Mb, that means within the reach of Real Mode programs. To do this, you can use BP's GlobalDosAlloc and GlobalDosFree functions, which are prototyped in the WinAPI unit.

When given the number of bytes to allocate, the GlobalDosAlloc function will return a longint that contains the selector in the low word and the Real Mode segment address in the high word. After the function call, I propose to store both values in a record structure (PPointer) that will almost completely replace the pointer type while you work with Real Mode drivers. Inside this record, there is a pointer named rm, which will be passed on to the interrupt, and a pointer called pm which you can use to fill the buffer.

Remember, though, that memory below 1Mb may be extremely scarce, so free the DOS memory block as soon as you can using GlobalDosFree, which will, by the way, only need to know the selector of the memory block. This is the code snippet for DOS memory allocation:

Listing 3
Borland Pascal 7.0
type LONGREC  = record lo,hi: word; end;   { LONGINT typecast }
     PPointer = record          { real/protected mode pointer }
                   rm,pm      : pointer;            { pointer }
                end;

(* Allocate DOS memory in Protected Mode.                    *)
function  AllocateDOS(var p; const size:word) : boolean;
var   l : longint;               { dummy for segment/selector }
begin
   l:=GlobalDosAlloc(size); AllocateDOS:=l<>0;
                                            { allocate memory }
   PPOINTER(p).rm:=Ptr(LONGREC(l).hi,0);
                                   { create real mode pointer }
   PPOINTER(p).pm:=Ptr(LONGREC(l).lo,0);
                              { create protected mode pointer }
end;

(* Free DOS memory in Protected Mode                         *)
procedure FreeDOS(const p; const size:word);
begin
   GlobalDosFree(PTRREC(PPOINTER(p).pm).seg);   { free memory }
end;

(* Clear DOS memory.                                         *)
procedure ClearDOS(var p; const size:word);
begin
   FillChar(PPOINTER(p).pm^,s,0);
                             { fill at protected mode pointer }
end;

procedure CopyPPointer(const source; var dest);
(* Copy pointers. THIS DOES NOT COPY THE DATA SOURCE POINTS  *)
(* TO TO DEST!!! This merely does dest:=source, as if source *)
(* and dest were pointers.                                   *)
begin
   PPOINTER(dest).rm:=PPOINTER(source).rm;
   PPOINTER(dest).pm:=PPOINTER(source).pm;
end;

Just a few remarks to the code above: The FreeDOS function requires you to pass the size in bytes, even if it is not used in this version. I did this because I maintain a Real Mode and a DPMI version of my IPX routines and wanted a consistent interface. All the functions are exactly the same, except for the memory management routines. While GlobalDosFree does not need to know the size of the block to free, FreeMem has to.

Secondly, the CopyPPointer procedure does not copy the data from one location to another. All it does is assign the values in one PPointer to a second one. In C++ you could overload the set- equal operator and write "ppointer1=ppointer2;".

Finally, I used untyped parameters for passing the PPointers. I did this because otherwise I would have had to typecast to a PPointer very often.

These procedures solve the cases when the interrupt driver wants to write to your memory. To read and write memory that belongs to Real Mode programs you have to do something completely different. You have to map a selector to the Real Mode address. This is a very delicate job, but DPMI saves the day (or the night). Explaining all the details of mapping is too much for this article. If you are interested please refer to other sources.

Basically, the GetMappedDPMIPtr (printed below) will get a new selector entry for you and then set the address and the size of the chunk of memory, after which you may manipulate the Real Mode memory. When you're finished with whatever you had to do, free the selector using FreeMappedDPMIPtr. Do this quickly, because the number of selectors is also limited.

Listing 4
Borland Pascal 7.0
function GetMappedDPMIPtr(var ProtPtr;
                          const RealPtr:pointer;
                          const Size:word) : boolean;assembler;
(* Get a Protected Mode pointer to a Real Mode address.      *)
asm
   xor ax,ax        { Get an LDT descriptor & selector for it }
   mov cx,1
   int 31h
   jc @@Error
   xchg ax,bx
   xor ax,ax                 { Set descriptor to real address }
   mov dx,RealPtr.Word[2]
   mov al,dh
   mov cl,4
   shr ax,cl
   shl dx,cl
   xchg ax,cx
   mov ax,7
   int 31h
   jc @@Error
   mov ax,8              { Set descriptor to limit Size bytes }
   xor cx,cx
   mov dx,Size
   add dx,RealPtr.Word[0]
   jnc @@1
   xor dx,dx
   dec dx
@@1:
   int 31h
   jc @@Error
   cld                      { Save selector:offset in ProtPtr }
   les di,ProtPtr
   mov ax,RealPtr.Word[0]
   stosw
   xchg ax,bx
   stosw
   mov ax,1
   jmp @@Exit
@@Error:
   xor ax,ax
@@Exit:
end;

function FreeMappedDPMIPtr(const ProtPtr:pointer) : boolean;
         assembler;
(* Free selector given by GetMappedDPMIPtr.                  *)
asm
   mov ax,1
   mov bx,ProtPtr.Word[2]
   int 31h
   mov ax,0
   jc @@Error
   inc ax
@@Error:
end;

As you can see, you have to tell the GetMappedDPMIPtr function how many bytes need to be mapped. Of course you could always put 0FFFFh in there, but in order to leave as much protection as you can in Protected Mode, limit yourself to the minimum needed. This is just a matter of good programming style, though.



Back to Top of Page


This article is © 1997-2008 by M. G. Ricken and was exclusively written for GP-Tricks.
Copyright © 1998-2008 by M.G.Ricken        E-Mail: Scalar@psynet.net     |     mgricken@gptricks.de