Palm OS Memory Leak Detection

Dr. Dobb's Journal July 2001

There's no tolerance for memory leaks in Palm apps

By Jeff Ishaq

Jeff has been a Palm consultant since 1996. He works out of his office in Burlington, Vermont, and can be contacted at jishaq@earthlink.net.

In the early days of the Palm platform, the Personal and Professional models running Palm OS 1.0 and 2.0 offered 12 KB of dynamic space. This is all you could use for an application's static variables, global variables, and dynamic allocations. The Palm III wooed developers by introducing Palm OS 3.0. Touting both a TCP/IP stack and infrared capabilities, the available dynamic space on the Palm III tripled to a whopping 36 KB. Life was pretty good for Palm developers who needed to write memory-intensive applications. With Palm OS 3.5, dynamic heap availability is becoming less of an issue, with devices sporting anywhere from 56 to 184 KB of usable dynamic heap. (For dynamic heap sizes of various hardware combinations, see http://oasis.palm.com/dev/kb/manuals/article.cfm?id=1748#929775; for hardware specifications, see http://www.PalmOS.com/dev/tech/hardware/compare.html.)

With such tight requirements on dynamic heap sizes, Palm developers have always been concerned about optimal memory consumption and minimal memory footprint. Nothing can stop Palm development faster than unexpectedly running out of memory, and Palm developers know that every byte of dynamic heap is precious. Consequently, the design phase of any successful Palm project must consider the memory footprint on the target device and the impact of integrating third-party components into the project.

In short, there is no tolerance for memory leaks in Palm applications. That's why memory leak detection is a big part of the Palm Powered Solution test suite (http://www.qpqa.com/palm/palm.download/PalmPoweredTestKit352.doc/). If your app leaks, its packaging can't bear the nifty Palm logo. Everyone stumbles across a memory leak from time to time during development, and seasoned Palm OS developers learn to drop everything and patch the leak before moving on. Face it, detecting memory leaks in a Palm application's codebase can be difficult and time consuming, especially if you're brought in as a consultant and not yet acquainted with the codebase.

In this article, I'll present techniques for hunting down existing memory leaks. In addition, I provide a robust memory leak detection system implemented as a CodeWarrior 6 project file. Its complete source code (available electronically, see "Resource Center," page 5) lets you immediately start hunting leaks in your own Palm projects. Because the code is not platform specific, it should port easily to other environments as well. Setting up a similar sample project in other development environments should be a matter of adding the necessary files and building.

I've used these techniques as a Palm OS developer on countless projects and their effectiveness overshadows the grim realization that it's time for a leak hunt. Once some of these techniques are implemented, it's easy to detect and prevent new leaks from being introduced into your Palm OS codebase as development progresses. This becomes a powerful proactive tool to avoid unexpected and costly memory leak hunts by flagging them as soon as they're introduced into the codebase.

Tips and Techniques

The first step toward leak detection is to run the app on an emulator running a debug version of the target ROM(s). The debug ROMs present an alert when they have detected leaked memory upon application exit, null pointer accesses, and countless other bad things. The Palm OS Emulator (POSEr) and a debug ROM are always my starting point when something is amiss, as they can uncover simple bugs as well as anomalous application execution problems. (The POSEr and ROM files can be found at http://www.PalmOS.com/dev/tech/tools/emulator.)

The beauty of this technique is its ease of implementation: You simply run the application on a debug ROM and message boxes pop up when something bad happens. In general, you should always take advantage of developing and testing on a debug ROM (if possible). Most projects that require interfacing with hardware typically won't afford the luxury of debugging on an emulator. One exception is the Symbol SPT-1740 device, with a debug ROM that is available at http://software.symbol.com/ and through Palm's developer seeding program. Don't lose sight of the big picture when developing using a debug ROM. You might be surprised to realize your code runs quite differently on an actual device using a release ROM.

One simple leak detection technique is to check the size of the dynamic memory heap using MemHeapFreeBytes( 0, ...) in PilotMain() and ensure it's the same value both before and after the execution loop. Heap #0 is the dynamic memory heap for the time being, though there's no guarantee this won't change in the future. As an example of a similar technique, the debug ROMs of Palm OS 2.0 (and up) run checksums of the low-memory globals using Crc16CalcBlock() periodically before, during, and after application execution. Any unexpected changes elicit the dreaded message "YourApp 1.0 has just written directly to Palm OS global variables," indicating you've done something bad.

Pointer validation is an essential technique in Palm OS development. (We aren't targeting a Windows CE device with bottomless memory stockpiles — fortunately.) Low-memory situations occur that challenge your development savvy and keep you thinking about the most efficient way to accomplish things. Applications must always expect memory allocation failure and handle it gracefully from anywhere within the code. Always assert a freshly allocated block pointer's validity by checking it for NULL, then ensuring MemPtrSize() returns a number larger than zero.

Advanced Leak Detection Overview

I'll now move to a more involved technique — granted, it's an old concept with which you might be familiar. Here's how it works: You track and match every allocation with its corresponding deallocation. At the end of program execution, any allocations that haven't been paired with a corresponding deallocation are likely to be memory leaks and must be patched.

Tracking Allocations

You can keep track of memory allocations and deallocations by writing wrappers around the stock Palm OS allocation/deallocation functions. Using two different techniques, you then remap the code (and operating system) so that it calls your wrapper functions. For each allocation in your remapped allocation function, store a record containing diagnostic information in a temporary Palm database (PDB) file on the device. Listing One shows what the structure of this record can look like.

For each memory allocation, keep track of the filename in which the actual allocation occurred, and the line number of the code that performed the allocation. Also note the size of the allocated block, the type — whether it's a handle or pointer — and actual address of the pointer or handle.

The creation of the temporary PDB that contains these records — as well as their insertion, location, and deletion — is done with simple helper functions, also included in the sample project.

Remapping the Palm OS Memory Manager

You can intercept calls into the Palm OS memory manager at two levels. The first and most rudimentary level is by wrapping MemHandleNew() and MemPtrNew() using preprocessing macros; see Example 1.

You take advantage of the compiler's __FILE__ and __LINE__ predefined macros to pass pertinent information from the compiler into your object code at compile time, so you don't have to hard code file names and line numbers. Any compiler that implements the ANSI C Standard should provide these macros. (In the sample code available electronically, these defines are in MemDbgMacros.h.)

Code-Level Remapping Using #define

It is important to remember the local context in which this rudimentary macro remapping occurs. Though the concept is as simple as understanding the scope of a #define macro, I'll risk overstating the obvious to make an important distinction. Only in code modules that you actually compile — and in which you #include your MemDbgMacros.h header file — will the allocation routines MemHandleFree() and MemPtrFree() remap to your debug versions. Consequently, allocation records in your MemDbgDB only get generated by the allocations that happen in said code modules.

For example, if your codebase calls into NetLibOpen(), it can trigger a call to MemPtrNew() inside the context of the NetLib shared library. However, this will not generate an allocation record into your MemDbgDB because Palm didn't compile Netlib with your remapping #defines. The same holds true for anything else that wasn't compiled to #include MemDbgMacros.h. If instead you desired this comprehensive level of memory manager remapping, you would utilize the global level of remapping, rather than #define macros. It is generally neither necessary nor practical to apply this global remapping to allocation functions, though the case is different for deallocation functions, as will be discussed next.

Global-Level Remapping by Patching Traps

For a comprehensive or global context, the second level of remapping memory manager functions occurs at the trap level. The corresponding deallocation functions to MemPtrNew() and MemHandleNew() are MemPtrFree() and MemHandleFree(), respectively. You need to remap these two deallocation functions in a global context (instead of just a local codebase context) for a subtle reason: Though allocations that happen outside of your codebase are generally of little interest, there are some cases in which the Palm OS automatically deallocates chunks you have allocated. This causes unmatched allocation entries in your MemDbgDB, which results in erroneously reported memory leaks. That's a bad thing.

One example of this automatic deallocation is in the case of a handle that holds the text of a field. Many developers choose manually to allocate this handle at run time, instead of hard coding it into the resource file's Max Characters property, for flexibility. When this form closes, the operating system asks each field to deallocate any nonnull handles they contain. If this happens, you have performed an allocation inside the local context of the codebase, yet the deallocation happens outside of this local context — by the operating system. The problematic result of this would be an allocation record in your MemDbgDB that had no corresponding deallocation: an apparent memory leak, though the memory was in fact deallocated for behind the scenes and you had no way of knowing about it.

By remapping MemPtrFree() and MemHandleFree() via patching their traps, you remap them in the global context, not just the local context of your code. This means any deallocation, whether it comes from the application, operating system, or a shared library like Netlib, goes through your replacement deallocation functions. This small inefficiency is necessary so that you don't miss any deallocations that happen outside the local scope of your codebase.

Patching traps is not for the squeamish and causes most Palm OS programmers to wince, calling trap patching a "hack." I agree, and there are complicated scenarios that can cause catastrophic problems when traps are patched by multiple processes at the same time. However, the chances of this happening are infinitesimal, and a hard reset takes care of even the worst catastrophic failure caused by this problem. For purposes here, this hack is warranted and performed only under a controlled in-house debugging environment. The huge rewards far outweigh the tiny risk in this case. To patch the traps of MemHandleFree() and MemChunkFree(), use Example 2. "Where did MemChunkFree() suddenly come from?" you may ask. In MemoryMgr.h, Palm #defines MemPtrFree() as a wrapper of the MemChunkFree() function. Since the MemPtrFree() macro doesn't generate a trap, we must transcend this level of indirection and work directly with MemChunkFree(), which does.

When the debugging session is finished, restore the two traps to their old addresses, to which you've kept global pointers. You run into severe problems if someone else is trying to restore traps at the same time, but until you can multitask applications on the Palm OS, the threat of this is limited.

If you are working with C++, keep an eye on new, new[], delete, and delete[]. I use the C++ placement new syntax to pass in the same __LINE__ and __FILE__ information into a customized new operator. If your application already has a module that uses the placement new operator, then you can either exclude the module in question from memory leak detection by #undefing the macro remapping of new, or you can modify the existing placement new operator to incorporate __LINE__ and __FILE__.

There are three steps to remapping the new and new[] operators:

You can get away with reusing _DbgMemPtrNew because Palm OS does not yet differentiate memory allocated with the C++ new operator from that allocated with C MemPtrNew() and MemHandleNew().

Putting It to Work for You

The sample code (available electronically) implements a basic version of these leak detection tools. The MemDbg module provides sample remappings of the standard Palm allocation and deallocation functions, trap patch examples, and PDB routines. The MemDbgMacros.h file is to be included in any module in which you'd like to check for memory leaks; and the Starter.cpp file illustrates the turning on/off of leak detection for the duration of an application's execution. Figure 1 is an example of a file that leaks memory. Figure 2 shows the debug boxes of POSEr for the offending calls detected in Figure 1. By clicking the Debug button, you drop into the debugger, where you can then analyze the call stack, inspect variable contents, step through the code, and investigate hints as to why memory was leaked.

Use the sample code as a basis for your own diagnostic applications. If you decide to test for buffer overruns, for example, add code to pad the end of allocated memory blocks with a few additional repeating characters. If the caller overruns the allocated buffer, these characters (and hopefully not much else) will become trashed. In your remapped deallocation function, if the additional repeated characters aren't intact and you're certain you allocated the block passing through, then you can throw up an ErrNonFatalDisplay() to expose the original allocation by its perpetrator.

When memory is getting tight, memory heap defragmentation often plays a vital role in the survival and efficiency of the running app. To keep statistical information on the level of memory heap fragmentation, keep track of the largest free block in the memory heap via MemHeapFreeBytes(). When this value starts getting small, it's usually because the heap has become fragmented, so throw up an ErrNonFatalDisplay(), enter debug mode, and crawl the call stack to track down the offending sequence of function calls leading to this undesired state. Watch for functions that make frequent small (1-12 byte) allocations, as well as those that lock down a big chunk for long periods of time. Both actions contribute to fragmentation.

Finally, MemHandleResize() is a useful (though at times inefficient) function. However, sometimes you can't avoid having to use it. If your codebase implements it, incorporate a patch for it in your own leak detection toolkit. Patching it with a #define should be sufficient, and you just need to find and modify existing allocation records in MemDbgDB that belong to the resized chunks.

Conclusion

It is my hope that this memory leak detection toolkit will improve the quality of your Palm projects. In the spirit of the Palm development community, I would enjoy hearing of any criticisms or interesting additions that can improve this toolkit.

DDJ

Listing One

// Each node knows if it came from MemHandleNew or MemPtrNew:
typedef enum tageAllocationType { PointerType, HandleType } eAllocationType;

typedef struct tagNode
{
   Char            fileName[MemDbgFilenameSize+1];
   ULong           lineNum,
                   size;
   eAllocationType type;
   union
   {
      VoidPtr      pAddr;
      VoidHand     hAddr;
   };
} Node;

Back to Article