Making Connections

Building one driver for Win7, Win8, Win8.1

Posted by Terry Moore
Terry Moore
 
User is currently offline
on Wednesday, 18 March 2015 in Useful Tips

Among other things, MCCI creates Windows kernel-mode drivers for internal product use and for licensing to customers to bundle with their products.

We want to run the same binaries on all compatible platforms. So (for example) we build one driver for x86 that will run on all supported operating systems.

This article summarizes our experience in doing this, and offers some concrete tips on how to do things (and how to solve problems).

We'll start with some reference material.

You have two choices, either limit yourself to the features of the oldest operating system, or dynamically determine what version you're running on, and dynamically import symbols using MmGetSystemRoutineAddress().

MCCI needs to do the latter (use the newer features on newer OS versions, and fall back to a reasonable substitute on older operating systems).

Microsoft has specific recommendations about what to do, posted in two MSDN articles:  https://msdn.microsoft.com/en-us/office/ff554887 (writing), and https://msdn.microsoft.com/en-us/office/jj572863 (building). It is odd to find a recommendation about kernel mode drivers in the "Office Dev Center", but let us not digress. The salient points for our use case are:

  1. Use the most recent applicable WDK (if you want to target 7 through 8.1, use WDK 8.1)
  2. Use RtlIsNtDdiVersionAvailable() to query whether a given feature is available, and then use MmGetSystemRoutineAddress() to get the function name if so.
  3. Use the Microsoft C "typedef typeExp ( functionName ) extension. This extension is somewhat like gcc "typedef typeof(function) typename"; it creates a type from a function, including all the SAL annotations and attributes like calling sequence (CDECL, STDCALL, FASTCALL, etc.)  You can directly create a pointer type from the function, but MCCI normally prefers to create a function type, as this lets us also define substitute functions that can be used when needed.
  4. Don't directly call functions that are not present in all systems of interest.
  5. Link with $(DDK_LIB_PATH)\BufferOverflowK.lib if you need to support Win7 and earlier. (The default if you're targeting Win8.1 willl be $(DDK_LIB_PATH)\BufferOverflowFastFailK.lib -- this is not what you want. Drivers built with the Win8.1 WDK will immediately bug-check on Win7.

This advice is all good -- but not sufficient.  (More about that below.)  Let's start by amplifying some of the above points.

Dynamically Importing APIs

It's very important to check RtlIsNtDdiVersionAvailable() before calling MmGetSystemRoutineAddress() -- MmGetSystemRoutineAddress() doesn't necessarily return NULL on platforms where the function is not supported.

Defining types for your imported functions

Because of calling-sequence issues, it's very important to use the typedef trick. But bear in mind that, at least with some Microsoft compilers, it doesn't work right for FASTCALL functions on X86. MCCI's workaround in that case is to cut and paste the exact definition from the WDK header file (normally wdm.h) and put it in our header file.

Selecting the right Buffer Overflow library

Linking with BufferOverflowK.lib is critical. But you have to make sure that you add this in the right place, because it uses the variable $(DDK_LIB_PATH), which must be already defined.

Find the following place in your .vcxproj file:

The above line will end up assigning a value to DDK_LIB_PATH.

After that line, insert the following:



  $(DDK_LIB_PATH)\BufferOverflowK.lib

The label "KernelBufferLib" is arbitrary.

If you put the property group in the wrong place, it won't have effect, and the WDK will silently use BufferOverflowFastFailK.lib -- which is definitely not what you want.

It's a very good idea to look at your msbuild.log and examine the link.exe invocation -- make sure that the library you wanted was linked. I just search for BufferOverflow in the log, and make sure that it's the library I want.

Don't directly call functions that are not present (KeInitializeSpinLock as an example)

This one is trickier than it looks. Microsoft sometimes changes function calling sequence, or changes from inline to calling a function exported by the kernel. In these cases, the MSDN documentation doesn't help you -- the API is present, but the way it compiles is different.

For example, the API KeInitializeSpinLock() is present in all versions of Windows. However.... In Windows 7 and earlier, it was implemented as an inline function. The kernel (ntkern.sys) didn't export a function named KeInitializeSpinLock.  A driver built with the Win7 WDK will not import KeInitializeSpinLock(); on Win8.1, it will use the inline instead of calling the kernel-exported function. No great loss, as the kernel function does the same thing, plus possibly inserting barriers and/or doing more Driver Verifier checks. (To be honest, I haven't looked into why they made the change.)

The tricky part is building the same driver with the Win8.1 WDK. It will run fine on Windows 8.1, but on Win7, the system won't load it. (You'll get a Code 39 error in device manager, and a message about the driver possibly being corrupt.)

To a developer, Code 39 almost always means "oops, I'm using an export that doesn't exist on this system". Finding the missing export can be tricky. I use depends.exe/depends.dll/depends.chm from the WDK 8.1 tools tree. (There's a website that claims to have it, but there wasn't enough authentication, so I didn't want to download it.) I put the three files on a thumb drive and carry it to the Win 7 system, and run the tool.

"depends.exe" is useful, but itself is a bit tricky. For me, it always complains that it can't find wmilib.sys. I have learned to ignore that as a false positive. The key thing is select each imported DLL, then click on the PI^ button in the top-right box. This will bring any unsatisfied imports to the top of the list, as in the following screenshot.

In fact, on Win7, you'll see that KeInitializeSpinLock appears in red. This tells you (1) Win7 doesn't export KeInitializeSpinLock(), and (2) Win8.1 does.  (You know it does, because your driver loads on Win8.1.) This should cause you to immediately compare the Win7 WDK wdm.h definition of KeInitializeSpinLock with the Win8.1 equivalent. You will immediately see that on Win7, KeInitializeSpinLock is always a FORCEINLINE function; whereas on Win8.1, it may be an external function. You will see some complicated conditional compiles, and (if you're like me) you'll conclude that trying to force the header file to generate the inline when targeting Win8.1 would be a mistake -- you don't know what else will happen. Finally (again, if you're like me), you will realize that there probably is an important reason to use the kernel's KeInitializeSpinLock if it's available.

So we have to do the run-time import MmGetSystemRoutineAddress() trick.

How to do this?

We have to start by defining a symbolic type based on KeInitializeSpinLock(). If you don't need to worry about compiling on older WDKs (no legacy code or systems, lucky you), you can probably just do this:

typedef MYDRIVER_KeInitializeSpinLock_t (KeInitializeSpinLock);

[MCCI has 20 years of customers who need to be supported, which means that I can't simply mandate that the Win7 WDK will go away; and we use common code; so I'm not as lucky as you. But we will discuss the solution to Win7 / Win8.1 WDK source compatibility in another post, if at all.]

You'll need someplace to store the pointer; you don't want to look this up every time.

MYDRIVER_KeInitializeSpinLock_t *MYDRIVER_gpKeInitializeSpinLock;

You'll need a function to use in case you're on an old system and there's no kernel export to call:

MYDRIVER_KeInitializeSpinLock_t KeInitializeSpinLock_DownLevel;

VOID KeInitializeSpinLock_DownLevel(PKSPIN_LOCK pSpinLock) {
    *pSpinLock = 0;
}

At driver initialization, you need to check the system version and import the name:

if (RtlIsNtDdiVersionAvailable(NTDDI_WIN8)) {
   UNICODE_STRING fName;
   RtlInitUnicodeString(&fName, L"KeInitializeSpinLock");
   pKeInitializeSpinLock = (MYDRIVER_KeInitializeSpinLock_t *)MmGetSystemRoutineAddress(&fName);
} else {
   pKeInializeSpinLock = KeInitializeSpinLock_DownLevel;
}

Then you must find all calls to KeInitializeSpinLock() in your code, and change (for example):

KeInitializeSpinLock(&pDriverExtension->MyLock);

to

(*MYDRIVER_gpKeInitializeSpinLock)(&pDriverExtension->MyLock);

MCCI driver code isn't permitted to use global variables at all; so instead of using a global, MCCI would put it in a "driver object extension", but the principle is the same.

By the way, the "MYDRIVER_" prefix is whatever prefix you use in your driver to distinguish your symbols from Microsoft's symbols. (You do use a prefix, don't you?)

Was this post useful to you? Want to see more on some topic? Leave a comment, or find me on Twitter (@TmmMcci) or LinkedIn (http://www.linkedin.com/in/terrillmoore/en).

Tags: Untagged

Comments

Please login first in order for you to submit comments
Legal and Copyright Information