How to find bugs and memory leaks in C++¶

  • A common tool for shared memory parallism is GDB: The GNU Project Debugger
  • Valgrind is an instrumentation framework for building dynamic analysis tools. There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail.

alt text

Debugging using gdb¶

For production runs, we compile the code as

In [1]:
%%bash 
g++ main_debug.cc -o debug_example

For debugging we need to add -g to our code to get debug information added

In [2]:
%%bash 
g++ -g main_debug.cc -o debug_example

Starting the executable using gdb¶

  • gdb name_of_executable

Note that all comand line options are not provided at this stage.

gdb debug_example
GNU gdb (GDB) Fedora 9.1-7.fc32
Copyright (C) 2020 Free Software Foundation, Inc.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from debug_example...
(gdb)

Running the code¶

  • We use the command run to execute the code within the debuuger
(gdb) run 3
Starting program: /home/diehlpk/debug_example 3
warning: Error disabling address space randomization: Operation not permitted
9
[Inferior 1 (process 4516) exited normally]

Setting break points¶

  • We use the break command to define a break point and the debugger will stop at this point and we can display the values of pointers and varibales.

    • break [function name]
    • break [file name]:[line number]
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;

int findSquare(int a)
{
    return a * a;
}

int main(int n, char** args)
{
    for (int i = 1; i < n; i++) 
    {
        int a = atoi(args[i]);
        cout << findSquare(a) << endl;
    }
    return 0;
}

Setting break points¶

(gdb) break findSquare
Breakpoint 1 at 0x40118d: file main_debug.cc, line 8.
(gdb) break main_debug.cc:15
Breakpoint 2 at 0x4011b3: file main_debug.cc, line 15.

Running the code with break points¶

(gdb) run 3
Starting program: /home/diehlpk/debug_example 3
warning: Error disabling address space randomization: Operation not permitted

Breakpoint 2, main (n=2, args=0x7ffd286b5b28) at main_debug.cc:15
15              int a = atoi(args[i]);

Moving to the next break point¶

  • To move to the next break point, we use the continue command
(gdb) continue
Continuing.

Breakpoint 1, findSquare (a=3) at main_debug.cc:8
8           return a * a;
(gdb) continue
Continuing.
9
[Inferior 1 (process 4547) exited normally]

Here, we see that a = 3 whch is good since we provided run 3 and the command line argument was corretly passed.

Deleting break points¶

  • We can delete any break point with the delete command:
    • delete - Will delete all break points
    • delete [break point number 1] - Will delete onlt this on break point
(gdb) delete 2
(gdb) run 3
Starting program: /home/diehlpk/debug_example 3
warning: Error disabling address space randomization: Operation not permitted

Breakpoint 1, findSquare (a=3) at main_debug.cc:8
8           return a * a;
(gdb) continue
Continuing.
9
[Inferior 1 (process 4560) exited normally]

Watching variables¶

  • (gdb) watch a - Will interupt the code and print the new and old value of a.
  • (gdb) print a - WIll print the value of a.
(gdb) print a
$2 = 3
(gdb

alt text

Handling segmentation faults¶

Let us look into some code with some segmentaiton fault:

In [ ]:
%%bash 

g++ main_debug_segfault.cc -o segfault
./segfault

bash: line 3: 8198 Segmentation fault (core dumped) ./segfault

Let us use gdb to find the segfault in the code

gdb segfault
GNU gdb (GDB) Fedora 9.1-7.fc32
Copyright (C) 2020 Free Software Foundation, Inc.
(gdb) run
Starting program: /home/diehlpk/segfault
warning: Error disabling address space randomization: Operation not permitted
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.31-6.fc32.x86_64
Program received signal SIGSEGV, Segmentation fault.
0x0000000000401191 in main ()
Missing separate debuginfos, use: dnf debuginfo-install libgcc-10.3.1-1.fc32.x86_64 libstdc++-10.3.1-1.fc32.x86_64
(gdb)

That is not really a helpful message, since we already know that the program fails.

Getting a more detailed error message¶

We need to compile the code using -g to get more details

In [4]:
%%bash 
g++ -g main_debug_segfault.cc -o segfault
gdb segfault
GNU gdb (GDB) Fedora 9.1-7.fc32
Copyright (C) 2020 Free Software Foundation, Inc.
(gdb) run
Starting program: /home/diehlpk/segfault
warning: Error disabling address space randomization: Operation not permitted
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.31-6.fc32.x86_64

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401191 in main (n=1, args=0x7ffc72343818) at main_debug_segfault.cc:8
8           std::cout << *pointer << std::endl;
Missing separate debuginfos, use: dnf debuginfo-install libgcc-10.3.1-1.fc32.x86_64 libstdc++-10.3.1-1.fc32.x86_64

Now we get the line number and the piece of code which is causing the segfault.

(gdb) run
Starting program: /home/diehlpk/segfault
warning: Error disabling address space randomization: Operation not permitted
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.31-6.fc32.x86_64

Program received signal SIGSEGV, Segmentation fault.
0x0000000000401191 in main (n=1, args=0x7ffc72343818) at main_debug_segfault.cc:8
8           std::cout << *pointer << std::endl;
Missing separate debuginfos, use: dnf debuginfo-install libgcc-10.3.1-1.fc32.x86_64 libstdc++-10.3.1-1.fc32.x86_64
(gdb) print pointer
$1 = (double *) 0x0

Now we can check the pointer and see that the pointer has the value 0x0 which is a nullptr and we have not allocated memory.

Let us have a look at the code:

#include <iostream>
using namespace std;


int main(int n, char** args)
{
    double * pointer = nullptr;
    std::cout << *pointer << std::endl;
     return 0;
}

Can you see why we have the segfault in the code?

This is the correct version of the code after debugging:

#include <iostream>
using namespace std;


int main(int n, char** args)
{
    double * pointer = 2;
    std::cout << *pointer << std::endl;
     return 0;
}

Can you see why we have the segfault in the code?

alt text

Checking memory usage using valgrind¶

Valgrind can help with the following:

  • Find non initlized memory
  • Check if the memory was allocated and free using the same methods. e.g. delete and delete[].
  • Check if all allocated memory was free before exiting the code.
  • Check if we write into memory which is not part of the pointer

Using memcheck¶

To detect the previous segfault, we could use the memcheck tool which is part of valgrind

  • valgrind ./segfault will enable the memory check
valgrind --tool=memcheck ./segfault
==24842== Memcheck, a memory error detector
==24842== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==24842== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==24842== Command: ./segfault
==24842== 
==24842== Invalid read of size 8
==24842==    at 0x401191: main (code.cc:8)
==24842==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

Checking if all memory is cleaned correclty¶

valgrind --tool=memcheck ./segfault2
==25418== Memcheck, a memory error detector
==25418== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==25418== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==25418== Command: ./segfault2
==25418== 
2
==25418== Mismatched free() / delete / delete []
==25418==    at 0x484565B: operator delete[](void*) (vg_replace_malloc.c:1103)
==25418==    by 0x4011F4: main (code2.cc:9)
==25418==  Address 0x4db2c80 is 0 bytes inside a block of size 8 alloc'd
==25418==    at 0x4841FF5: operator new(unsigned long) (vg_replace_malloc.c:422)
==25418==    by 0x4011AE: main (code2.cc:7)

Looking at the code:

#include <iostream>

int main(int n, char** args)
{
    double * pointer = new double(2);
    std::cout << *pointer << std::endl;
    delete [] pointer;
    return 0;
}

Checking if we only access allocated memory¶

valgrind  ./index
==28728== Memcheck, a memory error detector
==28728== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==28728== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==28728== Command: ./index
==28728== 
==28728== Invalid read of size 8
==28728==    at 0x4011AB: main (code3.cc:7)
==28728==  Address 0x4db2cb0 is 8 bytes after a block of size 40 alloc'd
==28728==    at 0x484322F: operator new[](unsigned long) (vg_replace_malloc.c:640)
==28728==    by 0x40119E: main (code3.cc:6)

Let us look at the code:

#include <iostream>

int main(int n, char** args)
{
    double * pointer = new double[5];
    std::cout << pointer[6] << std::endl;
    return 0;
}

Checking if allocated memory is relased after the program finished¶

valgrind --tool=memcheck ./free 
==9345== Memcheck, a memory error detector
==9345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==9345== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==9345== Command: ./free
==9345== 
==9345== 
==9345== HEAP SUMMARY:
==9345==     in use at exit: 0 bytes in 0 blocks
==9345==   total heap usage: 2 allocs, 2 frees, 72,712 bytes allocated
==9345==

Let us look at the code:

#include <iostream>

double * func(int i){

    return new double(i);
}

int main(int n, char** args)
{

    double* value = func(0);
    delete value;
}

alt text