MiniDbg is a minimalist Linux x86-64 debugger written in modern C++17 using the ptrace system call interface. It provides instruction-level stepping, software breakpoints via INT 3 (0xCC), general-purpose register inspection and modification, arbitrary process memory reading and writing, zero-dependency ELF64 symbol table parsing, and frame-pointer-based stack backtracing.
MiniDbg is an educational, systems-focused debugger designed to illustrate the fundamentals of low-level process control, x86-64 CPU architecture, Linux system programming, and ELF binary structures.
It avoids external framework abstractions, implementing the core mechanics of debugging directly against Linux kernel APIs and CPU architecture specifications.
Debuggers like GDB and LLDB are large codebases comprising hundreds of thousands of lines of code. Consequently, the actual primitives of debugging—how breakpoints alter machine instructions in memory, why the instruction pointer register must be rewound after INT 3, and how stack frames are traversed—are often obscured behind layers of abstraction.
I built MiniDbg to:
- Deeply understand how a parent process traces and controls a child process at the operating system boundary.
- Implement software breakpoints directly by manipulating raw instruction bytes.
- Understand the low-level lifecycle of registers, process virtual memory, and the x86-64 ABI.
- Parse ELF64 headers and symbol tables directly without external libraries.
- Prepare for System Software, Compiler, and Debugger engineering interviews by building a project where every line of code can be explained and defended from first principles.
- Process Tracing: Spawns and attaches to target binaries using
fork(),ptrace(PTRACE_TRACEME), andexecvp(). - Execution Control: Full support for
continue(PTRACE_CONT) and single-step machine instruction execution (PTRACE_SINGLESTEP). - Software Breakpoints: Breakpoints via
0xCC(INT 3) insertion, automaticRIPrewinding, and temporary single-step restoration (step-over logic). - Register Inspection & Modification: Reads and writes all x86-64 general-purpose registers (
rax,rbx,rcx,rdx,rsi,rdi,rbp,rsp,rip,r8–r15,eflags) viaPTRACE_GETREGSandPTRACE_SETREGS. - Memory Inspection & Modification: Reads and writes 64-bit words in the target process virtual address space via
PTRACE_PEEKDATAandPTRACE_POKEDATA. - ELF64 Symbol Resolution: Direct parser for
.symtab,.strtab, and section headers, enabling symbolic breakpoints (e.g.break main) and symbol backtrace resolution. - Frame-Pointer Backtracing: Call stack unwinding along the
RBPframe-pointer chain for binaries compiled with-fno-omit-frame-pointer. - Interactive REPL: Lightweight, intuitive command-line interface.
Debugger Process
|
ptrace
|
v
Target Process
|
+--> Registers (PTRACE_GETREGS / PTRACE_SETREGS)
+--> Memory (PTRACE_PEEKDATA / PTRACE_POKEDATA)
+--> Execution Control (PTRACE_CONT / PTRACE_SINGLESTEP)
+--> SIGTRAP / Breakpoints (INT 3 0xCC)
MiniDbg/
├── CMakeLists.txt # CMake build configuration
├── Makefile # Standard Unix Makefile
├── README.md # Technical documentation
├── docs/
│ ├── architecture.md # In-depth architectural & hardware explanations
│ ├── ptrace_notes.md # Linux ptrace syscall reference & states
│ ├── elf_notes.md # ELF64 binary format, symbols, ASLR & PIE
│ └── interview_questions.md # 20 technical interview Q&A pairs
├── include/
│ ├── Platform.h # Linux x86-64 ABI definitions & fallbacks
│ ├── RegisterUtils.h # Register mappings and table formatting
│ ├── MemoryUtils.h # Process memory peek/poke utilities
│ ├── Breakpoint.h # Software breakpoint byte management
│ ├── ElfParser.h # ELF64 header, section, and symbol parser
│ └── Debugger.h # Debugger engine and REPL controller
├── src/
│ ├── RegisterUtils.cpp
│ ├── MemoryUtils.cpp
│ ├── Breakpoint.cpp
│ ├── ElfParser.cpp
│ ├── Debugger.cpp
│ └── main.cpp # CLI entry point
└── tests/
├── sample_programs/ # Sample C++ test targets
│ ├── 01_simple.cpp
│ ├── 02_loop.cpp
│ └── 03_backtrace.cpp
├── test_registers.cpp
├── test_memory.cpp
├── test_breakpoints.cpp
├── test_elf.cpp
├── test_debugger_unit.cpp
└── run_all_tests.py # Automated test runner
-
Spawning the Target: The debugger calls
fork(). In the child process:ptrace(PTRACE_TRACEME, 0, nullptr, nullptr); execvp(program, args);
The kernel flags the process for tracing. When
execvereplaces the image, the kernel halts the child before its first user instruction and raisesSIGTRAP. -
Parent Synchronization: The parent debugger calls
waitpid(pid, &status, 0). Once the child halts onSIGTRAP, the debugger can set breakpoints or inspect registers.
- Linux x86-64
- GCC or Clang with C++17 support
- Make or CMake >= 3.14
cd MiniDbg
makecd MiniDbg
mkdir build && cd build
cmake ..
makeLaunch MiniDbg with the target executable:
./minidbg ./tests/sample_programs/01_simple| Command | Shorthand | Description |
|---|---|---|
run / continue |
c |
Continue execution of the target process |
step |
s |
Single-step one machine instruction |
break <addr|sym> |
b |
Set a breakpoint at address or symbol name |
delete <id|addr> |
Delete a breakpoint by ID or address | |
info breakpoints |
List all active breakpoints | |
registers |
Display all general-purpose registers | |
reg <name> |
Display value of a specific register (e.g. reg rip) |
|
setreg <name> <val> |
Modify a register value | |
x <addr> [count] |
Examine memory words at address | |
setmem <addr> <val> |
Write 64-bit word to memory | |
symbols |
Dump parsed ELF symbol table | |
backtrace |
bt |
Print frame-pointer stack backtrace |
help |
Show command reference | |
quit |
q |
Exit the debugger |
Software breakpoints work by modifying executable code in memory:
Normal instruction
↓
Replace first byte with INT3 (0xCC)
↓
Target executes INT3
↓
SIGTRAP
↓
Debugger restores original byte
↓
RIP = RIP - 1
↓
Single-step original instruction
↓
Reinsert INT3
- On x86-64, executing
0xCCadvancesRIPby 1 byte before raisingSIGTRAP. - When the debugger halts, it inspects
RIP - 1. If an active breakpoint exists at that address, the debugger decrementsRIPby 1 usingPTRACE_SETREGS. - To resume past the breakpoint:
- Restore the original byte via
PTRACE_POKEDATA. - Single-step the original instruction via
PTRACE_SINGLESTEP. - Reinsert
0xCCviaPTRACE_POKEDATA. - Continue execution via
PTRACE_CONT.
- Restore the original byte via
MiniDbg reads the target ELF binary directly from disk:
- Validates the ELF magic number (
\x7fELF) inElf64_Ehdr. - Locates the Section Header Table at
e_shoff. - Finds
.symtab(symbol entries) and.strtab(string table). - Iterates through all
Elf64_Symstructs, extracting function addresses (st_value), sizes (st_size), and names from the string table offset (st_name). - Supports resolving symbol names to addresses for setting breakpoints (
break main), and reverse-resolving addresses to symbol names for backtraces.
- Registers: Extracted via
PTRACE_GETREGSintouser_regs_struct. Registers can be read or modified individually. - Memory: Read using
PTRACE_PEEKDATAand written usingPTRACE_POKEDATA. Even though the target.textsegment is mapped read-only in user space, the Linux kernel overrides write protections for ptrace calls, enabling breakpoint injection.
Backtracing walks the linked list of stack frames along %rbp:
- Current frame: Instruction pointer in
%rip. - Stack walk:
*(uint64_t*)rbpyields the caller's saved%rbp.*(uint64_t*)(rbp + 8)yields the caller's return address.
- Each return address is resolved to its enclosing function symbol via
ElfParser.
Binaries must be compiled with
-fno-omit-frame-pointerfor this frame chain to be maintained.
Run the automated test suite:
python3 tests/run_all_tests.pyThis executes all unit tests:
test_registers: Verifies register name mapping, accessors, and formatting.test_memory: Verifies word-aligned memory reading, writing, and byte extraction.test_breakpoints: Verifies byte preservation, substitution with0xCC, and restoration.test_elf: Verifies synthetic ELF64 parsing, symbol lookups, and reverse address resolution.test_debugger_unit: Verifies breakpoint table deduplication, ID management, and lifecycle.
- Non-PIE binaries: Test binaries should be compiled with
-no-pie. For Position Independent Executables with ASLR enabled, runtime base addresses must be extracted from/proc/<pid>/maps. - Frame pointers required: Backtracing relies on
%rbpframe chains (-fno-omit-frame-pointer). It does not parse DWARF CFI (.eh_frame). - No DWARF line tables: Stepping operates at the instruction level, not source-line level.
- Single-threaded only: Does not handle multithreaded clone/thread tracing events.
- ASLR & PIE Base Address Resolution: Parse
/proc/<pid>/mapsdynamically on attach to calculate relocations. - DWARF Line Number Parsing: Parse
.debug_lineto support source-level stepping (next/step). - Hardware Breakpoints: Utilize x86-64 debug registers (
DR0–DR3,DR7) for data watchpoints without modifying memory bytes.