Lab 5 - Debugging in Valgrind

Lab goals:


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:

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/vl774NG6
Follow 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-name
where 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

Be sure to git push your work on this lab before 11:55 PM on Sunday, 9/28, to get credit for this lab.