;/* himem2.asm ; * ; * Microsoft Confidential ; * Copyright (C) Microsoft Corporation 1988-1991 ; * All Rights Reserved. ; * ; * Modification History ; * ; * Sudeepb 14-May-1991 Ported for NT XMS support ; * ; * williamh 25-Sept-1992 added UMB initialization ; */ page 95,160 title 'HIMEM2 - Initialization code' funky segment word public 'funky' extrn textseg:word ; in high segment extrn KiddValley:word extrn KiddValleyTop:word extrn cHandles:word extrn pack_and_truncate:near extrn end_of_funky_seg:byte extrn LEnblA20:word extrn LDsblA20:word funky ends .xlist include himem.inc ; get structures, equ's, etc. ; and open segment include xmssvc.inc .list ; public f000 public InitInterrupt public MachineNum ;************************************************************************ ;* * ;* Global Variables * ;* * ;************************************************************************ extrn pPPFIRET:word extrn dd_int_loc:word extrn Interrupt:near extrn hiseg:word extrn call_hi_in_di:near extrn fCanChangeA20:byte extrn fHMAMayExist:byte extrn fVDISK:byte extrn IsVDISKIn:near extrn A20Handler:near extrn EnableCount:word extrn MemCorr:word extrn MinHMASize:word extrn pReqHdr:dword extrn PrevInt2f:dword extrn TopOfTextSeg:word extrn AddMem:near extrn InstallA20:near extrn Int2fHandler:near ; extrn Is6300Plus:near extrn IsA20On:near extrn SignOnMsg:byte extrn ROMDisabledMsg:byte extrn UnsupportedROMMsg:byte extrn ROMHookedMsg:byte extrn BadDOSMsg:byte extrn NowInMsg:byte extrn On8086Msg:byte extrn NoExtMemMsg:byte extrn FlushMsg:byte extrn StartMsg:byte extrn HandlesMsg:byte extrn HMAMINMsg:byte extrn KMsg:byte extrn NoHMAMsg:byte extrn A20OnMsg:byte extrn HMAOKMsg:byte extrn VDISKInMsg:byte extrn BadArgMsg:byte extrn DevAttr:word extrn Int15MemSize:word extrn EndText:byte extrn A20State:byte extrn DOSTI:near extrn DOCLI:near ;************************************************************************ ;* * ;* Code/Data below here will be discarded after driver initialization * ;* * ;************************************************************************ ; Discardable Initialization Data public fShadowOff, f1stWasWarning fShadowOff db 0 ; NZ if shadow RAM should be disabled, ; 0/1 set by command line switch, 0FFh ; set if little extended and hope to disable f1stWasWarning db 0 ; NZ if 1st attempt to diddle A20 generated ; a warning (and not an error) public fA20Control fA20Control db 0ffh ; NZ if himem should take control of A20, even ; it was already on when himem loaded. public fCPUClock fCPUClock db 0 ; NZ if himem should try to preserve CPU clock ; speed when gating A20 public StringParm, MachineNum, MachineName StringParm db 13 DUP (' ') MachineNum dw -1 ; Note: the following table MUST be in the same order as the entries in the ; A20_Scan_Table! If you add entries here, also add one there! MachineName label byte db 'ptlcascade',0 ; Phoenix Cascade BIOS db 'att6300plus',0 ; AT&T 6300 Plus db 'ps2',0 ; IBM PS/2 db 'hpvectra',0 ; HP 'Classic' Vectra (A & A+) db 'acer1100',0 ; Acer 1100 db 'toshiba',0 ; Toshiba 1600 & 1200XE db 'wyse',0 ; Wyse 12.5 MHz 286 machine db 'tulip',0 ; Tulip machines db 'zenith',0 ; Zenith ZBIOS db 'at1',0 ; IBM AT/delay 0 db 'at2',0 ; IBM AT/delay 1 db 'at3',0 ; IBM AT/delay 2 db 'philips',0 ; Philips machines db 'css',0 ; CSS Lab machines db 'fasthp',0 ; Single byte method for HP Vectras db 'ibm7552',0 ; IBM 7552 Industrial Computer db 'bullmicral',0 ; Bull Micral 60 M004 db 'at',0 ; IBM AT db 0FFh ; end of table ;NOTE: there is code in GetParms which depends on AltNameTbl coming ; after MachineName table. public AltName1, AltName2, AltName3, AltName4, AltName5 public AltName6, AltName7, AltName8, AltName9, AltName10 public AltName11, AltName12, AltName13, AltName14, AltName15 public AltName16 ;M004 AltNameTbl label byte AltName3 db '3',0 ; Phoenix Cascade BIOS AltName5 db '5',0 ; AT&T 6300 Plus AltName2 db '2',0 ; IBM PS/2 AltName4 db '4',0 ; HP 'Classic' Vectra (A & A+) AltName6 db '6',0 ; Acer 1100 AltName7 db '7',0 ; Toshiba 1600 & 1200XE AltName8 db '8',0 ; Wyse 12.5 Mhz 286 machine AltName9 db '9',0 ; Tulip machine AltName10 db '10',0 ; Zenith ZBIOS AltName11 db '11',0 ; IBM AT/delay 0 AltName12 db '12',0 ; IBM AT/delay 1 AltName13 db '13',0 ; IBM AT/delay 2 db '13',0 ; Philips machines (same as AT3) db '12',0 ; CSS machines AltName14 db '14',0 ; Single byte HP Vectra m/cs AltName15 db '15',0 ; IBM 7552 Industrial Computer AltName16 db '16',0 ; Bull Micral 60 M004 AltName1 db '1',0 ; IBM AT db 0FFh ; end of table ifdef debug_tsr ;----------------------------------------------- ;*----------------------------------------------------------------------* ;* * ;* ExeStart - * ;* * ;* Entry point when himem is invoked as an .EXE. * ;* * ;*----------------------------------------------------------------------* lpCmdLine dd 81h ; far ptr to command tail public ExeStart ExeStart: mov word ptr cs:[lpCmdLine+2],es ; save PSP segment in pointer mov ax,cs ; Setup segment regs to all be the same mov ds,ax mov es,ax call InitDriver ; Initialize... mov ax,TopOfTextSeg ; TopOfTextSeg == 0 is error installing or ax,ax jnz @f mov ax,4C03h ; error, so just terminate int 21h @@: mov di,offset pack_and_truncate jmp call_hi_in_di ; terminate and stay resident endif ;------------------------------------------------ ;*----------------------------------------------------------------------* ;* * ;* InitInterrupt - * ;* * ;* Called by MS-DOS immediately after Strategy routine * ;* * ;* ARGS: None * ;* RETS: Return code in Request Header's Status field * ;* REGS: Preserved * ;* * ;* This entry point is used only during initialization. * ;* It replaces itself with a much shorter version which only * ;* serves to report the appropriate errors when this driver * ;* is called in error. * ;* * ;*----------------------------------------------------------------------* InitInterrupt proc far ; Save the registers including flags. push ax ; We cannot use pusha\popa because push bx ; we could be on an 8086 at this point push cx push dx push ds push es push di push si push bp pushf push cs ; Set DS=CS for access to global variables. pop ds les di,[pReqHdr] ; ES:DI = Request Header mov bl,es:[di].Command ; Get Function code in BL or bl,bl ; Only Function 00h (Init) is legal jz IInit cmp bl,16 ; Test for "legal" DOS functions jle IOtherFunc IBogusFunc: mov ax,8003h ; Return "Unknown Command" jmp short IExit IOtherFunc: xor ax,ax ; Return zero for unsupported functions jmp short IExit IInit: call InitDriver ; Initialize the driver les di,[pReqHdr] ; Restore es:di = Request Header IExit: or ax,0100h ; Turn on the "Done" bit mov es:[di].Status,ax ; Store return code popff ; restore the registers pop bp pop si pop di pop es pop ds pop dx pop cx pop bx pop ax mov dd_int_loc,offset Interrupt ; replace Interrupt with ret ; tiny permanent stub InitInterrupt endp ;*----------------------------------------------------------------------* ;* * ;* InitDriver - * ;* * ;* Called when driver is Initialized. * ;* * ;* ARGS: ES:DI = Address of the Request Header * ;* RETS: pHdr.Address = Bottom of resident driver code * ;* REGS: AX, CX and Flags are clobbered * ;* * ;*----------------------------------------------------------------------* public InitDriver InitDriver proc near cld ifndef debug_tsr call LocateHiSeg ; locate the hiseg in low memory properly mov ax,cs push es mov es,hiseg assume es:funky add textseg,ax ; relocate text segment pointer add LEnblA20+2, ax ; update ptrs to enble & disable a20 rtns add LDsblA20+2, ax pop es assume es:nothing endif ; mov ah,9 ; display signon message ; mov dx,offset SignOnMsg ; int 21h mov ah,30h ; make sure we've got DOS 3.00 or higher int 21h ; Get DOS versions number cmp al,3 jae IDCheckXMS mov dx,offset BadDOSMsg jmp IDFlushMe IDCheckXMS: mov ax,(INT2F_ID SHL 8) OR INT2F_INS_CHK int 2Fh ; make sure there's no other XMS installed cmp al,80h ; Is INT 2F hooked? jne IDNotInYet mov dx,offset NowInMsg jmp IDFlushMe IDNotInYet: call GetParms ; process command line parameters ;; don't call IsA20On at this moment because we haven't init it yet mov ax, 2 XMSSVC XMS_A20 ;;;; call IsA20On ; Is A20 already enabled? or ax,ax ; (may zap cx, si, di) jz IDInsA20 ; no, go install A20 handler mov dx,offset A20OnMsg ; "A20 already on" message cmp fA20Control,0 ; should we take control of A20 anyway? jne IDInsA20 ; yes, go muck with it mov [fCanChangeA20],0 ; no, don't allow changing of A20 mov ah,9 ; and tell user about it int 21h jmp short IDAfterA20 IDInsA20: call InstallA20 ; install proper A20 handler jc IDFlushMe ; CY means fatal error ; Note: A side affect of the previous InstallA20 is that MemCorr ; is set to reflect the adjustment factor if we're on an AT&T 6300+ IDAfterA20: call InitHandles ; initialize handle table call GetInt15Memory ; how much extended memory is installed? cmp ax,64 ; Is there >= 64K of extended? jae IDHMAOK push es mov es,hiseg assume es:funky mov bx,[KiddValley] ; get size of memory we already have in tables mov cx,[cHandles] IDAnyMem: cmp [bx].Flags,FREEFLAG jnz IDAnyMem_1 ; brif not a valid free block add ax,[bx].Len ; accumulate total IDAnyMem_1: add bx,SIZE Handle loop IDAnyMem pop es assume es:nothing mov dx,offset NoHMAMsg or ax,ax ; no HMA, any other memory to control? jnz disp_hma_msg ; jmp if some memory ; We can't find any memory to manage. mov dx,offset NoExtMemMsg ; Display the message in DX followed by the "Flush" message. IDFlushMe: mov ah,9 int 21h mov dx,offset FlushMsg mov ah,9 int 21h xor ax,ax ; discard the driver mov [TopOfTextSeg],ax ifndef debug_tsr ;------------------------------- les di,[pReqHdr] mov es:[di].Units,al and cs:DevAttr,not 8000h ; clr bit 15 in attrib of driver header endif jmp short IDReturn ;------------------------------- IDHMAOK: mov [fHMAMayExist],1 mov dx,offset HMAOKMsg disp_hma_msg: ; mov ah,9 ; int 21h ;; tell xms.lib where our variable is mov ax, cs mov bx, offset A20State XMSSVC XMS_INITUMB call HookInt2F ; "turn on" the driver ; Initialization finished (or failed) -- return to caller IDReturn: ifndef debug_tsr ;------------------------------- mov di,offset pack_and_truncate jmp call_hi_in_di ; pack stuff down and terminate endif ;------------------------------- ret InitDriver endp ; ;---------------------------------------------------------------------------- ; procedure : LocateHiSeg ; ; Locate the movable segment properly in the low seg. ; taking care of the stripped ORG zeroes. This function ; calculates the segment at which the hiseg should run ; with the ORG. If the segment cvalue goes below zero the ; code is moved up high enough to run the code from a seg value ; of zero. ; ; This function assumes that the 'funky' segment follows ; immediately after the text seg. ; ;---------------------------------------------------------------------------- ; LocateHiSeg proc near push ds mov ax, cs ; para start of text seg mov cx, offset _text:EndText ; end of text seg add cx, 15 ; para round it shr cx, 1 shr cx, 1 shr cx, 1 shr cx, 1 add ax, cx ; para start of funky seg cmp ax, (HISEG_ORG shr 4) ; will the seg go below zero? jb MoveHiSeg ; yeah, we have to move it sub ax, (HISEG_ORG shr 4) ; no, it fits in pop ds mov hiseg, ax ; update the segment in which ; it is going to run from. ret MoveHiSeg: mov ds, ax ; segment at which funky ; resides without the ORG xor ax, ax mov es, ax ; we want to movve the code ; to 0:HISEG_ORG mov di, offset funky:end_of_funky_seg mov si, di sub si, HISEG_ORG mov cx, si dec di dec si std ; move backward (safe when ; source & dest overlap rep movsb cld pop ds mov hiseg, 0 ; funky is going to run from ; segment zero ret LocateHiSeg endp ;*----------------------------------------------------------------------* ;* * ;* HookInt2F - * ;* * ;* Insert the INT 2F hook * ;* * ;* ARGS: None * ;* RETS: None * ;* REGS: AX, SI, ES and Flags are clobbered * ;* * ;*----------------------------------------------------------------------* public HookInt2F HookInt2F proc near call DOCLI xor ax,ax mov es,ax mov si,2Fh * 4 ; save previous int2f vector mov ax,offset Int2FHandler ; and exchange with new one xchg ax,es:[si][0] mov word ptr [PrevInt2F][0],ax mov ax,cs xchg ax,es:[si][2] mov word ptr [PrevInt2F][2],ax call DOSTI ret HookInt2F endp ;*----------------------------------------------------------------------* ;* * ;* GetInt15Memory - * ;* * ;* Returns the amount of memory INT 15h, Function 88h says is free * ;* * ;* ARGS: None * ;* RETS: AX = Amount of free extended memory in K-bytes * ;* REGS: AX and Flags are clobbered * ;* * ;*----------------------------------------------------------------------* GetInt15Memory proc near IFDEF WHEN_INT15_DONE mov ah,88h ; snag the int 15h memory clc int 15h ; Is Function 88h around? jnc xret_geti15 xor ax,ax ; No, return 0 xret_geti15: ifndef NOLIMIT ;M005 cmp ax,15*1024 ; Limit himem.sys to using 15 meg jb @f ; of extended memory for apps mov ax,15*1024 ; that don't deal with > 24 bit @@: ; addresses endif ;M005 ELSE XMSSVC XMS_EXTMEM ; return ext-mem in ax5 ENDIF ret GetInt15Memory endp ;*----------------------------------------------------------------------* ;* * ;* GetParms - * ;* * ;* Get any parameters off of the HIMEM command line * ;* * ;* ARGS: None * ;* RETS: None * ;* REGS: AX, BX, CX, DX, DI, SI, ES and Flags clobbered * ;* * ;* Side Effects: cHandles and MinHMASize may be changed * ;* * ;*----------------------------------------------------------------------* GPArgPtr dd ? GPRegSave dw ? public GetParms GetParms proc near cld ; better safe than sorry push ds ifdef debug_tsr ;------------------------------- lds si,lpCmdLine else ;------------------------------- les di,[pReqHdr] ; Running as a device driver lds si,es:[di].pCmdLine ; DS:SI points to first char ; after "DEVICE=" @@: call GPGetChar ; Skip over driver name, up to jc GPDatsAll ; first blank or / or eol jz GPNextArg cmp al,'/' jnz @b dec si ; Backup to get / again endif ;------------------------------- assume ds:nothing,es:nothing ; Scan until we see a non-blank or the end of line. GPNextArg: call GPGetChar jc GPDatsAll ; eol jz GPNextArg ; blank mov word ptr cs:[GPArgPtr], si ; save ptr to start of arg mov word ptr cs:[GPArgPtr+2], ds ; incase we want to complain dec word ptr cs:[GPArgPtr] ; (GPGetChar points at next) cmp al,'/' ; better be a / or not a valid arg jz GPGotOne ; Detected invalid parameter or value, complain to user GPBadParm: mov ah,9 ; tell'm something isn't right push cs pop ds mov dx,offset BadArgMsg int 21h lds si,cs:[GPArgPtr] ; backup to last parameter GPBadDisp: call GPGetChar ; disp arg up to space or eol jc GPDatsAll ; skips over bad arg while we're at it jz GPNextArg cmp al,'/' ; start of next arg? jnz @f dec si ; maybe yes, maybe no--might cmp si,word ptr cs:[GPArgPtr] ; be same arg jnz GPNextArg ; next, go process new arg inc si ; same, keep displaying @@: mov dl,al mov ah,2 int 21h jmp short GPBadDisp ; Finished, we're outta here... GPDatsAll: pop ds ret ; Save what we found and get the number or string after it. GPGotOne: lodsb mov cs:[GPRegSave],ax ; Scan past the rest of the parm for a number, EOL, or a space. GPNeedParm: call GPGetChar jc GPBadParm jz GPBadParm ; blank cmp al,':' ; start of string arg je GPString cmp al,'=' jne GPNeedParm ; Read the number at DS:SI into DX GPNeedNum: call GPGetChar jc GPDatsAll cmp al,'0' jb GPNeedNum cmp al,'9' ja GPNeedNum xor dx,dx GPNumLoop: sub al,'0' cbw add dx,ax call GPGetChar jc GPNumDone jz GPNumDone cmp al,'0' jb GPBadParm cmp al,'9' ja GPBadParm shl dx,1 ; Stupid multiply DX by 10 mov bx,dx shl dx,1 shl dx,1 add dx,bx jmp short GPNumLoop ; Move the string arg from ds:si to StringParm GPString: mov cx,(SIZE StringParm) - 1 push cs pop es mov di,offset _text:StringParm GPStrLoop: call GPGetChar jc GPStrDone jz GPStrDone stosb loop GPStrLoop GPStrDone: mov byte ptr es:[di],0 ; Null terminate the string mov dx,-1 ; In case parm expects a num, give'm ; a likely invalid one ; Which parameter are we dealing with here? GPNumDone: xchg ax,cs:[GPRegSave] cmp al,'H' ; HMAMIN= parameter? jne @f jmp GPGotMin @@: cmp al,'N' ; NUMHANDLES= parameter? jne @f jmp GPGotHands @@: cmp al,'M' ; MACHINE: parameter? je GPGotMachine cmp al,'A' ; A20CONTROL: parameter? je GPGotA20Control cmp al,'S' ; SHADOWRAM: parameter? jne @f jmp GPGotShadow @@: cmp al, 'I' ; INT15= jne @f jmp GPGotInt15 @@: cmp al, 'C' ; CPUCLOCK: jne @f jmp GPGotCPUClock @@: jmp GPBadParm ; Process /A20CONTROL: parameter GPGotA20Control: mov ax,word ptr [StringParm] or ax,2020h mov bl,0FFh cmp ax,'no' ; ON ? - means we take control jz GPSetA20 inc bl cmp ax,'fo' ; OFF ? - means we leave alone if on jz GPSetA20 jmp GPBadParm GPSetA20: mov fA20Control,bl ; Z if A20 should be left alone if jmp GPNextParm ; it's already on when we're loaded ; Process /MACHINE: parameter. GPGotMachine: push si ; save current location push ds ; in param string push cs pop ds mov di,offset _text:MachineName ; es:di -> MachineName GPNextTbl: xor bx,bx GPNextName: mov si,offset _text:StringParm ; ds:si -> StringParm GPChkNext: cmp byte ptr es:[di],0FFh ; end of name table? jz GPNoName lodsb ; char from StringParm cmp al,'A' ; force to lower case for match jb @f ; (might be numeric, so don't just OR) cmp al,'Z' ja @f or al,20h @@: cmp al,es:[di] ; match so far? jnz GPFlushName or al,al ; finished if matched up to & incl NULL jz GPFoundName inc di ; still matches, check next char jmp short GPChkNext GPFlushName: inc bx GPFN2: inc di cmp byte ptr es:[di],0FFh jz GPNoName cmp byte ptr es:[di],0 jnz GPFN2 inc di jmp short GPNextName GPFoundName: mov cs:[MachineNum],bx ; found a match, remember which entry jmp short GPNameDone ; it is for later GPNoName: cmp di,offset _text:AltNameTbl ja GPBadName mov di,offset _text:AltNameTbl jmp short GPNextTbl GPNameDone: pop ds ; recover parm line pointer pop si jmp GPNextParm GPBadName: pop ds ; clear stack and error out... pop si jmp GPBadParm ; Process /NUMHANDLES= parameter. GPGotHands: cmp dx,MAXHANDLES jna @f jmp GPBadParm @@: or dx,dx ; Zero? jnz @f jmp GPBadParm @@: push es mov es,hiseg assume es:funky mov [cHandles],dx ; Store it pop es assume es:nothing mov dx,offset StartMsg ; display descriptive message call GPPrintIt push es mov es,hiseg assume es:funky mov ax,[cHandles] pop es assume es:nothing call GPPrintAX mov dx,offset HandlesMsg call GPPrintIt jmp GPNextParm GPGotMin: cmp dx,64 ; process /hmamin= parameter jna @f jmp GPBadParm @@: push dx mov cs:[MinHMASize],dx mov dx,offset HMAMINMsg ; print a descriptive message call GPPrintIt mov ax,cs:[MinHMASize] call GPPrintAX mov dx,offset KMsg call GPPrintIt pop dx mov cl,10 ; Convert from K to bytes shl dx,cl mov cs:[MinHMASize],dx jmp short GPNextParm ; Process /SHADOWRAM: parameter GPGotShadow: mov ax,word ptr [StringParm] or ax,2020h xor bl,bl cmp ax,'no' ; ON ? - means we leave it alone jz GPSetShadow inc bl cmp ax,'fo' ; OFF ? - means we turn it off jz GPSetShadow jmp GPBadParm GPSetShadow: mov fShadowOff,bl ; NZ if Shadow RAM should be turned off jmp short GPNextParm ; Process /CPUCLOCK: parameter GPGotCPUClock: mov ax,word ptr [StringParm] or ax,2020h xor bl,bl cmp ax,'fo' ; OFF ? - means we don't worry about it jz GPSetClock inc bl cmp ax,'no' ; ON ? - means we preserve CPU clock jz GPSetClock ; rate jmp GPBadParm GPSetClock: mov fCPUClock,bl ; NZ if clock rate preserved jmp short GPNextParm ; Process /INT15= parameter GPGotInt15: cmp dx, 64 ; atleast 64K jae @f jmp GPBadParm @@: call GetInt15Memory cmp ax, dx ; enuf Ext Mem ? jae @f jmp GPBadParm @@: mov [Int15MemSize], dx ; Fall through to GetNextParm GPNextParm: mov ax,cs:[GPRegSave] ; are we at the end of the line? cmp al,13 ; may not be needed any longer... je GPExit cmp al,10 je GPExit jmp GPNextArg GPExit: pop ds ret GetParms endp ; Get the next character from DS:SI, set CY if it's an EOL (CR, LF), set ; Z if it's a space GPOffEOL dw -1 public GPGetChar GPGetChar proc near cmp si,cs:[GPOffEOL] ; are we already at EOL? jnb GPAtEOL lodsb ; no, get next char cmp al,10 ; is this the EOL? je GPHitEOL cmp al,13 je GPHitEOL cmp al,' ' ; set Z if blank clc ret GPHitEOL: mov cs:[GPOffEOL],si ; save EOL offset once GPAtEOL: stc ret GPGetChar endp ;*----------------------------------------------------------------------* GPPrintIt proc near push ds ; Save current DS push cs ; Set DS=CS pop ds mov ah,9 int 21h pop ds ; Restore DS ret GPPrintIt endp ;*----------------------------------------------------------------------* GPPrintAX proc near mov cx,10 xor dx,dx div cx or ax,ax jz GPAPrint push dx call GPPrintAX pop dx GPAPrint: add dl,'0' mov ah,2 int 21h ret GPPrintAX endp ;*----------------------------------------------------------------------* ;* * ;* InitHandles - * ;* * ;* Initialize the Extended Memory Handle Table * ;* * ;* ARGS: None * ;* RETS: None * ;* REGS: AX, BX, CX, and Flags are clobbered * ;* * ;*----------------------------------------------------------------------* assume ds:_text public InitHandles InitHandles proc near push es mov es,hiseg assume es:funky mov cx,[cHandles] ; Init the Handle table. mov bx,[KiddValley] xor ax,ax IHTabLoop: mov [bx].Flags,UNUSEDFLAG mov [bx].cLock,al mov [bx].Base,ax mov [bx].Len,ax if keep_cs mov [bx].Acs,ax endif add bx,SIZE Handle loop IHTabLoop mov [KiddValleyTop],bx ; save top for handle validation pop es assume es:nothing ret InitHandles endp _text ends ifdef debug_tsr EndStmt equ STACK segment stack 'STACK' db 1024 dup (?) STACK ends else EndStmt equ endif EndStmt