My Collection


Table Of Contents
Chapter 1
The Debugging Application Programming Interface
/CGTHREADS (Compiler Threads)
Getting Started (Debug Interface Access SDK)
Concepts for all driver developers
User mode and kernel mode
Virtual address spaces
Device nodes and device stacks
I/O request packets
Driver stacks
Minidrivers, miniport drivers, and driver pairs
KMDF as a generic driver pair model
KMDF extensions and driver triples
Upper and lower edges of drivers
Header files in the Windows Driver Kit
Writing drivers for different versions of Windows


Chapter 1
Export (0) Print
Expand All

The Debugging Application Programming Interface

 

Randy Kath
Microsoft Developer Network Technology Group

November 5, 1992


Abstract

This article demonstrates how the debugging support in the Microsoft Windows Application Programming Interface (API) can be used by developers to create custom debugging applications that behave exactly the way they want, including any specific features they desire. Specifically, this article discusses the following topics:

  • Exploring the built-in debugging support, including debug events and debug functions
  • Looking at the relationship between a debugger and the process being debugged
  • Representing information about a process being debugged
  • Using event objects for communicating between debugger threads
  • Managing the debugger's graphical user interface (GUI)
  • Responding to user commands in debug threads
  • Controlling the threads of a process being debugged
  • Accessing thread context information from threads of a process being debugged
  • Terminating and exiting a process being debugged
  • Calling debug functions from a process being debugged
  • Expanding on this debugger model

Each of the key concepts presented is supported with code segments extracted from a sample debugging application called DEBUGAPP.EXE, whose source is included with this article. The sample application stands on its own as a multiprocess debugging application, or its source code can be used as the framework for a more elaborate custom debugger.

Introduction

Of the time a programmer spends developing an application, a large portion is usually spent debugging that application. Consequently, developers rely on third-party debuggers almost as much as they do editors. Unlike editors, however, debuggers can rarely be customized much. If a debugger lacks an important feature or behaves in an unusual or irritating way, developers are simply forced to put up with it.

Windows appears ready to break this cumbersome debugging tradition with new, built-in debugging support included as part of the standard application programming interface (API). Now developers have the flexibility to create their own personal debugger that behaves exactly the way they wish. And once that is complete, having the source code to that debugger makes it all the more flexible. Developers can repeatedly add new features directly to the source code of the debugger as needed in the future.

The debugging architecture consists of a clean, relatively straightforward set of functions and events that make it useful to all developers, not just debugger builders. Simply being familiar with Windows and, more importantly, the Windows API is enough to build an understanding of the debugging support. The debugger sample application described in this article required only about three weeks for implementation, including the time required to make sense of the API.

Exploring the Built-In Debugging Support

DebugApp, the sample application associated with this article, is a high-level debugger that meets a number of important requirements for a debugger. It can debug multiple applications simultaneously, controlling the execution of each process being debugged and presenting feedback about noteworthy events that occur in each of the processes. It can also be used to view the 2 gigabyte (GB) heap space of each process for learning how memory allocations are organized. These are only some of the capabilities that could be added to a debugger. To get a better feel for what capabilities can be implemented in a debugger, you will need to gain some knowledge of how the debugging API works.

Debug Events

Debug events are the objects of interest to a debugger—they're noteworthy incidents that occur within the process being debugged, causing the kernel to notify the debugger when they occur. As defined by Windows, debug events are one of the following:

  • CREATE_PROCESS_DEBUG_EVENT occurs before a new process being debugged initializes or at the time a debugger attaches to an active process.
  • EXIT_PROCESS_DEBUG_EVENT occurs when the process being debugged exits.
  • CREATE_THREAD_DEBUG_EVENT occurs when the process being debugged creates a new thread.
  • EXIT_THREAD_DEBUG_EVENT occurs when a thread in the process being debugged exits.
  • LOAD_DLL_DEBUG_EVENT occurs when the process being debugged loads a DLL (either explicitly or implicitly).
  • UNLOAD_DLL_DEBUG_EVENT occurs when the process being debugged frees a DLL.
  • EXCEPTION_DEBUG_EVENT occurs when an exception occurs in the process being debugged.
  • OUTPUT_DEBUG_STRING_DEBUG_EVENT occurs when the process being debugged makes a call to the OutputDebugString function.

When a debug event is generated, it comes to the debugger packaged in a DEBUG_EVENT structure. The structure contains fields that represent an event code (listed above), the process ID of the process that generated the debug event, the thread ID of the thread executing when the debug event occurred, and a union of eight structures, one for each of the different events. This structure provides information necessary for the debugger to distinguish between different debug events and process them individually based on their unique requirements. The DEBUG_EVENT structure is:

typedef struct _DEBUG_EVENT {   /* de */
    DWORD dwDebugEventCode;
    DWORD dwProcessId;
    DWORD dwThreadId;
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO CreateProcess;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
    } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

Debug Functions

Two functions, WaitForDebugEvent and ContinueDebugEvent, are designed specifically for managing debug events as they occur in a process being debugged. These functions permit a debugger to wait for a debug event to occur, suspend execution of the process being debugged, process each debug event, and resume execution of the process being debugged when finished. Additionally, while the process being debugged is suspended, the debugger is able to change the thread context information of each of its threads. This ability provides a mechanism through which the debugger can alter normal execution of one or more threads in the process being debugged. It can, for example, change the instruction pointer for a thread to refer to an instruction at a new location. Then, when the thread resumes execution, it begins executing code at the new location. A discussion of this subject is presented later in the "Accessing Thread Context Information from Threads of a Process Being Debugged" section.

When called, the WaitForDebugEvent function does not return until a debug event occurs in the process being debugged or a time-out value is reached. The time-out value is set as one of the parameters in the function. The function returns TRUE if an event occurs, and FALSE if the function times out.

DEBUG.C

while (TRUE)
    {
    /* Wait for 1/10 second for a debug event. */
    if (WaitForDebugEvent (&de, (DWORD)100))
        {
        switch (de.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                ProcessExceptionEvent (&de);
                break;

            case CREATE_PROCESS_DEBUG_EVENT:
                ProcessCreateProcessEvent (&de);
                break;

            .
            .
            .

            case default:
                ProcessUnknownDebugEvent (&de);
                break;
            }
        ContinueDebugEvent (de.dwProcessId,
                            de.dwThreadId,
                            DBG_CONTINUE);
        }
    else
        /* Perform periodic debugger responsibilities. */
    }

In the code fragment above, notice that WaitForDebugEvent returns a Boolean value where a value of TRUE indicates that a debug event occurred and FALSE means that the function timed out. This example waits for 1/10 second for a debug event to occur, but if an event does not occur in that amount of time, it uses the time-out indicator to perform some other periodic debugger responsibilities. Since time-outs only happen when there are no debug events, this time is analogous to the idle time a CPU observes. Specifically, the DEBUGAPP.EXE sample uses this time to communicate with the main debugger thread in order to process user commands.

When a debug event occurs, execution of that process is suspended until the debugger calls the ContinueDebugEvent function. Consequently, all threads in the process are suspended while the debugger is processing the debug event. A debugger needs to be mindful of the performance impact this will impose on the process being debugged. A good design, in this case, is one that allows the process being debugged to continue as soon as possible after a debug event occurs. On the other hand, when the WaitForDebugEvent function times out, the process being debugged is able to run concurrently with the debugger process and no performance impact is observed. Any debug events that occur during the time-out period are queued until the debugger calls the WaitForDebugEvent function again. So, no need to worry—there is no possibility of missing a debug event because of this circumstance.

To call the ContinueDebugEvent function, the debugger must supply as parameters the thread ID and process ID of the process that generated the last debug event. Both the process and thread IDs are included as part of the DEBUG_EVENT structure with each debug event. They're also returned as part of the PROCESS_INFORMATION structure filled out by the CreateProcess function when starting a process for debugging. A debugger can attach to an active process for debugging, but an ID for that process is required prior to the attachment. Once the debugger has attached, the thread ID is retrieved from the DEBUG_EVENT structure.

The Relationship Between a Debugger and the Process Being Debugged

For one application (process) to become the debugger of another, it must either create the process as a debug process or attach to an active process. In both cases, a parent/child relationship is established between the debugger and the process being debugged. If the debugger process ends without ending the process being debugged, the latter process is terminated by the system. If the process being debugged ends, the debugger process becomes a normal process, able to start or attach to another process to debug.

When the parent/child association is made, the debugger thread responsible for establishing this dependence—the thread that attaches or starts the process to be debugged—becomes the parent thread to the process being debugged. Only the parent thread of a process being debugged is capable of receiving debug events for that process. Consequently, the parent thread is the only thread able to call the WaitForDebugEvent and ContinueDebugEvent functions. If another thread calls these functions, they simply return FALSE. The basis for the design of the sample application, DEBUGAPP.EXE, is inherent in this requirement.

Creating a process to debug

To create a process for debugging, the debugger calls the CreateProcess function with the fdwCreate parameter set to either DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS. DEBUG_PROCESS sets up the parent/child relationship so that the debugger will receive debug events from a process being debugged and any other processes created by that process. In this case, processes created by the process being debugged are automatically debugged by the same debugger. Using DEBUG_ONLY_THIS_PROCESS restricts debugging to the immediate process being debugged only. Processes created by the process being debugged are normal processes that have no debugging relationship established with any other process.

An abbreviated definition of CreateProcess is found below. A complete definition of the CreateProcess function is in the Platform SDK.

BOOL CreateProcess(
   LPCTSTR lpszImageName,        /* address of image file name */
   LPCTSTR lpszCommandLine,      /* address of the command line */
   LPSECURITY_ATTRIBUTES lpsaProcess,  /* optional process attrs */
   LPSECURITY_ATTRIBUTES lpsaThread,   /* optional thread attrs */
   BOOL fInheritHandles,         /* new process inherits handles? */
   DWORD fdwCreate,                           /* creation flags */
   LPVOID lpvEnvironment,    /* address of optional environment */
   LPTSTR lpszCurDir,        /* address of new current directory */
   LPSTARTUPINFO lpsi,                 /* address of STARTUPINFO */
   LPPROCESS_INFORMATION lppi);  /* address of PROCESSINFORMATION */

CreateProcess includes several parameters for establishing the environment of the process being debugged, passing command-line arguments to the process being debugged, specifying security attributes about the process, and indicating how to start the application. The LPPROCESS_INFORMATION parameter is used for receiving information about the process being started. Specifically, it consists of the process and thread IDs of the process being debugged that are used in ContinueDebugEvent and handles to both the process being debugged and its initial thread.

Attaching a debugger to an active process

A debugger can attach to any existing process in the system, providing that it has the ID of that process. Through the DebugActiveProcess function, a debugger can establish the same parent/child relationship described earlier with active processes. In theory, then, the debugger should be able to present a list of active processes to the user, allowing them to select which one they would like to debug. Upon selection, the debugger could determine the ID of the selected process and begin debugging it by means of the DebugActiveProcess function. All that is needed then is a mechanism for enumerating the handles of each active process in the system. Unfortunately, Windows provides no support for determining the ID of other processes in the system. While this seems to render the function useless, it really just limits the way it can be used. On its own, a debugger process cannot determine the ID of other active processes, but with some help from the system it can get the ID of a specific process in need of debugging.

Built into Windows is the ability for the system to start a debugger upon the occurrence of an unhandled exception in a process. When such an exception occurs, Windows starts the default debugger and passes it the ID of the process to be debugged as a command-line parameter. Windows also passes an event handle as a second command-line parameter. The debugger then calls the DebugActiveProcess function using the process ID passed as a command-line parameter. Once the debugger has established the debugging relationship with the offending process, it signals that it is ready to begin debugging by calling the SetEvent function on the event handle. At that time the system releases control of the process to the debugger.

Note
   
The system administrator can change the default debugger in Windows to be any third-party debugger or any other debugger you choose. To do this, an entry must be added to the WIN.INI file as indicated:
[AeDebug]
  Debugger = ntsd -d -p %d -e %d -g

To change from the standard default debugger, ntsd, to one of your choosing, simply replace the name ntsd with the name of the debugger you want. Also, make sure that the debugger resides in a directory in the path.

An application can take advantage of this built-in behavior as a way of invoking a debugger to debug itself if, and only if, a special circumstance occurs. For example, an application could be executing normally when a condition occurs that warrants debugging. The application could then start the debugger by calling the CreateProcess function similar to the way it was described above, only not as a process to debug. Also, the process needs to pass its ID as a single command-line parameter to the debugger process. The debugger is then started and passed an ID of the process to debug. One requirement of this technique is that the debugger must differentiate between the two ways that it can be created. The difference is that, when the system starts the debugger, there is a second command-line parameter representing a valid wait event that the debugger must eventually signal. When the process wishing to be debugged creates the debugger, there is no second command-line parameter and no wait event to signal.

DEBUGAPP.EXE, A Sample Implementation

Combining a knowledge of the debugging API and some idea of the features a custom debugger should have is important for devising the architecture of a debugger application. For example, Figure 1 portrays the architecture that was used in implementing the sample custom debugger, DebugApp.

ms809754.debug_1(en-us,MSDN.10).gif

Figure 1. DebugApp's architecture. A main debugger thread manages the debugger interface, while one thread exists for each process being debugged.

DebugApp consists of one main thread and one or more debug threads. The main thread is responsible for handling the entire graphical user interface (GUI) of the debugger. Each of the other debug threads is created and destroyed as new debug sessions are started and ended. This structure serves to contain the debug-specific functionality in the debug threads and the GUI-specific functionality in the main thread. Embedded, then, is a layer of encapsulation that promotes maintenance and revision of the source code. Further, the source code for these two types of threads is located in separate source modules, DEBUG.C and MAIN.C, making it easier to go back and add functionality to one part of the system without having an adverse impact on the other.

Once the underlying structure for the debugger is in place, the next issue is how to represent the debugger and, more specifically, each process being debugged in a single Windows® interface. This implementation uses multiple document interface (MDI) because MDI supports multiple process debugging simultaneously and offers basic multiple window management functionality for free. It turns out MDI is also a good selection because each MDI child window can be used as a separate object capable of maintaining its own private data structures. In that case, writing the interface code to support multiple processes for debugging is no more work than writing it for one.

A final consideration concerns the bells and whistles that should be added to the debugger. DEBUGAPP.EXE need only meet the basic requirements stated earlier in this article, but at this point many more features and behaviors could easily be applied to the underlying debugger architecture. Specifically, DEBUGAPP.EXE implements support for controlling the execution of individual threads in each of the processes being debugged. It also records all debug events chronologically and provides a mechanism for saving this log to a file for post-mortem review.

Representing Information About a Process Being Debugged

DEBUGAPP.EXE uses a single data structure for representing all of the information associated with a process being debugged and its threads. The structure is a simple, singly-linked list where the header (DBGPROCESS structure) represents the information of the process being debugged, and each node (DBGTHREAD structure) in the list represents information about each thread in the process. Figure 2 depicts this information and how it is organized.

ms809754.debug_2(en-us,MSDN.10).gif

Figure 2. A linked list stores information about the process being debugged and its threads.

As the process being debugged creates and destroys new threads, the linked list grows and shrinks dynamically. Storage for this data structure is allocated within the debugger process in the form of a serialized heap. Since all threads in the debugger process have access to a shared heap, their access must be serialized to allow one thread to finish accessing the heap before another thread begins accessing it. Windows provides serialized heaps as a mechanism to prevent access contention between threads that share a heap.

In addition to information about the process being debugged, Windows uses the linked-list header as a place to store information that is communicated between the main thread and the debug thread. Such information includes the MDI child window handle representing a specific debug session, the module path and filename of the process being debugged, a thread number for control information, and a handle to the heap itself, which is used both for destroying the heap when the debug session ends and allocating additional linked-list nodes. Both the linked-list header and node structures are presented in the code below.

DEBUG.H

// Define structures for debugging processes and threads.
typedef struct DBGTHREAD    *LPDBGTHREAD;               
typedef struct tagDbgThread                             
    {                                                   
    HANDLE                    hThread;         
    LPTHREAD_START_ROUTINE    lpStartAddress;  
    BOOL                      bfState;         
    LPDBGTHREAD               Next;            
    }DBGTHREAD;                                
                                               
typedef struct tagDbgProcess                   
    {                                          
    HANDLE       hDbgHeap;                     
    DWORD        dwProcessID;                  
    DWORD        dwThreadID;                   
    HANDLE       hProcess;                     
    HANDLE       hFile;                        
    LPVOID       lpImage;                      
    DWORD        dwDbgInfoOffset;              
    DWORD        nDbgInfoSize;                 
    DBGTHREAD    *lpThreads;                   
    HWND         hWnd;                         
    int          ProcessPriority;              
    HANDLE       hThread;                      
    int          ThreadPriority;               
    char         szModule[MAX_PATH];           
    }DBGPROCESS;                                        

DebugApp's main thread creates a serialized heap for each debug thread prior to creating the thread, and a pointer to the heap is passed to the debug thread at creation time. Both the debug thread and the main thread, each keeping a separate copy of the pointer to the structure, maintain the heap independently. The main thread stores its heap pointer in the window extra bytes of the MDI child window responsible for the process being debugged. This way each window can keep track of its own structures, independent of every other window.

Because the debug thread is implemented as a function (similar to a WinMain function) that returns only when the thread has completed, and because each thread has its own stack, each debug thread can keep the pointer to the heap as an automatic variable on its stack. In this way, each debug thread can access its pointer as a local variable because it resides permanently on the stack for that thread. When the thread exits (returns from the function), its stack is deallocated and the pointer is automatically freed. Keep in mind that the heap itself is not freed by this action; it must be explicitly freed through a call to DestroyHeap. When and where the heap gets deallocated is discussed in the "Terminating and Exiting a Process Being Debugged" section later in this article.

Using Event Objects for Communicating Between Debugger Threads

Because, as described above, the debug thread and the main thread both share access to the same heap, some type of synchronization is necessary for at least creating and destroying that heap. Also, because both the debug thread and the main thread independently perform functions that occasionally must be coordinated, it stands to reason that a debugger needs a mechanism to communicate between threads. DEBUGAPP.EXE uses wait event objects for this purpose.

Windows uses wait events as a signaling mechanism much like a traffic signal, except there's no yellow light. A wait event represents either a signaled or unsignaled state. A thread can wait for one or more of these events to become signaled and then perform some related action. While waiting for an event to become signaled, a thread is idle. Consequently, events are perfect synchronization objects that allow one thread to wait for a signal from another before performing a specific task. In DEBUGAPP.EXE, the main thread opens a set of event objects, one for each debug thread, and stores the handles in a segment of global memory. The memory is treated as an array of object handles called lpDbgEvents and is kept in the window extra bytes of the responsible child window, along with the linked-list structure mentioned in the previous section.

Threads cannot share handles to event objects, so the debug thread must open its own array of handles to access the same event objects. The strategy so far is reasonable, but a potential problem lurks here. Since the debug thread must open handles to the same event objects, it has to do so by referring to these objects by name. Windows provides support for naming objects when they are created and referring to objects by name when they are opened for exactly this purpose. Consequently, the wait event objects must be named so that both the main thread and the debug thread can refer to identical objects. The problem arises when you start a second debug session. The second debug session requires a unique set of wait event objects—objects that can be referred to by both the debug thread and the main thread, but that also must be distinguishable from the first debug session's objects.

To solve this problem, DEBUGAPP.EXE uses the process ID of each process being debugged as part of the name used to identify each object, as in the following example.

DEBUG.C

/* Local function creates debug event objects for thread */
/* synchronization. */
BOOL CreateDebugEvents (
    LPHANDLE    lpDbgEvents,
    DWORD       dwProcessID)
{
    char    szEvent[MAX_PATH];
    char    Buff[15];


    LoadString (GetModuleHandle (NULL), 
                IDS_DBGEVNTACTIVE,
                szEvent,
                sizeof (szEvent));
    strcat (szEvent, itoa (dwProcessID, Buff, 10));
    if (!(lpDbgEvents[DEBUGACTIVE] = CreateEvent (NULL,
                                                  TRUE,
                                                  FALSE,
                                                  szEvent)))
        return FALSE;
.
.
.

Each wait event object is given a static name that is stored in a string resource table. Then, when creating a new event or opening a new handle to an existing event, the ID of the process being debugged is appended to the end of the static string. Together the static string and the process ID uniquely identify an object belonging to a specific process.

Because each thread is responsible for storing its own array of wait event handles, each thread creates its own handles independently. Fortunately, Windows is robust enough that no synchronization is needed for this process. In fact, both threads can make the same call to CreateEvent using the same object name, but only the first call will actually create new objects. The second call will return a valid handle to the same object. For that reason, both threads use one function (CreateDebugEvents) to retrieve valid handles without regard to which one calls first. The debug thread stores its debug event handle array on the stack as an automatic variable.

Note
   
In an effort to make the code in DebugApp more readable, I defined constants to represent array indexes by name rather than number. Refer to DEBUG.H to find the array index value that corresponds to a specific wait event handle.

In addition to the array of wait events used for communication between the two threads, DebugApp uses two other wait events, one for synchronizing startup and one for shutdown of the debug thread. These two event objects need only be created and used for a relatively short duration, so no accommodation is needed for them. Instead, they are created, freed, and released, all within the context of a single window message in the main thread. In both cases they are used as in the following example.

DEBUG.C

/* Create initialize event. */
LoadString (GetModuleHandle (NULL),
            IDS_DBGEVNTINITACK,
            szEvent,
            MAX_PATH);
hEvent = CreateEvent (NULL, TRUE, FALSE, szEvent);

/* Create debug thread. */
if (!(CreateThread ((LPSECURITY_ATTRIBUTES)NULL,
                    4096,
                    (LPTHREAD_START_ROUTINE)DebugEventThread,
                    (LPVOID)lpDbgProcess,
                    0,
                    &TID)))
    return NULL;

/* Wait for debugger to complete initialization before opening */
/* debug events. */
WaitForSingleObject (hEvent, INFINITE);
CloseHandle (hEvent);

First, the main thread creates an event by name with an initial value of FALSE. Then, it starts the debug thread and waits for the object it created. At this point, the thread stops execution until the wait event becomes TRUE, its signaled state. Meanwhile, the debug thread starts execution at the same time. The following code fragment shows how the debug thread signals the same wait event, identified by a common name, once it has completed its initialization.

DEBUG.C

/* Create process to be debugged. */
.
.
.
/* Signal completion of initialization to calling thread. */
LoadString (GetModuleHandle (NULL),
            IDS_DBGEVNTINITACK,
            szEvent,
            MAX_PATH);
hEvent = OpenEvent (EVENT_ALL_ACCESS, FALSE, szEvent);
SetEvent (hEvent);
CloseHandle (hEvent);

The main thread is able to continue execution after the event is signaled. Then, it is free to release the event object because the synchronization is complete. A similar wait event is used for synchronization when the debug thread shuts down.

Managing the Debugger's Graphical User Interface

When DEBUGAPP.EXE begins, the first thread in the process gets started. This thread behaves exactly like a basic, single-threaded MDI Windows-based application. It registers window classes for the frame and debug windows, creates the frame and MDI client windows, and initializes application-specific data. When complete, the thread enters a continuous GetMessage loop, awaiting commands from the user.

When the command is sent to load a process for debugging, the main thread first calls the GetOpenFileName common dialog routine, validates the selected filename, and informs the MDI client to create a new child window. The MDI client then creates the new child window, allowing it to perform its own window initialization.

The following initialization is performed during the WM_CREATE message of the debugger window:

  1. The child window creates an edit control, used for recording debug information for this debug process, that completely fills its client area.
  2. The child window allocates a segment of global memory for storing the array of wait event object handles.
  3. The child window calls the StartDebugger function to create the debug thread and the process for debugging.

The segment of global memory is passed as a parameter and returned with the array filled with valid event handles. The StartDebugger function also returns a pointer to the serialized heap for this debugger. Both of these pointers are then placed in window extra bytes for this window. The new child window then returns—eventually back to the frame window where the command to load the process for debugging was originally sent—permitting the main thread to continue executing in support of the graphical user interface.

All subsequent file-loading commands work in exactly the same way, permitting the user to load simultaneously as many processes for debugging as the system can accommodate, given the amount of resources available. Other menu commands are distributed as appropriate by the frame window. Some of the commands are handled by the MDI client window, while others are processed only by the frame window. Still other commands, like View Thread and View Process, are intended for the debug window that is currently active. The frame distributes these messages directly to the active debugger window.

Most commands intended for a specific debugger window involve communication between that window and the corresponding debug thread. In these cases, the debugger window signals a wait event for the debug thread. The debug thread can then act upon that event the next time it waits for it. Once the event has been signaled, the main thread simply returns back to the message loop for the next user command. Since the debug thread has access to the window handle of the debug window in its DBGPROCESS data structure, it is able to submit data to the edit control directly. This permits the user free access to commands without having to wait for any prolonged processing on the part of the debug thread.

Responding to User Commands in Debug Threads

Besides handling debug events in the process being debugged, the debug thread also handles all user commands once they have been signaled as wait events in the debug window. To accommodate user command events from the main thread and still be able to debug the process, the debug thread implements a multiple-object wait loop.

DEBUG.C

while (TRUE)
    {
    int    nIndex;

    /* Wait for debugger active. */
    switch (nIndex = WaitForMultipleObjects (nDEBUGEVENTS,
                                             hDbgEvent,
                                             FALSE,
                                             INFINITE))
        {
        case CLOSEDEBUGGER:
            {
            int    i;

            /* Terminate process being debugged. */
            TerminateProcess (lpDbgProcess->hProcess, 0);

            /* Signal close command acknowledged event. */
            LoadString (GetModuleHandle (NULL),
                        IDS_DBGEVNTCLOSEACK,
                        szEvent,
                        MAX_PATH);

            hEvent = OpenEvent (EVENT_ALL_ACCESS,
                                FALSE,
                                szEvent);
            SetEvent (hEvent);

            /* Close all debug events. */
            for (i=0; i<nDEBUGEVENTS; i++)
                CloseHandle (hDbgEvent[i]);
            CloseHandle (hEvent);

            /* Exit debugger now. */
            return TRUE;
            }
            break;

        case SUSPENDDEBUGGER:
            SuspendDebuggeeProcess (lpDbgProcess);
            ResetEvent (hDbgEvent[DEBUGACTIVE]);
            ResetEvent (hDbgEvent[SUSPENDDEBUGGER]);
            break;

        case RESUMEDEBUGGER:
            ResumeDebuggeeProcess (lpDbgProcess);
            SetEvent (hDbgEvent[DEBUGACTIVE]);
            ResetEvent (hDbgEvent[RESUMEDEBUGGER]);
            break;

        case DEBUGACTIVE:
            /* If debug active */
            if ((WaitForDebugEvent (&de, (DWORD)100)))
                {
                if (de.dwProcessId == lpDbgProcess->dwProcessID)
                    {
                    switch (de.dwDebugEventCode)
                        {
                        case EXCEPTION_DEBUG_EVENT:
                            ProcessExceptionEvent (&de);
                            break;

                        case CREATE_PROCESS_DEBUG_EVENT:
                            ProcessCreateProcessDebugEvent (&de);
                            break;

                        .
                        .
                        .

                        default:
                            ProcessDefaultDebugEvent (&de);
                            break;
                        }
                    }

                else
                    /* Notify of sibling process debug event. */
                    AppendEditText (lpDbgProcess->hWnd,
                                    de.dwDebugEventCode +
                                        IDS_SIBLING,
                                    NULL);

                ContinueDebugEvent (de.dwProcessId,
                                    de.dwThreadId,
                                    DBG_CONTINUE);
                }
            break;
        }

    }

In the example above, the debug thread begins a loop and immediately calls WaitForMultipleObjects to await the signaling of any debugger event. The debugger events are described below:

  • CLOSEDEBUGGER signals the debug thread to abort debugging the current process. Highest priority event.
  • SUSPENDDEBUGGER signals the debugger to suspend debugging the current process.
  • RESUMEDEBUGGER signals the debugger to resume debugging the process.
  • DEBUGACTIVE signals the debug thread to debug the process because there is nothing else to do. Lowest priority event.

The debug thread remains suspended upon this call until an event becomes signaled. Then, the WaitForMultipleObjects function returns the index of the event that was signaled. If more than one debugger event is signaled at a time, the one with the highest priority is returned. Events are assigned priorities according to how they are ordered in the array of event handles, where the lower the array position the higher the priority. The array of handles is passed as an argument to the WaitForMultipleObjects function. DEBUGAPP.EXE places the highest priority on exiting the debugger and the lowest priority on actual debugging. That means that debugging is performed only when the debug thread has nothing else to do. Really this means that the debugger is able to respond immediately to commands that do not occur frequently—such as exit.

Also, debug events are separate events from the events that are signaled by the main thread in a debug window. Consequently, another wait loop is embedded within the first for handling debug events alone. However, the debug thread must break from the debug event loop periodically to wait for debugger events. To facilitate this requirement, a time-out is used on the debug event loop to allow the debug thread a way of breaking out of the debug event loop when no debug events are occurring. When the debugger is no longer debugging, as it were, it is able to check for other events that may have become signaled in the interim. While doing so, the debugger does not need to suspend the process being debugged. Any debug events that occur while the debugger is not waiting for them are queued until the debugger resumes its call to WaitForDebugEvent.

Wait events, as used by the debugger, can be either automatic or manual reset types. DebugApp uses manual reset events for more control over when the events become acknowledged. This is necessary because, while the debugger is handling (or waiting for) debug events, more than one debugger event command might have become signaled. When the debugger returns to handle the events, it must handle them one at a time. Two automatic wait events would automatically become reset as soon as the debugger returned from the WaitForMultipleObjects. Yet the debugger prioritizes itself so that it responds only to the signaled event with lowest priority. When complete, it returns to handle any others that are still signaled. While handling each one, it resets the event manually to acknowledge the completion of the task for that event. This prevents events from slipping through the cracks while other events are being processed.

Controlling the Threads of a Process Being Debugged

In preemptive, multithreaded operating systems, a mechanism often referred to as the system scheduler exists for scheduling each thread in the system. The system scheduler assigns each thread a rank or priority that it uses to determine how much processing to attribute to a thread before switching to the next thread. Specifically in Windows, each thread has a base priority in the range 1–31, where the higher the priority, the more processing time is attributed to the thread. Windows establishes the base priority by combining the specific thread priority and the priority class of its process, as shown in the diagram in Figure 3.

ms809754.debug_3(en-us,MSDN.10).gif

Figure 3. Base thread priorities for each of the process priority classes.

Windows provides processes with the capability of determining and adjusting their threads' base priorities. Windows provides this feature through the SetPriorityClass, GetPriorityClass, SetThreadPriority, and GetThreadPriority functions. Using these functions, processes can adjust their threads' base priority values. To adjust the base priority of threads belonging to a process other than itself, a process needs special access rights.

By default, a debugger process has PROCESS_SET_INFORMATION access to the process it is debugging and THREAD_SET_INFORMATION access to all threads in that process. These accesses permit the debugger process to change both the priority class of the process being debugged and the thread priority value for each thread in the process. In addition, unlike debug events, any thread in the debugger process can adjust both the process priority class and thread priority values of the process it is debugging. This means that the main debugger thread is able to perform these functions directly without having to synchronize the procedure with the appropriate debug thread.

Because of this, the GUI thread of the debugger can perform execution control for all processes being debugged. In DEBUGAPP.EXE, execution control is handled by a single dialog box, Thread Execution Control. When the dialog box is invoked, the process being debugged is suspended until the dialog box is dismissed by the user. In the interim, the user is able to modify the priority value for each thread and the priority class for the process being debugged. The dialog box lists each of the threads in the process being debugged, showing their base priorities for comparison. After adjusting priorities for the threads and process, the user exits the dialog box by clicking either OK or Cancel. Clicking OK changes the priorities in the process being debugged and resumes its execution. Clicking Cancel simply causes the process being debugged to resume with the priorities left unchanged.

In addition to adjusting the base priority of threads, the debugger process can also suspend and terminate the threads of the process being debugged. Specifically, suspending and resuming threads is only available to processes with THREAD_SUSPEND_RESUME access and termination to processes with THREAD_TERMINATE access, but again a debugger process has these access rights by default. The aforementioned Thread Execution Control dialog box in DEBUGAPP.EXE provides support for suspending and resuming threads, but not for terminating threads. Windows provides support for controlling threads through the SuspendThread, ResumeThread, and TerminateThread functions.

Accessing Thread Context Information from Threads of a Process Being Debugged

By default, a debugger can change the context of any thread in the process being debugged by virtue of the fact that it has access to the handle for each thread of that process. In DEBUGAPP.EXE, the thread handles of the process being debugged are saved as the thread is created in the linked-list structure described earlier in "Representing Information About a Process Being Debugged." To change the context of a thread in the process being debugged, the debugger calls the SetThreadContext function. The arguments to this function are simply a handle to the thread to be affected and a pointer to a CONTEXT structure filled with information describing how the thread context will exist after making the call. Similarly, to view the state of a thread's context, the debugger calls GetThreadContext. Any process can call Set/GetThreadContext for any other thread, providing that it has a valid handle to that thread.

Note
   
A thread context is implementation-specific, differing from one hardware architecture to another. The fields of the CONTEXT structure vary, depending on whether you're running on an Intel platform or a MIPS platform. For details on implementing the CONTEXT structure for a specific platform, refer to the specific header file where the structure is defined.

Terminating and Exiting a Process Being Debugged

There is more than one way to end a process. Windows provides support for terminating a process, given the handle to that process, in the TerminateProcess function. Yet, using this function prevents the process from having the opportunity to clean up volatile data. TerminateProcess ends the process immediately without calling the DllEntryPoint function of any dynamic-link libraries (DLLs) that the application may have loaded. TerminateProcess does not send any last messages to window procedures (like WM_DESTROY); it simply terminates. Windows, however, is robust enough to clean up all system resources owned by the process and associated DLLs. Unlike Windows version 3.1, a process does not leave the system in an unstable state solely by calling this function.

Terminating a process from the debugger may, in fact, be the appropriate way to end a process being debugged. If at any time a user of the debugger commands the process being debugged to exit from the debugger, it is understood that the process is closing abnormally. If the user wants to gracefully exit the process, the user can simply exit it normally directly from the application interface. In fact, abruptly exiting a process—but only after it's allowed to save its changes—is, in itself, a contradiction.

Another method of exiting a process is to have a process call ExitProcess itself. This method is considered a "graceful" exit because all associated DLLs get a chance to clean up before being detached. In this case, the DllEntryPoint function gets called for each thread as it terminates and once for when the process goes away. This function permits the process to save volatile data before exiting. Yet, since DllEntryPoint does not include a parameter for a process handle, it cannot be called by one process in the hope of exiting another process. So, the debugger cannot command the process being debugged to exit gracefully by calling this function.

The final technique a debugger process could employ to command the process being debugged to exit gracefully does not use a straightforward API call. Instead, it involves manipulating the context information of a thread in the process being debugged. Because ExitProcess can only be called from the process that intends to exit, the debugger can change the context information of the main thread in the process being debugged so that the next instruction it executes is a call to ExitProcess.

To do this, the debugger must:

  1. Suspend the process being debugged.
  2. Get the context information of a thread in the process being debugged. (It can be any thread in the process, as long as it is not a suspended thread.)
  3. Replace the instruction-pointer contents with the address of the ExitProcess function as referenced by the process being debugged.
  4. Set the altered context information structure back into the thread of the process being debugged.
  5. Resume execution of the process.

The process being debugged behaves as though it made a call to ExitProcess itself. (Actually that is exactly what it does.) The following function illustrates this technique.

DEBUG.C

void ExitDebuggee (
    DBGPROCESS *lppr)
{
    CONTEXT    thContext;

    GetThreadContext (((DBGTHREAD *)lppr->lpThreads)->hThread,
                      &thContext);
    thContext.Eip = lppr->ExitProcess;
    SetThreadContext (((DBGTHREAD *)lppr->lpThreads)->hThread,
                      &thContext);
}

Obtaining the address of the ExitProcess function is problematic in the above procedure. While it is relatively easy to determine the whereabouts of the function in a process that is executing (it is a member of the system DLL, KERNEL32.DLL), finding the exact address of that function in the process being debugged is more difficult. Each application maps all of the DLLs it uses into its own address space, based mostly on the order in which the DLLs are loaded. This means that, while more than one application may use a given DLL, two processes may or may not have loaded the same system DLL into the same location in their respective address spaces. Consequently, the same function called from a common DLL might be located at different virtual addresses in the two applications. It is tempting to draw the conclusion that system DLLs are loaded into the same base address in every application, for that would make this problem simply go away. This conclusion, however, is invalid. It may work in some cases, but it cannot be considered a fail-safe assumption. Developers are wise not to draw this conclusion about the system.

So, to make the ExitDebuggee function work properly (see code fragment above), the address of the ExitProcess function must be known in the context of the address space of the process being debugged.

Determining the location of ExitProcess

One safe assumption to make is that the location of a function in a DLL is always at the same offset from the DLL's base address. This assumption provides the necessary information to develop a technique that is fail-safe. The debugger is already notified when the process being debugged loads each of its DLLs, and at that time the base address of the DLL is provided to the debugger. All the debugger needs to do at this point is determine which DLL being loaded contains the ExitProcess function and the offset of that function within the DLL.

To determine the offset of the function in the DLL, call GetProcAddress with the handle to the appropriate DLL and a string identifying the ExitProcess function. This can be done within the context of the debugger process since the offset is consistent across processes. The handle to this DLL can be obtained by making a call to LoadLibrary, specifying KERNEL32.DLL by name. Then, subtract the base address of the DLL from the address returned from GetProcAddress. The difference is the offset into the DLL. The base address of KERNEL32.DLL can be determined by calling the VirtualQuery function, supplying the address of the ExitProcess function as the base address for the region of memory. VirtualQuery returns a filled-out MEMORY_BASIC_INFORMATION structure. One field of that structure is the base address for the region of memory. In this case, that will be the base address of the DLL code region.

Even more difficult is the task of determining which DLL is being loaded in the process being debugged when LOAD_DLL_DEBUG_EVENT occurs. During this debug event, the debugger receives the base address for the DLL being loaded, but only a module file handle with which to identify the DLL. Fortunately, the file handle can be used to read information about the file. To identify the file as the correct DLL, the debugger must determine the name of the DLL by extracting the filename, assigned by the linker, from the executable image. The name is found after tracing through a maze of offsets and tables of data embedded within the executable file.

A limitation, though, exists in this technique. Since the name of the executable is embedded in the file during the link process, there is no way of knowing whether a user renamed the file after linking. Unfortunately, there is no way around this limitation. No other way exists to determine the name of the DLL that is being loaded in the debugger—yet, one can always hope that this will be a feature included in a future release of Windows.

Once the name is extracted, it can be compared to see if, in fact, the DLL is the KERNEL32.DLL file. If so, the debugger saves this base address in the process structure for use in the ExitDebuggee function as shown in the code fragment in the previous section.

Debug Functions a Process Being Debugged Can Call

A few functions are provided as part of the Windows API for applications that are being debugged. Each of these functions generates a debug event in the debugger process:

  • DebugBreak is provided simply to insert a break point in an application. This function generates the EXCEPTION_DEBUG_EVENT event and with it an EXCEPTION_DEBUG_INFO structure that includes an EXCEPTION_RECORD structure, which includes an EXCEPTION_BREAKPOINT exception code for this event.
  • OutputDebugString provides the opportunity for the process being debugged to pass a string to the debugger application. This function can be extremely useful in a custom debugger application because it provides a mechanism for the process being debugged to pass information to the debugger. The debugger can then log these strings when they occur or respond according to their content. This function generates the OUTPUT_DEBUG_STRING_DEBUG_EVENT event and is accompanied by an OUTPUT_DEBUG_STRING_INFO structure. This structure contains the address and length of the string in the process being debugged and a Unicode® flag, indicating the type of string it is. The debugger can access the string by calling ReadProcessMemory and indicating the length and address of the string to read along with the process handle.
  • FatalExit and FatalAppExit are functions provided for an application to exit immediately but pass control to the debugger before going away.

The debugger handles each of the above calls as any application calling them would when encountering any other debug event. The only distinction is the type of event itself. If it is desirable to have the debugger execute special processing after one of these types of events, the debugger simply treats each of these debug events uniquely. The debug event loop is already prepared to handle this eventuality.

Expanding on This Debugger Model

The debugger presented in this article falls short of a full-fledged, source-level debugger in several ways. It does not provide any source-level functionality, like single-step execution and break points. It also lacks any symbolic information support. Many features could easily be added to this debugger—many without too much effort and some that would require considerable effort. The purpose of DEBUGAPP.EXE is to provide a base upon which a complete debugging environment could be built while at the same time introducing the debugging API. To that extent, this debugger is a solid debugging foundation, and it demonstrates extensive use of the debugging API. Don't be surprised to see future samples and technical articles based on this debugging sample!

© 2016 Microsoft
Export (0) Print
Expand All

/CGTHREADS (Compiler Threads)

 

Sets the number of cl.exe threads to use for optimization and code generation when link-time code generation is specified.

/CGTHREADS:[1-8]

number

The maximum number of threads for cl.exe to use, in the range 1 to 8.

The /CGTHREADS option specifies the maximum number of threads cl.exe uses in parallel for the optimization and code-generation phases of compilation when link-time code generation (/LTCG) is specified. By default, cl.exe uses four threads, as if /CGTHREADS:4 were specified. If more processor cores are available, a larger number value can improve build times.

Multiple levels of parallelism can be specified for a build. The msbuild.exe switch /maxcpucount specifies the number of MSBuild processes that can be run in parallel. The /MP (Build with Multiple Processes) compiler flag specifies the number of cl.exe processes that simultaneously compile the source files. The /cgthreads compiler option specifies the number of threads used by each cl.exe process. Because the processor can only run as many threads at the same time as there are processor cores, it's not useful to specify larger values for all of these options at the same time, and it can be counterproductive. For more information about how to build projects in parallel, see Building Multiple Projects in Parallel with MSBuild.

To set this linker option in the Visual Studio development environment

  1. Open the project's Property Pages dialog box. For details, see Working with Project Properties.

  2. Select the Configuration Properties, Linker folder.

  3. Select the Command Line property page.

  4. Modify the Additional Options property to include /CGTHREADS:number, where number is a value from 1 to 8, and then choose OK.

To set this linker option programmatically

© 2016 Microsoft
Export (0) Print
Expand All

Getting Started (Debug Interface Access SDK)

 

The Debug Interface Access (DIA) SDK supplies you with instructional documentation and a sample that illustrates how to use the DIA API. Use the interfaces and methods in the DIA SDK to develop custom applications that open the .pdb and .dbg files and search their content for symbols, values, attributes, addresses, and other debugging information. This SDK also provides reference tables for the properties associated with symbols found in C++ applications.

To best use the DIA SDK, you should be familiar with the following:

  • C++ programming language

  • COM programming

  • Visual Studio integrated development environment (IDE) for compiling the samples

The DIA SDK is normally installed with Visual Studio and its default location is [drive]\Program Files\Microsoft Visual Studio 9.0\DIA SDK. As part of the installation, the msdia90.dll, which implements the DIA SDK, is automatically registered so all that you need to do to use it is to include dia2.h in your program and link to diaguids.lib.

Header: include\dia2.h

Library: lib\diaguids.lib

DLL: bin\msdia80.dll

IDL: idl\dia2.idl

Overview (Debug Interface Access SDK)

Reviews the basic architecture of DIA.

Querying the .Pdb File

Provides step-by-step instructions on how to use the DIA API to query a .pdb file.

© 2016 Microsoft
Export (0) Print
Expand All
© 2016 Microsoft
Export (0) Print
Expand All

User mode and kernel mode

A processor in a computer running Windows has two different modes: user mode and kernel mode. The processor switches between the two modes depending on what type of code is running on the processor. Applications run in user mode, and core operating system components run in kernel mode. While many drivers run in kernel mode, some drivers may run in user mode.

When you start a user-mode application, Windows creates a process for the application. The process provides the application with a private virtual address space and a private handle table. Because an application's virtual address space is private, one application cannot alter data that belongs to another application. Each application runs in isolation, and if an application crashes, the crash is limited to that one application. Other applications and the operating system are not affected by the crash.

In addition to being private, the virtual address space of a user-mode application is limited. A processor running in user mode cannot access virtual addresses that are reserved for the operating system. Limiting the virtual address space of a user-mode application prevents the application from altering, and possibly damaging, critical operating system data.

All code that runs in kernel mode shares a single virtual address space. This means that a kernel-mode driver is not isolated from other drivers and the operating system itself. If a kernel-mode driver accidentally writes to the wrong virtual address, data that belongs to the operating system or another driver could be compromised. If a kernel-mode driver crashes, the entire operating system crashes.

This diagram illustrates communication between user-mode and kernel-mode components.

Block diagram of user-mode and kernel-mode components

Related topics

Virtual Address Spaces

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Virtual address spaces

When a processor reads or writes to a memory location, it uses a virtual address. As part of the read or write operation, the processor translates the virtual address to a physical address. Accessing memory through a virtual address has these advantages:

  • A program can use a contiguous range of virtual addresses to access a large memory buffer that is not contiguous in physical memory.

  • A program can use a range of virtual addresses to access a memory buffer that is larger than the available physical memory. As the supply of physical memory becomes small, the memory manager saves pages of physical memory (typically 4 kilobytes in size) to a disk file. Pages of data or code are moved between physical memory and the disk as needed.

  • The virtual addresses used by different processes are isolated from each other. The code in one process cannot alter the physical memory that is being used by another process or the operating system.

The range of virtual addresses that is available to a process is called the virtual address space for the process. Each user-mode process has its own private virtual address space. For a 32-bit process, the virtual address space is usually the 2-gigabyte range 0x00000000 through 0x7FFFFFFF. For a 64-bit process, the virtual address space is the 8-terabyte range 0x000'00000000 through 0x7FF'FFFFFFFF. A range of virtual addresses is sometimes called a range of virtual memory.

This diagram illustrates some of the key features of virtual address spaces.

Diagram of virtual address spaces for two processes

The diagram shows the virtual address spaces for two 64-bit processes: Notepad.exe and MyApp.exe. Each process has its own virtual address space that goes from 0x000'0000000 through 0x7FF'FFFFFFFF. Each shaded block represents one page (4 kilobytes in size) of virtual or physical memory. Notice that the Notepad process uses three contiguous pages of virtual addresses, starting at 0x7F7'93950000. But those three contiguous pages of virtual addresses are mapped to noncontiguous pages in physical memory. Also notice that both processes use a page of virtual memory beginning at 0x7F7'93950000, but those virtual pages are mapped to different pages of physical memory.

User space and system space

Processes like Notepad.exe and MyApp.exe run in user mode. Core operating system components and many drivers run in the more privileged kernel mode. For more information about processor modes, see User mode and kernel mode. Each user-mode process has its own private virtual address space, but all code that runs in kernel mode shares a single virtual address space called system space. The virtual address space for the current user-mode process is called user space.

In 32-bit Windows, the total available virtual address space is 2^32 bytes (4 gigabytes). Usually the lower 2 gigabytes are used for user space, and the upper 2 gigabytes are used for system space.

Diagram of system space

In 32-bit Windows, you have the option of specifying (at boot time) that more than 2 gigabytes are available for user space. The consequence is that fewer virtual addresses are available for system space. You can increase the size of user space to as much as 3 gigabytes, in which case only 1 gigabyte is available for system space. To increase the size of user space, use BCDEdit /set increaseuserva.

In 64-bit Windows, the theoretical amount of virtual address space is 2^64 bytes (16 exabytes), but only a small portion of the 16-exabyte range is actually used. The 8-terabyte range from 0x000'00000000 through 0x7FF'FFFFFFFF is used for user space, and portions of the 248-terabyte range from 0xFFFF0800'00000000 through 0xFFFFFFFF'FFFFFFFF are used for system space.

Diagram of paged pool and nonpaged pool

Code running in user mode has access to user space but does not have access to system space. This restriction prevents user-mode code from reading or altering protected operating system data structures. Code running in kernel mode has access to both user space and system space. That is, code running in kernel mode has access to system space and the virtual address space of the current user-mode process.

Drivers that run in kernel mode must be very careful about directly reading from or writing to addresses in user space. This scenario illustrates why.

  1. A user-mode program initiates a request to read some data from a device. The program supplies the starting address of a buffer to receive the data.

  2. A device driver routine, running in kernel mode, starts the read operation and returns control to its caller.

  3. Later the device interrupts whatever thread is currently running to say that the read operation is complete. The interrupt is handled by kernel-mode driver routines running on this arbitrary thread, which belongs to an arbitrary process.
  4. At this point, the driver must not write the data to the starting address that the user-mode program supplied in Step 1. This address is in the virtual address space of the process that initiated the request, which is most likely not the same as the current process.

Paged pool and Nonpaged pool

In user space, all physical memory pages can be paged out to a disk file as needed. In system space, some physical pages can be paged out and others cannot. System space has two regions for dynamically allocating memory: paged pool and nonpaged pool. In 64-bit Windows, paged pool is the 128-gigabyte range of virtual addresses that goes from 0xFFFFA800'00000000 through 0xFFFFA81F'FFFFFFFF. Nonpaged pool is the 128-gigabyte range of virtual addresses that goes from 0xFFFFAC00'00000000 through 0xFFFFAC1F'FFFFFFFF.

Memory that is allocated in paged pool can be paged out to a disk file as needed. Memory that is allocated in nonpaged pool can never be paged out to a disk file.

Diagram comparing memory allocation in paged pool to that in nonpaged pool

Related topics

User mode and kernel mode

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Device nodes and device stacks

In Windows, devices are represented by device nodes in the Plug and Play (PnP) device tree. Typically, when an I/O request is sent to a device, several drivers help handle the request. Each of these drivers is associated with a device object, and the device objects are arranged in a stack. The sequence of device objects along with their associated drivers is called a device stack. Each device node has its own device stack.

Device nodes and the Plug and Play device tree

Windows organizes devices in a tree structure called the Plug and Play device tree, or simply the device tree. Typically, a node in the device tree represents either a device or an individual function on a composite device. However, some nodes represent software components that have no association with physical devices.

A node in the device tree is called a device node. The root node of the device tree is called the root device node. By convention, the root device node is drawn at the bottom of the device tree, as shown in the following diagram.

Diagram of the device tree, showing device nodes

The device tree illustrates the parent/child relationships that are inherent in the PnP environment. Several of the nodes in the device tree represent buses that have child devices connected to them. For example, the PCI Bus node represents the physical PCI bus on the motherboard. During startup, the PnP manager asks the PCI bus driver to enumerate the devices that are connected to the PCI bus. Those devices are represented by child nodes of the PCI Bus node. In the preceding diagram, the PCI Bus node has child nodes for several devices that are connected to the PCI bus, including USB host controllers, an audio controller, and a PCI Express port.

Some of the devices connected to the PCI bus are buses themselves. The PnP manager asks each of these buses to enumerate the devices that are connected to it. In the preceding diagram, we can see that the audio controller is a bus that has an audio device connected to it. We can see that the PCI Express port is a bus that has a display adapter connected to it, and the display adapter is a bus that has one monitor connected to it.

Whether you think of a node as representing a device or a bus depends on your point of view. For example, you can think of the display adapter as a device that plays a key role in preparing frames that appear on the screen. However, you can also think of the display adapter as a bus that is capable of detecting and enumerating connected monitors.

Device objects and device stacks

A device object is an instance of a DEVICE_OBJECT structure. Each device node in the PnP device tree has an ordered list of device objects, and each of these device objects is associated with a driver. The ordered list of device objects, along with their associated drivers, is called the device stack for the device node.

You can think of a device stack in several ways. In the most formal sense, a device stack is an ordered list of (device object, driver) pairs. However, in certain contexts it might be useful to think of the device stack as an ordered list of device objects. In other contexts, it might be useful to think of the device stack as an ordered list of drivers.

By convention, a device stack has a top and a bottom. The first device object to be created in the device stack is at the bottom, and the last device object to be created and attached to the device stack is at the top.

In the following diagram, the Proseware Gizmo device node has a device stack that contains three (device object, driver) pairs. The top device object is associated with the driver AfterThought.sys, the middle device object is associated with the driver Proseware.sys, and the bottom device object is associated with the driver Pci.sys. The PCI Bus node in the center of the diagram has a device stack that contains two (device object, driver) pairs--a device object associated with Pci.sys and a device object associated with Acpi.sys.

Diagram showing device objects ordered in device stacks in the Proseware Gizmo and PCI device nodes

How does a device stack get constructed?

During startup, the PnP manager asks the driver for each bus to enumerate child devices that are connected to the bus. For example, the PnP manager asks the PCI bus driver (Pci.sys) to enumerate the devices that are connected to the PCI bus. In response to this request, Pci.sys creates a device object for each device that is connected to the PCI bus. Each of these device objects is called a physical device object (PDO). Shortly after Pci.sys creates the set of PDOs, the device tree looks like the one shown in the following diagram.

Diagram of PCI node and physical device objects for child devices

The PnP manager associates a device node with each newly created PDO and looks in the registry to determine which drivers need to be part of the device stack for the node. The device stack must have one (and only one) function driver and can optionally have one or more filter drivers. The function driver is the main driver for the device stack and is responsible for handling read, write, and device control requests. Filter drivers play auxiliary roles in processing read, write, and device control requests. As each function and filter driver is loaded, it creates a device object and attaches itself to the device stack. A device object created by the function driver is called a functional device object (FDO), and a device object created by a filter driver is called a filter device object (Filter DO). Now the device tree looks something like this diagram.

Diagram of a device tree showing the filter, function, and physical device objects in the Proseware Gizmo device node

In the diagram, notice that in one node, the filter driver is above the function driver, and in the other node, the filter driver is below the function driver. A filter driver that is above the function driver in a device stack is called an upper filter driver. A filter driver that is below the function driver is called a lower filter driver.

The PDO is always the bottom device object in a device stack. This results from the way a device stack is constructed. The PDO is created first, and as additional device objects are attached to the stack, they are attached to the top of the existing stack.

Note  

When the drivers for a device are installed, the installer uses information in an information (INF) file to determine which driver is the function driver and which drivers are filters. Typically the INF file is provided either by Microsoft or by the hardware vendor. After the drivers for a device are installed, the PnP manager can determine the function and filter drivers for the device by looking in the registry.

 

Bus drivers

In the preceding diagram, you can see that the driver Pci.sys plays two roles. First, Pci.sys is associated with the FDO in the PCI Bus device node. In fact, it created the FDO in the PCI Bus device node. So Pci.sys is the function driver for the PCI bus. Second, Pci.sys is associated with the PDO in each child of the PCI Bus node. Recall that it created the PDOs for the child devices. The driver that creates the PDO for a device node is called the bus driver for the node.

If your point of reference is the PCI bus, then Pci.sys is the function driver. But if your point of reference is the Proseware Gizmo device, then Pci.sys is the bus driver. This dual role is typical in the PnP device tree. A driver that serves as function driver for a bus also serves as bus driver for a child device of the bus.

User-mode device stacks

So far we've been discussing kernel-mode device stacks. That is, the drivers in the stacks run in kernel mode, and the device objects are mapped into system space, which is the address space that is available only to code running in kernel mode. For information about the difference between kernel mode and user mode, see User mode and kernel mode.

In some cases, a device has a user-mode device stack in addition to its kernel-mode device stack. User-mode drivers are often based on the User-Mode Driver Framework (UMDF), which is one of the driver models provided by the Windows Driver Frameworks (WDF). In UMDF, the drivers are user-mode DLLs, and the device objects are COM objects that implement the IWDFDevice interface. A device object in a UMDF device stack is called a WDF device object (WDF DO).

The following diagram shows the device node, kernel-mode device stack, and the user-mode device stack for a USB-FX-2 device. The drivers in both the user-mode and kernel-mode stacks participate in I/O requests that are directed at the USB-FX-2 device.

Diagram showing user-mode and kernel-mode device stacks

Related topics

Concepts for all driver developers
Driver stacks

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

I/O request packets

Most of the requests that are sent to device drivers are packaged in I/O request packets (IRPs). An operating system component or a driver sends an IRP to a driver by calling IoCallDriver, which has two parameters: a pointer to a DEVICE_OBJECT and a pointer to an IRP. The DEVICE_OBJECT has a pointer to an associated DRIVER_OBJECT. When a component calls IoCallDriver, we say the component sends the IRP to the device object or sends the IRP to the driver associated with the device object. Sometimes we use the phrase passes the IRP or forwards the IRP instead of sends the IRP.

Typically an IRP is processed by several drivers that are arranged in a stack. Each driver in the stack is associated with a device object. For more information, see Device nodes and device stacks. When an IRP is processed by a device stack, the IRP is usually sent first to the top device object in the device stack. For example, if an IRP is processed by the device stack shown in this diagram, the IRP would be sent first to the filter device object (Filter DO) at the top of the device stack.

Diagram of a device node and its device stack

Passing an IRP down the device stack

Suppose the I/O manager sends an IRP to the Filter DO in the diagram. The driver associated with the Filter DO, AfterThought.sys, processes the IRP and then passes it to the functional device object (FDO), which is the next lower device object in the device stack. When a driver passes an IRP to the next lower device object in the device stack, we say the driver passes the IRP down the device stack.

Some IRPs are passed all the way down the device stack to the physical device object (PDO). Other IRPs never reach the PDO because they are completed by one of the drivers above the PDO.

IRPs are self-contained

The IRP structure is self-contained in the sense that it holds all of the information that a driver needs to handle an I/0 request. Some parts of the IRP structure hold information that is common to all of the participating drivers in the stack. Other parts of the IRP hold information that is specific to a particular driver in the stack.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Driver stacks

Most of the requests that are sent to device drivers are packaged in I/O request packets (IRPs). Each device is represented by a device node, and each device node has a device stack. For more information, see Device nodes and device stacks. To send a read, write, or control request to a device, the I/O manager locates the device node for the device and then sends an IRP to the device stack of that node. Sometimes more than one device stack is involved in processing an I/O request. Regardless of how many device stacks are involved, the overall sequence of drivers that participate in an I/O request is called the driver stack for the request. We also use the term driver stack to refer to the layered set of drivers for a particular technology.

I/O requests that are processed by several device stacks

In some cases, more than one device stack is involved in processing an IRP. The following diagram illustrates a case where four device stacks are involved in processing a single IRP.

Diagram of four device nodes, each with a device stack

Here is how the IRP is processed at each numbered stage in the diagram:

  1. The IRP is created by Disk.sys, which is the function driver in the device stack for the My USB Storage Device node. Disk.sys passes the IRP down the device stack to Usbstor.sys.

  2. Notice that Usbstor.sys is the PDO driver for the My USB Storage Device node and the FDO driver for the USB Mass Storage Device node. At this point, it is not important to decide whether the IRP is owned by the (PDO, Usbstor.sys) pair or the (FDO, Usbstor.sys) pair. The IRP is owned by the driver, Usbstor.sys, and the driver has access to both the PDO and the FDO.
  3. When Usbstor.sys has finished processing the IRP, it passes the IRP to Usbhub.sys. Usbhub.sys is the PDO driver for the USB Mass Storage Device node and the FDO driver for the USB Root Hub node. It is not important to decide whether the IRP is owned by the (PDO, Usbhub.sys) pair or the (FDO, Usbhub.sys) pair. The IRP is owned by the driver, Usbhub.sys, and the driver has access to both the PDO and the FDO.

  4. When Usbhub.sys has finished processing the IRP, it passes the IRP to the (Usbuhci.sys, Usbport.sys) pair.

    Usbuhci.sys is a miniport driver, and Usbport.sys is a port driver. The (miniport, port) pair plays the role of a single driver. In this case, both the miniport driver and the port driver are written by Microsoft. The (Usbuhci.sys, Usbport.sys) pair is the PDO driver for the USB Root Hub node, and the (Usbuhci.sys, Usbport.sys) pair is also the FDO driver for the USB Host Controller node. The (Usbuhci.sys, Usbport.sys) pair does the actual communication with the host controller hardware, which in turn communicates with the physical USB storage device.

The driver stack for an I/O request

Consider the sequence of four drivers that participated in the I/O request illustrated in the preceding diagram. We can get another view of the sequence by focusing on the drivers rather than on the device nodes and their individual device stacks. The following diagram shows the drivers in sequence from top to bottom. Notice that Disk.sys is associated with one device object, but each of the other three drivers is associated with two device objects.

Diagram of a driver stack, showing the top driver associated with an FDO only, and the other three drivers associated with a PDO and an FDO

The sequence of drivers that participate in an I/O request is called the driver stack for the I/O request. To illustrate a driver stack for an I/O request, we draw the drivers from top to bottom in the order that they participate in the request.

Notice that the driver stack for an I/O request is quite different from the device stack for a device node. Also notice that the driver stack for an I/O request does not necessarily remain in one branch of the device tree.

Technology driver stacks

Consider the driver stack for the I/O request shown in the preceding diagram. If we give each of the drivers a friendly name and make some slight changes to the diagram, we have a block diagram that is similar to many of those that appear in the Windows Driver Kit (WDK) documentation.

Diagram of a driver stack showing friendly names for the drivers: Disk Class Driver on top followed by USB Storage Port Driver, and then USB Hub Driver and (USB 2 Miniport, USB Port) Driver

In the diagram, the driver stack is divided into three sections. We can think of each section as belonging to a particular technology or to a particular component or portion of the operating system. For example, we might say that the first section at the top of the driver stack belongs to the Volume Manager, the second section belongs to the storage component of the operating system, and the third section belongs to the core USB portion of the operating system.

Consider the drivers in the third section. These drivers are a subset of a larger set of core USB drivers that Microsoft provides for handling various kinds of USB requests and USB hardware. The following diagram shows what the entire USB core block diagram might look like.

Diagram showing the technology driver stack for possible USB core block

A block diagram that shows all of the drivers for a particular technology or a particular component or portion of the operating system is called a technology driver stack. Typically, technology driver stacks are given names like the USB Core Driver Stack, the Storage Stack, the 1394 Driver Stack, and the Audio Driver Stack.

Note  The USB core block diagram in this topic shows one of several possible ways to illustrate the technology driver stacks for USB 1.0 and 2.0. For the official diagrams of the USB 1.0, 2.0, and 3.0 driver stacks, see USB Driver Stack Architecture.
 

Related topics

Device nodes and device stacks
Minidrivers and driver pairs
Concepts for all driver developers

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Minidrivers, Miniport drivers, and driver pairs

A minidriver or a miniport driver acts as half of a driver pair. Driver pairs like (miniport, port) can make driver development easier. In a driver pair, one driver handles general tasks that are common to a whole collection of devices, while the other driver handles tasks that are specific to an individual device. The drivers that handle device-specific tasks go by a variety of names, including miniport driver, miniclass driver, and minidriver.

Microsoft provides the general driver, and typically an independent hardware vendor provides the specific driver. Before you read this topic, you should understand the ideas presented in Device nodes and device stacks and I/O request packets.

Every kernel-mode driver must implement a function named DriverEntry, which gets called shortly after the driver is loaded. The DriverEntry function fills in certain members of a DRIVER_OBJECT structure with pointers to several other functions that the driver implements. For example, the DriverEntry function fills in the Unload member of the DRIVER_OBJECT structure with a pointer to the driver's Unload function, as shown in the following diagram.

Diagram showing the DRIVER_OBJECT structure with the Unload member

The MajorFunction member of the DRIVER_OBJECT structure is an array of pointers to functions that handle I/O request packets (IRPs), as shown in the following diagram. Typically the driver fills in several members of the MajorFunction array with pointers to functions (implemented by the driver) that handle various kinds of IRPs.

Diagram showing the DRIVER_OBJECT structure with the MajorFunction member

An IRP can be categorized according to its major function code, which is identified by a constant, such as IRP_MJ_READ, IRP_MJ_WRITE, or IRP_MJ_PNP. The constants that identify major function code serve as indices in the MajorFunction array. For example, suppose the driver implements a dispatch function to handle IRPs that have the major function code IRP_MJ_WRITE. In this case, the driver must fill in the MajorFunction[IRP_MJ_WRITE] element of the array with a pointer to the dispatch function.

Typically the driver fills in some of the elements of the MajorFunction array and leaves the remaining elements set to default values provided by the I/O manager. The following example shows how to use the !drvobj debugger extension to inspect the function pointers for the parport driver.

0: kd> !drvobj parport 2
Driver object (fffffa80048d9e70) is for:
 \Driver\Parport
DriverEntry:   fffff880065ea070	parport!GsDriverEntry
DriverStartIo: 00000000	
DriverUnload:  fffff880065e131c	parport!PptUnload
AddDevice:     fffff880065d2008	parport!P5AddDevice

Dispatch routines:
[00] IRP_MJ_CREATE                      fffff880065d49d0	parport!PptDispatchCreateOpen
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       fffff880065d4a78	parport!PptDispatchClose
[03] IRP_MJ_READ                        fffff880065d4bac	parport!PptDispatchRead
[04] IRP_MJ_WRITE                       fffff880065d4bac	parport!PptDispatchRead
[05] IRP_MJ_QUERY_INFORMATION           fffff880065d4c40	parport!PptDispatchQueryInformation
[06] IRP_MJ_SET_INFORMATION             fffff880065d4ce4	parport!PptDispatchSetInformation
[07] IRP_MJ_QUERY_EA                    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              fffff880065d4be8	parport!PptDispatchDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff880065d4c24	parport!PptDispatchInternalDeviceControl
[10] IRP_MJ_SHUTDOWN                    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL                fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     fffff880065d4af4	parport!PptDispatchCleanup
[13] IRP_MJ_CREATE_MAILSLOT             fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       fffff880065d491c	parport!PptDispatchPower
[17] IRP_MJ_SYSTEM_CONTROL              fffff880065d4d4c	parport!PptDispatchSystemControl
[18] IRP_MJ_DEVICE_CHANGE               fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         fffff880065d4840	parport!PptDispatchPnp

In the debugger output, you can see that parport.sys implements GsDriverEntry, the entry point for the driver. GsDriverEntry, which was generated automatically when the driver was built, performs some initialization and then calls DriverEntry, which was implemented by the driver developer.

You can also see that the parport driver (in its DriverEntry function) provides pointers to dispatch functions for these major function codes:

  • IRP_MJ_CREATE
  • IRP_MJ_CLOSE
  • IRP_MJ_READ
  • IRP_MJ_WRITE
  • IRP_MJ_QUERY_INFORMATION
  • IRP_MJ_SET_INFORMATION
  • IRP_MJ_DEVICE_CONTROL
  • IRP_MJ_INTERNAL_DEVICE_CONTROL
  • IRP_MJ_CLEANUP
  • IRP_MJ_POWER
  • IRP_MJ_SYSTEM_CONTROL
  • IRP_MJ_PNP

The remaining elements of the MajorFunction array hold pointers to the default dispatch function nt!IopInvalidDeviceRequest.

In the debugger output, you can see that the parport driver provided function pointers for Unload and AddDevice, but did not provide a function pointer for StartIo. The AddDevice function is unusual because its function pointer is not stored in the DRIVER_OBJECT structure. Instead, it is stored in the AddDevice member of an extension to the DRIVER_OBJECT structure. The following diagram illustrates the function pointers that the parport driver provided in its DriverEntry function. The function pointers provided by parport are shaded.

Diagram of function pointers in a DRIVER_OBJECT structure

Making it easier by using driver pairs

Over a period of time, as driver developers inside and outside of Microsoft gained experience with the Windows Driver Model (WDM), they realized a couple of things about dispatch functions:

  • Dispatch functions are largely boilerplate. For example, much of the code in the dispatch function for IRP_MJ_PNP is the same for all drivers. It is only a small portion of the Plug and Play (PnP) code that is specific to an individual driver that controls an individual piece of hardware.
  • Dispatch functions are complicated and difficult to get right. Implementing features like thread synchronization, IRP queuing, and IRP cancellation is challenging and requires a deep understanding of how the operating system works.

To make things easier for driver developers, Microsoft created several technology-specific driver models. At first glance, the technology-specific models seem quite different from each other, but a closer look reveals that many of them are based on this paradigm:

  • The driver is split into two pieces: one that handles the general processing and one that handles processing specific to a particular device.
  • The general piece is written by Microsoft.
  • The specific piece may be written by Microsoft or an independent hardware vendor.

Suppose that the Proseware and Contoso companies both make a toy robot that requires a WDM driver. Also suppose that Microsoft provides a General Robot Driver called GeneralRobot.sys. Proseware and Contoso can each write small drivers that handle the requirements of their specific robots. For example, Proseware could write ProsewareRobot.sys, and the pair of drivers (ProsewareRobot.sys, GeneralRobot.sys) could be combined to form a single WDM driver. Likewise, the pair of drivers (ContosoRobot.sys, GeneralRobot.sys) could combine to form a single WDM driver. In its most general form, the idea is that you can create drivers by using (specific.sys, general.sys) pairs.

Function pointers in driver pairs

In a (specific.sys, general.sys) pair, Windows loads specific.sys and calls its DriverEntry function. The DriverEntry function of specific.sys receives a pointer to a DRIVER_OBJECT structure. Normally you would expect DriverEntry to fill in several elements of the MajorFunction array with pointers to dispatch functions. Also you would expect DriverEntry to fill in the Unload member (and possibly the StartIo member) of the DRIVER_OBJECT structure and the AddDevice member of the driver object extension. However, in a driver pair model, DriverEntry does not necessarily do this. Instead the DriverEntry function of specific.sys passes the DRIVER_OBJECT structure along to an initialization function implemented by general.sys. The following code example shows how the initialization function might be called in the (ProsewareRobot.sys, GeneralRobot.sys) pair.

C++

PVOID g_ProsewareRobottCallbacks[3] = {DeviceControlCallback, PnpCallback, PowerCallback};

// DriverEntry function in ProsewareRobot.sys
NTSTATUS DriverEntry (DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
   // Call the initialization function implemented by GeneralRobot.sys.
   return GeneralRobotInit(DriverObject, RegistryPath, g_ProsewareRobottCallbacks);
}

The initialization function in GeneralRobot.sys writes function pointers to the appropriate members of the DRIVER_OBJECT structure (and its extension) and the appropriate elements of the MajorFunction array. The idea is that when the I/O manager sends an IRP to the driver pair, the IRP goes first to a dispatch function implemented by GeneralRobot.sys. If GeneralRobot.sys can handle the IRP on its own, then the specific driver, ProsewareRobot.sys, does not have to be involved. If GeneralRobot.sys can handle some, but not all, of the IRP processing, it gets help from one of the callback functions implemented by ProsewareRobot.sys. GeneralRobot.sys receives pointers to the ProsewareRobot callbacks in the GeneralRobotInit call.

At some point after DriverEntry returns, a device stack gets constructed for the Proseware Robot device node. The device stack might look like this.

Diagram of the Proseware Robot device node, showing three device objects in the device stack: AfterThought.sys (Filter DO), ProsewareRobot.sys, GeneralRobot.sys (FDO), and Pci.sys (PDO)

As shown in the preceding diagram, the device stack for Proseware Robot has three device objects. The top device object is a filter device object (Filter DO) associated with the filter driver AfterThought.sys. The middle device object is a functional device object (FDO) associated with the driver pair (ProsewareRobot.sys, GeneralRobot.sys). The driver pair serves as the function driver for the device stack. The bottom device object is a physical device object (PDO) associated with Pci.sys.

Notice that the driver pair occupies only one level in the device stack and is associated with only one device object: the FDO. When GeneralRobot.sys processes an IRP, it might call ProsewareRobot.sys for assistance, but that is not the same as passing the request down the device stack. The driver pair forms a single WDM driver that is at one level in the device stack. The driver pair either completes the IRP or passes it down the device stack to the PDO, which is associated with Pci.sys.

Example of a driver pair

Suppose you have a wireless network card in your laptop computer, and by looking in Device Manager, you determine that netwlv64.sys is the driver for the network card. You can use the !drvobj debugger extension to inspect the function pointers for netwlv64.sys.

1: kd> !drvobj netwlv64 2
Driver object (fffffa8002e5f420) is for:
 \Driver\netwlv64
DriverEntry:   fffff8800482f064 netwlv64!GsDriverEntry
DriverStartIo: 00000000 
DriverUnload:  fffff8800195c5f4 ndis!ndisMUnloadEx
AddDevice:     fffff88001940d30 ndis!ndisPnPAddDevice
Dispatch routines:
[00] IRP_MJ_CREATE                      fffff880018b5530 ndis!ndisCreateIrpHandler
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff88001936f00 ndis!ndisDummyIrpHandler
[02] IRP_MJ_CLOSE                       fffff880018b5870 ndis!ndisCloseIrpHandler
[03] IRP_MJ_READ                        fffff88001936f00 ndis!ndisDummyIrpHandler
[04] IRP_MJ_WRITE                       fffff88001936f00 ndis!ndisDummyIrpHandler
[05] IRP_MJ_QUERY_INFORMATION           fffff88001936f00 ndis!ndisDummyIrpHandler
[06] IRP_MJ_SET_INFORMATION             fffff88001936f00 ndis!ndisDummyIrpHandler
[07] IRP_MJ_QUERY_EA                    fffff88001936f00 ndis!ndisDummyIrpHandler
[08] IRP_MJ_SET_EA                      fffff88001936f00 ndis!ndisDummyIrpHandler
[09] IRP_MJ_FLUSH_BUFFERS               fffff88001936f00 ndis!ndisDummyIrpHandler
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff88001936f00 ndis!ndisDummyIrpHandler
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff88001936f00 ndis!ndisDummyIrpHandler
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff88001936f00 ndis!ndisDummyIrpHandler
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff88001936f00 ndis!ndisDummyIrpHandler
[0e] IRP_MJ_DEVICE_CONTROL              fffff8800193696c ndis!ndisDeviceControlIrpHandler
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff880018f9114 ndis!ndisDeviceInternalIrpDispatch
[10] IRP_MJ_SHUTDOWN                    fffff88001936f00 ndis!ndisDummyIrpHandler
[11] IRP_MJ_LOCK_CONTROL                fffff88001936f00 ndis!ndisDummyIrpHandler
[12] IRP_MJ_CLEANUP                     fffff88001936f00 ndis!ndisDummyIrpHandler
[13] IRP_MJ_CREATE_MAILSLOT             fffff88001936f00 ndis!ndisDummyIrpHandler
[14] IRP_MJ_QUERY_SECURITY              fffff88001936f00 ndis!ndisDummyIrpHandler
[15] IRP_MJ_SET_SECURITY                fffff88001936f00 ndis!ndisDummyIrpHandler
[16] IRP_MJ_POWER                       fffff880018c35e8 ndis!ndisPowerDispatch
[17] IRP_MJ_SYSTEM_CONTROL              fffff880019392c8 ndis!ndisWMIDispatch
[18] IRP_MJ_DEVICE_CHANGE               fffff88001936f00 ndis!ndisDummyIrpHandler
[19] IRP_MJ_QUERY_QUOTA                 fffff88001936f00 ndis!ndisDummyIrpHandler
[1a] IRP_MJ_SET_QUOTA                   fffff88001936f00 ndis!ndisDummyIrpHandler
[1b] IRP_MJ_PNP                         fffff8800193e518 ndis!ndisPnPDispatch

In the debugger output, you can see that netwlv64.sys implements GsDriverEntry, the entry point for the driver. GsDriverEntry, which was automatically generated when the driver was built, performs some initialization and then calls DriverEntry, which was written by the driver developer.

In this example, netwlv64.sys implements DriverEntry, but ndis.sys implements AddDevice, Unload, and several dispatch functions. Netwlv64.sys is called an NDIS miniport driver, and ndis.sys is called the NDIS Library. Together, the two modules form an (NDIS miniport, NDIS Library) pair.

This diagram shows the device stack for the wireless network card. Notice that the driver pair (netwlv64.sys, ndis.sys) occupies only one level in the device stack and is associated with only one device object: the FDO.

Diagram of the Wireless network card device stack, showing Netwlv64.sys, ndis.sys as the driver pair associated with the FDO and Pci.sys associated with the PDO

Available driver pairs

The different technology-specific driver models use a variety of names for the specific and general pieces of a driver pair. In many cases, the specific portion of the pair has the prefix "mini." Here are some of (specific, general) pairs that are available:

  • (display miniport driver, display port driver)
  • (audio miniport driver, audio port driver)
  • (storage miniport driver, storage port driver)
  • (battery miniclass driver, battery class driver)
  • (HID minidriver, HID class driver)
  • (changer miniclass driver, changer port driver)
  • (NDIS miniport driver, NDIS library)
Note  As you can see in the list, several of the models use the term class driver for the general portion of a driver pair. This kind of class driver is different from a standalone class driver and different from a class filter driver.
 

Related topics

Concepts for all driver developers
Device nodes and device stacks
Driver stacks

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

KMDF as a generic driver pair model

In this topic, we discuss the idea that the Kernel Mode Driver Framework can be viewed as a generic driver pair model. Before you read this topic, you should understand the ideas presented in Minidrivers and driver pairs.

Over the years, Microsoft has created several technology-specific driver models that use this paradigm:

  • The driver is split into two pieces: one that handles general processing and one that handles processing that is specific to a particular device.
  • The general piece, called the Framework, is written by Microsoft.
  • The specific piece, called the KMDF driver, may be written by Microsoft or an independent hardware vendor.
Diagram of KMDF as a Generic Driver Pair

The Framework portion of the driver pair performs general tasks that are common to a wide variety of drivers. For example, the Framework can handle I/O request queues, thread synchronization, and a large portion of the power management duties.

The Framework owns the dispatch table for the KMDF driver, so when someone sends an I/O request packet (IRP) to the (KMDF driver, Framework) pair, the IRP goes to Framework. If the Framework can handle the IRP by itself, the KMDF driver is not involved. If the Framework cannot handle the IRP by itself, it gets help by calling event handlers implemented by the KMDF driver. Here are some examples of event handlers that might be implemented by a KMDF driver.

For example, a USB 2.0 host controller driver has a specific piece named usbehci.sys and a general piece named usbport.sys. Usbehci.sys, which is called the USB 2.0 Miniport driver, has code that is specific to USB 2.0 host controllers. Usbport.sys, which is called the USB Port driver, has general code that applies to both USB 2.0 and USB 1.0. The pair of drivers (usbehci.sys, usbport.sys) combine to form a single WDM driver for a USB 2.0 host controller.

The (specific, general) driver pairs have a variety of names across different device technologies. Most of the device-specific drivers have the prefix mini. The general drivers are often called port or class drivers. Here are some examples of (specific, general) pairs:

  • (display miniport driver, display port driver)
  • (USB miniport driver, USB port driver)
  • (battery miniclass driver, battery class driver)
  • (HID minidriver, HID class driver)
  • (storage miniport driver, storage port driver)

As more and more driver pair models were developed, it became difficult to keep track of all the different ways to write a driver. Each model has it's own interface for communication between the device-specific driver and the general driver. The body of knowledge required to develop drivers for one device technology (for example, Audio) can be quite different from the body of knowledge required to develop drivers for another device technology (for example, Storage).

Over time, developers realized that it would be good to have a single unified model for kernel-mode driver pairs. The Kernel Mode Driver Framework (KMDF), which was first available in Windows Vista, fulfills that need. A driver based on KMDF uses a paradigm that is similar to many of the technology-specific driver pair models.

  • The driver is split into two pieces: one that handles general processing and one that handles processing that is specific to a particular device.
  • The general piece, which is written by Microsoft, is called the Framework.
  • The specific piece, which is written by Microsoft or an independent hardware vendor, is called the KMDF driver.

The USB 3.0 host controller driver is an example of a driver based on KMDF. In this example, both drivers in the pair are written by Microsoft. The general driver is the Framework, and the device-specific driver is the USB 3.0 Host Controller Driver. This diagram illustrates the device node and device stack for a USB 3.0 host controller.

Diagram of device stack for USB 3 host controller

In the diagram, Usbxhci.sys is the USB 3.0 host controller driver. It is paired with Wdf01000.sys, which is the Framework. The (usbxhci.sys, wdf01000.sys) pair forms a single WDM driver that serves as the function driver for the USB 3.0 host controller. Notice that the driver pair occupies one level in the device stack and is represented by single device object. The single device object that represents the (usbxhci.sys, wdf01000.sys) pair is the functional device object (FDO) for the USB 3.0 host controller.

In a (KMDF driver, Framework) pair, the Framework handles tasks that are common to a wide variety of kernel-mode drivers. For example, the Framework can handle queuing of I/O requests, thread synchronization, most of the Plug and Play tasks, and most of the power management tasks. The KMDF driver handles tasks that require interaction with a specific device. The KMDF driver participates in processing requests by registering event handlers that the Framework calls as needed.

Related topics

Minidrivers and driver pairs
Kernel-Mode Driver Framework

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

KMDF extensions and driver triples

In this topic, we discuss class-based extensions to the Kernel Mode Driver Framework (KMDF). Before you read this topic, you should understand the ideas presented in Minidrivers and driver pairs and KMDF as a Generic Driver Pair Model.

For some device classes, Microsoft provides KMDF extensions that further reduce the amount of processing that must be performed by KMDF drivers. A driver that uses a class-based KMDF extension has these three pieces, which we call a driver triple.

  • The Framework, which handles tasks common to most all drivers
  • The class-based framework extension, which handles tasks that are specific to a particular class of devices
  • The KMDF driver, which handles tasks that are specific to a particular device.

The three drivers in a driver triple (KMDF driver, device-class KMDF extension, Framework) combine to form a single WDM driver.

An example of a device-class KMDF extension is SpbCx.sys, which is the KMDF extension for the Simple Peripheral Bus (SPB) device class. The SPB class includes synchronous serial buses such as I2C and SPI. A driver triple for an I2C bus controller would look like this:

  • The Framework handles general tasks that are common to most all drivers.
  • SpbCx.sys handles tasks that are specific to the SPB bus class. These are tasks that are common to all SPB busses.
  • The KMDF driver handles tasks that are specific to an I2C bus. Let's call this driver MyI2CBusDriver.sys.
KMDF Driver Triple Extension

The three drivers in the driver triple (MyI2CBusDriver.sys, SpbCx.sys, Wdf01000.sys) combine to form a single WDM driver that serves as the function driver for the I2C bus controller. Wdf01000.sys (the Framework) owns the dispatch table for this driver, so when someone sends an IRP to the driver triple, it goes to the wdf01000.sys. If the wdf01000.sys can handle the IRP by itself, SpbCx.sys and MyI2CBusDriver.sys are not involved. If wdf01000.sys cannot handle the IRP by itself, it gets help by calling an event handler in SbpCx.sys.

Here are some examples of event handlers that might be implemented by MyI2CBusDriver.sys:

  • EvtSpbControllerLock
  • EvtSpbIoRead
  • EvtSpbIoSequence

Here are some examples of event handlers that are implemented by SpbCx.sys

  • EvtIoRead

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Upper and lower edges of drivers

The sequence of drivers that participate in an I/O request is called the driver stack for the request. A driver can call into the upper edge of a lower driver in the stack. A driver can also call into the lower edge of a higher driver in the stack.

Before you read this topic, you should understand the ideas presented in Device nodes and device stacks and Driver stacks.

I/O requests are processed first by the top driver in the driver stack, then by the next lower driver, and so on until the request is fully processed.

When a driver implements a set of functions that a higher driver can call, that set of functions is called the upper edge of the driver or the upper-edge interface of the driver.

When a driver implements a set of functions that a lower driver can call, that set of functions is called the lower edge of the driver or the lower-edge interface of the driver.

Audio example

We can think of an audio miniport driver sitting below an audio port driver in a driver stack. The port driver makes calls to the miniport driver's upper edge. The miniport driver makes calls to the port driver's lower edge.

Diagram of audio port driver above miniport driver

The preceding diagram illustrates that it is sometimes useful to think of a port driver sitting above a miniport driver in a driver stack. Because I/O requests are processed first by the port driver and then by the miniport driver, it is reasonable to think of the port driver as being above the miniport driver. Keep in mind, however, that a (miniport, port) driver pair usually sits at a single level in a device stack, as shown here.

Diagram of device stack with (miniport/port) pair

Note that a device stack is not the same thing as a driver stack. For definitions of these terms, and for a discussion of how a pair of drivers can form a single WDM driver that occupies one level in a device stack, see Minidrivers and driver pairs.

Here's another way to draw a diagram of the same device node and device stack:

Diagram of device stack with port driver above miniport

In the preceding diagram, we see that the (miniport, port) pair forms a single WDM driver that is associated with a single device object (the FDO) in the device stack; that is, the (miniport, port) pair occupies only one level in the device stack. But we also see a vertical relationship between the miniport and port drivers. The port driver is shown above the miniport driver to indicate that the port driver processes I/O requests first and then calls into the miniport driver for additional processing.

The key point is that when the port driver calls into the miniport driver's upper-edge interface, that is not the same as passing an I/O request down the device stack. In a driver stack (not device stack) you can choose to draw a port driver above a miniport driver, but that does not mean that the port driver is above the miniport driver in the device stack.

NDIS example

Sometimes a driver calls the upper edge of a lower driver indirectly. For example, suppose a TCP/IP protocol driver sits above an NDIS miniport driver in a driver stack. The miniport driver implements a set of MiniportXxx functions that form the miniport driver's upper edge. We say that the TCP/IP protocol driver binds to the upper edge of the NDIS miniport driver. But the TCP/IP driver does not call the MiniportXxx functions directly. Instead, it calls functions in the NDIS library, which then call the MiniportXxx functions.

Diagram of TCP/IP and NDIS miniport stack

The preceding diagram shows a driver stack. Here's another view of the same drivers.

Diagram of device stack for a network card

The preceding diagram shows the device node for a network interface card (NIC). The device node has a position in the Plug and Play (PnP) device tree. The device node for the NIC has a device stack with three device objects. Notice that the NDIS miniport driver and the NDIS library work as a pair. The pair (MyMiniport.sys, Ndis.sys) forms a single WDM driver that is represented by the functional device object (FDO).

Also notice that the protocol driver Tcpip.sys is not part of the device stack for the NIC. In fact, Tcpip.sys is not part of the PnP device tree at all.

Summary

The terms upper edge and lower edge are used to describe the interfaces that drivers in a stack use to communicate with each other. A driver stack is not the same thing as device stack. Two drivers that are shown vertically in a driver stack might form a driver pair that sits at a single level in a device stack. Some drivers are not part of the PnP device tree.

Related topics

Concepts for all driver developers
Device nodes and device stacks
Driver stacks
Audio Devices
Network Drivers Starting with Windows Vista

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Header files in the Windows Driver Kit

The Windows Driver Kit (WDK) contains all the header files (.h files) that you need to build kernel-mode and user-mode drivers. Header files are in the Include folder in your WDK installation folder. Example: C:\Program Files (x86)\Windows Kits\10\Include.

The header files contain version information so that you can use the same set of header files regardless of which version of Windows your driver will run on.

Constants that represent Windows versions

Header files in the WDK contain conditional statements that specify programming elements that are available only in certain versions of the Windows operating system. The versioned elements include functions, enumerations, structures, and structure members.

To specify the programming elements that are available in each operating system version, the header files contain preprocessor conditionals that compare the value of NTDDI_VERSION with a set of predefined constant values that are defined in Sdkddkver.h.

Here are the predefined constant values that represent versions of the Microsoft Windows operating system.

ConstantOperating system version

NTDDI_WIN10

Windows 10

NTDDI_WINBLUE

Windows 8.1

NTDDI_WIN8

Windows 8

NTDDI_WIN7

Windows 7

NTDDI_WS08SP4

Windows Server 2008 with SP4

NTDDI_WS08SP3

Windows Server 2008 with SP3

NTDDI_WS08SP2

Windows Server 2008 with SP2

NTDDI_WS08

Windows Server 2008

 

You can see many examples of version-specific DDI elements in the WDK header files. This conditional declaration appears in Wdm.h, which is a header file that might be included by a kernel-mode driver.

#if (NTDDI_VERSION >= NTDDI_WIN7)
_Must_inspect_result_
NTKERNELAPI
NTSTATUS
KeSetTargetProcessorDpcEx (
    _Inout_ PKDPC Dpc,
    _In_ PPROCESSOR_NUMBER ProcNumber
    );
#endif

In the example you can see that the KeSetTargetProcessorDpcEx function is available only in Windows 7 and later versions of Windows.

This conditional declaration appears in Winspool.h, which is a header file that might be included by a user-mode driver.

C++
#if (NTDDI_VERSION >= NTDDI_WIN7)
...
BOOL
WINAPI
GetPrintExecutionData(
    _Out_ PRINT_EXECUTION_DATA *pData
    );

#endif // (NTDDI_VERSION >= NTDDI_WIN7)

In the example can see that the GetPrintExecutionData function is available only in Windows 7 and later versions of Windows.

Header files for the Kernel Mode Driver Framework

The WDK supports several versions of Windows, and it also supports several versions of the Kernel Mode Driver Framework (KMDF) and User Mode Driver Framework (UMDF). The versioning information in the WDK header files pertains to Windows versions, but not to KMDF or UMDF versions. Header files for different versions of KMDF and UMDF are placed in separate directories.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Writing drivers for different versions of Windows

When you create a driver project, you specify the minimum target operating system, which is the minimum version of Windows that your driver will run on. For example, you could specify that Windows 7 is the minimum target operating system. In that case, your driver would run on Windows 7 and later versions of Windows.

Note  If you develop a driver for a particular minimum version of Windows and you want your driver to work on later versions of Windows, you must not use any undocumented functions, and you must not use documented functions in any way other than how it is described in the documentation. Otherwise your driver might fail to run on the later versions of Windows. Even if you have been careful to use only documented functions, you should test your driver on the new version of Windows each time one is released.
 

Writing a multiversion driver using only common features

When you design a driver that will run on multiple versions of Windows, the simplest approach is to allow the driver to use only DDI functions and structures that are common to all versions of Windows that the driver will run on. In this situation, you set the minimum target operating system to the earliest version of Windows that the driver will support.

For example, to support all versions of Windows, starting with Windows 7, you should:

  1. Design and implement the driver so that it uses only those features that are present in Windows 7.

  2. When you build your driver, specify Windows 7 as the minimum target operating system.

While this process is simple, it might restrict the driver to use only a subset of the functionality that is available on later versions of Windows.

Writing a multiversion driver that uses version-dependent features

A kernel-mode driver can dynamically determine which version of Windows it is running on and choose to use features that are available in that version. For example, a driver that must support all versions of Windows, starting with Windows 7, can determine, at run time, the version of Windows that it is running on. If the driver is running on Windows 7, it must use only the DDI functions that Windows 7 supports. However, the same driver can use additional DDI functions that are unique to Windows 8, for example, when its run-time check determines that it is running on Windows 8.

Determining the Windows version

RtlIsNtDdiVersionAvailable is a function that drivers can use to determine, at run time, if the features that are provided by a particular version of Windows are available. The prototype for this function is as follows:

BOOLEAN RtlIsNtDdiVersionAvailable(IN ULONG Version)

In this prototype, Version is a value that indicates the required version of the Windows DDI. This value must be one of the DDI version constants, defined in sdkddkver.h, such as NTDDI_WIN8 or NTDDI_WIN7.

RtlIsNtDdiVersionAvailable returns TRUE when the caller is running on a version of Windows that is the same as, or later than, the one that is specified by Version.

Your driver can also check for a specific service pack by calling the RtlIsServicePackVersionInstalled function. The prototype for this function is as follows:

BOOLEAN RtlIsServicePackVersionInstalled(IN ULONG Version)

In this prototype, Version is a value that indicates the required Windows version and service pack. This value must be one of the DDI version constants, defined in sdkddkver.h, such as NTDDI_WS08SP3.

Note that RtlIsServicePackVersionInstalled returns TRUE only when the operating system version exactly matches the specified version. Thus, a call to RtlIsServicePackVersionInstalled with Version set to NTDDI_WS08SP3 will fail if the driver is not running on Windows Server 2008 with SP4.

Conditionally calling Windows version-dependent functions

After a driver determines that a specific operating system version is available on the computer, the driver can use the MmGetSystemRoutineAddress function to dynamically locate the routine and call it through a pointer. This function is available in Windows 7 and later operating system versions.

Note  To help preserve type checking and prevent unintentional errors, you should create a typedef that mirrors the original function type.
 

Example: Determining the Windows version and conditionally calling a version-dependent function

This code example, which is from a driver's header file, defines the PAISQSL type as a pointer to the KeAcquireInStackQueuedSpinLock function. The example then declares a AcquireInStackQueuedSpinLock variable with this type.

...
 //
// Pointer to the ordered spin lock function.
// This function is only available on Windows 7 and
// later systems
 typedef (* PAISQSL) (KeAcquireInStackQueuedSpinLock);
PAISQSL AcquireInStackQueued = NULL;
 ...
 

This code example, which is from the driver's initialization code, determines whether the driver is running on Windows 7 or a later operating system. If it is, the code retrieves a pointer to KeAcquireInStackQueuedSpinLock.

...
 
//
// Are we running on Windows 7 or later?
//
 if (RtlIsNtDdiVersionAvailable(NTDDI_WIN7) ) {
 
 //
  // Yes... Windows 7 or later it is!
  //
     RtlInitUnicodeString(&funcName,
                  L"KeAcquireInStackQueuedSpinLock");
 
 //
  // Get a pointer to Windows implementation
  // of KeAcquireInStackQueuedSpinLock into our
  // variable "AcquireInStackQueued"
     AcquireInStackQueued = (PAISQSL)
                  MmGetSystemRoutineAddress(&funcName);
 }
 
...
// Acquire a spin lock.
 
 if( NULL != AcquireInStackQueued) {
  (AcquireInStackQueued)(&SpinLock, &lockHandle);
} else {
    KeAcquireSpinLock(&SpinLock);
}
 

In the example the driver calls RtlIsNtDdiVersionAvailable to determine whether the driver is running on Windows 7 or later. If the version is Windows 7 or later, the driver calls MmGetSystemRoutineAddress to get a pointer to the KeAcquireInStackQueuedSpinLock function and stores this pointer in the variable named AcquireInStackQueued (which was declared as a PAISQSL type).

Later, when the driver must acquire a spin lock, it checks to see whether it has received a pointer to the KeAcquireInStackQueuedSpinLock function. If the driver has received this pointer, the driver uses the pointer to call KeAcquireInStackQueuedSpinLock. If the pointer to KeAcquireInStackQueuedSpinLock is null, the driver uses KeAcquireSpinLock to acquire the spin lock.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

User mode and kernel mode

A processor in a computer running Windows has two different modes: user mode and kernel mode. The processor switches between the two modes depending on what type of code is running on the processor. Applications run in user mode, and core operating system components run in kernel mode. While many drivers run in kernel mode, some drivers may run in user mode.

When you start a user-mode application, Windows creates a process for the application. The process provides the application with a private virtual address space and a private handle table. Because an application's virtual address space is private, one application cannot alter data that belongs to another application. Each application runs in isolation, and if an application crashes, the crash is limited to that one application. Other applications and the operating system are not affected by the crash.

In addition to being private, the virtual address space of a user-mode application is limited. A processor running in user mode cannot access virtual addresses that are reserved for the operating system. Limiting the virtual address space of a user-mode application prevents the application from altering, and possibly damaging, critical operating system data.

All code that runs in kernel mode shares a single virtual address space. This means that a kernel-mode driver is not isolated from other drivers and the operating system itself. If a kernel-mode driver accidentally writes to the wrong virtual address, data that belongs to the operating system or another driver could be compromised. If a kernel-mode driver crashes, the entire operating system crashes.

This diagram illustrates communication between user-mode and kernel-mode components.

Block diagram of user-mode and kernel-mode components

Related topics

Virtual Address Spaces

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Virtual address spaces

When a processor reads or writes to a memory location, it uses a virtual address. As part of the read or write operation, the processor translates the virtual address to a physical address. Accessing memory through a virtual address has these advantages:

  • A program can use a contiguous range of virtual addresses to access a large memory buffer that is not contiguous in physical memory.

  • A program can use a range of virtual addresses to access a memory buffer that is larger than the available physical memory. As the supply of physical memory becomes small, the memory manager saves pages of physical memory (typically 4 kilobytes in size) to a disk file. Pages of data or code are moved between physical memory and the disk as needed.

  • The virtual addresses used by different processes are isolated from each other. The code in one process cannot alter the physical memory that is being used by another process or the operating system.

The range of virtual addresses that is available to a process is called the virtual address space for the process. Each user-mode process has its own private virtual address space. For a 32-bit process, the virtual address space is usually the 2-gigabyte range 0x00000000 through 0x7FFFFFFF. For a 64-bit process, the virtual address space is the 8-terabyte range 0x000'00000000 through 0x7FF'FFFFFFFF. A range of virtual addresses is sometimes called a range of virtual memory.

This diagram illustrates some of the key features of virtual address spaces.

Diagram of virtual address spaces for two processes

The diagram shows the virtual address spaces for two 64-bit processes: Notepad.exe and MyApp.exe. Each process has its own virtual address space that goes from 0x000'0000000 through 0x7FF'FFFFFFFF. Each shaded block represents one page (4 kilobytes in size) of virtual or physical memory. Notice that the Notepad process uses three contiguous pages of virtual addresses, starting at 0x7F7'93950000. But those three contiguous pages of virtual addresses are mapped to noncontiguous pages in physical memory. Also notice that both processes use a page of virtual memory beginning at 0x7F7'93950000, but those virtual pages are mapped to different pages of physical memory.

User space and system space

Processes like Notepad.exe and MyApp.exe run in user mode. Core operating system components and many drivers run in the more privileged kernel mode. For more information about processor modes, see User mode and kernel mode. Each user-mode process has its own private virtual address space, but all code that runs in kernel mode shares a single virtual address space called system space. The virtual address space for the current user-mode process is called user space.

In 32-bit Windows, the total available virtual address space is 2^32 bytes (4 gigabytes). Usually the lower 2 gigabytes are used for user space, and the upper 2 gigabytes are used for system space.

Diagram of system space

In 32-bit Windows, you have the option of specifying (at boot time) that more than 2 gigabytes are available for user space. The consequence is that fewer virtual addresses are available for system space. You can increase the size of user space to as much as 3 gigabytes, in which case only 1 gigabyte is available for system space. To increase the size of user space, use BCDEdit /set increaseuserva.

In 64-bit Windows, the theoretical amount of virtual address space is 2^64 bytes (16 exabytes), but only a small portion of the 16-exabyte range is actually used. The 8-terabyte range from 0x000'00000000 through 0x7FF'FFFFFFFF is used for user space, and portions of the 248-terabyte range from 0xFFFF0800'00000000 through 0xFFFFFFFF'FFFFFFFF are used for system space.

Diagram of paged pool and nonpaged pool

Code running in user mode has access to user space but does not have access to system space. This restriction prevents user-mode code from reading or altering protected operating system data structures. Code running in kernel mode has access to both user space and system space. That is, code running in kernel mode has access to system space and the virtual address space of the current user-mode process.

Drivers that run in kernel mode must be very careful about directly reading from or writing to addresses in user space. This scenario illustrates why.

  1. A user-mode program initiates a request to read some data from a device. The program supplies the starting address of a buffer to receive the data.

  2. A device driver routine, running in kernel mode, starts the read operation and returns control to its caller.

  3. Later the device interrupts whatever thread is currently running to say that the read operation is complete. The interrupt is handled by kernel-mode driver routines running on this arbitrary thread, which belongs to an arbitrary process.
  4. At this point, the driver must not write the data to the starting address that the user-mode program supplied in Step 1. This address is in the virtual address space of the process that initiated the request, which is most likely not the same as the current process.

Paged pool and Nonpaged pool

In user space, all physical memory pages can be paged out to a disk file as needed. In system space, some physical pages can be paged out and others cannot. System space has two regions for dynamically allocating memory: paged pool and nonpaged pool. In 64-bit Windows, paged pool is the 128-gigabyte range of virtual addresses that goes from 0xFFFFA800'00000000 through 0xFFFFA81F'FFFFFFFF. Nonpaged pool is the 128-gigabyte range of virtual addresses that goes from 0xFFFFAC00'00000000 through 0xFFFFAC1F'FFFFFFFF.

Memory that is allocated in paged pool can be paged out to a disk file as needed. Memory that is allocated in nonpaged pool can never be paged out to a disk file.

Diagram comparing memory allocation in paged pool to that in nonpaged pool

Related topics

User mode and kernel mode

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Device nodes and device stacks

In Windows, devices are represented by device nodes in the Plug and Play (PnP) device tree. Typically, when an I/O request is sent to a device, several drivers help handle the request. Each of these drivers is associated with a device object, and the device objects are arranged in a stack. The sequence of device objects along with their associated drivers is called a device stack. Each device node has its own device stack.

Device nodes and the Plug and Play device tree

Windows organizes devices in a tree structure called the Plug and Play device tree, or simply the device tree. Typically, a node in the device tree represents either a device or an individual function on a composite device. However, some nodes represent software components that have no association with physical devices.

A node in the device tree is called a device node. The root node of the device tree is called the root device node. By convention, the root device node is drawn at the bottom of the device tree, as shown in the following diagram.

Diagram of the device tree, showing device nodes

The device tree illustrates the parent/child relationships that are inherent in the PnP environment. Several of the nodes in the device tree represent buses that have child devices connected to them. For example, the PCI Bus node represents the physical PCI bus on the motherboard. During startup, the PnP manager asks the PCI bus driver to enumerate the devices that are connected to the PCI bus. Those devices are represented by child nodes of the PCI Bus node. In the preceding diagram, the PCI Bus node has child nodes for several devices that are connected to the PCI bus, including USB host controllers, an audio controller, and a PCI Express port.

Some of the devices connected to the PCI bus are buses themselves. The PnP manager asks each of these buses to enumerate the devices that are connected to it. In the preceding diagram, we can see that the audio controller is a bus that has an audio device connected to it. We can see that the PCI Express port is a bus that has a display adapter connected to it, and the display adapter is a bus that has one monitor connected to it.

Whether you think of a node as representing a device or a bus depends on your point of view. For example, you can think of the display adapter as a device that plays a key role in preparing frames that appear on the screen. However, you can also think of the display adapter as a bus that is capable of detecting and enumerating connected monitors.

Device objects and device stacks

A device object is an instance of a DEVICE_OBJECT structure. Each device node in the PnP device tree has an ordered list of device objects, and each of these device objects is associated with a driver. The ordered list of device objects, along with their associated drivers, is called the device stack for the device node.

You can think of a device stack in several ways. In the most formal sense, a device stack is an ordered list of (device object, driver) pairs. However, in certain contexts it might be useful to think of the device stack as an ordered list of device objects. In other contexts, it might be useful to think of the device stack as an ordered list of drivers.

By convention, a device stack has a top and a bottom. The first device object to be created in the device stack is at the bottom, and the last device object to be created and attached to the device stack is at the top.

In the following diagram, the Proseware Gizmo device node has a device stack that contains three (device object, driver) pairs. The top device object is associated with the driver AfterThought.sys, the middle device object is associated with the driver Proseware.sys, and the bottom device object is associated with the driver Pci.sys. The PCI Bus node in the center of the diagram has a device stack that contains two (device object, driver) pairs--a device object associated with Pci.sys and a device object associated with Acpi.sys.

Diagram showing device objects ordered in device stacks in the Proseware Gizmo and PCI device nodes

How does a device stack get constructed?

During startup, the PnP manager asks the driver for each bus to enumerate child devices that are connected to the bus. For example, the PnP manager asks the PCI bus driver (Pci.sys) to enumerate the devices that are connected to the PCI bus. In response to this request, Pci.sys creates a device object for each device that is connected to the PCI bus. Each of these device objects is called a physical device object (PDO). Shortly after Pci.sys creates the set of PDOs, the device tree looks like the one shown in the following diagram.

Diagram of PCI node and physical device objects for child devices

The PnP manager associates a device node with each newly created PDO and looks in the registry to determine which drivers need to be part of the device stack for the node. The device stack must have one (and only one) function driver and can optionally have one or more filter drivers. The function driver is the main driver for the device stack and is responsible for handling read, write, and device control requests. Filter drivers play auxiliary roles in processing read, write, and device control requests. As each function and filter driver is loaded, it creates a device object and attaches itself to the device stack. A device object created by the function driver is called a functional device object (FDO), and a device object created by a filter driver is called a filter device object (Filter DO). Now the device tree looks something like this diagram.

Diagram of a device tree showing the filter, function, and physical device objects in the Proseware Gizmo device node

In the diagram, notice that in one node, the filter driver is above the function driver, and in the other node, the filter driver is below the function driver. A filter driver that is above the function driver in a device stack is called an upper filter driver. A filter driver that is below the function driver is called a lower filter driver.

The PDO is always the bottom device object in a device stack. This results from the way a device stack is constructed. The PDO is created first, and as additional device objects are attached to the stack, they are attached to the top of the existing stack.

Note  

When the drivers for a device are installed, the installer uses information in an information (INF) file to determine which driver is the function driver and which drivers are filters. Typically the INF file is provided either by Microsoft or by the hardware vendor. After the drivers for a device are installed, the PnP manager can determine the function and filter drivers for the device by looking in the registry.

 

Bus drivers

In the preceding diagram, you can see that the driver Pci.sys plays two roles. First, Pci.sys is associated with the FDO in the PCI Bus device node. In fact, it created the FDO in the PCI Bus device node. So Pci.sys is the function driver for the PCI bus. Second, Pci.sys is associated with the PDO in each child of the PCI Bus node. Recall that it created the PDOs for the child devices. The driver that creates the PDO for a device node is called the bus driver for the node.

If your point of reference is the PCI bus, then Pci.sys is the function driver. But if your point of reference is the Proseware Gizmo device, then Pci.sys is the bus driver. This dual role is typical in the PnP device tree. A driver that serves as function driver for a bus also serves as bus driver for a child device of the bus.

User-mode device stacks

So far we've been discussing kernel-mode device stacks. That is, the drivers in the stacks run in kernel mode, and the device objects are mapped into system space, which is the address space that is available only to code running in kernel mode. For information about the difference between kernel mode and user mode, see User mode and kernel mode.

In some cases, a device has a user-mode device stack in addition to its kernel-mode device stack. User-mode drivers are often based on the User-Mode Driver Framework (UMDF), which is one of the driver models provided by the Windows Driver Frameworks (WDF). In UMDF, the drivers are user-mode DLLs, and the device objects are COM objects that implement the IWDFDevice interface. A device object in a UMDF device stack is called a WDF device object (WDF DO).

The following diagram shows the device node, kernel-mode device stack, and the user-mode device stack for a USB-FX-2 device. The drivers in both the user-mode and kernel-mode stacks participate in I/O requests that are directed at the USB-FX-2 device.

Diagram showing user-mode and kernel-mode device stacks

Related topics

Concepts for all driver developers
Driver stacks

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

I/O request packets

Most of the requests that are sent to device drivers are packaged in I/O request packets (IRPs). An operating system component or a driver sends an IRP to a driver by calling IoCallDriver, which has two parameters: a pointer to a DEVICE_OBJECT and a pointer to an IRP. The DEVICE_OBJECT has a pointer to an associated DRIVER_OBJECT. When a component calls IoCallDriver, we say the component sends the IRP to the device object or sends the IRP to the driver associated with the device object. Sometimes we use the phrase passes the IRP or forwards the IRP instead of sends the IRP.

Typically an IRP is processed by several drivers that are arranged in a stack. Each driver in the stack is associated with a device object. For more information, see Device nodes and device stacks. When an IRP is processed by a device stack, the IRP is usually sent first to the top device object in the device stack. For example, if an IRP is processed by the device stack shown in this diagram, the IRP would be sent first to the filter device object (Filter DO) at the top of the device stack.

Diagram of a device node and its device stack

Passing an IRP down the device stack

Suppose the I/O manager sends an IRP to the Filter DO in the diagram. The driver associated with the Filter DO, AfterThought.sys, processes the IRP and then passes it to the functional device object (FDO), which is the next lower device object in the device stack. When a driver passes an IRP to the next lower device object in the device stack, we say the driver passes the IRP down the device stack.

Some IRPs are passed all the way down the device stack to the physical device object (PDO). Other IRPs never reach the PDO because they are completed by one of the drivers above the PDO.

IRPs are self-contained

The IRP structure is self-contained in the sense that it holds all of the information that a driver needs to handle an I/0 request. Some parts of the IRP structure hold information that is common to all of the participating drivers in the stack. Other parts of the IRP hold information that is specific to a particular driver in the stack.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Driver stacks

Most of the requests that are sent to device drivers are packaged in I/O request packets (IRPs). Each device is represented by a device node, and each device node has a device stack. For more information, see Device nodes and device stacks. To send a read, write, or control request to a device, the I/O manager locates the device node for the device and then sends an IRP to the device stack of that node. Sometimes more than one device stack is involved in processing an I/O request. Regardless of how many device stacks are involved, the overall sequence of drivers that participate in an I/O request is called the driver stack for the request. We also use the term driver stack to refer to the layered set of drivers for a particular technology.

I/O requests that are processed by several device stacks

In some cases, more than one device stack is involved in processing an IRP. The following diagram illustrates a case where four device stacks are involved in processing a single IRP.

Diagram of four device nodes, each with a device stack

Here is how the IRP is processed at each numbered stage in the diagram:

  1. The IRP is created by Disk.sys, which is the function driver in the device stack for the My USB Storage Device node. Disk.sys passes the IRP down the device stack to Usbstor.sys.

  2. Notice that Usbstor.sys is the PDO driver for the My USB Storage Device node and the FDO driver for the USB Mass Storage Device node. At this point, it is not important to decide whether the IRP is owned by the (PDO, Usbstor.sys) pair or the (FDO, Usbstor.sys) pair. The IRP is owned by the driver, Usbstor.sys, and the driver has access to both the PDO and the FDO.
  3. When Usbstor.sys has finished processing the IRP, it passes the IRP to Usbhub.sys. Usbhub.sys is the PDO driver for the USB Mass Storage Device node and the FDO driver for the USB Root Hub node. It is not important to decide whether the IRP is owned by the (PDO, Usbhub.sys) pair or the (FDO, Usbhub.sys) pair. The IRP is owned by the driver, Usbhub.sys, and the driver has access to both the PDO and the FDO.

  4. When Usbhub.sys has finished processing the IRP, it passes the IRP to the (Usbuhci.sys, Usbport.sys) pair.

    Usbuhci.sys is a miniport driver, and Usbport.sys is a port driver. The (miniport, port) pair plays the role of a single driver. In this case, both the miniport driver and the port driver are written by Microsoft. The (Usbuhci.sys, Usbport.sys) pair is the PDO driver for the USB Root Hub node, and the (Usbuhci.sys, Usbport.sys) pair is also the FDO driver for the USB Host Controller node. The (Usbuhci.sys, Usbport.sys) pair does the actual communication with the host controller hardware, which in turn communicates with the physical USB storage device.

The driver stack for an I/O request

Consider the sequence of four drivers that participated in the I/O request illustrated in the preceding diagram. We can get another view of the sequence by focusing on the drivers rather than on the device nodes and their individual device stacks. The following diagram shows the drivers in sequence from top to bottom. Notice that Disk.sys is associated with one device object, but each of the other three drivers is associated with two device objects.

Diagram of a driver stack, showing the top driver associated with an FDO only, and the other three drivers associated with a PDO and an FDO

The sequence of drivers that participate in an I/O request is called the driver stack for the I/O request. To illustrate a driver stack for an I/O request, we draw the drivers from top to bottom in the order that they participate in the request.

Notice that the driver stack for an I/O request is quite different from the device stack for a device node. Also notice that the driver stack for an I/O request does not necessarily remain in one branch of the device tree.

Technology driver stacks

Consider the driver stack for the I/O request shown in the preceding diagram. If we give each of the drivers a friendly name and make some slight changes to the diagram, we have a block diagram that is similar to many of those that appear in the Windows Driver Kit (WDK) documentation.

Diagram of a driver stack showing friendly names for the drivers: Disk Class Driver on top followed by USB Storage Port Driver, and then USB Hub Driver and (USB 2 Miniport, USB Port) Driver

In the diagram, the driver stack is divided into three sections. We can think of each section as belonging to a particular technology or to a particular component or portion of the operating system. For example, we might say that the first section at the top of the driver stack belongs to the Volume Manager, the second section belongs to the storage component of the operating system, and the third section belongs to the core USB portion of the operating system.

Consider the drivers in the third section. These drivers are a subset of a larger set of core USB drivers that Microsoft provides for handling various kinds of USB requests and USB hardware. The following diagram shows what the entire USB core block diagram might look like.

Diagram showing the technology driver stack for possible USB core block

A block diagram that shows all of the drivers for a particular technology or a particular component or portion of the operating system is called a technology driver stack. Typically, technology driver stacks are given names like the USB Core Driver Stack, the Storage Stack, the 1394 Driver Stack, and the Audio Driver Stack.

Note  The USB core block diagram in this topic shows one of several possible ways to illustrate the technology driver stacks for USB 1.0 and 2.0. For the official diagrams of the USB 1.0, 2.0, and 3.0 driver stacks, see USB Driver Stack Architecture.
 

Related topics

Device nodes and device stacks
Minidrivers and driver pairs
Concepts for all driver developers

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Minidrivers, Miniport drivers, and driver pairs

A minidriver or a miniport driver acts as half of a driver pair. Driver pairs like (miniport, port) can make driver development easier. In a driver pair, one driver handles general tasks that are common to a whole collection of devices, while the other driver handles tasks that are specific to an individual device. The drivers that handle device-specific tasks go by a variety of names, including miniport driver, miniclass driver, and minidriver.

Microsoft provides the general driver, and typically an independent hardware vendor provides the specific driver. Before you read this topic, you should understand the ideas presented in Device nodes and device stacks and I/O request packets.

Every kernel-mode driver must implement a function named DriverEntry, which gets called shortly after the driver is loaded. The DriverEntry function fills in certain members of a DRIVER_OBJECT structure with pointers to several other functions that the driver implements. For example, the DriverEntry function fills in the Unload member of the DRIVER_OBJECT structure with a pointer to the driver's Unload function, as shown in the following diagram.

Diagram showing the DRIVER_OBJECT structure with the Unload member

The MajorFunction member of the DRIVER_OBJECT structure is an array of pointers to functions that handle I/O request packets (IRPs), as shown in the following diagram. Typically the driver fills in several members of the MajorFunction array with pointers to functions (implemented by the driver) that handle various kinds of IRPs.

Diagram showing the DRIVER_OBJECT structure with the MajorFunction member

An IRP can be categorized according to its major function code, which is identified by a constant, such as IRP_MJ_READ, IRP_MJ_WRITE, or IRP_MJ_PNP. The constants that identify major function code serve as indices in the MajorFunction array. For example, suppose the driver implements a dispatch function to handle IRPs that have the major function code IRP_MJ_WRITE. In this case, the driver must fill in the MajorFunction[IRP_MJ_WRITE] element of the array with a pointer to the dispatch function.

Typically the driver fills in some of the elements of the MajorFunction array and leaves the remaining elements set to default values provided by the I/O manager. The following example shows how to use the !drvobj debugger extension to inspect the function pointers for the parport driver.

0: kd> !drvobj parport 2
Driver object (fffffa80048d9e70) is for:
 \Driver\Parport
DriverEntry:   fffff880065ea070	parport!GsDriverEntry
DriverStartIo: 00000000	
DriverUnload:  fffff880065e131c	parport!PptUnload
AddDevice:     fffff880065d2008	parport!P5AddDevice

Dispatch routines:
[00] IRP_MJ_CREATE                      fffff880065d49d0	parport!PptDispatchCreateOpen
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       fffff880065d4a78	parport!PptDispatchClose
[03] IRP_MJ_READ                        fffff880065d4bac	parport!PptDispatchRead
[04] IRP_MJ_WRITE                       fffff880065d4bac	parport!PptDispatchRead
[05] IRP_MJ_QUERY_INFORMATION           fffff880065d4c40	parport!PptDispatchQueryInformation
[06] IRP_MJ_SET_INFORMATION             fffff880065d4ce4	parport!PptDispatchSetInformation
[07] IRP_MJ_QUERY_EA                    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                      fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS               fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL              fffff880065d4be8	parport!PptDispatchDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff880065d4c24	parport!PptDispatchInternalDeviceControl
[10] IRP_MJ_SHUTDOWN                    fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL                fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[12] IRP_MJ_CLEANUP                     fffff880065d4af4	parport!PptDispatchCleanup
[13] IRP_MJ_CREATE_MAILSLOT             fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY                fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                       fffff880065d491c	parport!PptDispatchPower
[17] IRP_MJ_SYSTEM_CONTROL              fffff880065d4d4c	parport!PptDispatchSystemControl
[18] IRP_MJ_DEVICE_CHANGE               fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                   fffff80001b6ecd4	nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                         fffff880065d4840	parport!PptDispatchPnp

In the debugger output, you can see that parport.sys implements GsDriverEntry, the entry point for the driver. GsDriverEntry, which was generated automatically when the driver was built, performs some initialization and then calls DriverEntry, which was implemented by the driver developer.

You can also see that the parport driver (in its DriverEntry function) provides pointers to dispatch functions for these major function codes:

  • IRP_MJ_CREATE
  • IRP_MJ_CLOSE
  • IRP_MJ_READ
  • IRP_MJ_WRITE
  • IRP_MJ_QUERY_INFORMATION
  • IRP_MJ_SET_INFORMATION
  • IRP_MJ_DEVICE_CONTROL
  • IRP_MJ_INTERNAL_DEVICE_CONTROL
  • IRP_MJ_CLEANUP
  • IRP_MJ_POWER
  • IRP_MJ_SYSTEM_CONTROL
  • IRP_MJ_PNP

The remaining elements of the MajorFunction array hold pointers to the default dispatch function nt!IopInvalidDeviceRequest.

In the debugger output, you can see that the parport driver provided function pointers for Unload and AddDevice, but did not provide a function pointer for StartIo. The AddDevice function is unusual because its function pointer is not stored in the DRIVER_OBJECT structure. Instead, it is stored in the AddDevice member of an extension to the DRIVER_OBJECT structure. The following diagram illustrates the function pointers that the parport driver provided in its DriverEntry function. The function pointers provided by parport are shaded.

Diagram of function pointers in a DRIVER_OBJECT structure

Making it easier by using driver pairs

Over a period of time, as driver developers inside and outside of Microsoft gained experience with the Windows Driver Model (WDM), they realized a couple of things about dispatch functions:

  • Dispatch functions are largely boilerplate. For example, much of the code in the dispatch function for IRP_MJ_PNP is the same for all drivers. It is only a small portion of the Plug and Play (PnP) code that is specific to an individual driver that controls an individual piece of hardware.
  • Dispatch functions are complicated and difficult to get right. Implementing features like thread synchronization, IRP queuing, and IRP cancellation is challenging and requires a deep understanding of how the operating system works.

To make things easier for driver developers, Microsoft created several technology-specific driver models. At first glance, the technology-specific models seem quite different from each other, but a closer look reveals that many of them are based on this paradigm:

  • The driver is split into two pieces: one that handles the general processing and one that handles processing specific to a particular device.
  • The general piece is written by Microsoft.
  • The specific piece may be written by Microsoft or an independent hardware vendor.

Suppose that the Proseware and Contoso companies both make a toy robot that requires a WDM driver. Also suppose that Microsoft provides a General Robot Driver called GeneralRobot.sys. Proseware and Contoso can each write small drivers that handle the requirements of their specific robots. For example, Proseware could write ProsewareRobot.sys, and the pair of drivers (ProsewareRobot.sys, GeneralRobot.sys) could be combined to form a single WDM driver. Likewise, the pair of drivers (ContosoRobot.sys, GeneralRobot.sys) could combine to form a single WDM driver. In its most general form, the idea is that you can create drivers by using (specific.sys, general.sys) pairs.

Function pointers in driver pairs

In a (specific.sys, general.sys) pair, Windows loads specific.sys and calls its DriverEntry function. The DriverEntry function of specific.sys receives a pointer to a DRIVER_OBJECT structure. Normally you would expect DriverEntry to fill in several elements of the MajorFunction array with pointers to dispatch functions. Also you would expect DriverEntry to fill in the Unload member (and possibly the StartIo member) of the DRIVER_OBJECT structure and the AddDevice member of the driver object extension. However, in a driver pair model, DriverEntry does not necessarily do this. Instead the DriverEntry function of specific.sys passes the DRIVER_OBJECT structure along to an initialization function implemented by general.sys. The following code example shows how the initialization function might be called in the (ProsewareRobot.sys, GeneralRobot.sys) pair.

C++

PVOID g_ProsewareRobottCallbacks[3] = {DeviceControlCallback, PnpCallback, PowerCallback};

// DriverEntry function in ProsewareRobot.sys
NTSTATUS DriverEntry (DRIVER_OBJECT *DriverObject, PUNICODE_STRING RegistryPath)
{
   // Call the initialization function implemented by GeneralRobot.sys.
   return GeneralRobotInit(DriverObject, RegistryPath, g_ProsewareRobottCallbacks);
}

The initialization function in GeneralRobot.sys writes function pointers to the appropriate members of the DRIVER_OBJECT structure (and its extension) and the appropriate elements of the MajorFunction array. The idea is that when the I/O manager sends an IRP to the driver pair, the IRP goes first to a dispatch function implemented by GeneralRobot.sys. If GeneralRobot.sys can handle the IRP on its own, then the specific driver, ProsewareRobot.sys, does not have to be involved. If GeneralRobot.sys can handle some, but not all, of the IRP processing, it gets help from one of the callback functions implemented by ProsewareRobot.sys. GeneralRobot.sys receives pointers to the ProsewareRobot callbacks in the GeneralRobotInit call.

At some point after DriverEntry returns, a device stack gets constructed for the Proseware Robot device node. The device stack might look like this.

Diagram of the Proseware Robot device node, showing three device objects in the device stack: AfterThought.sys (Filter DO), ProsewareRobot.sys, GeneralRobot.sys (FDO), and Pci.sys (PDO)

As shown in the preceding diagram, the device stack for Proseware Robot has three device objects. The top device object is a filter device object (Filter DO) associated with the filter driver AfterThought.sys. The middle device object is a functional device object (FDO) associated with the driver pair (ProsewareRobot.sys, GeneralRobot.sys). The driver pair serves as the function driver for the device stack. The bottom device object is a physical device object (PDO) associated with Pci.sys.

Notice that the driver pair occupies only one level in the device stack and is associated with only one device object: the FDO. When GeneralRobot.sys processes an IRP, it might call ProsewareRobot.sys for assistance, but that is not the same as passing the request down the device stack. The driver pair forms a single WDM driver that is at one level in the device stack. The driver pair either completes the IRP or passes it down the device stack to the PDO, which is associated with Pci.sys.

Example of a driver pair

Suppose you have a wireless network card in your laptop computer, and by looking in Device Manager, you determine that netwlv64.sys is the driver for the network card. You can use the !drvobj debugger extension to inspect the function pointers for netwlv64.sys.

1: kd> !drvobj netwlv64 2
Driver object (fffffa8002e5f420) is for:
 \Driver\netwlv64
DriverEntry:   fffff8800482f064 netwlv64!GsDriverEntry
DriverStartIo: 00000000 
DriverUnload:  fffff8800195c5f4 ndis!ndisMUnloadEx
AddDevice:     fffff88001940d30 ndis!ndisPnPAddDevice
Dispatch routines:
[00] IRP_MJ_CREATE                      fffff880018b5530 ndis!ndisCreateIrpHandler
[01] IRP_MJ_CREATE_NAMED_PIPE           fffff88001936f00 ndis!ndisDummyIrpHandler
[02] IRP_MJ_CLOSE                       fffff880018b5870 ndis!ndisCloseIrpHandler
[03] IRP_MJ_READ                        fffff88001936f00 ndis!ndisDummyIrpHandler
[04] IRP_MJ_WRITE                       fffff88001936f00 ndis!ndisDummyIrpHandler
[05] IRP_MJ_QUERY_INFORMATION           fffff88001936f00 ndis!ndisDummyIrpHandler
[06] IRP_MJ_SET_INFORMATION             fffff88001936f00 ndis!ndisDummyIrpHandler
[07] IRP_MJ_QUERY_EA                    fffff88001936f00 ndis!ndisDummyIrpHandler
[08] IRP_MJ_SET_EA                      fffff88001936f00 ndis!ndisDummyIrpHandler
[09] IRP_MJ_FLUSH_BUFFERS               fffff88001936f00 ndis!ndisDummyIrpHandler
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    fffff88001936f00 ndis!ndisDummyIrpHandler
[0b] IRP_MJ_SET_VOLUME_INFORMATION      fffff88001936f00 ndis!ndisDummyIrpHandler
[0c] IRP_MJ_DIRECTORY_CONTROL           fffff88001936f00 ndis!ndisDummyIrpHandler
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         fffff88001936f00 ndis!ndisDummyIrpHandler
[0e] IRP_MJ_DEVICE_CONTROL              fffff8800193696c ndis!ndisDeviceControlIrpHandler
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     fffff880018f9114 ndis!ndisDeviceInternalIrpDispatch
[10] IRP_MJ_SHUTDOWN                    fffff88001936f00 ndis!ndisDummyIrpHandler
[11] IRP_MJ_LOCK_CONTROL                fffff88001936f00 ndis!ndisDummyIrpHandler
[12] IRP_MJ_CLEANUP                     fffff88001936f00 ndis!ndisDummyIrpHandler
[13] IRP_MJ_CREATE_MAILSLOT             fffff88001936f00 ndis!ndisDummyIrpHandler
[14] IRP_MJ_QUERY_SECURITY              fffff88001936f00 ndis!ndisDummyIrpHandler
[15] IRP_MJ_SET_SECURITY                fffff88001936f00 ndis!ndisDummyIrpHandler
[16] IRP_MJ_POWER                       fffff880018c35e8 ndis!ndisPowerDispatch
[17] IRP_MJ_SYSTEM_CONTROL              fffff880019392c8 ndis!ndisWMIDispatch
[18] IRP_MJ_DEVICE_CHANGE               fffff88001936f00 ndis!ndisDummyIrpHandler
[19] IRP_MJ_QUERY_QUOTA                 fffff88001936f00 ndis!ndisDummyIrpHandler
[1a] IRP_MJ_SET_QUOTA                   fffff88001936f00 ndis!ndisDummyIrpHandler
[1b] IRP_MJ_PNP                         fffff8800193e518 ndis!ndisPnPDispatch

In the debugger output, you can see that netwlv64.sys implements GsDriverEntry, the entry point for the driver. GsDriverEntry, which was automatically generated when the driver was built, performs some initialization and then calls DriverEntry, which was written by the driver developer.

In this example, netwlv64.sys implements DriverEntry, but ndis.sys implements AddDevice, Unload, and several dispatch functions. Netwlv64.sys is called an NDIS miniport driver, and ndis.sys is called the NDIS Library. Together, the two modules form an (NDIS miniport, NDIS Library) pair.

This diagram shows the device stack for the wireless network card. Notice that the driver pair (netwlv64.sys, ndis.sys) occupies only one level in the device stack and is associated with only one device object: the FDO.

Diagram of the Wireless network card device stack, showing Netwlv64.sys, ndis.sys as the driver pair associated with the FDO and Pci.sys associated with the PDO

Available driver pairs

The different technology-specific driver models use a variety of names for the specific and general pieces of a driver pair. In many cases, the specific portion of the pair has the prefix "mini." Here are some of (specific, general) pairs that are available:

  • (display miniport driver, display port driver)
  • (audio miniport driver, audio port driver)
  • (storage miniport driver, storage port driver)
  • (battery miniclass driver, battery class driver)
  • (HID minidriver, HID class driver)
  • (changer miniclass driver, changer port driver)
  • (NDIS miniport driver, NDIS library)
Note  As you can see in the list, several of the models use the term class driver for the general portion of a driver pair. This kind of class driver is different from a standalone class driver and different from a class filter driver.
 

Related topics

Concepts for all driver developers
Device nodes and device stacks
Driver stacks

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

KMDF as a generic driver pair model

In this topic, we discuss the idea that the Kernel Mode Driver Framework can be viewed as a generic driver pair model. Before you read this topic, you should understand the ideas presented in Minidrivers and driver pairs.

Over the years, Microsoft has created several technology-specific driver models that use this paradigm:

  • The driver is split into two pieces: one that handles general processing and one that handles processing that is specific to a particular device.
  • The general piece, called the Framework, is written by Microsoft.
  • The specific piece, called the KMDF driver, may be written by Microsoft or an independent hardware vendor.
Diagram of KMDF as a Generic Driver Pair

The Framework portion of the driver pair performs general tasks that are common to a wide variety of drivers. For example, the Framework can handle I/O request queues, thread synchronization, and a large portion of the power management duties.

The Framework owns the dispatch table for the KMDF driver, so when someone sends an I/O request packet (IRP) to the (KMDF driver, Framework) pair, the IRP goes to Framework. If the Framework can handle the IRP by itself, the KMDF driver is not involved. If the Framework cannot handle the IRP by itself, it gets help by calling event handlers implemented by the KMDF driver. Here are some examples of event handlers that might be implemented by a KMDF driver.

For example, a USB 2.0 host controller driver has a specific piece named usbehci.sys and a general piece named usbport.sys. Usbehci.sys, which is called the USB 2.0 Miniport driver, has code that is specific to USB 2.0 host controllers. Usbport.sys, which is called the USB Port driver, has general code that applies to both USB 2.0 and USB 1.0. The pair of drivers (usbehci.sys, usbport.sys) combine to form a single WDM driver for a USB 2.0 host controller.

The (specific, general) driver pairs have a variety of names across different device technologies. Most of the device-specific drivers have the prefix mini. The general drivers are often called port or class drivers. Here are some examples of (specific, general) pairs:

  • (display miniport driver, display port driver)
  • (USB miniport driver, USB port driver)
  • (battery miniclass driver, battery class driver)
  • (HID minidriver, HID class driver)
  • (storage miniport driver, storage port driver)

As more and more driver pair models were developed, it became difficult to keep track of all the different ways to write a driver. Each model has it's own interface for communication between the device-specific driver and the general driver. The body of knowledge required to develop drivers for one device technology (for example, Audio) can be quite different from the body of knowledge required to develop drivers for another device technology (for example, Storage).

Over time, developers realized that it would be good to have a single unified model for kernel-mode driver pairs. The Kernel Mode Driver Framework (KMDF), which was first available in Windows Vista, fulfills that need. A driver based on KMDF uses a paradigm that is similar to many of the technology-specific driver pair models.

  • The driver is split into two pieces: one that handles general processing and one that handles processing that is specific to a particular device.
  • The general piece, which is written by Microsoft, is called the Framework.
  • The specific piece, which is written by Microsoft or an independent hardware vendor, is called the KMDF driver.

The USB 3.0 host controller driver is an example of a driver based on KMDF. In this example, both drivers in the pair are written by Microsoft. The general driver is the Framework, and the device-specific driver is the USB 3.0 Host Controller Driver. This diagram illustrates the device node and device stack for a USB 3.0 host controller.

Diagram of device stack for USB 3 host controller

In the diagram, Usbxhci.sys is the USB 3.0 host controller driver. It is paired with Wdf01000.sys, which is the Framework. The (usbxhci.sys, wdf01000.sys) pair forms a single WDM driver that serves as the function driver for the USB 3.0 host controller. Notice that the driver pair occupies one level in the device stack and is represented by single device object. The single device object that represents the (usbxhci.sys, wdf01000.sys) pair is the functional device object (FDO) for the USB 3.0 host controller.

In a (KMDF driver, Framework) pair, the Framework handles tasks that are common to a wide variety of kernel-mode drivers. For example, the Framework can handle queuing of I/O requests, thread synchronization, most of the Plug and Play tasks, and most of the power management tasks. The KMDF driver handles tasks that require interaction with a specific device. The KMDF driver participates in processing requests by registering event handlers that the Framework calls as needed.

Related topics

Minidrivers and driver pairs
Kernel-Mode Driver Framework

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

KMDF extensions and driver triples

In this topic, we discuss class-based extensions to the Kernel Mode Driver Framework (KMDF). Before you read this topic, you should understand the ideas presented in Minidrivers and driver pairs and KMDF as a Generic Driver Pair Model.

For some device classes, Microsoft provides KMDF extensions that further reduce the amount of processing that must be performed by KMDF drivers. A driver that uses a class-based KMDF extension has these three pieces, which we call a driver triple.

  • The Framework, which handles tasks common to most all drivers
  • The class-based framework extension, which handles tasks that are specific to a particular class of devices
  • The KMDF driver, which handles tasks that are specific to a particular device.

The three drivers in a driver triple (KMDF driver, device-class KMDF extension, Framework) combine to form a single WDM driver.

An example of a device-class KMDF extension is SpbCx.sys, which is the KMDF extension for the Simple Peripheral Bus (SPB) device class. The SPB class includes synchronous serial buses such as I2C and SPI. A driver triple for an I2C bus controller would look like this:

  • The Framework handles general tasks that are common to most all drivers.
  • SpbCx.sys handles tasks that are specific to the SPB bus class. These are tasks that are common to all SPB busses.
  • The KMDF driver handles tasks that are specific to an I2C bus. Let's call this driver MyI2CBusDriver.sys.
KMDF Driver Triple Extension

The three drivers in the driver triple (MyI2CBusDriver.sys, SpbCx.sys, Wdf01000.sys) combine to form a single WDM driver that serves as the function driver for the I2C bus controller. Wdf01000.sys (the Framework) owns the dispatch table for this driver, so when someone sends an IRP to the driver triple, it goes to the wdf01000.sys. If the wdf01000.sys can handle the IRP by itself, SpbCx.sys and MyI2CBusDriver.sys are not involved. If wdf01000.sys cannot handle the IRP by itself, it gets help by calling an event handler in SbpCx.sys.

Here are some examples of event handlers that might be implemented by MyI2CBusDriver.sys:

  • EvtSpbControllerLock
  • EvtSpbIoRead
  • EvtSpbIoSequence

Here are some examples of event handlers that are implemented by SpbCx.sys

  • EvtIoRead

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Upper and lower edges of drivers

The sequence of drivers that participate in an I/O request is called the driver stack for the request. A driver can call into the upper edge of a lower driver in the stack. A driver can also call into the lower edge of a higher driver in the stack.

Before you read this topic, you should understand the ideas presented in Device nodes and device stacks and Driver stacks.

I/O requests are processed first by the top driver in the driver stack, then by the next lower driver, and so on until the request is fully processed.

When a driver implements a set of functions that a higher driver can call, that set of functions is called the upper edge of the driver or the upper-edge interface of the driver.

When a driver implements a set of functions that a lower driver can call, that set of functions is called the lower edge of the driver or the lower-edge interface of the driver.

Audio example

We can think of an audio miniport driver sitting below an audio port driver in a driver stack. The port driver makes calls to the miniport driver's upper edge. The miniport driver makes calls to the port driver's lower edge.

Diagram of audio port driver above miniport driver

The preceding diagram illustrates that it is sometimes useful to think of a port driver sitting above a miniport driver in a driver stack. Because I/O requests are processed first by the port driver and then by the miniport driver, it is reasonable to think of the port driver as being above the miniport driver. Keep in mind, however, that a (miniport, port) driver pair usually sits at a single level in a device stack, as shown here.

Diagram of device stack with (miniport/port) pair

Note that a device stack is not the same thing as a driver stack. For definitions of these terms, and for a discussion of how a pair of drivers can form a single WDM driver that occupies one level in a device stack, see Minidrivers and driver pairs.

Here's another way to draw a diagram of the same device node and device stack:

Diagram of device stack with port driver above miniport

In the preceding diagram, we see that the (miniport, port) pair forms a single WDM driver that is associated with a single device object (the FDO) in the device stack; that is, the (miniport, port) pair occupies only one level in the device stack. But we also see a vertical relationship between the miniport and port drivers. The port driver is shown above the miniport driver to indicate that the port driver processes I/O requests first and then calls into the miniport driver for additional processing.

The key point is that when the port driver calls into the miniport driver's upper-edge interface, that is not the same as passing an I/O request down the device stack. In a driver stack (not device stack) you can choose to draw a port driver above a miniport driver, but that does not mean that the port driver is above the miniport driver in the device stack.

NDIS example

Sometimes a driver calls the upper edge of a lower driver indirectly. For example, suppose a TCP/IP protocol driver sits above an NDIS miniport driver in a driver stack. The miniport driver implements a set of MiniportXxx functions that form the miniport driver's upper edge. We say that the TCP/IP protocol driver binds to the upper edge of the NDIS miniport driver. But the TCP/IP driver does not call the MiniportXxx functions directly. Instead, it calls functions in the NDIS library, which then call the MiniportXxx functions.

Diagram of TCP/IP and NDIS miniport stack

The preceding diagram shows a driver stack. Here's another view of the same drivers.

Diagram of device stack for a network card

The preceding diagram shows the device node for a network interface card (NIC). The device node has a position in the Plug and Play (PnP) device tree. The device node for the NIC has a device stack with three device objects. Notice that the NDIS miniport driver and the NDIS library work as a pair. The pair (MyMiniport.sys, Ndis.sys) forms a single WDM driver that is represented by the functional device object (FDO).

Also notice that the protocol driver Tcpip.sys is not part of the device stack for the NIC. In fact, Tcpip.sys is not part of the PnP device tree at all.

Summary

The terms upper edge and lower edge are used to describe the interfaces that drivers in a stack use to communicate with each other. A driver stack is not the same thing as device stack. Two drivers that are shown vertically in a driver stack might form a driver pair that sits at a single level in a device stack. Some drivers are not part of the PnP device tree.

Related topics

Concepts for all driver developers
Device nodes and device stacks
Driver stacks
Audio Devices
Network Drivers Starting with Windows Vista

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Header files in the Windows Driver Kit

The Windows Driver Kit (WDK) contains all the header files (.h files) that you need to build kernel-mode and user-mode drivers. Header files are in the Include folder in your WDK installation folder. Example: C:\Program Files (x86)\Windows Kits\10\Include.

The header files contain version information so that you can use the same set of header files regardless of which version of Windows your driver will run on.

Constants that represent Windows versions

Header files in the WDK contain conditional statements that specify programming elements that are available only in certain versions of the Windows operating system. The versioned elements include functions, enumerations, structures, and structure members.

To specify the programming elements that are available in each operating system version, the header files contain preprocessor conditionals that compare the value of NTDDI_VERSION with a set of predefined constant values that are defined in Sdkddkver.h.

Here are the predefined constant values that represent versions of the Microsoft Windows operating system.

ConstantOperating system version

NTDDI_WIN10

Windows 10

NTDDI_WINBLUE

Windows 8.1

NTDDI_WIN8

Windows 8

NTDDI_WIN7

Windows 7

NTDDI_WS08SP4

Windows Server 2008 with SP4

NTDDI_WS08SP3

Windows Server 2008 with SP3

NTDDI_WS08SP2

Windows Server 2008 with SP2

NTDDI_WS08

Windows Server 2008

 

You can see many examples of version-specific DDI elements in the WDK header files. This conditional declaration appears in Wdm.h, which is a header file that might be included by a kernel-mode driver.

#if (NTDDI_VERSION >= NTDDI_WIN7)
_Must_inspect_result_
NTKERNELAPI
NTSTATUS
KeSetTargetProcessorDpcEx (
    _Inout_ PKDPC Dpc,
    _In_ PPROCESSOR_NUMBER ProcNumber
    );
#endif

In the example you can see that the KeSetTargetProcessorDpcEx function is available only in Windows 7 and later versions of Windows.

This conditional declaration appears in Winspool.h, which is a header file that might be included by a user-mode driver.

C++
#if (NTDDI_VERSION >= NTDDI_WIN7)
...
BOOL
WINAPI
GetPrintExecutionData(
    _Out_ PRINT_EXECUTION_DATA *pData
    );

#endif // (NTDDI_VERSION >= NTDDI_WIN7)

In the example can see that the GetPrintExecutionData function is available only in Windows 7 and later versions of Windows.

Header files for the Kernel Mode Driver Framework

The WDK supports several versions of Windows, and it also supports several versions of the Kernel Mode Driver Framework (KMDF) and User Mode Driver Framework (UMDF). The versioning information in the WDK header files pertains to Windows versions, but not to KMDF or UMDF versions. Header files for different versions of KMDF and UMDF are placed in separate directories.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft
Export (0) Print
Expand All

Writing drivers for different versions of Windows

When you create a driver project, you specify the minimum target operating system, which is the minimum version of Windows that your driver will run on. For example, you could specify that Windows 7 is the minimum target operating system. In that case, your driver would run on Windows 7 and later versions of Windows.

Note  If you develop a driver for a particular minimum version of Windows and you want your driver to work on later versions of Windows, you must not use any undocumented functions, and you must not use documented functions in any way other than how it is described in the documentation. Otherwise your driver might fail to run on the later versions of Windows. Even if you have been careful to use only documented functions, you should test your driver on the new version of Windows each time one is released.
 

Writing a multiversion driver using only common features

When you design a driver that will run on multiple versions of Windows, the simplest approach is to allow the driver to use only DDI functions and structures that are common to all versions of Windows that the driver will run on. In this situation, you set the minimum target operating system to the earliest version of Windows that the driver will support.

For example, to support all versions of Windows, starting with Windows 7, you should:

  1. Design and implement the driver so that it uses only those features that are present in Windows 7.

  2. When you build your driver, specify Windows 7 as the minimum target operating system.

While this process is simple, it might restrict the driver to use only a subset of the functionality that is available on later versions of Windows.

Writing a multiversion driver that uses version-dependent features

A kernel-mode driver can dynamically determine which version of Windows it is running on and choose to use features that are available in that version. For example, a driver that must support all versions of Windows, starting with Windows 7, can determine, at run time, the version of Windows that it is running on. If the driver is running on Windows 7, it must use only the DDI functions that Windows 7 supports. However, the same driver can use additional DDI functions that are unique to Windows 8, for example, when its run-time check determines that it is running on Windows 8.

Determining the Windows version

RtlIsNtDdiVersionAvailable is a function that drivers can use to determine, at run time, if the features that are provided by a particular version of Windows are available. The prototype for this function is as follows:

BOOLEAN RtlIsNtDdiVersionAvailable(IN ULONG Version)

In this prototype, Version is a value that indicates the required version of the Windows DDI. This value must be one of the DDI version constants, defined in sdkddkver.h, such as NTDDI_WIN8 or NTDDI_WIN7.

RtlIsNtDdiVersionAvailable returns TRUE when the caller is running on a version of Windows that is the same as, or later than, the one that is specified by Version.

Your driver can also check for a specific service pack by calling the RtlIsServicePackVersionInstalled function. The prototype for this function is as follows:

BOOLEAN RtlIsServicePackVersionInstalled(IN ULONG Version)

In this prototype, Version is a value that indicates the required Windows version and service pack. This value must be one of the DDI version constants, defined in sdkddkver.h, such as NTDDI_WS08SP3.

Note that RtlIsServicePackVersionInstalled returns TRUE only when the operating system version exactly matches the specified version. Thus, a call to RtlIsServicePackVersionInstalled with Version set to NTDDI_WS08SP3 will fail if the driver is not running on Windows Server 2008 with SP4.

Conditionally calling Windows version-dependent functions

After a driver determines that a specific operating system version is available on the computer, the driver can use the MmGetSystemRoutineAddress function to dynamically locate the routine and call it through a pointer. This function is available in Windows 7 and later operating system versions.

Note  To help preserve type checking and prevent unintentional errors, you should create a typedef that mirrors the original function type.
 

Example: Determining the Windows version and conditionally calling a version-dependent function

This code example, which is from a driver's header file, defines the PAISQSL type as a pointer to the KeAcquireInStackQueuedSpinLock function. The example then declares a AcquireInStackQueuedSpinLock variable with this type.

...
 //
// Pointer to the ordered spin lock function.
// This function is only available on Windows 7 and
// later systems
 typedef (* PAISQSL) (KeAcquireInStackQueuedSpinLock);
PAISQSL AcquireInStackQueued = NULL;
 ...
 

This code example, which is from the driver's initialization code, determines whether the driver is running on Windows 7 or a later operating system. If it is, the code retrieves a pointer to KeAcquireInStackQueuedSpinLock.

...
 
//
// Are we running on Windows 7 or later?
//
 if (RtlIsNtDdiVersionAvailable(NTDDI_WIN7) ) {
 
 //
  // Yes... Windows 7 or later it is!
  //
     RtlInitUnicodeString(&funcName,
                  L"KeAcquireInStackQueuedSpinLock");
 
 //
  // Get a pointer to Windows implementation
  // of KeAcquireInStackQueuedSpinLock into our
  // variable "AcquireInStackQueued"
     AcquireInStackQueued = (PAISQSL)
                  MmGetSystemRoutineAddress(&funcName);
 }
 
...
// Acquire a spin lock.
 
 if( NULL != AcquireInStackQueued) {
  (AcquireInStackQueued)(&SpinLock, &lockHandle);
} else {
    KeAcquireSpinLock(&SpinLock);
}
 

In the example the driver calls RtlIsNtDdiVersionAvailable to determine whether the driver is running on Windows 7 or later. If the version is Windows 7 or later, the driver calls MmGetSystemRoutineAddress to get a pointer to the KeAcquireInStackQueuedSpinLock function and stores this pointer in the variable named AcquireInStackQueued (which was declared as a PAISQSL type).

Later, when the driver must acquire a spin lock, it checks to see whether it has received a pointer to the KeAcquireInStackQueuedSpinLock function. If the driver has received this pointer, the driver uses the pointer to call KeAcquireInStackQueuedSpinLock. If the pointer to KeAcquireInStackQueuedSpinLock is null, the driver uses KeAcquireSpinLock to acquire the spin lock.

 

 

Send comments about this topic to Microsoft

© 2016 Microsoft