| January 2003 Laboratory Notes: Computer Engineering II | ||
|---|---|---|
| Prev | Chapter 17 Differences Between Real Mode and Protected Mode | Next |
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!
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.
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.
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:
Switch to real-mode.
Perform a DOS interrupt call to allocate the memory the DOS is in charge of (below 1 MB).
Calculate the physical address of the memory DOS allocated (by shifting the segment left by 4 and adding the offset).
Allocate an LDT descriptor, and set its base address to the calculated physical address and the length to the size of the allocated memory.
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.
Now that the basics have been covered on how to call interrupts from protected mode, it's time to look at some example code!
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.
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.