Today’s lab will introduce another useful tool to keep on hand when you’re working in C: AddressSanitizer.
The exercises will walk you through AddressSanitizer’s capabilities, but at a high level, it is a tool that adds checks to your program to catch some of the pointer and memory-related errors you’ll likely make when writing C programs.
AddressSanitizer is a good complement to gdb;
where gdb allows you to walk a program through its execution and examine its internal state, AddressSanitizer adds automated checks to watch for specific errors and report useful diagnostic information when an error occurs.
Neither tool is a great replacement for the other, but together they help save you from what would otherwise have been a long, tedious debugging process.
There is no starter code repository for this lab. You can use the code examples included in the text below. Make sure you save your responses for each exercise so you can submit them with your lab report.
The first step when using AddressSanitizer is to recompile the program with an extra flag: -fsanitize=address.
As you might guess from the structure of that option, there are other sanitizers available to check for errors or undesirable program behaviors, but AddressSanitizer was the first.
We’ll start by building the following program:
int main() {
// Make an array of ten values
int values[10];
// Initialize the values array to zeroes
for (int i=0; i<sizeof(values); i++) {
values[i] = 0;
}
return 0;
}
This program contains a memory error. If you’ve spotted it already, please keep it to yourself; these can be tricky to spot in small programs, and nearly impossible in a larger program. We’ll use AddressSanitizer to catch the error in a moment.
If you save this code in a file named partA.c a normal compilation and run woud look like this:
$ clang -o partA partA.c
$ ./partA
What happens when you run the program? It’s likely the program fails with a segmentation fault on MathLAN machines. You might see different behavior on other computers. Some programs with similar errors will work on some runs and fail on others. These are good clues that we’re dealing with a memory error.
One of the first tools you should reach for when you encounter a memory error is gdb.
But to get useful debugging information, you’ll need to recompile partA with the -g flag before running it in gdb:
$ clang -g -o partA partA.c
$ gdb ./partA
Type run into gdb to run the program.
It will stop when the segmentation fault occurs.
Where does the program stop?
What useful information can you learn from the point where the program has stopped?
It turns out, not much.
This is where AddressSanitizer can be more useful. We can compile the program with AddressSanitizer like this:
$ clang -g -fsanitize=address -o partA partA.c
The program now includes extra checks for memory errors that will alert you when something goes wrong. The exercises below will ask you to try using AddressSanitizer to diagnose the bug in this program.
Copy the example code above to a source file named partA.c and use the instructions above to compile partA.c with AddressSanitizer enabled.
Now run the partA program.
The program should fail with a long AddressSanitizer error instead of just crashing.
You can ignore the parts of the error beyond the line that begins with “SUMMARY”, but everything above that line is useful diagnostic information.
Try to write out an explanation for what each line of the diagnostic output means, excluding the line that starts with “HINT”.
Your explanation should include a fairly clear statement of what the error is.
There will be a few terms you won’t recognize (like thread, at least in this context) but you can ignore these parts of the diagnostic message in this class.
Use the information from AddressSanitizer to fix the error. Once you’ve fixed the error you should be able to run the program with AddressSanitizer enabled, but receive no error reports.
For this part of the lab, you will explore the different kinds of memory errors AddressSanitizer can detect.
Each exercise asks you to write a program containing a specific error in C, and then to check if AddressSanitizer detects the error.
You’ll compare the diagnostic information from AddressSanitizer (if it produces any at all) to the information you can get from examining the error in gdb.
For some errors, you’ll almost certainly end up with compiler warnings. This lab is the one time you should ignore these warnings; they’re there to help you avoid errors, but we’re adding errors on purpose.
For each exercise, you will follow the same steps:
Write a program that contains the requested error.
Compile and run the program without AddressSanitizer. Write down what happens when you run the program. Does it crash or give any other evidence that an error has occurred?
Now try running the program in gdb.
Does the debugger give you any useful information to guide you to the error?
Finally, compile the program with AddressSanitizer and run it again. Does AddressSanitizer catch the error? If so, what diagnostic information does it print? Does the diagnostic information point directly at the error?
Which tool do you think is better for finding this type of error?
Pick either gdb or AddressSanitizer and explain why you think it’s more helpful for this error.
Memory Leak: Whenever we allocate memory with malloc we are supposed to free it before the program exits.
Create a source file named memory-leak.c and fill it inwith a program that allocates memory with malloc but does not free it.
Folow the steps listed above to see how AddressSanitizer and gdb handle this error.
Buffer Overflow: The example bug in part A was a buffer overflow because the code writes beyond the end of an array.
For this exercise, we’ll instead write code that accesses beyond the end of memory requested with malloc.
Create a source file named buffer-overflow.c and fill it in with a program that allocates space for an array with malloc, but then accesses at least one index beyond the end of the array.
Follow the steps listed above to see how AddressSanitizer and gdb handle this error.
Use After Free: When we free memory, we are not supposed to use it anymore.
Create a source file named use-after-free.c and fill it in with a program that allocates memory with malloc and returns it with free, but continues to use the memory after it is freed.
Follow the steps listed above to see how AddressSanitizer and gdb handle this error.
Double Free: Our programs are supposed to free memory returned from malloc exactly once, not more than once.
Create a source file named double-free.c and fill it in with a program that allocates memory with malloc returns it with free, and then frees it again.
Follow the steps listed above to see how AddressSanitizer and gdb handle this error.
Constant String Modifications: AddressSanitizer can help with errors that don’t have anything to do with dynamically allocated memory as well.
We’ve seen in an earlier lab that string constants in C are immutable.
Create a source file named constant-string.c and fill it in with a program that attempts to modify one or more characters in a string constant.
Follow the steps listed above to see how AddressSanitizer and gdb handle this error.
AddressSanitizer doesn’t detect all memory errors, so we still need to be careful when we write code that uses pointers. The two optional exercises below illustrate cases where AddressSanitizer needs a special option to detect a particular error, and one case where it does not detect an error at all:
use-after-return.c and fill it in with a program that includes this error.
AddressSanitizer will only detect this error if you run the program with an extra option:
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./use-after-return
uninitialized-read.c and fill it in with a program that contains this error.
Follow the steps listed above to see how AddressSanitizer and gdb handle this error.
As you’ll see, AddressSanitizer does not detect this error at all.
There is another project called MemorySanitizer that can detect these errors, but we do not yet have a working version on MathLAN.