Debugging 201: Debugging Segmentation faults in C++ Programs

You have written all your code for a lab in C or C++, and when you run it all you get is "Segmentation fault", possibly followed by "(core dumped)". Now what? This writeup describes debugging programs.

Why does it say "segmentation fault (core dumped)"? "Segmentation" refers to memory being divided into regions and only certain numbers being valid memory addresses. A segmentation fault is just a generic way of saying something went wrong. The "core dumped" message means that a copy of memory was written to the disk. In theory you could load that memory into a debugger in order to figure out what went wrong. This is done on huge systems where reproducing faults is hard, it is not useful for small projects where it is easier to just re-run the code.

The first task is to determine what line of code was last executed; that is, where the error occurs. Between pipelined architecture and delayed fault detection (both important to high performance), the system cannot tell you what line was last executed successfully. Do not rely on stepping through code to find the error. Stepping works great when you are very close to the region of code with an error, but stepping through hundreds of statements to find a bug takes forever and often results in accidentally skipping over the line that contains the error.

If you are running your code in an integrated development environment (IDE), you are in luck: set breakpoints at key places in your code and see how far things go before the error happens. For example, set breakpoints just before reading any data, after reading the data, after computing results, and just before printing them. Then run in debugging mode and examine key data at each point to make sure things are going as expected. If the code gets to a breakpoint on line A but not on line B, add more breakpoints between A and B and rerun. Once you have it narrowed down to a few key lines, you can try stepping through.

If you are in an environment where you cannot set breakpoints, you can do the same with output messages. However, there is one key: send your messages to standard error. For example, add statements like

        cerr << "At point A" << endl;
        ...
        cerr << "At point B" << endl;

In both cases, you will want to store your input in a file and use Run Configurations features to read your input from that file. Retyping input over and over is too error-prone, and if you change your input you might exercise a different error instead. Focusing on one error at a time is critical.

Minimizing your input is another key debugging trick. If you have a lot of data, determining if parts of your program are working is more difficult. So reduce your data using the following algorithm:

  1. Find an input that makes your program fail. You are probably reading this because you are already there!
  2. Cut that input in half and re-run the program.
  3. If the program still fails, go back and cut your new input in half.
  4. If the program now works, restore half of the data you removed and re-run the program.
With a few quick runs you can often get your input down to exactly the lines that exercise the error. Quite often that will give you important clues as to what went wrong, but if it does not, then you at least have a smaller input to use for debugging. Often the input will now be small enough that you can step through your program, and quite frequently just knowing what input exercises the fault will help you determine the fault.

Key tip: first find what fails, then figure out why. Determining why from just seeing a fault requires experience. This is why we instructors sometimes can tell what is wrong just by hearing about the error! It's not magic, it's just that we have gone through the same thing you are going through and have learned from our own mistakes! As you develop experience debugging code, you will get much better at it. This is one of the more important things you will learn in this course!