DPMI Meets C++

DPMI is a programming interface that allows application-level code to run in protected mode. This article looks at DPMI from an object-oriented perspective, using C++ class library as the basis for exploration.


October 01, 1992
URL:http://www.drdobbs.com/cpp/dpmi-meets-c/184408855

Figure 1


Copyright © 1992, Dr. Dobb's Journal

OCT92: DPMI MEETS C++

DPMI MEETS C++

An object-oriented abstraction of DPMI

This article contains the following executables: DPMI.ZIP

Frederick Hewett

Fred is vice president of Cypress Software Ltd. and can be contacted on CompuServe (72647,3472) or MCIMail (FHEWETT).


The DOS Protected Mode Interface (DPMI) has been a part of the DOS programming environment since the release of Windows 3.0. One measure of its success is the impact it has had on DOS-extender vendors and programmers who develop protected-mode applications. Virtually all of them have introduced tools that operate in a DPMI environment and are therefore compatible with Windows Enhanced mode. Both Microsoft and Borland have introduced compiler products not simply compatible with DPMI, but that require a DPMI host in order to run; both vendors bundle a DPMI host as part of their package. As a result, DPMI will be found on far more systems than it has to date, and an increasing number of applications will require and take advantage of it.

Given the likelihood that DPMI is on the threshold of wide proliferation, those who write large non-Windows programs for DOS stand to profit from a sound understanding of DPMI, its interface structure, and its capabilities. To that end, this article looks at DPMI from an object-oriented perspective, using a C++ class library developed by Qualitas (Bethesda, Maryland) as a basis for exploring DPMI. (Qualitas' 386MAX memory manager is a DPMI host.) This class library is available electronically; see "Availability" on page 5 of this issue.

DPMI Backgrounder

DPMI is a programming interface that allows application-level code to run in protected mode under DOS in a virtual 8086-mode operating environment. Programs ran in protected mode under DOS long before DPMI was conceived. Prior to 1988, DOS-extended programs were incompatible with virtual 8086-mode memory managers such as 386-MAX from Qualitas and QEMM from Quarterdeck. The Virtual Control Program Interface (VCPI) provided a means for protected-mode applications to run in these environments. To do so, however, the memory managers had to allow the application to run at the processor's most privileged level. The architects of Microsoft Windows 3.0 felt that this compromised system integrity, and offered DPMI as an alternative.

The key distinction between DPMI and VCPI is that DPMI allows applications to operate only at nonprivileged levels, thereby preventing them from having direct access to system-sensitive structures such as descriptor tables and page tables. Instead, DPMI hosts provide a set of services which, in a controlled fashion, expose the functionality required for protected-mode operation. Specifically, these services allow clients to do the following: allocate, free, and modify entries in the local descriptor table (LDT); allocate and free DOS memory (in the low megabyte); hook interrupts in both real and protected modes; trap processor exceptions (for instance, general-protection faults and segment-not-present faults); allocate, free, and resize blocks of extended memory; enable and disable interrupts; and access the processor's breakpoint capability (debug registers).

DPMI is specified at the assembly language level. Applications call DPMI by loading registers and issuing an INT 31h. For example, a program running in protected mode requests DPMI to allocate three entries in the LDT by setting AX=0000 (the function identifier), and CX = 3 (the number of descriptors to allocate) and issuing an INT 31h. The DPMI host returns the selector of the first of three consecutive descriptors that it allocates in AX, and clears the carry flag to indicate success.

DPMI does a decent job of abstracting the processor's protected-mode capabilities, and assembly language is the appropriate level for its specification. However, to learn about the capabilities of a DPMI host in general, and gain insight into the interface structure, a higher-level of abstraction is required. An object-oriented perspective serves this purpose.

DPMI Meets OOP

The process of making the abstraction consists of first isolating the different data structures DPMI supports (that is, memory blocks and interrupt handlers). The relationships between these entities, along with the operations that DPMI clients may perform on them, lead to the definition of C++ classes. The classes codify the structure of DPMI, clarifying aspects of the interface that might otherwise be obscure.

Over the last few years, much effort has gone into developing methodologies for determining optimal class structure. Since the problem at hand is small, an iterative approach is the most practical: Examine the interface, postulate classes based on that analysis, test the conceptualization with a prototype, then apply the results of the test to further analysis until a smoothly working set of classes emerges.

Before getting into the specifics of the classes, a word about some constraints on the implementation is necessary. The library is designed for 16-bit small-model operation (single code segment, single data segment). A large-model or 32-bit implementation would require a program loader that intelligently performs segment fix-ups on the executable image for protected-mode execution. While technically feasible, this is the domain of DOS extenders, and is beyond the scope of this instructional project. With small-model architecture, near pointers may be used in both real and protected mode.

The DPMI Class Library

Consider first the class DPMIhost (see Example 1), which abstracts the DPMI host itself. By simply declaring an instance of a DPMI host object, a program performs all the necessary operations for detecting the presence of and gaining linkage to a DPMI host. The program may then use the getStatus() member function to verify that a host was indeed found.

Example 1: The class DPMIhost.

  class DPMIhost {
  public:        DPMIhost(void);
          DPMIerr getStatus(void);
          boolean enter ProtectedMode (uChar bitness);
          void getVersion(uChar *major, uChar *minor);
          uChar getSelectorDelta(void);

The enterProtectedMode() member function makes the initial switch from real or virtual 8086 mode into protected mode. The function also installs a set of handlers that gracefully exit to DOS if a fatal exception occurs. Once in protected mode, it is safe to use the other classes in the DPMI library.

The class library defines a set of classes that abstract the DPMI implementation of exception handlers, interrupt handlers, real procedure calls, and real-mode callbacks. Due to space constraints, the complete library is available only electronically; see "Availability," on page 5 for details.

The classes that support memory as age under DPMI, diagrammed in Figure 1, illustrate the basic ideas of the library, and are perhaps the most useful. To see why, consider what DPMI presents to the developer with regards to memory. Conspicuously absent is a call with functionality equivalent to the familiar DOS memory-allocation call (INT 21h, AH=48h). For good reasons, the interface decouples descriptor management and linear-memory management. It requires several DPMI calls to allocate a block of linear memory and to fully initialize a descriptor that addresses the block.

The class library is designed to allow the same level of control that the raw DPMI interface allows, and at the same time make it easy to do common operations like allocation of an addressable block of memory, modification of individual attributes of segments, and release of memory and descriptors back to the host. To achieve both ease of use and low-level control you must create a primitive set of classes that map onto low-level DPMI entities, then use inheritance to synthesize higher-level functionality from these base classes.

Consider first raw linear memory: It contains little room (or need) for abstraction. The library defines the class Block, as shown in Example 2. The constructor takes as an argument a 32-bit value that specifies the desired size of the block. It makes the DPMI call to allocate the memory, and if successful, stores the linear address and a handle in protected data members of the class. The class provides member functions for reading these values.

Example 2: The class Block.

  class Block {
  public:
        Block (uLong size);
        ~Block (void);
        boolean setSize (uLong);
        uLong blockHandle(void);
        uLong blockSize(void);
        uLong blockBase(void);
  protected:
        uLong handle;
        uLong base;
        uLong size;

An instance of the class Block allocates raw linear memory from DPMI, but raw linear memory is of little use by itself. To be useful, one or more descriptors must target that memory. DPMI has a number of ways to create descriptors, and the class library must abstract the differing behaviors of descriptors created by each of these services.

To start, let's itemize the DPMI services whose invocations result in creation of descriptors; see Table 1. The first three create descriptors with the same behavior: They are fully modifiable and are released by DPMI function 0001h. The map-real-paragraph-to-descriptor service creates a descriptor that may not be modified or freed, and is therefore distinguished from the others. Similarly, the descriptors created by the allocate-DOS-memory service require special DPMI calls to modify or free them.

Table 1: DPMI services.

  Function  Name                 Description
  -------------------------------------------------------------------------

  0002h     Allocate Descriptor  Simply allocates one or more LDT entries.
  000Ah     Create Alias         Allocates a descriptor, then sets its
            Descriptor            base, limit, and  attributes according to
                                  an already-existing LDT entry.
  000Dh     Allocate Specific    Allocates a particular entry in the
            Descriptor            LDT.  (First 16 entries are reserved for
                                  this call.)
  0002h     Map Real Paragraph   Creates a descriptor that maps a paragraph
            to Descriptor         in the low megabyte, with a limit of 64
                                  Kbytes.
  0100h     Allocate DOS         Allocates memory from DOS, and allocates
            Memory                one or more LDT entries to map that
                                  memory.

This analysis gives rise to three distinct classes in the class library:

Although the three classes are distinct, one set of operations is common to all. Some examples of such operations are obtaining the segment's base address and size and querying the segment's properties. Each operation may be performed on any descriptor, regardless of how it came into existence. Other operations, such as resizing the segment, are illegal for the CommonRealSegment class, but make sense for Segment and DOSMemory, although different code is required to perform the task in question.

In this situation, you can leverage the power of C++ by introducing a base class that implements the behavior shared between the three segment classes, and that defines member functions corresponding to operations that may be performed on them (albeit in distinct ways). In the DPMI class library, the class AbstractSegment, shown in Example 3, serves this purpose. The only data member of AbstractSegment is selector, which simply stores the selector of the descriptor in question. The member functions segmentSize(), segmentBase(), and queryProp() each use DPMI calls to read information about the descriptor from the LDT. Since it is possible to read this information via DPMI for any descriptor regardless of how it was created, these methods are inherited and used by Segment, CommonRealSegment, and DOSMemory.

Example 3: The class AbstractSegment.

  class AbstractSegment
  {
  public:
       virtual uLong segmentSize(void);
       virtual uLong segmentBase(void);
       virtual booelan queryProp(SegmentProp_t);
       virtual operator+(SegmentProp_t)=0;
       virtual operator-(SegmentPRop_t)=0;
       virtual boolean resize (uShort)=0;
       virtual boolean move (uLong)=0;
       void far *ptrTo(void);
  protected:
       selector_t selector;
  }

Note that in class AbstractSegment, the addition and subtraction operators are overloaded when they act on the type SegmentProp_t. The type SegmentProp_t is an enumeration defined in Example 4. Each member of the enumeration corresponds to one or more bits of the modifiable descriptor attributes. The class library abstracts the operation of modifying descriptor attributes by allowing programmers to simply add or subtract properties from instances of classes derived from AbstractSegment. The resulting Boolean value indicates the DPMI host's success in effecting the requested modification.

Example 4: The type Segment Prop_t.

  typedef enum SegmentProperty {
        present,
        executable,
        readable,
        writable,
        big,
        expandDown
  } SegmentProp_t;

For example, the code in Example 5 shows how a program changes a data segment to an executable segment, and branches on the success of the operation. The syntax for modifying descriptor properties is natural, and demonstrates how operator overloading can improve code readability.

Example 5: Program change of a data segment to an executable segment.

  if (myDataSeg + executable)
  {
  // operation succeeded
  }
  else {
  // operation failed
  }

It's important to note that the operator+() and operator-() member functions are pure virtual functions--they are not implemented in the base class, and must be implemented in any derived class to be instantiated. The same is true for the member functions move() and resize(), which change the base and limit of a segment, respectively. It is incorrect to implement these members for AbstractSegment because their exact semantics depend on the actual descriptor type. AbstractSegment is, in fact, an abstract class; it provides a generic definition of common functionality. AbstractSegment itself, however, cannot be instantiated, because that would require allocation of a descriptor via some DPMI call, and would thereby fix a specific behavior of that object.

The last member function of AbstractSegment is ptrTo(). Using the selector stored within the class instance, this function simply returns a far pointer to the base of the segment that the descriptor defines. The class AbstractSegment implements ptrTo() as an inline function.

Deriving the Memory Classes

Moving up the hierarchy, the derived class Segment has three distinct constructors that correspond to the three DPMI functions that create fully modifiable descriptors. The first constructor has no argument, and uses the basic Allocate Descriptor call (function 0000h) to allocate one descriptor. The second takes a selector (unsigned 16-bit integer) as an argument, and uses the Allocate Specific Descriptor call to allocate one of the first 16 descriptors in the LDT. The third constructor takes a reference to an AbstractSegment (or derived class) and uses the Create Alias Descriptor call to allocate a descriptor and initialize its base, type, and limit in agreement with the descriptor passed to it. If any constructor is unable to allocate a descriptor, the constructor sets the selector field to 0, so that ptrTo() returns a null pointer.

The class Segment implements the pure virtual members of the class AbstractSegment using DPMI's LDT management services; the mappings between these services and the member functions are intuitive. The destructor calls DPMI to release the descriptor to the host, which relieves the programmer from freeing descriptors when they are no longer needed.

The member functions for the class CommonRealSegment return False to indicate that the corresponding operations cannot be performed on them. The class does not define a destructor, since DPMI does not permit clients to free these descriptors. Similarly, the DOSMemory class supports the resize() member function and has a destructor that uses DPMI function 0101h to free the corresponding DOS memory block. It does not, however, allow setting the base address (the move() member function returns False) or modifying segment properties.

What's missing from the set of classes defined so far? You may recall that the Allocate Descriptor call takes as an argument the number of consecutive descriptors to allocate, but an instance of class Segment only allocates a single descriptor. DPMI allows programmers to obtain an ordered set of descriptors that can be set up to span a partition of memory larger than 64 Kbytes--an important capability for addressing huge objects in 16-bit mode. The library includes the class HugeSegment, derived from AbstractSegment. The constructor for HugeSegment takes a 32-bit integer argument that specifies the size (in bytes) of the memory region the segment must span; the class implementation determines from this how many consecutive descriptors it will require. The constructor sets the bases of consecutive component descriptors at intervals of 64 Kbytes. The HugeSegment class supports all the member functions of AbstractSegment, although the implementation is more complex. All member functions, including add or remove property, act on all the component descriptors, and the destructor releases all component descriptors to the host.

Even with all these classes, there is not enough functionality to allocate memory and address it via a descriptor. The class library reflects DPMI's decoupling of memory and descriptors by defining a Block class, along with the descriptor classes derived from AbstractSegment. A class is needed that brings these classes together to create addressable blocks of memory.

You might think an elegant solution would be to simply override the global new operator so that all dynamic allocations use memory and descriptors allocated from DPMI. Unfortunately, this is not possible in the small model environment, because new is thus defined to return near pointers, and addressable memory blocks obtained from DPMI are necessarily far. In a large-model implementation, however, this would be a good solution.

For the small-model implementation, two additional classes provide the desired functionality: MemorySegment and HugeMemorySegment. MemorySegment is derived from Block and Segment, and HugeMemorySegment is derived from Block and HugeSegment.

Declaration of a MemorySegment constructs both a Block, resulting in allocation of raw linear memory, and a Segment, which the class initializes to address the allocated memory. After declaring a MemoryBlock, a program uses the inherited ptrTo() member function to get a far pointer to the memory allocated. The class overrides virtual members of AbstractSegment in a sensible way; the resize() member function, for example, resizes the memory block and updates the base and limit of the descriptor. Behavior is analogous for HugeMemorySegments, and the size is not restricted to less than 64 Kbytes. Multiple derivation of these classes, reflecting the structure of DPMI yields a higher level of functionality, while retaining the granularity of low-level DPMI services.






Copyright © 1992, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.