Developing Drivers with the Windows Driver Foundation (Pro Developer)

Annotations in source code significantly enhance the ability of PREfast to detect potential bugs while lowering the rate of false positives and false negatives. For example, if an annotation is added to indicate that a parameter represents a buffer of a particular size, PREfast can check for usage that would cause a buffer overrun. Annotations can be applied to functions as a whole, to individual function parameters, and to typedef declarations.

Annotations do not interfere with normal compilation on any compiler because the annotation system for PREfast uses macros. When PREfast runs, these macros expand into meaningful definitions. When the code is compiled normally, these macros expand to nothing, yielding the original unmodified program. Annotations are visible only to static analysis tools such as PREfast and to human readers, who often find them highly informative.

How Annotations Improve PREfast Results

Annotations can provide PREfast with information about global state and work that is performed outside the function, plus specific information about the roles and possible values of function parameters. This information makes it possible for PREfast to analyze code more accurately, with significantly fewer false positives and false negatives.

Annotations Extend Function Prototypes

Prototypes prevent many errors by establishing the type and number of function parameters, so that an incorrect call can be detected at compile time. However, prototypes do not provide enough information about the intended use of a function for a tool such as PREfast to identify or eliminate possible errors.

For example, C passes parameters by value, so the parameters themselves are always input parameters to a function. However, C can pass a pointer by value, so it is not possible to tell just by looking at a function prototype whether a passed value is intended as input to the function, output from the function, or both. PREfast cannot determine whether the parameter should be initialized before the call, and it can only flag an uninitialized parameter as a potential problem. Annotations help to clarify the intended purpose of function parameters.

Annotations Describe the Contract between a Function and its Caller

Annotations are like the clauses in a contract. As in any contract, both sides have obligations and expected results. For this kind of contract:

When checking the contract of a function call, PREfast checks that the caller has met its obligations and, for subsequent analysis, assumes that the callee has done its part correctly.

When checking the contract of a function body, PREfast does the opposite: it assumes that the caller has met its obligations and checks that the callee has done its part correctly. To the extent that annotations accurately represent the obligations of the caller and the callee, PREfast can check many interfunction relationships that would initially seem to be beyond the capabilities of a tool that analyzes one function at a time.

Annotations Help to Refine a Function's Design

The task of applying annotations to a well-designed function is a straightforward activity. In contrast, if a function's design is flawed or incomplete, applying annotations can help uncover significant issues for resolution early in development, as in the following examples:

Annotations Are Like the Notes on a Blueprint

Another way to think about source code annotations is to compare them to the annotations on a blueprint or other specification for a physical part, such as the mouse part shown in the following figure.

Just as a good design for a part includes all the sizes and the tolerances so that many different products can use the same part in their designs, a clear and testable specification for the behavior of a function makes it possible to reuse the function with a high degree of confidence that it is being used correctly. This has two benefits: it helps assure the correctness of the software that is using the function and it helps with function reuse, reducing the amount of nearly duplicate software that is written because the function specification is incomplete.

Annotations for PREf ast might add some visual clutter to a simple picture of your code, but the picture is not accurate without them, just as the annotations on the blueprint of the mouse part reduce the artistic purity of the drawing but are essential to successfully manufacture the part.-Donn Terry, PREfast for Drivers Team, Microsoft

Where to Place Annotations in Code

Typically, annotations are applied to functions and their parameters. They can also be applied to typedef declarations, including declarations of function types.

Annotations on Functions and Function Parameters

Generally, annotations that apply to an entire function should be placed immediately before the beginning of the function definition. Annotations that apply to a function parameter can be placed either inline with the parameter or enclosed in a __drv_arg annotation before the beginning of the function.

The example in Listing 23-4 shows the placement of various general-purpose and driver annotations. These annotations will be explained in more detail later in this chapter. This example is intended to show where annotations can appear rather than what they do.

Listing 23-4: Placement of PREfast annotations on a function

__checkReturn __drv_allocatesMem(Pool) __drv_when(PoolType&0x1f==2 || PoolType&0x1f==6, __drv_reportError("Must succeed pool allocations are" "forbidden. Allocation failures cause a system crash")) __bcount(NumberOfBytes) PVOID ExAllocatePoolWithTag( __in __drv_strictTypeMatch(__drv_typeExpr) POOL_TYPE PoolType, __in SIZE_T NumberOfBytes, __in ULONG Tag );

 Note  In this example and many of the examples in the rest of this chapter, annotations under discussion in source code examples are formatted in bold type to distinguish them from the surrounding code.

In this example:

Typically, simple annotations such as __in are more readable when applied to parameters, but placing more complicated annotations inline can make code difficult to read. As an alternative to placing parameter annotations inline, you can enclose parameter annotations in __drv_arg annotations and place them with the other annotations before the start of the function, to help improve the readability of more complicated annotations.

For example, Listing 23-5 shows the ExAllocatePoolWithTag function with annotations on the PoolType parameter enclosed in a __drv_arg annotation at the beginning of the function, instead of inline.

Listing 23-5: Alternative placement of PREfast annotations on function parameters

__checkReturn __drv_allocatesMem(Pool) __drv_when(PoolType&0x1f==2 || PoolType&0x1f==6, __drv_reportError("Must succeed pool allocations are" "forbidden. Allocation failures cause a system crash")) __drv_arg(PoolType, __in __drv_strictTypeMatch(__drv_typeExpr)) __bcount(NumberOfBytes) PVOID ExAllocatePoolWithTag( POOL_TYPE PoolType, __in SIZE_T NumberOfBytes, __in ULONG Tag );

See "General-Purpose Annotations" later in this chapter for more information about __checkReturn, __bcount, and __in.

See "Driver Annotations" later in this chapter for more information about __drv_arg, __drv_allocatesMem, __drv_when, __drv_reportError, and __drv_strictTypeMatch.

Annotations on typedef Declarations

Annotations that are applied to typedef declarations are implicitly applied to functions and parameters of that type. If you apply annotations to a typedef declaration-including function typedef declarations, you do not need to apply annotations to uses of that type. PREfast interprets annotations on typedef declarations in the same way as it interprets annotations on functions.

The use of annotations on typedef declarations is both more convenient and safer than annotating each individual function parameter of a given type. For example, consider a function that takes a null-terminated string as a parameter. In the C programming language, there is no difference between an array of characters and a string, other than the programmer's assumption that the string is null terminated. However, the semantics of many functions rely on the knowledge or assumption that a particular array of characters is null terminated and is thus semantically a string. If PREfast "knows" that an array of char or wchar_t is intended to be a string, it can perform additional checks on the array to ensure that it is properly null terminated. The primitive annotation that expresses this is __nullterminated.

In principle, you could explicitly apply __nullterminated to every function parameter that takes a string as a parameter, as in the following example:

size_t strlen(__nullterminated const char *s);

However, that quickly becomes tedious and error prone. Instead, if you declare a string parameter as a type that is already annotated with __nullterminated, then you are not required to explicitly annotate all of the functions that use strings to ensure that their parameters are null terminated. The following typedef declaration for PCSTR is an example:

typedef __nullterminated const char *LPCSTR, *PCSTR;

The function declaration becomes much simpler, as shown in the following example:

size_t strlen(PCSTR s);

In this example, PCSTR s implies that s is null terminated because the PCSTR type is annotated with __nullterminated. This declaration is easier to read than the previous example and expresses the intended use of the parameter more clearly to the programmer.

You should always use PCSTR or similar string types for strings in the functions you define. Use character types such as PCHAR only for strings that are not null terminated. If you use PCSTR and similar types for strings, you get the benefits of annotation without being required to explicitly apply them and it is easy to distinguish a function that takes a string from a function that takes an array of bytes as 8-bit numbers. However, if you use PCSTR or similar types to describe an array of bytes that might not be null terminated, the implicit __nullterminated annotation on the string type causes PREfast to issue a false positive.

The functions in Listing 23-6 show the difference between a string and an array of bytes. FindChar relies on the PSTR parameter type's implicit guarantee that the string is null terminated. The FindChar function cannot find zero as a character in the body of the string. A more realistic example would use annotations such as __drv_when and __drv_reportError to indicate that c must not be zero. See these annotations later in this chapter for details.

Listing 23-6: Two functions that show the difference between a string and an array of bytes

PSTR FindChar(__in PSTR str, __in char c) { // str ends in '\0', so stop when we see a 0. // Length is not needed. // c cannot be 0, because it will never match. } PCHAR * FindByte(__in PCHAR *arr, long len, __in char b) { // We have no idea if the byte after the end of the buffer happens // to be zero or not: we simply have to believe the length and quit // after looking at len bytes. // b could be zero: zero is just like any other value. }

FindByte relies on the PCHAR parameter type's implicit guarantee that zero is not special in arr and that len defines the length of the array to search, so binary zero is a valid value to search for. When PREfast analyzes the FindChar function, it checks whether str is missing the required null terminator because the PSTR parameter type specifies that the buffer at str is intended as a string rather than an array.

Annotations on Function Typedef Declarations

In C and C++, it is possible to declare a function type by using typedef. This is distinct from a function pointer type and historically has not been used very much in C code. However, with the addition of annotations and function type classes, which are described later in this chapter, function types become very useful.

Inside Out 

Although function typedef declarations might look unfamiliar, they are valid standard C-that is, they are correct and compilable. For example, Listing 23-7 shows the DRIVER_STARTIO function typedef declaration, which is defined in %wdk%\inc\ddk\Wdm.h.

Listing 23-7: DRIVER_STARTIO function typedef declaration

typedef VOID DRIVER_STARTIO ( __in struct _DEVICE_OBJECT *DeviceObject, __in struct _IRP *Irp ); typedef DRIVER_STARTIO *PDRIVER_STARTIO;

It's important to remember that DRIVER_STARTIO defines a function typedef, not a function pointer typedef. This function typedef declaration is used to declare that MyStartIo is a function of type DRIVER_STARTIO. A function that is declared with DRIVER_STARTIO is assignment-compatible with the familiar PDRIVER_STARTIO function pointer-that is, a pointer to one can be assigned to a pointer to the other.

Note also that the function parameters in DRIVER_STARTIO are already annotated with __in, which identifies them as input parameters. These annotations are implicit in the typedef declaration, so you are not required to annotate these parameters in your MyStartIo function unless you prefer, for readability.

If your MyStartIo function is intended to be a WDM StartIo function, you would declare MyStartIo as a function of type DRIVER_STARTIO by placing the following declaration before the first use of MyStartIo in your driver:

DRIVER_STARTIO MyStartIo;

Inside Out 

In addition to declaring MyStartIo as a function of the type DRIVER_STARTIO, this declaration applies to MyStartIo all of the system-supplied annotations on the DRIVER_STARTIO type as defined in %wdk%\inc\Wdm.h.

You are not required to follow the function typedef declaration with a full prototype. Many developers find that the function typedef declaration alone is more readable. You would implement the MyStartIo function body (that is, the function definition) in the same way as any other function. See the Toaster sample driver in the WDK at %wdk%\src\general\toaster\func\shared\toaster.h for an example of a driver that uses function typedef declarations and omits the prototypes.

The function typedef declaration is useful in another way: it tells other programmers that the function is intended to be an actual StartIo function, rather than just looking like one. For example, Cancel functions are assignment-compatible with StartIo functions, so a poorly chosen function name can lead to ambiguities in the source code.

You can also use the __drv_functionClass annotation to indicate to PREfast that a function type belongs to a particular function type class. This significantly increases the checking that PREfast can do because PREfast understands that this function is a callback and knows the specific contract it must meet. See "Function Type Class Annotations" later in this chapter for details.

When you use function typedef declarations, remember the following:

 Note  Driver function typedef declarations such as DRIVER_STARTIO are intended for use in drivers written in C. See the PREfast topic page on the WHDC Web site for an up-to-date list of white papers, tips, and resources for implementing PREfast in your testing practices.

Tips for Placing Annotations in Source Code

Here are some tips for placing annotations in source code:

Категории