Managing driver paging

Written by

in

After talking so much about virtual memory and paging, I received a question that coincidentally has everything to do with the subject of the last posts. “What are the pragma alloc_text that we see in the WDK examples for?” (Thiago Cardoso, Recife-PE). This question must have already crossed the minds of many who have taken a look at the WDK examples. Since all the WDK examples I know use this pragma, it actually took a while for someone to ask about it. But anyway, let us get to what matters.

As we have already seen, memory pages can be either in the RAM chips or on disk. We have also already seen that threads that are at a high execution priority cannot access data that is pageable. But how would we know which data is pageable or not?

Controlling Data Paging

When we allocate memory dynamically, we can choose whether the area of memory to be allocated will be pageable or not. The ExAllocatePool function, and its sisters (ExAllocatePoolWithTag, ExAllocatePoolWithQuota, ExAllocatePoolWithQuotaTag and ExAllocatePoolWithTagPriority), receive a parameter of type POOL_TYPE that defines whether the memory to be allocated will be pageable or not.

typedef enum _POOL_TYPE {
  NonPagedPool,
  PagedPool,
  NonPagedPoolMustSucceed,
  DontUseThisType,
  NonPagedPoolCacheAligned,
  PagedPoolCacheAligned,
  NonPagedPoolCacheAlignedMustS
} POOL_TYPE;
 
 
PVOID 
  ExAllocatePool(
    IN POOL_TYPE  PoolType,
    IN SIZE_T  NumberOfBytes
    );

You will have to manage which allocations will be accessed at different execution priorities. For example: A certain linked list is consulted only by functions that run at low IRQL, so all of its elements can be allocated in pageable memory (PagedPool). On the other hand, a linked list that is consulted by functions that run at high IRQLs must have its elements allocated in non-pageable memory (NonPagedPool).

Nice, Fernando, but not everything is allocated dynamically. What about static variables?

By default, all global variables are non-pageable. This is bad if your driver has many global variables, which would require more non-pageable memory for your driver to be loaded, and, as has also already been seen, non-pageable memory should be conserved. Fortunately, we can define that a set of global variables can be pageable, as long as they are only accessed by threads at low IRQL.

Within a module, be it an application, a DLL or even a driver, memory paging control is applied to the sections that compose them. For a group of variables to be defined in a pageable section, we can use the data_seg pragma. However, not every compiler allows us to do this. To know whether the compiler we are using supports the use of this pragma, we count on the WDK headers, which define the ALLOC_DATA_PRAGMA symbol when the compiler offers support for this feature. This, obviously, has become less significant, since the advisable thing is to use the WDK’s own compiler, but it does not hurt to anticipate that your code might be compiled by some other compiler. See the example below that defines both non-pageable and pageable variables.

//-f--> These are variables defined in a non-pageable section
PDEVICE_OBJECT  g_pControlDeviceObj;
PDRIVER_OBJECT  g_pDriverObj;
 
//-f--> Here I check whether the compiler I am using
//      supports the use of #pragma data_seg.
//      If so, I open the PAGEDATA section, which is a
//      pageable data section.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg("PAGEDATA")
#endif
 
//-f--> All the variables declared here will be pageable.
//      So, only threads that run at a low
//      priority level will be able to access these variables
 
//-f--> We define a giant buffer. Good thing it is pageable
UCHAR   g_Buffer[100000];
 
//-f--> Here we mark the end of the pageable section. The variables
//      defined after this #pragma will be non-pageable.
#ifdef ALLOC_DATA_PRAGMA
    #pragma data_seg()
#endif

Controlling Code Paging

Memory is memory, whether to store data or code. We can also control where the functions you write will be defined. So, we can put all the functions that execute at low priority in pageable code sections. Here we will use the pragma that gave rise to Thiago’s question, the alloc_text pragma.

This pragma has two limitations. The first of them is that the pragma must be applied after the declaration of the function, but before its definition. The other is that this pragma is not applicable to C++ functions, that is, class methods or functions with overloads will not have this luxury of being pageable. If you are like me, who prefers to use the strong typing of C++ in drivers, even if you only write simple functions, you must use the extern “C” modifier in the function declarations.

This pragma is not necessarily supported by all compilers, and just like data_seg, the WDK headers define the ALLOC_PRAGMA symbol to signal that the compiler used supports this feature. Here is one more example.

//-f--> Normally this declaration is made in a header
//      file. Note that if we are compiling in C++,
//      we will have to use extern "C" to be able to define the
//      section where the functions declared here will be defined.
#ifdef __cplusplus
extern "C"
{
#endif
    //-f--> Declares one
    ULONG SumOne(IN ULONG ulParam);
 
    //-f--> Declares another
    ULONG SumTwo(IN ULONG ulParam);
 
#ifdef __cplusplus
}
#endif
 
 
//-f--> The following part goes in the same module where the function is
//      defined, and must come before the function's definition.
//      Note that here we test whether this pragma is supported, and
//      if so, each function must receive its alloc_text pragma
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif
 
 
//-f--> After that, we can define the functions normally
 
ULONG SumOne(IN ULONG ulParam);
{
    //-f--> SumOne function that was written by someone.
 
    //-f--> This comment is only funny in English.
    //
    //      Function SumOne that has been written by someone
 
    PAGED_CODE();
 
    return ulParam + 1;
}
 
ULONG SumTwo(IN ULONG ulParam);
{
    //-f--> Damn function. (which is not funny at all)
    PAGED_CODE();
 
    return SumOne(SomeOne(ulParam));
}

Fernando, what are these PAGED_CODE macros that you used in the example for?

Well, here comes the little story. An uncomfortable thing is that problems with memory paging will only occur when the page you are accessing is on disk. This means that you can write a driver with a problem, test it, and if by “luck” the pages are all in RAM during the test, you will not see any problem. One of the things that helps a lot is this PAGED_CODE macro. In Checked Build, this macro is translated into a function that will check whether the current priority is low enough to execute pageable code. If it is not, a breakpoint exception will be thrown. I hope you have the debugger attached to see that happen. Otherwise, do not worry, a lovely blue screen will appear and you will end up connecting the debugger sooner or later. When compiled in Free Build, this macro is translated into nothing, avoiding a performance loss. In conclusion, this prevents you from calling a pageable function at high IRQL and everything working by “luck”.

...
 
#elif DBG
 
#define PAGED_CODE() {                                                       \
    if (KeGetCurrentIrql() > APC_LEVEL) {                                    \
        KdPrint(("EX: Pageable code called at IRQL %d\n", KeGetCurrentIrql())); \
        NT_ASSERT(FALSE);                                                    \
    }                                                                        \
}
 
...
 
#else
 
#define PAGED_CODE()        NOP_FUNCTION;
 
...
 
#endif

An excellent way to catch code paging problems is to use the Driver Verifier with the Force IRQL Checking option enabled. This option, besides checking whether you are calling the API functions at the correct priorities, also forces the paging of everything that is pageable in your driver every time the IRQL rises to DISPATCH_LEVEL or higher. This puts an end to that “luck” of using a pageable resource at high IRQL when the resource is already in RAM.

Discardable Section

Besides the PAGE pageable code section we saw, another section is also seen very frequently in the WDK examples. The INIT section is discarded when the call to your driver’s DriverEntry function returns to the system, and, if it is the case for your driver, after any reinitialization function has finished. If you do not know what a reinitialization function is, then take a pass through this post. So, if you have functions that are only used during the initialization of your driver (which in this case means: functions called by DriverEntry, or functions called by functions that were called by DriverEntry, or even functions called by functions that were called by functions… Ah! I think you got it), you can use the same procedure to define them in the INIT section in the same way as was done earlier. But it does not hurt to leave an example.

//-f--> The functions declared here will be removed from RAM when
//      the call to the DriverEntry function returns. Do not try to call them
//      after that, because they have already gone to the heaven of
//      initialization functions.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(INIT, DriverEntry);
    #pragma alloc_text(INIT, FunctionCalledByDriverEntry);
    #pragma alloc_text(INIT, AnotherFunctionCalledByDriverEntry);
 
    #pragma alloc_text(PAGE, SumOne);
    #pragma alloc_text(PAGE, SumTwo);
#endif

This is especially useful in Legacy Drivers, which perform many steps during initialization. Legacy drivers, besides searching for the hardware they are going to control, also need to do the association of the available resources (ports, interrupts, DMA channels, etc.), and this ends up consuming a significant amount of code. On the other hand, WDM drivers receive everything all chewed up from the Plug-and-Play Manager. The hardware was detected and the resource association was already negotiated. A beautiful thing of God!

Pageable or non-pageable, that is the question

Supposing that you have many functions that run at high IRQL, and that therefore must be in non-pageable memory, this would cause a large amount of non-pageable memory to be used to keep such functions, even if nobody is using the driver. We can also define our own sections and thus make them pageable or non-pageable when it is convenient for us. This would allow all those non-pageable functions to be pageable while nobody obtains a reference to our driver. This way, when we receive an IRP_MJ_CREATE or when we program the hardware to fire interrupts, we can tell the system that now we will need to make the section where such functions were defined non-pageable.

First of all, we will have to create our own custom sections, and we will do this using the alloc_text pragma to define sections that must have their name in the format PAGExxxx, where xxxx is a unique name in your driver. See the little example, just to ease the conscience.

//-f--> The functions declared here will be defined in a section
//      that can be non-pageable when it is convenient for us.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGEABCD, FunctionOne);
    #pragma alloc_text(PAGEABCD, FunctionTwo);
#endif
 
 
//-f--> The functions declared here will be defined in another
//      section that can be non-pageable. This way we can define
//      different groups of functions in different sections.
 
#ifdef ALLOC_PRAGMA
    #pragma alloc_text(PAGE1234, FunctionThree);
    #pragma alloc_text(PAGE1234, FunctionFour);
#endif

Now that we have defined which functions will be defined in that section, we can make it non-pageable only when such functions are used. For that we must use the MmLockPagableCodeSection function.

PVOID 
  MmLockPagableCodeSection(
    IN PVOID  AddressWithinSection
    );

To identify which section will be marked as non-pageable, we will have to pass an address that is inside the section. A name of a function defined in the section already solves the problem, but remember that all the functions inside the same section will be marked as non-pageable.

The MmLockPagableCodeSection function returns to us an opaque address that can be used as a parameter for the call to the MmUnlockPagableImageSection function, which marks the section as pageable again. This function is normally called before the driver is unloaded. Following the line of our example, we could call this function upon receiving an IRP_MJ_CLOSE. The same address returned by MmLockPagableCodeSection can be used as a parameter for the calls to the MmLockPagableSectionByHandle function to make a section non-pageable again, which is much faster than the call to MmLockPagableCodeSection. So, we must call MmLockPagableCodeSection at least once to obtain the opaque pointer, and after that, we can call MmLockPagableSectionByHandle and MmUnlockPagableImageSection.

VOID 
  MmLockPagableSectionByHandle(
    IN PVOID  ImageSectionHandle
    );
 
VOID 
  MmUnlockPagableImageSection(
    IN PVOID  ImageSectionHandle
    );

The same can be done in custom sections that define data, but we must use the MmLockPagableDataSection function to obtain the opaque pointer that identifies the section.

Can my function be pageable?

Fernando, if my function is called at PASSIVE_LEVEL, then can it be pageable?

It is not quite like that. Your function, even being called at PASSIVE_LEVEL, may contain intervals of code that need to be non-pageable. If you call functions that raise the IRQL, such as KeAcquireSpinLock, your function cannot be defined in a pageable section.

But Fernando, follow my reasoning. If the function was called and is executing at the moment, is it not obvious that the page it is contained in is in RAM?

You may not believe it, but a function is composed of a chain of bytes that may be on page boundaries. This means that the beginning of your function may be at the end of a page, which in fact was paged to RAM when the call was made, but we do not know where a new page may begin. This new page may be on disk, and if it is accessed at high IRQL, the paging will cause a blue screen. Look at the code below to get an idea of what I am talking about, and do not forget to read the comments.

/****
***     FunctionCalledAtPassiveLevel
**
**      Routine that is called at PASSIVE_LEVEL,
**      but has an IRQL elevation during the call.
*/
 
PLIST_ENTRY
FunctionCalledAtPassiveLevel(VOID)
{
    KIRQL       Irql;
    PLIST_ENTRY pEntry = NULL;
 
    PAGED_CODE();
 
    //-f--> Here our IRQL goes sky-high; if the code that
    //      comes after this call is on the next memory page
    //      that happens to be on disk, then (BOOM !!!)
    KeAquireSpinLock(&g_SpinLock, &Irql);
 
 
    //-f-->    -------======= Page boundary =======-------
 
 
    //-f--> The code here runs at DISPATCH_LEVEL, which
    //      prevents this function from being defined in a
    //      pageable section.
 
    if (!IsListEmpty(&g_NonPagedList))
    {
        pEntry = RemoveHeadList(&g_NonPagedList);
    }
 
    //-f--> It is fine for you to access only non-pageable data,
    //      but the code you use for such access also needs
    //      to be in non-pageable memory. After all, this code is
    //      running at high IRQL and retrieving a page of
    //      code from disk would result in horrible things.
 
    //-f--> We are back at PASSIVE_LEVEL
    KeReleaseSpinLock(Irql);
 
    return pEntry;
}

If you have functions that are large, but that contain occasional intervals with IRQL elevation, then separate such intervals into isolated functions that can be defined in non-pageable sections, thus allowing you to define your large and complex function in a pageable section.

Phew! I could still write a few more comments on this subject, but if I am already tired of writing, I can imagine how you are. The subject is dealt with in full detail in the reference. But if you have any doubt about it, just send me an e-mail and hope that I know how to answer.

See you!

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *