Kahibaro
Discord Login Register

6.1.3 Debugging tools (gdb, strace)

Why Debugging Tools Matter on Linux

Debugging tools let you see what your program is really doing instead of guessing. For C/C++ (and many compiled languages on Linux), the two most common low‑level tools are:

They solve different problems:

This chapter shows how to apply both tools in simple, practical ways as a developer on Linux.


Preparing Programs for Debugging

Most compiled languages can add debug information to generated binaries. For C/C++ using gcc:

-g includes debug symbols; -O0 disables optimizations that can make debugging confusing. For production, you typically build with optimizations and optionally -g (debug builds vs release builds).

Many distributions also ship -dbg / -dbgsym packages with debug symbols for system libraries; these help you debug deeper into library code.


gdb: The GNU Debugger

gdb lets you:

You’ll most often use gdb for C/C++, but it’s also used indirectly for other languages or mixed-language projects.

Starting gdb

Debugging an executable directly

gdb ./myprogram
(gdb) run arg1 arg2

You can also specify arguments in one go:

gdb --args ./myprogram arg1 arg2
(gdb) run

Attaching to a running process

First find the PID (e.g., with ps, pgrep, or top), then:

gdb -p <PID>

Examples:

gdb -p 12345

This is useful if your program is hung or misbehaving and you want to inspect it without restarting.

On some systems, attaching to other users’ processes is restricted by ptrace settings. You may need sudo or to adjust /proc/sys/kernel/yama/ptrace_scope (system admin concern).

Basic gdb Workflow

Inside gdb, you type commands at the (gdb) prompt. Key commands:

Example: Debugging a Simple Crash

Suppose you have:

#include <stdio.h>
int main() {
    int *p = NULL;
    *p = 42;  // crash
    printf("Value: %d\n", *p);
    return 0;
}

Compile with debug info:

gcc -g -O0 crash.c -o crash

Run in gdb:

gdb ./crash
(gdb) run

The program will crash (SIGSEGV). Then:

Example session:

(gdb) run
Starting program: /path/crash
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555156 in main () at crash.c:5
5       *p = 42;  // crash
(gdb) print p
$1 = (int *) 0x0
(gdb) bt
#0  0x0000555555555156 in main () at crash.c:5

From this, you clearly see you dereferenced a null pointer.


Breakpoints and Stepping

Breakpoints let you stop execution at interesting points.

Setting breakpoints

Common ways:

  (gdb) break main
  (gdb) break my_function
  (gdb) break myfile.c:42
  (gdb) break

Managing breakpoints:

Stepping through code

When you hit a breakpoint:

Inspecting Variables and the Stack

When stopped at a breakpoint (or crash):

You can use C expressions:

(gdb) print a + b * 2

For the call stack:

This is extremely helpful for understanding how you reached a certain state.


Debugging Core Dumps (Post‑Mortem)

If your program crashes, the kernel can produce a core dump file, which contains the memory image at crash time.

  1. Enable core dumps (per shell):
   ulimit -c unlimited
  1. Run your program until it crashes. A core (or core.<pid>) file will appear according to kernel.core_pattern.
  2. Open the core file with gdb:
   gdb ./myprogram core

Then use bt, frame, info locals, etc. to inspect the state at crash time.

This is often used on production or test systems where you can’t reproduce the problem interactively.


Using gdb with Other Build Systems

strace: Tracing System Calls

strace shows every system call a process makes and the signals it receives. While gdb shows what your code is doing, strace shows what the process is asking the kernel to do.

Common uses:

Running a Program Under strace

Basic usage:

strace ./myprogram arg1 arg2

You’ll see a stream like:

openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
mmap(NULL, 12345, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f...
close(3)                                = 0
write(1, "Hello\n", 6)                  = 6
exit_group(0)                           = 0

Key things:

Error codes are shown using errno names:

openat(AT_FDCWD, "/does/not/exist", O_RDONLY) = -1 ENOENT (No such file or directory)

Attaching to a Running Process

Just like gdb, you can attach by PID:

strace -p <PID>

Example:

strace -p 12345

To see system calls from child processes too:

strace -f -p 12345

Press Ctrl+C in the strace terminal to detach/stop tracing.


Making strace Output Manageable

strace can be very verbose. Useful options:

  strace -o trace.log ./myprogram
  strace -f ./server
    strace -e trace=file ./myprogram
    strace -e trace=network ./client
    strace -e trace=open,close,read,write ./myprogram
  strace -s 200 -e trace=read,write ./myprogram

You can combine options, e.g.:

strace -f -tt -o trace.log ./myprogram

Common strace Debugging Scenarios

“File not found” or “Permission denied”

If your program prints a vague error like “could not load configuration”:

strace -e trace=file ./myprogram

Look for lines like:

openat(AT_FDCWD, "/home/user/.myapprc", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/myapp.conf", O_RDONLY) = -1 EACCES (Permission denied)

You now know which path is failing and why (ENOENT, EACCES, etc.).

Program hangs

If a program is “frozen”:

  1. Get its PID (e.g., pidof myprogram).
  2. Attach:
   strace -p <PID>
  1. See which syscall it’s blocked on.

For example:

read(3,  <unfinished ...>

or

connect(3, {sa_family=AF_INET, sin_port=htons(80), ...}, 16 <unfinished ...>

This tells you it’s waiting on reading input or attempting a network connection.


strace vs gdb: When to Use Which

They also complement each other: you might use strace to locate the broad area of trouble (e.g., failing file open), then gdb to inspect why the code is trying that path or using bad arguments.


Practical Tips and Patterns

With these two tools in your daily workflow, you can move beyond “add more print statements” and systematically understand what your programs are doing on Linux.

Views: 189

Comments

Please login to add a comment.

Don't have an account? Register now!