Device drivers for MS-DOS always have an origin of zero but are otherwise assembled, linked, and converted into an executable module as though they were .COM files. (Although MS-DOS is also capable of loading installable drivers in the .EXE file format, this introduces unnecessary complexity into writing and debugging drivers and offers no significant advantages. In addition, it is not possible to use .EXE-format drivers with some IBM versions of MS-DOS because the .EXE loader is located in COMMAND.COM, which is not present when the installable device drivers are being loaded.) The driver should not have a declared stack segment and must, in general, follow the other restrictions outlined in Chapter 3 for memory-image (.COM) programs. A driver can be loaded anywhere, so beware that you do not make any assumptions in your code about the driver's location in physical memory. Figure 14-9 presents a skeleton example that you can follow as you read the next few pages.
name driver
page 55,132
title DRIVER.ASM Device-Driver Skeleton
;
; DRIVER.ASM MS-DOS device-driver skeleton
;
; The driver command-code routines are stubs only and have
; no effect but to return a nonerror "done" status.
;
; Copyright 1988 Ray Duncan
;
_TEXT segment word public 'CODE'
assume cs:_TEXT,ds:_TEXT,es:NOTHING
org 0
MaxCmd equ 24 ; maximum allowed command code:
; 12 for MS-DOS 2
; 16 for MS-DOS 3.0-3.1
; 24 for MS-DOS 3.2-3.3
cr equ 0dh ; ASCII carriage return
lf equ 0ah ; ASCII linefeed
eom equ '$' ; end-of-message signal
Header: ; device-driver header
dd -1 ; link to next device driver
dw 0c840h ; device attribute word
dw Strat ; "strategy" routine entry point
dw Intr ; "interrupt" routine entry point
db 'SKELETON' ; logical-device name
RHPtr dd ? ; pointer to request header, passed
; by MS-DOS kernel to strategy routine
Dispatch: ; interrupt-routine command-code
; dispatch table:
dw Init ; 0 = initialize driver
dw MediaChk ; 1 = media check
dw BuildBPB ; 2 = build BPB
dw IoctlRd ; 3 = IOCTL read
dw Read ; 4 = read
dw NdRead ; 5 = nondestructive read
dw InpStat ; 6 = input status
dw InpFlush ; 7 = flush input buffers
dw Write ; 8 = write
dw WriteVfy ; 9 = write with verify
dw OutStat ; 10 = output status
dw OutFlush ; 11 = flush output buffers
dw IoctlWt ; 12 = IOCTL write
dw DevOpen ; 13 = device open (MS-DOS 3.0+)
dw DevClose ; 14 = device close (MS-DOS 3.0+)
dw RemMedia ; 15 = removable media (MS-DOS 3.0+)
dw OutBusy ; 16 = output until busy (MS-DOS 3.0+)
dw Error ; 17 = not used
dw Error ; 18 = not used
dw GenIOCTL ; 19 = generic IOCTL (MS-DOS 3.2+)
dw Error ; 20 = not used
dw Error ; 21 = not used
dw Error ; 22 = not used
dw GetLogDev ; 23 = get logical device (MS-DOS 3.2+)
dw SetLogDev ; 24 = set logical device (MS-DOS 3.2+)
Strat proc far ; device-driver strategy routine,
; called by MS-DOS kernel with
; ES:BX = address of request header
; save pointer to request header
mov word ptr cs:[RHPtr],bx
mov word ptr cs:[RHPtr+2],es
ret ; back to MS-DOS kernel
Strat endp
Intr proc far ; device-driver interrupt routine,
; called by MS-DOS kernel immediately
; after call to strategy routine
push ax ; save general registers
push bx
push cx
push dx
push ds
push es
push di
push si
push bp
push cs ; make local data addressable
pop ds ; by setting DS = CS
les di,[RHPtr] ; let ES:DI = request header
; get BX = command code
mov bl,es:[di+2]
xor bh,bh
cmp bx,MaxCmd ; make sure it's legal
jle Intr1 ; jump, function code is ok
call Error ; set error bit, "unknown command" code
jmp Intr2
Intr1: shl bx,1 ; form index to dispatch table
; and branch to command-code routine
call word ptr [bx+Dispatch]
les di,[RHPtr] ; ES:DI = addr of request header
Intr2: or ax,0100h ; merge 'done' bit into status and
mov es:[di+3],ax ; store status into request header
pop bp ; restore general registers
pop si
pop di
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret ; back to MS-DOS kernel
; Command-code routines are called by the interrupt routine
; via the dispatch table with ES:DI pointing to the request
; header. Each routine should return AX = 0 if function was
; completed successfully or AX = (8000h + error code) if
; function failed.
MediaChk proc near ; function 1 = media check
xor ax,ax
ret
MediaChk endp
BuildBPB proc near ; function 2 = build BPB
xor ax,ax
ret
BuildBPB endp
IoctlRd proc near ; function 3 = IOCTL read
xor ax,ax
ret
IoctlRd endp
Read proc near ; function 4 = read (input)
xor ax,ax
ret
Read endp
NdRead proc near ; function 5 = nondestructive read
xor ax,ax
ret
NdRead endp
InpStat proc near ; function 6 = input status
xor ax,ax
ret
InpStat endp
InpFlush proc near ; function 7 = flush input buffers
xor ax,ax
ret
InpFlush endp
Write proc near ; function 8 = write (output)
xor ax,ax
ret
Write endp
WriteVfy proc near ; function 9 = write with verify
xor ax,ax
ret
endp
OutStat proc near ; function 10 = output status
xor ax,ax
ret
OutStat endp
OutFlush proc near ; function 11 = flush output buffers
xor ax,ax
ret
OutFlush endp
IoctlWt proc near ; function 12 = IOCTL write
xor ax,ax
ret
IoctlWt endp
DevOpen proc near ; function 13 = device open
xor ax,ax
ret
DevOpen endp
DevClose proc near ; function 14 = device close
xor ax,ax
ret
DevClose endp
RemMedia proc near ; function 15 = removable media
xor ax,ax
ret
RemMedia endp
OutBusy proc near ; function 16 = output until busy
xor ax,ax
ret
OutBusy endp
GenIOCTL proc near ; function 19 = generic IOCTL
xor ax,ax
ret
GenIOCTL endp
GetLogDev proc near ; function 23 = get logical device
xor ax,ax
ret
GetLogDev endp
SetLogDev proc near ; function 24 = set logical device
xor ax,ax
ret
SetLogDev endp
Error proc near ; bad command code in request header
mov ax,8003h ; error bit + "unknown command" code
ret
endp
Init proc near ; function 0 = initialize driver
push es ; save address of request header
push di
mov ax,cs ; convert load address to ASCII
mov bx,offset Ident1
call hexasc
mov ah,9 ; display driver sign-on message
mov dx,offset Ident
int 21h
pop di ; restore request-header address
pop es
; set address of free memory
; above driver (break address)
mov word ptr es:[di+14],offset Init
mov word ptr es:[di+16],cs
xor ax,ax ; return status
ret
Init endp
hexasc proc near ; converts word to hex ASCII
; call with AX = value,
; DS:BX = address for string
; returns AX, BX destroyed
push cx ; save registers
push dx
mov dx,4 ; initialize character counter
mov cx,4 ; isolate next four bits
rol ax,cl
mov cx,ax
and cx,0fh
add cx,'0' ; convert to ASCII
cmp cx,'9' ; is it 0-9?
jbe hexasc2 ; yes, jump
add cx,'A'-'9'-1 ; add fudge factor for A-F
hexasc2: ; store this character
mov [bx],cl
inc bx ; bump string pointer
dec dx ; count characters converted
jnz hexasc1 ; loop, not four yet
pop dx ; restore registers
pop cx
ret ; back to caller
hexasc endp
Ident db cr,lf,lf
db 'Advanced MS-DOS Example Device Driver'
db cr,lf
db 'Device driver header at: '
Ident1 db 'XXXX:0000'
db cr,lf,lf,eom
Intr endp
_TEXT ends
end
Figure 14-9. DRIVER.ASM: A functional skeleton from which you can implement your own working device driver.
The driver's device header must be located at the beginning of the file (offset 0000H). Both words in the link field in the header should be set to -1. The attribute word must be set up correctly for the device type and other options. The offsets to the strategy and interrupt routines must be relative to the same segment base as the device header itself. If the driver is for a character device, the name field should be filled in properly with the device's logical name. The logical name can be any legal 8-character filename, padded with spaces and without a colon. Beware of accidentally duplicating the names of existing character devices, unless you are intentionally superseding a resident driver.
MS-DOS calls the strategy and interrupt routines for the device by means of an intersegment call (CALL FAR) when the driver is first loaded and installed and again whenever an application program issues an I/O request for the device. MS-DOS uses the ES:BX registers to pass the strat routine a double-word pointer to the request header; this address should be saved internally in the driver so that it is available for use during the subsequent call to the intr routine.
The command-code routines for function codes 0 through 12 (0CH) must be present in every installable device driver, regardless of device type. Functions 13 (0DH) and above are optional for drivers used with MS-DOS versions 3.0 and later and can be handled in one of the following ways:
Don't implement them, and leave the associated bits in the device header cleared. The resulting driver will work in either version 2 or version 3 but does not take full advantage of the augmented functionality of version 3.
Implement them, and test the MS-DOS version during the initialization sequence, setting bits 6 and 11 of the device header appropriately. Write all command-code routines so that they test this bit and adjust to accommodate the host version of MS-DOS. Such a driver requires more work and testing but will take full advantage of both the version 2 and the version 3 environments.
Implement them, and assume that all the version 3 facilities are available. With this approach, the resulting driver may not work properly under version 2.
Remember that device drivers must preserve the integrity of MS-DOS. The driver must preserve all registers, including flags (especially the direction flag and interrupt enable bits), and if the driver makes heavy use of the stack, it should switch to an internal stack of adequate depth (the MS-DOS stack has room for only 40 to 50 bytes when a driver is called).
If you install a new CON driver, be sure to set the bits for standard input and standard output in the device attribute word in the device header.
You'll recall that one file can contain multiple drivers. In this case, the device-header link field of each driver should point to the segment offset of the next, all using the same segment base, and the link field for the last driver in the file should be set to -1,-1. The initialization routines for all the drivers in the file should return the same break address.