Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Sending IOCTLs to Windows NT Drivers



NT device drivers respond to a simple set of file-oriented commands: open, read, write, and close. The NT device driver model supports another command, however: the IOCTL (I/O Control) command. A driver can make available most any custom functionality via an IOCTL command. Many standard Windows NT device drivers provide IOCTL (I/O Control Code) command functionality in addition to the basic device read/write support. These IOCTL commands can sometimes be very useful to applications as well as to other drivers. For example, the NT floppy drive device driver supports an IOCTL command that reports whether or not a floppy is currently inserted in the drive. For file-oriented commands (e.g., open, read, write, and close), an NT application can use familiar functions such as ReadFile() and WriteFile(). Sending an IOCTL command, though, requires calling the somewhat less familiar Win32 function DeviceIoControl().

Device drivers can send IOCTL commands to other device drivers, just as applications do, though it is not as easy as calling DeviceIoControl(). Some programmers actually create filter drivers to obtain information that they could more easily obtain via an IOCTL command. That's a risky practice, since a bug in a filter driver usually is capable of causing many more problems than a bug in code that just sends an IOCTL command.

This article demonstrates IOCTL commands from the perspective of both applications and drivers. I will demonstrate three aspects of IOCTL commands:

  • How to support IOCTL commands in your own device driver.
  • How to send an IOCTL command to another driver from your device driver.
  • How to send an IOCTL command to a device driver from an application.

To demonstrate these concepts, I wrote a simple application and a simple Windows NT device driver that communicate via a custom-defined IOCTL. When the application sends an IOCTL command to the driver, the driver in turn sends a different IOCTL command to one of the standard built-in Windows NT drivers. This demonstrates how applications send IOCTL commands to drivers and how drivers send IOCTL commands to other drivers. It also provides a template for adding IOCTL commands to your own drivers. In practice, the application could just send an IOCTL directly to the second driver, but this example is contrived to demonstrate all three concepts. You will need the Windows NT DDK to build the examples in this article.

Defining the Custom IOCTL Value

When you decide that your device driver will support an operation via an IOCTL command, you must define a command code that callers can use. wdj.h (Listing 1) contains the definition of my custom IOCTL command code. Any other application or driver that wants to issue this IOCTL to my driver will #include this file.

The actual IOCTL command value is a bitmask that contains several pieces of information. I used the CTL_CODE macro (defined in winioctl.h for applications and ntddk.h for drivers) to define the custom command IOCTL_WDJ_REQUEST for my sample driver. The first argument to CTL_CODE is a value that describes the device type. NT defines several standard device types (such as FILE_DEVICE_DISK for persistent storage devices), but my sample driver doesn't really fit any of the predefined device categories, so I defined a new device type value of FILE_DEVICE_WDJ. Note that since this is an "OEM" device (rather than a standard device type defined by the operating system), I must use a value in the range 0x8000 to 0xFFFF.

The second argument of the CTL_CODE macro identifies the specific IOCTL command. When a driver supports more than one custom IOCTL command, this value must be different for each command. Since IOCTL_WDJ_REQUEST is a custom IOCTL command (rather than a standard IOCTL command, such as IOCTL_FORMAT_DISK_TRACKS, that is supported by a whole category of device drivers), I'm required to use a value in the range 0x800 to 0xFFF to define my IOCTL command. The third argument describes the type of data transfer; METHOD_BUFFERED is a typical choice for drivers that transfer a small amount of data. The final argument describes what kind of access the application must specify when opening a handle to this device. A value of FILE_ANY_ACCESS means the caller can open a handle to this device with any access rights and still be able to send the IOCTL_WDJ_REQUEST command via that file handle.

The Sample Driver

The sample driver code is in wdj.c (Listing 2). This driver has only two requirements: it must support the IOCTL_WDJ_REQUEST command and it must demonstrate how to send an IOCTL command from one driver to another driver. To demonstrate how one driver sends an IOCTL to another, I needed to find a standard NT device driver that accepts some useful IOCTL command. For my target driver I chose the standard Windows NT floppy driver, because nearly all computers have at least one floppy device and the consequences of accidently sending an errant command to the floppy driver are not usually catastrophic. You should be able to experiment with this sample driver on your own system.

The standard initialization entry point for all Windows NT device drivers (except certain layered drivers such as SCSI miniport drivers) is DriverEntry(). In DriverEntry(), a device driver typically creates one or more device objects to represent physical or logical devices. My sample driver doesn't actually talk to any real devices, so I create a single logical device object ("\\device\wdjdrv") by calling IoCreateDevice(). Device objects typically have names of the form "\\device\Xxx". This doesn't really give my sample application something to talk to yet, though, because device object names are not directly accessible to user-mode applications. I need to create a symbolic link between my device object and a name that is visible to user-mode applications by calling IoCreateSymbolicLink(). Symbolic link names have the form "\\DosDevices\Xxx". When an application passes the symbolic link name to CreateFile() and then sends read, write, or IOCTL commands to that file handle, the requests are routed to my driver, because my driver created the target device object. Since a driver can create multiple device objects, drivers usually use the private device extension area of the device object to store any information that may need to be retrieved in order to carry out the read, write, or IOCTL command request.

My driver also needs to send IOCTL commands to a device driver (the floppy driver), but NT device drivers don't have access to the Win32 API, so my driver can't call CreateFile() to obtain a handle. Moreover, the symbolic links that applications use to open devices represent a namespace that drivers don't have access to. Instead, my driver must use a DDK function to obtain a handle to one of the device objects that represent a physical floppy drive. The floppy driver creates device objects of the form "\\device\floppyX", where "X" is "0" for the first floppy device, etc. My driver retrieves a handle to the device object for the first floppy device by passing "\\device\floppy0" to IoGetDeviceObjectPointer(). IoGetDeviceObjectPointer() returns a pointer to both a file object and a device object for "\\device\floppy0"; since I only need the device object, I save it in my device extension area and dereference the file object.

Finally, my driver fills out the dispatch routine table in the device object before exiting DriverEntry(). The I/O system uses the dispatch routine table to route I/O requests targeted for my device object to the appropriate routine within my driver. When an application attempts to open, close, read, write, or send IOCTL commands to the symbolic link for my device object, the I/O system packages that request into an IRP (I/O Request Packet) and sends the IRP to the appropriate routine listed in the dispatch table. Since my sample driver doesn't really support anything other than IOCTL commands, I filled out only the IRP_MJ_CREATE, IRP_MJ_CLOSE, and IRP_MJ_DEVICE_CONTROL entries. I also provided an "Unload" routine for my driver. This routine is not called in response to an I/O request, so it is not passed an IRP or a device object. Rather, it is called just before my driver is unloaded. In WdjDrvUnload(), I simply delete the symbolic link and the device object that I created when the driver was initialized.

WdjDrvDispatch() handles more than one command (create, close, and IOCTL commands), so it examines the MajorFunction field in the current IRP stack location to identify the command. Since I specified METHOD_BUFFERED as the transfer type when I defined the IOCTL command, any data passed by the caller can be found in the AssociatedIrp->SystemBuffer field, and any data I transfer back to the caller will also be copied back into this buffer. Status information is communicated back to the I/O system (and ultimately back to the caller) by filling out the IoStatus.Status and IoStatus.Information fields in the IRP structure. You set the Status field to an NTSTATUS value that indicates whether or not the call was successful (STATUS_SUCCESS), and if not, what kind of error occured. In my case, the Information field is filled out with the size of any data that I copied to the SystemBuffer. I'm not actually doing anything special during the create and close commands, but I provided code stubs in case any readers wanted to add their own caller-specific initialization code.

In response to the IOCTL_WDJ_REQUEST command, I need to send an IOCTL command to the floppy driver. Recall that I already have a handle to the target device object tucked away in my device extension. First, I build an IRP to represent the IOCTL request for this device object by calling IoBuildDeviceIoControlRequest(). In this case, I'm calling the floppy driver's IOCTL_DISK_CHECK_VERIFY command, which tells me whether or not a floppy is present in this floppy drive. After building the IRP, I call IoCallDriver() to pass it to the target driver. Note that since some IOCTL commands are handled asynchronously, callers pass a kernel-mode event handle to IoBuildDeviceIoControlRequest(); the event is signaled when the request completes. If the Status field is set to STATUS_NO_MEDIA_IN_DEVICE on return, then I know that the floppy drive is empty. I copy a Boolean value to the user-mode application's buffer to let them know whether or not a floppy is present in this floppy drive. Finally, to complete the original IOCTL_WDJ_REQUEST command, I call IoCompleteRequest() and then return from WdjDrvDispatch().

You can programmatically install the sample driver by calling CreateService(). For more information, see "Dynamically Loading Drivers in Windows NT" in the May 1995 issue of WDJ.

The Sample Application

Now that I've demonstrated how drivers send IOCTL commands to other drivers, I'll show how applications send IOCTL commands to drivers. My sample application is app.c (Listing 3). The user-mode application must first open a handle to the appropriate device object by passing the symbolic link name to CreateFile(). Recall that the symbolic link name created in the sample driver is "\\DosDevices\wdjdrv". When passing this symbolic link name to CreateFile(), you should use the form "\\.\wdjdrv". It's important to specify the OPEN_EXISTING flag so that the CreateFile() call will fail appropriately if the driver is not loaded.

Once you have a handle open to the appropriate device object, you can send IOCTL commands to it by calling DeviceIoControl(). DeviceIoControl() is also used by applications running on Windows 95 to send commands to VxDs (note that the format for specifying the device name in the call to CreateFile() is different for VxDs). In addition to the file handle and the IOCTL command value, callers can pass an input buffer and an output buffer to DeviceIoControl(). For my custom IOCTL_WDJ_REQUEST command, no input buffer is required, but I do need to specify an output buffer that is at least large enough to hold the ULONG return value. DeviceIoControl() supports both synchronous and asynchronous operation via the lpOverlapped parameter. If you wish to call DeviceIoControl() asynchronously, then the file handle must have been opened with the FILE_FLAG_OVERLAPPED flag.

As I mentioned before, this example is contrived. My sample application could have bypassed the sample wdj.sys driver and called the floppy driver directly by opening a handle to "\\.\a:" and then passing the IOCTL_DISK_CHECK_VERIFY command directly to that file handle. I introduced the complication of wdj.sys just to demonstrate how drivers implement IOCTL commands, and how drivers can pass IOCTL commands to other drivers.

Summary

Drivers have access to a lot of useful information and can perform many useful tasks for applications. If a driver already supplies an IOCTL command that meets your needs, then it is a quite trivial matter for an application to call it. Likewise, drivers can sometimes avoid reinventing code by calling IOCTL commands in other drivers. If you need only to pass IOCTL commands to another driver, it is definitely overkill to layer yourself on top of that driver. Filter drivers are risky in that poorly written filter drivers can compromise the functionality of the driver on which they are layered. It is much simpler and safer in this case to get a pointer to the device object, build an IRP, and send it to the driver when necessary.

Paula Tomlinson has been developing DOS, Windows, and Windows-NT based applications and device drivers for nine years. The opinions expressed here are hers alone. She can be contacted via the internet at [email protected].

Get Source Code


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.