Program analysis is the art of learning how a program really behaves.
Sometimes that means reading source code carefully. Sometimes it means watching a process as it runs. Sometimes it means instrumenting a binary and collecting facts that would never be visible from source alone. In every case, the central habit is the same: move from guesswork to evidence.
In the context of this project, program analysis matters because it helps answer questions such as:
- What code paths run most often?
- Which functions call which other functions?
- Where does memory come from?
- Which syscalls are used?
- Which branches, instructions, or data accesses dominate execution?
- Is the slowdown caused by the target program or by the workload driver around it?
Taken as a subject, program analysis is broad. It helps to divide it into a few main areas and understand what each one contributes.
Static analysis studies a program without running it. It is the quiet, reflective side of the subject: the part where you inspect structure, relationships, and possible failures before execution ever begins.
Typical goals:
- find bugs and undefined behavior
- inspect control flow and call relationships
- reason about types, lifetimes, and data flow
- catch issues before runtime
Common tools:
- compiler warnings:
gcc,clang - static analyzers:
clang-tidy,cppcheck - sanitizers at build time:
-fsanitize=address,undefined,thread - source browsing and code search:
rg, IDE indexers
Dynamic analysis studies a program while it is running. This is where the subject becomes concrete, because the program stops being a static artifact and becomes a living process with timings, memory use, system calls, and observable behavior.
Typical goals:
- observe real execution behavior
- inspect function calls and timings
- trace memory usage
- trace system interaction
- collect counters and samples
Common tools:
valgrindperfstraceltracegdbbpftraceDynamoRIO
Binary instrumentation means inserting observation logic into a compiled program at runtime or rewrite time. It is one of the deeper parts of the field, and it becomes powerful when the easier forms of observation are no longer enough.
Typical goals:
- trace instructions
- trace memory references
- collect code coverage
- build custom analysis clients
- analyze behavior without recompiling the target
Common tools:
DynamoRIO- Intel PIN
- Valgrind instrumentation tools
The easiest way to keep the tooling straight is to group it by the kind of question it helps answer.
rg: fast code searchclang-tidy: style and correctness analysiscppcheck: static bug finding- compiler warnings: first line of defense
strace: syscalls, file access, process activity, futexes, mmap, I/Oltrace: dynamic library callsbpftrace: kernel and userspace dynamic tracing
gdb: breakpoints, stacks, memory, threads, disassemblyobjdump: assembly inspectionnm: symbolsreadelf: ELF and section inspection
valgrind --tool=callgrind: function cost and call graphvalgrind --tool=massif: heap growthheaptrack: heap allocation profilingperf record: sampled hotspots
DynamoRIO: dynamic instrumentation frameworkdrcachesim: cache simulation on top of DynamoRIOdrmemtrace: memory trace workflows
DynamoRIO is a dynamic binary instrumentation framework.
It is useful when you want to:
- inspect instructions at runtime
- instrument memory accesses
- collect custom traces
- study control flow
- build your own runtime analysis tools
It is more specialized than perf or strace, and that is exactly why it is so useful later. Usually you reach for it only after simpler tools have already narrowed the mystery and you need a deeper, more custom view of runtime behavior.
For a systems project, a good working order usually looks like this:
- Read the source and identify likely hot paths.
- Use
rgand code navigation to understand structure. - Run the target and validate behavior.
- Use
straceif you suspect syscall or file-I/O overhead. - Use
perforcallgrindto find expensive code paths. - Use
heaptrackormassifif memory behavior looks suspicious. - Use
gdbfor correctness debugging or state inspection. - Use
DynamoRIOonly when you need custom binary instrumentation.
This subject becomes much easier when it is learned in the order that its ideas depend on one another.
The temptation is always to start with the most impressive tool. Resist that. The strong path begins with the shape of a program, then moves to the shape of a process, then to observation, then to deeper tracing, and finally to advanced instrumentation.
Learn first:
- how compiled programs are built
- source files, object files, libraries, executables
- symbols, debug info, and build modes
- compiler warnings and basic sanitizers
- how to search and navigate a codebase
Why this comes first:
- all later analysis depends on understanding what you are analyzing
- symbol names, debug info, and binaries appear in almost every tool
Tools:
gcc,clangrgnmobjdumpreadelf
What to practice:
- build a small C or C++ program in debug and release mode
- inspect symbols in the binary
- inspect the executable with
readelf
Learn next:
- process, thread, address space, stack, heap
- system calls
- dynamic linking
- memory mapping
- file descriptors
Why this comes second:
- tracing tools make much more sense once you know what the OS sees
Tools:
straceltracegdb
What to practice:
- run a CLI tool with
strace -c - observe
open,read,write,mmap, andfutex - inspect stack frames and memory in
gdb
Learn next:
- call stacks
- function boundaries
- stepping through execution
- breakpoints and watchpoints
- source to assembly relationship
Why this comes before advanced profiling:
- hotspot reports are much easier to understand if you already know how calls and stacks work
Tools:
gdbobjdump- disassembly view in IDEs or debuggers
What to practice:
- step through a function in
gdb - inspect a backtrace
- compare a function's source with its disassembly
Learn next:
- statistical profiling
- hotspot discovery
- sampled stacks
- inclusive versus self cost at a high level
Why this stage matters:
- this is usually the first fast way to see where execution spends time
Tools:
perf recordperf report- FlameGraph
AMDuProfCLI
What to practice:
- profile one CLI program
- identify top hotspots
- generate and read a flame graph
Learn next:
- exact or more detailed cost models
- call graphs
- instrumentation overhead
- tradeoffs between precision and realism
Why this comes after sampling:
- instrumentation tools are slower and more specialized
- they are easier to use well once sampling has already narrowed the question
Tools:
valgrind --tool=callgrindvalgrind --tool=massifheaptrack
What to practice:
- compare
perfandcallgrindon the same workload - inspect inclusive cost versus self cost
- analyze heap growth and allocation sources
Learn next:
- allocation sources
- heap growth
- locality
- lock waits
- syscall-heavy behavior
- user space versus kernel space interaction
Why this matters:
- many important system-software issues are not just "hot function" issues
Tools:
heaptrackmassifstraceperf statbpftrace
What to practice:
- inspect allocation-heavy code
- compare syscall-heavy and compute-heavy workloads
- trace latency or wait behavior
Learn next:
- binary instrumentation concepts
- instruction tracing
- memory reference tracing
- custom analysis clients
Why this comes late:
- it is more specialized and benefits from strong foundations in earlier stages
Tools:
DynamoRIO- DynamoRIO clients such as
drcachesim
What to practice:
- understand what a binary instrumentation framework does
- run or study an existing DynamoRIO client
- compare sampling results with binary-instrumentation results
Learn last:
- eBPF tracing patterns
- live observability
- latency histograms
- dynamic probes in kernel and user space
- cross-layer analysis
Why this is a late stage:
- it combines OS knowledge, runtime behavior, and measurement design
Tools:
bpftrace- advanced
perf - AMD uProf advanced modes
What to practice:
- trace a latency distribution
- correlate syscall behavior with sampled hotspots
- build a workflow that combines multiple tools
Program Analysis Roadmap
Stage 1 Program structure and binaries
|
v
Stage 2 Process model, syscalls, runtime environment
|
v
Stage 3 Control flow, call stacks, debugging, source <-> assembly
|
+-----------------------------+
| |
v v
Stage 4 Sample-based analysis Stage 5 Instrumentation-based analysis
| |
+-------------+---------------+
|
v
Stage 6 Memory, data movement, OS interaction analysis
|
v
Stage 7 Binary instrumentation and custom runtime analysis
|
v
Stage 8 Advanced observability and cross-layer tracing
If you want one clean path from start to finish, use this:
- Build process, debug symbols, executable layout
- Process model, stack, heap, threads, syscalls
- Code navigation and source reading
gdband backtracesstraceperf recordandperf report- Flame graphs
callgrindmassifandheaptrackperf statbpftraceDynamoRIO- custom instrumentation and combined workflows
At its heart, program analysis is about answering:
- what ran,
- how it ran,
- what it touched,
- what it asked the OS to do,
- and where the cost or bug actually came from.
It is broader than performance engineering, but it supports performance work at almost every step. Before you can improve a system, you have to learn how to see it.