What is Linux Profiling and Symbol Tables-Free Embedded Linux Course

PREV_LEC | NEXT_LEC
Linux Profiling and Symbol Tables
Understand the observer effect and the compile-time choices that decide whether a profiler can even name the function it is measuring

Every free Linux kernel development course eventually reaches the point where “does it work” stops being the only question, and “how fast does it work, and where does it slow down” takes over. Before touching a single profiling tool, though, there are two things every embedded developer needs to internalize: profiling itself changes the thing being profiled, and a profiler is only as useful as the symbol and debug information available to it. This lecture builds both ideas from first principles, then proves the second one with a small original demo you can run on any Linux machine in minutes.

Key Terms in This Lecture
observer effect symbol table DWARF debug info -g compile flag frame pointer function inlining addr2line nm / objdump

Prerequisites

A working GNU toolchain (gcc, binutils) on a Linux machine or a Yocto/Buildroot-built target, comfort with basic C, and familiarity with compiling and running a program from the command line. No prior profiling experience is assumed — this lecture is the on-ramp for the rest of the profiling and tracing material in this free Linux kernel development course.

The Observer Effect: Measurement Has a Cost

In physics, checking the current in a wire means inserting a small resistor and reading the voltage drop across it — but that resistor changes the very current you are trying to measure. Software profiling has the identical problem. A profiler that samples the program counter every millisecond, a tracer that logs every function entry, or a debugger single-stepping through code all consume CPU cycles, evict cache lines, and sometimes touch disk or network I/O to store their own output. None of that activity would exist if you weren’t watching.

This is not a reason to distrust profiling — it is a reason to be deliberate about how you profile. Three consequences follow directly from the observer effect:

  • Timing shifts. A function that looked fast in isolation may look slower under a sampling profiler simply because the profiler’s own interrupt handler is stealing cycles from it.
  • Cache and TLB pollution. Instrumentation code and the buffers it writes into compete for the same L1/L2 cache lines as your workload, so cache-miss-sensitive code paths can look artificially worse than they are in production.
  • Heisenbugs in reverse. Sometimes a bug that reproduces reliably in normal operation becomes intermittent or vanishes entirely once a tracer is attached, because the timing window that triggered it no longer exists.
Rule of thumb: always try to measure on the actual target hardware, with a release build, using a realistic data set, and with as few extra background services running as possible. The closer your measurement environment is to production, the more you can trust the numbers you get back.

Why a Free Linux Kernel Development Course Spends Time on Symbol Tables

Once you accept that every measurement has overhead, the next question is: what does a profiler actually give you back? Under the hood, a sampling profiler like perf or a tracer like Ftrace does not record “function ep_fib ran for 4ms.” It records raw instruction pointer addresses — plain 64-bit numbers. Turning those numbers into function names and line numbers you can act on is a completely separate step, and it depends entirely on two things being present: a symbol table and, ideally, debug information.

A symbol table is a section inside an ELF binary (typically .symtab, or .dynsym for the exported subset) that maps addresses to names — function names, global variable names, and so on. Debug information is a much richer, separate set of ELF sections (in the DWARF format on Linux) that additionally maps addresses to source file names, line numbers, and even local variable locations. A binary can have a symbol table with no debug info, debug info with a stripped symbol table, or both, or neither. What you compiled with decides which of these you get.

Compile Flags That Decide What a Profiler Can See

The single most common mistake in any Linux profiling workflow — and one this free Linux kernel development course wants you to avoid from day one — is profiling a binary that was never built to be profiled. The table below summarizes the flags that matter and what each one actually buys you.

Flag What It Does Why a Profiler Cares
-g Emits DWARF debug information alongside the code Lets tools like addr2line, perf report, and GDB map addresses to source file and line, not just a bare function name
-O0 vs -O2/-O3 Controls how aggressively the compiler reorders, merges, and eliminates code Higher optimization levels inline small functions and reorder instructions, which can make line-level attribution approximate or misleading; lower levels give exact attribution at the cost of realistic timing
-fno-omit-frame-pointer Keeps a dedicated frame pointer register instead of reusing it as a general-purpose register Required by frame-pointer-based call-graph unwinding (used by many perf configurations and by simple stack walkers); without it, call stacks can be truncated or wrong
-pg Inserts mcount-style instrumentation calls at every function entry Required specifically by gprof-style call-graph profiling; has no effect on tools that don’t consume that instrumentation
strip / no strip Removes the symbol table from the final binary A stripped release binary shown to a profiler will report raw hex addresses instead of function names unless you keep an unstripped copy around for offline symbol resolution

The key trade-off

Debug symbols themselves add zero runtime overhead — they sit in ELF sections the loader never executes, so shipping a -g build changes nothing about how fast it runs. What does change behavior is -O0 and -fno-omit-frame-pointer, because those genuinely alter the generated code. In practice, the common target for kernel and application profiling work is a release-optimized build compiled with -g and -fno-omit-frame-pointer, keeping the unstripped binary on the build machine for later symbol resolution.

How a Profiler Turns a Raw Address Into Something Useful
Sampled instruction pointer (raw hex address)
→ Look up address in ELF .symtab (from nm/objdump) → nearest function symbol found
→ If DWARF debug info (-g) is present → resolve to source file : line number
→ If frame pointers were kept (-fno-omit-frame-pointer) → walk the call stack for full context
→ Profiler report shows: function name, file:line, and call chain

Hands-On: Watching Symbols Disappear Under Optimization

The clearest way to see why compile flags matter is to build the exact same source file twice — once the way you would ship it, and once the way you would profile it — and compare what a symbol table actually contains. Here is a small, original workload written for this lecture.

/* ep_profile_demo.c
 * A tiny CPU-bound workload used to explore how compiler flags
 * affect symbol tables and debug information. */

#include <stdio.h>
#include <stdlib.h>

static long ep_fib(int n)
{
    if (n < 2)
        return n;
    return ep_fib(n - 1) + ep_fib(n - 2);
}

static long ep_sum_squares(int limit)
{
    long total = 0;
    for (int i = 0; i < limit; i++)
        total += (long)i * i;
    return total;
}

int main(int argc, char **argv)
{
    int n = (argc > 1) ? atoi(argv[1]) : 30;

    long fib_result = ep_fib(n);
    long sum_result = ep_sum_squares(20000000);

    printf("fib(%d) = %ld\n", n, fib_result);
    printf("sum_squares = %ld\n", sum_result);

    return 0;
}

Now build it two different ways:

# A "shipping" release build — no debug info, aggressive optimization
gcc -O2 -o ep_profile_release ep_profile_demo.c

# A "profiling-ready" build — debug info, frame pointers kept
gcc -O0 -g -fno-omit-frame-pointer -o ep_profile_debug ep_profile_demo.c

Compare what each binary’s symbol table actually contains:

$ nm ep_profile_release | grep ep_
$ nm ep_profile_debug | grep ep_
0000000000401136 t ep_fib
00000000004011a2 t ep_sum_squares
Notice what happened: the first nm command against the release build often returns nothing at all for ep_fib and ep_sum_squares. At -O2, GCC is free to inline both small static functions directly into main, so there is no longer a separate function for the symbol table to describe. A profiler pointed at that release binary cannot attribute time to “ep_fib” by name — the function, as a distinct entity, no longer exists in the compiled output. This is exactly the situation the source material calls out: some tools need additional information the running system doesn’t naturally provide.

With the debug build, you can go one step further and resolve a raw address back to a source line:

$ objdump -d ep_profile_debug | grep -A1 "<ep_fib>:"
0000000000401136 <ep_fib>:
  401136:   f3 0f 1e fa             endbr64

$ addr2line -e ep_profile_debug -f 0x401136
ep_fib
/home/user/ep_profile_demo.c:8

Run the same addr2line command against ep_profile_release and it will either fail to resolve the address to any function you recognize, or point you at main instead — because that is genuinely where the inlined code now lives. This small experiment is the entire “symbol tables and compile flags” problem in miniature: the system you observe and the system you can meaningfully report on are not automatically the same system.

Continuing This Free Linux Kernel Development Course: Applying It to Real Tools

Every profiling and tracing tool covered later in this series inherits this same dependency:

  • perf works best when the running binary — application or kernel — has an unstripped copy with matching symbols available, either installed on the target or supplied via perf‘s build-id lookup mechanism.
  • Ftrace and LTTng for kernel-side tracing need the kernel itself built with the relevant tracer config options enabled, which in practice means building and deploying a kernel image specifically for this kind of work rather than assuming your production kernel already has it.
  • gprof requires the -pg instrumentation flag at compile time — without it, there is no call-graph data to collect no matter how the program is run afterward.

The pattern across all three: decide what you need to measure, then choose your build flags before you start, not after you’ve already collected a confusing set of hex addresses.

Common Mistakes and Troubleshooting

Symptom Likely Cause Fix
Profiler output is full of raw hex addresses instead of function names Binary was stripped, or the profiler can’t find a matching unstripped copy Keep an unstripped build alongside the stripped one you deploy, and point the profiling tool at it explicitly
Expected function is completely missing from the profile The function was inlined away at a higher optimization level Mark the function __attribute__((noinline)) for the duration of profiling, or rebuild that translation unit at -O0
Call graph is truncated or shows impossible call chains Frame pointer was omitted by the optimizer Rebuild with -fno-omit-frame-pointer, or switch the unwinder to DWARF-based (perf record --call-graph dwarf) if you cannot rebuild
Profiling results look wildly different from production behavior Debug or instrumented build changed timing characteristics, or extra services were running during the measurement Profile a release-optimized build with only debug info added back, on a quiet system, against a realistic workload

Best Practices

  • Treat “profiling build” as its own build configuration — release optimization plus -g plus -fno-omit-frame-pointer — rather than reusing either your debug build or your stripped release build unmodified.
  • Keep unstripped binaries archived alongside every release you ship, indexed by version or build ID, so you can symbolize a crash or a profile from production weeks later.
  • Change one variable at a time. If you need call graphs, add frame pointers. If you need line-level attribution, add debug info. Don’t drop to -O0 “just in case” — it changes the very performance characteristics you’re trying to measure.
  • Adopt a wait-and-see approach to instrumentation: only add tracepoints or recompile with instrumentation once you have a specific hypothesis to test, since every change you make to the system is a change to what you’re measuring.

Performance Considerations

Debug information adds disk and binary size but no runtime cost, since the loader never touches DWARF sections during execution. Frame pointer retention typically costs low single-digit percent overhead on modern x86-64 and ARM64 targets because it reserves one register that would otherwise be available for the optimizer — usually an acceptable trade for reliable call graphs in production profiling.

Security Considerations

Debug information can reveal internal function names, file paths, and sometimes source line content indirectly through crash reports — treat unstripped debug binaries for shipped products as sensitive build artifacts, store them separately from what you distribute publicly, and strip symbols from anything that leaves your build infrastructure.

Summary and Key Takeaways

  • Every profiling or tracing tool changes the system it observes — plan measurements with that overhead in mind, and prefer target hardware, release builds, and realistic data.
  • A symbol table maps addresses to names; DWARF debug info (from -g) additionally maps addresses to source file and line — a profiler needs at least the former and ideally both.
  • Optimization can delete the very functions you want to measure through inlining; frame pointer omission can break call-graph unwinding; instrumentation-based tools like gprof need their own dedicated compile flag.
  • The fix in every case is deciding your build configuration before you start profiling, not reverse-engineering hex addresses afterward.

This foundation — the observer effect on one side, symbol tables and compile flags on the other — is what makes every tool later in this free Linux kernel development course actually usable. Skip it, and even the most sophisticated tracer will hand you a report full of addresses you can’t act on.

Interview Questions

Why don’t debug symbols slow down a program at runtime?

Because DWARF debug sections are pure metadata stored alongside the code in the ELF file — the CPU never executes them. Only actual code changes, like disabling optimization or keeping a frame pointer, affect runtime behavior.

Why might a function be completely absent from a profiler’s report even though it’s clearly being called?

The compiler likely inlined it into its caller at a higher optimization level, so it no longer exists as a distinct symbol for the profiler to attribute samples to.

What’s the practical difference between a symbol table and debug information?

A symbol table (ELF .symtab) maps addresses to names only. Debug information (DWARF, from -g) is richer and maps addresses to source file, line number, and variable locations.

Why would you deliberately keep the frame pointer register instead of letting the compiler reuse it?

Frame-pointer-based stack unwinders, used by many profiling configurations, rely on that register to walk the call stack; omitting it can truncate or corrupt reported call graphs.

Frequently Asked Questions

Do I need to recompile my whole application just to profile it?

Not always. If you already build with -g and keep unstripped binaries, most sampling profilers work without a rebuild. Recompiling is only strictly required when you need gprof-style instrumentation (-pg) or want to prevent a specific function from being inlined away.

Does compiling with -O0 make profiling results meaningless?

It makes timing results less representative of production performance, since -O0 disables the optimizations your shipped binary actually uses. It’s best reserved for cases where you need exact line-level attribution and understand the trade-off, not as a default profiling configuration.

Can I profile a kernel the same way I profile an application?

The same symbol/debug-info principle applies, but the kernel needs its own debug symbols (CONFIG_DEBUG_INFO and related options) and, for tools like Ftrace and LTTng, specific tracer configuration options enabled at build time — this usually means building and deploying a kernel image dedicated to profiling and tracing work.

What tool shows me a binary’s symbol table directly?

nm lists symbols from the symbol table, readelf -S shows all ELF sections including debug sections if present, and objdump -d gives a disassembly annotated with whatever symbol names are available.

Is stripping a binary the same as never compiling with -g?

No. Stripping removes the symbol table from an already-built binary using strip or a linker option; you can strip a binary that was built with -g, and you can also build without -g from the start. Either path leaves you with a binary a profiler can’t symbolize on its own.

Why does the lecture recommend keeping frame pointers even for a release build?

Because the alternative — omitting the frame pointer for a small register-allocation gain — breaks the cheapest and most portable call-graph unwinding method available, forcing you onto slower DWARF-based unwinding or losing call-graph data entirely during production profiling.

Will this apply to embedded/cross-compiled targets too?

Yes — the same GCC flags and the same ELF/DWARF concepts apply whether you’re compiling natively or cross-compiling for an ARM or RISC-V target in Yocto or Buildroot. The only difference is you’ll typically run nm, objdump, and addr2line from your cross toolchain (e.g. arm-linux-gnueabihf-nm) against the target binary from your host machine.

Keep Building Real Kernel Skills
This lecture is part of EmbeddedPathashala’s free Linux kernel development course, covering everything from device drivers to profiling and tracing on real hardware. Continue to the next lecture to start putting these symbol-table fundamentals to work with top and perf.
PREV_LEC | NEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *