The A20 gate and the HMA

When version 5 of DOS appeared, along with it came the ability to load DOS "high" into the High Memory Area (HMA). This is accomplished with the ubiquitous DOS=HIGH seen in CONFIG.SYS files everywhere.

The HMA is a special memory area enabled by fiddling with the A20 circuitry on a motherboard. The original 8088 based PCís had an addressable range of memory up to 1M. At the 1M point addresses "wrapped" around to zero. The 80286 chip changed the behavior the 8088/8086 processors had at the 1M point. The native operation of a 286 chip in "real" mode is to not wrap around at the 1M boundary.

The designers of the original AT saw this difference in behavior as a compatibility problem and decided to fix it with some external logic on the ATís motherboard. This circuit is commonly referred to as the "A20 gate". The state of the A20 gate determines how the memory decoding circuitry on the motherboard will treat addresses that would have normally resulted in a wrap around on an 8088. If the A20 gate is off, then the machine behaves like an 8088 did and addresses wrap around at the 1M point. If the A20 gate is on, addresses donít wrap around. Just under 64K of memory above the 1M point becomes addressable in real mode when A20 is on.

The HIMEM.SYS driver can be used to manage "extended" memory in 286 or better machines. HIMEM also manages this HMA area that becomes available when the A20 gate is on. The API that HIMEM implements to manage the HMA isnít very sophisticated though. HIMEMís allocation of space in the HMA is an all or nothing affair. When DOS=HIGH, the HMA is given exclusively to DOS and nothing else can allocate the HMA.

However, when DOS is loaded in the HMA it is very rare that all of the HMA is actually occupied by DOS. On most typical configurations weíll see somewhere between 9K and 18K of memory in the HMA being unused. In terms of DOS device drivers and TSRís, this unused memory can be used to reduce the amount of memory that is going to be consumed in the precious low 640K area and the Upper Memory Block (UMB) area that a 386 memory manager will use to "load high" drivers and TSRís.

Even though the HMA may have been committed to DOS when DOS=HIGH, this doesnít mean we canít carve out bits of whatís left over for our own use. In fact there are two quasi-documented APIís that can help us do exactly that. Letís see how we can query and allocate from the "slack" space that typically gets wasted in the HMA when DOS=HIGH.

A program to determine free HMA space

Hereís a little program that demonstrates how to find out how much free HMA space there is when DOS is loaded high.

FREEHMA example
;----------------------------------------------------
; FREEHMA.ASM
;
; Report on free space available in the HMA.
;
; This little utility reports on the amount of
; HMA space left over.  With typical configurations
; this space will be in the 9-19K range and is
; available for allocation by TSR's and device
; drivers via the quasi-doc'd Int 2F fn 4A02
; API.
;
; This utility reports on the free HMA available
; by using the quasi-doc'd Int 2F fn 4A01 that
; reports on this memory.
;----------------------------------------------------
FreeHMA segment para public 'code'
        assume  cs:FreeHMA,ds:FreeHMA,ss:FreeHMA
        org     100h
start   label near
        jmp     init

;----------------------------------------------------
;                     Messages
;----------------------------------------------------
NoRoomMsg db    "No room in HMA (or DOS=LOW)",13,10,'$'

A20on   db      "The A20 line is ON",13,10,'$'
A20off  db      "The A20 line is OFF",13,10,'$'

HMAvail db      "[XXXXh] bytes available in HMA",13,10
        db      "HMA pointer--> "
HMAseg  label   byte
        db      "XXXX:"
HMAoff  label   byte
        db      "XXXX ",13,10,'$'

XMSentry dd     ?

;----------------------------------------------------
; Convert 16 bit parm in DX to 4 hex digits at [bx].
;----------------------------------------------------
Hex     proc    near
        mov     al,dh   ; Call to convert high byte
        call    B2A
        mov     al,dl   ; Fall through for low byte
B2A:    db      0d4h,10h; AAM trick splits nibbles in AL
        xchg    ah,al
        call    N2A
        xchg    ah,al
N2A:    add     al,90h
        daa
        adc     al,40h
        daa
        mov     [bx],al
        inc     bx
        ret             ; Quadruple duty RET
Hex     endp

;----------------------------------------------------
;                   Main program
;----------------------------------------------------
init    label near
        ;--------------------------------------------
        ; Int 2F function 4A01 will report on the
        ; amount of free space left in the HMA after
        ; DOS has loaded HIGH.
        ;
        ; On return, BX    = Number of free bytes
        ;            ES:DI = Pointer to start of
        ;                    free space.
        ;--------------------------------------------
        xor     bx,bx       ; Work around OS/2 problem
        mov     ax,4A01h
        int     2Fh
        or      bx,bx
        jz      NoHMAavail

        ;--------------------------------------------
        ; Convert free HMA bytes for display
        ;--------------------------------------------
        mov     dx,bx                   ; Insert free
        mov     bx,offset HMAvail+1     ; amount into
        call    Hex                     ; message

        ;--------------------------------------------
        ; Convert HMA pointer for display
        ;--------------------------------------------
        mov     dx,es                   ; Insert ptr
        mov     bx,offset HMAseg        ; segment into
        call    Hex                     ; message

        mov     dx,di                   ; Insert ptr
        mov     bx,offset HMAoff        ; offset into
        call    Hex                     ; message

        mov     dx,offset HMAvail       ; Display the
        jmp     short ShowMessage       ; message

        ;--------------------------------------------
        ; Display message saying the HMA doesn't have
        ; any free space.
        ;--------------------------------------------
NoHMAavail:
        mov     dx, offset NoRoomMsg

        ;--------------------------------------------
        ; Display a message at DS:DX
        ;--------------------------------------------
ShowMessage:
        mov     ah,9
        int     21h

Terminate:
        ;--------------------------------------------
        ; Display the state of the A20 gate if an
        ; XMS driver is present.
        ;--------------------------------------------
        mov     ax,4300h        ; XMS presence check
        int     2Fh             ;
        cmp     al,80h          ;
        jne     NoXMSpresent    ;

        mov     ax,4310h        ; Get driver entry
        int     2Fh             ;
        mov     word ptr XMSentry+0,bx   ;
        mov     word ptr XMSentry+2,es   ;

        mov     ah,0            ; Get XMS version
        call    dword ptr [XMSentry]

        dec     dx              ; HMA exist?
        jnz     NoHMApresent    ; DX=0001 if HMA

        mov     ah,7            ; Query A20 state
        call    dword ptr [XMSentry]
        or      bl,bl           ; Error?
        jnz     XMSerror        ;

        mov     dx,offset A20off;
        dec     ax              ; AX=0001 -> A20 on
        jnz     A20isOff        ;
        mov     dx,offset A20on ;
A20isOff:

        mov     ah,9            ; Show message with
        int     21h             ; state of A20

XMSerror:
NoHMApresent:
NoXMSpresent:
        mov     ax,4C00h        ; Terminate
        int     21h
FreeHMA ends
        end     start

FREEHMA makefile
#---------------------------------------------------------
# MAKEFILE for FREEHMA.COM
#---------------------------------------------------------

#------------------------------
# Borland tools
#------------------------------
# Adjust this path to point to wherever
# your Borland compiler has been installed.
BORLAND=D:\BC31\BIN
#freehma.com : freehma.asm makefile
#        $(BORLAND)\tasm  freehma
#        $(BORLAND)\tlink /t /m freehma

#------------------------------
# Microsoft/IBM tools
#------------------------------
# Adjust this path to point to wherever
# your MS/IBM tools have been installed.
MSIBM=D:\MASM
freehma.com : freehma.asm makefile
        $(MSIBM)\masm  freehma;
        $(MSIBM)\link  /map freehma;
        exe2bin freehma.exe freehma.com
        del freehma.exe

#------------------------------
# Watcom C/C++ tools
#------------------------------
# Adjust this path to point to wherever
# your Watcom compiler has been installed.
WATCOM=D:\WATCOM

#freehma.com : freehma.asm makefile
#        $(WATCOM)\BINB\wasm freehma
#        $(WATCOM)\BIN\wlink @wlink.rsp

clean:
        del freehma.obj
        del freehma.exe
        del freehma.com
        del freehma.lst
        del freehma.map

WLINK.RSP response file for Watcom linker
file freehma.obj
form dos com

Running FREEHMA.COM

When I ran FREEHMA on the machine Iím typing on this was the output:

Sample FREEHMA output
[22F0h] bytes available in HMA
HMA pointer--> FFFF:DD10
The A20 line is ON

22F0h bytes is 8944 bytes. This means thereís 8944 bytes of real mode addressable memory currently being wasted on this machine.

NOTE: Current versions of OS/2 (including Warp) donít support the Int 2F function 4A01 call. Zeroing BX before making the call will cause the call to fail gracefully and indicate that thereís no room in the HMA available.

Allocating free HMA space

Now that weíve seen how to determine if there is any free HMA space available, letís see how to allocate some of it for use.

Int 2F function 4A01 was used to query the free space in the FREEHMA program. Int 2F function 4A02 is used to allocate some of that free space. The BX register is passed containing a count of the number of bytes to allocate. The ES and DI registers will return a pointer to the start of the allocated block of HMA. This set of instructions will allocate a 1K chunk of memory from the HMA if it is available.

Query free HMA API
 mov    ax,4A02h
 mov    bx,1024
 int    2Fh

ES:DI will return a pointer to the chunk, or FFFF:FFFF if not enough space is available or DOS isnít high in the HMA. When this function succeeds, the returned ES value will always be FFFFh by the way. This is an important aspect when accessing the HMA.

skull CAVEAT: Any segment register used to access the HMA must be loaded with FFFFh.

This caveat has some important implications for using the HMA. Even if we stick executable code into the HMA, that code needs to be prepared to run with its CS and DS set to FFFFh. So, the usual technique we would use of accessing variables needs to be changed somewhat.

Code that used to look like this probably wonít work directly anymore:

mov ax, MemVar1

In a tiny or small model program, this kind of instruction will be resolved by the linker as having a DS relative address. When we move code and data into the HMA, the segment registers that access that code and data will always be FFFFh, so thereís a problem here.

skull WARNING: 16 bit offsets resolved by a linker are probably going to be wrong for when that code is loaded in the HMA.

There are two approaches to solving a problem like this one.

The first method may be the one to choose when working with an existing piece of code youíd like to move into the HMA. The second approach may be a better bet when developing a new piece of code.

By "location neutral" I mean a method where instructions that access memory donít have absolute memory offsets wired into them. I.e. memory accesses are relative to an index or base register. Like this:

mov ax,[bx+whatever]

If the code is written that way, the value in BX can be "adjusted" by whoever jumps into the code in the HMA so that BX gets loaded with the offset of the variables located in the block of HMA memory that was allocated.

Normally weíd keep a small low memory stub around in a device driver or TSR that loads into the HMA. This stub would be responsible for intercepting any hooked vectors, and jumping into the code that resides in the HMA. Another responsibility of a stub like this is to ensure that the A20 gate is on enabling the HMA to be accessed before trying to jump into the HMA or access any data residing there.

skull WARNING: You shouldnít trust the state of the A20 gate when accessing the HMA from a device driver or TSR! Do the XMS calls to "local enable" the A20 gate on entry, and "local disable" the A20 gate on exit. This should be done from a low memory stub.

A device driver or TSR has no control over the state of the A20 gate when entered. The code that called it may or may not have enabled the A20 gate. Normally one could presume a device driver would be called with A20 enabled -- since it is probably going to be DOS calling the driver, and DOS will be in the HMA as well. However, this isnít always true!

skull WARNING: There are some programs out there that go behind DOSís back, grope the DOS device driver chain, and then call device drivers directly. You need to be prepared to handle this kind of rude behavior.

Putting it all together

OK, weíve seen how to determine if HMA space is available, and how to allocate some of it. Now letís put all this together and examine a little TSR that hides part of itself in the HMA.

This is a little TSR program that hooks Int 21 and monitors the handle based read and write calls. It maintains counts of the number of reads and writes, and displays a "W" or "R" character in the top left corner of a color screen in text mode when it sees either of these DOS functions. The TSR is adaptive in that it can live in the HMA or load low if thereís no room in the HMA.

This program uses the low memory stub technique described above to handle locally enabling and disabling the A20 gate for HMA access. It also uses the variable pooling technique to get position independent addressability for the low memory stub and HMA based code and data.

Source code for HMAIOMON.ASM
;---------------------------------------------------------
; Int 21 read/write I/O monitor TSR that lives in the HMA
; if there's any free space available there.
;
; This TSR monitors Int 21 functions 3Fh and 40h.  These
; are the DOS handle based read and write calls.
;---------------------------------------------------------
        .186
IOmon   segment para public 'code'
        assume  cs:IOmon
        org     100h
start   label near
        jmp     init
        even

STUBLOADSTART label near
;---------------------------------------------------------
; This is the low memory stub that calls into the HMA for
; the rest of the TSR.  This routine intercepts Int 21
; calls and filters them.  If the TSR is loading in the
; HMA, this routine will be relocated to PSP:128.  If the
; TSR is loading low, this routine gets discarded.
;---------------------------------------------------------
LowMemStub proc far
        pushf           ; Save flags on user stack
        push    bx      ; Save BX on user stack
        call    StubVarsAddressability
        ;-------------------------------------------------
        ; Variables for the low memory stub
        ;-------------------------------------------------
LMSVARSTART     label   byte
SavedSP         dw      ?
SSPLOC          equ     (SavedSP - LMSVARSTART)
SavedSS         dw      ?
SSSLOC          equ     (SavedSS - LMSVARSTART)
                db      256+16 dup (?)
LocalStack      label   word
LSTKLOC         equ     (LocalStack - LMSVARSTART)
                dw      ?

;---------------------------------------------------------
; Local function to call the XMS driver.  This function
; guarantees that the BX register won't be altered across
; the call to the XMS driver.  We're using BX for local
; addressability to the stub's data, so this is important.
;---------------------------------------------------------
CallToXMS       proc    near
                push    bx      ; Save addressability reg
                db      9Ah     ; Far CALL opcode
XMSEntryPoint   dd      ?       ; Entry addr of XMS driver
                pop     bx      ; Restore addressability reg
                ret
CallToXMS       endp

StubVarsAddressability:
        pop     bx      ; BX-->local vars
        ;
        ; Switch to our local stack
        ;
        mov     cs:[bx+SSPLOC],sp
        mov     cs:[bx+SSSLOC],ss
        push    cs
        pop     ss
        lea     sp,[bx+LSTKLOC]

        ;-------------------------------------------------
        ; Save anything we may hit later. PUSHA is
        ; OK here because the stub won't be used
        ; unless there is space in the HMA.  Space
        ; in the HMA implies an 80286 or better CPU.
        ;-------------------------------------------------
        push    ds
        push    es
        pusha

        ;-------------------------------------------------
        ; Do a "local enable" of the A20 gate
        ;-------------------------------------------------
        push    ax              ; Save API # in AH
        mov     ah,5            ; 5=local enable
        call    CallToXMS
        or      ax,ax           ; 0=fail, 1=OK
        pop     ax              ; Restore API # in AH
        jz      XMSerror        ; Just chain on error

        ;-------------------------------------------------
        ; Call the HMA part of this TSR.  The A20 gate
        ; should be on at this point allowing access
        ; to the HMA.  The address of the TSR in the
        ; HMA was patched into this next instruction
        ; during initialization.
        ;
        ; The HMA portion of the TSR only cares about the
        ; value in AH for its purposes.  We've taken care
        ; to see that AX was preserved up to this point.
        ;-------------------------------------------------
        db      9Ah     ; Far CALL instruction opcode
HMAEntryPoint dd ?      ; Address of HMA part of the TSR

        ;-------------------------------------------------
        ; Do a "local disable" of the A20 gate
        ; The AH value doesn't matter this time.
        ; It will be restored by POPA as we exit.
        ; Any XMS error code doesn't matter much
        ; here either.  We can't do anything about
        ; it at this point - we're on the way out.
        ;-------------------------------------------------
        mov     ah,6            ; 6=local disable
        call    CallToXMS

        ;-------------------------------------------------
        ; Restore everything from local stack
        ;-------------------------------------------------
XMSerror:
        popa
        pop     es
        pop     ds

        ;-------------------------------------------------
        ; Switch back to user stack
        ;-------------------------------------------------
        mov     ss,cs:[bx+SSSLOC]
        mov     sp,cs:[bx+SSPLOC]

        ;-------------------------------------------------
        ; Chain the Int 21 forward to DOS
        ;-------------------------------------------------
Chain21 label   near
        pop     bx      ; Get BX back from user stack
        popf            ; Flags back from user stack
        ;-------------------------------------------------
        ; The address of the old Int 21 vector was patched
        ; into this next instruction during initialization.
        ;-------------------------------------------------
        db      0EAh    ; Far JMP to chain Int
OldInt21 dd     ?       ; 4 byte addr for immediate JMP
LowMemStub endp
STUBLOADEND label near

;-------------------------------------------------
; This equate defines the length of the low memory
; stub.  We'll need this value later when we
; relocate the stub up into the PSP.
;-------------------------------------------------
STUBLOADLENGTH equ (STUBLOADEND - STUBLOADSTART)

HMALOADSTART label near
;----------------------------------------------------
; This function gets moved into the HMA for execution
; A count of Int 21 read and write API calls is
; maintained, and the two characters in the upper
; left corner of a color text screen are twiddled
; for read and write requests.
;----------------------------------------------------
HMAproc proc    far
        pushf           ; Save what we
        push    ax      ;  may hit
        push    bx      ;
        push    es      ;
        push    ds      ;

        push    cs      ; Get addressability
        pop     ds      ;  to HMA based data via DS
        call    near ptr GetDataAddressability ;

        ;-------------------------------------------------
        ;
        ; The CALL just above puts the offset of
        ; where the following data items live in
        ; the HMA on the stack.  This offset will
        ; be POP'ed off into the BX register so
        ; that DS:BX points to the data items below.
        ;
        ;-------------------------------------------------
HMADATASTART label byte
ReadCount  dw    0
READCOUNTLOC  equ (ReadCount - HMADATASTART)
WriteCount dw    0
WRITECOUNTLOC equ (WriteCount - HMADATASTART)
B800       dw    0B800h
B800LOC       equ (B800 - HMADATASTART)

GetDataAddressability:
        pop     bx      ; DS:BX-->HMA based data items
        ;-------------------------------------------------
        ; Filter out read/write API's
        ;-------------------------------------------------
        cmp     ah,3Fh  ; Int 21 read API?
        je      DoReadAPI
        cmp     ah,40h  ; Int 21 write API?
        je      DoWriteAPI
        ;
        ; The API isn't for us if we get here.
        ;
ChainForward label near
        pop     ds      ; Restore what we
        pop     es      ;  hit.
        pop     bx      ;
        pop     ax      ;
        popf            ;

        ;-------------------------------------------------
        ; This next instruction will be patched to a far RET
        ; instruction if this function was loaded into the HMA.
        ; The RETF will take it back to the low memory stub.
        ; If loading low, then the stub isn't used and this
        ; code can chain directly to the old Int 21 vector.
        ;-------------------------------------------------
FarRETPatchPoint label byte
        db      0EAh    ; Far JMP to chain Int 21
Old21V  dd      ?       ; 4 byte addr for immediate JMP

DoWriteAPI:
        mov     al,'W'
        inc     word ptr [bx+WRITECOUNTLOC]
RWmerge:
        mov     es,[bx+B800LOC]    ; ES-->Color screen VRAM
        mov     byte ptr es:[3],78 ; Attrib=red/yellow
        mov     byte ptr es:[2],al ; Put "R"/"W" on screen
        jmp     short ChainForward

DoReadAPI:
        mov     al,'R'
        inc     word ptr [bx+READCOUNTLOC]
        jmp     short RWmerge
HMAproc endp

        even
HMALOADEND label near

;-------------------------------------------------
; This equate defines the length of the HMA r2esident
; portion of the TSR. We'll need this value later
; when we move "HMAproc" into the HMA
;-------------------------------------------------
HMALOADSIZE equ (HMALOADEND - HMALOADSTART)

;---------------------------------------------------------
; No HMA is availble to load the driver into.  This means
; we need to load it low.  We'll relocate the "HMAproc" up
; into the PSP to save some space.
;---------------------------------------------------------
LOWMEMRELOCADDRESS = 128 ; On top of the command tail

LoadingLowMsg db  "TSR is loading low!",13,10,'$'
LoadingHMAMsg db  "TSR is loading in the HMA!",13,10,'$'

NoHMAavail label near
        ;--------------------------------------------
        ; Ensure that the patch point has a far JMP
        ; if we're loading low.
        ;--------------------------------------------
        mov     FarRETPatchPoint,0EAh   ; Far JMP opcode

        ;--------------------------------------------
        ; Copy the TSR up into the PSP
        ;--------------------------------------------
        push    cs      ; DS=ES=PSP address
        pop     ds      ;
        push    cs      ;
        pop     es      ;
        mov     si,offset HMALOADSTART
        mov     di,LOWMEMRELOCADDRESS
        mov     cx,HMALOADSIZE
        rep     movsb   ;

        ;--------------------------------------------
        ; Say we're loading low
        ;--------------------------------------------
        mov     dx,offset LoadingLowMsg
        mov     ah,9
        int     21h

        ;--------------------------------------------
        ; Hook Int 21 and aim it at the TSR.
        ;--------------------------------------------
        xor     bx,bx
        mov     ds,bx
        mov     bx,(21h*4)
        mov     word ptr [bx],LOWMEMRELOCADDRESS
        mov     word ptr [bx+2],cs

        ;--------------------------------------------
        ; The TSR is "hot" at this point.  We can go
        ; resident now.
        ;--------------------------------------------
        mov     dx,(LOWMEMRELOCADDRESS + HMALOADSIZE)
        int     27h                     ; Go resident

NearTo_NoHMAavail:
        jmp     short NoHMAavail

;---------------------------------------------------------
;       This is the real start of the program
;---------------------------------------------------------
init    label near
        ;--------------------------------------------
        ; Get old Int 21 vector and patch the address
        ; into the low memory stub and the HMA routine.
        ; This needs to happen if loading in the HMA
        ; or if loading low.
        ;--------------------------------------------
        assume  ds:IOmon
        xor     ax,ax           ; ES-->IVT
        mov     es,ax           ;
        les     bx,es:[21h*4]   ; ES:BX = Int 21 vec
        mov     word ptr OldInt21+0,bx
        mov     word ptr OldInt21+2,es
        mov     word ptr Old21V+0,bx
        mov     word ptr Old21V+2,es
        assume  ds:nothing

        ;--------------------------------------------
        ; DOS 5 or better?  Earlier DOS's won't
        ; be able to load DOS=HIGH, so they won't
        ; have any spare HMA space available we
        ; could slide into.
        ;--------------------------------------------
        mov     ah,30h          ; DOS version check
        int     21h             ;
        cmp     al,5            ; On less than 5.0
        jb      NoHMAavail      ;  load low.

        ;--------------------------------------------
        ; Int 2F function 4A01 will report on the
        ; amount of free space left in the HMA after
        ; DOS has loaded HIGH.
        ;
        ; On return, BX    = Number of free bytes
        ;            ES:DI = Pointer to start of
        ;                    free space.
        ;--------------------------------------------
        ; OS/2 has a problem in that it doesn't
        ; support this API call currently.  It just
        ; returns with BX unchanged.  If we didn't
        ; zero out BX here, we could be tricked into
        ; thinking OS/2 supported the call.
        ;--------------------------------------------
        xor     bx,bx           ; Work around OS/2's
                                ;  not supporting
                                ;   this API call.

        mov     ax,4A01h        ; Is there
        int     2Fh             ;  any HMA
        or      bx,bx           ;   available?
        jz      NoHMAavail      ; No,

        cmp     bx,HMALOADSIZE  ; Is there enough
        jb      NoHMAavail      ;  for our needs?

        ;--------------------------------------------
        ; Allocate what we'll need in the HMA
        ;--------------------------------------------
        mov     ax,4A02h        ; 4A02=allocate HMA
        mov     bx,HMALOADSIZE  ; BX=byte count
        int     2Fh             ; ES:DI-->HMA block
                                ;  on return
        mov     word ptr HMAEntryPoint+0,di
        mov     word ptr HMAEntryPoint+2,es

        ;--------------------------------------------
        ; If HMA is there, we must have an XMS driver
        ; of some sort present.  Get its entry point.
        ;--------------------------------------------
        mov     ax,4310h        ; ES:BX will be XMS
        int     2Fh             ; driver entry point
        mov     word ptr XMSEntryPoint+0,bx
        mov     word ptr XMSEntryPoint+2,es

        ;--------------------------------------------
        ; Do a local A20 enable so we can slide the
        ; TSR's code into the HMA.
        ;--------------------------------------------
        mov     ah,5            ; 5=local enable
        call    dword ptr [XMSEntryPoint]
        dec     ax              ; Error?
        jnz     NearTo_NoHMAavail ; Bail out and load low

        ;--------------------------------------------
        ; We've determined that we are loading in the
        ; HMA for sure now. We need to patch the JMP
        ; in the TSR to a RETF at this point so it
        ; will return to the low memory stub rather
        ; than chain to DOS itself.
        ;--------------------------------------------
        mov     FarRETPatchPoint,0CBh   ; RETF opcode

        ;--------------------------------------------
        ; Slide the HMA portion of the TSR into the
        ; HMA with a REP MOV
        ;--------------------------------------------
        push    cs                      ;
        pop     ds                      ; DS:SI-->here
        mov     si,offset HMALOADSTART  ;
        les     di,HMAEntryPoint        ; ES:DI-->HMA
        mov     cx,HMALOADSIZE          ; CX=bytes
        rep     movsb                   ;

        ;--------------------------------------------
        ; Do a local A20 disable to restore A20 state
        ; to what it was when we were run.
        ;--------------------------------------------
        mov     ah,6            ; 6=local disable
        call    dword ptr [XMSEntryPoint]
        dec     ax              ; Error?
        jnz     NearTo_NoHMAavail ; Bail out and load low

        ;--------------------------------------------
        ; Say we're loading in the HMA
        ;--------------------------------------------
        mov     ah,9
        mov     dx,offset LoadingHMAMsg
        int     21h

        ;--------------------------------------------
        ; Slide the low memory stub up into the PSP
        ; to save some resident size.
        ;--------------------------------------------
        push    cs                      ;
        pop     ds                      ; DS-->PSP
        push    cs                      ;
        pop     es                      ; ES-->PSP
        mov     si,offset STUBLOADSTART ; DS:SI-->stub
        mov     di,LOWMEMRELOCADDRESS   ; ES:DI-->PSP:128
        mov     cx,STUBLOADLENGTH       ; CX=bytes
        rep     movsb                   ;

        ;--------------------------------------------
        ; Aim Int 21 it at our low memory stub routine.
        ; The original vector has already been saved
        ; by the time we get here.
        ;--------------------------------------------
        xor     ax,ax                   ; ES-->IVT
        mov     es,ax                   ;
        cli
        mov     word ptr es:[(21h*4)+0],LOWMEMRELOCADDRESS
        mov     word ptr es:[(21h*4)+2],cs
        sti

        ;--------------------------------------------
        ; We're "hot" right now.  The low memory stub
        ; is hooked, the code is in the HMA. Now we
        ; can go resident, and let the HMA based TSR
        ; code go to work.
        ;--------------------------------------------
        mov     dx, (LOWMEMRELOCADDRESS + STUBLOADLENGTH)
        int     27h                     ; Go resident

IOmon   ends
        end     start
MAKEFILE for HMAIOMON.ASM
#-----------------------------------------------------
# MAKEFILE for HMAIOMON.COM
#-----------------------------------------------------

#------------------------------
# Borland tools
#------------------------------
# Adjust this path to point to wherever
# your Borland compiler has been installed.
BORLAND=D:\BC31\BIN
#hmaiomon.com : hmaiomon.asm makefile
#        $(BORLAND)\tasm  hmaiomon
#        $(BORLAND)\tlink /t /m hmaiomon

#------------------------------
# Microsoft/IBM tools
#------------------------------
# Adjust this path to point to wherever
# your MS/IBM tools have been installed.
MSIBM=D:\MASM
hmaiomon.com : hmaiomon.asm makefile
        $(MSIBM)\masm  hmaiomon;
        $(MSIBM)\link  /map hmaiomon;
        exe2bin hmaiomon.exe hmaiomon.com
        del hmaiomon.exe

#------------------------------
# Watcom C/C++ tools
#------------------------------
# Adjust this path to point to wherever
# your Watcom compiler has been installed.
WATCOM=D:\WATCOM

#freehma.com : hmaiomon.asm makefile
#        $(WATCOM)\BINB\wasm hmaiomon
#        $(WATCOM)\BIN\wlink @wlink.rsp

clean:
        del hmaiomon.obj
        del hmaiomon.exe
        del hmaiomon.com
        del hmaiomon.lst
        del hmaiomon.map
WLINK.RSP response file for Watcom linker
file hmaiomon.obj
form dos com
Take a close look at the code at the start of the "HMAproc" routine in this sample. Thereís a powerful technique being used there. A CALL instruction is being used to generate the offset of where this programís data items live in the HMA. The CALL instruction pushes the address of the next instruction on the stack as its return address. This address conveniently corresponds to the offset in the HMA where the TSRís data lives. By popping this address into a base or index register, we can address those variables using the "variable pooling" technique described in an earlier chapter.

If code living in the HMA addresses its variables using the variable pooling technique, you wonít have to worry where that code is located -- it is totally location independent!

Also notice how the code in the low memory stub is independent of where it lives too. It also uses the same pooling technique that the HMA portion of the TSR uses. Once the addresses of the XMS entry point and the location of the HMA code have been patched in at initialization time, the stub could be moved around anywhere in memory.

Running HMAIOMON

When you run HMAIOMON from the command line, itís going to display a message saying where it loaded. If HMA space was available to load into, then it will do so. If no HMA space is available, then it gracefully degrades and loads itself low.

You should see the "W" character appear in the top left corner of the screen when DOS is sitting at a command prompt. To display its prompt, COMMAND.COM does handle based I/O writes.

One easy way to see the read monitor code get executed is to enter this command:

COPY CON NUL

This will force COMMAND.COM to do some handle based reads from the "CON" device. When this happens, the character in the upper left corner of the screen should change to an "R" character. Hit the enter key a few times, then Ctrl-Z, then enter again. This will terminate the COPY and cause COMMAND.COM to display your prompt again. At this point, the "W" character should appear again in the upper left corner of the screen.

Hiding stuff in the HMA works with 386 memory managers

The techniques that FREEHMA and HMAIOMON used also work fine with 386 memory managers like EMM386, QEMM, and 386Max. However, when a 386 memory manager is active, the physical A20 circuitry wonít be used. The memory managers put the CPU into protected mode and run DOS as a Virtual 8086 (V86) mode task. These memory managers still accomplish the same thing that HIMEM does when it uses the physical A20 gate circuitry though. The 386 memory managers do this by fiddling with the 386ís virtual memory page tables. From a programís point of view, remapping pages has the same effect as altering the state of the physical A20 gate. These memory managers also implement the same APIís that HIMEM does, so this page table manipulation stuff happens transparently behind your back.

HMA Tricks with newer Operating Systems

The methods described in this chapter work well with most newer Operating Systems as well as plain DOS. The DOS box in Microsoftís Windows NT implements the Int 2F calls needed to query and allocate free space in the HMA. Windows 95 also implements these Int 2F calls that showed up in DOS version 5. The one exception here is current versions of OS/2. The OS/2 DOS boxes donít implement the Int 2F calls we need to do these HMA tricks. As described previously, and demonstrated in the code for HMAIOMON, there is a way to avoid the problem with OS/2 by zeroing the BX register prior to the Int 2F function 4A01 call. This makes the call return and appear as if thereís no space available in the HMA even if OS/2ís DOS box code is in fact loaded high in the HMA.

check TIP: Zeroing BX before doing the Int 2F function 4A01 call will allow a program to degrade gracefully when run under OS/2.

The HMA under Windows 3.X and Windows 95

Windows 3.X and Windows 95 have the ability to create multiple virtual DOS sessions (the older Windows 386 product also had this capability). With Windows 3.X this capability exists only in "enhanced" mode. With Windows 95, its there all the time as Windows 95 always runs in the equivalent of Windows 3.Xís "enhanced" mode.

One interesting, and potentially useful, property of the HMA under these versions of Windows is that the HMA is not "instanced" across different DOS sessions.

check TIP: With Windows 3.X and Windows 95, a single copy of the HMA is shared across all open DOS sessions.

This may not sound all that interesting at first glance, but consider the implications of this shared HMA scheme in these versions of Windows. Shared memory between DOS sessions could be used to create a mechanism allowing programs running in two or more DOS sessions to quickly communicate with each other. Of course there are other ways to accomplish this kind of communication between DOS sessions too. There are Windows APIís that allow DOS sessions to access the Windows clipboard. Scribbling data to a common file is another tried and true standby. In a networked environment, you might use named pipes between sessions running in the same machine.

Using the clipboard has a downside though in that it blows away the clipboard contents. Using a common file has downsides in that it burns file handles, pollutes a disk cache, and is relatively slow. Named pipes are also relatively slow to access, and cause a bunch of named pipe code to be loaded into memory when theyíre being accessed.

Here are a couple of example DOS programs that demonstrate how the HMA can be used to communicate between DOS sessions under Windows 3.X or Windows 95. Theyíre called SERVER.COM and DETACH.COM.

SERVER is a background process ďserverĒ. When itís running it waits for DETACH to send it a command line to execute. The command line is passed via the common HMA. DETACH places a command line in the HMA for SERVER to execute. There can be multiple copies of SERVER running in separate DOS sessions too. DETACH can be run from any DOS session. A session running SERVER could also execute a DETACH if thereís another SERVER running that would pick up and execute the command.

SERVER and DETACH were written using the Borland C/C++ compiler. Doing the kinds of low level machine dependent groveling needed to implement programs like these was particularly easy with the Borland tools. Thereís nothing here that couldnít be translated to any of the current crop of C/C++ compilers fairly easily though.

Source code for SERVER.C

Source code for SERVER.C
/*
  Background detached command server for Win3x and Windows 95
*/
#include <stdio.h>
#include <conio.h>
#include <process.h>
#include <string.h>
#include <signal.h>
#include <dir.h>
#include <dos.h>
#include <io.h>

void _setupio(void){}
void _IOERROR(void){}

int (far *XMS)(void);

char far *pHMA;
char far *pHMAcommand;
unsigned short FreeHMA;
char CommandString[256];

char Signature[] = "HMA DETACHED PROCESS SERVER";

char A20state = 0;

int fstrncmp(char far *s1, char far *s2, size_t len)
   {
   while (len)
      {
      if (*s1++ != *s2++)
         return -1;
      len--;
      }
   return 0;
   }

int my_puts(char *s)
   {
   char localstr[200];

   strcpy(localstr,s);
   strcat(localstr,"\r\n$");

   _DX = &localstr[0];
   _AH = 9;
   geninterrupt(0x21);
   return 0;
   }
#define puts my_puts

int LocalEnableA20(void)
   {
   _AH = 5;
   XMS();
   if (_AX == 0)
      {
      A20state = 0;
      return -1;
      }
   A20state = 1;
   return 0;
   }

void LocalDisableA20(void)
   {
   _AH = 6;
   XMS();
   A20state = 0;
   }

int main(void)
   {
   unsigned i;
   char     BlockPresent=1;

   /*
     Ignore Ctrl-C
     This keeps us from exiting with the A20 state
     being something other than it was when we were
     started.
   */
   signal(SIGINT, SIG_IGN);
   signal(SIGTERM, SIG_IGN);

   /*
     Is Windows in "enhanced" mode?
   */
   _AX = 0x1600;
   geninterrupt(0x2F);
   if ((_AL != 0x03) && (_AL != 0x04))
      {
      puts("Windows in enhanced mode not detected");
      exit(-1);
      }

   /*
     XMS present?
   */
   _AX = 0x4300;
   geninterrupt(0x2F);
   if (_AL != 0x80)
      {
      puts("No XMS driver present!");
      exit(-1);
      }

   /*
     Get driver entry address
   */
   _AX = 0x4310;
   geninterrupt(0x2F);
   XMS = MK_FP(_ES,_BX);

   /*
     DOS in HMA?
   */
   _BX = 0;
   _AX = 0x4A01;
   geninterrupt(0x2F);
   FreeHMA = _BX;
   pHMA = MK_FP(_ES,_DI);
   if ((FreeHMA == 0) || (FreeHMA < 256))
      {
      puts("No space available in HMA, or DOS=LOW");
      exit(-1);
      }

   /*
     Begin Windows critical section
   */
   _AX = 0x1681;
   geninterrupt(0x2F);

   /*
     Is a block already present somewhere in HMA?
   */
   for (;;)
      {
      if (fstrncmp(pHMA, Signature, (sizeof(Signature)-1)) == 0)
         break;

      pHMA--;

      if ((unsigned)FP_OFF(pHMA) < 32767U)
         {
         pHMA = 0L;
         BlockPresent=0;
         break;
         }
      }

   /*
     Allocate 256 bytes
   */
   if (!BlockPresent)
      {
      _BX = 256;
      _AX = 0x4A02;
      geninterrupt(0x2F);
      pHMA = MK_FP(_ES,_DI);
      if ((FP_SEG(pHMA) == 0xFFFF) && (FP_OFF(pHMA) == 0xFFFF))
         {
         /*
           End critical section
         */
         _AX = 0x1682;
         geninterrupt(0x2F);
         puts("No space available in HMA, or DOS=LOW");
         exit(-1);
         }
      }

   /*
     Local enable A20
   */
   if (LocalEnableA20())
      {
      /*
        End critical section
      */
      _AX = 0x1682;
      geninterrupt(0x2F);
      puts("Can't local enable A20");
      exit(-1);
      }

   /*
     If we're the first SERVER, then place a signature
     in the block and zero out the communication buffer.
   */
   if (!BlockPresent)
      {
      /*
        Zero out the block
      */
      for (i=0; i < 256; i++)
         *(pHMA+i) = 0;

      /*
        Place our signature at the front.
      */
      for (i=0; i < (sizeof(Signature)-1); i++)
         *(pHMA+i) = Signature[i];
      }

   /*
     End Windows critical section
   */
   _AX = 0x1682;
   geninterrupt(0x2F);

   pHMAcommand = pHMA+(sizeof(Signature)-1);

   do
      {
      static count = 0;
      char cmd;

      /*
        Begin critical section
      */
      _AX = 0x1681;
      geninterrupt(0x2F);

      /*
        Is a command in the HMA buffer?
      */
      cmd = *pHMAcommand;
      if (cmd)
         {
         /*
           Collect the characters for the command line.
         */
         for (i=0; i < 256; i++)
            {
            CommandString[i] = pHMAcommand[i];
            if (CommandString[i] == '\0')
               break;
            }

         /*
           Put a zero at the beginning of the command
           string to indicate the command has been
           retrieved.
         */
         *pHMAcommand = '\0';

         /*
           End critical section
         */
         _AX = 0x1682;
         geninterrupt(0x2F);

         /*
           Execute the command.
         */
         LocalDisableA20();
         system(CommandString);
         LocalEnableA20();

         count = 30;
         }
      else
         {
         /*
           End critical section
         */
         _AX = 0x1682;
         geninterrupt(0x2F);
         }

      if ((count++ % 30) == 0)
         {
         char DirBuf[150];

         DirBuf[0] = '[';
         getcwd(DirBuf+1,70);
         strcat(DirBuf,"] Waiting for command...");
         puts(DirBuf);
         }

      /*
        Release the rest of this VM's time slice.
      */
      _AX = 0x1680;
      geninterrupt(0x2F);
      }
   while (!kbhit());

   /*
     Local disable A20
   */
   LocalDisableA20();

   return 0;
   }

Source code for DETACH.C
/*
  DETACH - command sends jobs to background servers for Win3x and Windows 95
*/
#include <stdio.h>
#include <conio.h>
#include <process.h>
#include <signal.h>
#include <string.h>
#include <dos.h>
#include <io.h>

void _setupio(void){ }
void _IOERROR(void){ }

int (far *XMS)(void);

char far *pHMA;
volatile char far *pHMAcommand;
unsigned short FreeHMA;
char CommandString[256] = {0};

char Signature[] = "HMA DETACHED PROCESS SERVER";

void fstrcpy(char far *dst, char far *src)
   {
   while (*src)
      *dst++ = *src++;
   *dst = *src;
   }

int fstrncmp(char far *s1, char far *s2, size_t len)
   {
   while (len)
      {
      if (*s1++ != *s2++)
         return -1;
      len--;
      }
   return 0;
   }

int my_puts(char *s)
   {
   char localstr[200];

   strcpy(localstr,s);
   strcat(localstr,"\r\n$");

   _DX = &localstr[0];
   _AH = 9;
   geninterrupt(0x21);
   return 0;
   }
#define puts my_puts

int main(int argc, char *argv[])
   {
   unsigned i;

   /*
     Ignore Ctrl-C
     This keeps us from exiting with the A20 state
     being something other than it was when we were
     started.
   */
   signal(SIGINT, SIG_IGN);
   signal(SIGTERM, SIG_IGN);

   if (argc <= 1)
      {
      puts("No parameters present!");
      exit(-1);
      }

   /*
     Construct command line to pass
   */
   for (i=1; i < argc; i++)
      {
      strcat(CommandString, argv[i]);
      if ((i+1) != argc)
         strcat(CommandString, " ");
      }

   {
   char s[200];
   strcpy(s,"Will attempt to send [");
   strcat(s,CommandString);
   strcat(s,"] to a background server");
   puts(s);
   }

   /*
     Is Windows in "enhanced" mode?
   */
   _AX = 0x1600;
   geninterrupt(0x2F);
   if ((_AL != 0x03) && (_AL != 0x04))
      {
      puts("Windows in enhanced mode not detected");
      exit(-1);
      }

   /*
     XMS present?
   */
   _AX = 0x4300;
   geninterrupt(0x2F);
   if (_AL != 0x80)
      {
      puts("No XMS driver present!");
      exit(-1);
      }

   /*
     Get driver entry address
   */
   _AX = 0x4310;
   geninterrupt(0x2F);
   XMS = MK_FP(_ES,_BX);

   /*
     DOS in HMA?
   */
   _BX = 0;
   _AX = 0x4A01;
   geninterrupt(0x2F);
   FreeHMA = _BX;
   pHMA = MK_FP(_ES,_DI);
   if ((FreeHMA == 0) || (FreeHMA < 256))
      {
      puts("No space available in HMA, or DOS=LOW");
      exit(-1);
      }

   /*
     Local enable HMA
   */
   _AH = 5;
   XMS();
   if (_AX == 0)
      {
      puts("Can't local enable A20");
      exit(-1);
      }

   /*
     Locate the HMA command server's HMA buffer
   */
   for (;;)
      {
      if (fstrncmp(pHMA, Signature, (sizeof(Signature)-1)) == 0)
         break;

      pHMA--;

      if ((unsigned)FP_OFF(pHMA) < 32767U)
         {
         puts("Can't seem to locate background server!");

         /*
           Local disable HMA
         */
         _AH = 6;
         XMS();

         exit(-1);
         }
      }

   pHMAcommand = pHMA+(sizeof(Signature)-1);

   while (*pHMAcommand)
      {
      static int count = 0;

      if ((count++ % 200) == 0)
         puts("Waiting for server...");

      /*
        Release the rest of this VM's time slice.
      */
      _AX = 0x1680;
      geninterrupt(0x2F);
      }

   /*
     Begin critical section
   */
   _AX = 0x1681;
   geninterrupt(0x2F);

   /*
     Copy the command string to the HMA.
   */
   fstrcpy((char far *)pHMAcommand, CommandString);


   /*
     End critical section
   */
   _AX = 0x1682;
   geninterrupt(0x2F);

   /*
     Local disable HMA
   */
   _AH = 6;
   XMS();

   puts("Command sent!");

   return 0;
   }

MAKEFILE for SERVER.COM and DETACH.COM
all: server.com detach.com

server.com: server.c makefile
        bcc -M -mt -lt server.c

detach.com: detach.c makefile
        bcc -M -mt -lt detach.c

The versions of SERVER and DETACH included on the enclosed diskette were built using the Borland version 4.52 C/C++ compiler. In the MAKEFILE, -M tells the Borland linker to create a map file. -mt specifies a "tiny" model build. -lt tells the linker to produce a COM file. The code for SERVER and DETACH will work fine even with the old Borland Turbo C 2.0 compiler by the way. All you need to do is change "bcc" in the MAKEFILE to "tcc".

How SERVER and DETACH operate

There will be a "signature" string placed in the HMA by SERVER when it starts up. A subsequent DETACH scans backwards through the HMA looking for a signature string placed there by a SERVER process. Right after the signature in the HMA is where the communications buffer between a SERVER and a DETACH lives.

Since "tiny" model programs work with 16 bit near pointers, thereís a "far" version of a string compare routine in each program. This is so the programs can do a string compare to find the signature in the HMA.

Thereís also a stripped down version of puts() that tries to avoid causing a lot of C runtime library buffered I/O stuff to be dragged in. When SERVER and DETACH start up they both do a bunch of tests to see if theyíre running in an environment where they can work -- is an HMA present? is Windows running? etc. SERVER also checks to see if itís the first instance of a SERVER to be run in the machine. If it was, it will allocate the HMA memory block to communicate through, place its signature string at the start of the block, and then zero out the block. If it wasnít the first SERVER to be run, then all it does is locate the block allocated by a prior version of SERVER. Once a copy of SERVER has a pointer to the HMA communications area, that SERVER goes into a loop looking for commands to execute. Inside the loop a few special Windows specific things need to happen. On each iteration of the loop, the SERVER makes a call to Windows to release its time slice. This is so "idle" SERVER processes donít impact the responsivness of the machine too much. When scanning for a command to execute a SERVER will also temporarily shutdown Windows multitasking (i.e. enter a "critical section") so that it can look at the HMA communications buffer in an uninterrupted way. This prevents a SERVER from trying to execute an incomplete command placed there by a DETACH that got timesliced out while placing a command in the buffer. The DETACH program also does one of these "critical sections" when it places a command in the buffer. The two critical sections guarantee that no incomplete commands will be placed into, or retrieved from the communications buffer.

When a SERVER notices a command it can execute, it copies it from the HMA to some local storage and then marks the command buffer in the HMA as "empty" by placing a zero byte at the first position of the command buffer. The local copy allows that SERVER to run the command while another DETACH can be placing a different command in the buffer.

A DETACH will "block" and keep trying to place a command in the HMA buffer if no SERVER has been along to pick up the current command in the HMA buffer. The more SERVERs you have running, the more commands you can have executing in the background. If all the running SERVERís a busy, there can be one command "pending" in the HMA before a DETACH will ďblockĒ and start waiting.

Running SERVER and DETACH

One thing to double check is that the DOS sessions a SERVER is to be run in have the Windows "background" option enabled. Without this, they wonít be able to run to pick up commands from the HMA buffer. This isnít critical for a session running a DETACH though. Normally a session running a DETACH will be in the forground and executing when you enter the DETACH command. You can use the Windows PIF file editor to create a PIF file that allows a DOS session to run in the background. This can also be set manually in a DOS sessionsís session "settings".

The easiest way to get a couple of SERVERís going is to place some icons for them into the "Startup" group or folder. Then theyíll be launched whenever you startup Windows or Windows 95.

Once at least one SERVER session is running in the machine, DETACH will be able to send commands via the HMA to execute in the background. To kill a running SERVER program just make it forground and hit a key. At the bottom of the SERVERís processing loop is a call to kbhit() that will terminate that SERVER when a key is struck.

If you create a PIF file for your SERVERs, one convenient thing to do is enable the option to close the session when SERVER exits. Doing this will cause the session to evaporate when you hit a key to kill a running SERVER. This is more convenient than having to type EXIT to close the session.

A VDISK device driver that lives in the HMA

HMAVDISK Makefile HMAVDISK Source HMAVDISK Operation
Now we'll turn our attention to exploiting free space in the HMA to loading device drivers. Even with newer OS's, like Windows 95, this remains a valuable technique. Free space in the HMA is generally a lot scarcer under Windows 95 than it was under plain DOS/Windows, but there is still enough there to be useful.

A device driver that's going to hide most of itself in the HMA still needs to keep a small low memory stub present as did the previous TSR example. In this VDISK example the low memory stub takes just over 300 bytes of low or UMB memory when the driver loads. As a comparison, the RAMDRIVE.SYS that came with Windows for Workgroups takes just over 1K of low memory when it loads. The RAMDRIVE.SYS included in IBM's PC DOS 7, while better than the Microsoft version, still takes over 400 bytes of low memory when it loads.

# ------------------------------------------
# MAKEFILE for HMAVDISK.SYS
# ------------------------------------------

MSIBM=D:\MASM
hmavdisk.sys : hmavdisk.asm makefile
        $(MSIBM)\masm  hmavdisk,hmavdisk,hmavdisk;
# Uncomment the following line to use Borland's TASM
#        tasm  hmavdisk
# Uncomment the following line to use Watcom's WASM
#        wasm  hmavdisk
        $(MSIBM)\link  /map hmavdisk;
        exe2bin hmavdisk.exe hmavdisk.sys
        del hmavdisk.exe
        del hmavdisk.com
        del hmavdisk.obj

clean:
        del hmavdisk.obj
        del hmavdisk.exe
        del hmavdisk.com
        del hmavdisk.lst
        del hmavdisk.map

;-----------------------------------------------------------
; HMAVDISK.ASM - A device driver that lives in the HMA
;
; HMAVDISK is an example of the ubiquitous VDISK sample
; driver that lives in the HMA.  It keeps a low stub that
; deals with making sure the A20 gate is on before jumping
; into the HMA where the bulk of the driver code lives at
; runtime.  The low stub is also responsible for switching
; to a local stack because stack hungry XMS functions will
; be called.
;-----------------------------------------------------------
        .186

QUERY_FREE_HMA        = 4A01h
ALLOC_PART_OF_HMA     = 4A02h

XMS_GET_ENTRY_POINT   = 4310h
XMS_LOCAL_ENABLE_A20  = 05h
XMS_LOCAL_DISABLE_A20 = 06h
XMS_QUERY             = 08h
XMS_ALLOC             = 09h
XMS_MOVE              = 0Bh

;-----------------------------------------------------------
;             Currently setup as ~100K drive.
;-----------------------------------------------------------
SECTORSIZE      = 512
CLUSTERSIZE     = 1
RESERVEDSECTORS = 1
NUMSECTORS      = 10
FATSECTORS      = 1     ; Must be 1 currently...
NUMTRACKS       = 20
NUMFATS         = 1
NUMHEADS        = 1
VDISKSIZE       = (NUMSECTORS*NUMTRACKS*NUMHEADS)

BOOTSECTOR      = 0
FAT1SECTOR      = SECTORSIZE
FAT2SECTOR      = (SECTORSIZE+SECTORSIZE)

MEDIADESCRIPTOR = 0FEh

;-----------------------------------------------------------
;         Basic header common to all request types
;-----------------------------------------------------------
rh      struc
len     db      ?
unit    db      ?
command db      ?
status  dw      ?
reserve db      8 dup (?)
rh      ends

;-----------------------------------------------------------
;                Init header (command 0)
;-----------------------------------------------------------
rh_init struc
         db    size rh dup (?)
nunits0  db    ?       ; # of units
chop0    dd    ?       ; DD chop address
BPBaddr0 dd    ?       ; Pointer to BPB
drvltr0  db    ?       ; First available drive
msgflg0  dw    ?       ; Message flag
rh_init ends

;-----------------------------------------------------------
;                Media check (command 1)
;-----------------------------------------------------------
rh_media_check struc
           db   size rh dup (?)
media1     db   ?       ; Media byte from BPB
mediastat1 db   ?       ; Media status
rh_media_check ends

;-----------------------------------------------------------
;                Get BPB check (command 2)
;-----------------------------------------------------------
rh_get_BPB struc
          db   size rh dup (?)
media2    db   ?       ; Media byte from BPB
DataAddr2 dd   ?       ; Pointer to transfer addr
BPBaddr2  dd   ?       ; Pointer to BPB
rh_get_BPB ends

;-----------------------------------------------------------
;                Input (command 4)
;-----------------------------------------------------------
rh_read struc
          db   size rh dup (?)
media4    db   ?       ; Media byte from BPB
DataAddr4 dd   ?       ; Pointer to transfer addr
Scount4   dw   ?       ; Sector count
StrtSec4  dw   ?       ; Start sector #
rh_read ends

;-----------------------------------------------------------
;                Output (command 8)
;-----------------------------------------------------------
rh_write struc
          db   size rh dup (?)
media8    db   ?       ; Media byte from BPB
DataAddr8 dd   ?       ; Pointer to transfer addr
Scount8   dw   ?       ; Sector count
StrtSec8  dw   ?       ; Start sector #
rh_write ends


;***********************************************************
;*                  Code and data start here               *
;***********************************************************
Vdisk   segment para public 'code'
        assume  cs:Vdisk
        org     0
;//////////////////////////////////////////////////////
;///                DEVICE HEADER                   ///
;//////////////////////////////////////////////////////
nxtdev  dd      -1              ; Link to next device
        dw      2000H           ; DevAttr=Block device
        dw      offset stubstrategy ;
devintr dw      offset initinterrupt; &tl;---Gets patched!
devname db      1               ; 1 block device here
        db      7 dup (0)       ; 7 pad bytes

StubXMSEntry  dd    ?   ; XMS driver entry point
HMAintrAddr   dd    ?   ; Addr of HMA interrupt routine
HMALoadPoint  dd    ?   ; Addr of HMA load point
OldStack      dd    ?   ; Save area for caller's SS:SP
StubRequest   dd    ?   ; Copy of request header for stub

StubBPB label byte
        dw    SECTORSIZE
        db    CLUSTERSIZE
        dw    RESERVEDSECTORS
        db    NUMFATS
        dw    (SECTORSIZE/32)
        dw    VDISKSIZE
        db    MEDIADESCRIPTOR
        dw    FATSECTORS

        even
        db    128 dup (?)
LocalStack    label word

;////////////////////////////////////////////////////
; All the stub strategy routine needs to do is save
; the request packet address.  This will be passed
; to the HMA interrupt routine.
;////////////////////////////////////////////////////
stubstrategy proc far
        assume  ds:nothing
        mov     word ptr cs:StubRequest+0,bx
        mov     word ptr cs:StubRequest+2,es
        ret
stubstrategy endp

;////////////////////////////////////////////////////
; This is the low stub interrupt routine.  It
; switches to a local stack, ensures A20 is on,
; then calls into the HMA to the HMA based interrupt
; routine that does all the real work.
;////////////////////////////////////////////////////
stubinterrupt proc far
        assume  ds:nothing
        ;--------------------------------------------
        ; Save old SS:SP
        ;--------------------------------------------
        mov     word ptr cs:OldStack+0,sp
        mov     word ptr cs:OldStack+2,ss

        ;--------------------------------------------
        ; Switch to local stack
        ;--------------------------------------------
        push    cs
        pop     ss
        mov     sp,offset LocalStack

        pusha                   ; Save regs that
        push    ds              ;  might get hit on
        push    es              ;   local stack

        ;--------------------------------------------
        ; Local enable A20
        ;--------------------------------------------
        mov     ah,XMS_LOCAL_ENABLE_A20
        call    dword ptr cs:[StubXMSEntry]

        ;--------------------------------------------
        ; Call into HMA interrupt routine with ES:SI
        ; containing pointer to the request header.
        ;--------------------------------------------
        les     si,cs:StubRequest
        call    dword ptr cs:[HMAintrAddr]

        ;--------------------------------------------
        ; Local disable A20
        ;--------------------------------------------
        mov     ah,XMS_LOCAL_DISABLE_A20
        call    dword ptr cs:[StubXMSEntry]

        ;--------------------------------------------
        ; Restore everything from the local stack
        ;--------------------------------------------
        pop     es
        pop     ds
        popa

        ;--------------------------------------------
        ; Switch back to caller's stack
        ;--------------------------------------------
        mov     ss,word ptr OldStack+2
        mov     sp,word ptr OldStack+0

        ret
stubinterrupt endp
        even
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
;!!    The low memory stub gets chopped off here    !!
;!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
LOWSTUBEND label near
        public  LOWSTUBEND

;//////////////////////////////////////////////////////
;///            HMA load image starts here          ///
;//////////////////////////////////////////////////////
HMALOADSTART label near
        public  HMALOADSTART

;------------------------------------------------------
; This procedure generates addressability to the
; driver's local data in the HMA.  DS:BX has offset
; of the first byte of HMA based data on return.
;------------------------------------------------------
DataAddressability proc near
        assume  ds:nothing
        call    JumpAroundData

HMADataPool label byte

req_ptr     label dword
HMAreq_ptr_offset equ ($-HMADataPool)
rh_offset   dw    ?
rh_seg      dw    ?

XMSEntryPointOffset = ($-HMADataPool)
XMSEntryPoint dd  ?

XMShandleOffset = ($-HMADataPool)
XMShandle   dw    ?

LowCSoffset = ($-HMADataPool)
LowCS       dw    ?

;------------------------------------------------------
;                XMS move structure
;------------------------------------------------------
XMSmoveStructOffset = ($-HMADataPool)
XMSmoveStruct label   byte
XbytesOffset = ($-HMADataPool)
Xbytes  dd      0               ; Byte count
XsrcHoffset = ($-HMADataPool)
XsrcH   dw      0               ; Source handle
XsrcOoffset = ($-HMADataPool)
XsrcO   dd      0               ; Source offset
XdstHoffset = ($-HMADataPool)
XdstH   dw      0               ; Destination handle
XdstOoffset = ($-HMADataPool)
XdstO   dd      0               ; Destination offset

;------------------------------------------------------
;          Boot sector/BPB for the drive
;------------------------------------------------------
        even
BootRecord label byte
        jmp     short BootEntry
        nop
        db      "INGENOSO"  ; VendorID
BPBVDISKoffset = ($-HMADataPool)
BPBVDISK label  byte
secsize  dw     SECTORSIZE
clusize  db     CLUSTERSIZE
ressec   dw     RESERVEDSECTORS
nfat     db     NUMFATS
dirsize  dw     (SECTORSIZE/32)
totsec   dw     VDISKSIZE
mediabyt db     MEDIADESCRIPTOR
fatsec   dw     FATSECTORS
BPBLEN = ($-BPBVDISK)
spt      dw     NUMSECTORS ; sectors per track
nheads   dw     NUMHEADS   ; Single sided
         db     11 dup (0)
BootEntry label near
        sti
        jmp     $
BOOTRECLEN = ($-BootRecord)

BPBpointer dw   offset BPBVDISK

JumpAroundData label near
        pop     bx      ; BX=offset of local data
        push    cs      ;
        pop     ds      ; DS:BX-->local data pool
        ret
DataAddressability endp


;////////////////////////////////////////////////////
;///       Routines that implement the VDISK      ///
;////////////////////////////////////////////////////

;----------------------------------------------------
;           Media check - command #1
;----------------------------------------------------
MediaCheck proc near
        assume  ds:nothing
        mov     byte ptr es:[si+media1],MEDIADESCRIPTOR
        mov     byte ptr es:[si+mediastat1],1 ; No change
MergeRetOK:
        mov     ax,0100h
        jmp     near ptr intrexit
MediaCheck endp

;----------------------------------------------------
;             Build BPB - command #2
;----------------------------------------------------
BuildBPB proc near
        assume  ds:nothing
        ;--------------------------------------------
        ; Return pointer to copy of BPB (in low stub)
        ;--------------------------------------------
        mov     ax,[bx+LowCSoffset]
        mov     word ptr es:[si+BPBaddr2+0],offset StubBPB
        mov     word ptr es:[si+BPBaddr2+2],ax
ToMergeRetOK:
        jmp     MergeRetOK
BuildBPB endp

;----------------------------------------------------
;                  Read - command #4
;----------------------------------------------------
Read proc near
        assume  ds:nothing
        ;--------------------------------------------
        ; Set destination address in XMS move structure
        ;--------------------------------------------
        push    es
        les     ax,dword ptr es:[si+DataAddr4]
        mov     [bx+XdstOoffset+0],ax
        mov     [bx+XdstOoffset+2],es
        pop     es

        ;--------------------------------------------
        ; Set source address in XMS move structure
        ;--------------------------------------------
        mov     ax,SECTORSIZE
        mul     word ptr es:[si+StrtSec4]
        mov     [bx+XsrcOoffset+0],ax
        mov     [bx+XsrcOoffset+2],dx

        ;--------------------------------------------
        ; Set byte count in XMS move structure
        ;--------------------------------------------
        mov     ax,SECTORSIZE
        mul     word ptr es:[si+Scount4]
        mov     [bx+XbytesOffset+0],ax
        mov     [bx+XbytesOffset+2],dx

        ;--------------------------------------------
        ; Set destination handle in XMS move structure
        ;--------------------------------------------
        mov     word ptr [bx+XdstHoffset],0 ; Low mem=0

        ;--------------------------------------------
        ; Set source handle in XMS move structure
        ;--------------------------------------------
        mov     ax,[bx+XMShandleOffset]
        mov     [bx+XsrcHoffset],ax

        ;--------------------------------------------
        ; Call XMS driver to do the move
        ;--------------------------------------------
RWmerge label near
        lea     si,[bx+XMSmoveStructOffset]
        mov     ah,XMS_MOVE
        call    dword ptr [bx+XMSEntryPointOffset]
        dec     ax
        jz      ToMergeRetOK

        mov     ax,8102h        ; Device not ready
        jmp     near ptr intrexit
Read endp

;----------------------------------------------------
;                  Write - command #8
;----------------------------------------------------
; Note: This routine also does Write with verify.
;----------------------------------------------------
Write proc near
        assume  ds:nothing
        ;--------------------------------------------
        ; Set source address in XMS move structure
        ;--------------------------------------------
        push    es
        les     ax,dword ptr es:[si+DataAddr8]
        mov     [bx+XsrcOoffset+0],ax
        mov     [bx+XsrcOoffset+2],es
        pop     es

        ;--------------------------------------------
        ; Set destination address in XMS move structure
        ;--------------------------------------------
        mov     ax,SECTORSIZE
        mul     word ptr es:[si+StrtSec8]
        mov     [bx+XdstOoffset+0],ax
        mov     [bx+XdstOoffset+2],dx

        ;--------------------------------------------
        ; Set byte count in XMS move structure
        ;--------------------------------------------
        mov     ax,SECTORSIZE
        mul     word ptr es:[si+Scount8]
        mov     [bx+XbytesOffset+0],ax
        mov     [bx+XbytesOffset+2],dx

        ;--------------------------------------------
        ; Set destination handle in XMS move structure
        ;--------------------------------------------
        mov     ax,[bx+XMShandleOffset]
        mov     [bx+XdstHoffset],ax

        ;--------------------------------------------
        ; Set source handle in XMS move structure
        ;--------------------------------------------
        mov     word ptr [bx+XsrcHoffset],0
        jmp     RWmerge
Write endp

;----------------------------------------------------
; Bridge jumps to actual routines
;----------------------------------------------------
To_MediaCheck label near
        jmp     MediaCheck

To_BuildBPB label near
        jmp     BuildBPB

To_Read label near
        jmp     Read

To_Write label near
To_WriteV label near
        jmp     Write

;////////////////////////////////////////////////////
;/// This version of the DD "interrupt" routine   ///
;/// will be called when when running in the HMA. ///
;////////////////////////////////////////////////////
HMAINTERRUPTOFFSET = ($-HMALOADSTART)

HMAinterrupt proc far
        assume  ds:nothing
        ;--------------------------------------------
        ; Get local data addressability.  This returns
        ; with DS:BX aimed at the HMA data pool.
        ;--------------------------------------------
        call    DataAddressability

        ;--------------------------------------------
        ; Save the request header pointer locally in
        ; HMA data space.
        ;--------------------------------------------
        mov     word ptr [bx+HMAreq_ptr_offset+0],si
        mov     word ptr [bx+HMAreq_ptr_offset+2],es

        ;--------------------------------------------
        ; Route the command
        ;--------------------------------------------
        mov     bp, word ptr es:[si].command
        and     bp,255                  ;

        ;--------------------------------------------
        ; Note: all commands are dispatched with DS:BX
        ;       aimed at the data pool, and ES:SI aimed
        ;       at the device driver request packet.
        ;--------------------------------------------
        cmp     bp,4
        je      To_Read
        cmp     bp,8
        je      To_Write
        cmp     bp,9
        je      To_WriteV
        cmp     bp,1
        je      To_MediaCheck
        cmp     bp,2
        je      To_BuildBPB
        jmp     short DevError

        ;--------------------------------------------
        ; All requests exit through this point
        ;--------------------------------------------
intrexit label near
        call    DataAddressability

        ;--------------------------------------------
        ; Insert status word (in AX) in req packet
        ;--------------------------------------------
        lds     si,ds:[bx+HMAreq_ptr_offset]; DS:SI-->rqh
        mov     [si+status],ax         ;

        ;--------------------------------------------
        ; Return to low memory stub interrupt routine
        ;--------------------------------------------
        ret

        ;--------------------------------------------
        ; Out of range command codes wind
        ; up down here.
        ;--------------------------------------------
DevError:
        mov     ax,0103h        ; Done+unknown command
        jmp     short intrexit
HMAinterrupt endp

        even
HMALOADEND label near
        public HMALOADEND

HMALOADSIZE = (HMALOADEND-HMALOADSTART)

;----------------------------------------------------
; Init routines are only called once, so this can be
; be thrown away if/when we decide to go resident.
;----------------------------------------------------
To_NoHMAavail:
        jmp     near ptr NoHMAavail

Init    proc    near
        assume  ds:Vdisk        ; DS==CS when called
        ;--------------------------------------------
        ; DOS 5 or better?  Earlier DOS's won't be
        ; able to load DOS=HIGH, so they won't have
        ; any spare HMA space available we could
        ; slide into.
        ;--------------------------------------------
        mov     ah,30h          ; DOS version check
        int     21h             ;
        cmp     al,5            ; On less than 5.0
        jb      To_NoHMAavail   ;  refuse to load.

        ;--------------------------------------------
        ; Int 2F function 4A01 will report on the
        ; amount of free space left in the HMA after
        ; DOS has loaded HIGH.
        ;
        ; On return, BX    = Number of free bytes
        ;            ES:DI = Pointer to start of
        ;                    free space.
        ;--------------------------------------------
        ; OS/2 has a problem in that it doesn't
        ; support this API call currently.  It just
        ; returns with BX unchanged.  If we didn't
        ; zero out BX here, we could be tricked into
        ; thinking OS/2 supported the call.
        ;--------------------------------------------
        xor     bx,bx           ; Work around OS/2's
                                ;  not supporting
                                ;   this API call.

        mov     ax,QUERY_FREE_HMA     ; Is there
        int     2Fh                   ;  any HMA
        or      bx,bx                 ;   available?
        jz      To_NoHMAavail         ; No,

        cmp     bx,HMALOADSIZE        ; Is there enough
        jb      To_NoHMAavail         ; for our needs?

        ;--------------------------------------------
        ; Allocate what we'll need in the HMA
        ;--------------------------------------------
        mov     ax,ALLOC_PART_OF_HMA
        mov     bx,HMALOADSIZE        ; BX=byte count
        int     2Fh                   ; ES:DI-->HMA block
                                      ; on return

        mov     word ptr HMALoadPoint+0,di
        mov     word ptr HMALoadPoint+2,es

        lea     ax,[di + HMAINTERRUPTOFFSET]
        mov     word ptr HMAintrAddr+0,ax
        mov     word ptr HMAintrAddr+2,es

        ;--------------------------------------------
        ; If HMA is there, we must have an XMS driver
        ; of some sort present.  Get its entry point.
        ;--------------------------------------------
        mov     ax,XMS_GET_ENTRY_POINT ; ES:BX will be
        int     2Fh                    ;  driver entry
        mov     word ptr XMSEntryPoint+0,bx
        mov     word ptr XMSEntryPoint+2,es
        mov     word ptr StubXMSEntry+0,bx
        mov     word ptr StubXMSEntry+2,es

        ;--------------------------------------------
        ; Do a local A20 enable so we can slide the
        ; driver's code into the HMA.
        ;--------------------------------------------
        mov     ah,XMS_LOCAL_ENABLE_A20
        call    dword ptr [XMSEntryPoint]
        dec     ax                      ; Error?
JNZ_To_NoHMAavail1:
        jnz     To_NoHMAavail           ; refuse to load.

        ;--------------------------------------------
        ; Check free XMS for the VDISK memory
        ;--------------------------------------------
        mov     ah,XMS_QUERY
        call    dword ptr [XMSEntryPoint]
        cmp     ax,(VDISKSIZE/(1024/SECTORSIZE))
        jb      To_NoHMAavail   ;  refuse to load.

        ;--------------------------------------------
        ; Allocate XMS for the VDISK memory
        ;--------------------------------------------
        mov     dx,(VDISKSIZE/(1024/SECTORSIZE))
        mov     ah,XMS_ALLOC
        call    dword ptr [XMSEntryPoint]
        dec     ax              ; Error?
        jnz     JNZ_To_NoHMAavail ;  refuse to load.
        mov     XMShandle,dx ;

        ;--------------------------------------------
        ; Slide the HMA portion of the driver into the
        ; HMA with a REP MOV
        ;--------------------------------------------
        mov     si,offset HMALOADSTART  ;
        les     di,HMALoadPoint         ; ES:DI-->HMA
        mov     cx,HMALOADSIZE          ; CX=# to move
        cld                             ;
        rep     movsb                   ;

        ;--------------------------------------------
        ; Do a local A20 disable to restore A20 state
        ; to what it was when we were run.
        ;--------------------------------------------
        mov     ah,XMS_LOCAL_DISABLE_A20
        call    dword ptr [XMSEntryPoint]
        dec     ax              ; Error?
JNZ_To_NoHMAavail:
        jnz     JNZ_To_NoHMAavail ;  refuse to load.

        ;--------------------------------------------
        ; Zero out the XMS block
        ;--------------------------------------------

   ZEROXMSLEN = 64

        mov     ax,XMShandle
        mov     XdstH,ax                ; Destination=XMS

        xor     ax,ax
        mov     XsrcH,ax                ; Source=low memory

        mov     word ptr Xbytes+0,ZEROXMSLEN
        mov     word ptr Xbytes+2,ax    ;

        mov     word ptr XsrcO+0,offset Zeros
        mov     word ptr XsrcO+2,cs     ; Src addr=zeros

        mov     word ptr XdstO+0,ax     ; Dst addrs=
        mov     word ptr XdstO+2,ax     ;  start of XMS block

        mov     cx,(VDISKSIZE*(SECTORSIZE/ZEROXMSLEN))

ZeroLoop:
        push    cx                      ;
        mov     si,offset XMSmoveStruct ; DS:SI-->move struct
        mov     ah,XMS_MOVE
        call    dword ptr [XMSEntryPoint]
        pop     cx                      ;
        dec     ax                      ; Error?
        jnz     JNZ_To_NoHMAavail       ; refuse to load.

        add     word ptr XdstO+0,ZEROXMSLEN ;
        adc     word ptr XdstO+2,0      ;
        loop    ZeroLoop

        ;--------------------------------------------
        ; Initialize the boot record in XMS
        ;               (first part)
        ;--------------------------------------------
        mov     word ptr XsrcO+0,offset BootRecord
        mov     word ptr XsrcO+2,cs

        mov     word ptr XdstO+0,BOOTSECTOR
        mov     word ptr XdstO+2,0

        xor     ax,ax
        mov     word ptr Xbytes+0,((BOOTRECLEN+2) and 0FFFEh)
        mov     word ptr Xbytes+2,ax

        mov     si,offset XMSmoveStruct ; DS:SI-->move struct
        mov     ah,XMS_MOVE
        call    dword ptr [XMSEntryPoint]

        ;--------------------------------------------
        ; Initialize the FAT in XMS
        ;--------------------------------------------
        mov     word ptr XsrcO+0,offset InitialFAT
        mov     word ptr XsrcO+2,cs     ;

        xor     ax,ax
        mov     word ptr XdstO+0,FAT1SECTOR
        mov     word ptr XdstO+2,ax     ;

        mov     word ptr Xbytes+0,4     ;
        mov     word ptr Xbytes+2,ax    ;

        mov     ah,XMS_MOVE
        mov     si,offset XMSmoveStruct ;
        call    dword ptr [XMSEntryPoint]

if (NUMFATS gt 1)
        xor     ax,ax
        mov     word ptr XdstO+0,FAT2SECTOR
        mov     word ptr XdstO+2,ax     ;

        mov     word ptr Xbytes+0,4     ;
        mov     word ptr Xbytes+2,ax    ;

        mov     si,offset XMSmoveStruct ;
        mov     ah,XMS_MOVE
        call    dword ptr [XMSEntryPoint]
endif

        ;--------------------------------------------
        ; Insert our drive letter in the loading
        ; message.
        ;--------------------------------------------
        lds     si,StubRequest ; DS:SI-->request header
        assume  ds:nothing
        mov     al,[si+drvltr0]
        add     byte ptr cs:DrvLetter,al

        ;--------------------------------------------
        ; Say we're loading in the HMA
        ;--------------------------------------------
        push    cs
        pop     ds
        mov     ah,9
        mov     dx,offset DDloadMsg
        int     21h

        ;--------------------------------------------
        ; Set chop point for low stub
        ;--------------------------------------------
        mov     ax,offset LOWSTUBEND
InitMergeExit:
        lds     si,StubRequest ; DS:SI-->request header
        assume  ds:nothing
        mov     byte ptr [si].nunits0,1 ; Supports 1 unit
        mov     word ptr [si].chop0+0,ax; Set driver end
        mov     word ptr [si].chop0+2,cs;  address
        mov     word ptr [si].BPBaddr0+0,offset BPBpointer
        mov     word ptr [si].BPBaddr0+2,cs
        mov     word ptr [si].msgflg0,0 ;
        mov     word ptr [si].status,0100h
        ret

NoHMAavail label near
        ;--------------------------------------------
        ; Say we couldn't load into the HMA
        ;--------------------------------------------
        push    cs
        pop     ds
        mov     dx,offset NoLoadMSG
        int     21h

        ;--------------------------------------------
        ; Set chop point to zero so device load gets
        ; aborted.
        ;--------------------------------------------
        xor     ax,ax           ; Device doesn't load
        jmp     short InitMergeExit
Init    endp

;----------------------------------------------------
; This routine is used exactly once - the first time
; the device is called - i.e. initialization.
;----------------------------------------------------
initinterrupt proc far
        ;--------------------------------------------
        ; Set this variable so the HMA portion of the
        ; driver knows where the low stub lives.
        ;--------------------------------------------
        mov     word ptr cs:LowCS,cs

        ;--------------------------------------------
        ; Patch interrupt routine offset so that
        ; the next call to this DD goes to the
        ; low memory stub interrupt routine.
        ;--------------------------------------------
        mov     word ptr cs:devintr, offset stubinterrupt

        ;--------------------------------------------
        ; Save SS:SP
        ;--------------------------------------------
        mov     word ptr cs:OldStack+0,sp
        mov     word ptr cs:OldStack+2,ss

        ;--------------------------------------------
        ; Switch to a local stack
        ;--------------------------------------------
        push    cs
        pop     ss
        mov     sp,offset LocalStack

        ;--------------------------------------------
        ; Save everything on the local stack
        ;--------------------------------------------
        push    es
        push    ds
        pusha

        ;--------------------------------------------
        ; Call the one-shot init routine
        ;--------------------------------------------
        push    cs              ; Call init with
        pop     ds              ; DS==CS
        call    Init

        ;--------------------------------------------
        ; Restore everything from local stack
        ;--------------------------------------------
        popa
        pop     ds
        pop     es

        ;--------------------------------------------
        ; Switch back to caller stack
        ;--------------------------------------------
        mov     ss,word ptr cs:OldStack+2
        mov     sp,word ptr cs:OldStack+0

        ret
initinterrupt endp

Zeros         db  ZEROXMSLEN dup (0)
InitialFAT    db  MEDIADESCRIPTOR,0FFh,0FFh,00h

;----------------------------------------------------
;                   Init Messages
;----------------------------------------------------

DDloadMsg db    "HMAVDISK: Loading in HMA as drive "
DrvLetter db    'A'
          db    ':',13,10,'$'

NoLoadMsg db    "HMAVDISK: Failed to load",13,10,'$'

Vdisk   ends
        end

How HMAVDISK operates

Initialization Low Mem BPB Copy Low Mem Stub Resident part Loading the driver

Initialization

HMAVDISK does something for initialization you won't always see in device driver examples. The "interrupt" pointer in the device driver header initially points to a "one shot" initialization routine. This pointer in the device header then gets patched to point to the permanent "interrupt" routine during initialization. By doing this HMAVDISK can throw away all of it's initialization code/data after DOS calls it the first time. By specializing the initialization "interrupt" routine, the size of the "interrupt" routine that gets used when running normally can be cut down some.

Initialization then does what we'd expect a driver of this type to do:

Why the low stub copy of the BPB exists

Note that a copy of the VDISK's BPB (BIOS Parameter Block) is kept in low memory along with the stub code. When the driver is asked to return a pointer to the BPB, it returns a pointer to this low memory copy instead of the copy in the HMA. The reason for this is to defend against a program that might goes around DOS and call the driver directly. By keeping this low memory copy of the BPB around, the pointer to the BPB that the driver returns will always be valid and not dependent on the state of the A20 gate being on. Remember -- we shouldn't depend on the state of the A20 gate.

How the low memory stub operates

The low memory stub is what catches requests sent to HMAVDISK when it's running normally. The stub "strategy" routine works like any normal device driver strategy routine and just saves the request header pointer. The stub "interrupt" doesn't operate like a normal interrupt routine though. It's job is to simply setup for a call code in the HMA where the real work gets done. The stub interrupt routine switches to a local stack and then ensures that the A20 gate in on before calling into the HMA. All of the real work that gets done in this driver is done by code that lives in the HMA.

The HMA resident portion of the driver

The portion of the driver that lives in the HMA is typical of what we'd expect in a VDISK with two exceptions:

Using a jump table with absolute offsets would introduce a position dependency in the code. You could use a jump table for this, but the offsets would have to be patched up during initialization to account for where the code gets loaded into the HMA.

Running HMAVDISK

To run HMAVDISK, you load it with a normal DEVICE= line in CONFIG.SYS just like any other DOS device driver.


Copyright © 1998, Tony Ingenoso e-mail graphic tonyi@ibm.net - Shut up and jump!
Last modified on Sunday, Dec 20, 1998
This page produced the old fashoned way - with a text editor