How to Use Annotations


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:

  • The calling function (that is, the caller) meets its obligations by correctly providing the required inputs.

  • The called function (that is, the callee) meets its obligations by correctly returning the expected results.

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:

  • The function does not provide enough information to prevent buffer overruns, which shows a potential bug that should be fixed, even before PREfast is run.

  • The annotation raises a design issue that should be resolved.

    A common example of this is whether a parameter is truly optional (that is, whether the parameter can be NULL).

  • The annotations for a function are hard to express, which can indicate that the function is poorly designed.

    Even if it is not possible to change the function in the short term, attempting to annotate the function identifies issues to fix in a future release.

image from book
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.

image from book

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

image from book

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

image from book
 __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    ); 
image from book

 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:

  • The __checkReturn, __drv_allocatesMem, __drv_when, and __drv_reportError annotations apply to the ExAllocatePoolWithTag function:

    • __checkReturn instructs PREfast to issue a warning if subsequent code ignores the function return value.

    • __drv_allocatesMem indicates that the function allocates memory-in this case, pool memory.

    • __drv_when specifies a conditional expression: If the function is called with one of the "must succeed" pool types, PREfast should display the error message specified by the __drv_reportError annotation.

  • The __bcount annotation applies to the PVOID function return value. This annotation indicates that the return value should be NumberOfBytes long.

  • The __in __drv_strictTypeMatch annotation applies to the PoolType parameter. This annotation indicates that the parameter can take only the types implied by __drv_typeExpr, which can be either literal constants or expressions that involve only operands of a specific type.

  • The other __in annotations apply to the NumberOfBytes and Tag parameters, respectively. These annotations indicate that PREfast should check that these parameters are valid on entering the function.

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

image from book
 __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 ); 
image from book

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

image from book
 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. } 
image from book

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

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

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:

  • The function must strictly match the type that the function typedef declares.

  • The function typedef declaration should have the required annotations. System-provided function typedef declarations already have these.

  • The declaration must precede the first mention of the function. If the function appears in a header file, the new declaration should precede-or simply replace-the mention in the header. If the first mention is the function definition itself, then the declaration should immediately precede the function definition.

  • The function definition is not required to be annotated because annotations that are applied to function typedef declarations are implicitly applied to functions and parameters of that type, the same as for any typedef.

  • Function typedef declarations can become difficult to read if you annotate each parameter individually. To make function typedef declarations more readable, consider whether to create and annotate a typedef for each parameter and then declare each parameter as the appropriate type in the function typedef declaration.

 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:

  • If a function has both a declaration (that is, a prototype) and a definition, annotate both with identical annotations. Function typedef declarations help to satisfy this requirement.

    If the annotations differ, PREfast issues a warning.

  • If a function has only a definition and does not have a separate prototype, annotate the definition.

    You do not need to create and annotate a separate prototype if you are only applying annotations and a prototype is not required for any other reason.

  • Place annotations that apply to an entire function immediately before the beginning of the function.

  • Place annotations that apply to a function parameter either inline immediately before the function parameter, or immediately before the beginning of the function, enclosed in a __drv_arg annotation that identifies the parameter to which the annotation applies.

  • Place annotations on typedef declarations to implicitly apply them to functions or parameters of that type.




Developing Drivers with the Microsoft Windows Driver Foundation
Developing Drivers with the Windows Driver Foundation (Pro Developer)
ISBN: 0735623740
EAN: 2147483647
Year: 2007
Pages: 224

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