Some of the worst bugs are those that disappear when normal debugging techniques are applied. These include behaviors such as:
printf()
, but not when
the printf()
is taken away.
To avoid these kinds of behaviors, debugging techniques should try and perturb the program as little as possible.
Unfortunately, the extended header pointer tracking technique discussed in the last section greatly perturbs the memory allocator behavior. It changes the sizes of the objects being allocated, which in turn will change the order in which memory blocks are allocated.
Warning: This section is extremely system dependent. The ideas should be generally applicable; however, the exact implementations described will need to be tailored to fit your system.
A much less intrusive solution is to store the extra debugging information about an object in a parallel structure, in memory obtained using a mechanism that does not change the memory allocator behavior at all. The debug memory obtained should be the same size as the expected maximum heap size.
Some of the techniques available to obtain memory for use without changing the behavior of the allocator are:
shmat()
, with an address you expect to be above
the top of the expected size of the heap.
mmap()
, again with an address you expect to be above the
top of the expected size of the heap.
alloca()
in main()
to allocate memory on the stack.
malloc()
in main()
to allocate the
required large chunk of memory; hopefully this will not cause too
much change in the behavior of the program.
You will also have to be able to obtain the address of the start of the heap. This was shown in an earlier section.
Putting these ideas together:
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
struct exthdr_ptr {
char * xhp_type_name;
unsigned xhp_number;
};
#define type_name_exthdr_ptr(_xhp) ((_xhp)->xhp_type_name)
#define number_exthdr_ptr(_xhp) ((_xhp)->xhp_number)
/* These are setup else where */
extern char * base_heap_ptr;
extern char * base_debug_ptr;
extern unsigned size_debug_ptr;
void *
do_xalloc(
unsigned size,
char * type_name,
unsigned number
)
{
char * ptr = malloc(size);
unsigned offset = ptr - base_heap_ptr;
struct exthdr_ptr * hdr =
(struct exthdr_ptr *)(base_debug_ptr+offset);
assert( ptr >= base_heap_ptr );
assert( offset + sizeof(struct exthdr_ptr) < size_debug_ptr );
type_name_exthdr_ptr(hdr) = type_name;
number_exthdr_ptr(hdr) = number;
return ptr;
}
int
check_exthdr_ptr(
void * ptr,
char * type_name,
unsigned number
)
{
unsigned offset = ptr - base_heap_ptr;
struct exthdr_ptr * hdr =
(struct exthdr_ptr *)(base_debug_ptr+offset);
assert( ptr >= base_heap_ptr );
assert( offset + sizeof(struct exthdr_ptr) < size_debug_ptr );
if( number != number_exthdr_ptr(hdr) ) {
return 0;
}
if( type_name != type_name_exthdr_ptr(hdr) ) {
if( strcmp(type_name, type_name_exthdr_ptr(hdr)) != 0 ) {
return 0;
}
type_name_exthdr_ptr(hdr) = type_name;
}
return 1;
}
This approach establishes a structure in the debug memory parallel to each allocated object. For the case of an int_stack
, the relationship looks like:
base_heap_ptr: base_debug_ptr:
. .
. .
. .
. .
__________ .
| 25 | .
|________| __________
stack-->| 16 | | +---> "struct int_stack"
|________| |________|
| 0 | |UINT_MAX|
|________| |________|
| |
|________|
We do not have to restrict ourselves to just keeping track of type and size information. For instance, if the minimum size object is 16 bytes, we might be able to also track the file name and line number where the object was allocated.