home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OS/2 Professional
/
OS2PRO194.ISO
/
os2
/
prgramer
/
demodd
/
demodd.doc
< prev
next >
Wrap
Text File
|
1991-02-02
|
27KB
|
490 lines
1.0 What this is about
------------------------------------
This document describes how to write OS/2 V1.X device drivers in C. While they
do require a small amount of assembler code, the majority of the device driver
can be coded in C. Only the interfaces to OS/2 and the functions that need
to access the CPU directly need be written in assembler.
When writing device drivers in assembler, the programmer has complete control
over his code. He can control the order of segments, what goes in which
segment, and in what order. He can pass parameters using any mechanism he
wishes. He can control exactly what gets linked in and what doesn't. A C
programmer, however, does not have quite that level of control over his final
executable. The compiler has made many decisions for him, or provides fairly
remote control of these parameters. The problem is how to regain the control
that the assembler programmer enjoys, yet keep the efficiencies afforded by
programming in a high level language. This document describes how to gain
that control.
Also included in this package is a sample device driver with source code. The
sample does not do anything useful, except display an installation message and
install. The only commands it accepts are INIT, OPEN, CLOSE, WRITE, OUTPUT
FLUSH, IOCTL, and DEINSTALL. On all of these, except INIT, it does nothing,
but return SUCCESS. INIT installs the device driver. It does demonstrate the
techniques described here. Anyone receiving this package is free to use the
source code in any way they see fit.
The text will reference the sample code as part of the explanation of the
techniques.
1.1 Scope
------------------------------------
This document assumes the reader is familiar with the organization and design
of OS/2 device drivers. It does not attempt to teach how to write device
drivers, but how to implement them in C. It also assumes a fairly solid
background in Intel 80286 and 80386 CPU architecture and assembler language
programming. Finally, it assumes that the reader understands the use of
Microsoft programming tools - LINK, LIB, and MAKE, and IBM's C/2 compiler.
1.2 Tools used
------------------------------------
The tools used to build the sample code are:
IBM C/2 V1.1
IBM MAKE V2.0
OS/2 Library Manager/2 V1.01
OS/2 Linker/2 V1.20
Microsoft MASM V5.1
The techniques have been used with Microsoft C6.0, this author has not used
them.
2.0 Problems to be solved
------------------------------------
Segment Control - How do you control the order of segments in the final
executable? How you do control which segments get grouped together, and
in what order? How do you control which segments are kept loaded after
INIT time? How do you control which segment library functions end up in?
How do you make sure the first thing in the Data Segment is the Device
Driver header?
Compiler and Library Control - How do you keep the compiler startup code from
being linked into the device driver? How do you make sure stack probes are
disabled in library functions? What library functions can you use and why?
What compiler memory models do you use and why? How do you guarantee access
to variables?
Passing Parameters - How do you get the pointer to the request packet into a
C variable? How do you load registers when calling the DevHelp facility?
DOS Box Memory Usage - How do you minimize the amount of memory the device
driver consumes of the Real Mode DOS Compatabilty session? How do you
gain access to memory above the 1Meg line when entered in Real Mode?
3.0 Segment Control
------------------------------------
Unlike programs, where you can organize your segments anyway you see fit, OS/2
requires a specific organization for its device drivers. The first segment
in the executable (.SYS), must the main DATA segment. The second segment must
be the main CODE segment - the one that contains all the device driver's entry
points. All other segments, both CODE and DATA, will be loaded at INIT time,
but they will be discarded after INIT is complete unless you have marked them
as having IOPL privilege at LINK time and LOCKed them down at INIT time.
Finally, the first thing in the main DATA segment must be the device driver
header, which points to the strategy entry point, defines the name and
characteristics of the device driver and has a pointer to the next device
driver in the chain.
The Demo Device driver uses 5 methods, in concert, to control segment
organization and placement. These are:
1. The MASM .SEQ directive
2. The MASM GROUP directive
3. Knowledge of the compiler's naming conventions
4. The SEGMENTS statement in the LINKer's definition (.DEF) file
5. The /NT option of the compiler to name the code segment created
The .SEQ directive tells the linker to place the segments in the executable in
the order encountered. This allows, along with knowledge of the compiler's
naming conventions, allows us to place the main DATA segment before the main
CODE segment. We do this by defining empty segments with the same name and
class as the segments defined by the compiler. Combine this with the MASM
GROUP directive and we now have our logical segments combined into physical
segments in the order we want, and the physical segments placed in the
executable in the order we need. The use of this method can be seen in
DEMO.ASM.
We use the same idea for the CODE segments, but with a twist. We are not
constrained to using the compiler's default segment names. The /NT option
when invoking the compiler allows us to give the CODE segmment generated any
name we wish. The names you will see in the sample code are: MAINSEG, INITSEG,
and KEEPSEG. Their purpose is obvious. This step is seen in DEMO.MAK, in
the definitions of the optXXXX variables.
The next step to segment control is done by using the SEGMENTS statment in the
definition file (.DEF) of the linker. The logical segment names are placed in
the order we want them to appear. It is optional whether you wish to specify
IOPL for the main DATA and CODE segments, but required for any others you wish
to remain accessable after INIT time. The ordering must match that
encountered by the .SEQ statment, or extra segments with the same name and
class will be generated and you will have no control over which of the 2
segments stuff will go into. This is seen in the file DEMO.DEF.
One final step is needed. At INIT time, a device driver is required to tell
OS/2 how much memory the CODE and DATA segments require after INIT is done.
Essentially, this means finding the offset of the last instruction in the
CODE segment and the offset of the last byte used in the DATA segment. It
is easier to just create a new segment with a single global variable and force
this logical segment to be the last in the GROUP that comprises the physical
segment. It is a simple matter, then, at INIT time to get the offset of these
variables and store them in the INIT request packet on returning to OS/2.
This is seen in the files DEMO.ASM, for the definition of these segments -
LAST_D and END_TEXT. The loading of the offsets into the request packet is
done (in C) in INIT.C.
4.0 Compiler and Library Control
------------------------------------
Other problems with using a compiled language compared to assembler are that
you have less control over the 'memory model' used. Essentially, this means
what kind of pointers - NEAR or FAR are used as well as the way functions are
called and how they return - again NEAR or FAR. You also have less control
over the inclusion of startup code and which segment holds the library
functions linked in.
The first thing is to decide on memory model. My feeling is that LARGE is the
way to go. This lets you place your functions in any segment that is
convenient, or that makes sense for your design, and be able to reach them
from any other segment. This means that functions that are used ONLY at
INIT time can reach utility functions that are used later. It means that
library functions can be placed in a segment of their own, and called by any
functions.
The LARGE model also implies FAR data pointers. This is almost
a given. All pointers passed by OS/2 to the device driver are FAR pointers.
Most of the pointers you will construct will be FAR pointers. To handle these,
you need the LARGE or COMPACT model.
Next, there is the question of Stack Segment and Data Segment. Most modern
compilers generate code that assumes SS equals DS. In device drivers, this is
emphatically false. Fortunately, the Microsoft family of C compilers has an
option that lets the programmer control whether the code generated makes this
assumption or not. Just say no. These compilers also have an option that lets
the programmer control whethere DS will be loaded at the entrance to each
function or not. Since OS/2 loads DS, SS and CS before entereing the device
driver, just say no to this one as well.
The net of this is that the memory model to be used is LARGE code, FAR pointers
and don't assume DS=SS. For IBM C/2, that translates to the option /Alfw.
This can be seen in the optXXXX definitions in DEMO.MAK.
Now comes the problem of startup code. We don't want any. It turns out that
Microsoft compilers have and external variable (__arctused) that controls the
inclusion of startup code. The variable is not used for anything, it is just
defined as external in all modules, and it is defined in the startup code.
this is sufficient to cause its inclusion. If you define this variable in
your main data segment, no startup code will be linked in. This can be seen
in DEMO.ASM.
Next is the problem of stack probes. Microsoft compilers allow you to turn
off stack probes in your code and it needs to be done. OS/2 controls your
stack size, not you. The problem is what about library functions that were
compiled with the stack probes turned on. To solve this problem, we replace
the stack probe function with a NULL function. Unfortunately, Microsoft
included the adjustment of the stack pointer for local variables in the same
function. To solve this problem, we need to write our own stack check
function that does the variable allocation, but doesn't do the stack pointer
comparisons. This function is __chkstk, in DEMO.ASM. The number of bytes
to reserve for local variables is passed in AX, and a FAR call to made to
__chkstk. This function pops the return address, adjusts the stack pointer
for the local variables and the returns. It needs to use a bit of trickery
in that adjusting the stack pointer causes the return address to be lost, so
it save the address and pushes it back on the stack after the adjustment is
made.
Now we come to variable access. Microsoft compilers put STATIC variables in
different segments depending on whether they are initialized or not, when
ising the LARGE model. They then store the segment of that variable in a
static variable called $T2000n, where n is different for each static variable
being referenced. This causes untold problems in accessing the variable
because the segment pointed to by $T2000n is valid at INIT time when the
device driver is at ring 3, but it is no longer valid later, when the device
driver is at ring 0. To solve this problem, we force all the logical data
segments into one physical segment using the techniques described above. Then,
we tell the compiler to use DS to access the variable with the near keyword.
In general, all global and static variables should be defined with this
keyword. A simple hint - if you look into the .COD listing and see a $T2000n
variable defined, you didn't get what you wanted and need to put a near on
a variable. A classic example of the use of the near keayword in this respect
can be seen in LOCK.C.
Finally, we come to the question of library functions. Which ones can and
cannot be used? The general answer is that you cannot use those that will
cause a call to the OS or will use the coprocessor. These include all the
floating point operations, file I/O, memory management functions, process
control and time functions. What remains are things like string and buffer
management, searching and sorting, character classification and some data
conversion (atoi(), for instance). Fortunately, the kinds of things
forbidden are either not the kinds of things done by device drivers or are
provided by the DevHelp functions.
5.0 Passing Parameters
------------------------------------
OS/2 uses registers to pass parameters back and forth with device drivers, but
Microsoft compilers use the stack for most of these. To solve this problem,
we need a layer of assembler between the device driver and OS/2. All entry
points are in assembler. All they do is push their parameters on the stack,
making a stack based parameter, and call the C code. The C code returns its
results (usually an integer, in AX), and the assembler converts it to a form
suitable to OS/2 (a particular flag set or cleared, for instance). This can
be seen in DEMO.ASM, in the form of the procedure _strategy. It pushes the
request packet pointer onto the stack and calls the real strategy function
written in C.
Calling the DevHelp facility is a bit different. One method is based on the
methods used for caling DOS in DOS based compilers. A structure is defined
that holds all the values that will be loaded into the registers. This
structure is loaded with the necessary values and a call made to an assembler
function that loads the registers from the strcture and then calls DevHelp.
On return, it stores the register values back into the structure, allowing
the C code to examine the results.
There are at least 2 problems with this method that need to be overcome.
One is that not all values are allowed to be loaded into segment registers.
Put the wrong value in and you will get a TRAP 000D and the system will lock
up. The only recourse is to power off. This is not exactly user friendly.
Therefore, either a way must be devides to tell the assember function to not
load a segment value from the structure, or the structure must always have
valid values in the segment register fields. 0 is always a valid value to
load into a segment register. It cannot be used, but it can be loaded. The
demo device driver uses the first method. It is in DEVHLP.ASM.
The second problem is that some DevHelp functions use DS to hold a selector
and others use ES. How can you make a general DevHelp caller when it has to
use a segment register to point to the DevHelp entry point to make the
indirect call? The solution is to use CS. This means that at INIT time,
the DevHelp function needs to create an alias to the CODE segment that is
writeable. To do this, it needs the DevHelp functions. The classic Catch-22.
The solution to this is to create 2 new DevHelp functions - one that uses
DS to call DevHelp and another that uses ES. These live in the INIT segment
and are discarded after INIT time. These functions are also in DEVHLP.ASM.
the C code to store the DevHelp entry point is in INIT.C.
6.0 DOS Box memory Usage
------------------------------------
The final problem addressed by this document is reducing the amount fo memory
used by the device driver in the DOS Compatability Session (aka the DOS Box).
OS/2 loads the main CODE and DATA segments into memory below the 1Meg line,
using an already scarce resource - the amount of memory available for DOS
programs. The key to solving this problem is the fact that all other segments
are loaded ABOVE the 1Meg line. What this means is that you put only those
functions that absolutely need to be there in the main CODE segment. These
include the DevHelp caller and all assembler entry points and the main
strategy function. All other code can be moved to above the 1Meg line.
Moving the DATA segment is a little trickier. The problem is that there is
no way to easily define a second DATA segment at compile/link time. It needs
to be dynamically allocated. All memory allocated via the AllocPhys DevHelp
call comes from above the 1Meg line. All of your system level global variables
can be allocated and initialized at INIT time and accessed by pointer later.
This lets you reduce the amount of DOS Box memory consumed to around 1K or so.
The final problem with moving these memory blocks above the 1Meg line is that
when in REAL mode, the CPU cannot access memory above the 1Meg line. How can
it get to the functions and data there? The solution to this is don't run in
REAL mode. At every entry point - there are only 5 possible, Strategy, Timer,
Interrupt, Notify, and IDC - check to see if the CPU is in REAL mode. If it
is, make a call to the ReadToProt DevHelp function, and note in a local
variable that the mode change was made. On exit, if the mode was changed,
go back via ProtToReal.
Note that this path may be seldom taken, depending on the number of interrupts
taken and how much time the CPU spends in the DOS box. The chances of hitting
it are reduced as OS/2 switches the CPU to PROT mode when entering the Strategy
function and Notify cannot be called fro a DOS program. This leaves the Timer
and Interrupt and IDC calls as the only ones to worry about. Since OS/2 is
always timeslicing into PROT mode, even when the DOS box is the active session,
the chances are reduced that the CPU will be in REAL mode when one of these
entry points are activated. Regardless, that possibility needs to be accounted
for.
An example of this method can be seen in STRATEGY.C. This call is not needed
here, but since the sample device driver has no other entry point, the
mechanism is shown here.
7.0 Some other ideas
------------------------------------
There are other ways to call DevHelp. Some people write a separate assembler
level function for each DevHelp call. Others pass the DevHelp entry point
on the stack instead of storing it in the CODE segment. Some write several
generic DevHelper callers. The point is that there are lots of solutions to
this problem. You need to make sure that the one you use is suitable to your
needs. You also need to make sure that they always call OS/2 from the main
CODE segment as any function that registers an entry point will use that CODE
segment as the selector portion of the entry point.
Another thing to watch out for is changing from REAL to PROT mode or when
going back. After the mode change has been made, the only valid address
on the stack is the return address. That is why those functions do not use
the normal DevHelper call in the sample code. They can be found in
ASMUTILS.ASM.
It is useful to have the compiler generate combined listings - that is, the
listing should contain the ASM listing along with the C code. The option
to make Microsoft compilers do this is /Fc. Another thing should be to tell
the compiler to pack data structures. OS/2 request packets have no extra
space in them. Another useful item is the MAP file. It will help you make
sure what is being generated is what you really want and it will help when
using some debuggers.
8.0 Putting it Together
------------------------------------
This section explains the sample device driver included in this package, in
light of the above discussion.
Put
DEVICE=<path>\DEMO.SYS
in your CONFIG.SYS and reboot. You will see the loading message come up.
After re-booting is done, you can copy files to the $DEMO and the system will
say all went well. If, however, you say copy $demo to some file, you'll get
an error saying that $DEMO doesn't like that command. You are seeing the
difference between READ and WRITE. READ is not recognized, while WRITE is
answered OK, we did it.
8.1 Directory Structure and the Make File
------------------------------------
The demo can be compiled by running MAKE against DEMO.MAK. This make file
will generate the entire device driver, the MAP, and all listings or .COD
files. It creates a library with all the .OBJ files stored in it, as well
as a MSG file used by INIT to display the loading commercial.
Currently, it stores the .LST and .COD files in a separate subdirectory off
the current one, the .OBJ in another, and all messages from the compile and
masm and link in a third. These directories are defined in the variables
lst, obj and msg, defined at the beginning of the make file. Note that they
use . as the current directory, making them relative references, not absolute.
Other variables are defined to tell MAKE where the library file is (and should
go) and where the source and include files can be found. Also note that the
.ARF file has a reference to both the.\obj and .\lst subdirectories. If you
change the .MAK file, you need to change this too.
There are 4 variables defined that put all the compiler options in one semi-
user friendly block. The first is OPTMAIN. This will cause the code from
this file to be included in the MAIN code segment (named MAINSEG). The second,
OPTSIZE, does the same, except it has size optimization turned on instead of
none like the others. OPTKEEP causes the code to go into the KEEPSEG segment
and OPTINIT puts it into the INITSEG segment (surprize!).
Each set of options has the link disabled (/c), stack probes disabled (/Gs),
structure packing (/Zp), warning level 3 (/W3), and the model as described
above (/Alfw). It causes a .COD listing to be generated (/Fc), puts the .OBJ
file in a specific subdirectory (/Fo), renames the code segment generated
(/NT) and redirects the error messages to the msg subdirectory.
The rest of the MAKE file is standard stuff. Each separate file is given
a separate set of dependencies so I can control the options on each. They
then are added to the library.
8.2 Assembler files
------------------------------------
There are 4 assembler files:
ASMUTILS.ASM
BRKPOINT.ASM
DEMO.ASM
DEVHLP.ASM
These contain the entry points for the device driver, the DevHelp caller, the
segment controlling stuff described in Section 3.0, and a bunch of utility
functions.
ASMUTILS.ASM has a bunch of utility functions. Not all are used by the DEMO
device driver, but they are useful as examples of the kinds of things you
resort to assembler for. Included are the calls to DevHelp to change the CPU
to REAL mode a back.
BRKPOINT.ASM holds the code to do an INT3 on demand. This function causes
a breakpoint in many debuggers. When a call to this function is placed in
INIT code, you can trace your initialization code. I also make it a habit
of createing an IOCTL call that invokes this function. That lets me get into
my device driver with a debugger without recompiling. This is a great boon
when you want to debug a particular version.
DEMO.ASM is the main assembler code file. It holds the strategy entry point,
it does all the segment and group definition stuff described in Section 3, and
it holds the main DATA segment, with the device driver header.
DEVHLP.ASM has the DevHelp caller, along with the temporary one used at INIT
time to get the DevHelp entry point stored in the main CODE segment. This
variable is also defined in thid file.
8.3 C files
------------------------------------
There are 8 C files. These are the meat of the function to the demo device
driver. They include a bunch of utility functions as well as the nexeccary
stuff. They are:
INIT.C
PRTMSG.C
STRATEGY.C
BADCMD.C
DDUTILS.C
GDTMEM.C
LDTMEM.C
LOCK.C
INIT.C holds the function to process the INIT command from OS/2. Basically, it
sets up the DevHelp entry point, figures out the name of the message file,
prints the loading message, sets up a pointer to the milliseconds since IPL
timer, tells OS/2 how much code and data to keep loaded and exits.
PRTMSG.C is used for printing messages at INIT time. It won't work at any
other time as it uses DOSGetMessage and DOSPutMessage.
STRATEGY.C is the main C function that figures out what OS/2 want's it to do
and calls the proper function to do it. It also makes sure that the CPU is
in PROT mode before proceeding. As explained above, this is unnecessary, but
it is instructive. Most of the functions just set the status to command not
recognized. Others say 'Yes, we did it', when they really didn't. INIT is
the only command that really calls another function. After we get past all
this, dev_done() is called (except for INIT, when it isn't valid), to set the
request packet status.
BAD_CMD.C just returns the value needed to set into the request packet status
to reflect that fact that we don't recognize the command.
The rest of the files hold all sorts of utility functions. There is stuff to
allocate and free memory, allocate GDT slots, Lock and unlock segments, block
a task and yield the CPU temporarily, and do all sorts of other things. Some
of these are used by the demo device driver, others are included for
instructional purposes (actually, I was too lazy to take them out).
8.4 H files
------------------------------------
There are 4 include files. Three that do the actual work anad a fourth to
gather them all together in one include line. The 3 are broken into constant
definitions, structure definitions and function prototypes.
8.5 Other files
------------------------------------
DEMO.ARF - Automatic Response file for the link stage. Note that this file
assumes that the map file is to go to the .\lst subidrectory and
the main .obj file comes from .\obj. If you change the subdir
structure, be sure to change these as well.
DEMO.DEF - Definition file for the link stage. Here is where you set the IOPL
bit on for a segment.
DEMO.TXT - Source for the message file.
DEMO.LIB - Library file made of all the .OBJs
DEMO.MSG - The message file. This is where the text for messages displayed
during INIT time are kept.
DEMO.SYS - The device driver (ta da!)
9.0 Who am I?
------------------------------------
My name is Dennis Rowe. I live in Lafayette, Colorado, an outlying suburb of
Denver. I work for IBM, developing products that use OS/2 as a base. I don't
work on OS/2 itself, I just use it like other developers. I've been doing
device driver work since about mid 1988. I have written 2 fairly large ones
of greater than 15K lines of code.