OSRLogo
OSRLogoOSRLogoOSRLogo x Seminar Ad
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  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

Build Tricks: Checked and Free Revisited


Let’s build production drivers with full symbolic debugging information that take up no more space than the standard Windows NT DDK build process for production drivers, which of course have nearly zero symbolic debugging information in them. And, lets make sure that our fully debugable production drivers are pretty much identical as binary executable images to the essentially undebugable drivers that are the default output from the NT DDK build process.

 

Why? Consider this scenario: you have been developing your NT kernel driver for some time. It is nearing the point where the driver is going to be released (an Alpha release for example) outside of your own insular internal test environment. You have been doing most of your testing using the checked version of your driver, and everything works fine. Now you switch over to the free version of your driver, install it on your test system, and fire up your regression test suite. Two and one half seconds into the stress test part of the regression test your test system crashes with a hideous Blue Screen of Death. “Gadzooks!,”  you exclaim, and you retry the exact same test with your favorite debugger and the checked version of your driver installed. The test works fine, no problems. You repeat the process, same results. The production version of your driver crashes while the debug version sails through your tests. Your debugger shows a function in your driver on the stack at the time of the crash, but any attempts to set a breakpoint in the source code for this function, or to symbolically examine the local variables for the function are useless.

 

Your production driver doesn’t have any symbolic debugging information.

 

But First Some Review

 

As you all are aware, the NT DDK comes with a set of tools for building device drivers. Unfortunately, while the tools work just fine, the documentation for those tools is rather lacking. This article explores part of the undocumented territory that exists within the driver development process.

 

The core of the toolkit is the build utility itself. Build is a front-end to nmake, Microsoft’s version of the traditional Unix make utility. Various files, that the developer either creates as part of the development process, or that are standard parts of the DDK itself, direct build’s operations. The most familiar of these files are the SOURCES file and the DIRS file. The SOURCES file simply defines values for a set of standard make macros that are then included into a standard makefile template used to build all device drivers.

 

A typical use of the SOURCES file is to simply enumerate the list of source code files that constitute a driver. Actually the build process can create many types of executable objects, not just NT kernel device drivers, so at a minimum the SOURCES file must indicate what type of object is being built. In addition the SOURCES file must indicate where the output of the build process should go, what the target name is, and it must list the set of source files that generate the executable.

 

A minimal device driver SOURCES file is as follows:

  

TARGETNAME=NOTHING

TARGETPATH=$(BASEDIR)\lib

TARGETTYPE=DRIVER

 

SOURCES=NOTHING.c

 

The DIRS file directs the build utility to build in sequence a list of subdirectories. Build will process each directory listed in a DIRS file. If it finds a SOURCES file in a directory it uses that to build the contents of the directory. If there is a DIRS file but no SOURCES file then build will recursively use the DIRS file of the subdirectory to process the new list of subdirectories. Build can easily re-build an entire complex software project consisting of many applications, libraries, and device drivers. It is, of course, the utility used by the Windows NT development team at Microsoft to build the software for Windows NT itself.

 

Enough review already. As you know, your driver can be built in one of two forms: checked or free. The checked version of your driver has the DBG compilation constant set to 1 so that all code wrapped in #if DBG - #endif constructs is included in your source code. In other words this is the debug version of your driver. If you build the free version of your driver DBG is set to zero, so all of your code wrapped within #if DBG - #endif constructs is not included in your driver.

 

Enough review already. To facilitate your progress, the checked and free versions of your driver have some other differences. Compiler optimization (otherwise known as code mangling) is turned off in the checked version of your driver. Consequently, even if you don’t have any DBG-enabled code at all in your driver the free version of your driver is a different executable than the debug version. But the difference I like most of all is that the free version of your driver has what is quaintly referred to in the documentation as minimal symbolic debugging information. By minimal they mean that you will not get more symbolic information out of your driver than the names of functions. Any debugging of your production driver will be through an assembly language view of the world.

 

The Gory Details

 

Let’s actually look at what the compiler settings are for both the checked and the free version of a driver.

 

Here is the compiler command line from a checked build of a simple driver:

cl -nologo -Ii386\ -I. -IE:\DDK\inc -I..\..\inc -IE:\DDK\inc -IE:\DDK\inc -IE:\DDK\inc -D_X86_=1 -Di386=1 
   -DSTD_CALL -DCONDITION_HANDLING=1 -DNT_UP=1  -DNT_INST=0 -DWIN32=100
   -D_NT1X_=100 -DWINNT=1 -D_WIN32_WINNT=0x0400   
   -DWIN32_LEAN_AND_MEAN=1 -DDBG=1 -DDEVL=1 -DFPO=0   
   -DNDEBUG -D_DLL=1 -D_IDWBUILD -DRDRDBG -DSRVDBG   
   /c /Zel /Zp8 /Gy -cbstring /W3 /Gz  /QIfdiv- /QIf  /Gi- /Gm- /GX- /GR- /GF 
  -Z7 /Od /Oi /Oy-  
   -FIE:\DDK\inc\warning.h  .\nothing.c

Here is the compiler command line from the free version build of the same driver:

cl -nologo -Ii386\ -I. -IE:\DDK\inc -I..\..\inc -IE:\DDK\inc -IE:\DDK\inc -IE:\DDK\inc -D_X86_=1 -Di386=1
    -DSTD_CALL -DCONDITION_HANDLING=1 -DNT_UP=1  -DNT_INST=0 -DWIN32=100
    -D_NT1X_=100 -DWINNT=1 -D_WIN32_WINNT=0x0400   
    -DWIN32_LEAN_AND_MEAN=1 -DDEVL=1 -DFPO=1   
    -DNDEBUG -D_DLL=1 -D_IDWBUILD   
    /c /Zel /Zp8 /Gy -cbstring /W3 /Gz  /QIfdiv- /QIf  /Gi- /Gm- /GX- /GR- /GF  
    /Oxs /Oy  
   -FIE:\DDK\inc\warning.h  .\nothing.c
 

In fact there are very few differences. First the checked build defines DBG as the value 1, while it is undefined in the free version. Either #if DBG or #ifdef DBG will work the same in your driver. Secondly both versions define the constant NDEBUG, for whatever reason. The checked version, oddly enough, defines RDRDBG and SRVDBG, in case you would like to have conditional code in your driver for debugging LanManager problems, I suppose.

 

The actual compiler flags differ only as follows: the checked version sets –Z7 /Od /Oi /Oy- while the free version sets /Oxs /Oy.  Lets look at what these flags do. On the checked side the compiler documentation has this to say:

 

-Z7 produces an .OBJ file and an .EXE file containing line numbers and full symbolic debugging information for use with the debugger. The symbolic debugging information includes the names and types of variables, as well as functions and line numbers. “

 

/Od turns off all optimizations in the program and speeds compilation. This option is the default. Because /Od suppresses code movement, it simplifies the debugging process.”

 

/Oi replaces some function calls with intrinsic or otherwise special forms of the function that help your application run faster. Programs that use intrinsic functions are faster because they do not have the overhead of function calls, but may be larger because of the additional code created.”

 

/Oy suppresses creation of frame pointers on the call stack. This option speeds function calls, since no frame pointers need to be set up and removed.” Note that the checked version uses /Oy- which explicitly disables this option. This is probably not needed as the /Od did the same thing.

 

On the free side we have the following:

 

The /Ox and /Os options are combined into the /Oxs option which “combines optimizing options to produce the fastest possible program” specifically enabling In-line Function Expansion, Global Optimizations, Generate Intrinsic Functions, Favor Fast Code, Frame-Pointer Omission, and Control Stack Probes. This Full Monty of Code Mangling is modified by /Os, Favor Small Code Size over fast code, and redundantly by the unneeded, gratuitous, and superfluous /Oy which re-asserts Frame-Pointer Omission in case it didn’t take the first time.

 

But wait, we aren’t done yet, the linker gets involved as well, so we need to take a look at the difference between linking checked and free versions of drivers as well.

 

There are two differences between the linking of a checked driver and the linking of a free driver. First, checked drivers are linked against the checked versions of int64.lib, ntoskrnl.lib, and hal.lib while free drivers are linked against the free equivalents. These are only partial linkings as at least ntoskrnl.lib and hal.lib are dynamic link libraries so the full resolution of external references to these libraries is only made when a driver is loaded into the kernel. This allows both checked and free drivers to be loaded into either checked or free kernels, as long as the exported interfaces in ntoskrnl and hal are identical, which they are. However, the implementation of the exported interfaces in ntoskrnl and hal can of course be completely different in the checked version of the kernel compared to the free version of the kernel. (Which is why ASSERT does absolutely nothing in your checked driver running in the free kernel).

The second set of differences in the link process is the debug settings for each type of build. Both versions set the /DEBUG linker flag, using the undocumented modifiers -debug:notmapped,FULL for the checked version and -debug:notmapped,MINIMAL for the free version. Both versions also set the /DebugType flag, the checked version sets -debugtype:both while the free version sets -debugtype:coff. The MINIMAL and COFF flags reduce the amount of symbolic information in a linked file to essentially the global symbol names in that file. The use of the FULL and BOTH options provides line numbers, local variables, and other debugging information that allows for full symbolic information.

Note that symbolic debugging information and code optimization are for the most part independent. Generally you can do source code debugging on a fully optimized executable as long as the compiler and the linker agreed to produce the appropriate symbolic information. So ideally, as long as we could be certain that the executable was identical, it sure would be nice to build our production driver with full symbolic information.

However, symbolic debugging information takes up a lot of space, and it can expose a lot of information about your source code to people that you may not want to let know exactly how you are doing whatever foul and depraved thing your driver is doing inside the operating system. Therefore our ideal production driver has full symbolic information but that symbolic information is separate from the actual executable image, or at least it can be separated from the executable image before the executable image leaves your premises.

What the DDK has to Say About This

 

Somewhere in the NT DDK documentation is the following:

 

“It is possible to get line-number (local) information in the image files using BUILD. However, this is not the default since it would have a severe impact on disk space requirements needed to build the system. This information can be included by setting the NTDEBUG environment variable as follows:

set NTDEBUG=ntsd

 

Setting this environment variable causes BUILD to generate .obj files and image files with full symbolic debugging information.”

 

This is a clue. The suggestion is to set an environment variable – which means you would have to change your “free build” icon settings. Every time you installed a new version of the DDK you would have to re-create you build environment again, and it would be difficult to share this process with other developers or put this process under version control. A modification to the SOURCES file for your project would be a much better way to go.

 

The checked build environment as supplied by the NT DDK sets NTDEBUG to ntsd and NTDEBUGTYPE to both. The free version does not set either of these environment variables. Makefile.dev (DDK\inc\makefile.def,) the global makefile used by build to create NT executables uses these variables to set up the compiler and linker debugging options discussed above and also to decide if a checked or free version of the driver is being built.  The italics are intentional. The procedure recommended by the DDK documentation quoted above will not work.

 

Look at the platform specific portion of the global makefile, Makefile.plt (DDK\inc\makefile.plt.) Right near the top of this file are the lines seen below.

 

!if ("$(NTDEBUG)" == "") || ("$(NTDEBUG)" == "retail") || ("$(NTDEBUG)" == "ntsdnodbg")
FREEBUILD=1
!else
FREEBUILD=0
!endif

 

If you set NTDEBUG to the value “ntsd”, either in your build environment or simply by added the line

NTDEBUG=ntsd

to your sources file, then you will get, free of charge, the checked version of your driver with the DBG constant set and code optimization turned off.

 

What you want to do instead is to set NTDEBUG to “ntsdnodbg”. It would also be nice to have NTDEBUG set in the SOURCES file itself. It doesn’t do any good to put the line:

NTDEBUG=ntsdnodbg

in your SOURCES file as this will result in always generating a free version of your driver, complete with code optimization, even when you are trying to build the checked version of your driver. The DBG constant will not be defined.

 

Luckily, There is a Simple Work-Around

 

Here is what I put in my driver SOURCES file:

# turn these on to produce symbolic information while
# leaving the DBG flag off
# BUT ONLY IF WE ARE NOT BUILDING THE CHECKED BUILD
#
 
!IF "$(DDKBUILDENV)" != "checked"
 

NTDEBUG=ntsdnodbg
NTDEBUGTYPE=both
USE_PDB=1
 
!ENDIF

By testing the independent environment variable DDKBUILDENV the sources file can conditionally set flags that only affect either the checked version or the free version of the driver.

 

As it turns out, the NTDEBUGTYPE flag is also required if we want our free driver to have the same debug format as its checked equivalent. This is the flag that is used to set the linker –debugtype switch. The test in Makefile.def is as follows:

!IF "$(NTDEBUGTYPE)" == "windbg"
LINKER_DBG_TYPE = -debugtype:cv
!ELSEIF "$(NTDEBUGTYPE)" == "ntsd" || "$(NTDEBUGTYPE)" == "coff" || "$(NTDEBUGTYPE)" == ""
LINKER_DBG_TYPE = -debugtype:coff
!ELSEIF "$(NTDEBUGTYPE)" == "both"
LINKER_DBG_TYPE = -debugtype:both
!ELSE
! ERROR NTDEBUGTYPE macro can one of "", "ntsd", "coff", "windbg" or "both"
!ENDIF

If we don’t set NTDEBUGTYPE build is going to generate COFF format symbols, which is not what we want.

 

You might notice that there is one more flag set in the excerpt from my SOURCES file above. The USE_PDB flag is set. If this flag is not set then the symbolic information is embedded into the driver executable image itself. This can result in an executable image that is at least two or three times larger than the equivalent standard free version driver. In addition, you might very well not want anyone to be able to look at all of the nice descriptive symbols you gave your functions variables and parameters.

 

There are manual tools to strip the symbols out of an executable file into a separate debug file, see the documentation on rebase for details. A much easier way is to convince the linker to produce .PDB files instead, as is done in the standard Visual C++ development environment. Windbg (circa January 1998 vintage, other versions are a mystery,) is happy to use PDB files for symbolic debugging, and SoftIce undoubtedly is able to do the same.

 

To wrap up, all you have to do to create production versions of your device driver that are both fully optimized and fully source code debugable is to add five lines to your SOURCES file. The result is a free version of your driver that:

 

·         contains no extra symbolic information (its all in the PDB file,)

·         uses the same compilation switches for code generation, so the actual executable is identical,

·         and takes up no more space in the kernel than the original production version would have.

 

Speaking of bugs, my version of Word, the ponderous Word97 would prefer to change debugable to debatable, which I figure is debatably a bug.

 

Related Articles
Don't Define NT_UP
Just Checking - Installing a Partially Checked Build
Taming the Beast - The Windows 2000 Build Environment
Save the Environment! - Integrating Build and Developer Studio
If You Build It - Visual Studio and Build Revisited
Building Within Visual Studio (IDE)
New Build Definitions?
Definition of DDKBUILDENV Changed in Windows XP®
Definition of “CPU” Environment Variable Changed

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

Post Your Comments.
Print this article.
Email this article.
bottom nav links