
Scalar's GP-TRICKS Homepage
| Site Map | Tutorial Section | 386 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
|