Advanced features of IBM Rational Purify: Debugging with Purify

Rational Purify APIs and memory watch points help you find and debug memory errors quickly

IBM® Rational® Purify® is a tool to accurately detect memory corruption errors, which are otherwise very difficult to analyze and fix. It monitors and analyzes how a program is using memory and reports errors with source code details that pinpoint the cause and location of the error. In this article, you will learn how to use Rational Purify APIs and watch points with any debugger to analyze memory errors proficiently.

Satish Chandra Gupta (satish.gupta@acm.org), Senior Software Engineer, IBM

Satish Chandra GuptaSatish Chandra Gupta is a programmer and loves building software engineering and programming tools. His interests include performance (CPU time, memory, energy) profiling tools, compilers, programming languages, type theory, software engineering, and software development environments. While at IBM, he was architect for UML Action Language tooling in Rational Software Architect, and tech lead for Rational PurifyPlus on AIX and Java leak detection tooling in Rational Application Developer. You can keep up with him through his Twitter and Google+ feeds.


developerWorks Contributing author
        level

Anand Gaurav (anand.gaurav@in.ibm.com), Programmer, PurifyPlus, IBM Japan

Anand GauravAnand Gaurav is a developer in the IBM Rational PurifyPlus group in Bangalore, India. His interests are in the areas of runtime analysis, object-oriented design, data structures, and algorithms. He earned his B.E from PESIT (VTU), Karnataka, India.



05 February 2008

Also available in Chinese

Memory errors are one of the most difficult classes of software errors to analyze and fix, because the source of memory corruption and the manifestation of the error are far apart, making it hard to correlate the cause and the effect. Moreover, the errors often occur in exceptional conditions, making it hard to reproduce them consistently. Typically, these errors result from complex interactions between different components of your program, third-party libraries, and the operating system. It is extremely hard to predict and comprehend these possibilities and scenarios just by inspecting source code. Often, it requires a considerable amount of debugging and investigation to understand mistakes in your program logic and design that are causing memory errors.

IBM® Rational® Purify® is an advanced memory-debugging tool to assist you in quickly locating the cause of memory corruption errors accurately. To use the tool, you first use Purity to instrument your program. Then, when you run the instrumented program, Purify scrutinizes memory accesses and manipulations by your program and identifies memory errors that are about to happen. This considerably reduces the debugging time and complexity.

You can learn about various types of memory errors and how to use Purify to detect them by reading this IBM developerWorks article: Navigating "C" in a "leaky" boat? Try Purify. If you are familiar with Purify and memory errors, you can either skim or skip that article.

Purify has several unique and advanced features to assist you in debugging memory errors. In this article, you will first learn about application programming interfaces (APIs) in Purify and how to use them from the debugger. Then you will learn about APIs specific to memory watch points.

Application programming interface

Purify provides various APIs that you can invoke from your program or debugger to further assist you in debugging memory problems. For example, you can use the purify_what_colors function to find out the status of a memory range:

int purify_what_colors (char *addr, unsigned int size);

Purify keeps track of the status of every byte of memory used by your program and uses four colors to represent the status: red, yellow, green, and blue. Initially, all memory is red, which represents unallocated and uninitialized memory. After you allocate memory, it becomes yellow, which represents allocated but uninitialized memory. After you initialize a memory location, it becomes green, representing allocated and initialized memory. And when you free memory, it becomes blue, representing previously allocated but then freed memory. This lifecycle is shown in Figure 1.

  • It is legal to read from or write to memory marked green.
  • It is legal to write to yellow memory, but illegal to read from it. Purify reports a Uninitialized Memory Read (UMR) error when you do.
  • It is illegal to read or write blue and red memory.

While debugging a memory corruption, you can call the purify_what_colors API from the debugger to see the Purify color state for an interesting memory location. This example shows the color state for the bytes of an eight-byte buffer, where only the first two have been initialized:

(gdb) print purify_what_colors(buf, sizeof(buf)+1) 
color codes of 9 bytes at 0xffffe820: GGYYYYYYR

The API prints out the memory state of sizeof(buf)+1 bytes, starting at memory address buf. The memory state of each byte of memory is represented by one of these letters R, Y, G, or B. These letters correspond to the colors red, yellow, green, and blue, respectively.

Figure 1. Lifecycle of memory locations
screen capture

Using Rational Purify with the debugger

Most of the time, Purify provides enough information about a memory error for you to identify the cause and fix it. But sometimes, you may need to use that information as a starting point and debug the program to find the cause. For example, let's say that Purify reports a UMR error that surprises you because you do see a statement in your program that initializes that memory. Clearly, there exists a control path in which either the initialization statement is not executed or, in the case of a pointer to a memory buffer, the pointer is being reassigned to another memory buffer that may not have been initialized yet.

In such situations, you need to debug your program to find the exact cause. The good news is that instead of debugging your program, you can debug the Purify-instrumented program. Purify issues memory error reports just before the error is about to happen. That allows you to examine and analyze relevant variables and memory contents in a debugger. Purify also provides APIs for examining the status of memory locations.

There are two ways that you can engage a debugger for a Purify-instrumented program:

  • First, start the instrumented program under the debugger and put a breakpoint at purify_stop_here Purify API function. The debugger will stop at every Purify error message:
    (gdb) break purify_stop_here
    (dbx) stop in purify_stop_here
    (xdb) b purify_stop_here
  • Alternatively you can configure just-in-time (JIT) debugging through the Options > JIT Debug menu in the Purify GUI (see Figure 2), and select the error types of interest. Whenever a Purify error of the selected type is reported, Purify invokes the debugger and attaches it to your running application.
Figure 2. Purify JIT debugger dialog
screen capture

When you are inside a debugger, you can use various Purify API functions to investigate the status and type of various memory locations:

  • purify_what_colors(char *addr, unsigned int size):
    Prints the memory state of size bytes starting at memory address (addr), as explained previously in the Application programming interface section.
  • purify_describe(void *addr):
    Prints specific details about the memory at location addr, including its location (stack, heap, text) and, if it is heap memory, then the call chains of its allocation and free history.
  • purify_assert_is_readable(const char *addr, int size):
    Simulates reading size bytes starting at address addr, generates any Purify errors that read would cause, and calls purify_stop_here upon error. Returns 0 if errors are detected, and returns 1 if no errors are detected.
  • purify_assert_is_writable(const char *addr, int size):
    Simulates writing size bytes starting at address addr, generates any Purify errors that write would cause, and calls purify_stop_here upon error. Returns 0 if errors are detected, and returns 1 if no errors are detected.

While debugging, you may want to focus on some piece of code, thus not be interested in Purify errors that are reported before the program control reaches that piece of code. Purify provides APIs to turn error reporting off or on (notice that memory use monitoring is not turned off):

  1. Using a debugger, put a breakpoint at the main function (or at the program location where you want to turn off Purify error reporting), and run the instrumented program.
  2. When the debugger stops at that breakpoint, type this command:
    (gdb) print purify_stop_checking()
  3. Put a breakpoint at the program location where you want to resume Purify error reporting, and continue running the program.
  4. When the debugger stops at that breakpoint, type this command:
    (gdb) print purify_start_checking()

Memory watch points

Purify offers a wide set of memory watch point APIs that you can call from the debugger to assist you in debugging the memory corruption problems in your program. The code shown in Listing 1 shows both a memory leak and a dangling pointer.

Listing 1. Code with memory leak and dangling pointer (mem_errors.c)
 1  #include <stdio.h>
 2  
 3  char *namestr;
 4  
 5  void foo() {
 6      namestr = (char *) strdup("Rational PurifyPlus");
 7      printf("Product = %s\n", namestr);
 8      free(namestr); /* free the memory allocated by strdup */
 9  }
10  
11  void main() {
12      namestr = (char *)malloc(20 * sizeof(char));
13      foo();
14      strcpy(namestr, "IBM");
15      printf("Company = %s\n", namestr);
16      free(namestr);
17  }

Interestingly, if you look at the method main() or foo() independently, both functions look correct. The method main() allocates memory, calls foo(), uses the allocated memory, and then frees it and exits. The method foo() calls method strdup(), which allocates memory, uses that memory, and then frees it. However, it is the interaction of these two functions and using a global pointer variable called namestr that causes both the leak and the dangling pointer. When strdup() is called in foo(), the namestr variable value is overwritten, thereby losing the pointer to the memory allocated in main(), and that causes the leak. In main, after returning from foo(), namestr is actually a dangling pointer, because foo() has freed that memory before returning.

It is easy to spot the problems in this simple example by inspecting the code. But that is not possible in large programs with complex control flows where the problematic functions could be in different libraries. That is when Purify and its memory watch point API becomes handy.

Here is how you can purify this program:

$ purify cc -g mem_errors.c -o mem_errors.pure

When you run the purified program, Purify will report the following errors (also see Figure 3):

  • MLK (Memory Leak) for the memory allocated for namestr in the function main
  • FMW (Free Memory Write) at the strcpy() call in the function main
  • FMR (Free Memory Read)at the printf() call in the function main
  • FUM (Freeing Unallocated Memory) at the free() call in the function main
Figure 3. Memory errors reported by Purify in mem_errors.c
screen capture

For this small example, the programming mistake can be easily fixed by using information provided along with Purify errors. But for a complex program, where a function such as foo() might be called from various locations and possibly in a loop, debugging the program by using the Purify memory watch point APIs will be very useful.

The watch point feature lets you ask Purify to pay special attention to an area of memory and issue a report any time that memory gets read (WPR: Watch Point Read), written (WPW: Watch Point Write), or freed (WPF: Watch Point Free). This way, you can answer questions such as, "Where in the program does this variable get written?" or "Where does this variable get used?" And for memory in the heap, "Which function frees this memory?"

Here is how you can purify your program and run it under a debugger (gdb is used here to illustrate the process, but you can use any of your favorite debuggers):

$ purify cc -g mem_errors.c -o mem_errors.pure
$ gdb mem_errors.pure

Looking at Purify reporting memory leaks and FMR, FMW, or FUM, there are several interesting questions that you may want to ask:

  • Given that the namestr variable is a pointer to memory allocated in main(), why does namestrstart pointing to some other memory?
  • Where exactly does namestr get written with another address value, and thereby lose the last pointer to memory allocated in main(), causing the leak?
  • After the memory allocation in main(), where is the namestr variable used and overwritten?

You can answer these questions by putting a breakpoint just after the malloc call in the main()function (Line 13), and then using Purify to set a memory watch point on &namestr to track all write operations happening on the namestr variable. Whenever an address is written to the namestr variable, the Purify watch point will show a WPW (Watch Point Write) message in the Purify viewer:

$ gdb mem_errors.pure
(gdb) break 13
Breakpoint 1 at 0x10000aec: file mem_errors.c, line 13.
(gdb) run
Starting program: mem_errors.pure 
Breakpoint 1, main () at mem_errors.c:13
13          foo();
(gdb) print purify_watch_n(&namestr, 4, "w")
$1 = 1
(gdb) continue

The purify_watch_n() function takes the address of the memory location to be watched (&namestr), the size (4 bytes, the size of a pointer) and the watch mode (r for read, w for write, rw for read-write). Whenever a new address is stored in namestr, Purify will show a WPW (Watch Point Write) result in the viewer. On expanding, it looks like this message:

WPW: Watch point write:
  * This is occurring while in:
        foo            [mem_errors.c:6]
        main           [mem_errors.c:13]
        __start        [mem_errors.pure]
  * Watchpoint 1
  * Writing 4 bytes to 0x20103b38 in the initialized data section.
  * Value changing from  537934728 (0x20103b88, " \020;\210")
                     to  537934968 (0x20103c78, " \020")
  * Address 0x20103b38 is global variable "namestr".
    This is defined in mem_errors.pure.

This message indicates that in the foo() method at Line 6, another address is stored in namestr. Even before freeing the memory, the value of namestr has been changed, and that is why Purify reports an MLK (memory leak) error. Let's now debug the cause of the FMR (Free Memory Read) and FMW (Free Memory Write) errors. When reporting FMR or FMW, Purify also specifies where the memory allocation happened. For this example, Purify indicates that the FMW and FMR errors at Lines 14 and 15, respectively, in method main() happen because of accesses to already-freed memory that was allocated by the strdup() call at Line 6 in method foo(). Therefore, you need to track all reads and writes on the memory block (and not the pointer) allocated at Line 6. You can do that by putting a read-write watch point after the strdup()call:

(gdb) break 7
Breakpoint 2 at 0x10000c38: file mem_errors.c, line 7.
(gdb) continue
Continuing.
Breakpoint 2, foo () at mem_errors.c:7
7           printf("Product = %s\n", namestr);
(gdb) print purify_watch_n(namestr, 20, "rw")
$2 = 2

Because this is a read-write watch point, any attempt to read or modify the contents of the memory block that namestr points to would trigger a WPR or WPW message, respectively. Notice the difference between using namestr here and &namestr earlier. The earlier example was watching the memory that held the namestr pointer itself; therefore, the address of the watched area is given by &namestr. This second example is watching the memory that namestr points to, instead.

A WPR shows at Line 7 at the printf() call, and then a WPF (Watch Point Free) shows on Line 8 at the free() call. Both of these are expected, but now, after reporting WPF, you should follow the control path more carefully.

In fact, you can set a breakpoint at purify_stop_here (as explained earlier in the "Using Purify with the debugger" section) for Purify to stop at every error (or message). Any access to this memory thereafter should be an error, because the memory pointed to by namestr has been freed. Therefore, stepping through the code at Line 14 generates a WPW message, because the memory that has already been freed is being written to that message by a call to strcpy(). This WPW explains the FMW (Free Memory Write) that Purify reported:

(gdb) next
main () at mem_errors.c:14
14          strcpy(namestr, "IBM");
(gdb) next
15         printf("Company = %s\n", namestr);

Similarly, stepping through Line 15 generates a WPR message and, thus, a FMR error. The FUM error reported toward the end of the program at Line 16 is also obvious now, because the memory for namestr(the new value, allocated by strdup) was already freed at Line 8 in function foo (where a WPF message was reported).

Figure 4 shows a sample Purify window with all of these watch point errors. To summarize: Memory watch points help you in tracing the use of a given memory block. Using them along with the debugger helps you track memory use with the program execution which manifests a memory error.

Figure 4. Watch point messages reported by Purify
screen capture

Purify watch points can report the following messages for the given memory address:

  • Reads
  • Writes
  • Allocation
  • De-allocation
  • Coming into scope at function entry
  • Going out of scope at function exit

There are several watch point API functions for convenience (Table 1). The simplest is purify_watch(addr), which sets a read-write watch point on four bytes starting at the given address. The APIs that set watch points return an integer value, which is the watch point number that was just set. You can pass that integer to watchpoint_remove to remove it. All of the API convenience functions are equivalent to using purify_watch_n with the appropriate address, size, and type.

Table 1. APIs to set watch points
Watch point APIDescription
purify_watch_n(addr, size, type) Set a watch point of type type on size bytes starting at addr. Set Type to read ("r"), write ("w"), or read and write("rw").
purify_watch(addr)
purify_watch_1(addr)
purify_watch_2(addr)
purify_watch_4(addr)
purify_watch_8(addr)
Watch four bytes (or the indicated number) starting at addr, type ("rw").
purify_watch_r(addr)
purify_watch_r_1(addr)
purify_watch_r_2(addr)
purify_watch_r_4(addr)
purify_watch_r_8(addr)
Watch four bytes (or the indicated number) starting at addr, type ("r").
purify_watch_w(addr)
purify_watch_w_1(addr)
purify_watch_w_2(addr)
purify_watch_w_4(addr)
purify_watch_w_8(addr)
Watch four bytes (or the indicated number) starting at addr, type ("w").
purify_watch_rw(addr)
purify_watch_rw_1(addr)
purify_watch_rw_2(addr)
purify_watch_rw_4(addr)
purify_watch_rw_8(addr)
Watch four bytes (or the indicated number) starting at addr, type ("rw").

To get information about watch points and remove them, you can use the following APIs:

  • purify_watch_info(), which shows all active Purify memory watch points
  • purify_watch_remove(int watchpoint_no), which removes the watch point with the given number
  • purify_watch_remove_all(), which removes all watch points

Using Purify APIs in your programs

Apart from using Purify APIs in the debugger, you can also embed them from programs for checking errors and reporting extra information. In that case, even if you run your purified program through an automated test suite; when an error occurs, it will dump extra messages into the Purify log that will help you identify the problem.

There are two ways of embedding Purifying APIs in your program:

  • Using #ifdef guards
  • Linking with Purify stubs

Using #ifdef guards

As shown in the example in Listing 2, you can guard Purify API calls in your program by surrounding them with #ifdef definition guards. In this way, you don't need to change the source code to build the purified executable program that exploits Purify APIs nor to build the executable program that you ship as a product.

The example has an implementation of strncpy, where the source and destination strings are checked first, respectively, to be readable and writable. If any of the tests fail, an appropriate message is printed in the Purify console or log by calling purify_printf. Then purify_describe is called, which prints specific details about the memory address, including its location (stack, heap, text) and, for heap memory, the call chains at its allocation time and its free() call history. Finally, purify_what_colors is called to print the color of the memory buffer. The copy is performed only if no error is found.

Listing 2: Part of file mystring.c that uses Purify API with guards
#ifdef PURIFY
#include <purify.h>
/*
 * The purify.h file has needed API declaration.
 */
#endif

void mystrncpy(char* dest, const char* src, int length) {
#ifdef PURIFY
    if (!purify_assert_is_readable(src, length)) {
        purify_printf("strncat: caller gave bad source");
        purify_describe(src);
        purify_what_colors(src, length);
    } else if (!purify_assert_is_writable(dest, length)) {
        purify_printf("strncat: caller gave bad destination");
        purify_describe(dest);
        purify_what_colors(dest, length);
    } else {
#endif
        /*
         * skip: copy n bytes from src to dest only if safe
         */
#ifdef PURIFY
    }
#endif
}

int main() {
    /* skip: main body that calls mystrncpy */
}

The makefile shown in Listing 3 demonstrates how you can turn on Purify API calls by using the -DPURIFY flag for building a purified version of the executable file (see the rule for mystring.pure) and linking it with the Purify API library.

Listing 3. Part of makefile that builds mystring and mystring.pure
#
# makefile to build mystring programs, and its purified versions
#

# ... skip ...

# Purify header and API lib locations
PURIFYINCLUDE = -I`purify -print-home-dir`
# For 64-bit program, replace lib32 by lib64 in following:
PURIFYAPILIB  = `purify -print-home-dir`/lib32/libpurify_stubs.a 

# ... skip ...

mystring : mystring.c
	$(CC) $(FLAGS) -o $@ $?

mystring.pure : mystring.c
	purify $(CC) $(FLAGS) -g -DPURIFY $(PURIFYINCLUDE) -o $@ $? \
		$(PURIFYAPILIB)

# ... skip ...

Linking with Purify stubs

The drawback of using a guard is that you have to recompile the whole program. In this example, only one C file is used, but in large systems, typically various libraries are built and finally linked to build the executable file. In such situations, if you want to purify your program, you must recompile all C files that use the guard.

An alternative is to always link your application with the libpurify_stubs.a by changing the rule in Listing 3 to build mystring:

mystring : mystring.c
	$(CC) $(FLAGS) -o $@ $? $(PURIFYAPILIB)

The libpurify_stubs.a is a small library that has empty stubs for all Purify API functions. When you instrument your program, Purify provides the real definitions for the API functions, and stubs are ignored.

You can surround multiple Purify APIs with if(purify_is_running()) to keep Purify function calls from slowing down your uninstrumented program (see Listing 4).

Listing 4. Part of file mystring.c that uses Purify API without guards
#include <purify.h>
/*
 * The purify.h file has needed API declaration.
 */

void mystrncpy(char* dest, const char* src, int length) {
if (purify_is_running()) {
        if (!purify_assert_is_readable(src, length)) {
            purify_printf("strncat: caller gave bad source"); 
            purify_describe(src);
            purify_what_colors(src, length);
        } else if (!purify_assert_is_writable(dest, length)) {
            purify_printf("strncat: caller gave bad destination"); 
            purify_describe(dest);
            purify_what_colors(dest, length);
        }
}

    /*
     * skip: copy n bytes from src to dest only if safe
     */
}

int main() {
    /* skip: main body that calls mystrncpy */
}

The tradeoffs between using #ifdef and Purify stubs are that the former requires you to recompile your program with the -DPURIFY flag, while the latter involves a runtime cost of calling purify_is_running (which is negligible), and linking your program with Purify's empty stubs even in production code. Use the alternative that suits your need.

Summary

In this article, you learned about the memory color concept in Purify, the APIs, and the memory watch points. You can use these APIs from the debugger, or you can embed them in your programs, instead. Either way, with the help of Purify APIs and memory watch points, you can debug memory errors in your programs more effectively.

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Rational software on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Rational
ArticleID=284951
ArticleTitle=Advanced features of IBM Rational Purify: Debugging with Purify
publish-date=02052008