GNU Debugger — Solving a code crime
Let’s learn to unwind and solve code flaws
In very simple terms, debugging is the process of finding out bugs in program execution.
This includes finding logical errors, analyzing crashes, watching variable contents while executing a program line by line.
Let’s start with a bug -
Following is the code to find the area of specified shape
Inputs:
(choice) Type of shape (1 — Rectangle, 2 — Triangle)
(base) Base of shape
(height) Height of shape
Now there is surely something wrong in the code.
The answer should be 35721
How will you solve this case?
Approach 1 — Debug statements
Add debug statements at the appropriate places, which will print intermediate results.
You got the clues now —
- All switch cases are getting executed. Probably I need to break switch. YES! I am missing “break”
- Why am I getting an answer as -29815 in the first case? Is it getting overflowed? Is it because of data type — “short”?
- I must get an answer as 17860.5 for the second case. But it is 17860. I must use floating-point data types.
When can you use this approach?
You can use this approach of debugging if —
- Scope of inspection is small
- Cost of re-compiling code is small
- The complexity of code to be inspected is less
Approach 2 — Debugging with GDB
Let’s use GDB (GNU Debugger) to perform step by step execution of the program.
We will study more about GDB in a later section of the blog.
Watch how the control flows and variable values are printed to check for logic correctness —
Nice Sherlock! You got the clues again.
This time you are able to see the code flow visually.
Debugging with GDB helps you to understand objects in memory, code flow, resolve memory crashes.
This is the reason why debugging is treated as a skill
Let’s take an example of memory crash -
The following program copies a string from one variable to another.
However, the code crashed with error — stack smashing detected
How to find the root cause?
If we take approach 1 — debug statements, we may not be able to find the root cause.
All the debug statements are printing expected values.
With GDB approach —
We can see that the in-memory value of mybuff is truncated to length 10 and is no longer NULL-terminated. This tells that we tried to copy into small space hereby writing to non-allocated memory locations
The more we explore GDB, the more exciting becomes debugging
Let’s get started
What is GDB?
GNU Debugger (GDB) is the most popular debugger for UNIX systems to debug C and C++ programs.
GDB uses a simple command-line interface.
Requirements
GDB requires Debugging Symbol Table that maps every instruction with the corresponding line, function, or variable in the source code.
This table is created by specifying a flag “-g” to the compiler. The symbol table is always created at compile time.
gcc area.c -o calc_area ……………………………Creates a production binary
gcc area.c -g -o calc_area…………………………Creates a debug binary
Why is it called debug binary?
After creating a binary with the “-g” flag, it holds symbol table information which is used for debugging, which makes it debug binary.
Due to this extra information, debug binary is of larger size and slower to execute as compared to production binary
It is time to start Debugging — Now
Once we have a debug binary at hand, we can now proceed with debugging :P
Step 1 — A) Load debug binary
gdb calc_area
Above command will load the debug binary along with its symbol table
If you forget to create debug binary, then GDB will warn you — No debugging symbols found in the program
Step 1 — B) Load debug binary with a core dump
You can even load the core dump. A core dump is a file containing a process’s memory contents when the process terminates unexpectedly.
See what are core dumps and how to enable them -
gdb calc_area <core_dump_file>
Once the dump file is loaded, you can perform the next mentioned steps except executing the binary, as here your code is already executed and dumped.
Step 1 — C) Attach the running process
You can attach the existing running process to gdb and set breakpoints to pause and debug the program. This technique is used to debug daemon processes.
sourabh@TheBugsTerz:~$ ps -a
PID TTY TIME CMD
1851 pts/0 00:00:00 calc_area
1852 pts/1 00:00:00 ps
sourabh@TheBugsTerz:~$ gdb attach 1851
Once we attach the running debug process using process id, we can perform the next mentioned steps except executing the program as we are already debugging the running process.
Step 2 — Set breakpoints
Once the binary is loaded, we need to set breakpoints.
Breakpoints are the places in the source code at which code execution will pause for debugging purposes.
Many-a-times breakpoints are added before conditions to verify logic. They are also added to a function to check how many times it is invoked or even it is invoked or not?
Breakpoints can be added using line number in source code or using function name or same file or another
Reading symbols from calc_area…
(gdb) b main
Breakpoint 1 at 0x1209: file area_print.cpp, line 5.
(gdb) b 9
Breakpoint 2 at 0x1224: file area_print.cpp, line 9.
(gdb) b another.cpp:hello
Breakpoint 3 at 0x1335: file another.cpp, line 2.
(gdb) b another.cpp:4
Breakpoint 4 at 0x133d: file another.cpp, line 4.
Step 3 — Execute the code
Once we are set with a breakpoint, now it is time to invoke your program.
(gdb) r
Starting program: /home/sourabh/scripts/calc_area
Inside GDB console, enter “r” or “run” to execute the loaded program.
This program will continue to execute until first breakpoint hits or any exception occurs or it is signaled to stop by the system.
Step 4 — Debug
We can now start debugging after program execution hits its first breakpoint or exception.
Breakpoint 1, example_function () at bt.cpp:1
1 void example_function() {
(gdb)
What actions you can perform now?
- View backtrace (bt / backtrace)
This allows to view calling functions or flow of execution from functions
(gdb) bt
#0 example_function () at bt.cpp:1
#1 0x0000555555555141 in gdb_example () at bt.cpp:6
#2 0x0000555555555151 in function () at bt.cpp:11
#3 0x0000555555555161 in main () at bt.cpp:15 - Print variables, structure objects at that point in time (p / print)
This is mostly done before conditions or logic statements to validate correctness.
(gdb) p s1
$1 = {name = “Sourabh\000\000\000\000\000\000\000\000\000@@UU”,
age = 24,
date_of_birth = “October 25th\000\000\000\000W\334\377\377”}
(gdb) p funct_var_int
$2 = 12
(gdb) p funct_var_string
$3 = 0x555555556004 “function_variable”
(gdb) p s1.name
$4 = “Sourabh\000\000\000\000\000\000\000\000\000@@UU” - Set variables (set)
You can alter variable values to validate logic and conditions under different scenarios
(gdb) p s1.age
$10 = 24
(gdb) set s1.age = 18
(gdb) p s1.age
$11 = 18 - View Memory Map (x)
We can view / examine memory map from a specified address
(gdb) x/4sb 0x555555556004
0x555555556004: “function_variable”
0x555555556016: “”
0x555555556017: “”
0x555555556018: “\001\033\003;\\”
(gdb) x/4ib 0x555555556070
0x555555556070: mov $0x1,%al
0x555555556072: add %al,(%rax)
0x555555556074: add %al,(%rax)
0x555555556076: add %al,(%rax)
Format:
x/nfu address
……n: No of units to show
……f: Display format. It can be one of: {x, d, u, o, t, a, c, f, s, i}
…………..o — octal
…………..x — hexadecimal
…………..d — decimal
…………..u — unsigned decimal
…………..t — binary
…………..f — floating point
…………..a — address
…………..c — char
…………..s — string
…………..i — instruction
……u: Unit size. It can be {b: byte, h: halfword, w: word, g: giant byte} - Call functions for different inputs
You can invoke a function for different inputs to test its behavior in different scenarios.
(gdb) call add(2, 7)
$4 = 9
(gdb) call add(20000, 15000)
$5 = -30536 ← My function failed to handle this scenario :(
(gdb) call add(91, 9)
$6 = 100
These are common operations performed with debugging.
Hope you are now excited to solve few of the code crimes on your own. Start practising now and you will eventually earn this skill.
Share your experiences with debugging any tricky bugs.
Suggestions and recommendations are welcomed :)