Coding Conventions and Techniques

< BACK  NEXT >
[oR]

Writing a trusted kernel-mode component is not the same as writing an application program. This section presents some basic conventions and techniques to make it easier to code in this environment.

General Recommendations

First of all, here are some general guidelines to follow when writing a driver.

Avoid the use of assembly language in a driver. It makes the code hard to read, nonportable, and difficult to maintain. The C programming language is only a small step away from assembly language, anyway. Further, the HAL macros provide the only safe mechanism to access I/O device registers. Therefore, the use of assembly language in a driver should be extremely rare. Be sure to isolate such code into its own module.

For platform-specific code, provide a separate module, or at the very least, bracket it with #ifdef/#endif directives.

A driver should not be linked with the standard C runtime library. Besides being wasteful of memory space (each of 20 drivers should not include the same C runtime library support), some library routines are stateful or hold context information that is not thread-safe or driver-safe.

This particular guideline is perhaps the most uncomfortable aspect of writing device drivers. C programmers who live with their runtime environment day in and day out, often do not make a clear distinction between the C language and the C runtime library. The C runtime library requires initialization. It attempts to initialize a heap area and, in the case of C++, invoke constructors of global objects. All of these tasks interfere with proper driver operation.

Windows 2000 provides its own environmental support for kernel-mode code. This support includes RtlXxx functions (RunTime Library) to provide many of the common C runtime library services. Many of these routines are described later in this chapter.

Driver projects should be managed with source-code control. Microsoft Visual Source Safe is a popular choice. For large projects that span multiple platforms, ClearCase from Rational Software should also be considered.

Naming Conventions

All large software projects should adopt some standard naming convention for routines and data defined throughout the code. Device driver projects are no exception. Naming conventions improve the efficiency of development, debugging, testing, and maintenance of the driver.

Microsoft provides a naming convention for use with the DDK. A header file, NTDDK.h, defines all the data types, structures, constants, and macros used by base-level kernel-mode drivers. By DDK convention, all of these types of names are capitalized. Even native C-language data types are provided a corresponding DDK name. For example, the C data type void* is given the name PVOID by NTDDK.h. These definitions make future ports to 64-bit platforms easier.

Microsoft recommends that a driver-specific prefix be added to each of the standard driver routines. For example, if writing a mouse class driver, the Start I/O routine might be named MouseClassStartIo. Similarly a shorter two or three character prefix should be applied to internal names. This yields a name such as MouConfiguration. This recommendation is often, but not always, followed by driver authors.

Regardless of what convention is chosen for a driver project, it is important to establish a consistent way of naming entities within the entire project. It pays to spend a few hours making this decision early in the development life cycle.

Header Files

Besides including NTDDK.h or WDM.h, a driver should use private header files to hide various hardware and platform dependencies. For example, register access macros should be provided in a private header file. These macros should be surrounded by #ifdef compiler directives that allow for simple platform-to-platform porting. This technique, of course, solves the issue of register access differences between I/O space and memory space.

Even if portability were not a concern, register access macros make the driver easier to read and maintain. The following code fragment is an example of some hardware beautification macros for a parallel port device. The example assumes that some initialization code in the driver has put the address of the first device register in the PortBase field of the device extension.

 // // Define device registers as relative offsets // #define PAR_DATA      0 #define PAR_STATUS    1 #define PAR_CONTROL   2 // // Define access macros for registers. Each macro // Takes a pointer to a Device Extension // as an argument. // #deinfe ParWriteData( pDevExt, bData ) \ (WRITE_PORT_UCHAR(                    \      pDevExt->PortBase + PAR_DATA, bData ) )       #define ParReadStatus( pDevExt )    \ (READ_PORT_UCHAR(                  \      pDevExt->>PortBase + PAR_STATUS ))       #define ParWriteControl( pDevExt, bData )  \ (WRITE_PORT_UCHAR(                        \      pDevExt->PortBase + PAR_CONTROL, bData ) ) 

Status Return Values

The kernel-mode portion of Windows 2000 uses 32-bit status values to describe the outcome of a particular operation. The data type of these codes is NTSTATUS. There are three situations in which this status code is used.

  • When using any of the internal Windows 2000 functions, the success or failure of the call is reported by an NTSTATUS value.

  • When the I/O Manager calls a driver-supplied callback routine, that routine usually has to return an NTSTATUS value to the system.

  • After completing the processing of the I/O request, a driver must mark the IRP with an NTSTATUS value. This value ultimately is mapped onto a Win32 ERROR_XXX code. It is worth noting that NTSTATUS values and Win32 error codes are not identical. The I/O Manager provides a mapping between the two. A DDK topic describes how the mapping occurs and is often not logical.

NTSTATUS.h describes symbolic names for a large number of NTSTATUS values. These names all have the form STATUS_XXX, where XXX describes the actual status message. STATUS_SUCCESS, STATUS_NAME_EXISTS, and STATUS_INSUFFICIENT_RESOURCES are all examples of these names.

When a system routine that returns an NTSTATUS value is called, the DDK header file provides a convenient macro to test for the success or failure of the call. The following code fragment illustrates this technique:

 NTSTATUS status; : status = IoCreateDevice ( ... ); if ( !NT_SUCCESS( status )) {       // clean up and exit with failure       : } 

Always, always, always check the return value from any system routine called. Failure to follow this rule allows an error to propagate into other areas of the driver code and perhaps system code. Catching errors early is a cardinal rule of software engineering. (Of course, the examples supplied with this book are exempt from this rule for the sake of clarity.)

Windows 2000 Driver Support Routines

The I/O Manager and other kernel-mode components of Windows 2000 export a large number of support functions that a driver can call. The reference section of the DDK documentation describes these functions, and this book includes many examples of their use. For the moment, it's enough to point out that the support routines fall into specific categories based on the kernel module that exports them. Table 5.1 gives a brief overview of the kinds of support that each kernel module provides.

The ZwXxx functions require more explanation. These are actually an internal calling interface for all the NtXxx user-mode system services. The difference between the user and kernel-mode interfaces is that the ZwXxx functions don't perform any argument checking. Although there are a large number of these functions, the DDK reference material describes only a few of them. Use of undocumented functions is always a risk because Microsoft reserves the right to change or delete any of these functions at a future time.

Table 5.1. Driver Support Routine Categories
Windows 2000 Driver Support Routines
Category Supports Function names
Executive Memory allocation Interlocked queues Zones ExXxx()
Lookaside lists System worker threads
HAL Device register access Bus access HalXxx()
I/O Manager General driver support IoXxx()
Kernel Synchronization DPC KeXxx()
Memory Manager Virtual-to-physical mapping Memory allocation MmXxx()
Object Manager Handle management ObXxx()
Process Manager System thread management PsXxx()
Runtime library String manipulation Large integer arithmetic Registry access Security functions Time and date functions Queue and list support RtlXxx() (mostly)
Security Monitor Privilege checking Security descriptor functions SeXxx()
(Miscellaneous) Internal system services ZwXxx()

One final point to make life easier: The I/O Manager provides several convenience functions that are nothing more than wrappers around one or more lower-level calls to other kernel modules. These wrappers offer a simpler interface than their low-level counterparts, and should be used whenever possible.

Discarding Initialization Routines

Some compilers support the option of declaring certain functions as discardable. Functions in this category will disappear from memory after a driver has finished loading, making the driver smaller. If the development environment offers this feature, it should be used.

Good candidates for discardable functions are DriverEntry and any subroutines called only by DriverEntry. The following code fragment shows how to take advantage of discardable code in the Microsoft C environment:

 #ifdef ALLOC_PRAGMA #pragma alloc_text( init, DriverEntry ) #pragma alloc_text( init, FuncCalledByDriverEntry ) #pragma alloc_text( init, OtherFuncCalledByDriverEntry ) : #endif 

The alloc_text pragma must appear after the function name is declared, but before the function itself is defined so remember to prototype the function at the top of the code module (or better yet, in a suitable header file). Also, functions referenced in the pragma statement must be defined in the same compilation unit as the pragma.

Controlling Driver Paging

Nonpaged system memory is a precious resource. A driver can reduce the burden it places on nonpaged memory by defining appropriate routines in paged memory. Any function that executes only at PASSIVE_LEVEL IRQL can be paged. This includes Reinitialize routines, Unload and Shutdown routines, Dispatch routines, thread functions, and any helper functions running exclusively at PASSIVE_LEVEL IRQL. Once again, it is the alloc_text pragma that performs the declaration. An example follows.

 #ifdef ALLOC_PRAGMA #pragma alloc_text( page, Unload ) #pragma alloc_text( page, Shutdown ) #pragma alloc_text( page, DispatchRead ) #pragma alloc_text( page, DispatchHelper ) : #endif 

Finally, if the entire driver is seldom used, it can be temporarily paged out. The system routine MmPageEntireDriver overrides a driver's declared memory management attributes and makes the entire module temporarily paged. This function should be called at the end of the DriverEntry routine and from the Dispatch routine for IRP_MJ_CLOSE when there are no more open handles to any of its devices. Be sure to call MmResetDriverPaging from the IRP_MJ_CREATE Dispatch routine to ensure that the driver's page attributes revert to normal while the driver is in use.

When using this technique, beware of the inherent dangers. First, make sure there are no IRPs being processed by high-IRQL portions of the driver before calling MmPageEntireDriver. Second, be certain that no device interrupts arrive while the driver's ISR is paged.

< BACK  NEXT >


The Windows 2000 Device Driver Book(c) A Guide for Programmers
The Windows 2000 Device Driver Book: A Guide for Programmers (2nd Edition)
ISBN: 0130204315
EAN: 2147483647
Year: 2000
Pages: 156

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net