Category Archives: driver


A practitioner’s guide to development of an asynchronous RS-232 terminal driver for OS/2 in C

AUTHOR: Steven J. Mastrianni

NOTE: I found this on the author’s site and is a Byte Article from 1991. Reprinted here without permission. M Greene

OS/2 device drivers continue to be a limiting factor in the acceptance and use of OS/2. DOS drivers abound, but OS/2 drivers are scarce as hen’s teeth — for a variety of reasons. OS/2 drivers are more complicated than DOS drivers. They’ve got to handle context switching and priorities and accommodate dual-mode operation (real versus protected) — issues foreign to many DOS programmers. In this article, I’ll describe how to build an asynchronous RS-232 terminal driver for OS/2 in C, complete with interrupt handler and timer support (the code you’ll need to build this driver is available on BIX). Once you’ve seen how that’s done, you’ll have the basic understanding you need to write OS/2 drivers for other types of devices.

The Nature of the Beast

OS/2 device drivers, like other multitasking drivers, shield applications from the physical characteristics of I/O devices (e.g., timing or I/O port addressing). An application in need of I/O service transmits a request to the OS/2 kernel, which in turn calls a driver. The device driver handles all the hardware details, such as register setup, interrupt handling, and error checking. When the request is complete, the device driver massages the data into a format recognizable by the application. It sends the data or a status indication to the application and notifies the kernel that the request is complete. If the request cannot be handled immediately, the driver may either block the requesting thread or return a Request Not Done status to the kernel. Either way, the driver then relinquishes the CPU so that other threads can run.

DOS device drivers do not have a direct OS/2 counterpart. They are simple single-task polling drivers. Even interrupt drivers under DOS poll until interrupt processing is complete. DOS device drivers support one request at a time, and any subsequent requests from the DOS kernel will cause the system to crash.

In contrast, an OS/2 driver must manage overlapping requests from different processes and threads, and it must therefore be reentrant. It must also handle interrupts from the device and interrupts from a timer handler. In addition, the OS/2 driver must oversee switches from protected mode to real mode. It must accomplish these operations in an efficient manner, allowing other threads to gain access to the CPU, and, most important, it must do all these tasks reliably. Because it operates at ring 0, the OS/2 driver is the only program that has access to critical system functions (e.g., the interrupt system and timer). The driver therefore must be a trusted program, because any error in the driver can cause a fatal system crash.

OS/2 device drivers must also be bimodal, which means they must operate in real mode and protected mode. The interrupts must continue to be processed, and the requests must be completed, even if the user switches from the OS/2 prompt to the DOS compatibility box and back. They must be able to deinstall when requested, releasing any memory used by the driver to OS/2. Additionally, OS/2 drivers may support device monitors, programs that monitor data as it is passed to and from the driver. Fortunately, OS/2 offers a wide range of system services called Device Helper routines, or DevHlps, to provide this functionality.

Tools of the Trade

Designing an OS/2 device driver requires a thorough understanding of the role of a device driver, as well as a solid working knowledge of the OS/2 operating system and design philosophy. Debugging OS/2 drivers can be difficult, even with the proper tools. The OS/2 device driver operates at ring 0 with full access to the system hardware. However, it has almost no access to OS/2 support services, except a handful of DevHlp routines. Many driver failures occur in a real-time context, such as in the midst of interrupt handling. It may be difficult or impossible to find a driver problem using normal debugging techniques. In such cases, it is necessary to visualize the operation of the device driver and OS/2 at the time of the error to help locate the problem.

The most important tool for driver development is the driver debugger. Generally, I use the kernel debugger from Microsoft, which comes with the Device Driver Development Toolkit, or DDK. Several other companies offer good driver development tools. A more complete version of this article in book form and a complete C-callable DevHlp library can be purchased from PSS. PentaSoft offers a C-callable interface to the DevHlp routines. OS Technologies offers a driver debugger that is OS/2 version-independent. And FutureWare offers a driver debugger and a C-callable interface to the DevHlp routines.

I write all my device drivers, including the interrupt and timer handlers, in Microsoft C 6.0. A device driver written in C can be written in approximately half the time it would take to write the same driver with the Microsoft Macro Assembler. In special cases, especially when writing drivers for very fast devices or where performance is extremely critical, it only makes sense to write a few subroutines in assembly language. Mostdrivers, however, work fine when written in C.

Anatomy of an OS/2 Device Driver

OS/2 drivers receive requests from the OS/2 kernel. When the driver is originally opened with a DosOpen call, the kernel returns a handle to the program that requested access to the driver. This handle is used for subsequent access to the driver, and the driver name is no longer used (or needed).

When an application makes a call to a driver, the kernel intercepts the call and formats the driver request in a standard driver data structure, called the request packet. The request packet contains the data and pointers that the driver uses to honor the request. In the case of a DosRead or DosWrite, for example, the request packet contains the physical address of the caller’s buffer. In the case of an I/O control operation (IOCtl), the request packet contains the virtual address of a data and parameter buffer. Depending on the request, the data in the request packet will change, but the length and format of the request packet’s header remain constant. The kernel passes the driver a bimodal pointer to the request packet. This bimodal, or tiled, address is a pointer valid in either protected mode
or real mode, because the processor may be in either mode when the driver is called.

How does the kernel know which driver to send the request to? Drivers are loaded by the OS/2 initialization code at boot time, and the kernel keeps a list of the installed drivers by name. Before a driver is used, it must be DosOpened from the application. The DosOpen specifies an ASCII-Z string with the device name as a parameter. The kernel compares this name with its list of installed drivers, and if it finds the name, it calls the Open section of the driver Strategy section to open the device. If that operation succeeds, the kernel returns a handle to the application to use for future driver access. The ASCII-Z name is never used again while the device remains open. The device handles are usually assigned sequentially, starting with 3 (0, 1, and 2 are claimed by OS/2). However, the handle value should never be assumed. The ASCII-Z device name is located in the device driver header.

The OS/2 Request Packet

An OS/2 device driver consists of a Strategy section and optional Interrupt and Timer sections. The Strategy section receives requests from the kernel in the form of a request packet. The Strategy section verifies the request and, if possible, completes the request and sends the result back to the kernel. If the request cannot be completed immediately, the driver optionally queues up the request to be completed at a later time and starts the I/O operation if necessary. The kernel calls the Strategy section directly by finding its offset address in the device header.

The first entry in the request packet is the request-packet length, filled in by the kernel. The second parameter is the unit code. When a driver supports multiple logical units, the value stored here selects among them. The third field is the command code. The command code is filled in by the kernel. This is the code used by the switch statement in the Strategy section to decode the type of request from the kernel. The next field is the status word returned to the kernel. This field will contain the result of the driver operation along with the Done bit to notify the kernel the request is complete (this is not always the case; the driver may return without the Done bit set). To make things easier, I use a union to access specific types of requests and place the request-packet structures in an include file.

Building the Device Header

A simple OS/2 device driver consists of one code segment and one data segment, although more memory can be allocated if necessary (by means of DevHlp routines). The first data that appears in the data segment must be the device-driver header.

The device-driver header is a fixed-length, link-list structure that contains information for use by the kernel during INIT and normal operation. The first entry in the header is a link pointer to the next device the driver supports. If no other devices are supported, the pointer is set to -1L. This terminates the list of devices supported by this driver. If the driver supports multiple devices, such as a four-port serial board or multiple-disk controller, the link is a far pointer to the next device header.

The next entry in the device header is the attribute word, followed by a one-word offset to the driver Strategy section. Only the offset is necessary, because the driver is written in the small model with a 64-kilobyte code segment and a 64-KB data segment (this is not always true; in special cases, the driver can allocate more code and data space if needed).

The succeeding entry is an offset address to an interdriver communications routine if the driver supports IDC. (The DAW_IDC bit in the device attribute word must also be set; otherwise, the AttachDD call from the other driver will fail.)

The last field is the device name, which must be eight characters in length. Names with fewer than eight characters must be padded with blanks. Remember, any mistake in coding the device-driver header will cause an immediate crash and burn when booting.

Providing a Register Interface to the C Driver OS/2 device drivers are normally written in C, using the small model, which means 64 KB of data and 64 KB of code (code and data space may be increased in special cases). The driver .SYS file must load the data segment before the code segment. When you write an OS/2 driver in C, you must provide a mechanism for putting the code and data segments in the proper order, and you must also provide a low-level interface
to handle device and timer interrupts. Because the device header must be the first item that appears in the data segment, you have to prevent the C compiler from inserting the C start-up code before the device header. You may also have to provide a method of detecting which device is being requested for drivers that support multiple devices. The small assembly language program in listing 4 takes care of these requirements. The _acrtused entry point prevents the C start-up code from being inserted before the driver data segment. The segment-ordering directives ensure that the data segment precedes the code segment.

Note the _STRAT entry point. How does this get called? Remember, this is the address that is placed in the driver’s data-segment device header. The kernel, when making a request to the driver, looks up this address in the device header and makes a far call to it. The assembly language routine then calls the C mainline. Thus, the linkage from the kernel to the driver is established.

Why is there a push 0 at the beginning of the _STRAT routine? That’s the device number. Each device supported by the device driver requires a separate device header, and each device header contains an offset address to its own Strategy section. Using the assembly language interface, the routine pushes the device number on the stack and passes it to the driver Strategy section for service.

The Strategy Section

The Strategy section is nothing more than a big switch statement. Common driver requests, such as DosWrite and DosRead, have standard function and return codes. The driver may ignore any or all of these requests by returning a Done status to the kernel. This tells the kernel that the request has been completed. The status returned to the kernel can also include error information that the kernel returns to the calling program.

Note that in the case of a standard driver function, the kernel will map the error value returned from the driver to one of the standard return codes. It is therefore impossible to pass any special return codes to the application via a standard driver request. If you attempt to do so, the kernel will intercept the special return code and map it to one of the standard return codes. The only way to return a special code to the application is by means of an IOCtl request. IOCtls are
used for special driver-defined operations (e.g., port I/O). IOCtls are accessed when the application issues a DosDevIOCtl call with the driver’s handle. This flexibility allows the driver writer to customize the device driver to fit any device. For instance, if you had a serial driver that monitored bus traffic and reported the
occurrence of one or more special characters, you could use an IOCtl read and pass back the character in the return code.

Listing 5 shows the skeleton of a Strategy section. Note the switch on the request-packet command. Several standard driver functions have command codes predefined in OS/2. The driver writer can act on or ignore any of the requests to the driver. Although it would not make sense, the driver could ignore the Open command, issued by the kernel in response to a DosOpen call. Or, more logically, the driver can refuse to be deinstalled by rejecting a Deinstall request.

The INIT call is made only once, during system loading in response to a DEVICE= in CONFIG.SYS. The call is made in the INIT mode from ring 3, but with I/O privileges. The INIT routine is where you would insert the code to initialize your device, such as configuring a UART or sending a disk to track 0.

The very first thing you must do in the initialization code is to save the DevHlp entry-point address in the driver’s data segment. This is the only time the address is valid. It must be saved, or it is lost forever. The address of the DevHlp entry point is passed in the INIT request packet. The initialization code performs two other functions. First, it issues the sign-on message to the screen that the driver is attempting to load. Second, it finds the segment address of the last data and last code item, and it sends them back to OS/2. OS/2 uses the code- and data-segment values to size memory. If a driver fails installation, it must send back zeroes for the CS and DS registers so that OS/2 can use the memory space it occupied.

One of the most common techniques in OS/2 driver design is for the Strategy section to request service from the device and wait for a device or timer interrupt to signal completion of the request. The fragment in listing 6 shows an implementation of this scheme for the Read function of my sample serial communications driver. In this case, the Strategy section starts the I/O and issues a Block DevHlp call, which blocks the calling thread. When the device interrupt signals that the operation is done, the interrupt section runs the blocked thread, completing the request. To protect against the request’s never being completed (e.g., in the case of a down device), the Block call can contain a time-out parameter. If the time expires before the completion interrupt occurs, the Strategy section can end the proper error back to the kernel.

Another way to time-out a device is to use the SetTimer DevHlp routine. You can attach a timer handler to the OS/2 system clock and have the handler run the blocked thread after a specified number of ticks.

The commands allowed by the Strategy section are up to the device driver writer. You can process only the commands you wish to act on and let the others simply pass by sending a Done status back to the kernel. You may instead wish to trap the illegal function calls and return an ERROR_BAD_COMMAND message to the kernel. Keep in mind, however, that the kernel frequently issues its own commands to the driver without your knowledge. For example, when the user of the application that opened the driver types a Control-C, the kernel checks the application’s list of open drivers and issues a Close request to each one. In general, I’ve found it easier to ignore all the requests I’m not waiting for and just flag them as done.

In the simplest of drivers, the Strategy section can only contain an Open, Close, and Read or Write request. In a complicated driver, such as a disk driver, the Strategy section may contain over two dozen standard driver functions and several additional IOCtl calls. IOCtl calls are actually Strategy functions, but they are broken down one step further to provide more detailed or device-specific operations. For instance, a driver might send a list of parameters to an I/O port to initialize it and return the input value of a status port with the status of the initialization.

A Sampler of Standard Driver Functions

INIT (code 0x00). This function is called by the kernel during driver installation at boot time. The INIT section should initialize your device, such as setting the baud rate, parity, stop bits, and so forth on a serial port or checking to see if the device is installed by issuing a status request to the device controller. This INIT function is called in a special mode in ring 3 with some ring 0 capabilities.

The driver may turn off interrupts, but they must be turned back on before returning to the kernel. The INIT code may perform direct port I/O without protection violations. Usually, the driver writer will allocate buffers and data storage during initialization, to be sure the driver will work when installed. Because the initialization is being performed in ring 3, the system can check to make sure the buffer and storage allocations are valid and the segments are owned by the driver. If not, the driver can remove itself from memory, freeing up any previously allocated space for other system components or another driver. Because initialization is done only once during system boot-up, it is not critical to optimize the section. Do all your initializations here, as it may be time-prohibitive or even impossible to do initialization during normal driver operation.

Media Check (code 0x01). This function is called by the kernel prior to disk access, and it is therefore valid only for block devices. The kernel passes the driver the media ID byte corresponding to the type of disk it expects to find in the selected drive.

BuildBPB (code 0x02). When the block driver gets a Build Bios Parameter Block call, it must return a pointer to the BPB that describes the mass-storage device.

Read (code 0x04). The application calls the Read section by issuing a DosRead with the handle obtained during the DosOpen. The Read routine may return one character at a time, but more often it returns a buffer full of data. How the Read function works is up to the driver writer. The driver returns the count of characters read and stores the received data in the data segment of the application. Read returns a standard driver return code.

Nondestructive Read (code 0x05). In response to this request, the driver must get the first character in the driver buffer and return it to the caller. If no character is present, the driver must return immediately with the proper error bits and Done bit set.

Input Status (code 0x06). The driver must clear the Busy bit in the request packet if one or more characters are in the driver’s buffer, or set it if no characters are present. This is a Peek function to determine the presence of data.

Flush Input Buffer(s) (code 0x07). This function should flush any receiver queues or buffers and return a Done status to the kernel.

Write (code 0x08). This is a standard driver request called by the application as a result of a DosWrite call. The application passes to the driver the address of data to write (usually in the application’s data segment) and the count of characters to write. The driver writes the data and returns the status to the application along with the number of characters that were actually written. Write returns a standard driver return code.

Write with Verify (code 0x09). The driver writes data as in the Write function code above, but it verifies that the data was written correctly.

Output Status (code 0x0a). The driver must set the Busy bit in the request packet if an operation is in progress, or clear it if the transmitter is free.

Output Flush (code 0x0b). The driver must flush the output queues and buffers and return a Done status to the kernel.

Device Open (code 0x0d). This function is called as a result of the application issuing a DosOpen call. The kernel makes note of the DosOpen request, and if it is successful (done with no errors) the kernel sends back a handle to the application to use for subsequent driver service. The driver writer can use this section to initialize a device, flush any buffers, reset the buffer pointer, initialize the character queues, or anything necessary for a clean starting operation.

Device Close (code 0x0e). This function is called as a result of the application doing a DosClose with the correct driver handle. It’s a good idea to make sure the application closing the driver is the same one that opened it, so save the process ID of the application that opened the driver and make sure the closing PID is the same. If not, reject it as a bogus request. You should make all your devices quiescent at this time.

Removable Media (code 0x0f). The driver receives this request when an application generates an IOCtl call to category 8, function 0x20. Instead of calling the IOCtl, the kernel issues this request. The driver must set the Busy bit of the request-packet status if the media is nonremovable, or clear it if it is removable.

Generic IOCtl (code 0x10). This is a special type of function call. It is very flexible, as the data passed to the driver is stored in two buffers owned by the caller. These buffers may contain any type of data; the format is up to the driver writer.

The first and second parameters of an IOCtl are the address of the application program’s data buffer and parameter buffer, respectively. The parameter buffer might contain a list of USHORTs, UCHARs, or pointers. The data buffer parameter might be a data buffer address in the application program, where the driver would store data from the device.

IOCtls can extend the range of status information that drivers can convey to applications. Suppose, for example, a driver needed to report to an application that the data was in ASCII or binary format, or that a parity error was detected while receiving it. Here an IOCtl would be the answer. The reason? The kernel massages return codes from standard function calls to fit within the standard error definitions. The IOCtl, however, will pass back codes to the application exactly as they were set in the driver. In several drivers that I have written, the DosRead and DosWrite sections of the Strategy routine are commented out and never used. I use IOCtls for the reads and writes to allow the driver to communicate directly with the application without interference from the kernel.

PrepareForSysShutdown. This function tells the device driver it should post any open buffers to their devices before the system powers down. This occurs when you select Shutdown from the Desktop window.

The Interrupt Section

When OS/2 calls your interrupt handler, it does so with interrupts disabled, so any extended time spent in the interrupt handler could cause performance problems. When activated in response to the receipt of data, the interrupt handler must store the data and exit quickly. In the case of character devices, the OS/2 DevHlp library supports fast reads and writes to circular character queues. For block devices, interrupt handling is fast because the interrupt is usually caused by a DMA completion or disk seek completion. For block devices, data is ordinarily transferred to the user buffer using DMA, eliminating the need to transfer data during the interrupt processing. On a DMA transfer, the driver can exit once the DMA controller starts so that other threads can run. When the DMA completes, it generates a DMA completion interrupt that activates the driver’s interrupt handler.

The interrupt handler routine is not difficult to write or understand, but it can be very difficult to debug. Errors that occur in the interrupt handler frequently appear only in a real-time context, when the interrupt handler is active in response to a hardware interrupt. You can’t do a printf() from the interrupt routine or inspect
variables with an application debugger, such as CodeView. You must use the OS/2 KDB (Kernel Debugger) supplied with the DDK or a similar debugger. Even with the KDB, a breakpoint will halt the program, and further interrupts may pass undetected while you decide what to type next. Because of this pause in execution, you lose the real-time context of the program, which may be the root of the original problem. In the end, there’s no substitute for the ability to visualize the correct operation of the interrupt handler.

The Timer Handler

In an OS/2 driver, you can hook the system timer interrupt with a call to the DevHlp library SetTimer function. You pass OS/2 a near pointer to your timer handler, and for each system timer tick, OS/2 calls your timer handler routine and any other timer handler that had been previously registered.

If no data appears within one or two 32-millisecond time ticks, the driver assumes that data input has stopped or at least paused. If a valid Read request is pending, it sends back the data to the blocked Strategy section by issuing a Run request with the same ID used to block the requesting thread. The Strategy section becomes unblocked, gets the data from the receiver queue, and sends the data to the application’s data buffer.

Do You Really Need a Device Driver?

Maybe not. OS/2 1.x allows programs with I/O Privilege (IOPL) enabled to do direct register I/O to a device. If the device is a parallel card or digital switch, a driver may not be necessary. You can set or clear bits using IN and OUT instructions, and as long as the device is not time critical, such a method will be sufficient.

Yet devices that generate interrupts, require asynchronous service, or operate in a time-critical environment must use a device driver. Take a serial device, for example. It would be difficult or impossible to read data from the device using the IOPL method. By definition, asynchronous data may come in at any time. Because OS/2 may be running another thread at the time the data appears, your chances of missing data are excellent. But an interrupt driver could continue to read and buffer the incoming data until the OS/2 scheduler ran your thread.

Optionally, you can allow interrupts to preempt the current running thread and run your thread immediately. You need not wait for the
scheduler to run it. This sort of preemptive multitasking sets OS/2 apart from other multitasking systems, like Unix. In Unix, the
currently running program retains the CPU until it exhausts its time slice. It cannot be preempted based on an event, such as a device
interrupt. That’s why OS/2 is my choice for time-critical applications.


Steven J. Mastrianni is an independent consultant in South Windsor, Connecticut, who specializes in OS/2 device drivers. You can contact
him on BIX as “smastrianni.”




FutureWare, Inc.
78 Temple Ave., Suite 15
Hackensack, NJ 07601
(201) 343-3921

Microsoft Corp.
One Microsoft Way
Redmond, WA
(800) 227-6444

OS Technologies
532 Longley Rd.
Groton, MA 01450
(508) 448-9653

17541 Stone Ave. N
Seattle, WA 98133
(206) 546-0470

PSS Corp.
290 Brookfield St.
South Windsor, CT 06074
(203) 644-4764


OS/2 Device Drivers for Dummies – The Beginning

Last change 09/05/2007

1. Introduction

Let me first state that I fully intend to plagiarize as much as possible, deal with it. There seems to be only a few options for the hobbyist programmer to easily learn how to build a device driver for eCS-OS/2. Some good examples that exist are Alger Pike’s EDM/2 articles from years ago, so they are somewhat dated. Another example is contained in the Open Watcom directory [samples\os2\pdd]. Of course there are various driver examples available from many additional sources, just use Google. This text deals with 16 bit eCS-OS/2 drivers.

I am using several printed and electronic text references which can be be found both online or through book sites such as

  • 1. OS/2 Physical Device Driver Reference
  • 2. Design of OS/2 by Harvey M. Deitel and Michael S. Kogan (note 1)
  • 3. Writing OS – 2 2.1 Device Drivers in C by Steven J. Mastrianni (note 2)
  • 4. Device driver information contained on the second eCS CD
  • 5. Writing OS/2 Device Drivers by Raymond Westwater

Reference 3 and 5 are very good, but only deal with 16 bit drivers. However, there is rumored to be an unpublished version of reference 3 for OS/2 Warp (1997) floating around the net somewhere. I will be taking parts of all the above and with luck provide something that can be used to learn what I did much quicker for a beginner.

You need some knowledge of C and enough ASM know-how to understand some basic source. For ASM I know how to do a HelloWorld executable but that is the extent. Also, the current version of Open Watcom needs to be installed and working. Later you might want to read up on privilege levels and a good explanation exists on EDM/2 by Holger Veit (what happened to him?).

I have modified Pike’s Hello World example so it can be compiled with Open Watcom and does not require the OS/2 DDK [here]. It outlines the basic parts of a device driver, but really does nothing. During system boot it displays a message which indicates it has loaded and executed initialization. I have provided a test.exe which when run opens and closes the driver. The driver will beep on open and close, not very exciting but it does provide feedback that things are working correctly.

What is the most important thing to remember? Nothing comes fast and easy and the following information is from my continued learning about device drivers. I started with no knowledge and am still very limited. The source code for the driver and test program are HERE. Enjoy…

Note 1: I believe there are PDF versions of this book floating around, I have a hard cover copy. However, I did email Dr. Kogan asking if he would release it to the masses. He reply that he had wanted to do this sometime ago, but his coauthor would not agree to it.

Note 2: I know there is an unpublished 3rd revision PDF floating around which covers Warp (approx 1997).

2. What is a Device Driver?

Exactly what is a device driver? Simply put, a device driver is a piece of code which is dedicated to controlling a particular device. It is the device dependent module that provides the low-level I/O and interrupt support for a device. Also, the device driver will manage any memory that the device may use for Direct Memory Access. The device can be anything from a video card to a data acquisition board. A driver is necessary because many of the previous operations must occur at ring level 0 for them to be successful under eCS-OS/2, and for code to run in ring 0, it must be in a device driver. So, device drivers are trusted modules that have access to the kernel. For the hobbyist programmer this means you can really crash the system now!

Device drivers come in two flavors, block and character. Block devices are used for mass storage devices and character devices handle data one character at a time. Is this true? I have read that it is so, most of the time. However, in Gordon Letwin’s Inside OS/2 he says the following:

OS/2 device drivers are divided into two general categories: those for character mode devices and those for block mode devices. This terminology is traditional, but dont take it too literally because character mode operations can be done to block mode devices. The actual distinction is that character mode device drivers do I/O synchronously; that is, they do operations in first in, first out order. Block mode device drivers can be asynchronous; they can perform I/O requests in an order different from the one I which they received them.

Device drivers operate in three different modes: INIT, Kernel (or task), and Interrupt. INIT mode is a special mode executed during at system boot with RING 3 privileges, however, it is allowed some RING 0 privileges (see OS/2 PDD reference). The Kernel mode is in effect when the device driver is called by the kernel in response to an I/O request. The Interrupt mode is in effect when the device driver’s interrupt handler is called in response to an external interrupt. In this example I will only be concerned with INIIT and Kernel modes.

I intend only to deal with 16-bit driver and not some of the 32-bit additions IBM added.

So, where does this all start? During system boot time the kernel finds a DEVICE= statement in the CONFIG.SYS file and then loads the device driver. Next, it reads the device driver header which is where I start this adventure.

3. The Header

The device driver consists of at least one code segment and one data segment, although more memory can be allocated if required. Additionally, the device driver is an EXE type program which is linked as a DLL. The header contains information used by the kernel during initialization. The data segment, which contains the Device Header, must appear as the very first data item. No data items or code can be placed before the Device Header. An OS/2 device driver which does not adhere to this rule will not load. While I do not intend to get ASM deep, the devsegs.asm source and #pragma data_seg ( “_HEADER”, “DATA” ) in header.c keeps the segments in order.

The initialization thread opens the driver module and reads the first segment into low memory (below 1M). This segment will be the main data segment. The second segment is loaded into low memory and will be the main code segment. Any additional segments are read into high memory (above 1M).

device segments
The device header is defined in devhdr.h:

struct DEVHEADER {
  struct DEVHEADER FAR *next;      // next driver in chain
  uint16_t         DAWFlags;       // device attribute word
  NPVOID           StrategyEntry;  // offset to strategy routine
  NPVOID           IDCEntry;       // offset to IDC routine
  uint8_t          Name[8];        // driver name
  uint16_t         DAWProtCS;      // * Protect-mode CS of strategy entry pt
  uint16_t         DAWProtDS;      // * Protect-mode DS
  uint16_t         DAWRealCS;      // * Real-mode CS of strategy entry pt
  uint16_t         DAWRealDS;      // * Real-mode DS
  uint32_t         Capabilities;   // Capabilities bit strip

This Hello World header is defined in header.c with the following values:

	-1L,                                         // Link to next header in chain
	DAW_CHARACTER|DAW_OPENCLOSE|DAW_LEVEL1,      // device attribute word
	Strategy,                                    // Entry point to strategy routine
	0,                                           // Entry point to IDC routine
	{"Hello$  "},                                // Device driver name
	0,0,0,0,                                     // Reserved
	CAP_NULL                                     // Capabilities bit strip (for level 3 DDs)

The “next driver in chain” link is set to -1L to mark the end of DEVHEADER chain (see devhdr.h) because the example device driver contains only a single device. If a second device were to be defined in this driver then the field would point to it. Next, the Device Attribute Word which is used to define the operational characteristics of the device driver. This is set to DAW_CHARACTER | DAW_OPENCLOSE | DAW_LEVEL1 which are listed and explained in devhdr.h. The strategy routine entry point is next and is explained in section 4 of this article. The IDC entry point offset follows and is used if the device driver supports inter-device driver communications. The Hello World driver does not support IDC so this is set to 0. The Device driver name is next and must be 8 characters in length, notice how the Hello$ name is padded with spaces. The final field is the Capabilities Bit Strip word defines additional features on level 3 drivers (OS/2 v2.0 (support of memory above 16MB). See devhdr.h for Capabilities Bit Strip options, however, the Hello World driver does not utilize level 3 options.

A more detailed description of the header is located here.

4. The Strategy Section

When I started, the title “Strategy” kind of scared me. Like most code, the name tends to scare a hobbyist programmer until one sees what it really is! The Strategy section is just a large switch statement and from my point of view the heart of the device driver. Remember, I am not covering interrupt drivers. The device driver receives a request from the kernel on behalf of the calling application which are passed to the Strategy. Also, the Strategy is called at initialization with RP_INIT which will execute the INIT routine. In the Hello World example the StratInit( ) funtion is called.

5. The INIT Mode

I would like to summarize what has been presented up to now. The kernel found a DEVICE statement during system boot, it loaded and then looked for the device header. It examines the header, finding the Strategy entry point (strategy.c) and the device name (Hello$). The kernel now calls the Strategy provided in the header with RP_INIT. To be more detailed and in Hello World context, it passes a request packet REQP_INIT (see devreqp.h) to the strategy entry point:

typedef struct _REQP_HEADER {
	uint8_t            length;        // Length of request packet
	uint8_t            unit;          // Unit code (B)
	uint8_t            command;       // Command code
	uint16_t           status;        // Status code
	uint32_t           res1;          // Flags
	struct REQP_HEADER FAR *next;     // Link to next request packet in queue

typedef struct {
	REQP_HEADER header;
	        uint8_t   res;            // Unused
	        uint32_t  devhlp;         // Address of Dev Help entry point
	        int8_t    *parms;         // Command-line arguments   PCHAR
	        uint8_t   drive;          // Drive number of first unit
	    } in;
	    struct  {
	        uint8_t   units;          // Number of supported units
	        uint16_t  finalcs;        // Offset to end of code
	        uint16_t  finalds;        // Offset of end of data
	        void      *bpb;           // BIOS parameter block   PVOID
	    } out;

On entry to Strategy( ) REQP_INIT rp->command will be set to RP_INIT which will call StratInit( ) to perform initialization. Again, it is important to remember during INIT the driver is actually at ring level 3 with some access ring level 0 functions. The following tables lists the API calls available at INIT:

DosBeep Generate sound from speaker
DosCaseMap Perform case mapping
DosChgFilePtr Change (move) file read/write pointer
DosClose Close file handle
DosDelete Delete file
DosDevConfig Get device configuration
DosDevIOCtl I/O control for devices
DosFindClose Close find handle
DosFindFirst Find first matching file
DosFindNext Find next matching file
DosGetEnv Get address of process environment string
DosGetInfoSeg Get address of system variables segment
DosGetMessage Get system message with variable text
DosOpen Open file
DosPutMessage Output message text to indicated handle
DosQCurDir Query current directory
DosQCurDisk Query current disk
DosQFileInfo Query file information
DosQFileMode Query file mode
DosRead Read from file
DosSMRegisterDD Register session switch notification
DosWrite Synchronous write to file

The initialization routine performs two important jobs. First, to save the value of the entry point for the device’s DevHlp routines (REQP_INIT rp->in.devhlp). Second, and to set the end of data and code segments (REQP_INIT rp->out.finalcs and REQP_INIT rp->out.finalds). The data and code segments after these points will be discarded after all device driver headers in the driver have been initialized. If the data length is set to zero then the driver will be unloaded.

I found the best simple explanation of the DevHlp entry point on USENET by Holger Veit:

“The kernel itself IS the device helper., i.e. when registering a device driver you get a 16:16 pointer to a kernel routine that is named DevHlp and all so called device helper functions call this entry point indirectly (after setting the appropriate registers).”

The DevHelp entry point should be declared as:

PFN  Device_Help  = NULL;

Although Resource Manager functions are not used in this Hello World example, you should plan for the future using Resource Management services. The PDD reference states: The PFN Device_Help variable must be initialized by your driver prior to calling any Resource Manager services. It is expected to contain the Device Help entry point provided in the OS/2 Init Request Packet your driver receives.

If the size of the segments remain zero, at the end of INIT the driver will unload.

6. The Kernel Mode

Need to add – MKG

7. Compiling and Testing the Driver

Compiling is easy. Ensure that Open Watcom is installed and working correctly. Unzip the archive and then wmake. The result will be hello.sys and test.exe.

Here is the part where it all comes together! Place a statement in the config.sys (example: DEVICE=C:\hello.sys) and reboot. During boot you should see:

Hello World Driver Installed.
(C) ACP Soft 1996.
M Greene <> 2007.
All Rights Reserved.

So, what just happened? The kernel found our DEVICE statement, loaded the driver, read the header, and then sent a RP_INIT to the Strategy Section. The Strategy Section received the RP_INIT and called the StratInit( ) function. The StratInit( ) function displayed the above message and returned. If the system did not hang or trap then hello.sys is loaded and ready.

As I stated, the driver does nothing spectacular. The test.exe executable interfaces with hello.sys to make some noise. When test.exe is run the following will be displayed with a couple beeps:

About to beep….
DosOpen return 0
Sleep for 5 seconds….
About to beep….
DosClose return 0

Ok, here is what just happened:

  • test.exe prints “About to beep” and issues a DosOpen to Hello$
  • The Strategy Section receives a RP_OPEN and executes the StratOpen( ) function
  • The StratOpen( ) function issues a DosBeep and RPDONE then returns
  • Now back in test.exe DosOpen return is printed with the return code
  • test.exe prints Sleep for 5 seconds and sleeps for 5 seconds
  • Next test.exe issues a DosClose to Hello$
  • The Strategy Section receives a RP_CLOSE and executes the StratClose( ) function
  • The StratClose( ) function issues a DosBeep and RPDONE then returns
  • Back in test.exe DosClose return is printed with the return code
  • test.exe exits


8. Summary

Exciting, right??? Well maybe not, but it is a good and simple drive driver example.



This is a special entry into DosKillProcess, which is more rigid in killing processes: the killed process won’t be able to execute its kill signal handler, thus open files might not be closed properly. A process currently operating in ring 0 will terminate as soon as it returns to ring 3 (this means, a process blocked in the kernel, e.g. on a pending I/O, can still not be killed). This devhlp corresponds to the SESKillProcess API.
Calling convention

  • BX = Pid of process to kill, 0 = current process
  • DL = 07dh


  • CY=0, AX = 0, okay
  • CY=1, AX = APIRET error code

Data structures



Valid at TASK time only.

Republished without permission from Holger Veit’s long gone webpages – MKG