Introduction
Valgrind is a programming tool for debugging memory related issues and memory profiling. When you work with dynamic memory allocation, you should be really careful about resources you use. Simple programs can be debugged by careful visual inspection of the code or by adding corresponding print statements here and there to check the state of the program. However, when the programs become bigger and more complex, the debugging process might require additional measures. Valgrind is one of the possible tools that provide help with memory debugging.
Valgrind contains several tools for debugging and profiling. We will not go through all of them in this tutorial, however, we will only focus on those tools that are of particular interest for the Programming 2 course.
Memcheck
The basic tool of Valgrind and the most useful tool when you start to work with dynamic memory allocation, because it detects memory-management errors. The tool checks all reads and writes to the memory, every call of malloc/calloc/realloc/free functions. Memcheck can detect if the program tries to access the memory it should not (areas not yet allocated, that have been freed or where it does not have access to), uses uninitialized variables in calculations or comparison, if there are memory leaks, incorrect frees of memory blocks, etc.
In order to debug your program using Memcheck, you will need to run it with the following command:
1 |
valgrind --tool=memcheck --leak-check=yes ./your_program_name |
The leak-check option provides a detailed overview of the memory leaks. In order to get more detailed output from the valgrind, it is advisable to compile your program with -g flag. It will introduce debugging symbols to the program and allows to provide the line number where the error occurred to the Valgrind output.
Example 1
Let’s try to debug a simple program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <stdio.h> #include <stdlib.h> int main(void){ int i; int array[5]; while(i <= 5){ array[i] = i; printf("El. %d: %d\n", i, array[i]); i++; } return 0; } |
This is how the Valgrind report will look like:
Let’s go through the report and see what information about the program we can get from it. First of all, we can get an ID number assigned to the process, it is the number on the left, though it is not important for us.
Unconditional jump or move
Jump or move is the computer instruction (when your C code is compiled into a machine code, your program becomes a list of such instructions that are executed sequentially) that indicates the transition to some place in the program. Jumps may be conditional and unconditional. Conditional jumps correspond to if statements, while and for loop conditional statements. This error refers to the while(i <= 5)
statement, where we compare uninitialized variable i
with the number 5, which potentially might cause problems during the process execution – the program might not enter the while loop at all.
Uninitialized value
Note that each error description is followed by the stack trace, showing where the problem occurred. It is a chain of function calls, where the topmost function indicates the actual error location and the one in the bottom is in most cases the main function.
Those errors corresponds to the usage of uninitialized variable i
in array elements assignment and in the printf()
function. Valgrind defines the error as the usage of uninitialized value of size 8, however, on my machine integer is 4 bytes. What does this mean? Valgrind refers to a pointer (an address actually). I use 64-bit system, therefore, each address value is 8 bytes. Remember what does the index of an array element mean. It is an offset to the starting address of an array, where each element of an array is accessed the following way:
1 |
element_address = starting_address + index * element_size. |
Hence the first element has index 0. Whenever we are trying to access array elements to either get or set their value, we are doing either read or write operation to the address of the element. Internally array[i]
is always represented as (array + i)
. When i
is used uninitialized for the memory address calculation, we get an uninitialized address value.
Notice that only use of uninitialized value on lines 9 and 10 was reported. The statement i++
on the line 11, which also used the uninitialized value of i
for the calculation, was not reported. Valgrind keeps track of every uninitialized variable, however, report its use only when it becomes visible and affects the flow of the program.
Then several errors connected to the same uninitialized variable that happen inside the printf()
function are reported. Since we already identified the problem, they are not interesting for us.
At the end of Valgrind report we have a heap and memory leaks summary.
No memory leaks in this program, everything is fine.
In the heap report there is one memory allocation, though we have not allocated any memory explicitly. Where did it come from? It was an internal allocation done by printf()
function for the standard output buffer.
Origins of uninitialized values
In this simple example we can easily track the uninitialized variable ourselves, however, in more tricky cases it is possible to use --track-origins=yes
option to see where uninitialized values are coming from.
Valgrind defines four origins for uninitialized values: a heap block, a stack allocation, a client request, or other source. If an uninitialized value originates from a heap block, Memcheck shows where the block was allocated. In case of a stack allocation (statically allocated variable), Memcheck can only tell which function allocated the value. It is a good start anyway, especially if you have a big program. Make sure to carefully check all local variables declared in the function.
As you can see, the output of the program is printed in-between the Valgrind report. To separate them, it is possible to write Valgrind report to a file with the --log-file=filename
option.
We checked all the errors reported, however, there is a problem in our program that Memcheck cannot detect: the out-of-range read or write to the array that is allocated statically. When i
becomes 5, the program writes a value to the array[5]
element, which is out of range of our array. It is possible to track such errors with Valgrind tools as well, however, we will come back to it later.
Example 2
This was a simple example. Let us try the more complicated one.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LENGTH 20 #define MAX_SIZE 3 typedef struct data{ char name[MAX_LENGTH]; int votes_num; } data; FILE *openFile(char *fileName, char *mode); int readFile(FILE *pfile, data *massiv); void printData(data *massiv, int records); int main(void){ FILE *input = openFile("votes.txt","r"); data *votes = NULL; int recordsNumber = readFile(input,votes); printData(votes,recordsNumber); free(votes); return 0; } FILE *openFile(char *fileName, char *mode){ FILE *pf = fopen(fileName, mode); if(pf == NULL){ perror("Can't open input file"); exit(1); } return pf; } int readFile(FILE *pfile, data *massiv){ int i = 0; // Allocating memory for an array of three data structs // What will happen with this pointer at the end of the function? massiv = malloc(sizeof(data) * MAX_SIZE); if(massiv == NULL){ perror("Memory allocation failed"); exit(1); } // Reading through the file until there are two values on the line // However, there is a mistake here. What important condition is missing? while(fscanf(pfile,"%s %d", (massiv + i)->name, &((massiv + i)->votes_num)) == 2){ i++; } return i; } void printData(data *massiv, int records){ int i; for(i = 0; i < records; i++){ printf("Party: %s. Number of votes: %d\n", massiv[i].name, massiv[i].votes_num); } } |
1 2 3 4 |
Democrats 450 Republicans 34 Nationals 256 Liberals 123 |
The Valgrind report for this program looks more interesting. We even got a segmentation fault. But lets go through it one by one.
Invalid write
We have several records indicating invalid write somehow connected to the memory allocation done in the readFile()
function. Invalid write happens when the program tries to write something to the memory that it does not have access to.
We have invalid write of size 1, which is a size of a character. Valgrind reports that the program wants to write the first character from the party name to an invalid location. Valgrind also reports that the invalid address the program is trying to write to is located 0 bytes after the block of 72 bytes allocated by malloc()
function. This means, that we allocated 72 bytes for three data structs, but then tried to access the fourth one. Indeed, if you look carefully at the code, you will see that the while loop in readFile()
function does not check for the limits. We allocated the memory only for three elements of type data struct, therefore, we can only store three records from the file, we cannot store the fourth one. This way, Valgrind can detect the out-of-order read or write to the dynamically allocated array.
Invalid write of size 4, which is a size of an integer, is obviously an attempt to write the number of votes. But what is the second invalid write of size 1 that the program tries to write 8 bytes after the allocated block for three structures? If you check the text file, the name of the fourth party Liberals has exactly 8 symbols. So what does the program write after those 8 symbols? What should be at the end of every string? A null character '\0'
of course.
Invalid read and segmentation fault
Now, let’s check the next error.
Invalid read, as you can guess, occurs when the program tries to read the data from the invalid address. Memcheck cannot identify the address, however, we can try to trace the problem ourselves. At line 63 we are printing the values we have written to the data struct array, but what is wrong? We created a pointer of type data, we allocated the memory for the data array in the function readFile()
, we assigned the starting address of the allocated memory block to that pointer, we filled that array. Why can’t we access this memory in the printData()
function? Because the moment we left the readFile()
function we lost the address of the memory allocated. We passed votes pointer by value not by reference, therefore, any changes we made to this variable in the readFile()
were only valid inside this function.
And this is actually why the program crashed with a segmentation fault. Note that Valgrind does not stop your program from accessing invalid memory region and crashing, nor does it stop the program from using uninitialized values for example, but the Valgrind can point out where the problem occurred.
Memory leak
Now, let’s check heap summary and leak summary.
What do we get from this summary? We have 3 memory allocations, but only 1 allocated block was freed. We lost a memory block of 72 bytes, that we allocated in the readFile()
function for the data array, because we lost a pointer to the starting address of that block and never freed it. This is identified by Valgrind as a Definitely lost block. Take a note that leaks are checked by Valgrind at the end of the program, at that moment the pointer to the data struct array is long gone. And we have a mysterious block of 552 bytes that is still reachable. Still reachable means that the pointer to the starting address of that block is found. But how can we identify this block? To get more information, we can rerun the Valgrind with --leak-check=full --show-leak-kinds=all
options.
Now we can see that these 552 bytes were allocated by fopen()
function for the file buffer, however, since we forgot to close the file, this memory was not freed.
Example 3
Indirectly lost memory
We have identified two memory leak kinds, let’s check the others. Indirectly lost means that the block is unreachable not because there is no pointer to its starting address but because all the blocks pointing to it are lost. Consider the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LENGTH 20 #define MAX_SIZE 4 // Structure definition has changed // name is not an array now, it is a pointer // And we will allocate memory for each name typedef struct data{ char *name; int votes_num; } data; FILE *openFile(char *fileName, char *mode); int readFile(FILE *pfile, data *massiv); void printData(data *massiv, int records); int main(void){ FILE *input = openFile("votes.txt","r"); data *votes = NULL; int recordsNumber = readFile(input,votes); printData(votes,recordsNumber); free(votes); return 0; } FILE *openFile(char *fileName, char *mode){ FILE *pf = fopen(fileName, mode); if(pf == NULL){ perror("Can't open input file"); exit(1); } return pf; } int readFile(FILE *pfile, data *massiv){ char nameBuffer[MAX_LENGTH]; int i = 0; // Allocating memory for an array of three data structs massiv = malloc(sizeof(data) * MAX_SIZE); if(massiv == NULL){ perror("Memory allocation failed\n"); exit(1); } // Reading through the file until there are two values on the line // We are reading a party name in a buffer now and then allocate memory for it while(fscanf(pfile,"%s %d", nameBuffer, &((massiv + i)->votes_num)) == 2){ (massiv + i)->name = malloc(sizeof(char) * (strlen(nameBuffer)+1)); strcpy((massiv + i)->name, nameBuffer); i++; } return i; } void printData(data *massiv, int records){ int i; for(i = 0; i < records; i++){ printf("Party: %s. Number of votes: %d\n", massiv[i].name, massiv[i].votes_num); } } |
In the data struct, instead of a statically defined char array name
we define a char pointer, and later in readFile()
function allocate memory for each party name. The memory block allocated for the data struct array will be definitely lost, but memory blocks for those strings containing political party names will be indefinitely lost, because the block that contained the pointers to these elements is lost.
Possibly lost means that the pointer to a memory block was found, however, it is a pointer to the middle of the block. There are several ways how this can occur: e.g. the pointer initially was pointing to the start of the block but was moved along deliberately or accidentally, or the pointer value might be completely random and points to the allocated memory block by accident.
Example 4
Lets check the last example. It is small and simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <stdio.h> #include <stdlib.h> #define SIZE 10 int main(void){ int i; int *pInt = (int *)malloc(sizeof(int) * SIZE); for(i = 0; i < SIZE; i++){ *(pInt + i) = i; } free(pInt); free(pInt); float *pFloat = (float *)malloc(sizeof(float) * SIZE); free(pInt); char *pChar = (char *)malloc(sizeof(char) * SIZE); printf("pFloat = %p\n", pFloat); printf("pChar = %p\n", pChar); free(pFloat); free(pChar); return 0; } |
Valgrind reports only one error called invalid free.
Double or invalid free
Invalid free occurs when program is calling free() function on already freed memory location. According to C specification, invalid free leads to undefined behavior. This can cause program to crash or in some circumstances two later calls to malloc() can return the same pointer, because it corrupts the state of the memory manager. What happened in my case? On Linux, program crashed. On Windows, pFloat and pChar actually got the same address. If you want to avoid such situation, set an already freed pointer to NULL.
Overview of the commands for the Memcheck tool
Compile your program with debugging flag:
1 |
gcc -g -o your_program_name your_program_code.c |
The basic command, will give the leak summary:
1 |
valgrind --tool=memcheck --leak-check=yes ./your_program_name |
Detailed information on leaks:
1 |
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all ./your_program_name |
Track the origin of the uninitialised values:
1 |
valgrind --tool=memcheck --track-origins=yes ./your_program_name |
Write Valgrind output to a file:
1 |
valgrind --tool=memcheck --leak-check=full --log-file=filename ./your_program_name |
SGCheck
Remember we said that Memcheck cannot check the bounds for statically allocated arrays, however, there is a tool in Valgrind toolbox that has such capabilities. SGCheck is a stack and global array overrun detector. It is important to note that this is an experimental tool; it uses technique and assumptions that have certain limitations and can produce false positive or false negative results. It also runs slower than Memcheck. You do not need to use it within the course, Memcheck is more important for us, however, let’s look at how it works.
To use SGCheck, run the program with the command:
1 |
valgrind --tool=exp-sgcheck ./your_program_name |
In our simple case the tool actually gives a correct result. Valgrind reports the invalid write and invalid read of size 4 (integer) and provides information on expected and actual addresses. An invalid address is expected to be a part of statically allocated (i.e. allocated on stack) array called array
(good naming) of size 20 (5 * 4 = 20), but actually it is located 0 bytes after that array. This way, we come out of bounds.