Compile and debug C/C++ programs sometimes take a lot of time. So it is a good idea to make the full use of the powerful CPU on the server and move all the compile, debug, and test processes there. To do jobs on the server nowadays, usually Docker is a requirement.
In this blog post, I am going to show how to debug C/C++ programs for logic errors, segmentation faults, and memory leaks, using CMake, GDB and Valgrind in Docker containers.
GitHub
The C++ examples and Dockerfile could be found in C++ Debug Docker on GitHub.
Docker
Create Docker Image
The basic Docker image for C++ debugging has CMake, GDB, and Valgrind installed.
The arguments --cap-add=SYS_PTRACE and --security-opt seccomp=unconfined are required for C++ memory profiling and debugging in Docker.
Examples
Build Examples
CMake was used to build the C++ programs. Please run the following command in the terminal to build.
1 2 3 4
$ mkdir build $ cd build $ cmake .. $ make
To make the C++ program GDB compatible, we set CMAKE_BUILD_TYPE to be Debug by adding the following line of code to the CMakeLists.txt file.
1
set(CMAKE_BUILD_TYPE Debug)
Setting CMAKE_BUILD_TYPE to be RelWithDebInfo also allows GDB to debug, and the performance of the program compiled should not be affected if it is running without GDB. However, the debugging process might be different from the program compiled with Debug since the code had been optimized by the compiler.
When we ran the program, we would find that the program did not return values as expected.
1 2 3 4 5 6 7 8
$ ./logicalError This program is used to compute the value of the following series : (x^0)/0! + (x^1)/1! + (x^2)/2! + (x^3)/3! + (x^4)/4! + ........ + (x^n)/n! Please enter the value of x : 2
Please enter an integer value for n : 3
The value of the series for the values entered is inf
We started to use GDB to debug this program.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
$ gdb ./logicalError GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty"for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration"for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type"help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./logicalError...done. (gdb)
We created a breaking point to the ComputeSeriesValue function.
1 2
(gdb) break ComputeSeriesValue(double, int) Breakpoint 1 at 0xc40: file /mnt/header.cpp, line 18.
It is equivalent to use b for break.
We started to run the program and stopped at the breaking point we just created.
1 2 3 4 5 6 7 8 9 10 11
(gdb) run Starting program: /mnt/build/logicalError This program is used to compute the value of the following series : (x^0)/0! + (x^1)/1! + (x^2)/2! + (x^3)/3! + (x^4)/4! + ........ + (x^n)/n! Please enter the value of x : 2
We suspected that the ComputeFactorial is problematic. We ran the program step by step using next or n several times.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
(gdb) next 19 double xpow = 1; (gdb) n 21 for (int k = 0; k <= n; k ++) { (gdb) n 22 seriesValue += xpow / ComputeFactorial(k); (gdb) n 23 xpow = xpow * x; (gdb) n 21 for (int k = 0; k <= n; k ++) { (gdb) n 22 seriesValue += xpow / ComputeFactorial(k); (gdb) n 23 xpow = xpow * x; (gdb) n 21 for (int k = 0; k <= n; k ++) { (gdb) n 22 seriesValue += xpow / ComputeFactorial(k);
When we reach a line of code containing ComputeFactorial, we step into the function ComputeFactorial using step or s.
1 2 3
(gdb) step ComputeFactorial (number=2) at /mnt/header.cpp:6 6 int fact = 0;
We could also print the value of fact using print.
1 2
(gdb) print fact $1 = 0
We executed ComputeFactorial further but found the value of fact remains to be 0 before it is returned.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
(gdb) print fact $1 = 0 (gdb) n 10 for (int j = 1; j <= number; j ++) { (gdb) n 11 fact = fact * j; (gdb) n 10 for (int j = 1; j <= number; j ++) { (gdb) n 11 fact = fact * j; (gdb) n 10 for (int j = 1; j <= number; j ++) { (gdb) n 14 return fact; (gdb) print fact $2 = 0
So we realized that there is something wrong with fact.
We could also know where the execution is by viewing the stack using backtrace or bt.
1 2 3 4
(gdb) backtrace #0 ComputeFactorial (number=2) at /mnt/header.cpp:14 #1 0x0000555555554c6f in ComputeSeriesValue (x=2, n=3) at /mnt/header.cpp:22 #2 0x0000555555554b3c in main () at /mnt/logicalError.cpp:25
$ gdb ./coreDumped GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty"for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration"for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type"help". Type "apropos word" to search for commands related to "word"... Reading symbols from ./coreDumped...done. (gdb) run Starting program: /mnt/build/coreDumped
Program received signal SIGSEGV, Segmentation fault. 0x000055555555466c in main () at /mnt/coreDumped.cpp:14 14 temp[3] = 'F'; (gdb) bt #0 0x000055555555466c in main () at /mnt/coreDumped.cpp:14
It told us the problem is in the line 14 of coreDumped.cpp.
Memory Leak
GDB is not able to find memory leak. Valgrind comes into play. We executed the program using Valgrind and an argument --leak-check=yes to see whether there are potential risks of memory leak.
$ valgrind --leak-check=yes ./memoryLeak ==471== Memcheck, a memory error detector ==471== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==471== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info ==471== Command: ./memoryLeak ==471== ==471== ==471== HEAP SUMMARY: ==471== in use at exit: 12 bytes in 1 blocks ==471== total heap usage: 2 allocs, 1 frees, 72,716 bytes allocated ==471== ==471== 12 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==471== at 0x4C3089F: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so) ==471== by 0x10868B: main (memoryLeak.cpp:3) ==471== ==471== LEAK SUMMARY: ==471== definitely lost: 12 bytes in 1 blocks ==471== indirectly lost: 0 bytes in 0 blocks ==471== possibly lost: 0 bytes in 0 blocks ==471== still reachable: 0 bytes in 0 blocks ==471== suppressed: 0 bytes in 0 blocks ==471== ==471== For counts of detected and suppressed errors, rerun with: -v ==471== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
It told us the problem is in the line 3 of memoryLeak.cpp.
Final Remarks
The C++ bugs we would have for real applications are usually much harder to debug. But the basic debugging skills mentioned above are requirements.