EN JP CN

Tuning C/C++ analysis

Tuning C/C++ analysis

Tuning allows you to increase the accuracy of the Klocwork analysis by finding more real defects, or reducing the number of false positives and uninteresting issues. There are two ways of looking at tuning: changing the code that Klocwork sees during analysis, or changing Klocwork's understanding of the code.

How do you tune Klocwork?

This article describes three common methods of tuning Klocwork analysis:

  • Using the __KLOCWORK__ macro takes advantage of conditional compilation during Klocwork analysis to supply an alternative version of source code for the compiler.
  • Overriding macro definitions with the #kw_override preprocessor directive suppresses or replaces the macros Klocwork sees in your code. This tuning method is slightly more restrictive than the __KLOCWORK__ macro in terms of compiler-specific modifications, but it doesn't require changes to your source code.
  • Using knowledge base records tunes specific functions in your code, so that Klocwork understands the behavior of the code differently. In this method, which is the most flexible way of tuning, you can create knowledge base records that replace Klocwork's automatically generated records, or provide new models if source code is not available.

The walk-through examples in this article describe typical scenarios that show how to use these tuning methods.

Walk-through 1: Handling NPD false positives with the __KLOCWORK__ macro

A typical use of the __KLOCWORK__ macro is in handling NPD false positives caused by custom asserts.

Assertions

Assertions are widely used in C/C++ code to implement validity checks and the pre- and post-conditions of a function. For example, if a function expects its argument to be a non-NULL pointer, the code typically looks like this:

1  void my_function(my_data_t *ptr)
2  {
3     MY_ASSERT(ptr != NULL);
4     char *name = ptr->name;
5  }

It's important for a static analysis tool to recognize assertions in the code and handle them properly, as a condition check. For example, if MY_ASSERT in the above code sample is not recognized as "check ptr for NULL", Klocwork would report a NULL pointer dereference issue at line 4 (a false positive).

Although the standard C/C++ library provides a default implementation of assert() in assert.h (typically a macro that checks the condition and aborts on failure), it is common practice to define a custom ASSERT macro, to provide:

  • better diagnostics-the standard version typically prints a failed condition, filename, and line number; a custom assert could possibly add the function or module name to the error message, or report the error through some logging system to file.
  • more flexibility with respect to build configuration-the debug version of the product could define assert to implement extended error reporting or automatically start a debugger, whereas the release version of assert could expand into an empty statement to eliminate the overhead of extra sanity checks.

Using the __KLOCWORK__ macro

Adding conditionally compiled code to your source is a very familiar way of dealing with differing environments. The __KLOCWORK__ macro, which is always defined by the Klocwork compiler, allows certain code to be active only when Klocwork is working and is otherwise ignored. Adding an alternative definition of an assert-type macro in a __KLOCWORK__ block can be non-intrusive and simple to manage.

For example, assume we have the following code:

1   MY_ASSERT(a!=NULL);
2   use(a->mm);

If Klocwork incorrectly processes your production definition of MY_ASSERT so that it doesn't understand the abort condition, it may report a false positive NPD on line 2. You can reduce the probability of this happening by providing Klocwork with a simpler, more standard, definition of your custom assert:

1   #ifdef __KLOCWORK__
2   # define MY_ASSERT(x) do { if (!(x)) abort(); } while (0)
3   #else
4   # define MY_ASSERT(x) ... production, advanced definition of assert ...
5   #endif

Walk-through 2: Handling NPD false positives with #kw_override

The __KLOCWORK__ macro supports generic code replacement using standard preprocessor mechanisms, and is very powerful, but it still means that you have to modify your source code specifically for Klocwork. If the modification you need to make to your code is localized to one or more macro definitions, you can use an override file and avoid source modification.

An override file is a header file that you import into your Klocwork project. The Klocwork compiler automatically includes and processes your override file with every source module built for the project. In the file, the Klocwork-specific preprocessor directive, #kw_override, is used to change the definition of macros in your source.

Assume that in your production source you have this macro definition:

# define MY_ASSERT(x) (void) ((x) __handle_failed_assert(#x, __FILE__, __LINE__, __FUNCTION__))

If you want the compiler to see a simpler, more direct assertion, you can create an override file containing the following definition:

# kw_override MY_ASSERT(x) do { if( !(x) ) abort(); } while(0) 

When you import this override file into your Klocwork project and build the project, a source function such as:

1   void myFunction(char *aString)
2   {
3     MY_ASSERT(aString); 
4   }

is expanded by the Klocwork compiler to the Klocwork-specific definition of MY_ASSERT:

1    void myFunction(char* aString)
2    {
3      do { if( (!aString) ) abort(); } while(0);
4    }

while your production compiler continues to expand the macro using the original definition.

The #kw_override directive affects only macros, and only those macros that are defined in your production source.

To apply the override file to the integration build analysis, import it into a project or the projects_root directory. To apply the override file to your standalone desktop, use kwcheck import.

Walk-through 3: Tuning with knowledge base records

Klocwork automatically generates a knowledge base during the first analysis of your source code, and updates this information with every new analysis. Native system function behavior is specified in the default knowledge bases. To handle false positives and negatives, you can fine-tune your results by creating your own knowledge base records to change the way Klocwork understands your code.

In this walk-through, we show four examples of tuning with knowledge base records, and explain how to implement the addition of knowledge base records:

  • Example 1: Reducing false positives with ALLOC ignore
  • Example 2: Tuning memory leak issues with XMRF
  • Example 3: Tuning the NPD checker with NORET
  • Example 4: Tuning memory leaks with ALLOC
  • Implementing additions to the knowledge base

For details of knowledge base record syntax, see C/C++ knowledge base reference.

Example 1: Reducing false positives with ALLOC ignore

There are times when you may want to avoid issue reports on possible memory allocation functions. For example, in the following snippet, the Klocwork memory leak checker, MLK, would normally report a memory leak for 'p' at line 9.

1   void* C::custom_alloc(unsigned int n)
2   {
3       return malloc(n);
4   }
5
6   void C::foo()
7   {
8       void* p = custom_alloc(10);
9       return;
10  }

You can suppress the finding by adding an ALLOC ignore knowledge base record to the analysis:

    C::custom_alloc - ALLOC ignore

This record specifies a fully-qualified function name, the function key (always a hyphen, '-'), the record kind (ALLOC in this case), and the specification for this knowledge base record, 'ignore'.

As a result of the new record, Klocwork assumes that custom_alloc isn't a memory-allocating function, and no report is issued.

Example 2: Tuning memory leak issues with XMRF

By default, the memory leak checker assumes that if a pointer to allocated memory is passed to an unknown function, that pointer will probably be modified in some way (aliased, stored in a shared pool, or released), and so stops tracking it.

This behavior can be modified with an XMRF record, which you can use to specify whether the memory ownership will be retained. If memory ownership is retained, tracking continues, but if memory ownership is not retained, the engine assumes the called function is taking responsibility for the memory and stops tracking it.

In the following snippet, Klocwork would normally stop tracking 'p' after line 8, and no report would occur at line 9:

1    // some library function
2    // does not take ownership or free memory pointed to by 'ptr'
3    void process_ptr(void*);
4
5    void foo()
6    {
7       void* p = malloc(100);
8       process_ptr(p);         // Klocwork assumes that ownership of 'p' is not retained
9    }                          // FN: no memory leak reported here

To avoid this false negative, you can add a memory retention (XMRF) record to the analysis:

    process_ptr - XMRF $1 : 1

This record informs the checker that the calling function retains ownership of the memory passed to 'process_ptr' as its first argument ($1). The retention flag can be either 0 to signal that the caller does not retain ownership, or 1 to show that the caller does retain ownership. When this record is applied to the analysis of the sample code snippet, the checker keeps tracking pointer 'p' after line 8, and correctly reports a memory leak at line 9.

Conversely, when a function parameter is a qualified const, or 'this' for a non-static class method, the MLK checker continues to track a pointer to allocated memory even if it's passed to a function that the checker considers unknown. These situations can result in false positives, as shown by the following two snippets:

1    // Collect 'ptr' for pool management
2    extern int pool_ptr(const void* ptr);
3
4    int foo()
5    {
6      void* p = malloc(100);
7      int res = pool_ptr(p);   //"const" heuristics: keep tracking 'p'
8      return res;              // FP: memory leak reported
9    }
1    class C
2    {
3    public:
4      // Collect 'this' for pool management
5      int pool_self();
6    };
7
8    int bar()
9    {
10     C* c = new C();
11     int res = c->pool_self();  // 'this' heuristics: keep tracking 'c'
12     return res;                // FP: memory leak reported
13   }

In these cases, the following XMRF records can be added to the analysis to avoid the false positive reports:

    pool_ptr - XMRF $1 : 0
    C::pool_self - XMRF $0 : 0

These entries tell the checker that memory ownership is not retained by the caller and shouldn't be tracked afterwards.

Example 3: Tuning the NPD checker with NORET

The NULL pointer dereference checker, NPD, may produce false positives if it doesn't know that a function call never returns. For example, it is typical for programs to use error severity levels to determine a response in a reporting function, such as an abort or longjmp() away from the current context. Lacking that knowledge, the following code snippet would normally cause an NPD report detailing a check for NULL at line 15 followed by a dereference at line 17:

1   class C
2   {
3   public:
4     static C* findInstance(const char* name);
5     virtual void run();
6   };
7
8   // Error handling, abort() on ERROR or FATAL
9   enum { INFO, ERROR, FATAL };
10  void error_msg(int level, const char *msg);
11
12  void foo()
13  {
14    C *ptr = C::findInstance("default");
15    if( !ptr )
16        error_msg(FATAL, "Can't find default object");
17    ptr->run();
18  }

To avoid false positives when the error_msg function reports a FATAL error and then always exits, you could use the following NORET record:

    error_msg - NORET

However, since error_msg in this code exits only when it is called with the first argument greater than 1 (ERROR or FATAL), NORET is too general for this situation and you should use CONDNORET instead:

    error_msg - CONDNORET $1 > 1

Example 4: Tuning memory leaks with ALLOC

The memory allocation record ALLOC allows you to specify how, and under what conditions, memory is allocated if you don't make use of typical system primitives.

Specifically, ALLOC allows you to define that a function:

  • allocates new memory under conditions affected by its parameters (preconditions)
  • stores the allocated address in a particular parameter or result (returned address)
  • returns particular values if memory allocation was successful (postconditions)

Depending on how much it is able to determine about 'get_buf' and the inter-relationship of the return code and a successful allocation, Klocwork might report a false positive memory leak at line 10 in the following code snippet:

1   // get_buf allocates memory and returns 0 on success
2   extern int get_buf(void** buf);
3
4   void process_data()
5   {
6     void* buffer;
7     if( !get_buf(&buffer) )
8        free(buffer);
9     else
10       fprintf(stderr, "operation failed!\n");
11  }

To eliminate such a report, you can define an ALLOC knowledge base record:

    get_buf - ALLOC stdc : 1 : *$1 : $$ EQ(0)

This record states that:

  • the allocation belongs to the 'stdc' group, and can be expected to behave like 'malloc' and related functions in terms of being a natural source for 'free'
  • there is no precondition (1), the allocation is always attempted
  • when the allocation is successful, the pointer to the newly allocated memory is stored in the address pointed to by the first parameter (*$1)
  • the allocation is successful only if the return code is 0 ($$ EQ 0)

Implementing additions to the knowledge base

To implement your knowledge base records, you need to create a knowledge base file and apply the file to the Klocwork analysis.

Creating a knowledge base file:

To create a knowledge base file:

  1. Create a new text file with the extension .kb.
  2. Using the editor of your choice, add records as required, with each record occupying one line of the file.
  3. Make sure you're overriding the generated knowledge base exactly as you intend. Your new .kb file records will override all the records in the generated .kb with the same record name for that particular function, and only those records. For example, the CONDNORET record shown in the following sample file will override any and every CONDNORET record in the generated .kb file for the error_msg function, but won't affect any NORET, BO, CHECKRET, or any other generated record.
Here is an example of a .kb file containing the records from the four examples in this article:
    C::custom_alloc - ALLOC ignore
    process_ptr - XMRF $1 : 1
    pool_ptr - XMRF $1 : 0
    C::pool_self - XMRF $0 : 0
    error_msg - CONDNORET $1 > 1
    get_buf - ALLOC stdc: 1 : *$1 : $$EQ(0)
If you're not sure of the effect your new record will have, copy all the records of the same name for your function in the generated file that you don't want overridden into your new .kb file.

Applying the file to your analysis:

To apply your new configuration file to the integration build analysis, import it into a project or the projects_root. For example:

    kwadmin import-config <projectname> <file> 

The file will be synchronized to connected desktops.

To apply your knowledge base file to a standalone desktop project, see Customizing your desktop analysis.

Run a full build analysis

For your tuning changes to take effect, you need to run a full build analysis.