GDB Poor Man’s Profiler
A free linux kernel development course lecture: profile any running Linux application using nothing but GDB — no special tools, no kernel config changes.
Before you reach for a dedicated profiler, it helps to understand the simplest form of
statistical profiling that exists on Linux: repeatedly interrupting a running
program with a debugger and recording where it stopped. This lecture is part of our
free linux kernel development course and our broader
free embedded systems course track, and it lays the conceptual groundwork
you will need before the next lecture, where we move on to the kernel’s own
perf_events subsystem. Everyone who takes a free linux device drivers course
eventually needs to answer “why is my driver’s user-space test app slow?” — this technique is
often the fastest way to get a first answer, and it works on any target that has GDB, embedded
or not.
Key Concepts in This Lecture
What You Will Learn
- What statistical profiling means and why sampling works
- Attaching GDB to a live process, locally and remotely with gdbserver
- Building a repeatable sample-and-continue loop by hand
- A hands-on demo: catching a hidden hotspot in a small C program
- Where this technique breaks down and when to reach for perf instead
Prerequisites
You should be comfortable compiling C on Linux and know basic GDB commands
(break, run, continue). Familiarity with a target/host
cross-debug setup helps but is not required — this lecture works the same on a native desktop
Linux install as it does on an embedded board.
Why Sampling Works
A profiler’s job is to tell you where a program spends its time. There are two broad families of technique. Instrumenting profilers insert extra code at every function entry and exit and measure exact elapsed time — accurate, but they change the program’s timing and can be heavy to set up. Statistical profilers take the opposite approach: they interrupt the running program at intervals and simply record which function it happened to be in at that instant. A single sample tells you almost nothing. A few hundred samples tell you a great deal, because functions where the program spends more time are proportionally more likely to be caught by a random interrupt. This is the same statistical principle behind opinion polling — you don’t need to ask every voter to get a good estimate of the result.
The “poor man’s profiler” is simply this idea applied using a tool every embedded developer already has installed: GDB. You attach, note the stack, resume, and repeat. No extra packages, no kernel reconfiguration, no cross-compiling a profiling agent for your target — which is exactly why it is worth knowing even in 2026, when far more capable tools exist.
The Manual Sampling Loop
The procedure has four steps, repeated as many times as you have patience for:
Manual Statistical Sampling Loop
Attaching Locally vs. Remotely
On a desktop or a target where GDB itself runs natively, you attach directly to a process ID:
$ gdb -p $(pidof ep_hotspot_demo)
(gdb) bt
(gdb) continue
On a resource-constrained embedded target, running full GDB on the board is often impractical.
Instead you run the small gdbserver stub on the target and drive it from a
cross-GDB on your build host:
# On the target board:
target$ gdbserver --attach :2345 $(pidof ep_hotspot_demo)
# On the development host:
host$ arm-linux-gnueabihf-gdb ep_hotspot_demo
(gdb) target remote target-ip:2345
(gdb) bt
(gdb) continue
continue — typically a second or more. For a program with a
tight real-time deadline, this pause itself can distort behavior. Keep that intrusiveness in
mind before you conclude a function is “slow”: you may just be looking at it because it happens
to be running whenever you glance at the clock.
Hands-On Demo: Catching a Hidden Hotspot
Here is a small original demo program with one obviously slow function buried among several fast ones, so you can watch the sampling loop find it.
/* ep_hotspot_demo.c */
#include <stdio.h>
#include <unistd.h>
static void ep_fast_task(void)
{
volatile int i;
for (i = 0; i < 1000; i++)
;
}
static void ep_slow_task(void)
{
volatile long i;
for (i = 0; i < 200000000L; i++)
;
}
static void ep_work_cycle(void)
{
ep_fast_task();
ep_slow_task(); /* the hidden hotspot */
ep_fast_task();
}
int main(void)
{
while (1)
ep_work_cycle();
return 0;
}
Build it with debug symbols so backtraces show real function names and line numbers:
$ gcc -g -O0 -o ep_hotspot_demo ep_hotspot_demo.c
$ ./ep_hotspot_demo &
Now attach and sample it a few times:
$ gdb -p $(pidof ep_hotspot_demo)
(gdb) bt
#0 ep_slow_task () at ep_hotspot_demo.c:15
#1 ep_work_cycle () at ep_hotspot_demo.c:21
#2 main () at ep_hotspot_demo.c:27
(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
ep_slow_task () at ep_hotspot_demo.c:15
15 for (i = 0; i < 200000000L; i++)
(gdb) bt
#0 ep_slow_task () at ep_hotspot_demo.c:15
#1 ep_work_cycle () at ep_hotspot_demo.c:21
#2 main () at ep_hotspot_demo.c:27
(gdb) continue
Repeat the Ctrl+C / bt / continue cycle ten or fifteen
times and you will find that the overwhelming majority of your backtraces land inside
ep_slow_task, even though it is a single call among three in
ep_work_cycle. That is statistical profiling working exactly as designed — the
function consuming the most CPU time gets caught the most often.
| Aspect | Poor Man’s Profiler (GDB) | perf (next lecture) |
|---|---|---|
| Setup cost | None — GDB is already there | Needs CONFIG_PERF_EVENTS + perf binary |
| Overhead per sample | High (full stop-the-world) | Very low (PMU-driven) |
| Sample count achievable | Tens, by hand | Thousands per second |
| Whole-system view | No — one process at a time | Yes — system, kernel, or process |
| Best for | Quick sanity check, no tooling available | Serious performance investigation |
Common Mistakes
- Too few samples. Five backtraces prove nothing; aim for at least 20-30 before drawing conclusions.
- Sampling only during idle periods. If you attach right as the program starts, you may only be watching one-time setup code, not steady-state behavior.
- Building without debug symbols. Without
-g, backtraces show raw addresses instead of function names, making the samples nearly useless. - Forgetting the intrusiveness. On latency-sensitive code, this technique can itself trigger the very timing problems you are trying to diagnose.
Best Practices
- Always compile the target binary with
-g(and avoid full-O2/-O3if you want backtraces to map cleanly to source lines). - Sample across the full lifetime of a workload, not just its startup burst.
- Use this technique as a five-minute first check, then move to
perfwhen you need statistical confidence or system-wide visibility. - On embedded targets, prefer
gdbserverover installing a full GDB on the board — it keeps the target’s storage and memory footprint small.
Summary and Key Takeaways
The poor man’s profiler turns an ordinary debugger into a rough-and-ready statistical profiler:
attach, record the stack, resume, repeat. It costs nothing to set up, works on any target with
GDB, and is intrusive enough that you should treat its results as a starting hint rather than a
final answer. In the next lecture of this free linux kernel development course, we build on the
same statistical idea using the kernel’s own perf_events subsystem, which samples
with far lower overhead and far higher sample rates.
Frequently Asked Questions
Is the poor man’s profiler accurate?
It is a rough estimate, not a precise measurement. With enough samples it reliably points at the right hotspot, but the exact percentages it implies are not trustworthy — for that, use a dedicated sampling profiler like perf.
Does this technique work on a live production system?
It is not recommended. Each attach-and-stop cycle pauses the process, which is disruptive on production or real-time systems. Use it on a test or development instance instead.
Can I automate the Ctrl+C / backtrace / continue loop?
Yes — GDB Python scripting or an external wrapper script can drive the loop automatically and log each backtrace, removing the need to type commands by hand for every sample.
What if my program isn’t multi-threaded — does profiling still work?
Yes. The technique works identically on single-threaded and multi-threaded programs; for multi-threaded programs, use GDB’s thread apply all bt to capture every thread’s stack in one sample.
Why not just use perf from the start?
perf needs a kernel built with CONFIG_PERF_EVENTS and a matching perf binary on the target. On a board where that isn’t available yet, GDB sampling gives you an immediate first answer with zero extra setup.
Does this technique require root access?
You need permission to attach a debugger to the target process — root if it belongs to another user, but no special privileges beyond normal ptrace permissions for your own processes.
Keep Learning Linux Kernel Development — Free
This lecture is part of EmbeddedPathashala’s free linux kernel development course and free embedded systems course. Continue to the next lecture to learn statistical profiling the kernel’s own way, with perf.

2 Comments