home *** CD-ROM | disk | FTP | other *** search
- Creating User-Installable Device Drivers in MS-DOS 2.0+
-
- Bruce Bordner, 1985
- 1713 4th Avenue
- Asbury Park, NJ 07712
-
-
-
- Prior to version 2.0, driver programs to support new (or non-
- IBM) peripherals required some complex and ugly programming to
- interface with DOS. This became so much of a problem that it was
- fixed in the first major revision (2.0) by providing a "legal" way
- to install device drivers and interface with the DOS I/O functions.
- This was primarily intended as a convenience for OEMs, but
- Microsoft did include a chapter (14) about the subject in the DOS
- manual. I haven't found any better source on the subject; which
- means that I found practically no other information. [Only other
- source: "Modifying MS-DOS Device Drivers" by Mike Higgins, Computer
- Language 3/85 - a good article which can clarify the DOS
- documentation, but does not treat several points explained here.]
-
- Assuming for the moment that driver software can be home-
- brewed, it is not an obviously useful technique. You could make
- your own driver for a 500 megabyte drive rather than waiting for
- the manufacturer to do it, but this is a last-ditch move. However,
- a driver does not necessarily have to be controlling a physical
- device. A DOS device driver must be a COM file, which limits the
- code space to 64K total. It has a specified header and function
- call structure. DOS will only make and accept I/O operations which
- are given in the manual. You cannot return an error code other than
- those used by DOS, and most of those are not returned to your
- calling program. Other than that, you can do as you like. This
- opens up many possibilities. Driver software becomes a part of the
- memory-resident sections of DOS during the boot-up operation. It
- becomes another resource available to any of your programs. The
- classic example, as given in the DOS manual, is a ram-based disk
- simulator. This is a "virtual" device, where the code and data is
- seen as a package by the operating system and user programs. This
- module can perform any function which you can fit into a COM
- program, with predefined interfaces to DOS, other programs, and
- other device drivers. Although Microsoft built some limiting
- assumptions into DOS which make it difficult to implement certain
- functions, many possibilities remain. One option which I would
- like to explore is to offload a driver to an outboard processor for
- concurrent background operation.
-
- _DOS Function Calls for Device Drivers_
-
- In order to keep things simple, I only used the "extended file
- management" (version 2.0) functions under system interrupt 21H.
- These are used by the "fread()" and "fwrite()" functions of the C86
- C compiler. The primary functions affecting drivers are:
-
-
- 3D Open a file or device, return a 16-bit file handle in
- register AX.
-
- 3E Close the file or device associated with the handle in
- BX.
-
- 3F Read from a file or device. The following registers must
- be loaded as indicated:
- BX => file handle of device
- CX => number of bytes to read ( 64K max)
- DX => segment offset of your data buffer storage
- DS => segment of your data buffer
- Reads CX bytes from a device into the buffer address
- given. What is not explained by Microsoft is that DOS
- actually requests only one byte per call to the device
- driver, making CX number of calls. This has all sorts
- of ugly side effects, which will become evident in the
- description of my sample driver. Microsoft has
- apparently built in the assumption that all character-
- oriented devices talk to fairly slow serial hardware,
- like printers. If I read this right, DOS has the
- ability to respond to interrupts between each byte
- transferred, but it does make things clumsy. DOS
- apparently increments the DS:DX buffer location after
- each call. The driver will be re-run from START for each
- byte, and must maintain its own data pointers to maintain
- synchronism with the DOS transfer.
-
- 40 Write to a file or device.
- Same as above, but data is transferred to your buffer
- area. Same warning.
-
- 44 I/O Control for Devices. This function has 8 subfunction
- codes which are used for some rudimentary device
- controls. Subfunction 2 is to read CX number of bytes
- from the "control channel" of the device, with the same
- register settings as for the normal read. The stated
- purpose is to provide a way of reading device driver
- status rather than data from the device itself. However,
- up to 64K bytes made be transferred per call. What your
- device does with it is up to you. Subfunction 3 is the
- corresponding write call.
- For these calls, DOS actually requests CX bytes from
- the device on one call. This is used in the second
- version of my sample driver, which is much simpler than
- the standard read-write calls used in the first version.
-
- _How DOS Translates Your Read/Write Function Calls into Device
- Driver Requests_
-
- When any of the above functions is called by your application
- program, DOS develops a data structure called the "Request Header"
- by the manual. This structure consists of a 13-byte defined header
- which may be followed by other data bytes depending on the function
- requested. The fixed part of the request header is as follows:
-
- _BYTE_ _PURPOSE_
- 0 Length in bytes of the total request header (0-255)
-
- 1 Unit code, used to determine subunit to use in block
- devices (not used for character devices).
-
- 2 Command code (0-12) to activate specific device
- function.
-
- 3-4 Status word, returned by the driver
-
- 5-12 The manual states that this area is "reserved for
- DOS". Another source indicates that this consists
- of two double-word (4-byte) pointers to be used to
- maintain a linked list of request headers for this
- device and a list of all current device requests
- being processed by DOS. This is apparently in the
- works for a future concurrent-DOS.
-
- The 13 command codes are detailed on pages 14-12 of the
- manual; only the following are used by the character devices
- explained in this paper:
-
- _CODE_ _FUNCTION_
- 0 INIT - perform all initialization required at DOS
- boot time to install the driver and set local driver
- variables.
-
- 3 IOCTL INPUT - read a specified number of bytes from
- the device driver's IO control channel.
-
- 4 INPUT - normal device "read". Reads a number of
- bytes from the device your driver is controlling.
-
- 8 OUTPUT - normal device "write" call from user
- program.
-
- 12 IOCTL OUTPUT - write bytes to driver control
- channel.
-
- For each of these function calls, the driver receives the
- following:
-
- INIT: This function must be built into any driver program. It
- is called only by DOS during boot time, to reserve the system
- memory needed to hold the driver and to link the driver into the
- set of active devices managed by DOS. DOS sends: 13-byte request
- header
- BYTE number of units (not used by char devices)
- DWORD ending address of driver
- DWORD pointer to BPB array (not used by char devices)
- The driver program must load the ending address at a minimum; any
- local initialization may also be performed at this time.
-
- INPUT, OUTPUT, IOCTL INPUT, or IOCTL OUTPUT:
- For all of these, DOS sends:
- 13-byte request header
- BYTE media descriptor (not used for char devices)
- DWORD offset and segment of the data buffer in calling program
- WORD number of bytes to transfer in this call
- WORD starting sector (not used for char devices)
- The driver must perform the requested read or write function, set
- the "number of bytes to transfer" location to the number actually
- done, and set the status word in the request header to indicate any
- errors.
-
- The actual use of these structures will be detailed in the
- driver function description.
-
- _Required Structure for a Device Driver_
-
- Listing 1 (DOSDEV.ASM) is a template containing the minimum
- requirements for a character-oriented device driver. This is
- detailed in pages 14-3 to 14-8 of the DOS manual. The driver
- program must meet the requirements of a normal COM file. However,
- COM files usually start with an ORG 100H to allow room for the DOS
- Program Segment Prefix structure. For a driver, you must use ORG
- 0, as the PSP is not used. The Device Header data structure must
- be the first object defined in your file. It consists of:
- DWORD Pointer to the next device driver currently
- installed. This should be initialized to -1,
- DOS will fill this field as necessary during system
- initialization (boot).
-
- WORD Device attribute. I used C000H to indicate that
- this is a character device with IOCTL capability.
- This field is also used to indicate if this device
- is to be the standard output or input device.
- WORD Pointer to "device strategy" function in the driver.
- This function is called whenever a request is made
- to the driver, and must store the location of the
- request header from DOS.
- WORD Pointer to function which activates driver routines
- to perform the command in the current request
- header. This is called by DOS after the call to
- the strategy function, and should reset to the
- request header address stored by "strategy", to
- allow for the possibility of interrupts between the
- two calls.
- 8-BYTES Name field. For character devices, fill this with
- the name which you must use when opening the device.
-
- After this structure, you may include any local data
- definitions needed for the internal operation of your driver. The
- DOSDEV example includes only the minimum; a pointer to the request
- header and a table of addresses of the functions which will be
- called by the command code from DOS. The function addresses are
- arranged according to their calling function code (0 to 12) so that
- the function router can use the DOS command code as an offset into
- this table.
-
- _Required Device Driver Functions_
-
- For simplicity, I will discuss these functions as given in the
- DOSDEV.ASM listing.
-
- XDV STRAT: This function is called directly by DOS when a
- request has been made to use this device. Its only purpose is to
- save a segment and offset pointer to the request header. At the
- time DOS calls the device, the segment of the request header is in
- register ES and the offset is in register BX. These values are
- copied into the variables RH SEG and RH OFF. The fact that
- Microsoft calls this a "device strategy" function leads me to
- believe that more complex processing will be required in this
- function when DOS becomes multi-user or multi-processing oriented.
-
- XDV FUNC: This is called by DOS immediately after XDV STRAT.
- The function pushes all machine registers to save the current data
- until the device has finished the requested operation. Data
- segment register DS is set to the Code segment value, as all local
- variables exist in the code segment. Registers ES and BX are
- loaded from RH SEG and RH OFF to reset them to the start of the DOS
- request header. The command code from the request header (at
- ES:[BX+2] ) is then used as an offset into the function address
- table FUNTAB to initiate the driver function requested. In DOSDEV,
- only the INIT function has been coded, all others drop out to EXIT
- after setting the status word of the request header to "done; no
- error". All you need to do is fill in the function code for any
- driver function you intend to use.
-
- INIT: When DOS is booted, it reads your CONFIG.SYS file to
- determine which programs to install as device drivers
- (DEVICE=filename.ext). After loading the file image into memory,
- DOS sends a request header with the command code "0" to the device.
- The INIT function must load an offset (at ES:[BX+14]) and segment
- value (at ES:[BX+16]) into the request header to indicate the
- ending address for the driver program, including space for any
- memory used as a virtual device. The function may also do any
- initial variable setting within the driver. INIT then exits back
- to DOS, which uses the address given to set the boundary of DOS
- including the new driver storage.
-
- EXIT: This function restores all machine registers and
- returns to DOS.
-
- _Examples of Character-Oriented Device Drivers_
-
- Listing 1 (STKDEV.ASM) and 2 (STKDEV2.ASM) show the use of a
- virtual device driver to implement a "stack". User programs may
- "push" bytes or entire records by writing them to the device, and
- "pop" them with a read request. I/O control calls are used to set
- the record size to be used by the driver. This may not be very
- useful in itself, but this example shows solutions to most of the
- problems without being difficult to read. STKDEV is constructed
- in the recommended fashion; I got much of the code from the example
- device driver in the DOS manual. Read and write calls from the
- user program activate the functions INPUT and OUTPUT, while IOCTL
- IN and IOCTL OUT are used to read and write the record size
- setting. I developed the first version in a few days, but then
- spent two months of spare time trying to find out why it wouldn't
- work. It's an undocumented feature of MS-DOS, although I can see
- some hints of it in the manual - now that I know what to look for.
- When your user program makes a read or write call to a device,
- you send DOS the number of bytes to transfer, which may be 1 to
- 64K. You make one call to DOS (interrupt 21H). The request
- header for I/O contains a full word to contain the byte count sent
- from DOS. I made the mistake of assuming that when I make a 10
- byte I/O request to my driver, the driver would see a 10 byte
- count. Actually, it sees 10 unrelated 1-byte requests from DOS.
- STKDEV's INPUT and OUTPUT functions show the effects. I had
- to establish two new variables (NUM2READ and NUM2WRITE) to keep
- track of how many bytes had been transferred, so that the driver
- would know if it was done with a "record". This is required
- because the "top of stack" pointer (CURRENT) is set to the next
- free address following the last byte written. A "pop" operation
- (INPUT) requires decrementing the pointer by the record size,
- transferring a full record in the byte order written, then
- resetting the pointer back to the used record's start to allow
- overwriting and repeated "pops". There must be an easier way to
- do this, but I think this mess shows the problems more clearly.
- STKDEV2 uses IOCTL functions rather than the standard I/O. On
- IOCTL calls, DOS sends the full byte count in the request header.
- This made things simpler in my driver code, but complicated my user
- programs by requiring custom read/write functions. Take your
- choice. DOS apparently starts at the buffer address which your
- program supplies in the I/O call, transferring one byte with a
- request to the specified driver, then incrementing the buffer
- pointer by 1, and repeating until the specified number of bytes is
- copied. The device driver must track this indexing carefully in
- some applications, for others it may not matter.
-
- Mike Higgins' article included many debugging tips. One of
- them is the "yell" macro at the beginning of STKDEV. This displays
- one character on the screen by writing directly to the video
- memory. If you use DOS function calls to display the status of
- your driver, DOS will overwrite the request header which your
- driver has started processing. I have left "yell" invocations
- throughout the function code; it was this macro that finally showed
- me what my device was receiving from DOS.
-
- Functional Description of STKDEV.ASM:
-
- The procedure and device name is XSTK, which must be used when
- opening the device for I/O. It is assembled and linked normally,
- then use EXE2BIN to convert the EXE file to COM form. I used
- EXE2BIN STKDEV.EXE XSTK.SYS, changing the file name because any
- references to XSTK once it is installed cause weirdness. The
- CONFIG.SYS file must contain DEVICE=XSTK.SYS. Reboot and XSTK has
- added 32K+ to memory-resident DOS.
-
- XSTK STRAT:
- This is the "device strategy" function, which is called first
- by DOS for every request header. DOS has set ES and BX to the
- address of the request header; these are stored RH SEG and RH OFF
- to ensure that the driver will be able to find the request header.
- It may be omitted for DOS 2.0.
-
- XSTK FUNC:
- DOS calls this entry point second on all device requests. The
- call is a signal to begin processing the data in the request
- header. DOS is now suspended (in this version) until your device
- returns to it. All machine registers are saved on the stack, and
- ES and BX are reloaded to the address stored by XSTK STRAT. The
- command code at ES:[BX+2] is used as an index to jump to the
- requested function.
-
- INIT:
- This function is called only by DOS, only during installation
- (boot) time. As it will not be needed while the device is
- operating, INIT could be located after the end address returned to
- DOS, saving some memory. STORAGE is the variable marking the
- end of the XSTK code. However, I add 32K for stack storage. This
- value is then copied to the request header and returned to DOS for
- memory allocation. Local variables are set to default "stack
- empty" values.
-
- IOCTL IN:
- Used to read the current record size from the driver into the
- calling program's data buffer. In order to use the REP MOVSB
- instruction, CX is set to the requested byte count, DS and SI point
- to the internal variable RECSIZE, ES and DI point to the buffer
- address contained in the request header. SI and DI are incremented
- by the REPeat prefix until CX bytes have been transferred. ES and
- BX are then reset to the request header address.
-
- IOCTL OUT:
- Write a new record size to the device. Same deal as above,
- backward.
-
- INPUT:
- Processes read requests. The double word at ES:[BX+14]
- contains the address of the data buffer in the calling program, and
- the word at ES:[BX+18] is the byte count for the request. This
- value will always be 1 for DOS 2.0, but this is subject to change.
- The first process required is to check whether the previous
- write (OUTPUT) completed storing RECSIZE bytes. This is done by
- checking the NUM2WRITE variable. If NUM2WRITE is not 0, the
- CURRENT pointer is set to a record boundary before reading.
- Next, INPUT checks to see if it is in the process of reading a
- record or if it is starting a new record. If NUM2READ is 0, INPUT
- must reset CURRENT to the start of the last record written. At
- this time, INPUT checks CURRENT against BOTMEM to ensure that reads
- will not go past the bottom of the stack space. I tried to return
- an error code of 30H, to give my calling program a different error
- than those used by DOS. However, DOS apparently checks this value
- against the approved list, and I get a "Disk drive error" on the
- display. So, it appears that only the given error codes will be
- sent to calling programs. The actual read transfer is set up
- at PULLIT. Again, I used the REP MOVSB instruction, even though
- DOS will only call for one byte per request header. CX is loaded
- with the count from the request header, DS and SI have been set to
- the proper address in the stack storage, ES and DI are set to the
- data buffer address of the calling program. NUM2READ is
- decremented on each request. While NUM2READ is not 0, the value
- of SI is stored in CURRENT; SI has been incremented by the REP
- MOVSB to point at the next byte of the record. If NUM2READ is 0,
- a full record has been read and CURRENT must be reset to the
- starting address of the record. Finally, ES and BX are reset to
- point to the DOS request header. The status word of the request
- header is filled with the code for "done; no error" and the process
- completes through EXIT.
-
- OUTPUT:
- Similar to INPUT, except that CURRENT always increments.
-
-
- Description of STKDEV2:
-
- This version reverses the use of IOCTL and INPUT/OUTPUT. Most
- of the code is the same as STKDEV, but the variables NUM2READ and
- NUM2WRITE are no longer needed, as the DOS request header will
- request the actual number of bytes given by the calling program.
- THerefore, the driver implicitly knows that each request will
- consist of a complete record. If you compare IOCTL IN with INPUT,
- and IOCTL OUT with OUTPUT of STKDEV, it is obvious that this
- approach was easier to code for this application.
-
-
- _Testing the Sample Device Drivers_
-
- Listing 4 (TXSTK.C) is a C program to perform simple test
- calls to STKDEV. Listing 5 (TXSTK2.C) tests STKDEV2 by using IOCTL
- calls in place of the read/write system calls used in TXSTK.
-
- TXSTK uses segread() to determine the DS segment value of
- itself. This is used to pass DOS the segment value of the data
- buffer "instr". Then XSTK is opened. I used sysint21() calls
- instead of fopen, fwrite() and fread() just to simplify matters.
- "Outstr" is then written to XSTK. Although I fill callregs.cx
- with the count of 5, I know that DOS will make 5 one-byte calls.
- XSTK is currently set to the default RECSIZE of one, so a write and
- corresponding read produces "olleH" from my string "Hello" written.
- I then use IOCTL calls to reset XSTK's RECSIZE to 5 bytes.
- Although the following write and read are in the same form as
- before, XSTK now knows to treat input as 5-byte records. So,
- "Hello" returns "Hello".
-
- TXSTK2 is the same through opening XSTK. However, the first
- byte-at-a-time write/read must use a loop to cycle through the 5
- characters of the output and input strings. After resetting XSTK's
- RECSIZE to 5 using I/O calls, the write/read calls request 5 bytes,
- which is now processed in one call to XSTK. The strings are
- returned as with TXSTK; 1"olleH" and "Hello".