AnyLearn
All lessons
Programmingadvanced

Dynamic analysis and debuggers

Reverse engineering by running the binary. Why dynamic analysis sees what static cannot, how debuggers and breakpoints actually work (INT3 vs hardware vs page-fault), tracing (strace, ltrace, dtrace, eBPF), dynamic binary instrumentation with Frida and PIN, the common anti-debug tricks, sandboxing with Unicorn and Qiling, and the static-dynamic loop that does the real work.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 8

What dynamic analysis sees that static cannot

Static analysis sees every possible code path; dynamic analysis sees the taken one, with real values flowing through it. Each loses what the other captures:

QuestionStaticDynamic
What can this binary do?All branchesOnly the one executed
What values does this variable hold?Symbolic / unknownConcrete
Is this code dead?Hard to proveEasy to observe
Does this string get decrypted at runtime?InvisibleTrivial to dump
Does this branch depend on the wall clock?Hard to detectVisible from a single trace

Most real RE work is hybrid: a static pass identifies interesting functions and call sites; dynamic execution provides the runtime values that close the loop. Encrypted strings appear in plaintext mid-execution. Hash functions reveal their constants. License-validation logic flows through real inputs. The static view tells you where to look; the dynamic view tells you what is actually happening at that point.

Full lesson text

All 8 steps on one page — for reading, reference, and search.

Show

1. What dynamic analysis sees that static cannot

Static analysis sees every possible code path; dynamic analysis sees the taken one, with real values flowing through it. Each loses what the other captures:

QuestionStaticDynamic
What can this binary do?All branchesOnly the one executed
What values does this variable hold?Symbolic / unknownConcrete
Is this code dead?Hard to proveEasy to observe
Does this string get decrypted at runtime?InvisibleTrivial to dump
Does this branch depend on the wall clock?Hard to detectVisible from a single trace

Most real RE work is hybrid: a static pass identifies interesting functions and call sites; dynamic execution provides the runtime values that close the loop. Encrypted strings appear in plaintext mid-execution. Hash functions reveal their constants. License-validation logic flows through real inputs. The static view tells you where to look; the dynamic view tells you what is actually happening at that point.

2. The major debuggers

Four debuggers cover most reverse engineering:

DebuggerPlatformsStyleNotes
gdbLinux, BSD, embeddedCommand-line, scriptableThe default; remote debugging via gdbserver; pwndbg / GEF plugins add RE-flavored views
lldbmacOS, iOS, LinuxCommand-lineDefault on Apple platforms; Python scriptable; closely tracks LLVM IR
x64dbgWindowsGUIOpen source; the de-facto Windows RE debugger; excellent assembly view
WinDbgWindows + kernelGUI / consoleMicrosoft's tool; the only realistic option for kernel-mode and crash-dump analysis

Across all of them, the same primitive operations:

  • Run / continue / interrupt — start or pause execution.
  • Step — single instruction (stepi), single line (step/next if source available), or over function calls (nexti).
  • Breakpoints — pause execution when reaching an address.
  • Watchpoints — pause when a memory location is read or written.
  • Read/write memory and registers — at any pause point.
  • Scripting — gdb's Python API, lldb's commands, x64dbg's scripts, WinDbg's JavaScript engine.

The scripting layer is what makes a debugger an analysis tool rather than a step-through interface. Logging every call to a specific function with its arguments and return value is a five-line script in any of these.

3. How breakpoints actually work

Three breakpoint mechanisms, with different trade-offs:

  • Software breakpoint (INT3 / 0xCC). The debugger overwrites the target byte with 0xCC (int3 — debug trap). When the CPU hits it, it raises an interrupt; the OS routes the trap to the debugger. On resume, the debugger restores the original byte, single-steps one instruction, then puts the 0xCC back. Cheap and unlimited in count, but modifies the code — visible to anti-debug code that checksums itself or scans for 0xCC.
  • Hardware breakpoint (DR0–DR7 on x86). The CPU has four debug registers that trigger a trap when a specific address is executed, read, or written. Invisible to code inspection, but you only get four of them. Used by watchpoints and by stealthier breakpoints on anti-debug-aware targets.
  • Page-fault breakpoint. Mark the target page as non-executable (or non-readable / non-writable for watchpoints) via the page table; the CPU faults on access; the OS notifies the debugger. Unlimited and stealthy but coarse-grained (page-sized) and slower than the other two. Used by some advanced debuggers for ranges that would exhaust hardware slots.

Knowing which mechanism you're using matters. INT3 breakpoints inside critical anti-debug code get detected. Hardware breakpoints on IsDebuggerPresent survive checksum sweeps but conflict with the next analyst who wants the same DR slot. Page-fault breakpoints survive both but slow execution noticeably.

4. Tracing — watching from outside

Sometimes you don't need to pause execution — you need to log what the binary did. The tracing tools by platform:

# Linux: system calls and library calls.
strace -f ./app                  # syscalls + arguments
strace -e trace=network ./app    # only network-related calls
ltrace ./app                     # libc / library calls
ltrace -l libssl.so.3 ./app      # only calls into libssl

# Linux: kernel-level tracing.
perf trace ./app                 # newer, BPF-backed
bpftrace -e 'tracepoint:syscalls:sys_enter_open { @[comm] = count(); }'

# macOS:
dtruss ./app                     # roughly strace for dtrace
fs_usage / sc_usage              # filesystem + syscall view

# Windows:
Process Monitor                  # filesystem + registry + network + process events
API Monitor                      # hook arbitrary API calls
ETW (Event Tracing for Windows)  # the kernel-side tracing substrate

Tracing is non-invasive in the sense that it doesn't pause the process, but it reveals only what crosses the boundary you're watching — syscalls, library calls, registry reads, file I/O. Internal state changes are invisible to a pure tracer. Combine with debug pauses at interesting points to get both: the call sequence from the trace, the values at each call from the debugger.

5. Dynamic Binary Instrumentation

Dynamic Binary Instrumentation (DBI) injects code into a running process at the instruction level. The major frameworks:

  • Frida. JavaScript-driven; cross-platform (Linux, macOS, Windows, iOS, Android); the fastest path to scripted hooks. Used widely for mobile RE and for runtime API monitoring.
  • PIN (Intel). C++ instrumentation framework; very fast; the go-to for performance and architectural analyses. Older but battle-tested.
  • DynamoRIO. Open source; closer to the metal than PIN; supports many architectures.

Frida's hook pattern in 10 lines:

// Inject into a running app; hook crypto_verify and log every call.
Interceptor.attach(Module.findExportByName(null, "crypto_verify"), {
  onEnter(args) {
    this.input = args[0];
    console.log("[verify] input =", Memory.readUtf8String(args[0]));
  },
  onLeave(retval) {
    console.log("[verify] returned", retval);
    if (retval.toInt32() === 0) {
      console.log("[verify] forcing success");
      retval.replace(1);
    }
  },
});

Three things this enables that pure debugging does not:

  • High-throughput logging without pausing the process — useful for protocols and games.
  • Inline transformation of arguments and return values — the script can rewrite behavior, not just observe it.
  • Coverage and trace collection at scale — every call to read(), every comparison of an input byte against a key byte, captured to a log for offline analysis.

6. Anti-debugging — what targets watch for

Binaries that resist analysis check for a debugger and behave differently when one is attached. Common techniques and what they look for:

TechniqueHow it worksBypass
IsDebuggerPresent()Reads PEB flag set by the loader when a debugger is attachedPatch the function or the PEB byte
CheckRemoteDebuggerPresentSame idea, more generalPatch the return value
NtQueryInformationProcess(ProcessDebugPort)Direct query to the kernel about debug portHook the ntdll call
Timing checksMeasure how long a small loop takes; debuggers slow single-step by orders of magnitudePatch out the timing call or fake the rdtsc result
INT3 scanningWalk own code, hash it or count 0xCC bytes; bail if changedUse hardware breakpoints (DRn) instead
TLS callbacksCode that runs before main, often containing the anti-debug check; debuggers that break at main miss itConfigure the debugger to stop at the actual entry point or set breakpoints on TlsCallback_*
Self-modifying code / packersReal code only appears after runtime decryption (see binary-formats lesson)Run to the OEP, dump the unpacked image, analyze that

These checks are visible in static analysis. The standard playbook: read the binary statically to identify which anti-debug methods are in use, plan bypasses before attaching a debugger, and confirm with one dynamic run that the bypass holds before doing serious analysis.

7. Emulation and sandboxes

Sometimes you don't want to run the binary on your real machine — you want to execute fragments in a controlled environment, or run code for a different architecture, or simply isolate untrusted samples. The relevant tools:

  • CPU emulators. Unicorn is a CPU-only emulator extracted from QEMU's TCG; it lets you load arbitrary bytes, set up memory, run, and read registers afterwards — no OS, no syscalls. Useful for shellcode analysis and small algorithmic chunks (decrypt a buffer with the binary's own routine without launching the full program).
  • OS emulators. Qiling layers Unicorn with userspace OS emulation (syscalls, libc, file system). Lets you run an ELF or PE binary on a different host, with hooks at any syscall.
  • Full-system emulation. QEMU (full system) boots a complete VM. Combine with PANDA for whole-system replayable traces.
  • Symbolic execution. angr runs a binary symbolically — every input is a variable; the engine explores branches and accumulates path constraints. Solves "find the input that makes this return 0 branch run" automatically. Powerful for crackmes; expensive to scale.
  • Sandboxes for malware. Cuckoo, CAPE, vendor sandboxes (Hybrid Analysis, Joe Sandbox) — run the binary in a snapshotted VM, log everything, snapshot back.

The choice scales by scope: Unicorn for a function, Qiling for a small binary, QEMU for a whole system, sandbox services when you need the report and not the process.

8. The static-dynamic loop

Mature RE is a loop, not a sequence:

┌────────────────────────────────────────────────┐
│  1. Static triage (format, strings, imports)   │
│     → form hypothesis about purpose            │
└────────────────┬───────────────────────────────┘
                 ▼
┌────────────────────────────────────────────────┐
│  2. Static disassembly + decompilation         │
│     → identify interesting functions           │
└────────────────┬───────────────────────────────┘
                 ▼
┌────────────────────────────────────────────────┐
│  3. Dynamic run with breakpoints / tracing     │
│     → observe actual values, branches taken    │
└────────────────┬───────────────────────────────┘
                 ▼
┌────────────────────────────────────────────────┐
│  4. Annotate the static view with what was     │
│     learned dynamically                        │
└────────────────┬───────────────────────────────┘
                 │
                 └──── back to (2) with new questions

The key insight: every dynamic observation should feed back into the static model. Saw the value 0x52415459 at this comparison? Note it as a magic-number check in the decompiler. Saw a function called five times with the same first argument? That's a constant — rename it. Saw a string decrypted at this point? Add a comment with the plaintext.

The binary doesn't change. Your map of the binary grows after each loop. A well-annotated decompiler project, after several rounds of static + dynamic + annotation, is the real artifact of RE work — not any single insight, but a reference document that the next pass can build on without redoing the work.

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. A software breakpoint on x86 is implemented by:
    • Writing `0xCC` (INT3) over the target byte; the CPU's debug trap fires when execution reaches it
    • Setting a CPU flag that pauses execution at the address
    • Marking the target page as non-executable so the CPU faults
    • Patching the OS scheduler to stop the thread at that address
  2. On x86, hardware breakpoints are limited to four because:
    • Operating systems cap them artificially for security
    • There are only four debug registers (DR0–DR3) available to set them
    • The instruction encoding allows at most four trap targets
    • There is no limit; the question's premise is false
  3. `strace`, `ltrace`, and `bpftrace` differ from a debugger in that they:
    • Replace the binary with a logging stub during compilation
    • Log events at a boundary (syscalls, library calls, kernel tracepoints) without pausing the process
    • Run the binary in a different CPU architecture for isolation
    • Require source code to attach to function entry points
  4. A Frida script attaches to a running app and calls `Interceptor.attach(..., { onLeave(retval) { retval.replace(1); } })` on a function. What does this do?
    • Logs the function's return value but does not change it
    • Replaces the return value of every call with 1, while letting the function execute normally
    • Replaces the function's body with `return 1` and skips its original code
    • Crashes the process — `retval.replace` is not a valid API
  5. A target binary measures the time taken by a short loop using `rdtsc` to detect debuggers. Why is this an effective check, and what's the standard bypass?
    • Effective because debuggers can't read `rdtsc`; bypass is impossible without recompiling the binary
    • Effective because a debugger's single-step slows execution by orders of magnitude; bypass is to patch out the check or fake the `rdtsc` reading via DBI / debugger script
    • Effective because `rdtsc` only works inside a debugger; remove the call to bypass
    • Not effective — `rdtsc` is meaningless on virtualized hardware

Related lessons

Programming
advanced

Disassembly and decompilation

Reading machine code back into something a human can reason about. Instruction decoding (linear sweep vs recursive descent), x86-64 calling conventions, stack frames, control-flow graph recovery, what decompilers actually do and what they fundamentally cannot recover, and the practical tool landscape (IDA, Ghidra, Binary Ninja, radare2).

8 steps·~12 min
Programming
advanced

Binary formats and what the loader does

The first layer of reverse engineering — the file format on disk and the loader that maps it into memory. ELF, PE, and Mach-O as variants of the same idea; sections vs segments; symbol tables and what stripping actually removes; PLT/GOT/IAT and dynamic linking; packers like UPX; and the triage you do before opening a disassembler.

8 steps·~12 min
Programming
intermediate

Linting from first principles

What a linter actually is, what its rules enforce, how AST-based analysis works, the cost of auto-fix, the ecosystem (ESLint, Ruff, Clippy, golangci-lint), where to wire it in (LSP / pre-commit / CI), why false-positive rate is the real budget, and when a custom rule pays for itself.

8 steps·~12 min
Programming
advanced

SQLAlchemy 2.0 async ORM in production

SQLAlchemy 2.0 from the production angle. The engine/session/transaction layering, the typed declarative, the identity map, the N+1 query problem, async-only gotchas (no lazy loading), savepoint nesting, and the connection-pool knobs that decide whether the backend survives load.

8 steps·~12 min