Книга: Writing Windows WDM Device Drivers

User Buffers

User Buffers

As a driver can run in the context of any thread, a plain pointer into the user's address space is not guaranteed to access the correct memory.

A driver can use two main methods to access the user's buffer properly, either Buffered I/O or Direct I/O. When you create a device, you must set the DO_BUFFERED_IO bit in the Flags field of the new device object to use Buffered I/O. For Direct I/O, set the DO_DIRECT_IO bit in Flags.

Buffered I/O

If you use Buffered I/O, the kernel makes the user's buffer available in some nonpaged memory and stores a suitable pointer for you in the AssociatedIrp.SystemBuffer field of the IRP header. Simply read or write this memory in your driver.

This technique is the easiest one for driver writers to use. However, it is slightly slower overall, as the operating system usually will have to copy the user buffer into or out of non-paged memory.

Direct I/O

It is faster to use a Memory Descriptor List (MDL). However, this is only available to hardware that can perform Direct Memory Access (DMA). DMA and MDLs are not explained in this book, although Chapter 24 lists the changes in W2000 for those of you who have used DMA in NT 4 and earlier.

The MDL of the user's buffer is put in the MdlAddress field of the IRP header.

Neither

A final and uncommon technique for accessing a user's buffer is to use neither Buffered I/O nor Direct I/O. In this case, the user's buffer pointer is simply put in the UserBuffer field of the IRP header. If you are certain that your driver is the first driver to receive a request, the dispatch routine can directly access the buffer, as the driver will be operating in the context of the user's thread. Be very careful if you try to use this technique.

DeviceIoControl Buffers

DeviceIoControl requests can use a combination of these user buffer access techniques. Each IOCTL can use a different method, if need be. However, most drivers simply use Buffered I/O, as IOCTL buffers are usually fairly small. I shall show how to define IOCTLs shortly.

The TransferType portion of the actual IOCTL code indicates the buffer access technique. For Buffered I/O, specify METHOD_BUFFERED for TransferType. For Direct I/O, use either METHOD_IN_DIRECT or METHOD_OUT_DIRECT.

If you use METHOD_BUFFERED, AssociatedIrp.SystemBuffer is used for the input and output buffer. The buffer size is the maximum of the user's input and output buffer sizes. As the same memory is used for both input and output, make sure that you use (or copy) the input data before you start writing any output data.

For both METHOD_IN_DIRECT and METHOD_OUT_DIRECT the DeviceIoControl input data appears in buffered memory at Irp->AssociatedIrp.SystemBuffer. In both cases, an MDL for the DeviceIoControl output data is put in Irp->MdlAddress. The size of the read or write buffer is in Parameters.DeviceIoControl.OutputBufferLength.

Finally, for METHOD_NEITHER, the input buffer user space pointer is put in Parameters.DeviceIoControl.Type3InputBuffer in the stack. The user space output buffer pointer is put in the IRP header UserBuffer field.

Wdm1 Dispatch Routines

The dispatch routines for the Wdm1 driver are in the file Dispatch.cpp, which is available on the book CD-ROM. These routines all run at PASSIVE_LEVEL IRQL, so they can be put in paged memory. All the basic dispatch routines complete the IRP straightaway.

The dispatch routines include various DebugPrint trace calls in the code. If you use the checked build version of Wdm1, you can view the trace output using the DebugPrint Monitor application. Listing 7.1 shows the (slightly edited) DebugPrint output on Windows 2000. The Wdm1 driver was started at around 12:00 and the Wdm1Test program was run at 12:10. You can follow the program execution as each test in Wdm1Test is run. The DebugPrint output would be different in Windows 98 because the SetFilePointer function does not work for device files.

Listing 7.1 DebugPrint output in Windows 2000

Monitor 12:03:04 Version 1.02 starting to listen under Windows 2000 (5.0 build 1877)
DebugPrint 11:59:09 Version 1.02 started Wdm1 12:00:42 DebugPrint logging started
Wdm1 12:00:42 RegistryPath is REGISTRYMachineSystemControlSet002SERVICESWdml
Wdm1 12:00:42 DriverEntry completed
Wdm1 12:00:42 AddDevice
Wdm1 12:00:42 FDO is 80AAB020
Wdm1 12:00:42 Symbolic Link Name is ??Root#UNKNOWN#0003#{c0cf0640…}
Wdm1 12:00:42 PnP IRP_MJ_PNP:IRP_MN_QUERY_CAPABILITIES
Wdm1 12:00:42 PnP IRP_MJ_PNP:IRP_MN_FILTER_RESOURCE_REQUIREMENTS
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_START_DEVICE
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_CAPABILITIES
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_PNP_DEVICE_STATE
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_BUS_INFORMATION
Wdm1 12:00:43 PnP IRP_MJ_PNP:IRP_MN_QUERY_DEVICE_RELATIONS
Wdm1 12:10:14 Create File is
Wdm1 12:10:14 Read 4 bytes from file pointer 0
Wdm1 12:10:14 Read: 4 bytes returned
Wdm1 12:10:14 Write 4 bytes from file pointer 0
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 Read 1 bytes from file pointer 3
Wdm1 12:10:14 Read: 1 bytes returned
Wdm1 12:10:14 Write 4 bytes from file pointer 3
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 0022200C InputLength 0 OutputLength 4
Wdm1 12:10:14 DeviceIoControl: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 7
Wdm1 12:10:14 DeviceIoControl: 7 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 8
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222004 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222010 InputLength 0 OutputLength 7
Wdm1 12:10:14 DeviceIoControl: 7 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222008 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 0022200C InputLength 0 OutputLength 4
Wdm1 12:10:14 DeviceIoControl: 4 bytes written
Wdm1 12:10:14 DeviceIoControl: Control code 00222014 InputLength 0 OutputLength 0
Wdm1 12:10:14 DeviceIoControl: 0 bytes written
Wdm1 12:10:14 Write 4 bytes from file pointer 0
Wdm1 12:10:14 Write: 4 bytes written
Wdm1 12:10:14 Close

Create and Close

The Wdm1 create and close routines do nothing except complete the IRP successfully. A helper function, CompleteIrp, is used that sets the IRP header IoStatus fields to the given parameters and calls IoCompleteRequest.

The create routine shows how to access the current I/O stack location using IoGetCurrentIrpStackLocation. In the checked build version, it prints out the FileName field in the stack FileObject.

PIO_STACK_LOCATION IrpStack = IoGetCurrentlrpStackLocation(Irp);
DebugPrint("Create File is %T", &(IrpStack –>FileObject->FileName));

Write

Things start to get interesting in the write dispatch routine, Wdm1Write, shown in Listing 7.2. It starts by getting the current stack location pointer and retrieving the current file pointer and the number of bytes to transfer. If the file pointer is less than zero (the kernel should ensure that it never is), it returns STATUS_INVALID_PARAMETER. It is possible to receive a transfer length of zero.

Listing 7.2 Wdm1 write dispatch routine

NTSTATUS Wdm1Write(IN PDEVICE_OBJECT fdo, IN PIRP Irp) {
 PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp);
 NTSTATUS status = STATUS_SUCCESS;
 ULONG BytesTxd = 0;
 // Get call parameters
 LONGLONG FilePointer = IrpStack->Parameters.Write.ByteOffset.QuadPart;
 ULONG WriteLen = IrpStack->Parameters.Write.Length;
 DebugPrint("Write %d bytes from file pointer %d", (int)WriteLen,(int)FilePointer);
 if (FilePointer<0) status = STATUS_INVALID_PARAMETER;
 else {
  // Get access to the shared buffer
  KIRGL irql ;
  KeAcquireSpinLock(&BufferLock,&irql);
  BytesTxd = WriteLen;
  // (Re)allocate buffer if necessary
  if ( ((ULONG)FilePointer)+WriteLen>BufferSize) {
   ULONG NewBufferSize = ((ULONG)FilePointer)+WriteLen;
   PVOID NewBuffer = ExAllocatePool(NonPagedPool.NewBufferSize);
   if (NewBuffer==NULL) {
    BytesTxd = BufferSize – (ULONG)FilePointer;
    if (BytesTxd<0) BytesTxd = 0;
   } else {
    RtlZeroMemory(NewBuffer,NewBufferSize);
    if (Buffer!=NULL) {
     RtlCopyMemory(NewBuffer,Buffer,BufferSize);
     ExFreePool(Buffer);
    }
    Buffer = (PUCHAR)NewBuffer;
    BufferSize = NewBufferSize;
   }
  }
  // Write to shared memory
  if (BytesTxd>0 && Buffer!=NULL) RtlCopyMemory(Buffer+FilePointer, Irp->AssociatedIrp.SystemBuffer, BytesTxd);
  // Release shared buffer
  KeReleaseSpinLock(&BufferLock,irql);
 }
 DebugPrint("Write: %d bytes written", (int)BytesTxd);
 // Complete IRP
 return Completelrp(Irp,status,BytesTxd);
}

The shared memory buffer is implemented using these three global variables.

KSPIN_LOCK BufferLock;
PUCHAR Buffer = NULL;
ULONG BufferSize = 0;

If the buffer size is greater than zero, Buffer points to some nonpaged memory of this size. As mentioned earlier in this chapter, there must be some mechanism to protect access to such global variables in a multiprocessor environment (e.g., to prevent one dispatch routine from changing BufferSize while another, or even the same, routine tries to access or change it simultaneously).

Spin Locks

A kernel spin lock called BufferLock provides this protection. A spin lock can be used where code needs access to a resource of some sort for a short time.

The spin lock is initialized in the Wdm1 DriverEntry routine as follows.

KeInitializeSpinLock(&BufferLock);

Use the KeAcquireSpinLock function to acquire a spin lock and KeReleaseSpinLock to release it. Only one instance of a piece of code can acquire a spin lock at the same time. Other attempts to acquire the spin lock will "spin" until the resource becomes available. "Spinning" means that KeAcquireSpinLock keeps looking continuously. For this reason, make sure that you only hold a spin lock for a short time. The DDK recommends that you never hold a spin lock for more than 25 microseconds.

As shown in the code example, you must provide a pointer to a KIRQL variable in the call to KeAcquireSpinLock. This stores the original IRQL level before it is raised (if necessary) to DISPATCH_LEVEL The call to KeReleaseSpinLock lowers the IRQL if necessary. If you are certain that your code is working at DISPATCH_LEVEL, you can use the KeAcquireSpinLockAtDpcLevel and KeReleaseSpinLockFromDpcLevel routines for better performance.

The Wdm1 driver acquires the BufferLock spin lock for the duration of any accesses to the Buffer and BufferSize variables. Do not access paged code or data while holding a spin lock, as the system will almost certainly crash. Definitely do not exit a main dispatch routine while holding a spin lock.

Write Algorithm

The write dispatch stores the write data in the shared memory buffer, starting from the given file pointer. It extends the buffer, if necessary.

If there is no buffer at all, or the buffer needs to be extended, ExAllocatePool is called to allocate some nonpaged memory. Notice that the algorithm checks for a NULL return value and copes as best as it can.

A new memory buffer is zeroed using RtlZeroMemory. If an old shorter buffer exists, it is copied to the start of the new buffer using RtlCopyMemory. RtlMoveMemory can be used if the source and destination pointers overlap. The old buffer is removed with ExFreePool.

Finally, Wdm1Write copies the data from the user buffer using RtlCopyMemory. As Wdm1 uses Buffered I/O, it can simply copy the data from Irp->AssociatedIrp.SystemBuffer.

This algorithm is fairly crude, because the buffer may have to be reallocated often. A much-enhanced version of Wdm1 could implement a RAM disk.

The driver unload routine frees any shared memory buffer.

if (Buffer!=NULL) ExFreePool(Buffer);

Read

The read dispatch routine for Wdm1, Wdm1Read, is simpler than the write handler. It acquires the spin lock while it accesses the global variables. The required number of bytes are copied to the user's buffer at Irp->AssociatedIrp.SystemBuffer. If the user requests more data than is in the buffer, the request is truncated.

IOCTL

The Wdm1DeviceControl dispatch routine handles the four IOCTLs defined for Wdm1 devices: Zero the buffer, Remove the Buffer, Get the buffer size, and Get the buffer.

All these IOCTLs use Buffered I/O, so any input and output data is found at Irp->Associ-atedIrp.SystemBuffer. As usual, the routine acquires the shared buffer spin lock for the duration of the call. The actual implementation of each IOCTL is straightforward. The Get buffer size and Get buffer handlers check that the output buffer is large enough; if not, they return STATUS INVALID_PARAMETER.

Defining IOCTLs

An IOCTL code is a 32-bit value formed using the CTL_CODE macro shown in Table 7.4. The Wdm1 example defines its IOCTL codes in Ioctl.h, as shown in this example.

//define IOCTL_WDM1_ZERO_BUFFER CTL_CODE(
 FILE_DEVICE_UNKNOWN,
 0x801,
 METHOD_BUFFERED,
 FILE_ANY_ACCESS)

Table 7.4 CTL_CODE macro parameters

Parameter Description
DeviceType FILE_DEVICE_XXX value given to IoCreateDevice.
Control Code IOCTL Function Code 0x000–0x7FF Reserved for Microsoft 0x800-0xFFF Private codes
TransferType METHOD_BUFFERED METHOD_IN_DIRECT METHOD_OUT_DIRECT METHOD_NEITHER
RequiredAccess FILE_ANY_ACCESS FILE READ_DATA FILE_WRITE_DATA FILE READ_DATA|FILE_WRITE_DATA

Ioctl.h is also included in the Wdm1Test project. It includes the standard winioctl.h header file first to get the definition of CTL_CODE.

System Control

Dispatch.cpp also contains a handler for the Windows Management Instrumentation IRP, IRP_MJ_SYSTEM_CONTROL. Wdm1SystemControl simply passes the IRP down to the next driver in the stack. A full explanation of this process is given later.

IoSkipCurrentIrpStackLocation(Irp);
PWDM1_DEVICE_EXTENSION dx = (PWDMl_DEVICE_EXTENSION)fdo->DeviceExtension;
return IoCallDriver(dx->NextStackDevice, Irp);

Оглавление книги


Генерация: 0.031. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз