Debugging C and C++ with cgdb and Vim Keybindings
cgdb is a curses-based frontend to gdb that keeps your source code visible while you debug. Unlike raw gdb, it splits the interface into two panes: source code on top, gdb command prompt below. If you’re already comfortable with Vim keybindings, cgdb feels natural—it uses the same navigation keys and modal editing paradigms you’re used to.
Why cgdb over gdb alone
Raw gdb hides your code context. You type commands and watch output scroll past, losing track of where you are in the source. cgdb solves this by maintaining a visual split: navigate the code pane with Vim keys (hjkl, /, G, gg), set breakpoints by pressing spacebar, and issue debug commands without context switching.
Function keys handle common operations—F5 to run, F8 to step over, F10 to step into. When you need arbitrary gdb commands, press i to drop into GDB mode, type what you need, then Ctrl+[ to return to code viewing. This is faster than reaching for Escape on a split keyboard.
Essential cgdb shortcuts
| Shortcut | Action |
|---|---|
| F5 | Run program |
| F6 | Continue execution |
| F7 | Finish current function (step out) |
| F8 | Next (step over) |
| F10 | Step (step into) |
| Spacebar | Toggle breakpoint at current line |
t |
Set temporary breakpoint at current line |
d |
Delete breakpoint at current line |
i |
Enter GDB command mode |
I |
Enter TTY mode (interact with program stdin/stdout) |
Ctrl+[ |
Return to code viewer mode |
/ |
Search in current file |
G |
Jump to end of file |
gg |
Jump to start of file |
The I key is particularly useful for programs that read input. It allows you to interact with the running program’s stdin while maintaining the debugger state.
Building with debug symbols
Always compile with -g. For macro expansion and better stepping accuracy, use -g3. Avoid high optimization levels during development:
gcc -g3 -Og -Wall -Wextra -o my_program main.c
-Og optimizes for debugging—it enables some optimizations without breaking stepping or variable inspection. If you use -O2 or -O3, stepping becomes unintuitive and local variables disappear.
For C++, add the same flags:
g++ -g3 -Og -Wall -Wextra -std=c++20 -o my_program main.cpp
Configuration
Create or edit ~/.cgdbrc:
set winsplit=top_big
set syntax=on
set showlinenum=on
set autosourcereload=on
set srcpath=/path/to/source
winsplit=top_big: Makes the source pane larger, giving you more room to see codesyntax=on: Enable syntax highlightingshowlinenum=on: Display line numbersautosourcereload=on: Reload source files if they change on disksrcpath: Specify source directories for out-of-tree builds
If your binary was built in one directory but sources live elsewhere, add multiple srcpath entries:
set srcpath=/home/user/project/src
set srcpath=/home/user/project/include
Custom keybindings
Map frequently used gdb operations to fewer keystrokes:
bind c F6
bind s F8
bind n F10
bind f F7
bind b spacebar
Now c continues, s steps over, n steps into, f finishes the function, and b toggles breakpoints. Avoid remapping navigation keys (hjkl, /, G, gg) as they conflict with Vim-style movement.
Workflow example
Compile with debug symbols:
gcc -g3 -Og -Wall -o my_program main.c
Start cgdb:
cgdb ./my_program arg1 arg2
Navigate to the line where you want to break and press spacebar:
spacebar
Run with F5:
F5
The program executes and stops at your breakpoint. Step through with F8 (next) or F10 (step into). To enter a function, press F10. To skip over it, press F8. To exit the current function, press F7.
For variable inspection, press i to enter GDB mode:
print my_variable
p *(my_ptr)
p my_array@10
bt
info registers
Print variables, dereference pointers, inspect array elements, print backtraces, and examine registers. Press Ctrl+[ to return to the code viewer.
Debugging multi-threaded programs
For programs with multiple threads, use GDB mode to inspect them:
info threads
thread 2
bt
Switch between threads and print their backtraces. Set thread-specific breakpoints:
break main.c:42 thread 2
This breakpoint only triggers when thread 2 reaches line 42. cgdb displays the thread ID when a breakpoint is hit.
For race conditions, you can temporarily disable other threads:
thread apply all step
Step all threads together, or:
thread apply 1 continue
Continue only thread 1 while others remain suspended.
Debugging optimized binaries
High optimization levels (-O2, -O3) break cgdb’s ability to map instructions back to source. Local variables vanish, stepping jumps unexpectedly, and the source view becomes misleading.
If you must debug optimized code, use print-based debugging or conditional breakpoints in GDB mode:
break main.c:42 if condition
commands
print my_var
continue
end
This breakpoint prints a variable every time it’s hit before automatically continuing—useful for tracing program state in optimized builds.
Installation
On most Linux distributions:
# Debian/Ubuntu
apt install cgdb
# Fedora/RHEL
dnf install cgdb
# Arch
pacman -S cgdb
# macOS
brew install cgdb
cgdb requires readline and ncurses development libraries. On older systems, you may need to build from source:
git clone https://github.com/cgdb/cgdb.git
cd cgdb
./autogen.sh
./configure
make
sudo make install
Alternatives and modern approaches
For pure source-level debugging without launching a separate tool, Neovim’s DAP integration (Debug Adapter Protocol) has matured significantly. Plugins like nvim-dap and nvim-dap-ui let you debug within your editor:
- gdb-dashboard: Python-based TUI that runs inside gdb itself, no separate binary needed
- LLDB: Modern C/C++ debugger with DAP support via
lldb-mi - rr (record and replay): Records program execution and lets you step backward through code—powerful for reproducing intermittent bugs
If you’re already in Neovim and want an integrated experience, DAP is worth exploring. For quick debugging sessions from the terminal or when jumping between projects, cgdb’s lightweight startup and Vim-like navigation remain faster than configuring editor plugins.
Common gotchas
Breakpoints not hit: Ensure the binary has debug symbols (readelf -S my_program | grep debug should show .debug_* sections). Recompile with -g.
Variables unavailable: You’re likely debugging optimized code. Recompile with -Og instead of -O2 or -O3.
Source not found: Use set srcpath in ~/.cgdbrc to tell cgdb where your sources live, or use directory /path/to/source in GDB mode.
Program doesn’t stop at breakpoint: Check that the breakpoint was set (highlighted in red in the source pane). Use info breakpoints in GDB mode to verify.
