Reverse engineering / guide
Debugging C++ call chains, objects, and registers with GDB
Inspect C++ member calls, object state, thread boundaries, and architecture-dependent registers with GDB.
Source-level operations can pass through wrappers, adapters, virtual dispatch, or a queue. Establish the actual call and thread relationship before inferring where time was spent. This guide uses the generic Dispatcher and Worker classes in the event correlation laboratory.
The commands below target a Linux debug build with DWARF information and -O0. Register-specific discussion is limited to Linux x86-64 with the System V AMD64 ABI. The initial source-level session also runs on Linux aarch64.
1. Stop on an actual member function#
Build the lab's event_pipeline executable and create its results directory, then launch:
gdb --args ./build/event_pipeline direct baseline 3 gdb-run results/gdb.csv
Use this complete session in GDB:
set pagination off
break Worker::process
run
bt
info threads
info args
print seq
print this->completed
up
info args
down
finish
print worker_.completed
disable breakpoints
continue
At the first stop, expect Worker::process, Dispatcher::dispatch, and main. seq is 1 and this->completed is 0. After finish, the dispatcher can inspect its worker's counter as 1. These are deliberate properties of this small program, rather than assumptions about an arbitrary application's objects.
This direct call uses one thread. In the lab's queued mode, the source runs on the producer thread and the member call chain runs on the consumer thread. The producer's stack is not a caller frame underneath the consumer's stack. Connect those two sides using the propagated sequence number. A different TID in unmatched observations does not prove an asynchronous handoff.
2. Choose the right stepping boundary#
step enters a source-level call; next advances over one. finish runs until the selected frame returns. stepi follows one machine instruction, including into a call; nexti normally steps over a call. Signals, other breakpoints, and optimized code can interrupt those operations.
To examine the actual instructions, use disassemble /r 'Dispatcher::dispatch(unsigned int)', x/10i $pc, and info symbol ADDRESS with a real address from the target. Exact C++ signatures depend on the compiled symbol; info functions Dispatcher helps discover them. Outside GDB, nm -C build/event_pipeline displays readable C++ names and nm build/event_pipeline shows linker names. Do not assume a demangled display label is a valid raw uprobe symbol.
Inlining and tail-call optimization can remove expected frames or change their presentation. Reproduce with matching symbols and inspect disassembly before diagnosing a missing frame as a corrupted stack.
3. Interpret registers in the selected frame#
Start with frame 0, info frame, and info registers. The $pc and $sp aliases avoid assuming x86 register names. On x86-64, info registers rdi rsi rsp rbp rip shows relevant registers, but their meaning depends on the current instruction and ABI.
A non-static member function has an implicit this argument. In this lab's simple Worker::process(unsigned) entry under the System V AMD64 ABI, the expected incoming values are the object pointer in RDI and seq in ESI. This explanation is not a rule for all C++ signatures: return-value conventions, argument types, wrappers, and thunks can change the layout.
A source breakpoint may stop after the prologue has already reused a register. To test entry registers, first resolve the exact function entry and stop there, as in the first GDB laboratory. Do not interpret an arbitrary RSI value halfway through a method as a valid pointer. Do not read memory through it until its type and mapping are established. Registers in outer frames can be reconstructed by unwinding rather than representing their live hardware contents.
4. Test a suspected overwrite#
At a valid Worker::process stop, GDB understands this and the object's members. You can inspect print *this and save the counter address with set $counter = &this->completed. A separate investigation can use watch -location *$counter and continue to the write. Keep the object alive and delete the watchpoint when its lifetime ends.
A watchpoint can establish which instruction changes a watched value. It has hardware size/count limits and may fall back to expensive software stepping. It does not retrospectively identify an earlier overwrite. This workload does not intentionally corrupt memory; its expected increment is not a vulnerability demonstration.
For a real fault, preserve the exact binary and inspect bt full, info frame, info proc mappings, and print $_siginfo when signal information is available. Compare the fault address and access with actual mappings. A failed unwind, unusual register, canary failure, and stack exhaustion are different observations. A sanitizer report or a watchpoint catching the relevant write provides stronger evidence than an unfamiliar-looking backtrace alone.
5. Attach and remote debugging are separate measurements#
With an already running process, use its exact executable and gdb ./build/event_pipeline -p "$LAB_PID". info threads maps debugger numbers to target thread IDs; thread apply all bt 15 gathers bounded stacks. Normal attach stops the target. Use detach to resume it, and keep debugger-affected timestamps separate from a baseline.
For a remote target, preserve executable/debug identity, target architecture support, and source-path mapping. The runtime latency guide provides an SSH-stdio gdbserver example. set substitute-path changes source lookup; it cannot repair a mismatched executable. IDE frontends have the same identity and mapping requirements. A breakpoint resolving to the intended function and source line is the check, rather than a successful connection alone.
Sources#
- GDB C++ expressions documents member access and
this. - GDB backtraces, machine-code inspection, and watchpoints explain the observation boundaries.
- System V x86-64 psABI defines argument classification for that target.
- GDB attach/detach documents process stopping and release.