21C. Dangling Pointers and Memory Faults


Ownership of memory

An operating system (OS) can run several programs at once. A running program is called a process. Each process has been granted some memory by the OS, and the OS keeps careful track of just which parts of memory each process owns. The OS does not allow a process to look at or store anything into memory that it does not own. No process can interfere with any other process.

It is useful to think of different parts of the memory of a process as as having one of a few possible ownerships within the process.

  1. The heap manager is part of a process. It asks for memory from the OS as needed, and can return memory to the OS. Typically, it requests or gives back fairly large chunks of memory.

    The heap manager keeps its own table concerning the memory in the heap. As far as the heap manager is concerned, there are two kinds of memory in the heap.

    1. Some of the memory in the heap has been allocated by the heap manager to the running program, and the program is free to do whatever it chooses with that memory.

    2. Other parts of the heap are marked as belonging to the heap manager. That memory still belongs to the process, but should not be used by the program. The OS does not know about the heap manager's concept of ownership of memory in the heap. As far as the OS knows, it is all owned by the process.

  2. Some of a process's memory is used for the run-time stack. The OS keeps track of the boundary of the run-time stack, and does not allow a program to use memory that has not been allocated as part of a frame.

    Memory in the run-time stack is allocated and deallocated by the OS in small chunks that correspond to frames.


Memory faults

A memory fault occurs when a process either uses memory in an incorrect way or uses memory that does not belong to it, according to the OS. When a memory fault occurs, the OS terminates the process immediately.

On a Linux system, a memory fault is called a segmentation fault.

Memory faults are usually caused by one of three errors: (1) using dangling pointers, (2) using null pointers and (3) trying to modify read-only memory. Let's look at those in more detail.

The null pointer

Memory address 0 is called the null pointer. Your program is never allowed to look at or store anything into memory address 0, and attempting to do that will cause a memory fault.

The null pointer is a way of saying "a pointer to nothing". We will encounter uses of it later. Write a null pointer as NULL in your program. For example,

  char* p = NULL;
creates a pointer variable p and makes it hold memory address 0. We will draw a null pointer as an X;

Note that NULL is not the same as the null character; do not confuse the two. The null pointer is a pointer. The null character is a character.

(NULL is not really part of C++. It is added using the preprocessor by defining it in header files including in <cstdio>, <iostream>. #Include one of those to get NULL defined.)


Dangling pointers

A dangling pointer is a pointer to memory that your program should not use. Using dangling pointers is a common mistake among beginners. Read this and pay attention. What you ignore can lead to long hours of debugging.

There are a few forms that a dangling pointer can have.

  1. One kind of dangling pointer is an address in a part of memory that the OS knows does not belong to your process. Using such a dangling pointer leads to a memory fault.

  2. Another kind of dangling pointer is a pointer to memory in the heap that you have deleted. For example, after

      int* p = new int;
      *p = 0;
      delete p;
    
    variable p holds a dangling pointer. Using p will probably not cause a memory fault, because that memory is almost surely still owned by this process; the heap manager considers itself to be the owner. But it is an error nonetheless. The heap manager uses the memory that it owns to keep track of what it owns and what the program owns. Performing
      int* p = new int;
      delete p;
      *p = 0;
    
    can corrupt the heap-manager's information, and can cause future uses of new to fail spectacularly.

    A pointer to memory that you have deleted does not necessarily refer to memory that is owned by the heap manager. Look at the following.

      int* p = new int;
      delete p;
      int* q = new int;
      *p = 0;
    
    It is entirely possible (but not a sure thing) that the memory that was allocated to p and then given back to the heap manager is used again for q. By changing *p, you might also have changed *q because p and q might point to the same place. Your program is unwittingly using the same memory for two different purposes.

  3. Using the & operator, you can get the address of a variable in a function's frame in the run-time stack. After the function returns, the function's frame is removed, and that address becomes a dangling pointer.

    Suppose that p is a dangling pointer that points into the run-time stack. What happens when you use p depends on what other things your program has done.

    1. If p points to memory that does not belong to any allocated frame, then your process will get a memory fault. It will instantly be killed.

    2. It is possible that your program has done more function calls, causing more frames to be allocated, and causing p to point into one of those new frames. Using p will not cause a memory fault, but you cannot know what will happen with it.

      Getting the value of *p will probably just lead to a senseless value. Storing something into *p can have very bad consequences to the program, since it ruins a function's stack frame. It can destroy critical information that is used to manage the run-time stack. If you corrupt the run-time stack, all is lost. Even a debugger cannot tell what is happening in the program after that. Really.

  4. An uninitialized pointer variable is usually called a dangling pointer simply because you do not know where it points. Performing

      int* p;
      *p = 0;
    
    has unpredictable effects. It is never good. If you draw pointer diagrams and do hand simulations, you will not make this error.

  5. Using an array index that is out of the array's bounds is also considered use of a dangling pointer. For example,

      int a[10];
      a[10] = 1;
    
    has unpredictabe effects. The allowed indices of array a are 0 to 9, and a[10] does not exist. (There is no automatic check for array bounds errors. You are expected to do those checks yourself.)


Read-only memory

Recall that some memory is marked read-only. If your program tries to change something in read-only memory, it will get a memory fault. For example, a string constant is stored in read-only memory. We will see that string constants can be treated as arrays. Performing
  char* str = "a string constant";
  str[0] = 'b';
leads to a memory fault, since it tries to alter the memory where "a string constant" is stored. Do not use a string constant as a substitute for allocating memory using new.

Memory faults due to running out of memory

If your program runs out of memory, either in the run-time stack or the heap, it will probably get a memory fault. That is only likely to happen if the program runs amok and starts allocating huge amounts of memory.


Recognizing and finding memory errors

Using a dangling pointer can cause very strange errors. If something happens that appears to be impossible, then you should suspect that you are using a dangling pointer.

If you get a memory fault, use a debugger to track down exactly where the fault occurs. Just run the program in the debugger. When the memory fault occurs, the program will stop. Look at the values of pointers and array indices at that point. If you are not sure whether a pointer p is valid, try to show the value of *p.

Pointers are shown in hexadecimal notation (base 16). In a Linux system, a null pointer is shown as either 0x0 or (null); a pointer into the heap is a relatively small number such as 0x6a003c; and a pointer into the run-time stack is a very large number, such as 0xffffffffffff2104.


Exercises

  1. What is a dangling pointer? Answer

  2. What is a memory fault? Answer

  3. If you delete memory and immediately try to use it, do you always get a memory fault? Answer

  4. Look at the following.

      int* p = new int;
      int* q = p;
      *p = 50;
      *q = 100;
      delete p;
      printf("*q = %i\n", *q);
      delete q;
    
    Comment on what happens when this is run. Answer

  5.   int* p;
      *p = 50;
      printf("*p = %i\n", *p);
    
    Comment on what happens when this is run. Answer

  6. What are the consequences of using a dangling pointer? Answer