Chapter 5. Memory Management

All nontrivial C programs allocate memory at runtime. The standard C library provides four memory-management routines: malloc, calloc, realloc, and free. The Mem interface repackages these routines as a set of macros and routines that are less prone to error and that provide a few additional capabilities.

Unfortunately, memory-management bugs are common in C programs, and they are often difficult to diagnose and fix. For example, the fragment

   p = malloc(nbytes);
   ...
   free(p);

calls malloc to allocate a block of nbytes of memory, assigns the address of the first byte of that block to p, uses p and the block it points to, and frees the block. After the call to free, p holds a dangling pointer — a pointer that refers to memory that logically does not exist. Subsequently dereferencing p is an error, although if the block hasn’t been reallocated for another purpose, the error might go undetected. This behavior is what makes these kinds of access errors hard to diagnose: when the error is detected, it may manifest itself at a place and time far away from the origin of the error.

The fragment

   p = malloc(nbytes);
   ...
   free(p);
   ...
   free(p);

illustrates another error: deallocating free memory. This error usually corrupts the data structures used by the memory-management functions, but it may go undetected until a subsequent call to one of those functions.

Another error is deallocating memory that wasn’t allocated by malloc, calloc, or realloc. For example, the intent of

   char buf[20], *p;
   if (n >= sizeof buf)
       p = malloc(n);
   else
       p = buf;
   ...
   free(p);

is to avoid allocation when n is less than the size of buf; but the code erroneously calls free even when p points to buf. Again, this error usually corrupts the memory-management data structures and isn’t detected until later.

Finally, the function

   void itoa(int n, char *buf, int size) {
       char *p = malloc(43);

       sprintf(p, "%d", n);
       if (strlen(p) >= size - 1) {
           while (--size > 0)
               *buf++ = '*';
           *buf = '';
       } else
           strcpy(buf, p);
   }

fills buf[0..size-1] with the decimal representation of the integer n or with asterisks if that representation takes more than size-1 characters. This code looks robust, but it contains at least two errors. First, malloc returns the null pointer if the allocation fails, and the code fails to test for this condition. Second, the code creates a memory leak: it doesn’t deallocate the memory it allocates. The program will slowly consume memory each time itoa is called. If itoa is called often, the program will eventually run out memory and fail. Also, itoa works correctly when size is less than two, but it does so by setting buf[0] to the null character. Perhaps a better design would be to insist that size exceed two and to enforce that constraint with a checked runtime error.

The macros and routines in the Mem interface offer some protection from these kinds of memory-management errors. They don’t eliminate all such errors, however. For example, they can’t guard against dereferencing corrupt pointers or using pointers to local variables that have gone out of scope. C novices often commit the latter error; this apparently simpler version of itoa is an example:

   char *itoa(int n) {
       char buf[43];

       sprintf(buf, "%d", n);
       return buf;
   }

itoa returns the address of its local array buf, but once itoa returns, buf no longer exists.

Interface

The Mem interface exports exceptions, routines, and macros:

<mem.h>≡
  #ifndef MEM_INCLUDED
  #define MEM_INCLUDED
  #include "except.h"

  <exported exceptions 70>
  <exported functions 70>
  <exported macros 70>

  #endif

Mem’s allocation functions are similar to those in the standard C library, but they don’t accept zero sizes and never return null pointers:

<exported exceptions 70>≡
   extern const Except_T Mem_Failed;

<exported functions 70>≡
   extern void *Mem_alloc (long nbytes,
       const char *file, int line);
   extern void *Mem_calloc(long count, long nbytes,
       const char *file, int line);

Mem_alloc allocates a block of at least nbytes and returns a pointer to the first byte. The block is aligned on an addressing boundary that is suitable for the data with the strictest alignment requirement. The contents of the block are uninitialized. It is a checked runtime error for nbytes to be nonpositive.

Mem_calloc allocates a block large enough to hold an array of count elements each of size nbytes, and returns a pointer to the first element. The block is aligned as for Mem_alloc, and is initialized to zeros. The null pointer and 0.0 are not necessarily represented by zeros, so Mem_calloc may not initialize them correctly. It is a checked runtime error for count or nbytes to be nonpositive.

The last two arguments to Mem_alloc and Mem_calloc are the file name and line number of the location of the call. These are supplied by the following macros, which are the usual way to invoke these functions.

<exported macros 70>≡
   #define ALLOC(nbytes) 
       Mem_alloc((nbytes), __FILE__, __LINE__)
   #define CALLOC(count, nbytes) 
       Mem_calloc((count),  (nbytes), __FILE__, __LINE__)

If Mem_alloc or Mem_calloc cannot allocate the memory requested, they raise Mem_Failed and pass file and line to Except_raise so that the exception reports the location of the call. If file is the null pointer, Mem_alloc and Mem_calloc supply the locations within their implementations that raise Mem_Failed.

Many allocations have the form

   struct T *p;
   p = Mem_alloc(sizeof (struct T));

which allocates a block for an instance of the structure T and returns a pointer to that block. A better version of this idiom is

   p = Mem_alloc(sizeof *p);

Using sizeof *p instead of sizeof (struct T) works for any pointer type, except void pointers, and sizeof *p is independent of the pointer’s referent type. If the type of p is changed, this allocation remains correct, but the one with sizeof (struct T) must be changed to reflect the change in p’s type. That is,

   p = Mem_alloc(sizeof (struct T));

is correct only if p is really a pointer to a struct T. If p is changed to a pointer to another structure and the call isn’t updated, the call may allocate too much memory, which wastes space, or too little memory, which is disastrous, because the client may scribble on unallocated storage.

This allocation idiom is so common that Mem provides macros that encapsulate both the allocation and the assignment:

<exported macros 70>+≡
   #define  NEW(p) ((p) = ALLOC((long)sizeof *(p)))
   #define NEW0(p) ((p) = CALLOC(1, (long)sizeof *(p)))

NEW(p) allocates an uninitialized block to hold *p and sets p to the address of that block. NEW0(p) does the same, but also clears the block. NEW is provided on the assumption that most clients initialize a block immediately after allocating it. The argument to the compile-time operator sizeof is used only for its type; it is not evaluated at runtime. So NEW and NEW0 evaluate p exactly once, and it’s safe to use an expression that has side effects as an actual argument to either macro, such as NEW(a[i++]), for example.

malloc and calloc take arguments of type size_t; sizeof yields a constant of type size_t. The type size_t is an unsigned integral type capable of representing the size of the largest object that can be declared, and it’s used in the standard library wherever object sizes are specified. In practice, size_t is either unsigned int or unsigned long. Mem_alloc and Mem_calloc take integer arguments to avoid errors when negative numbers are passed to unsigned arguments. For example,

   int n = -1;
   ...
   p = malloc(n);

is clearly an error, but many implementations of malloc won’t catch the error because when −1 is converted to a size_t, it usually winds up as a very large unsigned value.

Memory is deallocated by Mem_free:

<exported functions 70>+≡
   extern void Mem_free(void *ptr,
       const char *file, int line);

<exported macros 70>+≡
   #define FREE(ptr) ((void)(Mem_free((ptr), 
       __FILE__, __LINE__), (ptr) = 0))

Mem_free takes a pointer to the block to be deallocated. If ptr is nonnull, Mem_free deallocates that block; if ptr is null, Mem_free has no effect. The FREE macro also takes a pointer to a block, calls Mem_free to deallocate the block, and sets ptr to the null pointer, which, as mentioned in Section 2.4, helps avoid dangling pointers. Since ptr is null after its referent has been deallocated by FREE, a subsequent dereference will usually cause the program to crash with some kind of addressing error. This definite error is better than the unpredictable behavior that dereferencing a dangling pointer can cause. Note that FREE evaluates ptr more than once.

As detailed in the sections that follow, there are two implementations that export the Mem interface. The checking implementation implements checked runtime errors to help catch access errors like those described in the previous section. In that implementation, it is a checked runtime error to pass Mem_free a nonnull ptr that was not returned by a previous call to Mem_alloc, Mem_calloc, or Mem_resize, or a ptr that has already been passed to Mem_free or Mem_resize. The values of Mem_free’s file and line arguments are used to report these checked runtime errors.

In the production implementation, however, these access errors are unchecked runtime errors.

The function

<exported functions 70>+≡
   extern void *Mem_resize(void *ptr, long nbytes,
       const char *file, int line);

<exported macros 70>+≡
   #define RESIZE(ptr, nbytes) ((ptr) = Mem_resize((ptr), 
       (nbytes), __FILE__, __LINE__))

changes the size of the block allocated by a previous call to Mem_alloc, Mem_calloc, or Mem_resize. Like Mem_free, the first argument to Mem_resize is the pointer that holds the address of the block whose size is to be changed. Mem_resize expands or contracts the block so that it holds at least nbytes of memory, suitably aligned, and returns a pointer to the resized block. Mem_resize may move the block in order to change its size, so Mem_resize is logically equivalent to allocating a new block, copying some or all of the data from ptr to the new block, and deallocating ptr. If Mem_resize cannot allocate the new block, it raises Mem_Failed, with file and line as the exception coordinates. The macro RESIZE changes ptr to point at the new block — a common use of Mem_resize. Note that RESIZE evaluates ptr more than once.

If nbytes exceeds the size of the block pointed to by ptr, the excess bytes are uninitialized. Otherwise, nbytes beginning at ptr are copied to the new block.

It is a checked runtime error to pass Mem_resize a null ptr, and for nbytes to be nonpositive. In the checking implementation, it is a checked runtime error to pass Mem_resize a ptr that was not returned by a previous call to Mem_alloc, Mem_calloc, or Mem_resize, and to pass it one that has already been passed to Mem_free or Mem_resize. In the production implementation, these access errors are unchecked runtime errors.

The functions in the Mem interface can be used in addition to the standard C library functions malloc, calloc, realloc, and free. That is, a program can use both sets of allocation functions. The access errors reported as checked runtime errors by the checking implementation apply only to memory managed by that implementation. Only one implementation of the Mem interface may be used in any given program.

Production Implementation

In the production implementation, the routines encapsulate calls to the memory-management functions in the standard library in the safer package specified by the Mem interface:

<mem.c>≡
  #include <stdlib.h>
  #include <stddef.h>
  #include "assert.h"
  #include "except.h"
  #include "mem.h"

  <data 74>
  <functions 74>

For example, Mem_alloc calls malloc and raises Mem_Failed when malloc returns the null pointer:

<functions 74>≡
   void *Mem_alloc(long nbytes, const char *file, int line){
       void *ptr;

       assert(nbytes > 0);
       ptr = malloc(nbytes);
       if (ptr == NULL)
           <raise Mem_Failed 74>
       return ptr;
   }

<raise Mem_Failed 74>≡
   {
        if (file == NULL)
            RAISE(Mem_Failed);
        else
            Except_raise(&Mem_Failed, file, line);
   }

<data 74>≡
  const Except_T Mem_Failed = { "Allocation Failed" };

If a client doesn’t handle Mem_Failed, Except_raise will give the caller’s coordinates, which are passed to Mem_alloc when it reports the unhandled exception. For example:

Uncaught exception Allocation Failed raised @parse.c:431 aborting...

Similarly, Mem_calloc encapsulates a call to calloc:

<functions 74>+≡
   void *Mem_calloc(long count, long nbytes,
       const char *file, int line) {
       void *ptr;

       assert(count > 0);
       assert(nbytes > 0);
       ptr = calloc(count, nbytes);
       if (ptr == NULL)
           <raise Mem_Failed 74>
       return ptr;
   }

When either count or nbytes is zero, calloc’s behavior is implementation defined. The Mem interface specifies what happens in these cases, which is one of its advantages and helps avoid bugs.

Mem_free just calls free:

<functions 74>+≡
   void Mem_free(void *ptr, const char *file, int line) {
       if (ptr)
           free(ptr);
   }

The standard permits null pointers to be passed to free, but Mem_free doesn’t pass them, because old implementations of free may not accept null pointers.

Mem_resize has a much simpler specification than does realloc, which is reflected in its simpler implementation:

<functions 74>+≡
   void *Mem_resize(void *ptr, long nbytes,
       const char *file, int line) {

       assert(ptr);
       assert(nbytes > 0);
       ptr = realloc(ptr, nbytes);
       if (ptr == NULL)
           <raise Mem_Failed 74>
       return ptr;
   }

Mem_resize’s only purpose is to change the size of an existing block. realloc does this, too, but it also frees a block when nbytes is zero and allocates a block when ptr is the null pointer. These additional capabilities, which are only loosely related to changing the size of an existing block, invite bugs.

Checking Implementation

The functions exported by the checking implementation of the Mem interface catch the kinds of access errors described at the beginning of this chapter and report them as checked runtime errors.

<memchk.c>≡
  #include <stdlib.h>
  #include <string.h>
  #include "assert.h"
  #include "except.h"
  #include "mem.h"

  <checking types 80>
  <checking macros 79>
  <data 74>
  <checking data 77>
  <checking functions 79>

Mem_free and Mem_resize can detect access errors if Mem_alloc, Mem_calloc, and Mem_resize never return the same address twice and if they remember all of the addresses they do return and which ones refer to allocated memory. Abstractly, these functions maintain a set S whose elements are the pairs (α, free) or (α, allocated), where α is the address returned by an allocation. The value free indicates that the address α does not refer to allocated memory; that is, it has been deallocated explicitly, and the value allocated indicates that α points to allocated memory.

Mem_alloc and Mem_calloc add the pair (ptr, allocated) to S, where ptr is their return value, and they guarantee that neither (ptr, allocated) nor (ptr, free) was in S before the addition. Mem_free(ptr) is legal if ptr is null or if (ptr, allocated) is in S. If ptr is nonnull and (ptr, allocated) is in S, Mem_free deallocates the block at ptr and changes the entry in S to (ptr, free). Similarly, Mem_resize(ptr, nbytes, ...) is legal only if (ptr, allocated) is in S. If so, Mem_resize calls Mem_alloc to allocate a new block, copies the contents of the old one to the new one, and calls Mem_free to deallocate the old one; these calls make the appropriate changes to S.

The condition that the allocation functions never return the same address twice can be implemented by never deallocating anything. This approach wastes space, and it’s easy to do better: never deallocate the byte at an address previously returned by an allocation function. S can be implemented by keeping a table of the addresses of these bytes.

This scheme can be implemented by writing a memory allocator that sits on top of the standard library functions. This allocator maintains a hash table of block descriptors:

<checking data 77>≡
   static struct descriptor {
       struct descriptor *free;
       struct descriptor *link;
       const void *ptr;
       long size;
       const char *file;
       int line;
   } *htab[2048];

ptr is the address of the block, which is allocated elsewhere as described below, and size is the size of the block. file and line are the block’s allocation coordinates — the source coordinates passed to the function that allocated the block. These values aren’t used, but they’re stored in descriptors so that debuggers can print them during a debugging session.

The link fields form a list of descriptors for blocks that hash to the same index in htab, which is an array of pointers to descriptors. These descriptors also form a list of free blocks; the head of this list is the dummy descriptor

<checking data 77>+≡
   static struct descriptor freelist = { &freelist };

and the list is threaded through the free fields of the descriptors. This list is circular: freelist is the last descriptor on the list and its free field points to the first descriptor. At any given time, htab holds descriptors for all of the blocks, both free and allocated, and the free blocks are on freelist. Thus, the descriptor’s free field is null if the block is allocated and nonnull if it’s free, and htab implements S. Figure 5.1 shows these data structures at one point in time. The space associated with each descriptor structure appears behind it. Shaded spaces are allocated; clear spaces are free, solid lines emanate from link fields, and the dotted lines show the free list.

htab and freelist structures

Figure 5.1. htab and freelist structures

Given an address, find searches for its descriptor. It returns either a pointer to the descriptor or the null pointer:

<checking functions 79>≡
   static struct descriptor *find(const void *ptr) {
       struct descriptor *bp = htab[hash(ptr, htab)];

       while (bp && bp->ptr != ptr)
           bp = bp->link;
       return bp;
   }

<checking macros 79>≡
   #define hash(p, t) (((unsigned long)(p)>>3) & 
       (sizeof (t)/sizeof ((t)[0])-1))

The hash macro treats the address as a bit pattern, shifts it right three bits, and reduces it modulo the size of htab. find is enough to write a version of Mem_free in which access errors are checked runtime errors:

<checking functions 79>+≡
   void Mem_free(void *ptr, const char *file, int line) {
       if (ptr) {
           struct descriptor *bp;
           <set bp if ptr is valid 79>
           bp->free = freelist.free;
           freelist.free = bp;
       }
   }

If ptr is nonnull and is a valid address, the block is deallocated by appending it to the free list for possible reuse by a subsequent call to Mem_alloc. A pointer is valid if it points to an allocated block:

<set bp if ptr is valid 79>≡
   if (((unsigned long)ptr)%(sizeof (union align)) != 0
   || (bp = find(ptr)) == NULL || bp->free)
       Except_raise(&Assert_Failed, file, line);

The test ((unsigned long)ptr)%(sizeof (union align)) != 0 avoids calls to find for those addresses that aren’t multiples of the strictest alignment and thus cannot possibly be valid block pointers.

As shown below, Mem_alloc always returns pointers that are aligned on an address that is a multiple of the size of the following union.

<checking types 80>≡
   union align {
       int i;
       long l;
       long *lp;
       void *p;
       void (*fp)(void);
       float f;
       double d;
       long double ld;
   };

This alignment ensures that any type of data can be stored in the blocks returned by Mem_alloc. If the ptr passed to Mem_free isn’t so aligned, it can’t possibly be in htab and is thus invalid.

Mem_resize catches access errors by making the same check, and then calls Mem_free, Mem_alloc, and the library function memcpy:

<checking functions 79>+≡
   void *Mem_resize(void *ptr, long nbytes,
       const char *file, int line) {
       struct descriptor *bp;
       void *newptr;

       assert(ptr);
       assert(nbytes > 0);
       <set bp if ptr is valid 79>
       newptr = Mem_alloc(nbytes, file, line);
       memcpy(newptr, ptr,
           nbytes < bp->size ? nbytes : bp->size);
       Mem_free(ptr, file, line);
       return newptr;
   }

Likewise, Mem_calloc can be implemented by calling Mem_alloc and the library function memset:

<checking functions 79>+≡
   void *Mem_calloc(long count, long nbytes,
       const char *file, int line) {
       void *ptr;

       assert(count > 0);
       assert(nbytes > 0);
       ptr = Mem_alloc(count*nbytes, file, line);
       memset(ptr, '', count*nbytes);
       return ptr;
   }

All that remains is to allocate the descriptors themselves and the code for Mem_alloc. One way to do both tasks with one allocation is to allocate a block large enough to hold a descriptor and the storage requested by a call to Mem_alloc. This approach has two drawbacks. First, it complicates carving up a block of free storage to satisfy several smaller requests, because each request needs its own descriptor. Second, it makes the descriptors vulnerable to corruption by writes through pointers or indices that stray just outside of allocated blocks.

Allocating descriptors separately decouples their allocations from those done by Mem_alloc and reduces — but does not eliminate — the chances that they will be corrupted. dalloc allocates, initializes, and returns one descriptor, doling it out of the 512-descriptor chunks obtained from malloc:

<checking functions 79>+≡
   static struct descriptor *dalloc(void *ptr, long size,
       const char *file, int line) {
       static struct descriptor *avail;
       static int nleft;

       if (nleft <= 0) {
           <allocate descriptors 82>
           nleft = NDESCRIPTORS;
       }
       avail->ptr = ptr;
       avail->size = size;
       avail->file = file;
       avail->line = line;
       avail->free = avail->link = NULL;
       nleft--;
       return avail++;
   }

<checking macros 79>+≡
   #define NDESCRIPTORS 512

The call to malloc might return the null pointer, which dalloc passes back to its caller.

<allocate descriptors 82>≡
   avail = malloc(NDESCRIPTORS*sizeof (*avail));
   if (avail == NULL)
       return NULL;

As shown below, Mem_alloc raises Mem_Failed when dalloc returns the null pointer.

Mem_alloc allocates a block of memory using the first-fit algorithm, one of many memory-allocation algorithms. It searches freelist for the first free block that is large enough to satisfy the request and divides that block to fill the request. If freelist doesn’t contain a suitable block, Mem_alloc calls malloc to allocate a chunk of memory that’s larger than nbytes, adds this chunk onto the free list, and tries again. Since the new chunk is larger than nbytes, it is used to fill the request the second time around. Here’s the code:

<checking functions 79>+≡
   void *Mem_alloc(long nbytes, const char *file, int line){
       struct descriptor *bp;
       void *ptr;

       assert(nbytes > 0);
       <round nbytes up to an alignment boundary 83>
       for (bp = freelist.free; bp; bp = bp->free) {
           if (bp->size > nbytes) {
               <use the end of the block at bp->ptr 83>
           }
           if (bp == &freelist) {
               struct descriptor *newptr;
               <newptr ← a block of size NALLOC + nbytes 84>
               newptr->free = freelist.free;
               freelist.free = newptr;
           }
       }
       assert(0);
       return NULL;
   }

Mem_alloc starts by rounding nbytes up so that every pointer it returns is a multiple of the size of the union align:

<round nbytes up to an alignment boundary 83>≡
   nbytes = ((nbytes + sizeof (union align) - 1)/
       (sizeof (union align)))*(sizeof (union align));

freelist.free points to the beginning of the free list, which is where the for loop starts. The first free block whose size exceeds nbytes is used to fill the request. The nbytes at the end of this free block are carved off, and the address of that block is returned after its descriptor is created, initialized, and added to htab:

<use the end of the block at bp->ptr 83>≡
  bp->size -= nbytes;
  ptr = (char *)bp->ptr + bp->size;
  if ((bp = dalloc(ptr, nbytes, file, line)) != NULL) {
      unsigned h = hash(ptr, htab);
      bp->link = htab[h];
      htab[h] = bp;
      return ptr;
  } else
      <raise Mem_Failed 74>

Figure 5.2 shows the effect of this chunk: on the left is a descriptor that points to some free space before it’s carved up. On the right, the allocated space is shaded and a new descriptor points to it. Notice that the new descriptor’s free list link is null.

Allocating the tail of a free block

Figure 5.2. Allocating the tail of a free block

The test bp->size> nbytes guarantees that the value of bp->ptr is never reused. Large free blocks are divided to fill smaller requests until they’re reduced to sizeof (union align) bytes, after which bp->size never exceeds nbytes. The first sizeof (union align) bytes of each chunk are never allocated.

If bp reaches freelist, the list does not hold a block whose size exceeds nbytes. In that case, a new chunk of size

<checking macros 79>+≡
   #define NALLOC ((4096 + sizeof (union align) - 1)/ 
       (sizeof (union align)))*(sizeof (union align))

plus nbytes is added to the beginning of the free list; it will be visited on the next iteration of the for loop and will be used to fill the request. This new chunk has a descriptor just as if it had been previously allocated and freed:

<newptr ← a block of size NALLOC + nbytes 84>≡
   if ((ptr = malloc(nbytes + NALLOC)) == NULL
   ||  (newptr = dalloc(ptr, nbytes + NALLOC,
           __FILE__, __LINE__)) == NULL)
       <raise Mem_Failed 74>

Further Reading

One of the purposes of Mem is to improve the interface to the standard C allocation functions. Maguire (1993) gives a critique of these functions and describes a similar repackaging.

Memory-allocation bugs so pervade C programs that entire companies are devoted to building and selling tools that help diagnose and fix such bugs. One of the best is Purify (Hastings and Joyce 1992), which detects almost all kinds of access errors, including those described in Section 5.3. Purify checks every load and store instruction; since it does so by editing object code, it can be used even when source code is unavailable, such as for proprietary libraries. Instrumenting source code to catch access errors is at the other end of the implementation spectrum; for example, Austin, Breach, and Sohi (1994) describe a system in which “safe” pointers carry enough information to catch a wide range of access errors. LCLint (Evans 1996) has many of the features of tools like PC-Lint and can detect many potential memory-allocation errors at compile time.

Knuth (1973a) surveys all of the important memory-allocation algorithms, and explains why first fit is usually better than, for example, best fit, which looks for the free block whose size is closest to the request. The first-fit algorithm used in Mem_alloc is similar to the one described in Section 8.7 of Kernighan and Ritchie (1988).

There are endless variations on most memory-mangement algorithms, usually designed to improve performance for specific applications or allocation patterns. Quick fit (Weinstock and Wulf 1988) is one of the most widely used. It capitalizes on the observation that many applications allocate blocks of only few different sizes. Quick fit keeps N free lists, one for each of the N most frequently requested sizes. Allocating a block of one of these sizes simply removes the first block from the appropriate list, and freeing a block adds it to the appropriate list. When the lists are empty or the request is for an odd size, an alternate algorithm, such as first fit, is used.

Grunwald and Zorn (1993) describe a system that generates implementations of malloc and free tuned for a specific application. They first run the application with versions of malloc and free that collect statistics about block sizes, frequency of allocation versus deallocation, and so forth. They then feed these data to a program that generates source code for versions of malloc and free customized for the application. These versions often use quick fit with a small, application-specific set of block sizes.

Exercises

5.1

Maguire (1993) advocates initializing uninitialized memory to some distinctive bit pattern to help diagnose bugs that are caused by accessing uninitialized memory. What are the properties of a good bit pattern? Propose a suitable bit pattern and change the checking implementation of Mem_alloc to use it. Try to find an application where this change catches a bug.

5.2

Once a free block is whittled down to sizeof (union align) bytes in the chunk <use the end of the block at bp->ptr 83>, it can never satisfy a request yet remains in the free list. Change this code to remove such blocks. Can you find an application for which measurements can detect the effect of this improvement?

5.3

Most implementations of first fit, such as the one in Section 8.7 of Kernighan and Ritchie (1988), combine adjacent free blocks to form larger free blocks. The checking implementation of Mem_alloc doesn’t combine adjacent free blocks because it may not return the same address twice. Devise an algorithm for Mem_alloc that can combine adjacent free blocks without returning the same address twice.

5.4

Some programmers might argue that raising Assert_Failure in Mem_free is a draconian reaction to an access error because execution can continue if the erroneous call is simply logged and then ignored. Implement

extern void Mem_log(FILE *log);

If Mem_log is passed a nonnull file pointer, it announces access errors by writing messages to log instead of by raising Assert_Failure. These messages can record the coordinates of the erroneous call and of the allocation coordinates. For example, when Mem_free is called with a pointer to a block that has already been freed, it might write

** freeing free memory
Mem_free(0x6418) called from parse.c:461
This block is 48 bytes long and was allocated from sym.c:123

Similarly, when Mem_resize is called with a bad pointer, it might report

** resizing unallocated memory
Mem_resize(0xf7fff930,640) called from types.c:1101

Permit Mem_log(NULL) to turn off logging and reinstate assertion failure for access errors.

5.5

The checking implementation has all of the information it needs to report potential memory leaks. As described on page 68, a memory leak is an allocated block that is not referenced by any pointer and thus cannot be deallocated. Leaks cause programs to run out of memory eventually. They aren’t a problem for programs that run for only a short time, but they’re a serious problem for long-running programs, such as user interfaces and servers. Implement

extern void Mem_leak(apply(void *ptr, long size,
    const char *file, int line, void *cl), void *cl);

which calls the function pointed to by apply for every allocated block; ptr is the location of the block, size is its allocated size, and file and line are its allocation coordinates. Clients can pass an application-specific pointer, cl, to Mem_leak, and this pointer is passed along to apply as its last argument. Mem_leak doesn’t know what cl is for, but presumably apply does. Together, apply and cl are called a closure: They specify an operation and some context-specific data for that operation. For example,

void inuse(void *ptr, long size,
    const char *file, int line, void *cl) {
    FILE *log = cl;

    fprintf(log, "** memory in use at %p
", ptr);
    fprintf(log, "This block is %ld bytes long "
        "and was allocated from %s:%d
", size,
        file, line);
}

writes messages like

** memory in use at 0x13428
This block is 32 bytes long and was allocated from gen.c:23

to the log file described in the previous exercise. inuse is called by passing it and the file pointer for the log file to Mem_leak:

Mem_leak(inuse, log);
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.15.128.243