GNU Debugger — Solving a code crime

Sourabh Edake
7 min readJul 2, 2020

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

C1 — Code to find area of specified shape
Output for C1

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.

C2 — Code to find the area of shape (Debug Statements)
Output for C2

You got the clues now —

  1. All switch cases are getting executed. Probably I need to break switch. YES! I am missing “break”
  2. Why am I getting an answer as -29815 in the first case? Is it getting overflowed? Is it because of data type — “short”?
  3. 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 —

  1. Scope of inspection is small
  2. Cost of re-compiling code is small
  3. 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 —

GNU Debugger — Step by Step Execution

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.

C3 — Code to copy the string

However, the code crashed with error — stack smashing detected

Output for C3

How to find the root cause?

If we take approach 1 — debug statements, we may not be able to find the root cause.

C4 — Code with debug statements

All the debug statements are printing expected values.

Output for C4

With GDB approach —

GDB — Rectifying the bug

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

Debugging production binary

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?

  1. 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
  2. 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”
  3. 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
  4. 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}
  5. 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.

Commands at glance

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 :)

--

--