Comparing and contrasting the
runtime error detection
technologies used in
HeapAgent™ 3.0,
Purify NT® 4.0, and
BoundsChecker Pro™ 4.0

Copyright © 1996-1997 MicroQuill Software Publishing, Inc.
All rights reserved.

You can also download this document in Microsoft Word for Windows version 6.0/7.0 format.

 

Table of contents

  • Overview
  • HeapAgent
  • Purify NT
  • BoundsChecker Pro
  • Summary
  • How HeapAgent works
  • How heap replacement/instrumentation and stack instrumentation work
  • Limitations of HeapAgent's error detection
  • How Purify works
  • How object code insertion works
  • Limitations of Purify's error detection
  • How BoundsChecker Pro works
  • How compile-time instrumentation works
  • Limitations of BoundsChecker Pro with CTI error detection
  • Compile/link time tests
  • The test and the results
  • How HeapAgent affects compile/link time
  • How Purify affects compile/link time
  • How BoundsChecker Pro with CTI affects compile/link time
  • Runtime tests
  • The test and the results
  • How HeapAgent affects runtime performance
  • How Purify affects runtime performance
  • How BoundsChecker Pro with CTI affects runtime performance
  • Comparison of memory errors detected
  • Additional comments on the errors detected
  • Overwrites
  • Reads of uninitialized and freed memory
  • Stack errors
  • Leaks
  • Pure and NuMega redundantly detect errors caught by the hardware
  • Conclusion
  • Overview

    HeapAgent, Purify, and BoundsChecker Pro use three very different techniques for detecting runtime errors. The error detection breadth, compile/link intrusiveness, and runtime performance of each product are direct results of the respective techniques used and dictate where in the development life cycle each product is appropriate:

    HeapAgent

    HeapAgent uses heap replacement/instrumentation and stack instrumentation. HeapAgent replaces your application's heap manager with a debugging implementation that instruments the individual allocations to detect invalid operations performed on the heap, regardless of which code is responsible for the error. HeapAgent also instruments the stack and checks for invalid stack operations.

    Purify NT

    Purify NT uses object code insertion (OCI). This technique translates your application's object files, inserting checking instructions between your app's instructions. Only those object files that are instrumented are checked.

    BoundsChecker Pro

    BoundsChecker Pro uses compile-time instrumentation (CTI) to instrument your source code. This technique uses a pre-processor to insert checking statements between your application's code statements. Only those source files that are recompiled with instrumentation are checked.

    BoundsChecker Pro includes BoundsChecker Standard Edition. Because this paper assesses memory error detection and because BoundsChecker Standard detects so few memory errors, we have not done an in-depth analysis of the details of its "malloc wrapper" technology. For more information, see the detailed chart at "Comparison of memory errors detected."

    Note: BoundsChecker Pro and Standard also validate/detect a variety of non-memory-related errors including Windows and OLE API errors and leaks, and resource leaks. In addition, BoundsChecker Pro, by instrumenting your app with CTI, can detect invalid pointer manipulations and source code logic errors.

    Summary

    HeapAgent doesn't require a special build, incurs negligible runtime penalty, and automatically begins checking every time the application is run or debugged. As a result, HeapAgent is most valuable during development, where it catches the nastiest bugs, heap and stack errors, immediately after they're introduced, when they are easiest to fix.

    Purify detects the same classes of errors as HeapAgent, but requires a special build and incurs a significant runtime penalty. As a result, Purify is used less frequently during development and more during testing.

    The error detection of BoundsChecker Pro with CTI is the broadest, but its special compile and often-debilitating runtime performance make it useful almost exclusively during testing.


    Return to the Table of contents.


    How HeapAgent works

    HeapAgent re-routes your application's memory allocation calls through a debugging heap implementation that "instruments" the heap with special fill values (guard fill, free fill, and in-use fill). HeapAgent validates each heap-related call and continually scans the heap itself, in a background thread, looking for corruption. HeapAgent also instruments the stack with fill values and checks for stack overwrites during function return. By watching the heap and validating all heap calls, HeapAgent detects all heap-related errors, including those caused by system and third-party DLLs. By watching the stack during function calls, HeapAgent detects many stack errors as well.

    How heap replacement/instrumentation and stack instrumentation work

    HeapAgent uses heap replacement, heap- and stack-memory instrumentation, heap-call validation, and deferred freeing to detect heap and stack errors. HeapAgent's technology should not to be confused with the simple "malloc wrapper" techniques used by many home-grown and low-end heap-checking tools such as BoundsChecker Standard Edition. HeapAgent's performance nor its comprehensive error detection can be achieved without implementing the heap itself.

    Diagram 1, below, shows how heap replacement/instrumentation works.

    Heap-and stack-memory instrumentation

    HeapAgent instruments the heap and stack with special fill values (guard fill, free fill, and in-use fill). Thanks to these fill values, HeapAgent always knows how the heap and stack should look and can check the heap and stack themselves for corruption caused by overwrites. This technique detects overwrites regardless of the code that caused them, for example, the application code, a system DLL, or a third-party DLL.

    Instrumenting the heap and stack also lets HeapAgent catch invalid references to previously freed or uninitialized memory. Because HeapAgent's free and in-use fill patterns are intentionally invalid addresses, the processor generates an exception fault if your application tries to dereference freed or uninitialized memory.

    Finally, because it implements the heap, HeapAgent can identify, without question, all of the allocations that haven't been freed.

    Heap-call validation

    On every call to allocate or free memory, HeapAgent validates the parameters and examines the state of the allocation to verify that this use of memory is legal. HeapAgent immediately detects and reports all invalid or failing memory allocation or deallocation calls.

    Deferred freeing

    HeapAgent uses deferred freeing to temporarily delay the recycling of memory. When memory is freed, the freed memory is filled with the free-fill value, but it isn't actually freed. Instead, it's placed in a queue of defer-freed allocations.

    Deferred freeing lets HeapAgent detect invalid reads, writes, and frees of previously freed memory that would have gone undetected if the memory had been returned to the heap and reused.

    Heap browsers

    While not an error detection technology as such, HeapAgent's heap browsers are unique to HeapAgent and critical to error diagnosis. These browsers let a programmer browse all heap data at any point in a running application. HeapAgent browsers offer several different views of data:

    These browsers are hot-linked together, so it's easy to navigate from one view of data to another. For example, from the Allocation Browser, the programmer can select an object and either display a Dump Browser that shows the contents of memory for the object or display a Source Browser that shows the source code that allocated the object.


    Diagram 1: How heap replacement/instrumentation works

    1. Your heap expands with memory from the OS

    HeapAgent automatically fills the memory retrieved from the operating system with the free fill character (0xDD).

    [Your heap expands with memory from the OS]


    2. Your application calls operator new

    foo.cpp
    main()
    {
        char *str = new char[11];
    }
    

    When allocating memory, HeapAgent:

    [Your application calls operator new]

    The HeapAgent header contains:


    3. Your application enters a value in the allocation

    strcpy(str, "Joe Schmoe");
    
    [Your application enters a value in the allocation]

    4. Your application calls operator delete

    foo.cpp
    main()
    {
        char *str = new char[11];
        strcpy(str, "Joe Schmoe");
        delete str;
    }
    

    When de-allocating memory, HeapAgent:

    [Your application calls operator delete]

    5. Whenever your application is running but not using every CPU cycle

    HeapAgent uses a separate thread, set at idle priority, to incrementally and continually scan the heap for overwrites. This checking is completely independent of the overwrite checking that HeapAgent performs when an individual object is allocated or freed. Because the thread is set at idle priority, it has negligible effect on runtime performance.


    Return to the Table of contents.


    Limitations of HeapAgent's error detection


    Return to the Table of contents.


    How Purify works

    Purify uses a technique called object code insertion (OCI) to insert checking code around every instruction in your application that references, allocates, or deallocates memory.

    How object code insertion works

    Purify's object code insertion works by analyzing each instruction in your application, finding all of the instructions that reference memory, and rewriting those object files with checking code inserted.

    Purify uses a memory-coloring scheme to keep track of the state of every byte of memory in the application. This requires every instruction in the app, including system DLLs, to be fully instrumented, or Purify will be unable to know the correct state of memory, since non-instrumented code could otherwise initialize, allocate, or free memory without Purify's knowledge.

    Each time any portion of your code changes, it must be reinstrumented after it is compiled. This is roughly equivalent to an extra code-generation and link phase on each compile.

    For more detailed information on OCI, see Pure's white paper:

    http://www.pure.com/products/purify/PYhasjoywp.html
    

    Limitations of Purify's error detection


    Return to the Table of contents.


    How BoundsChecker Pro works

    BoundsChecker Pro uses compile-time instrumentation (CTI), which is a completely different technique for detecting errors than the technique HeapAgent uses. As we mentioned earlier, BoundsChecker Pro is a superset of BoundsChecker Standard so it can detect some memory errors on debug builds, without CTI. This paper discusses the CTI technology in depth, and does not discuss the malloc wrapper, because it's the CTI technology that lets BoundsChecker Pro detect the great majority of the memory errors that the product can detect. For more information, see the detailed chart at "Comparison of memory errors detected."

    NuMega™ publishes very little information on how CTI works. However, because they licensed this technology (they didn't invent it or build it themselves), there is plentiful information available on the web site of Parasoft, the company NuMega licensed the technology from. For more detail, see the Parasoft white paper "Insure ++: A tool to support Total Quality Software™":

    http://www.parasoft.com/insure++/papers/technical.html
    

    Return to the Table of contents.


    How compile-time instrumentation works

    BoundsChecker Pro's compile-time instrumentation technology adds a pre-process step to the Microsoft Visual C++ build procedure. When a BoundsChecker Pro build is started, BoundsChecker Pro first analyzes each of the source files and, based on this analysis, inserts calls to runtime checking functions and also inserts temporary variables into the code. (The temporary variables are needed to hold intermediate results.) The checking functions are inserted at every location in the source that:

    The following "before" and "after" code example shows how this works.

    Diagram 2: How compile-time instrumentation works

    Your source code before instrumentation

    CDB_ERR dbSession::Close( void )
    {
        CDB_ERR     err = CDB_ERR_NONE;
        dbConnect   *connect;
    

    Your source code after instrumentation

    CDB_ERR dbSession ::Close(void)
    {
        int _Insight_spmark;
        class dbConnect *_Insight_1;
        CDB_ERR _Insight_2;
        if (_Insight_init) 
            _Insight_I_src_cdb_c823552227();
        _Insight_func_top(1995, 29852, (long int) &_Insight_spmark, _Insight_strtable);
        _Insight_pop_this((void *) this);
        _Insight_decl_lwptr(_Insight_fid_44, 133L, 13, (void *) &_Insight_1,0, 1);
        _Insight_decl_lwptr(_Insight_fid_44, 133L, 20, (void *) &_Insight_2,0, 1);
    
        CDB_ERR err;
        _Insight_decl_lwptr(_Insight_fid_44, 134L, 652, (void *) &err,0, 2);
        _Insight_assign_ptr2(1234, (void **) &err, (void *) ((dbError *) 0),
            (void *) ((dbError *) 0));
        err = ((dbError *) 0);
    
        dbConnect *connect;
        _Insight_decl_lwptr(_Insight_fid_44, 135L, 286, (void *) &connect,0, 3);
    

    Before

        while ((connect = dbConnect::FindConnect( this )) != NULL) {
            CDBVERIFY( connect->CloseDb( ));
            delete connect;
        }
    

    After

        _Insight_stack_call(0);
        _Insight_1 = dbConnect ::FindConnect(this);
        _Insight_assign_ptr3((void **) &_Insight_1, 1990, 1);
        _Insight_after_call();
        _Insight_assign_ptr1(1991, (void **) &connect, (void **) &_Insight_1,(void *) _Insight_1);
        connect = _Insight_1;
        _Insight_ptra_check(286, (void **) &connect, (void *) connect);
        while(connect != 0) {
            _Insight_clear_temp((void **) &_Insight_1);
            _Insight_stack_call(0);
            _Insight_2 = (connect->CloseDb());
            _Insight_assign_ptr3((void **) &_Insight_2, 1992, 1);
            _Insight_after_call();
            _Insight_assign_ptr1(1993, (void **) &err, (void **) &_Insight_2,(void *) _Insight_2);
            err = _Insight_2;
            _Insight_ptra_check(652, (void **) &err, (void *) err);
            if (err != 0) {
                _Insight_clear_temp((void **) &_Insight_2);
                goto fail;
            } else 
                _Insight_clear_temp((void **) &_Insight_2);
            ;
            _Insight_deletea(286, (void **) &connect, (void *) connect, 8192);
            delete connect;
            _Insight_stack_call(0);
            _Insight_1 = dbConnect ::FindConnect(this);
            _Insight_assign_ptr3((void **) &_Insight_1, 1990, 1);
            _Insight_after_call();
            _Insight_assign_ptr1(1991, (void **) &connect, (void **) &_Insight_1,(void *) _Insight_1);
            connect = _Insight_1;
            _Insight_ptra_check(286, (void **) &connect, (void *) connect);
        }
        _Insight_clear_temp((void **) &_Insight_1);
    

    Before

        // free what appears to be leaks
        m_username.Empty( );
        m_password.Empty( );
    

    After

        // free what appears to be leaks
        _Insight_stack_call(0);
        m_username.Empty();
        _Insight_after_call();
        _Insight_stack_call(0);
        m_password.Empty();
        _Insight_after_call();
    

    Before

        if (m_hsession) gblHandles->ReleaseUserHandle( m_hsession );
        if (m_dberr) delete m_dberr;
    

    After

        if (m_hsession) {
            _Insight_stack_call(0);
            gblHandles->ReleaseUserHandle(m_hsession);
            _Insight_after_call();
        } 
    
        _Insight_ptra_check(1994, (void **) &m_dberr, (void *) m_dberr);
        if (m_dberr) {
            _Insight_deletea(1994, (void **) &m_dberr, (void *) m_dberr, 0);
            delete m_dberr;
        } 
    

    Before

        TheSystem.RemoveSession( this );
    

    After

        _Insight_stack_call(0);
        TheSystem.RemoveSession(this);
        _Insight_after_call();
    

    Before

    fail:
        return err;
    }
    

    After

    fail:
        _Insight_returning((void *) &err, 4096, 1);
        return err;
    }
    

    Return to the Table of contents.


    Limitations of BoundsChecker Pro with CTI error detection

    BoundsChecker Pro's compile-time instrumentation detects memory errors using "data usage" analysis. This technique does not rely on, or even have any direct knowledge of, what is happening in the heap. Errors are judged to be errors based entirely on whether or not your use of pointers and arrays is "proper."

    A very real problem with this technique is that the indeterminate nature of the use of pointers in C and C++ makes it impossible to reliably determine "what is proper" from the code itself. For example, casting a pointer into a local variable can be interpreted as leaked memory due to pointer assignment, when in fact this is a perfectly acceptable and necessary operation in a comparison routine. (This same problem is the bane of code optimizers, whose data flow analyses are prone to incorrectly interpreting pointer usage and introducing errors during optimization.)

    There are several other by-products of this technique:


    Return to the Table of contents.


    Compile/link time tests

    The test and the results

    We recompiled some of the MFC source code (approximately 110K lines in 190 files) and measured the amount of time required by Purify and by BoundsChecker Pro with CTI. We performed three runs for each; the results listed below are the average times:

    Product              Total compile/link time      Factor
    ========================================================
    Visual C++ 
      debug build        23 minutes, 25 seconds       1x
    
    HeapAgent            23 minutes, 25 seconds       1x
                         (works on debug build, so
                         no recompile is required)
    
    Purify 
      object-code 
      insertion          24 minutes, 20 seconds       1.04x
    
    BoundsChecker Pro
      compile-time 
      instrumentation    100 minutes, 55 seconds      4.31x
    

    Note To enable stack checking, HeapAgent requires the /Ge compile flag, which activates stack probes for every function call that requires storage for local variables.

    If you'd like to try the BoundsChecker Pro recompile test yourself, or if you'd like to see which compile flags we used, you can download the batch file we used to compute these timings. (No special files are required for HeapAgent or Purify.)

    Download cti_test.zip
    

    How HeapAgent affects compile/link time

    HeapAgent does not affect compile or link time because it works on debug builds.

    Each time your EXE is launched, HeapAgent is notified by the operating system. As the EXE is loaded, HeapAgent transparently patches the heap calls in the application's EXE and DLLs (including any C runtime library DLLs) to re-route all heap calls in the application to the HeapAgent debugging heap implementation. This patching affects only the currently loaded copy of the EXE and its DLLs; patching doesn't affect the on-disk copy of the program files or any other processes running on the system. All of this is done completely transparently -- without making the programmer recompile or relink to produce a special checkable build.

    Working with standard builds of an application has a major benefit apart from ease of use: it ensures that the actual application is being tested, with no possibility of incorrect error reports or incorrect program logic resulting from the modifications that the source and object instrumentation techniques make to the application's code.

    How Purify affects compile/link time

    Purify requires a special "checkable" build that instruments your app's object code (unlike BoundsChecker Pro's CTI, which instruments your app's source code). Purify's instrumentation has two compile-time repercussions. First, it requires substantially more disk space than normal builds to accommodate the much larger object modules, as well as the redundant system and third-party DLLs that must also be instrumented. Second, it slows the build. For small apps this build time degradation is minimal. For larger apps, the larger object modules often cause the system to go virtual, thereby causing severe slowdowns.

    How BoundsChecker Pro with CTI affects compile/link time

    BoundsChecker Pro's compile-time instrumentation is like Purify's object-code insertion in that it requires substantially more disk space than normal builds to accommodate the preprocessed source and the much larger object modules. In addition, BoundsChecker Pro's instrumentation can cause the compile time to increase exponentially.


    Return to the Table of contents.


    Runtime tests

    The test and the results

    We wanted to test runtime performance on an app that performed some operation that most C++ developers would readily understand. In addition, we wanted an app that was:

    For the runtime tests, we used GNU Bison 1.25. Bison takes a grammar file as a command-line parameter and generates C code that implements a parser for that grammar. Bison contains 11,000 lines of C code; compiled with Microsoft Visual C++ debugging information, bison.exe is 163 KB.

    The goal was to compare runtime performance for typical debugging sessions. Execution time was measured by printing the time of day at the top and bottom of main(). Execution times were compared for a debug Visual C++ build, HeapAgent 3.0, Purify 4.0, and BoundsChecker Pro 4.0 with and without compile-time instrumentation (CTI).

    Bison is available at:

    http://www.cdrom.com/pub/gnu/bison-1.25.tar.gz
    

    For this benchmark, we chose the C++ grammar file that is distributed with GNU GCC. This file, parse.y, can be found at:

    http://www.cdrom.com/pub/gnu/gcc-2.7.2.tar.gz
    

    Note Bison uses long file names, so you can only run this test on Windows NT or Windows 95.

    Also note You can extract the contents of these files using WinZip, which is available at:

    http://www.winzip.com
    

    If you'd like to run these tests yourself, you can download a zip file that contains a makefile, an INI file for the BoundsChecker compile-time instrumentation test, and the description you're now reading:

    Download ha_bench.zip
    

    The test environment

    We ran the tests in the following test environment:

    The test code

    We modified main.c to print the time of day at the top and bottom of main():

    main.c
    ======
    24d23
    < #include <time.h>
    48d46
    <  { time_t curtime;time(&curtime);puts(ctime(&curtime)); }
    90d87
    <  { time_t curtime;time(&curtime);puts(ctime(&curtime)); }
    

    The other change to the standard distribution was the addition of makefile, which is included in ha_bench.zip, as noted above. The command line to build the executable is:

    nmake CC=cl
    

    Running the benchmark tests

    Run Bison with only the Visual C++-supplied memory debugging:

    bison parse.y
    

    HeapAgent test

    Start HeapAgent and open bison.exe. Set the command-line arguments to parse.y. Use all default settings.

    Purify test

    Start Purify and Run bison.exe parse.y. Use all default settings.

    BoundsChecker test (without compile-time instrumentation)

    Start BoundsChecker and open bison.exe. Set the arguments to parse.y. Set the Error Detection mode to Quick in BoundsChecker Settings.

    BoundsChecker test (with compile-time instrumentation)

    For the BoundsChecker compile-time instrumentation test, you need to recompile bison.exe. We created an INI file, bc_cti.ini, for BoundsChecker to use during this recompile (bc_cti.ini is included in ha_bench.zip, as noted above):

    checking_level full
    checking_uninit_compile on
    compiler_fault_recovery on
    precompiled_header off
    

    Then rebuild:

    nmake clean
    nmake CC="bcompile -Zop bc_cti.ini"
    

    Finally, run bison.exe under BoundsChecker as described in the previous test.

    The results

    Each test was performed three times. The displayed starting time was subtracted from the displayed ending time to find the execution time. The three execution times were then averaged to arrive at the following results:

    Test                                              mm:ss    Factor
    ==================================================================
    Visual C++                                        00:04      1x
    HeapAgent                                         00:04      1x
    Purify                                            00:44     11x
    BoundsChecker Pro (without CTI--"Quick" mode)     06:56    104x
    BoundsChecker Pro (without CTI--"Normal" mode)    06:57    104x
    BoundsChecker Pro (without CTI--"Maximum" mode)   57:43    865x
    BoundsChecker Pro (with CTI--"Quick" mode)        10:35    158x
    BoundsChecker Pro (with CTI--"Normal" mode)       15:45    236x
    BoundsChecker Pro (with CTI--"Maximum" mode)      66:18    994x
    
    

    Return to the Table of contents.


    How HeapAgent affects runtime performance

    HeapAgent's design goal was to build a memory checker that could be used every run. As a result, runtime performance was a major consideration in the design. Here are the major reasons HeapAgent has minimal runtime impact on your application:

    Note HeapAgent does affect runtime performance under certain conditions. HeapAgent incurs some extra overhead with each allocation (for guard bytes, etc.) and defers the freeing of allocations (to help find writes into previously freed memory). As a result, the HeapAgent heap is larger than the normal heap and will exceed available physical memory earlier. In cases where the HeapAgent heap exceeds physical memory and the compiler allocator's heap does not, there will be performance degradation from disk hits.

    However, most programmers have enough RAM that their test cases rarely exceed physical memory even with the larger HeapAgent heap. Overall, HeapAgent's checking is so efficient that the performance hit is typically more than offset by the performance gains an app gets from using the (SmartHeap-based) faster alloc and free functions.


    Return to the Table of contents.


    How Purify affects runtime performance

    The most severe impact of OCI is on runtime performance. Purify was originally designed to work on RISC processors where most instructions manipulate registers and only two instructions -- load and store -- reference memory; always a word at a time. In the Intel CISC architecture, however, dozens of different instructions reference memory in very complex ways: a single instruction can both load and store memory, or load or store one, two, or dozens of bytes. This means that Purify on Intel must insert checking code in many more locations in your application. It also means that the checking code must be significantly more complex. Worse, OCI interferes with the Pentium instruction pipeline and on-chip cache, resulting in even more slowdown than Purify's designers may have anticipated.

    Finally, Purify causes an application's runtime memory footprint and working set to explode, which can result in excessive swapping even for small applications. Two factors cause this ballooning of memory usage:


    Return to the Table of contents.


    How BoundsChecker Pro with CTI affects runtime performance

    The runtime impact of BoundsChecker Pro with CTI is even more severe than its compile-time impact. This is because operations, such as using a pointer, that normally consume just a few machine instructions now call at least one additional function, and often two. These functions will consume orders of magnitude more cycles to find the pointer, test its value, and record changes to its state (not to mention the function call/return overhead itself). In the code example in Diagram 2, "How compile-time instrumentation works," earlier in this paper, the four-line while loop, which consisted of three calls, ballooned to over 30 lines of code. Most of this code was added to make 20 additional calls to the checking functions of BoundsChecker Pro with CTI.


    Return to the Table of contents.


    Comparison of memory errors detected

    The following table shows the memory errors detected by HeapAgent, Purify, BoundsChecker Pro with CTI, and BoundsChecker Standard, and compares error reporting and diagnosis features.

                                                           BoundsChecker
    Errors detected                   HeapAgent   Purify   Pro w/CTI  Std.
    ======================================================================
    Underwrites before the 
      beginning of heap objects          Yes        Yes       Yes      No
    Overwrites beyond the 
      end of heap objects                Yes        Yes       Yes      Yes
    Reads before the 
      beginning of heap objects          Yes        Yes       Yes      No
    Reads beyond the 
      end of heap objects                Yes        Yes       Yes      No
    Overwrites over internal 
      heap data structures               Yes        Yes       Yes      No
    Reads of uninitialized 
      heap objects                       Yes        Yes       Yes      No
    Writes into freed 
      heap objects                       Yes        Yes       Yes      No
    Reads of free heap objects           Yes        Yes       Yes      No
    Double frees                         Yes        Yes       Yes      Yes
    Invalid parameters to 
      heap-related calls                 Yes        Yes       Yes      Yes
    Retained references to 
      reallocated memory                 Yes        Yes       No       No
    Leakage -- objects not freed 
      at end of program                  Yes        Yes       Yes      Yes
    Leakage -- at other points 
      in program                         Yes        Yes       Yes      No
    Stack underwrites                    Yes        Yes       Yes      No
    Stack overwrites                     Yes        No        Yes      Yes
    Reads of uninitialized 
      stack objects                      Yes        No        Yes      No
    Reads beyond stack pointer           Yes        Yes       Yes      No
    Underwrites of static 
      (global variables) memory          No         No        Yes      No
    Overwrites of static 
      (global variables) memory          No         No        Yes      Yes
    Reads of uninitialized 
      static (global variables) memory   No         No        Yes      No
    

    Note The table above lists memory errors only. BoundsChecker also validates/detects Windows and OLE APIs and leaks, resource leaks, and, by instrumenting your source, invalid pointer manipulations and source code logic errors.

    Also note BoundsChecker Pro has three error detection levels, Quick, Normal, and Maximum. More errors are detected at higher levels, but it's unclear from NuMega's marketing literature, printed documentation, or online help exactly which errors are detected at each level. The table above lists the errors detected at Maximum.

                                                           BoundsChecker
    Error reporting                   HeapAgent   Purify   Pro w/CTI  Std.
    ======================================================================
    File name, line number, and 
      stack trace identifying the call
      at which the error was detected    Yes        Yes       Yes      Yes
    File name, line number, and 
      stack trace identifying the call
      that allocated the object          Yes        Yes       Yes      Yes
    Pass count at file/line to 
      pinpoint specific object           Yes        Yes       No       No
    Unique identifier for each object    Yes        No        No       No
    Object parameters, including size, 
      contents, and address              Yes        Yes       Yes      No
    User-defined object checkpoints      Yes        No        No       No
    
    
                                                           BoundsChecker
    Error diagnosis                   HeapAgent   Purify   Pro w/CTI  Std.
    ======================================================================
    Browse source code associated 
      with the detected error            Yes        Yes       Yes      Yes
    Dump and browse contents of 
      memory related to the error        Yes        No        No       No
    Format memory contents as ASCII,
      int, float, pointer, etc.          Yes        No        No       No
    Select any byte of memory and 
      display the source that 
      allocated it                       Yes        No        No       No
    Search the heap for allocations 
      matching filter criteria           Yes        No        No       No
    Select any source line and 
      display the objects it allocated   Yes        No        No       No
    Set breakpoints on 
      user-specified heap events         Yes        Yes       No       No
    Change error detection settings 
      based on user-specified 
      heap events                        Yes        No        No       No
    Break to the compiler-supplied 
      debugger from any error            Yes        Yes       Yes      Yes
    
    

    Return to the Table of contents.


    Additional comments on the errors detected

    HeapAgent, BoundsChecker Pro with CTI, and Purify each use very different techniques to detect errors. These techniques determine when errors are, and are not, detected by each product.

    Overwrites

    Pure and NuMega like to point out how their OCI and CTI technologies, respectively, pinpoint the location of errors by catching them as they occur at runtime. They suggest that HeapAgent catches errors later -- meaning after many lines of code have executed. This is misleading in that overwrites are the only errors that Purify/BoundsChecker Pro catch earlier than HeapAgent in a given run. All other errors are caught at the same time by all three products.

    The reason Purify and BoundsChecker Pro with CTI detect overwrites at the instruction level is that they explicitly check for overwrites before code instructions that can potentially cause an overwrite. HeapAgent, on the other hand, uses a background thread running at idle priority to perform continuous incremental scanning of the heap for actual overwrites.

    With BoundsChecker Pro with CTI, you can only be sure you're catching all overwrites if you recompile every source file in your application, including third-party libraries. BoundsChecker Pro doesn't catch overwrites that are caused by a source file that hasn't been instrumented with CTI.

    There are two reasons why HeapAgent's technique is, in practice, more effective at finding overwrites earlier in the development cycle:

    HeapAgent detects the same errors but without a performance hit. By being there all the time and catching each error as it's introduced, HeapAgent eliminates errors much earlier than the other tools, which are too slow to use all the time.


    Return to the Table of contents.


    Reads of uninitialized and freed memory

    Purify and BoundsChecker Pro with CTI detect invalid memory reads the same way they detect overwrites: by explicitly checking for the error before every code instruction that reads memory. This technique generally lets both products detect all instances of these errors, though there are some exceptions. Because BoundsChecker Pro with CTI doesn't place guards before the beginning of memory allocations and doesn't implement deferred freeing, it can miss invalid reads and references to free memory caused by non-source-instrumented code.

    HeapAgent, on the other hand, avoids this slow checking by leveraging the hardware's error-detection capabilities. HeapAgent fills free, uninitialized, and pre- and post-guard areas of memory with a unique fill value that is an intentionally invalid address. When a pointer is read from uninitialized, free, or out-of-bounds memory, the processor generates an exception fault as soon as the pointer is dereferenced because the fill pattern contains an invalid address.

    HeapAgent then intercepts the exception fault and reports the error as an invalid read or write, identifying the file, line, and pass count of both the error and the allocation involved. This is much more efficient than inserting checking code between each memory reference -- with HeapAgent, no extra runtime overhead is incurred because the error is detected by hardware rather than software. The only time HeapAgent won't detect an invalid reference is when the value read from free memory is an integer or floating-point value instead of a pointer.

    For the most part, however, references to uninitialized memory are caused by a programmer forgetting to initialize an entire data structure. HeapAgent detects references to the items in the data structure that contain pointers; when the first invalid reference is detected, the underlying logic error -- the failure to initialize the data structure -- will be obvious. The approach used by Purify and BoundsChecker Pro with CTI, which reports each uninitialized member of the data structure, is not only slower but redundant and overwhelms the programmer with volumes of irritating reports of the same logic error.

    Likewise, the vast majority of references to free memory involve reading a class member of a deleted object. HeapAgent immediately detects any dereference of a pointer stored in a deleted object, including any call through a virtual function. Here again, the underlying logic error (premature free of the object) is apparent from HeapAgent's error report. Error reports of reads of other (non-pointer) members of the data structure would be of no further help because the error is already identified.

    As with overwrites, HeapAgent detects the same errors but without a performance hit. By being there all the time and catching each error as it's introduced, HeapAgent eliminates errors much earlier than the other tools that are too slow to use all the time.


    Return to the Table of contents.


    Stack errors

    HeapAgent will miss reads of non-pointer values from unallocated or uninitialized stack space. HeapAgent doesn't detect under/overwrites of individual local variables, only under/overwrites of the stack frame as a whole. Purify doesn't detect stack errors within any stack frame currently in use. Purify only detects reads/writes beyond the current stack pointer.

    Leaks

    Purify and BoundsChecker Pro with CTI use garbage collection and source code analysis, respectively, to identify what they believe are "true" memory leaks. HeapAgent, on the other hand, identifies all memory allocations that the program fails to free.

    HeapAgent's leak detection is designed to identify every object that was not deleted. We chose this technique because many apparently benign memory leaks actually expose logic bugs whose ramifications go far beyond the actual leak itself. This is especially true in C++, where failing to delete an object also means that its destructor is not called. Since important resource management is often done during object destruction, failure to report this type of memory leak can later result in unforeseen problems. For diagnosis of leaks, HeapAgent provides an allocation checkpointing mechanism, a search and filter facility, and allocation browsers that let a programmer see the contents of and relationship between any subset of allocations in the heap in real time. Finally, any time a HeapAgent user determines that a reported leak is not significant, the "suppress" button lets the user turn off reporting of the error in all future runs.

    Purify and BoundsChecker Pro with CTI use different leakage detection techniques that do not report seemingly "benign" leaks. As a result, these products will sometimes miss the serious logic errors that these leaks represent.

    Worse, the technique of source code analysis used by BoundsChecker Pro with CTI often reports false positives where there is no leakage at all. The following is an example:

    class Foo {
        Foo() { foo_global = this; }
    } *foo_global;
    
    void main()
    {
        new Foo();          // BoundsChecker Pro with CTI 
                            // reports leakage
        delete foo_global;  // object is deleted here
    }
    

    Return to the Table of contents.


    Pure and NuMega redundantly detect errors caught by the hardware

    Our "errors detected" list above is smaller than the lists advertised by Pure and NuMega. One reason is that our list includes only memory errors. Another reason is that we haven't included, in our list, all of the errors that Purify and BoundsChecker Pro with CTI claim credit for but that are, in fact, detected by the hardware or the operating system.

    Following are the errors on the Purify or BoundsChecker Pro with CTI "errors detected" lists that are automatically detected by the hardware or OS and reported by the Visual C++ debugger right at the responsible line of code. Because Purify and BoundsChecker Pro with CTI are inserting checking code before every instruction, they do detect some of these errors in software rather than leaving it to the hardware, but there is no net error detection gain to you (only slower performance).

    Errors detected            Claimed by           Claimed by
    by hardware or OS            Purify     BoundsChecker Pro with CTI
    ==================================================================
    Writing null pointer          Yes                  Yes
    Reading null pointer          Yes                  Yes
    Freeing null pointer                               Yes
    Array parameter is null                            Yes
    Expression uses null                               Yes
    Function pointer is null                           Yes
    Exception continued           Yes
    Exception handled             Yes
    Exception ignored             Yes
    Exception unhandled           Yes
    Exception continued           Yes
    Output debug string           Yes
    

    Return to the Table of contents.


    Conclusion

    Pure's and NuMega's advertisements of error-detection omnipotence sound impressive until you realize that memory errors their products detect aren't markedly different from HeapAgent's, that they can require special builds, and that they can slow runtime performance by 10x to 900x or more, even in simple test cases. Both BoundsChecker Pro's compile-time instrumentation and Purify's object code insertion techniques incur substantial slowdowns because checking code is being inserted between every instruction in the app! The bottom line: both Purify and BoundsChecker Pro were designed as testing tools, not as development tools.

    HeapAgent's design goal was very different from that of Purify and BoundsChecker Pro with CTI. We wanted to detect the same errors, but without the compile/link obtrusiveness and runtime degradation endemic to these products. As a result, HeapAgent's detection techniques were carefully designed to avoid any runtime slowdown by leveraging the multi-threading and hardware exception capabilities of modern OSs and CPUs, thereby producing a tool that programmers can use every time they run their apps.

    HeapAgent's design is centered around the fact that the cause of runtime errors is logic errors introduced by programmers. By giving programmers a tool that they'll actually use every day, you'll leverage your most valuable asset in eliminating runtime errors: the programmers who introduce them. Nothing else you do could possibly have the same impact on the quality or timeliness of your software deliverables.

    By all means, get a copy of Purify and/or BoundsChecker Pro to use for regression testing. But if you're serious about eliminating runtime errors from your software without killing productivity, give each of your programmers a copy of HeapAgent that they'll use "every run."


    Return to the Table of contents.