52964.fb2
This chapter introduces the basic concepts and structures that you will need to write drivers. Some ideas are sufficiently important and deserve to be mentioned now. All the other techniques, routines, and objects are covered in the following chapters.
Windows uses a layered design for passing requests from the end user to a driver stack and eventually to the hardware. As a trusted part of the operating system, a driver must be written well to fit into the kernel. It should use the kernel's abstract model of the processor and use memory carefully. Wherever possible, a driver should use the recommended techniques, routines, and structures.
A good device driver needs to be well written. Remember that it is a trusted part of the operating system. The PC will not get the protection that a normal Win32 application has. If anything goes wrong, it is likely that the system will crash, or worse — destroy valuable data before crashing.
For important projects, my ultimate design philosophy is to design code three times. At the outset, it is definitely worth while knowing where I am going and what features I am going to implement. I then prefer an incremental approach, in which each individual section of code is designed as I go along. However, as with many programs, it often only when I have finished a first-cut prototype of the whole driver that I really understand what is going on.
Doing some real hardware interactions might invalidate some of my initial design decisions. Similarly, I might decide to expose an IOCTL interface instead of reads and writes. In the end, it might be best to scrap some sections of code and start again[4].
It is particularly important that you document your driver well and provide ample comments in your code. Device drivers are complicated enough at the best of times. Having to look at someone else's code without any explanation of what is going on can be difficult.
Your hardware and firmware engineering colleagues should have provided you with a suitable interface specification for the device you have to control. You will bring these requirements together with the specification of any bus or class drivers you intend to use. At the end of the day you will have specified your driver's lower edge — the hardware and software interactions that you use to get your job done.
Similarly, you should define your upper edge. This is the interface that your driver presents to user mode applications or higher-level drivers. You need to define what devices you make available and which Win32 functions can be used to access the devices. Specify the parameters in detail and the contents of all buffers. Give your Win32 colleagues a suitable header file.
Whatever your approach, a good design needs to be configurable, portable, preemptive, interruptible, and multiprocessor-safe.
A driver should be as configurable as possible — at least as configurable as the hardware allows. A device on a configurable bus ought to have a Plug and Play driver. Use the registry and the control panel to control how a driver runs.
Making a driver portable between platforms is usually fairly straightforward. Drivers should be easily recompiled to support non-x86 processors. WDM drivers need to run on W98 and W2000. Make sure that you only use routines that are available on both platforms. Write the driver in C or C++ and avoid using assembly language. Use the appropriate kernel routines to access hardware. Beware of data types that have different sizes on different processors. Other processors may have different virtual memory page sizes.
When your driver is running at a low interrupt level (see the following text), the operating system may perform a context switch at any time. Remember that higher priority interrupts can occur at any time, so your driver must bear this in mind. Even your ISR can be interrupted.
In an NT/W2000 multiprocessor system, your driver could be called simultaneously on different processors, so the code needs to be reentrant. As NT/W2000 supports symmetric multiprocessing, an interrupt may be handled on any processor. Even on a single processor system, your code needs to be reentrant. Your code could block while processing one IRP, or a context switch could occur. This could allow another IRP to start processing before the first IRP has completed.
Various techniques and routines are used to handle interrupts and multiprocessor systems. For example, critical sections ensure that a device operation is not interrupted. Spin lock tokens can guard access to work queues. Various dispatcher objects (events, Mutexes, semaphores, timers, and threads) can be used to signal conditions and synchronize with the other parts of your driver.
The kernel also provides mechanisms to arbitrate access to shared hardware devices such as the system DMA controllers. In NT7W2000, if your own hardware has a controller that is shared between devices, use the Controller object to arbitrate access, rather than rolling your own.
Look to see if the kernel already provides you with a feature (e.g., string manipulation facilities and management of a pool of memory blocks). These facilities are discussed as appropriate throughout the book.
A good driver checks each and every return value from any routines that it uses (e.g., in the kernel or other drivers). If an error occurs, make sure you rollback your transaction correctly before returning the error.
The layered device driver design
By now you will, of course, be writing Y2K-compliant code. However, this problem may not occur all that often in drivers. If appropriate, your driver should include accessibility options for people who have difficulty performing certain tasks. Bear in mind that many of your users may not use English as their first language, so internationalize as necessary. For example, provide different language event messages. Note that the strings that drivers must use are in Unicode.
Finally, it is best if you follow the driver routine naming conventions described later. Document and test your work as you go along. At the end, go through all the comments and make sure they are up to date.
Device drivers cannot access any standard C, C++, or Win32 functions. You cannot use the C++ new and delete operators. Instead, you must make use of a large number of kernel routines. The main categories of kernel routines are shown in Table 3.1.
The book describes each kernel routine when it is first used. However, I decided not to provide a full list of each function's parameters because it would take up too much room. You may find it useful to have the Driver Development Kit (DDK) documentation to hand. However, I hope that you can follow the thread of the argument without having to sit beside your computer.
If you are using one of the proprietary toolkits listed in Chapter 1, these will provide a set of classes that make drivers easier to write. This includes routines that make it easier to call the kernel. For example, drivers must use Unicode strings when talking to the rest of the kernel. The kernel routines that manipulate Unicode strings are fairly laborious to use. Most proprietary toolkits provide a class wrapper for Unicode strings that makes them easier to use.
There are various Win32 function calls that are used only in connection with device drivers. In particular, the SetupDi… routines are used to find devices that implement a specified device interface.
Table 3.1 Kernel Mode Routines available to drivers
Ex… | Executive Support |
Hal… | Hardware Abstraction Layer (NT/W2000 only) |
Io… | I/O Manager (including Plug and Play routines) |
Ke… | Kernel |
Ks… | Kernel stream IRP management routines |
Mm… | Memory Manager |
Ob… | Object Manager |
Po… | Power Management |
Ps… | Process Structure |
Rtl… | Runtime Library |
Se… | Security Reference Monitor |
Zw… | Other Routines |
Table 3.2 shows routines that are used only by specific types of driver. Most of these are provided for miniclass drivers, miniport drivers, and minidrivers. Others, such as the Hid… and Usb… functions are used by client drivers.
In addition to those routines listed, audio miniport drivers define several standard interfaces. The IEEE 1394 bus driver makes many routines available, with no common initial name.
Table 3.2 Bus driver and class specific routines
BatteryClass… | Battery class routines for miniclass drivers |
Hid… | Human Input Device routines |
Pc… | Port class driver routines |
ScsiPort… | SCSI port driver routines for miniport drivers |
StreamClass… | Stream class driver functions for stream minidrivers |
TapeClass… | SCSI Tape class routines for miniclass drivers |
Usb… | Universal Serial Bus Driver Interface routines for USB client drivers |
If you look in the DDK header files, you will find that a few of the kernel functions are implemented as macros. The definition of one or two of these macros is quite poor. For example, RemoveHeadList is defined as follows:
#define RemoveHeadList(ListHead) (ListHead)->Flink; {RemoveEntryList((ListHead)->Flink);}
If you call RemoveHeadList in the following way, the wrong code is compiled.
if (SomethingInList) Entry = RemoveHeadList(list);
The only way to make this safe is to use braces.
if (SomethinglnList) {
Entry = RemoveHeadList(list);
}
Therefore, to be on the safe side, it is best to use braces in all if, for, and while statements, etc.
The DDK documentation makes much use of the word object when describing kernel structures. This does not mean object in the C++ sense of the word. However, it means the same in principle. In the purest definition, a kernel object should only be accessed using kernel function calls. In practice, each kernel structure usually has many fields that can be accessed directly. However, it is definitely true that some fields should be considered private and not touched by your driver. This book describes which fields can be used safely in each kernel object.
There is a useful convention for naming driver routines, similar to the kernel API naming scheme. Each routine name should have a small prefix based on the driver's name (e.g. Par for a Parallel port driver). This prefix should be followed by verbs or nouns as necessary. This naming scheme makes it easier to identify a driver when looking at a debugger trace. A driver initially has just one exposed routine that must be called DriverEntry so it can found.
Many driver routines have standard names that most people use. For example, the Create IRP handler in the Wdm1 driver is called Wdm1Create; the Read IRP is handled in Wdm1Read, etc.
Windows 98, NT, and Windows 2000 all primarily run in x86 systems. However, NT and W2000 can also run on Alpha processors. To handle any differences, WDM uses a model of a general processor and provides an abstract view of the resources available on the supported processors.
Always use kernel routines to access hardware. For some types of drivers, you do not need to talk to hardware directly. Instead, make the appropriate calls to the relevant bus driver to get it to perform the low-level I/O for you.
The operating system requires that the processor support only two modes: a user mode for one or more applications to run in and a kernel mode for the bulk of the operating system. User mode programs are protected so that they cannot easily damage each other or the kernel. Conversely, the kernel can do anything it wants to and can access all memory.
Interrupts and exceptions are ways of stopping the processor from doing something and asking it to do something else. A hardware interrupt is an input signal to the processor from external hardware. Software interrupts are instructions that interrupt the processor to switch it to kernel mode. Exceptions are interrupts generated by the processor, usually when something goes wrong.
User programs make kernel function calls using software interrupts. In fact, a Win32 call initially goes to a user mode Win32 subsystem DLL. If appropriate, this then calls the main operating system in kernel mode.
The kernel uses a regular timer hardware interrupt to switch between the available user program threads of execution. This makes it appear that the threads are running simultaneously. On multiprocessor systems, the threads really can be running simultaneously. The kernel can also have its own threads running, which are switched in the same way as user threads.
The result is that drivers can be called in several ways. A user application can issue a Win32 device I/O request. Eventually, a driver is called to process this call. The driver might then start its hardware. If its hardware generates an interrupt, then the driver is called to process it (i.e., stop the interrupt), flag that it has happened, and, if necessary, start another operation. In both these cases, the driver does not necessarily operate in the context of the user application that called it.
Drivers can also be called in the context of a kernel thread. For example, the Plug and Play calls to a driver are usually called in the context of a system thread. As will be shown, this gives the driver more scope to do things. Finally drivers can set up their own kernel mode threads that can run as long as needed, getting their own share of the processor time.
Hardware or software interrupts stop the processor from doing one task and force it to run some interrupt handling code. A processor prioritizes interrupts so that lower priority interrupts can themselves be interrupted by higher priority interrupts. This makes sure that very important tasks are not interrupted by jobs that can be done at a later stage.
Table 3.3 Abstract Processor Interrupt Levels
IRQL | Description | |
---|---|---|
No interrupts | PASSIVE_LEVEL | Normal thread execution |
Software interrupts | APC_LEVEL | Asynchronous Procedure Call execution |
DISPATCH_LEVEL | Thread scheduling Deferred Procedure Call execution | |
Hardware interrupts | DIRQLs | Device Interrupt Request Level handler execution |
PROFILE_LEVEL | Profiling timer | |
CLOCK2_LEVEL | Clock | |
SYNCH_LEVEL | Synchronization level | |
IPI_LEVEL | Interprocessor interrupt level | |
POWER_LEVEL | Power failure level |
Table 3.3 shows the interrupt levels that Windows uses to provide an abstract view of the actual processor interrupt levels. The lowest priority interrupt levels are at the top. Hardware-generated interrupts always take priority over software interrupts.
Drivers only use three of these interrupt levels. Somewhat confusingly, driver dispatch routines are called at PASSIVE_LEVEL[5] (i.e., with no interrupts). Many driver callbacks run at DISPATCH_LEVEL. Finally, driver hardware interrupt service routines operate at Device Interrupt Request Level (DIRQL).
The DIRQLs level in fact represents the many hardware interrupt levels that are available on the specific processor. A disk hardware interrupt level may well have higher priority than a parallel port hardware interrupt, so the disk may interrupt a parallel port interrupt. Usually a driver has just one DIRQL, though there is no reason why it cannot support more, if that is how its hardware works. The relevant driver interrupt level is referred to as its DIRQL.
The interrupt level at which a driver is operating is very important, as it determines the types of operation that can be undertaken. For example, driver hardware interrupt service routines cannot access memory that might be paged out to a swap file.
A driver has to be aware that it could be interrupted by a higher priority task at any stage. This means that its own interrupt service routine could run in the middle of its own dispatch routines. For this reason, if a normal driver routine is about to interact with its own hardware, it uses a kernel function to raise its interrupt level temporarily to its DIRQL. This stops its own interrupt handler from running at the same time. In addition, in a multiprocessor system, another driver routine might be attempting the same task on another processor. The KeSynchronizeExecution routine is used to run such critical sections in a multiprocessor-safe way.
The Interrupt Level should not be confused with the scheduling priority. All threads normally run at the lowest interrupt level, PASSIVE_LEVEL. The scheduler uses the priority values to determine which thread to run next. Any other interrupt takes priority over a thread.
Drivers need to be aware of scheduling priorities. When it completes an IRP, it can give the calling thread a temporary priority boost so that, for example, programs that interact with the mouse can continue their run more quickly. This technique generally improves the perceived responsiveness of the system.
Any routine servicing a hardware interrupt stops normal program execution. For this reason, it is best to make an interrupt service routine as quickly as possible.
Any nonessential interrupt processing should be deferred until later. Windows lets driver writers use Deferred Procedure Call (DPC) routines, which are called at DISPATCH_LEVEL. The interrupt routine requests that the driver's DPC post-interrupt routine (DpcForIsr) is called when things have calmed down. A typical job in this routine is to indicate that the current I/O request is complete; the relevant kernel call can only be carried out at DISPATCH_LEVEL
When writing your driver, be careful to comment the interrupt level at which each routine can be called. Similarly, make only kernel calls that are appropriate for the current interrupt level.
A device driver has to be very careful when allocating or accessing memory. However, it is not as gruesome to access memory as some other types of driver. For example, you do not need to worry about the murky x86 world of segments and selectors, as the kernel handles all this stuff.
Windows implements virtual memory, where the system pretends that it has more memory than it really has. This allows more applications (and the kernel) to keep running than would otherwise be the case.
Virtual memory is implemented by breaking each application's potential address space into fixed size chunks called pages. (x86 processors have a 4KB-page size, while Alpha processors use 8KB.) A page can either be resident in physical memory, or not present and so swapped to hard disk.
Drivers can allocate memory that can be paged out, called paged memory. Alternatively, it can allocate memory that is permanently resident, called nonpaged memory. If you try to access paged memory at DISPATCH_LEVEL or above, you will cause a page fault and the kernel will crash. If you access nonresident paged memory at PASSIVE_LEVEL, the kernel will block your thread until the memory manager loads the page back into memory.
Please do not make extravagant use of nonpaged memory. However, you will find that most of the memory your driver uses will be in nonpaged memory. You must use nonpaged memory if it is going to be accessed at DISPATCH_LEVEL or above. Your driver StartIo and ISRs, for example, can access only nonpaged memory.
Table 3.4 shows how to allocate both paged and nonpaged memory using the kernel ExAllocatePool function. The table uses the DDK convention of listing IN or OUT before each parameter. IN parameters contain information that is passed to the function, and vice versa. Instances of IN and OUT in DDK header files are removed using macros.
Specify the ExAllocatePool PoolType parameter as PagedPool if you want to allocate paged memory or NonPagedPool if you want to allocate nonpaged memory. The other PoolType values are rarely used. Do not forget to check whether a NULL error value was returned by ExAllocatePool. The ExAllocatePool return type is PVOID, so you will usually need to cast the return value to the correct type.
When you are finished using the memory, you must release it using ExFreePool, passing the pointer you obtained from ExAllocatePool. Use ExFreePool for all types of memory. If you forget to free memory, it will be lost forever, as the kernel does not pick up the pieces after your driver has unloaded.
The ExAllocatePoolWithTag function associates a four-letter tag with the allocated memory. This makes it easier to analyze memory allocation in a debugger or in a crash dump.
Table 3.4 ExAllocatePool function
PVOID ExAllocatePool | (IRQL<=DISPATCH_LEVEL) If at DISPATCH_LEVEL, use one of the NonPagedXxx values |
---|---|
Parameter | Description |
IN POOL_TYPE PoolType | PagedPool PagePoolCacheAligned NonPagedPool NonPagedPoolMustSucceed NonPagedPoolCacheAligned NonPagedPoolCacheAlignedMustS |
IN ULONG NumberOfBytes | Number of bytes to allocate |
Returns | Pointer to allocated memory or NULL |
If your driver keeps on allocating and deallocating small amounts of pool memory then it will be inefficient and the available heap will fragment. The kernel helps in this case by providing lookaside lists[6] for fixed-size chunks of memory. A lookaside list still gets memory from the pool. However, when you free a chunk of memory, it is not necessarily returned to the pool. Instead, some chunks are kept in the lookaside list, ready to satisfy the next allocation request. The number of chunks kept is determined by the kernel memory manager.
Lookaside lists can contain either paged or nonpaged memory. Use ExInitializeNPagedLookasideList to initialize a lookaside list for nonpaged memory and ExDeleteNPaged-LookasideList to delete the lookaside list. When you want to allocate a chunk of memory, call ExAliocateFromNPagedLookasideList. To free a chunk, call ExFreeToNPaged-LookasideList. A similar set of functions is used for paged memory. Consult the DDK for full details of these functions.
The kernel stack is nonpaged memory for local variables[7]. There is not room for huge data structures on the kernel stack because it is only 8KB-12KB long.
Drivers need to be reentrant, so they can be called simultaneously on different processors. The use of global variables is, therefore, strongly discouraged. However, you might read in some registry settings into globals, as they are effectively constant for all the code. Local static variables should also not normally be used for the same reason.
Finally, you can reduce your driver's memory usage in other ways. Once the driver initialization routines have completed, they will not be needed again, so they can be discarded. Similarly, some routines may be put into a pageable code segment. However, routines running at DISPATCH_LEVEL or above need to be in nonpageable nondiscardable memory.
There are two main techniques for accessing user data buffers. If you use Buffered I/O, the I/O Manager lets you use a nonpaged buffer visible in system memory for I/O operations. The I/O Manager copies write data into this buffer before your driver is run, and copies back read data into user space when the request has completed.
The alternative technique, Direct I/O, is preferable, as it involves less copying of data. However, it is slightly harder to use and is usually only used by DMA drivers that transfer large amounts of data. The I/O Manager passes a Memory Descriptor List (MDL) that describes the user space buffer. While a driver can make the user buffer visible in the system address space, the MDL is usually passed to the DMA handling kernel routines.
Direct Memory Access (DMA) hardware controllers perform I/O data transfers direct from a device to main memory (or vice versa) without going through the processor. The first implication is that DMA memory cannot be paged. Secondly, the DMA controller has to be programmed with a physical memory address, not a processor virtual address. The kernel provides routines to help with both these tasks.
I/O Request Packets (IRPs) are central to the operation of drivers. It is helpful to look at them briefly at this point, but they are described in full later. In particular, this section looks at how IRP parameters are stored and how the IRP stack lets an irp be processed by a stack of drivers.
An IRP is a kernel "object", a predefined data structure with a set of I/O Manager routines that operate on it. The I/O Manager receives an I/O request and then allocates and initializes an IRP before passing it to the highest driver in the appropriate driver stack.
An IRP has a fixed header part and a variable number of IRP stack location blocks, as shown in Figure 3.1. Each I/O request has a major function code (such as IRP_MJ_CREATE corresponding to a file open) and possibly a minor function code. For example, the IRP_MJ_PNP Plug and Play IRP has several minor functions (e.g., IRP_MN_START_DEVICE). Table 3.5 lists the common IRP major function codes.
Table 3.5 Common IRP Major Function Codes
IRP_MJ_CREATE | Create or open device file |
IRP_MJ_CLOSE | Close handle |
IRP_MJ_READ | Read |
IRP_MJ_WRITE | Write |
IRP_MJ_CLEANUP | Cancel any pending IRPs on a file handle |
IRP_MJ_DEVICE_CONTROL | Device I/O control |
IRP_MJ_INTERNAL_DEVICE_CONTROL This is also called IRP_MJ_SCSI | Device I/O control from a higher driver |
IRP_MJ_SYSTEM_CONTROL | Windows Management Instrumentation |
IRP_MJ_POWER | Power Management request |
IRP_MJ_PNP | Plug and Play message |
IRP_MJ_SHUTDOWN | Shutdown notification |
The fixed part of an IRP (the IRP structure itself) contains fixed attributes of the IRP. Each stack location — an IO_STACK_LOCATION structure — in fact contains most of the pertinent IRP parameters.
More than one IRP stack location is used when an IRP might be processed by more than one driver. Each driver gets its irp parameters from the current IRP stack location. If you pass an irp down the stack of drivers for the current device, you must set up the next stack location with the correct parameters. The parameters that you pass down may be different from the ones you are working with.
Figure 3.1 IRP Overview
When a write I/O request is made into an IRP, the I/O Manager fills in the main IRP header and builds the first IRP stack location. For a write, the IRP header contains the user buffer information. If you use Buffered I/O, the IRP AssociatedIrp.SystemBuffer field contains a pointer to the nonpaged copy of the user's buffer. For Direct I/O, the IRP MdlAddress field has a pointer to the user buffer MDL.
The IRP stack location has the main write request parameters. The stack MajorFunction field has the IRP_MJ_WRITE major function code that indicates that a write has been requested. The Parameters.Write.Length field has the byte transfer count and Parameters.Write.ByteOffset has the file pointer. The stack also has other important fields that are described later.
As I said earlier, you have to set up the next stack location if you call another driver. This means that you can theoretically change the IRP major function code to something else. While this might work in some cases, it is generally not a good idea, as the parameters in the fixed part of the IRP might not be correct.
However, it might be appropriate to change the number of bytes to transfer and the file pointer. You might do this if you know that the lower driver can only handle short transfers. To handle a large transfer, you might, therefore, send the IRP down several times. Each transfer request you send down will be within the capabilities of the lower driver.
In practice, the I/O stack locations are not usually used to alter these fundamental IRP parameters. Instead, the IRP stack is normally used to let an IRP be processed by all the drivers in a device stack.
Figure 3.2 shows how an IRP might be processed by four drivers in the device stack. The first IRP arrives at the highest driver, Driver 1. This uses the function IoGetCurrentlrpStack-Location to obtain a pointer to the current stack location. The figure shows that this returns the topmost IRP stack location.
Driver 1 decides that it needs to pass the IRP down the stack for processing. The IRP might be a Power Management IRP that the lowest bus driver needs to see. Driver 1 might not do anything with this IRP, but it still needs to pass the IRP down the stack.
Driver 1 therefore sets up the stack location for the next driver. In many cases, it simply copies the current stack location to the next using the IoCopyCurrentlrpStackLocation-ToNext or IoSkipCurrentlrpStackLocation functions. If you need to alter the next stack location, then use IoGetNextlrpStackLocation to get a pointer to it.
Driver 1 then calls the next driver down the stack using the IoCallDriver function. The I/O Manager now changes the "current IRP stack location" pointer so that Driver 2 sees the second IRP stack location down (the one that Driver 1 set up for it). This process continues until the lowest driver, Driver 4, receives the IRP.
Driver 4 now processes the IRP. When it has finished with the IRP, Driver 4 calls IoCompleteRequest to indicate that it has finished processing the IRP. The IRP travels back up the device stack until it eventually pops out the top and is returned to the user.
Each of the drivers in the stack is given an opportunity to work with the IRP again as it travels up the stack, if they wish to do so. To do this, a driver must attach a completion routine to an IRP using the IoSetCompletionRoutine function. The completion routine information is stored in each driver's IRP stack location. This lets each of the drivers numbered 1 to 3 work on the IRP as it travels back up the stack in the order: Driver 3, Driver 2, and then Driver 1. A driver does not need to attach a completion routine. In this case, the I/O Manager does not call the driver as the IRP passes up the stack.
Figure 3.2 How all drivers in a stack process an IRP
A driver does not have to pass an IRP down the stack. If it detects an error in a parameter, or is able to process the IRP itself, then it should do its job and complete the IRP with IoCompleteRequest.
When a driver processes an IRP as it travels back up the device stack, it does not necessarily let the IRP progress further up the stack straightaway. If Driver 2 for example, splits Write IRPs into small chunks that Driver 3 can digest, then it may send the IRP back down the stack again, with changed parameters.
A further possibility is that a driver can build a new IRP and send it down the stack. Driver 2 might be processing a read request. To satisfy this request, it has to send an IOCTL request to the lower drivers. When the IOCTL request returns, Driver 2 checks that it worked satisfactorily and carries on processing its Read IRP.
To reiterate, an IRP includes a stack of I/O request operations. A driver only looks at the current IRP stack location and does not have to worry if there are higher-level drivers above.
Windows provides a well-defined hierarchy and structure for drivers. Go with the grain and learn how to fit in nicely with the rest of the operating system. Do not forget to cope with multiprocessor machines.
Windows provides a generic model of different processors that may be running. Drivers have routines that run at PASSIVE_LEVEL, DISPATCH_LEVEL, and DIRQL interrupt levels. Use system memory carefully and access user memory using the relevant kernel routines.
The kernel calls a driver in many ways. Most driver processing is in response to I/O Request Packets (IRPs). Conversely, a driver can make use of a whole host of kernel routines. Many system drivers have specific interfaces defined to handle particular I/O control codes. Drivers should use these interfaces wherever possible.
Enough prevarication, let's start on the first real device driver. The following chapters describe the Wdm1 virtual device driver in detail, how it is implemented and used, and how to test and debug drivers.
I used this approach when writing this book. My initial contents were completely different from the end result, and I rewrote these initial chapters after the rest of the book was complete.
If a file system driver is fetching a page back into memory then the IRQL may be APC_LEVEL.
NT 3.51 drivers should use zone buffers, which are now obsolete.
Actually, the kernel stack may be paged if a driver issues a user-mode wait (e.g. in KeWaitForSingleObject).