Copyright © 2003 James Antognini. All rights reserved.
If you are new to building drivers or other software via the Windows build mechanism, there is much to learn. If you are an experienced developer, you may not know about some useful features that the standard build procedure provides. This article lays out the essentials of building drivers for Win2000, WinXP and Win2003 with the Windows DDK and describes a number of advanced features in building.
“Build” is the creation of executables and other binaries from source code, with constraints such as target OS level, optimization and debugging capabilities. The process picks up changes made since the last build operation, with an option to rebuild completely. The DDK’s build.exe invokes utilities like the compiler and linker with command-line options appropriate for type type of target you are building. You control the process by putting directives in environment variables and in control files. Then, usually in a command window you open by a Start menu drop-down created by DDK installation, you launch build. But you don’t have to build in a command window; you can, if you wish, build in Visual Studio by having it run DDK build.
Practically speaking, you need the Driver Development Kit (DDK) in order to build drivers. This isn’t the time or place to discuss whether you could somehow get by with header and library files obtained some other way, or whether you can use a different compiler or linker. Follow these steps to install the DDK:
You will have some choices to make when you run setup:
- It is prudent to accept the default high-level directory, \WINDDK\3790. If you specify a name of your own, do not use one with imbedded blanks, because some build tools will parse commands incorrectly.
- It is probably best to choose Win2000, WinXP and Win2003 as targets, so that you can build for any of them. (If you are planning on a 64-bit target, select the options for that, too.) For example:
- You should choose all the documentation and all the tools.
- You should install all the samples. You may want to study them from time to time, and they will make it easy to verify that you have installed the DDK correctly.
- If the instructions warn against installing over a previous DDK, uninstall that earlier version first.
Start
Programs
Development Kits
Windows DDK 3790
Build Environments
<Win2000 | WnXP | Win2003>
<Free build | Checked build>
(It may be handy to create a shortcut on the desktop for a target that you often use.)
Note: The SDK and Visual Studio do not need to be installed to do DDK build.
A driver project consists of at least one directory, one or more source files, possibly header files and sundry other files. Here are typical contents of a project:
!INCLUDE $(NTMAKEENV)\makefile.def
Here is how a driver project might look before the first build. (Note: you can download this project from the WD-3 site at this URL: http://www.wd-3.com/downloads/WinBuild.zip.)
(I’m not showing the contents of \DriverPart2, \inc and \TalkDriver.)
After you have a successful build, there will be some new files:
There will be several new subdirectories, a log file and a browser (.bsc) file. Under the new directories there are (not shown) .obj files, a .lib file, a .sys file (the driver executable), an .exe file (a user-space program to call the driver) and symbol files.
Note: It’s not a good idea to put your projects under the DDK directory, as you are likely to be installing a newer DDK from time to time and could lose projects located under the DDK directory. It’s unfortunate that the standard build environment shortcuts in the Start menu leave you at a subdirectory within the DDK. Just remember to issue a CD command to move to a more sensible place.
You control build by providing directives, which the DDK documentation calls “macros.” The documentation attempts to distinguish between directives supplied in a sources file (where they allow substitution) and directives in the form of environmental variables in the command window. The documentation admits that the distinction is a fine one. I’ll ignore it, because you can specify many of the directives in either form. Sometimes it makes sense to specify them in the sources file, and sometimes it is easier to set them up via environmental variables.
It will help to know a little about how build does what it does. The process begins when you invoke build.exe. This program figures out dependencies in the project files and then repeatedly calls nmake.exe to make parts. Build (build.exe and nmake.exe taken as a unit) functions in stages, with compilation, link-edit and other operations each constituting a stage. At each stage, build scans DDK makefiles, sets up command-line option strings and calls a compiler, the link-editor or another utility to do the real work of producing an .obj, .lib, .sys, .exe, .dll or other file.
Build has no memory between stages, and this lack of memory can produce some oddities. One peculiarity is that directives occasionally behave inconsistently with respect to each other. TARGETPATH and TARGETLIBS are such a case, in that you have to specify path information for the same binary differently in each (see below).
This is where build gets most of its directives. Here are some of the more important ones — and note that the first four are required:
In case you’re wondering how names like ntoskrnl.exe and hal.dll are generated for kernel modules, the answer is that build has special-case logic. Don’t try to create kernel modules of your own with an extension of .exe or .dll.
Note: The specific form TARGETPATH=lib$(BUILD_ALT_DIR) is often employed, and the resulting directory path is one beneath the one where the sources file is located; for a WinXP checked-build target, that path would be libchk_wxp_x86\i386. The path name comes from concatenating “lib” with the evaluated environmental variable BUILD_ALT_DIR (see environmental variables) and with “\i386,” which designates the platform type. If you’re using this form to produce a .lib file, you will specify the file’s path in TARGETPATH differently from the way you will specify the path for that same file in building an executable incorporating the .lib file (see TARGETLIBS).
SOURCES= \
BozoStuff.cpp \
miniport.c \
passthru.c \
passthru.rc \
protocol.c \
UtilRtns.cNotice the mixture of .c and .cpp files: The compiler’s default action is to apply C or C++ language rules according to file type.
Note: If a directive is to have more than one entry, you may specify all on a single line, or you may put them on several lines, but then you must end each line with a backslash and no following characters, not even blanks. This restriction applies to directives in sources files and in dirs files.
A further note: In SOURCES, names can take the form filename.ext, .\ filename.ext and ..\ filename.ext, but not ..\..\ filename.ext and not a fully qualified name like c:\dir\ filename.ext. That is another quirk of build.
INCLUDES= \
.\inc \
C:\JA.pgm\src\C\Drivers\JADriver\incNote: Unlike SOURCES, INCLUDES allows forms like ..\..\Dir and fully qualified directory paths. Do not, however, specify paths with imbedded blanks. Instead, use the 8.3 name (you can discover the 8.3 names of directories and files below a directory by issuing the command dir /x <directory> in a command window).
USER_C_FLAGS=$(USER_C_FLAGS) /FAsc
would pick up any value defined in the environmental variable USER_C_FLAGS in the command window and would append /FAsc to that value. See compiler parameters for other possibilities.
Note: TARGETLIBS does not have the quite same special handling that TARGETPATH does. If you created a .lib file with a path of TARGETPATH=lib$(BUILD_ALT_DIR), in the sources file for the .sys file you would designate the .lib file’s path more fully, by lib$(BUILD_ALT_DIR)\*\ (asterisk means implicit platform type).
To make the point above clear, let’s suppose you have this for the .lib file:
TARGETNAME=DriverPart2
TARGETTYPE=DRIVER_LIBRARY # output is a .lib file
TARGETPATH=lib$(BUILD_ALT_DIR) # where to put the .lib file.For the .sys file, you would have:
TARGETNAME=MyDriver
TARGETTYPE=DRIVER # output is a .sys file
TARGETLIBS=lib$(BUILD_ALT_DIR)\*\DriverPart2 # where to find the .lib file.
TARGETPATH=lib$(BUILD_ALT_DIR) # where to put the .sys file.
BROWSER_INFO=1
BROWSERFILE=$(TARGETNAME).bsc -n
!if !defined(DDK_TARGET_OS) || "$(DDK_TARGET_OS)"=="Win2K"
# The driver is for Win2K. Build with NDIS 4.0
C_DEFINES=$(C_DEFINES) -DNDIS40_MINIPORT=1
C_DEFINES=$(C_DEFINES) -DNDIS40=1
!else
# The driver is for WinXP or Win2003, so build# with NDIS 5.1.
C_DEFINES=$(C_DEFINES) -DNDIS51_MINIPORT=1
C_DEFINES=$(C_DEFINES) -DNDIS51=1
!endif
Putting it all together, you might have this in a complete sources file:
TARGETNAME=MyPassthru
TARGETYPE=DRIVER
TARGETPATH=lib$(BUILD_ALT_DIR)
TARGETLIBS=$(DDK_LIB_PATH)\ndis.lib
SOURCES= \
BozoStuff.cpp \
miniport.c \
passthru.c \
passthru.rc \
protocol.c \
UtilRtns.c
INCLUDES= \
.\inc \
C:\JA.pgm\src\C\Drivers\JADriver\inc
USER_C_FLAGS=$(USER_C_FLAGS) /FAsc
!if !defined(DDK_TARGET_OS) || "$(DDK_TARGET_OS)"=="Win2K"
C_DEFINES=$(C_DEFINES) -DNDIS40_MINIPORT=1
C_DEFINES=$(C_DEFINES) -DNDIS40=1
!else
C_DEFINES=$(C_DEFINES) -DNDIS51_MINIPORT=1
C_DEFINES=$(C_DEFINES) -DNDIS51=1
!endif
BROWSER_INFO=1
BROWSERFILE=$(TARGETNAME).bsc -n
You can find a full list of build directives in the DDK documentation under the heading “Build Utility Macros.”
Note: After you make a change in a sources file, it is usually necessary to rebuild completely to pick up the changes.
The dirs file is another ancillary file that build uses. The file can serve several purposes. It is needed when a project contains several distinct pieces, such as a driver proper (a .sys binary) and a kernel DLL (also a .sys binary; you can find an example on my website at kernel DLL). Another such case is a driver and a user-space program that communicates with the driver. A dirs file (or maybe more than one) is needed when different parts of a project require different build parameters; the sources file in each part can specify these. Finally, a dirs file makes it possible to break up a large project into smaller pieces owned by different developers, with each piece under a separate subdirectory. In this type of project, the sources file in each developer’s subdirectory would build a library. There would be a master subdirectory whose sources file would assemble the complete driver using the libraries.
The usual arrangement of a multi-piece project is that the dirs file is in the project’s top directory, and each piece is in a subdirectory. For example, the dirs file might contain:
DIRS= \
Driver \
DriverDLL
A project with a driver and a kernel DLL might have components like these:
dirs
.\Driver
BozoStuff.cpp
makefile
miniport.c
passthru.c
passthru.rc
protocol.c
sources
UtilRtns.c
.\DriverDLL
DriverDLL.cpp
makefile
sources
.\inc
OtherHdrs.h
passthru.h
In the above, the sources file in the Driver subdirectory would specify TARGETTYPE=DRIVER, resulting in a .sys file. The sources file in the DriverDLL subdirectory would specify TARGETTYPE=EXPORT_DRIVER, resulting in another .sys file.
A project with two pieces for a driver and a user-space program might take this form:
dirs
.\Driver
BozoStuff.cpp
makefile
miniport.c
passthru.c
passthru.rc
protocol.c
sources
UtilRtns.c
.\DriverRtns
DriverRtn1.cpp
DriverRtn2.cpp
makefile
sources
.\UserProgram
TalkDriver.cpp
.\inc
OtherHdrs.h
passthru.h
The Driver sources file would specify TARGETTYPE=DRIVER; the DriverRtns sources file, TARGETTYPE=DRIVER_LIBRARY; and the UserProgram sources file, TARGETTYPE=PROGRAM. For Driver, a .sys file would be built; for DriverRtns, a .lib file would be built (and later incorporated into the Driver .sys file); and for UserProgram, an .exe file would be built.
One last point: It is possible for a dirs file to point to one or more dirs files, in a tree, with a sources file in each leaf. You took advantage of this feature when you built the DDK samples for verification (you did verify, right?).
Build will respect certain environment variables in the command window. You can refer to such variables in a sources file with this syntax: $(variable_name). You can also redefine them with syntax like ‘variable_name = value’.
Here are some of the environment variables that build uses:
You can define your own compiler preprocessor variables this way. If, for example, you want to have a variable called MyVarLvl (value “2”) visible to the preprocessor, say this: set cl=/DMyVarLvl=2. (See compiler parameters to understand how this works.)
You can see the full set of environmental variables in force in a build window by issuing set.
Note: Capitalization of environmental variables is generally unimportant, but capitalization of their values may well be significant.
Free versus checked build
In choosing the target environment, you must decide whether to do a free (a/k/a “release”) or checked (a/k/a “debug”) build. Header files are used a little differently in the two types. The principal effect is whether debug code is produced. A secondary effect is that specifying a checked build tells the compiler not to optimize code.
Note: Whether your driver is built free or checked has nothing to do with whether your code will be running in the free or checked build of the OS itself. You can debug the “checked” version of the driver on either the free or the checked OS. You can run the “free” version of the driver in either place, too.
Here are a few compiler options that might be useful when you build a driver:
Prefix an option with slash (“/”) or dash (“-“) when you specify it. You can set options in the build window via the CL environment variable, e.g., set cl=/Fasc /TC /DMyVar=1; or you can utilize C_DEFINES or USER_C_FLAGS in a sources file.
You build your project by starting the target environment of your choice, using the CD command to make project directory current, and typing build. This causes build to recreate any project binary with a constituent that has changed or that uses anything (e.g., an include file) that has changed since the previous build. If this is the first build for the project or if you wish to recreate everything, type build –cef.
You can pass parameters to build.exe so as to affect results in that invocation. Some useful parameters are:
Miscellany
Here are a few other tips for building drivers.
Start
Programs
Development
Kits
Windows DDK
3790
Help
DDK Documentation
You may find it handy to create a desktop shortcut for starting the online documentation.
When your driver won’t build
Start by looking at the command window output, which will make many problems clear, e.g., problems in a source-code file. Next, look at your log file! You will find it under whatever was in the current directory when you built. This file will often tell you all you need to know. Examine the error message(s) in it. If you have a compilation problem, look at the parameters with which build invoked the compiler (“cl”). If you have a link-edit problem, look at the parameters given to the linker (“link”) or the library manager (“lib”).
Here’s the explanation for a few common errors:
A browser file enables Visual Studio to show you where a variable, function, API, type or whatever was defined. You first get build to produce the browser file (see browser information). Then you set up a VS project for browsing (you don’t need to build in that project, although that is possible, too, as described below under the heading DDKBUILD):
With the VS project set up, double-click on a source file in the left pane to open the file in an editor window. Find something whose definition you want to locate, for example, PDEVICE_OBJECT, right-mouse-click on it and then click on Go To Definition. (Note: In VS .NET, you will sometimes have to choose Go To Reference rather than Go To Definition.) The editor will show the definition that build used. If you right-click on KDPC in the definition of PDEVICE_OBJECT, you will be taken to the definition of KDPC.
A tip for finding an OS-supplied definition before you’ve built your driver is this: find a DDK sample that uses the API or type or whatever. In the sources file for that sample, specify a browser file. Build the sample. Set up a VS project for the sample and find the definition. And note that you can utilize the same VS project over and over to track down definitions, simply by adding new source files from a built sample and updating the name of the browser file.
DDKBUILD, Visual Studio and other ways to build
You may wish to build in Visual Studio. To do this whilst adhering to the Microsoft-approved process, you can employ DDKBUILD. This is basically a front-end to DDK build, and you are going to get Visual Studio to employ DDKBUILD to do most of VS’s work. DDKBUILD consists of a .bat file written by Mark Roddy (get this at http://www.hollistech.com/Resources/ddkbuild/ddkbuild.htm. Read more about Mark’s thinking about building drivers here: http://www.wd-3.com/archive/HowBuild.htm). Here is Mark’s method.
Note: Where the VS project (.dsp) file is located is important! I warned you about this in browsable definitions, but let me repeat the point. The project file must be where the sources (or dirs) file is. Let’s suppose your driver directory is c:\temp\Driver1. You should name the VS project Driver1 and give its location as c:\Temp. In that case, the project file will be c:\Temp\Driver1\Driver1.dsp and so will be under the same directory as the sources file. Do not give the location as c:\Temp\Driver1, because then you would get c:\Temp\Driver1\Driver1\Driver1.dsp, which is in a different directory from the sources file.
Since the VS project must be a makefile, click on Makefile and OK. If this project is for an XP target, enter ddkbuild -XP checked . in the Debug command line; observe that a period is part of the command parameters. (The Output and Rebuild lines may be left as they are.) Click Next and enter ddkbuild -XP free . in the Release command line (again, observe the period). Click OK and finish.
You’ve built a driver in Visual Studio.
A difficulty with running DDKBUILD in Visual Studio is that you cannot readily change environmental variables to affect build. You can set such variables statically in Environment Variables in the Advanced tab under System in Control Panel. For more flexibility, put the invocation of DDKBUILD in another .bat file and specify that outer file on the command line when you create the project; the latter file would set some environment variables and then invoke DDKBUILD itself.
Here is a demonstration driver that you can download. It has source files, header files, a dirs file, sources files and so forth.
James Antognini is a software engineer in White Plains, New York, and a frequent contributor to online support forums. You can reach him at antognini@mindspring.com. You can find some of his work at http://home.mindspring.com/~antognini.