Much of the I/O Manager operation supports a standard Read/Write abstraction. The requestor supplies a buffer and transfer length, and data is transferred from or to the device. Not all devices or their operations always fit this abstraction. For example, a disk format or repartition are requests that are not well-suited to a normal Read or Write operation. Such kinds of requests are handled with one of two extensible I/O function request codes. These codes allow any number of driver-specific operations, without the restrictions of the Read/Write abstraction.
As should be apparent, the implementation of either of these Dispatch routines requires a secondary dispatch based on the value of IoControlCode in the IRP. This value is also known as the IOCTL device control code. Since the secondary dispatch mechanism is completely contained within the driver's private routine(s), the interpretation of the IOCTL value is driver-specific. The remainder of this section describes the details of the device control interface. Defining Private IOCTL ValuesThe IOCTL values passed to a driver follow a specific structure. Figure 7.2 illustrates the fields of this 32-bit structure. The DDK includes a macro, CTL_CODE, that offers a convenient mechanism to generate IOCTL values. Table 7.3 describes the arguments to this macro. Figure 7.2. Layout of the IOCTL code structure.IOCTL Argument-Passing MethodThe extended functions defined with an IOCTL value within a driver often require an input or output buffer. For example, a driver might report performance data using an IOCTL value. The data reported would be transferred through a buffer supplied by the user. Indeed, the Win32 DeviceIoControl function defines parameters for two buffers, one for input, one for output. The buffer transfer mechanism provided by the I/O Manager is defined within the IOCTOL value itself. It can be either buffered or direct I/O. As described previously, with buffered I/O, the I/O Manager copies the user buffer (into or out of) nonpaged system memory, where driver code can then conveniently operate. With direct I/O, the driver is given direct access to the user buffer. Interestingly, the driver's overall strategy for buffer handling (defined during DriverEntry) is not enforced for IOCTL transfers. Instead, the buffer transfer mechanism is defined with each IOCTL value specification and is a field within the IOCTL structure. This provides maximum flexibility when performing DeviceIoControl operations.
The TransferType field of the IOCTL field is two-bits wide and defines one of the following:
Since the TransferType field is a part of the IOCTL code itself, the public codes defined by Microsoft specify the I/O transfer mechanism. For private IOCTL values (driver defined), any appropriate transfer mechanism can be defined. For small, slower transfer, buffered I/O is appropriate. For faster, larger transfers, direct I/O is most suitable. Writing IOCTL Header FilesSince both the driver project itself and all the clients of the driver need symbolic definitions for the IOCTL codes, it is customary for the driver author to provide a separate header file with device control-code definitions. This header file should also contain any structure definitions that describe the buffer contents of specific control operations. A Win32 program needs to include WINIOCTL.h before including the driver's IOCTL header. A driver project needs to include DEVIOCTL.h before including the driver-specific IOCTL header. These files define the CTL_CODE macro, among other things. The following is an example of an IOCTL header file: #define IOCTL_MISSLEDEVICE_AIM CTL_CODE( \ FILE_DEVICE_UNKNOWN, 0x801, \ METHOD_BUFFERED, \ FILE_ACCESS_ANY ) // Structures used by IOCTL_MISSLEDEVICE_AIM typedef struct _AIM_IN_BUFFER { ULONG Longitude; ULONG Latitude; } AIM_IN_BUFFER, *PAIM_IN_BUFFER; typedef struct _AIM_OUT_BUFFER { ULONG ExtendedStatus; } AIM_OUT_BUFFER, *PAIM_OUT_BUFFER; #define IOCTL_MISSLEDEVICE_LAUNCH CTL_CODE( \ FILE_DEVICE_UNKNOWN, \ 0x802, \ METHOD_NEITHER, \ FILE_ACCESS_ANY ) Processing IOCTL RequestsOnce a driver has announced Dispatch routines for either IRP_MJ_DEVICE_CONTROL or IRP_MJ_INTERNAL_DEVICE_CONTROL function codes, the I/O Manager starts passing IRPs directly to driver code. The interpretation of the IOCTL Device Control code is strictly the responsibility of the driver. Not even the various fields of the IOCTL code itself are verified by the I/O Manager prior to invocation of the driver's Dispatch routine. Any random number passed by the requestor as an IOCTL code finds its way to the driver. Therefore, the typical structure of a device control Dispatch routine is a large switch statement. The following is an example of such a routine: NTSTATUS DispatchIoControl( IN PDEVICE_OBJECT pDO, IN PIRP pIrp ) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_EXTENSION pDE; PVOID userBuffer; ULONG inSize; ULONG outSize; ULONG controlCode; // will be the IOCTL request // The stack location contains the user buffer info PIO_STACK_LOCATION pIrpStack; pIrpStack = IoGetCurrentIrpStackLocation( pIrp ); // Dig out the IOCTL request controlCode = pIrpStack-> Parameters.DeviceIoControl.IoControlCode; // and the requested transfer sizes inSize = pIrpStack-> Parameters.DeviceIoControl.InputBufferLength; OutSize = pIrpStack-> Parameters.DeivceIoControl.OutputBufferLength; // // Now perform the secondary dispatch switch (controlCode) { case IOCTL_MISSLEDEVICEAIM: // Always validate parameters for each case... if (inSize < sizeof(AIM_IN_BUFFER) || (outSize < sizeof(AIM_OUT_BUFFER) ) { status = STATUS_INVALID_BUFFER_SIZE; break; } // Valid IRP values - start the device IoMarkIrpPending( pIrp ); IoStartPacket( pDO, pIrp, 0, NULL); return STATUS_PENDING; case IOCTL_DEVICE_LAUNCH: if (inSize > 0 || outSize > 0) { // Is it really an error to pass buffers // to a function that doesn't use them? // Maybe not, but the caller is now forced // to re-think the purpose of the call. status = STATUS_INVALID_PARAMETER; break; } // Same kind of processing start the device // : return STATUS_PENDING; default: // Driver received unrecognized control code status = STATUS_INVALID_DEVICE_REQUEST; break; } // Valid control code cases returned above. // Execution here means an error occurred. // Fail the IRP request...pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = 0; // no data xfered IoCompleteRequest( pIrp, IO_NO_INCREMENT ) return status; } Managing IOCTL BuffersIOCTL requests can specify both an input and an output buffer in the same call. As a result, they present a read-after-write abstraction to the caller. IOCTL requests differ in user buffer access in two ways.
The following sections describe how the different buffer strategies work with IOCTL control codes. METHOD_BUFFEREDThe I/O Manager allocates a single temporary buffer from nonpaged pool, large enough to hold the larger of the caller's input or output buffer. The address of this pool buffer is placed in the IRP's AssociatedIrp. SystemBuffer field. It then copies the requestor's input buffer into the pool buffer and sets the UserBuffer field of the IRP to the user-space output buffer address. Upon completion of the IOCTL request, the I/O Manager copies the contents of the system buffer into the requestor's user-space buffer. Notice that only a single internal buffer is presented to the driver code, even though the user has specified independent input and output buffers. The driver code must take care to extract all necessary information from the requestor's input before performing writes into the output buffer. METHOD_IN_DIRECTThe I/O Manager checks the accessibility of the requester's input buffer and locks it into physical memory. It then builds an MDL for the input buffer and stores a pointer to the MDL in the MdlAddress field of the IRP. It also allocates a temporary output buffer from nonpaged pool and stores the address of this buffer in the IRP's AssociatedIrp.SystemBuffer field. The IRP's UserBuffer field is set to the original caller's output buffer address. When the IOCTL IRP is completed, the contents of the system buffer are copied into the caller's original output buffer. METHOD_OUT_DIRECTThe I/O Manager checks the accessibility of the caller's output buffer and locks it down in physical memory. It then builds an MDL for the output buffer and stores a pointer to the MDL in the MdlAddress field of the IRP. The I/O Manager also allocates a temporary input buffer from non-paged pool and stores its address in the IRP's AssociatedIrp. SystemBuffer field. It copies the contents of the caller's original input buffer into the system buffer and sets the IRP's UserBuffer field to NULL. METHOD_NEITHERThe I/O Manager puts the address of the caller's input buffer in the Parameters.DeviceIoControl.Type3InputBuffer field of the IRP's current I/O stack location. It stores the address of the output buffer in the IRP's UserBuffer field. Both of these are user-space addresses.
|