This site uses advanced css techniques
Microsoft's printing subsystem has been somewhat of a moving target for Win32 developers over the last ten years or so, by moving printer drivers from user mode, to kernel mode, and then back to user mode. For a time, both have been allowed, but Vista reportedly blocks all kernel mode drivers, so there's been quite a scramble to make that "last" conversion back to user mode.
Most parts of the driver code will remain unchanged during this conversion, but some of the housekeeping and administrative sections do require modification, and we have found scant resources to help the developer tasked with this conversion.
Some of Microsoft's MSDN pages provide some guidance, and Google gives some additional clues, but we've seen no real centralized guide to the process. Several of us have independently burned multiple days on the same "obvious" issues — it's been very frustrating — and this prompted us to create this Tech Tip.
We've been working with print drivers on and off since Windows NT 3.51 but we do not have a comprehensive knowledge of what's required for this conversion: we only wrote these notes while tripping over things during conversion of an existing driver. Other drivers may have other issues.
We're creating a guide with advice, not an authoritative reference.
These notes apply only to full Win32 systems: NT4, Windows 2000, XP, 2003, and beyond. We have no experience whatsoever with Windows 95, 98 or ME and would be surprised if any of this advice applied to those systems.
We're also targetting only monolithic drivers, which are more or less standalone, and do not apply to the PostScript or UniDriver OEM extension DLLs. We have a great deal of experience with PostScript OEM extensions in user mode, but don't know how much of this applies to a kernel-to-user conversion.
All of our development was done using the Windows 2003 SP1 DDK, and testing done on Windows XP.
Since video drivers use more or less the same GDI subsystem as do print drivers, so some of this information may apply, though we're not sure anybody is converting video drivers to user mode.
When showing code snippets, we've adopted the convention of showing both user and kernel mode code with #ifdef using USERMODE_DRIVER:
#ifdef USERMODE_DRIVER user-mode code here #else kernel-mode code being replaced #endif
We're not proposing that the code actually contain both paths of the #ifdef — a user-mode driver probably won't need to revert — but it felt like the most effective method to convey the gist of the kernel-mode code being replaced.
During the conversion process, some things which are wrong are obviously wrong, and the error messages or behavior makes it clear what must be done to remedy the concern.
But some area are not obvious, and more than one of us has spent days looking for the reason for this or that incorrect behavior. These are danger zones which are very easy to miss, and we've highlighted them with this icon.
Finally, we're really fuzzy on quite a few aspects of the process, mainly because it's our first real commercial kernel to user conversion.
We don't fully understand all the parts of the existing kernel driver, so we're marking these sections with a fuzzy-dice icon to indicate less clarity than usual. Take these sections with a grain of salt.
All Win32 print drivers have two parts: a user-interface part for configuration by the user, and a rendering part which does the actual graphics. The two parts are installed and work as a pair (usually including helper files, such as fonts or a PPD).
The UI component always runs in user mode — usually in the context of the process of the user requesting the printing — but the rendering part has been quite schizophrenic over the last ten years, moving back and forth between user mode and kernel mode.
In NT 3.51 — the first version that we wrote for — the rendering DLL ran in user mode, with full access to the full Win32 API that any process has.
In NT 4, Microsoft moved GDI (printer and video) drivers to kernel mode. This done for performance reasons: video drivers suffered when an output device required back and forth between kernel and user mode, and putting the rendering in the kernel produced a substantial speedup.
Though for video drivers this was particularly important, it's not clear that it was that important for printer drivers, but it was clear that the penalty for printer driver errors was a BSOD, not just a program crash.
Furthermore, this was an awful change for driver developers, because kernel mode did not provide the same environment as user mode:
Furthermore, kernel-mode drivers substantially reduced system stability for all but the most robust drivers, so in Windows 2000 the drivers optionally moved back to user mode, supporting both.
Kernel mode drivers have been more and more discouraged with each subsequent OS release: Windows 2000 Logo compliance requires user mode drivers (though an exception was made early in the program for proven reliable kernel mode drivers), and Windows 2003 does not allow them by default, requiring a Group Policy change to enable.
Windows Vista will not permit kernel mode drivers at all.
User-mode and kernel-mode drivers are fundamentally different, and we think it's a bad idea to reuse the name of the kernel-mode DLL in the user-mode project.
For testing this probably doesn't matter much, especially if the testing machine has no fragments of the old driver around, but for deployment, we sense ugly collisions if the same filename is used for both flavors fo the driver.
Whether the project and directories are renamed is a matter of personal preference, but the names of the output DLLs really ought to be changed. This involves changing things in several places:
These are the bare minimum steps to change the name of the product as seen by the installers, and we recommend incrementing the product version information as defined in the resource files and the like.
Though not strictly required for the conversion, it's really helpful to enable precompiled header support for the project directories. This makes a big difference in compile time, and this kind of project typically requires a lot of build cycles.
This all works by precompiling all the definitions and typedefs and macros in header files up to a certain point, and it's used by all subsequent source files in that directory. Our approach has been to make a single file "precomp.h" as the first header file in every source file in the directory, and this file contains all the system includes:
/* * anchor for precompiled headers */ #include <stddef.h> #include <stdlib.h> #include <stdarg.h> #include <windef.h> #include <winerror.h> #include <wingdi.h> #include <winddi.h> #include <tchar.h> #include <windows.h> #include <winspool.h> #include <stdio.h>
Our practice has long been to include only system header files — not headers defined by the project — and each project directory gets its own copy of this file. The files may be slightly different for each one, so this gives us the flexibility to tailor the file to the particular DLL or library being built.
Then, a few lines must be added to the SOURCES file to instruct the build system to use it:
PRECOMPILED_INCLUDE=precomp.h PRECOMPILED_PCH=precomp.pch PRECOMPILED_OBJ=precomp.obj PRECOMPILED_CXX=1 — only if C++; else C assumed
This may provoke some reorganization of header file use in general, because this effectively requires that all source files start with the same set of header files, and they may not be currently designed for that. We feel that absent a really good reason, a consistent header-file arrangement is probably a plus for the project in general even if the benefits of precompiled headers are not considered.
Test this by running a build: hopefully it's much faster now.
The driver DDK requires that all software be built from the command line using the build tool, and it uses the SOURCES and DIRS files to direct which directories and files are required, as well as many options for configuring the build process.
Converting a kernel-mode SOURCES file requires numerous changes, some of which are subtle and whose omission is hard to detect directly.
These changes must be made in not only the rendering DLL build directory, but in any common libraries that are shared between kernel and user mode. Many driver projects include common code in a separate library directory, building two versions of the same code (with separate UM and KM build subdirectories).
C_DEFINES=$(C_DEFINES) -DUNICODEThis is probably not strictly necessary for the UI component, but if the common library keys off USERMODE_DRIVER, there may be no harm including it everywhere.-DKERNEL_MODE-DUSERMODE_DRIVER
TARGETTYPE=DYNLINKGetting this wrong is silent but very painful
DLLENTRY=_DllMainCRTStartup USE_MSVCRT=1The first insures that the library internals get properly initialized at startup - DllMain() is called by the startup routine. If this is bypassed, all kinds of strange problems are likely to erupt.
TARGETLIBS=..\mylib\km/obj$(BUILD_ALT_DIR)/*/libdrv.libChange ...\KM\... to ...\UM\... as required.
$(DDK_LIB_PATH)\umpdddi.lib $(DDK_LIB_PATH)\user32.lib $(DDK_LIB_PATH)\kernel32.lib $(DDK_LIB_PATH)\winspool.libOthers may be required, but these are a good start.
Since we're now producing a regular DLL instead of a loadable kernel module, we must provide the usual DllMain() function required of all DLLs. In addition, one further function must be provided to tell the printing system that this is a user-mode driver, not a kernel mode one.
First, add DllMain(): we typically save a copy of the instance handle (into ghInstance, which ought to be global) in case we later need access to DLL resources. We also typically show a bit of startup information into the debugging log:
HINSTANCE ghInstance; BOOL __stdcall DllMain(HANDLE hModule, ULONG ulReason, PCONTEXT pContext ) { switch (ulReason) { case DLL_PROCESS_ATTACH: ghInstance = hModule; OutputDebugStringA("MyDrv DllMain() " __DATE__ " " __TIME__ "\n"); break; case DLL_PROCESS_DETACH: break; } return TRUE; }
The DrvQueryDriverInfo() function was introduced in the Windows 2000 DDK to allow the spooler to ask questions of the driver DLL, and the only query now is "Are you a user-mode driver?". Clearly Microsoft anticipates adding new queries, and it seems like a reasonable mechanism.
If this function returns FALSE in the pointed-to buffer, or if it is missing entirely, this driver is assumed to be kernel mode only, so we have no choice but to implement it. A suitable implementation might look like:
BOOL __stdcall DrvQueryDriverInfo( DWORD dwMode, PVOID pBuffer, DWORD cbBuf, PDWORD pcbNeeded ) { switch (dwMode) { case DRVQUERY_USERMODE: *pcbNeeded = sizeof(DWORD); if (pBuffer == NULL || cbBuf < sizeof(DWORD)) { SetLastError(ERROR_INSUFFICIENT_BUFFER); return FALSE; } *((PDWORD) pBuffer) = TRUE; return TRUE; default: OutputDebugStringA("DrvQueryDriverInfo - bad parameter\n"); SetLastError(ERROR_INVALID_PARAMETER); return FALSE; } }
Finally, since this is a DLL, we require the .DEF file to describe the exported functions, and there should be just four of them. This small file can be added to the project:
; LIBRARY MYDRV EXPORTS DllMain DrvQueryDriverInfo DrvEnableDriver DrvDisableDriver
The build tool requires that the name provided on the LIBRARY directive exactly match (including case) the TARGET name in the SOURCES file, so it's probably best to leave the LIBRARY line out of the .DEF file entirely and let the build system handle it automatically.
Omitting this file produces a linkage error during build time.
When trying to get our first-cut of our converted driver working, we found that the the AddPrinterDriver() function would always treat it as a kernel-mode driver.
After about three days, we ran across the reason: it seems that AddPrinterDriver() does not consider the version number in the DRIVER_INFO_n structure, but instead uses the version information in the .DLL.
Changing the second value on the FILEVERSION line from 2 to 3 got the system to recognize it as a user-mode driver:
... VS_VERSION_INFO VERSIONINFO FILEVERSION 0,3,0,0 // NOT 0,2,x,x PRODUCTVERSION 4,0,1381,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS VS_FF_DEBUG // 0x1L #else FILEFLAGS 0x0L #endif FILEOS VOS_NT_WINDOWS32 // 0x40004L FILETYPE VFT_DLL // 0x2L FILESUBTYPE VFT2_DRV_PRINTER // 0x1L ...
Missing this burned a lot of time.
We're not sure if the UI DLL requires the same attention, but it seems prudent to make them all agree on the version.
Because this is a user-mode driver, the .DLL file is automatically loaded into memory. The handle is passed to DllMain() and should be saved into a global variable, which can now be used directly (these examples assume that pdev points to the driver-defined PDEV structure).
#ifdef USERMODE_DRIVER pdev->hModule = ghInstance; #else pdev->hModule = EngLoadModule(EngGetDriverName(hdev)); if (pdev->hModule == NULL) { DBGERRMSG("EngLoadModule"); FreePdevMemory(pdev); return NULL; } #endif
It appears that releasing a loaded module is done with UnmapViewOfFile(), but for the rendering DLL itself this should not be called.
A printer can also have a "data file" associated with it, which could be be a PPD for a PostScript driver, or some other kind of file.
The documented API is EngGetPrinterDataFileName(), but this has always failed for us with error #127 (ERROR_PROC_NOT_FOUND). This function is imported by the DLL, it's exported by GDI32.DLL, and we can't find any reason why this shouldn't work.
But it doesn't, so we instead must use GetPrinterDriver() to fetch this data, as shown in the MSPLOT DDK sample.
LPWSTR pwszDataFile = NULL; #ifdef USERMODE_DRIVER PDRIVER_INFO_2 pDriverInfo = NULL; DWORD dwBytesNeeded; if (!GetPrinterDriver(hDriver, NULL, 2, NULL, 0, &dwBytesNeeded) && GetLastError() == ERROR_INSUFFICIENT_BUFFER && (pDriverInfo = (PDRIVER_INFO_2)LocalAlloc(LPTR, dwBytesNeeded)) && GetPrinterDriver(hDriver, NULL, 2, (LPBYTE)pDriverInfo, dwBytesNeeded, &dwBytesNeeded)) { pwszDataFile = pDriverInfo->pDataFile; } #else /* not supported in user mode - dunno why */ pwszDataFile = EngGetPrinterDataFileName(hdev); #endif
We've found no explanation why this is required.
When the spooler calls the driver's DrvEnableDriver() function, it passes a flag indicating the operating system in use. This flag, defined in <winddi.h>, is one of:
Macro | Operating system | Hex Value |
---|---|---|
DDI_DRIVER_VERSION_NT4 | Windows NT4 | 0x00020000 |
DDI_DRIVER_VERSION_SP3 | Windows NT4 w/ SP3 | 0x00020003 |
DDI_DRIVER_VERSION_NT5 | Windows 2000 | 0x00030000 |
DDI_DRIVER_VERSION_NT5_01 | Windows XP | 0x00030100 |
DDI_DRIVER_VERSION_NT5_01_SP1 | Windows XP w/ SP1 | 0x00030101 |
These reflect not so much the operating system version, but the device driver interface version; each time Microsoft added or changed the API for talking to a printer driver, it incremented the numbers.
Looking at the hex values, one can see which changes were major and which were minor: it's no surprise that NT4 to Windows 2000 involved a major jump, but XP to XP/SP1 was very minor. Unfortunately, we don't know exactly which changes occurred in each version.
The driver makes use of this DDI version information in two ways:
We find it curious that Windows 2003 and Vista don't have any entries in this table, which suggests either that they actually haven't made any changes to the DDI, or that the DDK/MSDN haven't been updated yet. We'll see.
Microsoft's MSDN documentation on DRVENABLEDATA provides this table showing how the version support spans the various levels of OS:
Windows Version | Value of iDriverVersion |
---|---|
Windows NT 4.0 | iDriverVersion == DDI_DRIVER_VERSION_NT4 |
Windows NT 4.0 SP3 | DDI_DRIVER_VERSION_NT4 <= iDriverVersion <= DDI_DRIVER_VERSION_SP3 |
Windows 2000 | DDI_DRIVER_VERSION_NT4 <= iDriverVersion <= DDI_DRIVER_VERSION_NT5 |
Windows XP | DDI_DRIVER_VERSION_NT4 <= iDriverVersion <= DDI_DRIVER_VERSION_NT5_01 |
Windows XP SP1 | DDI_DRIVER_VERSION_NT4 <= iDriverVersion <= DDI_DRIVER_VERSION_NT5_01_SP1 |
We're not entirely sure what to make of this in the context of a user-mode driver. Though we think that a kernel-mode driver could support all operating systems from NT4 through XP/SP1, we believe that user-mode drivers must start with DDI_DRIVER_VERSION_NT5.
#ifdef USERMODE_DRIVER # define DDI_DRIVER_VERSION DDI_DRIVER_VERSION_NT5 // Probably? #else # define DDI_DRIVER_VERSION DDI_DRIVER_VERSION_NT4 #endif BOOL DrvEnableDriver(...) { if (iEngineVersion < DDI_DRIVER_VERSION || cb < sizeof(DRVENABLEDATA)) { DBGMSG(DBG_LEVEL_ERROR, "Invalid parameters.\n"); SETLASTERROR(ERROR_BAD_DRIVER_LEVEL); return FALSE; }
The printing system provides a driver writer with a host of support functions, most of which are prefixed with Eng (for graphics Engine). The majority are supported in both modes, though some require adjustment in their new home. We'll touch on the ones we know about here.
Curiously, just four functions are defined as being outright unsupported in user mode, with no obvious replacement:
If the driver uses any of these functions, they must be removed. Though some (such as EngDebugBreak()) appear to be debugging functions which may be unnecessary in user mode — the normal debugging tools should work just fine — we're not sure how one replaces the need to get a list of Type 1 fonts.
The rest of the EngXXX functions are generally available, but Microsoft recommends (Here: "Choosing User Mode or Kernel Mode") that true user-mode functions be used instead of the Eng functions. Not all the conversions are obvious, and the guidance given should be used.
For instance, though one can replace the kernel-mode EngAcquireSemaphore() with the Win32 CreateSemaphore(), it's more efficient to replace all the semaphore calls with Critical Section objects.
C Runtime library support has always been problematic in kernel mode, because it doesn't appear to have a "real" C runtime library, but user mode code has no such limitations. There are a host of familiar functions, mainly related to string processing, that are fully available in user mode.
Kernel-mode does not support native floating point with the usual C data types, requiring instead a set of cumbersome macros and functions. This has proven to be a real annoyance to a driver author, and a substantial relief in moving back to user mode.
New user-mode code should just use the normal C floating-point data types and operations just like any other application, without any special arrangements, but code coming from kernel mode must address the cumbersome macros and functions.
There are three pseudo-types dealing with floating point which must be considered, and each has a different definition in each mode.
The central unit of quasi-floating point is the FLOATOBJ, defined in <winddi.h>, and in kernel mode this maps to a structure containing a pair of unsigned longwords (the same size as a C double data type), but in use mode it's instead mapped to FLOAT, which is defined as float (we find it curious that the "real" floating type is smaller than the fake one).
This table represents the various pseudo-floating point types used in kernel mode, and the real underlying types they map to in each mode:
Pseudo-Type | Kernel Mode | User Mode | Defined in |
---|---|---|---|
FLOATOBJ | struct _FLOATOBJ { ULONG ul1; ULONG ul2; }; |
FLOAT | <winddi.h> |
FLOATL | DWORD | FLOAT | <winddi.h> |
FLOAT | undefined | float | <wtypes.h> |
Though it's possible to completely rip out all of this and replace it with native types, it's probably not necessary, at least for the first conversion pass. Since the FLOATOBJ functions and macros all work in user mode, it might make sense to leave them alone until the driver is actually working properly, treating this as an optimization pass for later.
The Windows NT4 DDK provided some limited user-mode hooks for the rendering side of a print driver, mainly to allow a small user mode component to intercept the output from the kernel-mode driver before it streams to the output device.
These UMPD (User Mode Print Drivers) functions go in a separate DLL and install with the UI and rendering DLLs during installation time. They are only applicable to kernel-mode drivers, and are now considerd obsolete.
These are the UMPD entry points we know about:
These probably build in a separate directory which should be excluded from the project's top-level DIRS file - we won't need the UMPD components.
There is probably a small component in the UI as well: we've found that the UI's DrvPrinterEvent() has some code in the PRINTER_EVENT_INITIALIZE handler that doing some registry manipulation related to UMPD. Inspect this code and (most likely) remove it.
Note: if the UMPD is actually performing specific functions on the data stream (rather than merely passing the data from the kernel GDI driver back to the spooler), these functions must be merged into the user-mode rendering driver.
Printer drivers — whether user mode or kernel — become security nightmares when they have buffer-overflow bugs. At best, it produces system instability when the bugs are tripped inadvertently, and at worst; provide a system compromise at the highest level.
These are to be strenuously avoided.
There is no substitute for careful attention while coding, but buffer overflows are notoriously difficult to get right: this means that one must use all the help one can get to help avoid these.
Some of the most common culprits are the unbounded string operations which do not respect the size of their target buffers:
strcpy(internalBuffer, userString); // boom
When a user-provided string — a font name, a directory path, a username, etc. — is as expected, it all works well, but when an attacker causes a much longer string to be passed (perhaps via the printing APIs), then the buffer gets overwritten, smashing information past the end of the buffer.
This is where bad things happen, the particular badness depending on lots of factors. But the best case is an application crash, alerting one to the problem.
Microsoft has created a set of libraries which help mitigate many of these problems by providing full buffer-bounding for all the usual string routines: <strsafe.h>:
These largely replace the normal strxxx routines, and the header file actually marks the legacy code as deprecated; this ought to discourage their use.
Converting to the safe-string mechanisms is likely to be tedious, but ought to go a long way to avoiding these buffer overflow issues.
We've long been advocated of using C++ for print-driver development, from back in the NT 3.51 days. User mode printer drivers provide complete support for the usual language features we all know and love, including full exception handling (both SEH and C++ exceptions).
We're not sure how much support for C++ exists for kernel mode drivers, but we can't help but believe that many of these driver authors limited themselves to C only as a defensive measure.
For drivers that require a strict conversion only, with no projected feature development, it may make sense to make as few changes as possible (i.e., leave it in C), but when the product has a future of enhancements ahead of it, a conversion to C++ may enable much more productive development.
However: as much as we favor C++, we nevertheless recommend holding off on the C++ conversion until the existing driver has been largely converted to user mode. We distinguish between mode-change tasks and feature-enhancement tasks, and understand that driver failure can be caused by problems in either.
By doing the bare minimum conversion to user mode first, then working from there. This reduces the number of variables that must be addressed when trying to track down a driver failure.
Testing a print driver starts as a manual process just to make sure the overall infrastructure works properly — the DLL loads, the job starts, pages are rendered, debugging is recorded — but large-scale systematic tests get much more tedious.
Print Gremlin is a Microsoft-provided tool in the DDK designed exactly for this purpose: it exercises a printer driver very heavily and helps track down problems. These tests — especially the font tests — are very extensive.
The PGREMLIN.MSI installer is found in the tools\print\x86\ directory of the DDK, and installation requires a reboot due to the many language packs installed. Microsoft's documentation notes that PGREMLIN doesn't clean up after itself, so this suggests using this on a test machine (rather than the developer's box) for fear of messing something up.
PGREMLIN is a console-mode application with no user interaction: when launched, it just starts testing. By default it exercises all printers on the box, but can be given the name of one or more particular printers on the command line (perhaps configured via a desktop shortcut).
The Microsoft documentation describes a /SkipComments:"string" option which skips printers with string in the name, but we found that it included those printers, not excluded. This looks like a bug.
On our low-powered test system, the print gremlin produced 32 pages of output (including a cover sheet with summary information discovered about the driver). It took Print Gremlin itself about two minuts to run, and our HP4200 PostScript printer about five minutes to render all the pages.
It seems prudent to make a control run to a known-good printer to have some valid output to compare with the tested driver.
To thoroughly test a driver, one cannot help but actually print real output and visually inspect it, but exercising the internals of the driver is a useful exercise as well. We've seen PGREMLIN produce messages about SEH (Structured Exception Handling) faults during the tests, though it doesn't give any specifics about what prompted the error (i.e., the particular API call in question).
When testing the internals, one may be more interested in the error messages (or lack thereof) than the output, so it's a viable strategy to Pause the printer in the Printers & Faxes applet so that the print jobs queue up while the tests are run, sending no actual data to the printer.
The jobs can later be released if needed, or Canceled if not.
We'll note that Print Gremlin has a useful behavior: when it's exercising a particular printer, it does direct printing, which effectively loads the rendering DLL into its own address space. Because the DLL is inside the PGREMLIN.EXE process and not under the spooler, it's able to detect all kinds of faults via the Structured Exception Handling mechanism.
This is a big win for the driver developer: rather than have to debug the spooler service — spoolsv.exe — as a whole, one can launch pgremlin.exe directly to watch it run and see its behavior. This makes it much easier to track down serious issues with the rendering DLL.
While researching this project, we came across several useful references that we're pointing to here. Some directly bear on the task of converting a printer driver from user to kernel mode, while others provide more ancillary information.
Microsoft is notorious for moving links around on their website, so for most MSDN/Technet links we've shown in a small sidebar the navigational hierarchy used to find that document on their site.
A very special thanks to Dr. Herbert Steiner of HSM Informatik AG for his most extensive assistance on this project.
First published: 2006/07/29 (blogged)