home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / devddemo.zip / DEMODD.DOC < prev    next >
Text File  |  1991-02-02  |  27KB  |  490 lines

  1. 1.0 What this is about
  2. ------------------------------------
  3. This document describes how to write OS/2 V1.X device drivers in C.  While they
  4. do require a small amount of assembler code, the majority of the device driver
  5. can be coded in C.  Only the interfaces to OS/2 and the functions that need
  6. to access the CPU directly need be written in assembler.
  7.  
  8. When writing device drivers in assembler, the programmer has complete control
  9. over his code.  He can control the order of segments, what goes in which
  10. segment, and in what order.  He can pass parameters using any mechanism he
  11. wishes.  He can control exactly what gets linked in and what doesn't.  A C
  12. programmer, however, does not have quite that level of control over his final
  13. executable.  The compiler has made many decisions for him, or provides fairly
  14. remote control of these parameters.  The problem is how to regain the control
  15. that the assembler programmer enjoys, yet keep the efficiencies afforded by
  16. programming in a high level language.  This document describes how to gain
  17. that control.
  18.  
  19. Also included in this package is a sample device driver with source code.  The
  20. sample does not do anything useful, except display an installation message and
  21. install.  The only commands it accepts are INIT, OPEN, CLOSE, WRITE, OUTPUT
  22. FLUSH, IOCTL, and DEINSTALL.  On all of these, except INIT, it does nothing,
  23. but return SUCCESS.  INIT installs the device driver.  It does demonstrate the
  24. techniques described here.  Anyone receiving this package is free to use the
  25. source code in any way they see fit.
  26.  
  27. The text will reference the sample code as part of the explanation of the
  28. techniques.
  29.  
  30.  
  31. 1.1 Scope
  32. ------------------------------------
  33. This document assumes the reader is familiar with the organization and design
  34. of OS/2 device drivers.  It does not attempt to teach how to write device
  35. drivers, but how to implement them in C.  It also assumes a fairly solid
  36. background in Intel 80286 and 80386 CPU architecture and assembler language
  37. programming.  Finally, it assumes that the reader understands the use of
  38. Microsoft programming tools - LINK, LIB, and MAKE, and IBM's C/2 compiler.
  39.  
  40.  
  41. 1.2 Tools used
  42. ------------------------------------
  43. The tools used to build the sample code are:
  44.    IBM C/2  V1.1
  45.    IBM MAKE V2.0
  46.    OS/2 Library Manager/2 V1.01
  47.    OS/2 Linker/2 V1.20
  48.    Microsoft MASM V5.1
  49.  
  50. The techniques have been used with Microsoft C6.0, this author has not used
  51. them.
  52.  
  53.  
  54. 2.0 Problems to be solved
  55. ------------------------------------
  56.  
  57. Segment Control - How do you control the order of segments in the final
  58.   executable?  How you do control which segments get grouped together, and
  59.   in what order?  How do you control which segments are kept loaded after
  60.   INIT time?  How do you control which segment library functions end up in?
  61.   How do you make sure the first thing in the Data Segment is the Device
  62.   Driver header?
  63.  
  64. Compiler and Library Control - How do you keep the compiler startup code from
  65.   being linked into the device driver?  How do you make sure stack probes are
  66.   disabled in library functions?  What library functions can you use and why?
  67.   What compiler memory models do you use and why?  How do you guarantee access
  68.   to variables?
  69.  
  70. Passing Parameters - How do you get the pointer to the request packet into a
  71.   C variable?  How do you load registers when calling the DevHelp facility?
  72.  
  73. DOS Box Memory Usage - How do you minimize the amount of memory the device
  74.   driver consumes of the Real Mode DOS Compatabilty session?  How do you
  75.   gain access to memory above the 1Meg line when entered in Real Mode?
  76.  
  77.  
  78. 3.0 Segment Control
  79. ------------------------------------
  80. Unlike programs, where you can organize your segments anyway you see fit, OS/2
  81. requires a specific organization for its device drivers.  The first segment
  82. in the executable (.SYS), must the main DATA segment.  The second segment must
  83. be the main CODE segment - the one that contains all the device driver's entry
  84. points.  All other segments, both CODE and DATA, will be loaded at INIT time,
  85. but they will be discarded after INIT is complete unless you have marked them
  86. as having IOPL privilege at LINK time and LOCKed them down at INIT time.
  87. Finally, the first thing in the main DATA segment must be the device driver
  88. header, which points to the strategy entry point, defines the name and
  89. characteristics of the device driver and has a pointer to the next device
  90. driver in the chain.
  91.  
  92. The Demo Device driver uses 5 methods, in concert, to control segment
  93. organization and placement.  These are:
  94. 1. The MASM .SEQ directive
  95. 2. The MASM GROUP directive
  96. 3. Knowledge of the compiler's naming conventions
  97. 4. The SEGMENTS statement in the LINKer's definition (.DEF) file
  98. 5. The /NT option of the compiler to name the code segment created
  99.  
  100. The .SEQ directive tells the linker to place the segments in the executable in
  101. the order encountered.  This allows, along with knowledge of the compiler's
  102. naming conventions, allows us to place the main DATA segment before the main
  103. CODE segment.  We do this by defining empty segments with the same name and
  104. class as the segments defined by the compiler.  Combine this with the MASM
  105. GROUP directive and we now have our logical segments combined into physical
  106. segments in the order we want, and the physical segments placed in the
  107. executable in the order we need.  The use of this method can be seen in
  108. DEMO.ASM.
  109.  
  110. We use the same idea for the CODE segments, but with a twist.  We are not
  111. constrained to using the compiler's default segment names.  The /NT option
  112. when invoking the compiler allows us to give the CODE segmment generated any
  113. name we wish.  The names you will see in the sample code are: MAINSEG, INITSEG,
  114. and KEEPSEG.  Their purpose is obvious.  This step is seen in DEMO.MAK, in
  115. the definitions of the optXXXX variables.
  116.  
  117. The next step to segment control is done by using the SEGMENTS statment in the
  118. definition file (.DEF) of the linker.  The logical segment names are placed in
  119. the order we want them to appear.  It is optional whether you wish to specify
  120. IOPL for the main DATA and CODE segments, but required for any others you wish
  121. to remain accessable after INIT time.  The ordering must match that
  122. encountered by the .SEQ statment, or extra segments with the same name and
  123. class will be generated and you will have no control over which of the 2
  124. segments stuff will go into.  This is seen in the file DEMO.DEF.
  125.  
  126. One final step is needed.  At INIT time, a device driver is required to tell
  127. OS/2 how much memory the CODE and DATA segments require after INIT is done.
  128. Essentially, this means finding the offset of the last instruction in the
  129. CODE segment and the offset of the last byte used in the DATA segment.  It
  130. is easier to just create a new segment with a single global variable and force
  131. this logical segment to be the last in the GROUP that comprises the physical
  132. segment.  It is a simple matter, then, at INIT time to get the offset of these
  133. variables and store them in the INIT request packet on returning to OS/2.
  134. This is seen in the files DEMO.ASM, for the definition of these segments -
  135. LAST_D and END_TEXT.  The loading of the offsets into the request packet is
  136. done (in C) in INIT.C.
  137.  
  138.  
  139. 4.0 Compiler and Library Control
  140. ------------------------------------
  141. Other problems with using a compiled language compared to assembler are that
  142. you have less control over the 'memory model' used.  Essentially, this means
  143. what kind of pointers - NEAR or FAR are used as well as the way functions are
  144. called and how they return - again NEAR or FAR.  You also have less control
  145. over the inclusion of startup code and which segment holds the library
  146. functions linked in.
  147.  
  148. The first thing is to decide on memory model.  My feeling is that LARGE is the
  149. way to go.  This lets you place your functions in any segment that is
  150. convenient, or that makes sense for your design, and be able to reach them
  151. from any other segment.  This means that functions that are used ONLY at
  152. INIT time can reach utility functions that are used later.  It means that
  153. library functions can be placed in a segment of their own, and called by any
  154. functions.
  155.  
  156. The LARGE model also implies FAR data pointers.  This is almost
  157. a given.  All pointers passed by OS/2 to the device driver are FAR pointers.
  158. Most of the pointers you will construct will be FAR pointers.  To handle these,
  159. you need the LARGE or COMPACT model.
  160.  
  161. Next, there is the question of Stack Segment and Data Segment.  Most modern
  162. compilers generate code that assumes SS equals DS.  In device drivers, this is
  163. emphatically false.  Fortunately, the Microsoft family of C compilers has an
  164. option that lets the programmer control whether the code generated makes this
  165. assumption or not.  Just say no.  These compilers also have an option that lets
  166. the programmer control whethere DS will be loaded at the entrance to each
  167. function or not.  Since OS/2 loads DS, SS and CS before entereing the device
  168. driver, just say no to this one as well.
  169.  
  170. The net of this is that the memory model to be used is LARGE code, FAR pointers
  171. and don't assume DS=SS.  For IBM C/2, that translates to the option /Alfw.
  172. This can be seen in the optXXXX definitions in DEMO.MAK.
  173.  
  174. Now comes the problem of startup code.  We don't want any.  It turns out that
  175. Microsoft compilers have and external variable (__arctused) that controls the
  176. inclusion of startup code.  The variable is not used for anything, it is just
  177. defined as external in all modules, and it is defined in the startup code.
  178. this is sufficient to cause its inclusion.  If you define this variable in
  179. your main data segment, no startup code will be linked in.  This can be seen
  180. in DEMO.ASM.
  181.  
  182. Next is the problem of stack probes.  Microsoft compilers allow you to turn
  183. off stack probes in your code and it needs to be done.  OS/2 controls your
  184. stack size, not you.  The problem is what about library functions that were
  185. compiled with the stack probes turned on.  To solve this problem, we replace
  186. the stack probe function with a NULL function.  Unfortunately, Microsoft
  187. included the adjustment of the stack pointer for local variables in the same
  188. function.  To solve this problem, we need to write our own stack check
  189. function that does the variable allocation, but doesn't do the stack pointer
  190. comparisons.  This function is __chkstk, in DEMO.ASM.  The number of bytes
  191. to reserve for local variables is passed in AX, and a FAR call to made to
  192. __chkstk.  This function pops the return address, adjusts the stack pointer
  193. for the local variables and the returns.  It needs to use a bit of trickery
  194. in that adjusting the stack pointer causes the return address to be lost, so
  195. it save the address and pushes it back on the stack after the adjustment is
  196. made.
  197.  
  198. Now we come to variable access.  Microsoft compilers put STATIC variables in
  199. different segments depending on whether they are initialized or not, when
  200. ising the LARGE model.  They then store the segment of that variable in a
  201. static variable called $T2000n, where n is different for each static variable
  202. being referenced.  This causes untold problems in accessing the variable
  203. because the segment pointed to by $T2000n is valid at INIT time when the
  204. device driver is at ring 3, but it is no longer valid later, when the device
  205. driver is at ring 0.  To solve this problem, we force all the logical data
  206. segments into one physical segment using the techniques described above.  Then,
  207. we tell the compiler to use DS to access the variable with the near keyword.
  208. In general, all global and static variables should be defined with this
  209. keyword.  A simple hint - if you look into the .COD listing and see a $T2000n
  210. variable defined, you didn't get what you wanted and need to put a near on
  211. a variable.  A classic example of the use of the near keayword in this respect
  212. can be seen in LOCK.C.
  213.  
  214. Finally, we come to the question of library functions.  Which ones can and
  215. cannot be used?  The general answer is that you cannot use those that will
  216. cause a call to the OS or will use the coprocessor.  These include all the
  217. floating point operations, file I/O, memory management functions, process
  218. control and time functions.  What remains are things like string and buffer
  219. management, searching and sorting, character classification and some data
  220. conversion (atoi(), for instance).  Fortunately, the kinds of things
  221. forbidden are either not the kinds of things done by device drivers or are
  222. provided by the DevHelp functions.
  223.  
  224. 5.0 Passing Parameters
  225. ------------------------------------
  226. OS/2 uses registers to pass parameters back and forth with device drivers, but
  227. Microsoft compilers use the stack for most of these.  To solve this problem,
  228. we need a layer of assembler between the device driver and OS/2.  All entry
  229. points are in assembler.  All they do is push their parameters on the stack,
  230. making a stack based parameter, and call the C code.  The C code returns its
  231. results (usually an integer, in AX), and the assembler converts it to a form
  232. suitable to OS/2 (a particular flag set or cleared, for instance).  This can
  233. be seen in DEMO.ASM, in the form of the procedure _strategy.  It pushes the
  234. request packet pointer onto the stack and calls the real strategy function
  235. written in C.
  236.  
  237. Calling the DevHelp facility is a bit different.  One method is based on the
  238. methods used for caling DOS in DOS based compilers.  A structure is defined
  239. that holds all the values that will be loaded into the registers.  This
  240. structure is loaded with the necessary values and a call made to an assembler
  241. function that loads the registers from the strcture and then calls DevHelp.
  242. On return, it stores the register values back into the structure, allowing
  243. the C code to examine the results.
  244.  
  245. There are at least 2 problems with this method that need to be overcome.
  246. One is that not all values are allowed to be loaded into segment registers.
  247. Put the wrong value in and you will get a TRAP 000D and the system will lock
  248. up.  The only recourse is to power off.  This is not exactly user friendly.
  249. Therefore, either a way must be devides to tell the assember function to not
  250. load a segment value from the structure, or the structure must always have
  251. valid values in the segment register fields.  0 is always a valid value to
  252. load into a segment register.  It cannot be used, but it can be loaded.  The
  253. demo device driver uses the first method.  It is in DEVHLP.ASM.
  254.  
  255. The second problem is that some DevHelp functions use DS to hold a selector
  256. and others use ES.  How can you make a general DevHelp caller when it has to
  257. use a segment register to point to the DevHelp entry point to make the
  258. indirect call?  The solution is to use CS.  This means that at INIT time,
  259. the DevHelp function needs to create an alias to the CODE segment that is
  260. writeable.  To do this, it needs the DevHelp functions.  The classic Catch-22.
  261. The solution to this is to create 2 new DevHelp functions - one that uses
  262. DS to call DevHelp and another that uses ES.  These live in the INIT segment
  263. and are discarded after INIT time.  These functions are also in DEVHLP.ASM.
  264. the C code to store the DevHelp entry point is in INIT.C.
  265.  
  266. 6.0 DOS Box memory Usage
  267. ------------------------------------
  268. The final problem addressed by this document is reducing the amount fo memory
  269. used by the device driver in the DOS Compatability Session (aka the DOS Box).
  270. OS/2 loads the main CODE and DATA segments into memory below the 1Meg line,
  271. using an already scarce resource - the amount of memory available for DOS
  272. programs.  The key to solving this problem is the fact that all other segments
  273. are loaded ABOVE the 1Meg line.  What this means is that you put only those
  274. functions that absolutely need to be there in the main CODE segment.  These
  275. include the DevHelp caller and all assembler entry points and the main
  276. strategy function.  All other code can be moved to above the 1Meg line.
  277.  
  278. Moving the DATA segment is a little trickier.  The problem is that there is
  279. no way to easily define a second DATA segment at compile/link time.  It needs
  280. to be dynamically allocated.  All memory allocated via the AllocPhys DevHelp
  281. call comes from above the 1Meg line.  All of your system level global variables
  282. can be allocated and initialized at INIT time and accessed by pointer later.
  283. This lets you reduce the amount of DOS Box memory consumed to around 1K or so.
  284.  
  285. The final problem with moving these memory blocks above the 1Meg line is that
  286. when in REAL mode, the CPU cannot access memory above the 1Meg line.  How can
  287. it get to the functions and data there?  The solution to this is don't run in
  288. REAL mode.  At every entry point - there are only 5 possible, Strategy, Timer,
  289. Interrupt, Notify, and IDC - check to see if the CPU is in REAL mode.  If it
  290. is, make a call to the ReadToProt DevHelp function, and note in a local
  291. variable that the mode change was made.  On exit, if the mode was changed,
  292. go back via ProtToReal.
  293.  
  294. Note that this path may be seldom taken, depending on the number of interrupts
  295. taken and how much time the CPU spends in the DOS box.  The chances of hitting
  296. it are reduced as OS/2 switches the CPU to PROT mode when entering the Strategy
  297. function and Notify cannot be called fro a DOS program.  This leaves the Timer
  298. and Interrupt and IDC calls as the only ones to worry about.  Since OS/2 is
  299. always timeslicing into PROT mode, even when the DOS box is the active session,
  300. the chances are reduced that the CPU will be in REAL mode when one of these
  301. entry points are activated.  Regardless, that possibility needs to be accounted
  302. for.
  303.  
  304. An example of this method can be seen in STRATEGY.C.  This call is not needed
  305. here, but since the sample device driver has no other entry point, the
  306. mechanism is shown here.
  307.  
  308. 7.0 Some other ideas
  309. ------------------------------------
  310. There are other ways to call DevHelp.  Some people write a separate assembler
  311. level function for each DevHelp call.  Others pass the DevHelp entry point
  312. on the stack instead of storing it in the CODE segment.  Some write several
  313. generic DevHelper callers.  The point is that there are lots of solutions to
  314. this problem.  You need to make sure that the one you use is suitable to your
  315. needs.  You also need to make sure that they always call OS/2 from the main
  316. CODE segment as any function that registers an entry point will use that CODE
  317. segment as the selector portion of the entry point.
  318.  
  319. Another thing to watch out for is changing from REAL to PROT mode or when
  320. going back.  After the mode change has been made, the only valid address
  321. on the stack is the return address.  That is why those functions do not use
  322. the normal DevHelper call in the sample code.  They can be found in
  323. ASMUTILS.ASM.
  324.  
  325. It is useful to have the compiler generate combined listings - that is, the
  326. listing should contain the ASM listing along with the C code.  The option
  327. to make Microsoft compilers do this is /Fc.  Another thing should be to tell
  328. the compiler to pack data structures.  OS/2 request packets have no extra
  329. space in them.  Another useful item is the MAP file.  It will help you make
  330. sure what is being generated is what you really want and it will help when
  331. using some debuggers.
  332.  
  333. 8.0 Putting it Together
  334. ------------------------------------
  335. This section explains the sample device driver included in this package, in
  336. light of the above discussion.
  337.  
  338. Put
  339.  
  340. DEVICE=<path>\DEMO.SYS
  341.  
  342. in your CONFIG.SYS and reboot.  You will see the loading message come up.
  343. After re-booting is done, you can copy files to the $DEMO and the system will
  344. say all went well.  If, however, you say copy $demo to some file, you'll get
  345. an error saying that $DEMO doesn't like that command.  You are seeing the
  346. difference between READ and WRITE.  READ is not recognized, while WRITE is
  347. answered OK, we did it.
  348.  
  349. 8.1 Directory Structure and the Make File
  350. ------------------------------------
  351. The demo can be compiled by running MAKE against DEMO.MAK.  This make file
  352. will generate the entire device driver, the MAP, and all listings or .COD
  353. files.  It creates a library with all the .OBJ files stored in it, as well
  354. as a MSG file used by INIT to display the loading commercial.
  355.  
  356. Currently, it stores the .LST and .COD files in a separate subdirectory off
  357. the current one, the .OBJ in another, and all messages from the compile and
  358. masm and link in a third.  These directories are defined in the variables
  359. lst, obj and msg, defined at the beginning of the make file.  Note that they
  360. use . as the current directory, making them relative references, not absolute.
  361. Other variables are defined to tell MAKE where the library file is (and should
  362. go) and where the source and include files can be found.  Also note that the
  363. .ARF file has a reference to both the.\obj and .\lst subdirectories.  If you
  364. change the .MAK file, you need to change this too.
  365.  
  366. There are 4 variables defined that put all the compiler options in one semi-
  367. user friendly block.  The first is OPTMAIN.  This will cause the code from
  368. this file to be included in the MAIN code segment (named MAINSEG).  The second,
  369. OPTSIZE, does the same, except it has size optimization turned on instead of
  370. none like the others.  OPTKEEP causes the code to go into the KEEPSEG segment
  371. and OPTINIT puts it into the INITSEG segment (surprize!).
  372.  
  373. Each set of options has the link disabled (/c), stack probes disabled (/Gs),
  374. structure packing (/Zp), warning level 3 (/W3), and the model as described
  375. above (/Alfw).  It causes a .COD listing to be generated (/Fc), puts the .OBJ
  376. file in a specific subdirectory (/Fo), renames the code segment generated
  377. (/NT) and redirects the error messages to the msg subdirectory.
  378.  
  379. The rest of the MAKE file is standard stuff.  Each separate file is given
  380. a separate set of dependencies so I can control the options on each.  They
  381. then are added to the library.
  382.  
  383.  
  384. 8.2 Assembler files
  385. ------------------------------------
  386. There are 4 assembler files:
  387.    ASMUTILS.ASM
  388.    BRKPOINT.ASM
  389.    DEMO.ASM
  390.    DEVHLP.ASM
  391.  
  392. These contain the entry points for the device driver, the DevHelp caller, the
  393. segment controlling stuff described in Section 3.0, and a bunch of utility
  394. functions.
  395.  
  396. ASMUTILS.ASM has a bunch of utility functions.  Not all are used by the DEMO
  397. device driver, but they are useful as examples of the kinds of things you
  398. resort to assembler for.  Included are the calls to DevHelp to change the CPU
  399. to REAL mode a back.
  400.  
  401. BRKPOINT.ASM holds the code to do an INT3 on demand.  This function causes
  402. a breakpoint in many debuggers.  When a call to this function is placed in
  403. INIT code, you can trace your initialization code.  I also make it a habit
  404. of createing an IOCTL call that invokes this function.  That lets me get into
  405. my device driver with a debugger without recompiling.  This is a great boon
  406. when you want to debug a particular version.
  407.  
  408. DEMO.ASM is the main assembler code file.  It holds the strategy entry point,
  409. it does all the segment and group definition stuff described in Section 3, and
  410. it holds the main DATA segment, with the device driver header.
  411.  
  412. DEVHLP.ASM has the DevHelp caller, along with the temporary one used at INIT
  413. time to get the DevHelp entry point stored in the main CODE segment.  This
  414. variable is also defined in thid file.
  415.  
  416.  
  417. 8.3 C files
  418. ------------------------------------
  419. There are 8 C files.  These are the meat of the function to the demo device
  420. driver.  They include a bunch of utility functions as well as the nexeccary
  421. stuff.  They are:
  422.    INIT.C
  423.    PRTMSG.C
  424.    STRATEGY.C
  425.    BADCMD.C
  426.    DDUTILS.C
  427.    GDTMEM.C
  428.    LDTMEM.C
  429.    LOCK.C
  430.  
  431. INIT.C holds the function to process the INIT command from OS/2.  Basically, it
  432. sets up the DevHelp entry point, figures out the name of the message file,
  433. prints the loading message, sets up a pointer to the milliseconds since IPL
  434. timer, tells OS/2 how much code and data to keep loaded and exits.
  435.  
  436. PRTMSG.C is used for printing messages at INIT time.  It won't work at any
  437. other time as it uses DOSGetMessage and DOSPutMessage.
  438.  
  439. STRATEGY.C is the main C function that figures out what OS/2 want's it to do
  440. and calls the proper function to do it.  It also makes sure that the CPU is
  441. in PROT mode before proceeding.  As explained above, this is unnecessary, but
  442. it is instructive.  Most of the functions just set the status to command not
  443. recognized.  Others say 'Yes, we did it', when they really didn't.  INIT is
  444. the only command that really calls another function.  After we get past all
  445. this, dev_done() is called (except for INIT, when it isn't valid), to set the
  446. request packet status.
  447.  
  448. BAD_CMD.C just returns the value needed to set into the request packet status
  449. to reflect that fact that we don't recognize the command.
  450.  
  451. The rest of the files hold all sorts of utility functions.  There is stuff to
  452. allocate and free memory, allocate GDT slots, Lock and unlock segments, block
  453. a task and yield the CPU temporarily, and do all sorts of other things.  Some
  454. of these are used by the demo device driver, others are included for
  455. instructional purposes (actually, I was too lazy to take them out).
  456.  
  457. 8.4 H files
  458. ------------------------------------
  459. There are 4 include files.  Three that do the actual work anad a fourth to
  460. gather them all together in one include line.  The 3 are broken into constant
  461. definitions, structure definitions and function prototypes.
  462.  
  463. 8.5 Other files
  464. ------------------------------------
  465. DEMO.ARF - Automatic Response file for the link stage.  Note that this file
  466.            assumes that the map file is to go to the .\lst subidrectory and
  467.            the main .obj file comes from .\obj.  If you change the subdir
  468.            structure, be sure to change these as well.
  469.  
  470. DEMO.DEF - Definition file for the link stage.  Here is where you set the IOPL
  471.            bit on for a segment.
  472.  
  473. DEMO.TXT - Source for the message file.
  474.  
  475. DEMO.LIB - Library file made of all the .OBJs
  476.  
  477. DEMO.MSG - The message file.  This is where the text for messages displayed
  478.            during INIT time are kept.
  479.  
  480. DEMO.SYS - The device driver (ta da!)
  481.  
  482.  
  483. 9.0 Who am I?
  484. ------------------------------------
  485. My name is Dennis Rowe.  I live in Lafayette, Colorado, an outlying suburb of
  486. Denver.  I work for IBM, developing products that use OS/2 as a base.  I don't
  487. work on OS/2 itself, I just use it like other developers.  I've been doing
  488. device driver work since about mid 1988.  I have written 2 fairly large ones
  489. of greater than 15K lines of code.
  490.