lab_memory

Malevolent Memories

Assignment Description

In this lab, you will learn about the memory checking utility valgrind.

Valgrind will help you detect memory errors and practice implementing the big three. Valgrind is an extremely useful tool for debugging memory problems and for fixing segfaults or other crashes. This lab is also particularly important because we will be checking for memory errors and leaks on your assignments. You will lose points for memory leaks and/or memory errors (we will also teach you the difference between a memory leak and a memory error). You should check your code with Valgrind before handing it in. You should also be aware that Valgrind will only detect a leak if your program allocates memory and then fails to deallocate it. It cannot find a leak unless the code containing the leak is executed when the program runs. Thus, you should be sure to test your code thoroughly and check these tests with Valgrind.

While this page describes debugging tools and commands, if you want a second perspective please see the Valgrind and GDB resource pages.

Lab Insight

As you can tell from the description, this lab will be heavily focused on memory management and good practices when dealing with memory management issues such as memory leaks. (You can refresh what memory leak means here). It is even possible to build lightweight tools similar to Valgrind, where these tools can trace the memory used by the program and report information related to memory management issues such as memory leaks. If this lab is interesting for you, CS 341 is a course that covers both how to write memory allocation models as well as how to develop tools similar to Valgrind in functionality.

Code Description

For this lab, you will be fixing bugs in course staff’s Student-To-Room allocation program. Since many CS classes are very large (CS 225 has nearly 1200 students!), exams are usually spread across several rooms. Before each exam, course staff have to allocate different students to different rooms, so that everyone can take the test with enough space.

For example, if there were only two classrooms of equal size, students in the first half of the alphabet (last name letters A - N) might go to Siebel 1404, while students in the second half of the alphabet (letters M - Z) might go to DCL 1320.

However, with more rooms, this problem becomes more difficult. In the sample situation provided, there are 9 classrooms for the exam, varying in seating capacity from 43 to 70 seats (i.e. 21 to 35 students seated every-other-desk). Although we’ll have to break up the alphabet more, we’d still like to assign students with the same first letter of their last name to the same room, as this makes going to the right room easier.

We’ve provided you the code to solve this problem, however, it has several memory bugs in it. You’ll have to use Valgrind, as well as some debugging skills from lab_debug, to find the bugs and fix them. Note, there are no bugs in the fileio namespace.

A reference for the lab is provided for you in Doxygen form.

Checking Out the Code

All assignments will be distributed via our release repo on github this semester. You will need to have set up your git directory to have our release as a remote repo as described in our git set up.

You can merge the assignments as they are released into your personal repo with

git pull --no-edit --no-rebase release main --allow-unrelated-histories
git push

The first git command will fetch and merge changes from the main branch on your remote repository named release into your personal. The --no-edit flag automatically generates a commit message for you, and the--no-rebase flag will merge the upstream branch into the current branch. Generally, these two flags shouldn’t be used, but are included for ease of merging assignments into your repo.

The second command will push to origin (your personal), which will allow it to track the new changes from release.

You will need to run these commands for every assignment that is released.

All the files for this lab are in the lab_memory directory.

Preparing Your Code

This semester for MPs we are using CMake rather than just make. This allows for us to use libraries such as Catch2 that can be installed in your system rather than providing them with each assignment. This change does mean that for each assignment you need to use CMake to build your own custom makefiles. To do this you need to run the following in the base directory of the assignment. Which in this assignment is the lab_memory directory.

mkdir build
cd build

This first makes a new directory in your assignment directory called build. This is where you will actually build the assignment and then moves to that directory. This is not included in the provided code since we are following industry standard practices and you would normally exclude the build directory from any source control system.

Now you need to actually run CMake as follows.

cmake ..

This runs CMake to initialize the current directory which is the build directory you just made as the location to build the assignment. The one argument to CMake here is .. which referes to the parent of the current directory which in this case is top of the assignment. This directory has the files CMake needs to setup your assignment to be build.

At this point you can in the build directory run make as described to build the various programs for the MP.

Background on Valgrind

Valgrind is a useful tool to detect memory errors and memory leaks.

Valgrind is a free utility for memory debugging, memory leak detection, and profiling. If you are working natively, be aware that Valgrind runs only on Linux systems and needs to be compiled with the debug options -g and -O0. Ho. However for everyone using our cmake and VM infrastructure you can run valgrind simply by following the following commands:

valgrind ./yourprogram

To instruct valgrind to also check for memory leaks, run:

valgrind --leak-check=full ./yourprogram

If you are struggling to read the full valgrind output or would simply prefer to work with a text file, you might want to run the following (You can name the text file whatever you like):

valgrind --log-file="valgrind_log.txt" ./yourprogram

You will see a report about all found mistakes or inconsistencies. Each row of the report starts with the process ID (the process ID is a number assigned by the operating system to a running program). Each error has a description, a stack trace (showing where the error occurred), and other data about the error. It is important to eliminate errors in the order that they occur during execution, since a single error early could cause others later on.

Here is a list of some of the errors that Valgrind can detect and report. (Note that not all of these errors are present in the exercise code.)

  • Invalid read/write errors. This error will happen when your code reads or writes to a memory address which you did not allocate. Sometimes this error occurs when an array is indexed beyond its boundary, which is referred to as an “overrun” error. Unfortunately, Valgrind is unable to check for locally-allocated arrays (i.e., those that are on the stack.) Overrun checking is only performed for dynamic memory.
  • Use of an uninitialized value. This type of error will occur when your code uses a declared variable before any kind of explicit assignment is made to the variable.
  • Invalid free error. This occurs when your code attempts to delete allocated memory twice, or delete memory that was not allocated with new.
  • Mismatched free() / delete / delete []. Valgrind keeps track of the method your code uses when allocating memory. If it is deallocated with different method, you will be notified about the error.
  • Memory leak detection. Valgrind can detect three sources of memory leakage.
    • A still reachable block happens when you forget to delete an object, the pointer to the object still exists, and the memory for object is still allocated.
    • A lost block is a little tricky. A pointer to some part of the block of memory still exists, but it is not clear whether it is pointing to the block or is independent of it.
    • A definitely lost block occurs when the block is not deleted but no pointer to it is found.

More information about the Valgrind utility can be found at the following links:

Fixing Memory Bugs (using Valgrind)

Before fixing the bugs, you’ll need to compile the code:

make

This will create an executable file called memory, which you can run with:

./memory

You can then run Valgrind on memory:

valgrind --log-file="valgrind_log.txt" ./memory

This works fine for fixing the memory errors, however, to fix the memory leaks, you’ll need to add --leak-check=full before ./memory:

valgrind --leak-check=full --log-file="valgrind_log.txt" ./memory

Once you have fixed all the Valgrind errors, you can test your program output using:

./memory > output.txt
diff output.txt ../data/soln_output.txt

Note that most of the work in this lab consists of fixing Valgrind’s errors and memory leaks, rather than the program’s output, which should be correct once the memory errors are fixed.

Below is sample code and the corresponding valgrind output to give an idea of the output that vlagrind will give with certain errors.

The number, 22153, represents the process id (PID) given to the program by the operating system. Below is a break down of each error.

  • The first error, originating from line 17, is due to the fact that y was never initialized
  • The second error comes from the fact that the wrong delete is being used in line 18. From looking at the declaration of arr we can see that delete[] should’ve been used instead.
  • The last error occurs on line 20 when trying to delete something that wasn’t initialized or pointing to the heap.

  • Stack buffer overflow. This error occurs when you go out of bounds within an array created on the stack.
  • Use after return. This error occurs when you return a stack variable at the end of a function and try to use it after it’s out of scope.
  • Double free or Invalid free. Double free occus when you free an heap object twice. Invalid free’s occur when you free a non-heap object.

Further Testing

To test the code further, you may use the provided test program to see whether your program runs fine without any memory leaks. We expect that your code produces no memory leaks for both the memory program and the test program. To compile the test program, please run the following:

make test

You can test whether the test program successful works without any memory issues by running

valgrind ./test

If you notice no errors or memory leak errors outputted from Valgrind, your test program has successfully run as well. If not, it’s time to Valgrind your way through the code once more :)

To get more specific issues about memory leak related issues with Valgrind, you may get more information by using:

valgrind --leak-check=full ./test

GDB: A Debugger

While Valgrind tells you what went wrong in your program after it has been executed, GDB is a debugging tool that allows you to see what is going on ‘inside’ your program WHILE it executes! It can also be used after your program has crashed for debugging purposes as well. In particular, Valgrind works well for memory related errors, but GDB can handle crashes from those as well as other classes of bugs, such as logic errors. To learn how to use GDB for your lab and mps, visit this page:

3 lines to find your segfault with GDB (use LLDB for macOS)

$ gdb memory # "memory" is the name of your executable file
(gdb) r # short for "run"
(gdb) bt # short for "backtrace"

Grading Information

The following files are used to grade this assignment:

  • allocator.cpp
  • allocator.h
  • letter.cpp
  • letter.h
  • room.cpp
  • room.h

All other files, including memory.cpp and any testing files you have added will not be used for grading. Remember that submissions must be done through Prairielearn!