Lab 5 - Debugging in Valgrind
Lab goals:
- Understand how Valgrind helps detect memory allocation errors in C programs.
- Identify common memory issues: leaks, use-after-free, uninitialized reads, and invalid frees.
- Practice fixing memory management bugs in C programs using Valgrind reports.
Pre-lab
What is Valgrind?
Valgrind is a dynamic analysis tool that helps you detect memory management bugs. It tracks allocations, accesses, and frees, and reports problems like memory leaks, invalid memory reads and writes, use-after-free errors, and accessing uninitialized memory.
On CLEAR, Valgrind is pre-installed. You do not need to install anything.
Basic Valgrind Usage
Compile with debugging info enabled:
gcc -g -Wall -Wextra -Werror -o myprog myprog.c
Then run with Valgrind:
valgrind ./myprog
You can use flags to enhance output - some examples are shown below:
--leak-check=full— show detailed info on memory leaks--show-leak-kinds=all— show definite, indirect, and possible leaks--track-origins=yes— track the origin of uninitialized values--log-file=filename— redirect Valgrind output to a file--gen-suppressions=all: Create error suppression rules for known false positives.--track-fds=yes: Detect file descriptor leaks in long-running programs.
You can find complete Valgrind documentation and usage details at: https://valgrind.org.
Basic Examples
Example 1: Memory Leak Detection
Create a file called example1.c with the following contents:
#include <stdlib.h>
int main()
{
int *array = malloc(10 * sizeof(int));
array[0] = 42;
return 0; // Forgot to free memory
}
Compile and run with:
gcc -g -Wall -Wextra -Werror -o example1 example1.c
valgrind --leak-check=full ./example1
You should see an error summary as follows:
==357007== Memcheck, a memory error detector
==357007== Copyright (C) 2002-2024, and GNU GPL, by Julian Seward et al.
==357007== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==357007== Command: ./example1
==357007==
==357007==
==357007== HEAP SUMMARY:
==357007== in use at exit: 40 bytes in 1 blocks
==357007== total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==357007==
==357007== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==357007== at 0x484482F: malloc (vg_replace_malloc.c:446)
==357007== by 0x401137: main (example1.c:5)
==357007==
==357007== LEAK SUMMARY:
==357007== definitely lost: 40 bytes in 1 blocks
==357007== indirectly lost: 0 bytes in 0 blocks
==357007== possibly lost: 0 bytes in 0 blocks
==357007== still reachable: 0 bytes in 0 blocks
==357007== suppressed: 0 bytes in 0 blocks
==357007==
==357007== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Fix the leak by adding free(array); before return 0; and run again. You should see no errors after the fix.
==357035== Memcheck, a memory error detector
==357035== Copyright (C) 2002-2024, and GNU GPL, by Julian Seward et al.
==357035== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==357035== Command: ./example1
==357035==
==357035==
==357035== HEAP SUMMARY:
==357035== in use at exit: 0 bytes in 0 blocks
==357035== total heap usage: 1 allocs, 1 frees, 40 bytes allocated
==357035==
==357035== All heap blocks were freed -- no leaks are possible
==357035==
==357035== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Extra: Logging Output
Run Valgrind with a log file:
valgrind --leak-check=full --log-file=example1_valgrind.log ./example1
Examine example1_valgrind.log for detailed diagnostics.
Example 2: Invalid Memory Access
Create example2.c:
#include <stdlib.h>
int main()
{
int *x = malloc(sizeof(int));
free(x);
*x = 5; // Use after free
return 0;
}
Run Valgrind:
gcc -g -Wall -Wextra -Werror -o example2 example2.c
valgrind ./example2
You should see an error summary as follows:
==357837== Memcheck, a memory error detector
==357837== Copyright (C) 2002-2024, and GNU GPLd, by Julian Seward et al.
==357837== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==357837== Command: ./example2
==357837==
==357837== Invalid write of size 4
==357837== at 0x40115C: main (example2.c:7)
==357837== Address 0x4a7b040 is 0 bytes inside a block of size 4 freed
==357837== at 0x4847B4C: free (vg_replace_malloc.c:989)
==357837== by 0x401157: main (example2.c:6)
==357837== Block was allocated at
==357837== at 0x484482F: malloc (vg_replace_malloc.c:446)
==357837== by 0x401147: main (example2.c:5)
==357837==
==357837==
==357837== HEAP SUMMARY:
==357837== in use at exit: 0 bytes in 0 blocks
==357837== total heap usage: 1 allocs, 1 frees, 4 bytes allocated
==357837==
==357837== All heap blocks were freed -- no leaks are possible
==357837==
==357837== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
==357837==
==357837== 1 errors in context 1 of 1:
==357837== Invalid write of size 4
==357837== at 0x40115C: main (example2.c:7)
==357837== Address 0x4a7b040 is 0 bytes inside a block of size 4 freed
==357837== at 0x4847B4C: free (vg_replace_malloc.c:989)
==357837== by 0x401157: main (example2.c:6)
==357837== Block was allocated at
==357837== at 0x484482F: malloc (vg_replace_malloc.c:446)
==357837== by 0x401147: main (example2.c:5)
==357837==
==357837== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Fix the bug by moving the free after the access to x.
Example 3: Uninitialized Memory Use
Create example3.c:
#include <stdio.h>
int main()
{
int x;
printf("%d\n", x); // Uninitialized read
return 0;
}
Run with the following compilation. We exclude the -W* flags here to demonstrate how Valgrind helps with catching initialization bugs.
gcc -g -o example3 example3.c
valgrind --track-origins=yes ./example3
You should see an error summary as follows:
==359399== Memcheck, a memory error detector
==359399== Copyright (C) 2002-2024, and GNU GPL, by Julian Seward et al.
==359399== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==359399== Command: ./example3
==359399==
==359399== Conditional jump or move depends on uninitialised value(s)
==359399== at 0x48CF813: __vfprintf_internal (in /usr/lib64/libc.so.6)
==359399== by 0x48C4A5E: printf (in /usr/lib64/libc.so.6)
==359399== by 0x401141: main (example3.c:6)
==359399== Uninitialised value was created by a stack allocation
==359399== at 0x401126: main (example3.c:4)
==359399==
==359399== Use of uninitialised value of size 8
==359399== at 0x48C3E6B: _itoa_word (in /usr/lib64/libc.so.6)
==359399== by 0x48CF132: __vfprintf_internal (in /usr/lib64/libc.so.6)
==359399== by 0x48C4A5E: printf (in /usr/lib64/libc.so.6)
==359399== by 0x401141: main (example3.c:6)
==359399== Uninitialised value was created by a stack allocation
==359399== at 0x401126: main (example3.c:4)
==359399==
==359399== Conditional jump or move depends on uninitialised value(s)
==359399== at 0x48C3E7C: _itoa_word (in /usr/lib64/libc.so.6)
==359399== by 0x48CF132: __vfprintf_internal (in /usr/lib64/libc.so.6)
==359399== by 0x48C4A5E: printf (in /usr/lib64/libc.so.6)
==359399== by 0x401141: main (example3.c:6)
==359399== Uninitialised value was created by a stack allocation
==359399== at 0x401126: main (example3.c:4)
==359399==
==359399== Conditional jump or move depends on uninitialised value(s)
==359399== at 0x48CFDFB: __vfprintf_internal (in /usr/lib64/libc.so.6)
==359399== by 0x48C4A5E: printf (in /usr/lib64/libc.so.6)
==359399== by 0x401141: main (example3.c:6)
==359399== Uninitialised value was created by a stack allocation
==359399== at 0x401126: main (example3.c:4)
==359399==
==359399== Conditional jump or move depends on uninitialised value(s)
==359399== at 0x48CF24C: __vfprintf_internal (in /usr/lib64/libc.so.6)
==359399== by 0x48C4A5E: printf (in /usr/lib64/libc.so.6)
==359399== by 0x401141: main (example3.c:6)
==359399== Uninitialised value was created by a stack allocation
==359399== at 0x401126: main (example3.c:4)
==359399==
0
==359399==
==359399== HEAP SUMMARY:
==359399== in use at exit: 0 bytes in 0 blocks
==359399== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==359399==
==359399== All heap blocks were freed -- no leaks are possible
==359399==
==359399== For lists of detected and suppressed errors, rerun with: -s
==359399== ERROR SUMMARY: 5 errors from 5 contexts (suppressed: 0 from 0)
Fix the bug by initializing x (e.g., int x = 0;).
In-lab
GitHub Repository for This Lab
To obtain your private repo for this lab, please point your browser to the starter code for the lab at:
https://classroom.github.com/a/vl774NG6Follow the same steps as for previous labs and assignments to to create your repository on GitHub and to then clone it onto CLEAR. The directory for your repository for this lab will be
debugging-in-valgrind-namewhere name is your GitHub userid.
Exercise 1: Uninitialized Reads in Conditional Logic
Open uninit.c:
#include <stdio.h>
int main()
{
int flag;
if (flag) {
printf("Flag is set!\n");
} else {
printf("Flag is not set.\n");
}
return 0;
}
This example appears simple, but it uses an uninitialized variable flag to control a conditional branch. The result is unpredictable behavior. Sometimes the message might be correct by chance, but it’s still a bug.
Compile and run as follows. We exclude the -W* flags here to learn about how Valgrind helps with catching initialization bugs.
gcc -g -o uninit uninit.c
valgrind --track-origins=yes ./uninit
Valgrind will flag the conditional as using an uninitialized value. This is especially valuable in larger programs where such errors are hard to spot.
Fix the bugs and recompile. Use valgrind to verify the fixes.
Exercise 2: Analyzing a Complex Memory Leak
Open leaky.c and examine the following program:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* create_message(const char* input)
{
char* buffer = malloc(100);
strcpy(buffer, input);
return buffer;
}
int main()
{
char* msg1 = create_message("Hello, world!");
char* msg2 = create_message("Goodbye, world!");
printf("%s\n", msg1);
// Forgot to free msg1 and msg2
return 0;
}
In this program, we allocate two blocks of memory using malloc() inside the create_message function. These blocks are never freed. Valgrind will report both of these as definite memory leaks. Note that although the program works and prints output correctly, the memory it allocated persists beyond program termination — this is a memory management bug, especially if such patterns are repeated many times in a larger system.
Compile and run as follows. We exclude the -W* flags here to learn about how Valgrind helps with catching initialization bugs.
gcc -g -o leaky leaky.c
valgrind --leak-check=full ./leaky
Fix the bugs and recompile. Use valgrind to verify the fixes.
Exercise 3: Catching Use-After-Free and Dangling Pointers
Open badaccess.c and study the following code:
#include <stdio.h>
#include <stdlib.h>
void corrupt_memory()
{
int* data = malloc(sizeof(int));
*data = 42;
free(data);
*data = 13; // Writing after free
printf("%d\n", *data);
}
int main()
{
corrupt_memory();
return 0;
}
Here, memory is allocated and correctly freed, but then reused via the dangling pointer data. This is extremely dangerous and can lead to undefined behavior, including data corruption or crashes. Valgrind will clearly identify this as a use-after-free error.
Compile and run it:
gcc -g -Wall -Wextra -Werror -o badaccess badaccess.c
valgrind ./badaccess
Fix the bugs and recompile. Use valgrind to verify the fixes.
Exercise 4: Advanced Debugging
Study the code in student_scores.c and fix the code until Valgrind detects 0 errors.
Post-lab
Submission Checklist
uninit.cfixed to initialize all valuesleaky.cwith memory properly freedbadaccess.cfixed to avoid use-after-freestudent_scores.cfixed to address all bugs
Be sure to git push your work on this lab before 11:55 PM on Sunday, 9/28, to get credit for this lab.