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.
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
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.
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.
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.
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.
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!
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.
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
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.
WLINK.RSP response file for Watcom linker file hmaiomon.obj form dos com
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.
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.
TIP: Zeroing BX before doing the Int 2F function 4A01 call will allow a program to degrade gracefully when run under OS/2.
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.
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 /* 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".
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.
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.
HMAVDISK Makefile | HMAVDISK Source | HMAVDISK Operation |
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
Initialization | Low Mem BPB Copy | Low Mem Stub | Resident part | Loading the driver |
Initialization then does what we'd expect a driver of this type to do:
- Checks the DOS version number
- Checks to see if HMA space is available
- Checks to see if some XMS memory for the VDISK memory is available
- Allocate some HMA space for the driver
- Slide the HMA resident portion of the driver into the HMA
- Allocate some XMS for the VDISK
- Initialize the VDISK memory, boot sector, and FAT(s)
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.
- All of the HMA resident data is addressed using the "data pool" technique so references are position independent.
- The function dispatching is done with a series of compare and jump instructions rather than the usual jump table found in most device driver examples.