OSRLogo
OSRLogoOSRLogoOSRLogo x OSR Custom Development Services
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Wed, 13 Dec 2017     115635 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
Downloads
ListServer / Forum
Driver Jobs
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

Global Relief Effort - C++ Runtime Support for the NT DDK

 Click Here to Download: Code Associated With This Article Zip Archive, 70KB

Introduction

 

If you are addicted to reading the usenet newsgroup for NT kernel programming, you may have noticed that there is an interminable intermittent debate about the evil nature of C++ code inside the Windows NT operating system. On one side you have those who dare not speak the name of C++ inside the kernel lest the gods awaken, on the other side you have fairly rational people who wonder why this is even an issue.

 

While the arguments against using C++ are basically emotional, there are a few minor technical problems that present valid obstacles that have to be addressed:

 

1.        1.        There is no kernel C++ runtime library consequently there is no global new, no global delete, no support for C++ exceptions, and no support for initialization of global class objects.

2.      2.        See (1).

 

This may seem like bad news, and indeed it is. The good news is that supporting new and delete is trivial. The better news is that supporting global class objects is not difficult at all. This article examines the gory details of implementing a usable Kernel C++ Runtime Library.

 

Supporting New and Delete

 

You can probably live without using global class objects in your driver. However, if you do intend to use C++ and you do intend to define classes that can be allocated out of heap, then having a new and delete operator available is a basic requirement.

 

The function prototypes for the two standard global new operators supported by VisualC are as follows:

 

void * __cdecl operator new(size_t size);

 

void * __cdecl operator new(size_t size, void *location);

 

The __cdecl part is Microsoft compiler specific syntax, otherwise the declarations of these functions are standard. (Note without the __cdecl declaration the linker will not resolve explicit or implicit references to operator new.)

 

The first version is the normal new operator used for heap allocation, as in:

 

Foo * foo = new Foo(blortz);

 

Implementation is very simple:

 

void * __cdecl operator new(size_t size)

{

    return malloc(size, 'ppcO', NonPagedPool);

}

 

Our new operator simple defers to a standard malloc() routine, well not quite standard as it appears to have three arguments rather than the expected one, and it looks suspiciously like it is going to call one of the standard DDK allocation functions. We are forcing allocation into NonPagedPool() for all uses of global new.  This behavior can easily be modified by overloading the new operator with one of your own that allocates from a specified pool, or by providing a class new operator for class specific allocation.

The second new operator is the standard placement version. It is invoked as

 

PUCHAR buffer[ABIGENUFFSIZE];

 

Foo * foo = new(buffer) Foo(blortz);

 

The implementation adds the new parameter after the standard size parameter:

 

void * __cdecl operator new(size_t size,  void *location)

{

    return location;

}

 

This variation allows an object to be created in a preallocated location (within a buffer for example.) You should of course not call delete on the returned buffer. (You can define a placement delete operator that corresponds to the placement new.)

 

Oh yes, we should of course define the behavior of malloc():

 

PVOID __cdecl malloc(ULONG x,  ULONG id= 'ppcO', POOL_TYPE pool = NonPagedPool);

 

PVOID __cdecl malloc(ULONG x,  ULONG id, POOL_TYPE pool)

{

    PVOID buffer = ExAllocatePoolWithTag(pool, x, id);

 

    if (buffer) {

        //

        // Always clear our buffers to zero!

        //

        RtlZeroMemory(buffer, x);

    }

 

    return buffer;

}

 

The use of C++ default arguments in our malloc() allows for the implementation of the standard Unix C library malloc(size_t) with full support for the NT DDK ExAllocatePool() functions, and represents some of the power and elegance of the C++ compiler at its best.

 

Hint: TAG ALL MEMORY ALLOCATIONS WITH UNIQUE PER-OBJECT TAGS.

 

While the allocation mechanisms described above will supply a default allocation tag (Ocpp) you really should overload each class you define with its own unique tag. To be complete, we also support a tag and pool specific placement new. Note that this placement new uses the kernel heap allocator, consequently you should of course use delete on the returned value. Strictly speaking we should define a corresponding placement delete operator to go with it.

 

void *__cdecl operator new( size_t size, ULONG tag, POOL_TYPE pool= NonPagedPool);

 

The requirement for this non-standard variation on new is that you must specify a tag value (we insist) but you get a default value for POOL_TYPE.

 

As usual the implementation is trivial:

 

void *__cdecl operator new( size_t size, ULONG tag, POOL_TYPE pool)

{

    return malloc(size, tag, pool);

}

 

Note that there is no attempt here to provide any sort of exception processing for allocation failures.

 

Supporting delete is even easier than supporting new.  A single version exists:

 

void __cdecl operator delete(void * pVoid)

{

    free(pVoid);

}

 

Of course if we are going to support malloc() then we have to support free().

 

void __cdecl free(PVOID x)

{

    if (x != NULL) {

 

        ExFreePool(x);

    }

}

 

Note that conformance with C++ standards requires that delete NULL works. Consequently the implementation of free must test for a NULL pointer value as ExFreePool()will perform some unspecified behavior if handed a NULL pointer.

 

Finally, the simplicity of the allocator implementation shown above strongly suggests that all of these functions should be offered as inline functions.

 

Supporting Global Objects

Now for the tricky bit.

 

New and delete are easy, and if you are already using C++ in your driver, then you have undoubtedly implemented functions similar to the ones outlined above. However there remains yet to be addressed the problem that there is no mechanism in the kernel to call the constructors and destructors of class objects with global (external) scope. These objects are either declared outside any function, declared as static inside a function, or have a static constructor.

 

Up (or down, depending on how you view the world) in user space, the VisualC Compiler and library support global class objects. We are rather convinced that this support is not magic, and that the compiler has no clue if the code it is compiling is a user space DLL or a kernel DLL. The compiler is going to emit the same code regardless of the intended use of the object files produced by the compiler. The linker might know the difference between a kernel DLL (as in a driver) and a user space DLL, but the compiler simply translates source code into object files. The point being that whatever support there is for global objects is 1) partially built into the compiler and 2) partially supported by the C++ runtime libraries that come with the compiler.

 

One of the nice things about VisualC is that it comes with the source code for both MFC and the entire set of C/C++ runtime (CRT) libraries. (This is not the same as Open Source. See the section Open Source? Open Sores? regarding our limitations on providing a kernel C++ runtime library.) So really all that is required is to understand, by reading the CRT source code, how the compiler and the CRT libraries conspire to support global constructors.

 

The goals are as follows: for each class object at global scope, the compiler and the runtime library must call the constructor of the class object before the entry point function of the linkage unit is called. (So for an executable, this is before main() or its equivalent. For a DLL this is before DllMain() or its equivalent. For a kernel driver, this would be before DriverEntry()). The order that global object constructors are called in is determined by the order of declaration within compilation units and by the order in which object files are linked together. (Note that this means that interdependencies between constructors at global scope are a huge problem and should be avoided altogether). The order of construction is the inverse of the order of destruction. The last constructed object is the first object to have its destructor called on termination of the linkage unit.

 

The VisualC compiler and the CRT libraries have an agreement between themselves that provides support for C++ global objects. The compiler is going to emit sniglets of code, some rather oddly named functions, that always have as a prototype:

 

void Function(void); // (a void function with no parameters.)

 

There is a standard CRT data type defined as a pointer to this sort of function:

 

typedef (__cdecl *PVFV) (void) ;

 

 A pointer to each of these PVFV functions generated by the compiler is going to be forced into a specific named linkage data segment by the compiler, effectively building an array of PVFV pointers. The CRT libraries, understanding what the compiler is doing, have built an empty array of PVFV pointers at the start of the named linkage data segment. By knowing the size of the named segment, the CRT initialization routines can walk this array, calling each pointer in turn.

 

The functions generated by the compiler are all PVFV functions. Within each of these generated functions is the correct information to call a specific instance of a class object constructor.  If we have a very simple class:

 

class global  {

 

public:

    global(int x);

    virtual ~global();

 

    int getX();

 

private:

    int m_x;

};

 

global::global(int x)

{

    m_x = x;

}

 

And if we declare a global instance of class global, such as:

 

global one(1);

 

The compiler is going to generate a PVFV function something like:

 

void E1(void)

{

     (theAddressOfglobal’sglobal)(&one, 1);

}

 

And the compiler is going to create a pointer to E1 in a well-known named data segment.

 

Who is this well-known named data segment? It is of course: .CRT$XCA. All CRT libraries contain a data segment named .CRT$XCA, and very near (and dear) to .CRT$XCA is another data segment named .CRT$XCZ. You can verify this by creating a map file for a DLL or an executable. For example we built a truly simple user (win32) DLL and it has the following linkage segments:

 

testcprt

 

 Timestamp is 371228a2 (Mon Apr 12 13:08:50 1999)

 

 Preferred load address is 00400000

 

 Start         Length     Name                   Class

 0001:00000000 0000e468H .text                   CODE

 0001:0000e468 00010008H .textbss                CODE

 0002:00000000 000013a9H .rdata                  DATA

 0002:000013a9 00000000H .edata                  DATA

 0003:00000000 00000104H .CRT$XCA                DATA

 0003:00000104 00000104H .CRT$XCU                DATA

 0003:00000208 00000104H .CRT$XCZ                DATA

 0003:0000030c 00000104H .CRT$XIA                DATA

 0003:00000410 0000010eH .CRT$XIC                DATA

 0003:00000520 00000104H .CRT$XIZ                DATA

 0003:00000624 00000104H .CRT$XPA                DATA

 0003:00000728 00000104H .CRT$XPX                DATA

 0003:0000082c 00000104H .CRT$XPZ                DATA

 0003:00000930 00000104H .CRT$XTA                DATA

 0003:00000a34 00000104H .CRT$XTZ                DATA

 0003:00000b40 00000b89H .data                   DATA

 0003:000016cc 0000197cH .bss                    DATA

 0004:00000000 00000014H .idata$2                DATA

 0004:00000014 00000014H .idata$3                DATA

 0004:00000028 00000110H .idata$4                DATA

 0004:00000138 00000110H .idata$5                DATA

 0004:00000248 000004afH .idata$6                DATA

 

Looking at the CRT runtime library source files, one can see that each version of the CRT libraries allocates an empty PVFV array in data segments .CRT$XCA and .CRT$XCZ. The compiler, as we stated earlier, conspires to allocate additional PVFV objects in .CRT$XCA, one for each global constructor that needs to be called.

 

To make this a little more explicit, the kernel runtime libraries need to declare some data segments and allocate storage within those data segments as follows:

 

#pragma data_seg(".CRT$XCA")

PVFV __crtXca[] = { NULL }; // c++

 

 

#pragma data_seg(".CRT$XCZ")

PVFV __crtXcz[] = { NULL };

 

#pragma data_seg()  // return control to the normal ddk linkage process

 

//

// this may or may not be required

//

#pragma comment(linker, "/merge:.CRT=.data")

 

Having done that there are only a few minor details to sort out.

 

First our kernel CRT library needs to be the driver DLL entry point. So we have to replace your DriverEntry() with our own. The include file provides a simple macro to accomplish this incredible feat of software engineering:

 

#define CPP_DRIVER_ENTRY(Do, Rp) extern "C" NTSTATUS Cp_DriverEntry(Do, Rp)

 

All you need to do is replace your DriverEntry() with this macro. So your DriverEntry() routine might look as follows:

 

CPP_DRIVER_ENTRY(PDRIVER_OBJECT DriverObject,

                 PUNICODE_STRING RegistryPath)

{

    DriverObject->DriverUnload = testUnload;

 

   

    return STATUS_SUCCESS;

 

}

 

Our library will of course provide a function actually named DriverEntry(), and will call your function (real name is Cp_DriverEntry,) after setting things up correctly.

 

What does our DriverEntry() do? Basically it just calls each function in the array of functions at __crtXca. It figures out how big __crtXca is by using __crtXcz as a terminator.

 

Things get a little murkier now. We lied. Up above we provided pseudo code for what the compiler might be emitting as these mysterious generated PVFV functions. In addition to calling the constructor, these generated functions also call another defined CRT function: atexit().

 

In addition to calling constructors, a CRT library must also arrange to call the destructors for global objects on termination. Registering a PVFV (what else,) function to be called on termination does this. Of course the compiler is once again generating these functions to invoke your destructors with the proper parameter values.

 

What does atexit() do? It takes as input a PVFV function pointer. It then allocates storage for the pointer and adds the pointer to a LIFO list of terminator functions. When the linkage unit in question terminates, the LIFO list is run, calling through each stored function pointer in turn.

 

But what the heck does “linkage unit terminate” mean for a kernel driver? Our interpretation is that this is what happens when your DriverObject() DriverUnload() vector gets invoked. So our kernel CRT library, in its own DriverEntry() routine, simply steals your DriverUnload() vector. If you set DriverObject->DriverUnload, then, if your driver is unloaded, our unload routine gets called. Our unload routine calls your unload routine, and then it unwinds the list of termination functions setup by atexit().

 

Note that atexit() is a defined external entry point in our cpp runtime library. You can register your own termination functions by calling this routine.

 

Note further that no attempt is made to cleanup on system shutdown. It would be easy to provide a callable interface to run the atexit() created termination list. At the moment our kernel C++ runtime library does not export the termination interface function (doexit()) but this could easily be fixed.

 

Let's Go for a Test Drive

 

 Don’t take our word for it. Download our kernel CRT library and try it yourself.  Here is a simple (very simple) device driver that demonstrates the functionality present in the OSR kernel C++ Runtime Library.

 

First, the source code:

 

#include "osrcpp.h"

 

//

// define a very simple class hierarchy

//

enum baseType { BIRD, PLANE, SUPERMAN, LLAMA };

 

class base {

 

public:

    base();

    virtual ~base();

    virtual baseType isa()=0;

};

 

//

// global derives from base.

//

class global : public base {

 

public:

    global(int x);

    global();

    virtual ~global();

 

    int getX();

    virtual baseType isa();

 

private:

    int m_x;

};

 

//

// declare the implementation code for these classes

//

base::base()

{

#if DBG

    DbgPrint("base: this %x\n", this);

#endif

}

 

base::~base()

{

#if DBG

    DbgPrint("~base: this %x\n", this);

#endif

}

 

global::global(int x)

{

#if DBG

    DbgPrint("global( %x): this %x\n", x, this);

#endif   

    m_x = x;

}

 

global::global()

{

#if DBG

    DbgPrint("global: this %x\n", this);

#endif   

    m_x = 0;

}

 

global::~global()

{

#if DBG

    DbgPrint("~global: this %x\n", this);

#endif   

    m_x = 0;

}

 

int global::getX()

{

#if DBG

    DbgPrint("global::getX m_x %x this %x\n", m_x, this);

#endif   

    return m_x;

}

 

baseType global::isa()

{

#if DBG

    DbgPrint("global::isa this %x\n", this);

#endif   

    return BIRD;

}

 

//

// define some global data for our driver

//

 

global one(2);

 

global two;

 

global other(3);

 

//

// define the prototype for our driver unload routine

//

extern "C" void

testUnload(PDRIVER_OBJECT DriverObject);

 

//

// finally the driver entry routine.

//

// Use the CPP_DRIVER_ENTRY macro to construct the

// linkage between osrcpp and your driver.

//

CPP_DRIVER_ENTRY(PDRIVER_OBJECT DriverObject,

                 PUNICODE_STRING RegistryPath)

{

#if DBG

    DbgPrint("CPP_DRIVER_ENTRY\n");

#endif

 

    DriverObject->DriverUnload = testUnload;

 

    //

    // test that our global objects are initialized

    //

    if (one.getX() != 2) {

 

        return STATUS_UNSUCCESSFUL;

 

    }

 

     if (two.getX() != 0) {

 

        return STATUS_UNSUCCESSFUL;

 

    }

   

    // allocate a new object.

  

    global * three = new('vrdT') global(4); // this is not global!

 

    if (!three) {

        //

        // allocation failure!

        //

 

        return STATUS_UNSUCCESSFUL;

    }

 

    if (other.getX() != 3) {

 

        return STATUS_UNSUCCESSFUL;

 

    }

 

    // declare a local object

 

    global four(4444);

 

    if (four.getX() != 4444) {

 

        delete three;

 

        return STATUS_UNSUCCESSFUL;

 

    }

 

    // our allocated object has to be explicitly deleted!

 

    delete three;

 

    return STATUS_SUCCESS;

 

}

 

//

// and of course we need an unload routine to test delete!

//

void

testUnload(PDRIVER_OBJECT DriverObject)

{

#if DBG

    DbgPrint("testUnload\n");

#endif

 

    return;

}

 

 

On startup, our test driver traces the following information on the debug console:

 

Module Load: TESTDRV.SYS  (symbol loading deferred)

file e:\ddk\src\osr\cpprun\cpplib\cpprun.cpp, line 96 DriverEntry

file e:\ddk\src\osr\cpprun\cpplib\cpprun.cpp, line 172 _initterm

base: this f85b6ec0

global( 2): this f85b6ec0

base: this f85b6ec8

global: this f85b6ec8

base: this f85b6eb8

global( 3): this f85b6eb8

CPP_DRIVER_ENTRY

global::getX m_x 2 this f85b6ec0

global::getX m_x 0 this f85b6ec8

base: this fd830888

global( 4): this fd830888

global::getX m_x 3 this f85b6eb8

base: this f842fe70

global( 115c): this f842fe70

global::getX m_x 115c this f842fe70

~global: this fd830888

~base: this fd830888

~global: this f842fe70

~base: this f842fe70

 

And on unload we can see the following:

 

file e:\ddk\src\osr\cpprun\cpplib\cpprun.cpp, line 131 _CppDriverUnload

testUnload

~global: this f85b6eb8

~base: this f85b6eb8

~global: this f85b6ec8

~base: this f85b6ec8

~global: this f85b6ec0

~base: this f85b6ec0

Module Unload: TESTDRV.SYS

 

Open Sources?  Open Sores?

 

We’d like to make the source for our kernel C++ runtime library available to anyone under the GPL (copyleft). Unfortunately portions are certainly derived from Microsoft’s CRT source and include files, in particular the definition of the linkage based segment data structures has to be exactly as defined by the CRT libraries or it simply isn’t going to work with the VisualC compiler. The CRT software is all protected by a standard copyright, and while our VisualC license allows us to distribute derived binaries, it prohibits the distribution of directly derived source code.

 

So we are a bit stuck. The runtime library binary object file and the global include file can be downloaded from our web site. The source code, other than the descriptions in this article, cannot be made public. We are sure that there are lots of clever people out there who could fix/enhance/re-write what we’ve done, but until further notice, they will have to do so by sending us email suggestions.

 

To Do List

 

This library is not complete.

 

No support is provided for C++ exception handling. Consequently the STL is unusable as it relies heavily on exception processing. As Microsoft specific Structured Exception Handling (SEH) and C++ exception handling are somewhat incompatible, support in this area will take a bit more work than was involved in the current runtime library.

 

There are a lot of C++ hooks, such as _set_new_handler that are not implemented.

Caveat Emptor: You gets what you paid for

Download osrcpp.lib and osrcpp.h from our website and give it a try. If you have problems, send us a bug report. However, this software is provided as-is with no warranty of suitability for any purpose whatsoever. It probably isn’t even safe to use the printed image of the include file as kitty litter. 

 

As the banner on osrcpp.h says

 

“This sofware is supplied for demonstration purposes only.

 

OSR Open Systems Resources, Inc. (OSR) expressly disclaims any warranty for this software.  THIS SOFTWARE IS PROVIDED  “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MECHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK ARISING FROM THE USE OF THIS SOFTWARE REMAINS WITH YOU.  OSR’s entire liability and your exclusive remedy shall not exceed the price paid for this material.  In no event shall OSR or its suppliers be liable for any damages whatsoever (including, without limitation, damages for loss of business profit, business interruption, loss of business information, or any other pecuniary loss) arising out of the use or inability to use this software, even if OSR has been advised of the possibility of such damages.  Because some states/ jurisdictions do not allow the exclusion or limitation of liability for consequential or incidental damages, the above limitation may not apply to you.”

 

 

Related Articles
The Exception to the Rule - Structured Exception Handling
Guest Article: C++ in an NT Driver

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

"Link Error"
I use it on WDK 7600, But I have the follow error.

C:\YYY\osrcpp.lib(cpprun.obj) : error LNK2026: module unsafe for SAFESEH image. C:\YYY\xxx.sys : error LNK1281: Unable to generate SAFESEH image.

Rating:
04-May-12, allen zhang


Post Your Comments.
Print this article.
Email this article.

Writing WDF Drivers I: Core Concepts
LAB

Nashua (Amherst), NH
15-19 May 2017

Writing WDF Drivers II: Advanced Implementation Techniques
LAB

Nashua (Amherst), NH
23-26 May 2017

Kernel Debugging and Crash Analysis
LAB

Dulles (Sterling), VA
26-30 Jun 2017

Windows Internals and Software Driver Development
LAB

Nashua (Amherst), NH
24-28 Jul 2017

 
 
 
 
x
LetUsHelp
 

Need to develop a Windows file system solution?

We've got a kit for that.

Need Windows internals or kernel driver expertise?

Bring us your most challenging project - we can help!

System hangs/crashes?

We've got a special diagnostic team that's standing by.

Visit the OSR Corporate Web site for more information about how OSR can help!

 
bottom nav links