home *** CD-ROM | disk | FTP | other *** search
/ Complete Bookshop / CompleteWorkshop.iso / compute / dosdev2 / dosdev2.doc < prev    next >
Text File  |  1989-10-07  |  23KB  |  443 lines

  1.  Creating User-Installable Device Drivers in MS-DOS 2.0+
  2.  
  3.                Bruce Bordner, 1985
  4.                1713 4th Avenue
  5.                Asbury Park, NJ 07712
  6.  
  7.  
  8.  
  9.      Prior to version 2.0, driver programs to support new (or non-
  10. IBM) peripherals required some complex and ugly programming to
  11. interface with DOS.  This became so much of a problem that it was
  12. fixed in the first major revision (2.0) by providing a "legal" way
  13. to install device drivers and interface with the DOS I/O functions. 
  14. This was primarily intended as a convenience for OEMs, but
  15. Microsoft did include a chapter (14) about the subject in the DOS
  16. manual.  I haven't found any better source on the subject; which
  17. means that I found practically no other information.  [Only other
  18. source: "Modifying MS-DOS Device Drivers" by Mike Higgins, Computer
  19. Language 3/85 - a good article which can clarify the DOS
  20. documentation, but does not treat several points explained here.]
  21.  
  22.      Assuming for the moment that driver software can be home-
  23. brewed, it is not an obviously useful technique.  You could make
  24. your own driver for a 500 megabyte drive rather than waiting for
  25. the manufacturer to do it, but this is a last-ditch move.  However,
  26. a driver does not necessarily have to be controlling a physical
  27. device.   A DOS device driver must be a COM file, which limits the
  28. code space to 64K total.  It has a specified header and function
  29. call structure.  DOS will only make and accept I/O operations which
  30. are given in the manual. You cannot return an error code other than
  31. those used by DOS, and most of those are not returned to your
  32. calling program.  Other than that, you can do as you like.  This
  33. opens up many possibilities.  Driver software becomes a part of the
  34. memory-resident sections of DOS during the boot-up operation.  It
  35. becomes another resource available to any of your programs.  The
  36. classic example, as given in the DOS manual, is a ram-based disk
  37. simulator.  This is a "virtual" device, where the code and data is
  38. seen as a package by the operating system and user programs.  This
  39. module can perform any function which you can fit into a COM
  40. program, with predefined interfaces to DOS, other programs, and
  41. other device drivers.  Although Microsoft built some limiting
  42. assumptions into DOS which make it difficult to implement certain
  43. functions, many possibilities remain.  One option which I would
  44. like to explore is to offload a driver to an outboard processor for
  45. concurrent background operation.
  46.  
  47. _DOS Function Calls for Device Drivers_
  48.  
  49.      In order to keep things simple, I only used the "extended file
  50. management" (version 2.0) functions under system interrupt 21H.  
  51. These are used by the "fread()" and "fwrite()" functions of the C86
  52. C compiler.  The primary functions affecting drivers are:
  53.  
  54.  
  55.        3D Open a file or device, return a 16-bit file handle in
  56.           register AX.
  57.  
  58.        3E Close the file or device associated with the handle in
  59.           BX.
  60.  
  61.        3F Read from a file or device.  The following registers must
  62.           be loaded as indicated:
  63.                BX => file handle of device
  64.                CX => number of bytes to read ( 64K max)
  65.                DX => segment offset of your data buffer storage  
  66.                DS => segment of your data buffer
  67.           Reads CX bytes from a device into the buffer address
  68.           given.  What is not explained by Microsoft is that DOS
  69.           actually requests only one byte per call to the device
  70.           driver, making CX number of calls.  This has all sorts
  71.           of ugly side effects, which will become evident in the
  72.           description of my sample driver.  Microsoft has
  73.           apparently built in the assumption that all character-
  74.           oriented devices talk to fairly slow serial hardware,
  75.           like printers.  If I read this right, DOS has the      
  76.           ability to respond to interrupts between each byte
  77.           transferred, but it does make things clumsy.  DOS
  78.           apparently increments the DS:DX buffer location after
  79.           each call.  The driver will be re-run from START for each
  80.           byte, and must maintain its own data pointers to maintain
  81.           synchronism with the DOS transfer.
  82.  
  83.        40 Write to a file or device.
  84.           Same as above, but data is transferred to your buffer
  85.           area.  Same warning.
  86.  
  87.        44 I/O Control for Devices.  This function has 8 subfunction
  88.           codes which are used for some rudimentary device
  89.           controls. Subfunction 2 is to read CX number of bytes
  90.           from the "control channel" of the device, with the same
  91.           register settings as for the normal read.  The stated
  92.           purpose is to provide a way of reading device driver
  93.           status rather than data from the device itself.  However,
  94.           up to 64K bytes made be transferred per call.  What your
  95.           device does with it is up to you.  Subfunction 3 is the
  96.           corresponding write call.
  97.              For these calls, DOS actually requests CX bytes from
  98.           the  device on one call.  This is used in the second
  99.           version of my sample driver, which is much simpler than
  100.           the standard read-write calls used in the first version.
  101.  
  102. _How DOS Translates Your Read/Write Function Calls into Device
  103. Driver Requests_
  104.  
  105.      When any of the above functions is called by your application
  106. program, DOS develops a data structure called the "Request Header"
  107. by the manual.  This structure consists of a 13-byte defined header
  108. which may be followed by other data bytes depending on the function
  109. requested.  The fixed part of the request header is as follows:
  110.  
  111.      _BYTE_    _PURPOSE_
  112.        0       Length in bytes of the total request header (0-255)
  113.  
  114.        1       Unit code, used to determine subunit to use in block
  115.                devices (not used for character devices).
  116.  
  117.        2       Command code (0-12) to activate specific device
  118.                function.
  119.  
  120.       3-4      Status word, returned by the driver
  121.  
  122.       5-12     The manual states that this area is "reserved for
  123.                DOS".  Another source indicates that this consists
  124.                of two double-word (4-byte) pointers to be used to
  125.                maintain a linked list of request headers for this
  126.                device and a list of all current device requests
  127.                being processed by DOS.  This is apparently in the
  128.                works for a future concurrent-DOS.
  129.  
  130.      The 13 command codes are detailed on pages 14-12 of the
  131. manual; only the following are used by the character devices
  132. explained in this paper:
  133.  
  134.      _CODE_    _FUNCTION_
  135.        0       INIT - perform all initialization required at DOS
  136.                boot time to install the driver and set local driver
  137.                variables.
  138.  
  139.        3       IOCTL INPUT - read a specified number of bytes from
  140.                the device driver's IO control channel.
  141.  
  142.        4       INPUT - normal device "read".  Reads a number of
  143.                bytes from the device your driver is controlling.
  144.  
  145.        8       OUTPUT - normal device "write" call from user
  146.                program.
  147.  
  148.       12       IOCTL OUTPUT - write bytes to driver control
  149.                channel.
  150.  
  151.      For each of these function calls, the driver receives the
  152. following:
  153.  
  154. INIT:     This function must be built into any driver program.  It
  155. is called only by DOS during boot time, to reserve the system
  156. memory needed to hold the driver and to link the driver into the
  157. set of active devices managed by DOS. DOS sends:  13-byte request
  158. header
  159.           BYTE number of units (not used by char devices)
  160.           DWORD ending address of driver
  161.           DWORD pointer to BPB array (not used by char devices)
  162. The driver program must load the ending address at a minimum; any
  163. local initialization may also be performed at this time.
  164.  
  165. INPUT, OUTPUT, IOCTL INPUT, or IOCTL OUTPUT:
  166.      For all of these, DOS sends:
  167.      13-byte request header
  168.      BYTE media descriptor (not used for char devices)
  169.      DWORD offset and segment of the data buffer in calling program
  170.      WORD number of bytes to transfer in this call
  171.      WORD starting sector (not used for char devices)
  172. The driver must perform the requested read or write function, set
  173. the "number of bytes to transfer" location to the number actually
  174. done, and set the status word in the request header to indicate any
  175. errors.
  176.  
  177.      The actual use of these structures will be detailed in the
  178. driver function description.
  179.  
  180. _Required Structure for a Device Driver_
  181.  
  182.      Listing 1 (DOSDEV.ASM) is a template containing the minimum
  183. requirements for a character-oriented device driver.  This is
  184. detailed in pages 14-3 to 14-8 of the DOS manual.      The driver
  185. program must meet the requirements of a normal COM file.  However,
  186. COM files usually start with an ORG 100H to allow room for the DOS
  187. Program Segment Prefix structure.  For a driver, you must use ORG
  188. 0, as the PSP is not used.    The Device Header data structure must
  189. be the first object defined in your file.  It consists of: 
  190.      DWORD     Pointer to the next device driver currently
  191.                installed.     This should be initialized to -1,
  192.                DOS will fill this field as necessary during system
  193.                initialization (boot).
  194.  
  195.      WORD      Device attribute.  I used C000H to indicate that
  196.                this is a character device with IOCTL capability. 
  197.                This field is also used to indicate if this device
  198.                is to be the standard output or input device.
  199.      WORD      Pointer to "device strategy" function in the driver. 
  200.                This function is called whenever a request is made
  201.                to the driver, and must store the location of the
  202.                request header from DOS.
  203.      WORD      Pointer to function which activates driver routines
  204.                to perform the command in the current request
  205.                header.  This is called by DOS after the call to
  206.                the strategy function, and should reset to the
  207.                request header address stored by "strategy", to
  208.                allow for the possibility of interrupts between the
  209.                two calls.
  210.      8-BYTES   Name field.  For character devices, fill this with
  211.                the name which you must use when opening the device.
  212.  
  213.      After this structure, you may include any local data
  214. definitions needed for the internal operation of your driver.  The
  215. DOSDEV example includes only the minimum; a pointer to the request
  216. header and a table of addresses of the functions which will be
  217. called by the command code from DOS.    The function addresses are
  218. arranged according to their calling function code (0 to 12) so that
  219. the function router can use the DOS command code as an offset into
  220. this table.
  221.  
  222. _Required Device Driver Functions_
  223.  
  224.      For simplicity, I will discuss these functions as given in the
  225. DOSDEV.ASM listing.
  226.  
  227.      XDV STRAT:  This function is called directly by DOS when a
  228. request has been made to use this device.  Its only purpose is to
  229. save a segment and offset pointer to the request header.  At the
  230. time DOS calls the device, the segment of the request header is in
  231. register ES and the offset is in register BX.     These values are
  232. copied into the variables RH SEG and RH OFF. The fact that
  233. Microsoft calls this a "device strategy" function leads me to
  234. believe that more complex processing will be required in this
  235. function when DOS becomes multi-user or multi-processing oriented.
  236.  
  237.      XDV FUNC:  This is called by DOS immediately after XDV STRAT. 
  238. The function pushes all machine registers to save the current data
  239. until the device has finished the requested operation.  Data
  240. segment register DS is set to the Code segment value, as all local
  241. variables exist in the code segment.    Registers ES and BX are
  242. loaded from RH SEG and RH OFF to reset them to the start of the DOS
  243. request header.  The command code from the request header (at
  244. ES:[BX+2] ) is then used as an offset into the function address
  245. table FUNTAB to initiate the driver function requested.  In DOSDEV,
  246. only the INIT function has been coded, all others drop out to EXIT
  247. after setting the status word of the request header to "done; no
  248. error".  All you need to do is fill in the function code for any
  249. driver function you intend to use.
  250.  
  251.      INIT:  When DOS is booted, it reads your CONFIG.SYS file to
  252. determine which programs to install as device drivers
  253. (DEVICE=filename.ext).  After loading the file image into memory,
  254. DOS sends a request header with the command code "0" to the device. 
  255. The INIT function must load an offset (at ES:[BX+14]) and segment
  256. value (at ES:[BX+16]) into the request header to indicate the
  257. ending address for the driver program, including space for any
  258. memory used as a virtual device.  The function may also do any
  259. initial variable setting within the driver.  INIT then exits back
  260. to DOS, which uses the address given to set the boundary of DOS
  261. including the new driver storage.
  262.  
  263.      EXIT:  This function restores all machine registers and
  264. returns to DOS.
  265.  
  266. _Examples of Character-Oriented Device Drivers_
  267.  
  268.      Listing 1 (STKDEV.ASM) and 2 (STKDEV2.ASM) show the use of a
  269. virtual device driver to implement a "stack".  User programs may
  270. "push" bytes or entire records by writing them to the device, and
  271. "pop" them with a read request.  I/O control calls are used to set
  272. the record size to be used by the driver.    This may not be very
  273. useful in itself, but this example shows solutions to most of the
  274. problems without being difficult to read.    STKDEV is constructed
  275. in the recommended fashion; I got much of the code from the example
  276. device driver in the DOS manual.  Read and write calls from the
  277. user program activate the functions INPUT and OUTPUT, while IOCTL
  278. IN and IOCTL OUT are used to read and write the record size
  279. setting.  I developed the first version in a few days, but then
  280. spent two months of spare time trying to find out why it wouldn't
  281. work.  It's an undocumented feature of MS-DOS, although I can see
  282. some hints of it in the manual - now that I know what to look for.
  283.      When your user program makes a read or write call to a device,
  284. you send DOS the number of bytes to transfer, which may be 1 to
  285. 64K.  You make one call to DOS (interrupt 21H).   The request
  286. header for I/O contains a full word to contain the byte count sent
  287. from DOS.  I made the mistake of assuming that when I make a 10
  288. byte I/O request to my driver, the driver would see a 10 byte
  289. count.  Actually, it sees 10 unrelated 1-byte requests from DOS.
  290.      STKDEV's INPUT and OUTPUT functions show the effects.  I had
  291. to establish two new variables (NUM2READ and NUM2WRITE) to keep
  292. track of how many bytes had been transferred, so that the driver
  293. would know if it was done with a "record".  This is required
  294. because the "top of stack" pointer (CURRENT) is set to the next
  295. free address following the last byte written.  A "pop" operation
  296. (INPUT) requires decrementing the pointer by the record size,
  297. transferring a full record in the byte order written, then
  298. resetting the pointer back to the used record's start to allow
  299. overwriting and repeated "pops".  There must be an easier way to
  300. do this, but I think this mess shows the problems more clearly.
  301. STKDEV2 uses IOCTL functions rather than the standard I/O.  On
  302. IOCTL calls, DOS sends the full byte count in the request header. 
  303. This made things simpler in my driver code, but complicated my user
  304. programs by requiring custom read/write functions.  Take your
  305. choice.   DOS apparently starts at the buffer address which your
  306. program supplies in the I/O call, transferring one byte with a
  307. request to the specified driver, then incrementing the buffer
  308. pointer by 1, and repeating until the specified number of bytes is
  309. copied.   The device driver must track this indexing carefully in
  310. some applications, for others it may not matter.
  311.  
  312.      Mike Higgins' article included many debugging tips.  One of
  313. them is the "yell" macro at the beginning of STKDEV.  This displays
  314. one character on the screen by writing directly to the video
  315. memory.  If you use DOS function calls to display the status of
  316. your driver, DOS will overwrite the request header which your
  317. driver has started processing.  I have left "yell" invocations
  318. throughout the function code; it was this macro that finally showed
  319. me what my device was receiving from DOS.
  320.  
  321. Functional Description of STKDEV.ASM:
  322.  
  323.      The procedure and device name is XSTK, which must be used when
  324. opening the device for I/O.  It is assembled and linked normally,
  325. then use EXE2BIN to convert the EXE file to COM form.  I used
  326. EXE2BIN STKDEV.EXE XSTK.SYS, changing the file name because any
  327. references to XSTK once it is installed cause weirdness.  The
  328. CONFIG.SYS file must contain DEVICE=XSTK.SYS.  Reboot and XSTK has
  329. added 32K+ to memory-resident DOS.
  330.  
  331. XSTK STRAT:
  332.      This is the "device strategy" function, which is called first
  333. by DOS for every request header.   DOS has set ES and BX to the
  334. address of the request header; these are stored RH SEG and RH OFF
  335. to ensure that the driver will be able to find the request header. 
  336. It may be omitted for DOS 2.0.
  337.  
  338. XSTK FUNC:
  339.      DOS calls this entry point second on all device requests.  The
  340. call is a signal to begin processing the data in the request
  341. header.  DOS is now suspended (in this version) until your device
  342. returns to it. All machine registers are saved on the stack, and
  343. ES and BX are reloaded to the address stored by XSTK STRAT.  The
  344. command code at ES:[BX+2] is used as an index to jump to the
  345. requested function.
  346.  
  347. INIT:
  348.      This function is called only by DOS, only during installation
  349. (boot) time.  As it will not be needed while the device is
  350. operating, INIT could be located after the end address returned to
  351. DOS, saving some memory.      STORAGE is the variable marking the
  352. end of the XSTK code.  However, I add 32K for stack storage.  This
  353. value is then copied to the request header and returned to DOS for
  354. memory allocation.  Local variables are set to default "stack
  355. empty" values.
  356.  
  357. IOCTL IN:
  358.      Used to read the current record size from the driver into the
  359. calling program's data buffer.     In order to use the REP MOVSB
  360. instruction, CX is set to the requested byte count, DS and SI point
  361. to the internal variable RECSIZE, ES and DI point to the buffer
  362. address contained in the request header.  SI and DI are incremented
  363. by the REPeat prefix until CX bytes have been transferred.  ES and
  364. BX are then reset to the request header address.
  365.  
  366. IOCTL OUT:
  367.      Write a new record size to the device.  Same deal as above,
  368. backward.
  369.  
  370. INPUT:
  371.      Processes read requests.  The double word at ES:[BX+14]
  372. contains the address of the data buffer in the calling program, and
  373. the word at ES:[BX+18] is the byte count for the request.  This
  374. value will always be 1 for DOS 2.0, but this is subject to change.
  375.      The first process required is to check whether the previous
  376. write (OUTPUT) completed storing RECSIZE bytes.  This is done by
  377. checking the NUM2WRITE variable.  If NUM2WRITE is not 0, the
  378. CURRENT pointer is set to a record boundary before reading.      
  379. Next, INPUT checks to see if it is in the process of reading a
  380. record or if it is starting a new record.  If NUM2READ is 0, INPUT
  381. must reset CURRENT to the start of the last record written.  At
  382. this time, INPUT checks CURRENT against BOTMEM to ensure that reads
  383. will not go past the bottom of the stack space.  I tried to return
  384. an error code of 30H, to give my calling program a different error
  385. than those used by DOS.  However, DOS apparently checks this value
  386. against the approved list, and I get a "Disk drive error" on the
  387. display. So, it appears that only the given error codes will be
  388. sent to calling programs.     The actual read transfer is set up
  389. at PULLIT.  Again, I used the REP MOVSB instruction, even though
  390. DOS will only call for one byte per request header.  CX is loaded
  391. with the count from the request header, DS and SI have been set to
  392. the proper address in the stack storage, ES and DI are set to the
  393. data buffer address of the calling program.  NUM2READ is
  394. decremented on each request.  While NUM2READ is not 0, the value
  395. of SI is stored in CURRENT; SI has been incremented by the REP
  396. MOVSB to point at the next byte of the record.  If NUM2READ is 0,
  397. a full record has been read and CURRENT must be reset to the
  398. starting address of the record.    Finally, ES and BX are reset to
  399. point to the DOS request header.  The status word of the request
  400. header is filled with the code for "done; no error" and the process
  401. completes through EXIT.
  402.  
  403. OUTPUT:
  404.      Similar to INPUT, except that CURRENT always increments.
  405.  
  406.  
  407. Description of STKDEV2:
  408.  
  409.      This version reverses the use of IOCTL and INPUT/OUTPUT.  Most
  410. of the code is the same as STKDEV, but the variables NUM2READ and
  411. NUM2WRITE are no longer needed, as the DOS request header will
  412. request the actual number of bytes given by the calling program. 
  413. THerefore, the driver implicitly knows that each request will
  414. consist of a complete record.  If you compare IOCTL IN with INPUT,
  415. and IOCTL OUT with OUTPUT of STKDEV, it is obvious that this
  416. approach was easier to code for this application.
  417.  
  418.  
  419. _Testing the Sample Device Drivers_
  420.  
  421.      Listing 4 (TXSTK.C) is a C program to perform simple test
  422. calls to STKDEV.  Listing 5 (TXSTK2.C) tests STKDEV2 by using IOCTL
  423. calls in place of the read/write system calls used in TXSTK.
  424.  
  425.      TXSTK uses segread() to determine the DS segment value of
  426. itself.  This is used to pass DOS the segment value of the data
  427. buffer "instr".  Then XSTK is opened.  I used sysint21() calls
  428. instead of fopen, fwrite() and fread() just to simplify matters. 
  429.      "Outstr" is then written to XSTK.  Although I fill callregs.cx
  430. with the count of 5, I know that DOS will make 5 one-byte calls. 
  431. XSTK is currently set to the default RECSIZE of one, so a write and
  432. corresponding read produces "olleH" from my string "Hello" written.
  433.      I then use IOCTL calls to reset XSTK's RECSIZE to 5 bytes. 
  434. Although the following write and read are in the same form as
  435. before, XSTK now knows to treat input as 5-byte records.  So,
  436. "Hello" returns "Hello".
  437.  
  438.      TXSTK2 is the same through opening XSTK.  However, the first
  439. byte-at-a-time write/read must use a loop to cycle through the 5
  440. characters of the output and input strings.  After resetting XSTK's
  441. RECSIZE to 5 using I/O calls, the write/read calls request 5 bytes,
  442. which is now processed in one call to XSTK.  The strings are
  443. returned as with TXSTK; 1"olleH" and "Hello".