17.3 Using Interrupts in Protected Mode

Suprisingly enough, calling interrupts in protected mode under DPMI is very similar to calling interrupts in real mode. Even though this isn't 16-bit DOS and DOS interrupt handlers are 16-bit code, most interrupt calls get mirrored automatically by DPMI from protected to real mode. However, there are many situations for which this automatic translation doesn't work. Keep reading for details!

17.3.1 The Interrupt Descriptor Table

Unlike in real mode, where the interrupt vector table is always at address 00000h, in protected mode, the Interrupt Descriptor Table can be anywhere in memory and is protected by the processor and the OS. Also unlike the real-mode interrupt vector table, the IDT stores additional information about the various handlers, but it is essentially still a "jump table" indexed on the interrupt number.

17.3.2 Calling Interrupts under DPMI

Under DPMI, the entries in the IDT point to DPMI's own interrupt service routines. For most of these, DPMI is kind enough to just drop into real mode, call the corresponding real-mode interrupt, and return to protected mode before returning to the calling program. Pretty complicated, but DPMI handles all these extra steps automatically.

For interrupts that don't use segment registers, this automatic translation works extremely well. Interrupts that have inputs or outputs in segment registers, however, need to be called using a special DPMI function, because the segment registers change when switching from protected to real mode and back. Also, even if the segment registers didn't change, in protected mode setting a segment register to a real-mode segment value will (usually) cause a General Protection Fault, because the segment registers must hold a valid selector value while in protected mode. DPMI function 0300h, "Simulate Real Mode Interrupt," allows the program to set all registers that can be read by the real-mode interrupt handler. After the interrupt returns, the values of all the registers set by the real-mode interrupt handler can be read back by the protected mode program.

17.3.3 Giving Data to Real Mode Interrupts

However, there's a more fundamental problem here! Most interrupts that take segment registers as inputs use them to point to data, and they use the segment registers in real-mode fashion (not as selectors). How can a protected mode program give a real-mode interrupt data if this is the case?

Unfortunately, the program can't just call a magic function that translates a selector into a segment. This is impossible unless the selector is located in the 20-bit address space of real mode and is less than 64k in length. Even if it were possible, the offset would still be limited to 64k. Fortunately, DPMI provides an alternative solution: providing a way for a protected mode program to allocate a segment of memory that's guaranteed to be accessible in real mode. DPMI function 0100h, "Allocate DOS Memory Block," allocates space in the low 1 MB of RAM (the 20-bit address space visible to real-mode programs), and returns both a selector that can be used to access this memory from protected mode and a segment that can be used to access this memory from real mode.

Armed with the selector and segment values, a protected mode program can copy the data it wants to give the real-mode interrupt into this memory and give the real-mode segment to the interrupt. The real-mode interrupt then reads the data, and everything works great!

Naturally, the same process can work in reverse: the real-mode interrupt writing into the memory and the protected mode program reading out of it after the interrupt returns.

So how does this DPMI function actually work internally? How does it ensure the memory allocation is in the low 1 MB of RAM? DPMI function 0100h, "Allocate DOS Memory Block," has to go through a number of steps to accomplish this:

  1. Switch to real-mode.

  2. Perform a DOS interrupt call to allocate the memory the DOS is in charge of (below 1 MB).

  3. Calculate the physical address of the memory DOS allocated (by shifting the segment left by 4 and adding the offset).

  4. Allocate an LDT descriptor, and set its base address to the calculated physical address and the length to the size of the allocated memory.

  5. Return the index of this LDT entry in DX

Actually, all of these steps can be accomplished without too much trouble by normal protected mode code. However, it's far easier just to use the DPMI interrupt.

Before exiting, DPMI function 0101h, "Free DOS Memory Block," should be used to free the memory allocated by DPMI function 0100h.

17.3.4 Examples

Now that the basics have been covered on how to call interrupts from protected mode, it's time to look at some example code!

17.3.4.1 Getting the Time

The first example program takes advantage of the automatic mirroring of interrupts into real mode by DPMI. It gets the current time using DOS interrupt 21h, function 2Ch.

Example 17-1. Getting the Time in Protected Mode

BITS 32

GLOBAL _main

_main:
        mov ah, 2Ch     ; Function 2Ch
        int 21h         ; Call DOS interrupt.
                        ;  (Automatically mirrored by DPMI)
; At this point all the return values should be in the protected mode
; registers (CH, CL, DH, DL).

        ret             ; Return to DOS

Build this example program the same way "basic" was built in Section 16.1. Run it under CV32 and look at the registers after the interrupt call to make sure it did actually return the correct time.

17.3.4.2 Output a String to the Screen

This example is going to perform a much more complex task than the first. Essentially it's the old LIB291 dspmsg in protected mode. All dspmsg did internally was call DOS interrupt 21h, function 09h.

The first thing to notice about function 09h is that one of the inputs uses a segment register (DS). This should immediately bring the discussion in Section 17.3.2 to mind: we'll need to use DPMI function 0300h to make the interrupt call, as we can't set DS without using it.

The second thing to notice is that the input takes a pointer to data that the protected mode program needs to provide. This means the program will need to use the procedure detailed in Section 17.3.3 to put the string someplace where DOS can get to it.

Example 17-2. Protected Mode "dspmsg" Program

BITS 32

GLOBAL _main

DOS_BUFFER_LEN  equ     128     ; DOS buffer length, in bytes

SECTION .bss

; DPMI Registers structure used by INT 31h, function 0300h
DPMI_Regs
DPMI_EDI        resd    1
DPMI_ESI        resd    1
DPMI_EBP        resd    1
DPMI_RESO       resd    1
DPMI_EBX        resd    1
DPMI_EDX        resd    1
DPMI_ECX        resd    1
DPMI_EAX        resd    1
DPMI_FLAGS      resw    1
DPMI_ES         resw    1
DPMI_DS         resw    1
DPMI_FS         resw    1
DPMI_GS         resw    1
DPMI_IP         resw    1
DPMI_CS         resw    1
DPMI_SP         resw    1
DPMI_SS         resw    1

; These variables will hold the selector and segment of the DOS
;  memory block allocated by DPMI.
_Transfer_Buf   resw    1
_Transfer_Buf_Seg resw  1

SECTION .data

; String to print to screen
str     db 'Hello, World!',13,10,'$'

SECTION .text

_main

        ; Allocate DOS Memory Block using DPMI
        ; Note that BX=# of paragraphs, so must divide by 16.
        mov     ax, 0100h
        mov     bx, DOS_BUFFER_LEN/16
        int     31h
        ; Save the return info in the Transfer_Buf_* variables.
        mov     [_Transfer_Buf], dx
        mov     [_Transfer_Buf_Seg], ax

        ; When debugging this program, look at the LDT for the
        ;  selector in dx here.  It should be labeled as 16-bit
        ;  data, the starting address should = ax << 4, and its
        ;  length should be DOS_BUFFER_LEN-1.

        ; Now that the memory has been allocated in DOS space,
        ;  copy the string into it.  Note that while a loop is
        ;  used here, for larger amounts data (or data of fixed
        ;  size), it would probably make more sense to use a
        ;  string instruction such as rep movsd here.
        push    es
        mov     es, dx          ; Put the selector into es.
        xor     ebx, ebx        ; Offset into string
.loop:
        mov     al, [str+ebx]   ; Copy from string
        mov     [es:ebx], al    ; Into DOS memory area, starting at offset 0
        cmp     al, '$'         ; Stop at the '$' marker.
        jne     .loop
        pop     es

        ; Set up the input registers for the DOS interrupt in the
        ;  DPMI_Regs structure above.
        mov     word [DPMI_EAX], 0900h  ; Neat trick: even though DPMI_EAX
                                        ;  is a dword, thanks to little
                                        ;  endian storing just a word works.
                                        ; Note that AH=09h, not AL.
                                        ; mov byte [DPMI_EAX+1], 09h would
                                        ;  have worked here too.  Why? :)
        mov     word [DPMI_EDX], 0      ; Again, only care about the low 16
                                        ;  bits (DX).  Set to 0 because that's
                                        ;  where the string was copied to!
        mov     ax, [_Transfer_Buf_Seg] ; Put the real mode segment into
        mov     word [DPMI_DS], ax      ;  the DS variable the DOS interrupt
                                        ;  sees.

        ; Now simulate the interrupt using DPMI function 0300h.
        mov     ax, 0300h       ; DPMI function 0300h
        mov     bl, 21h         ; Real mode interrupt number (DOS interrupt)
        mov     bh, 0           ; No need to set any flags
        mov     cx, 0           ; Don't copy any stack, not necessary
        mov     dx, ds          ; Point ES:EDI at DPMI_Regs
        mov     es, dx          ;  ES=our DS
        mov     edi, DPMI_Regs  ;  EDI=offset to DPMI_Regs
        int     31h             ; Call DPMI, which calls DOS, which prints
                                ;  the string!

        ; Free the DOS memory block before exiting.
        mov     ax, 0101h
        mov     dx, [_Transfer_Buf]     ; It needs the selector
        int     31h

        ; We're done, exit back to DOS.
        ret

Assemble and run the above code. Run it under cv32, and look at the LDT entry of the selector returned by DPMI function 0100h. Play with the code some (until it breaks, and then figure out what's wrong with it)! We'll come back to this code later (in Section 18.4), and rewrite it so it uses PModeLib functions.