Overview
- This article introduces GNU, GDB, ELF, and LLDB, showing how to compile and debug C programs, inspect memory, and handle crashes.
- Even if you don’t use C, learning these concepts is valuable: it helps you understand memory layout, call stacks, and debugging techniques that apply to many programming languages.
What is GNU?
- A free operating system often used with the Linux kernel (GNU/Linux). GNU provides compilers, libraries, shells, utilities and other needed software to run a computer. While Linux manages hardware, memory, and processes.
What is GDB ?
- GNU Debugger, A command-line tool used to debug programs by inspecting them as they run, analyze and modify their state, add breakpoints, and more.
-
If a language/compiler outputs a Linux executable in ELF format and you include debug symbols, GDB can debug it.
- Executable and Linkable Format (ELF) is the standard binary file format on Linux. It defines how compiled code and data are stored so the OS can load and run programs.
- Object file: Partially compiled code with unresolved references. Containing:
- Machine code for one source file: Your code compiled to CPU instructions – shown as mnemonics for humans, but the computer actually uses only 0’s and 1’s.
- Symbol Definitions: (functions/variables it provides) "hey linker, I'm declaring these variables"
- Symbol References: (functions/variables it needs but doesn't have) "hey linker, I need these variables"
-
Relocation Info: (places that need addresses fixed later by the linker)
void foo() { ... } void bar() { foo(); } // relocation info says: "Hey linker, set the // correct address for foo()"
-
Shared library: A compiled library (
.sofile) containing reusable code that programs can load at runtime, like OS functions (file system, math, etc...) - Executable: A fully linked program in ELF format that combines all object files and libraries, with all addresses resolved, ready for the OS to run.
- The Link process: The step where the linker takes the object files and libraries, resolves symbol references, fixes addresses (relocation), and produces the final executable or shared library.
1. Compile your code with the necessary flag
gcc test.c -g -O0 -o test-
-g: Generate source-level debug information. -
-O0: Disable optimizations. - This generates an object file (
.ofile), not a final executable. It's not yet linked with necessary libraries or other object files to form a complete runnable program.
2. Run your executable with GDB
-
gdb ./test: Start gdb with your compiled object file
3. Add and remove breakpoints
-
Adding breakpoint:
-
break line number: break 30 -
break func name: break main
-
-
Clearing breakpoint:
-
clear line number: clear 30 -
clear func name: clear main
-
4. More basic commands (first run gdb to start )
-
help: list of gdb commands. -
run: Run your program -
watch variable_name: Stop execution when variable changes. -
print variable_name: Print the value of the variable. -
set variable x = 42: Change variable value. -
step: Enters the next function and stops at first line inside the function. -
next: Run next line, if a function, it runs the entire function but do not enter on it, stops at the first line after the function. -
bt: show the callstack (backtrace). -
list 1: show code lines around line 1. -
quit: to exit.
If you are using MacOS, use LLDB instead of GDB;
LLDB is similar to GDB, a debugger, but designed for the LLVM compiler toolchain.
-
Commands:
-
help: list lldb commands -
lldb ./executable: start debugging your executable with lldb -
run: run your program inside lldb
-
-
Breakpoint management:
-
b 4: add breakpoint at line 4 -
b main.c:30: add breakpoint at line 30 -
b main: add breakpoint at function main -
br l: list all breakpoints and its ids -
br del id: delete breakpoint 1 -
br del: delete all breakpoints -
watchpoint set variable var_name: stop when variable changes
-
-
Frame: A level in the call stack. Level 0 is the top of the stack, showing the current function, variables, and more. (
frshortcut)-
frame info: detailed information about current frame. (fr infoworks too) -
fr var: show all local variables -
fr var var_name: show specific variable-
frame vshortcut
-
-
fr select 1: change the current frame for debugging variables and more. (frame s 1shortcut)
-
-
expression var_name: evaluate expression and print output (
e somethingshortcut)-
e *myPointer-> dereference myPointer and show value -
e x + y-> prints x+y -
e foo(10)-> run and print output of foo(10) -
e x = 42-> change variable value -
step: step into next line (enter functions)sshortcut -
next: step over next linenshortcut -
finish: run until function returns -
continueorc: resume execution, run until next breakpoint, watchpoint or the program ends -
bt: show the callstack (backtrace) with all its frames (0 is the top), others are the callers.
-
-
List: it will show 10 lines, you can press enter to see more 10 lines.
-
list: show code lines around current line. -
list 1: show code lines around line 1. -
list main: show code lines around function main. -
quit: exit lldb
-
-
Short Tips:
- Use
e *myPtrto see dereferenced pointer values - Use
fr s 0to do a frame select 0 and see current and around lines you've stopped if needed. - Use
br delto delete all breakpoints -
If you get a segfault, use
btto see the stack frames. - Then select the frame before the segfault with
fr s 1, and usefr vto inspect variablese expresionto run expressions.- You can continue debugging from there.
- It’s like traveling back in time: you can evaluate expressions, check variables, and continue debugging normally.
- Note: breakpoints set after the crash will not work, since the segfault has already occurred.
- If you see something like
(char *) $7 = 0x0000000000000000this is a null pointer.- Notice something cool: we have 16 zeros. Since it is hexadecimal, each digit represents 16 values (2^4), which means each digit is 4 bits.
- 16 digits * 4 bits = 64 bits, which matches the architecture of my computer (
arm64).
- Use
For unix terminal users: Create a shortcut like this one in .zshrc
# you can run: gcc-debug ./filename.c
gcc-debug() {
set -x # show commands tracing
gcc "$1" -g -O0 -o test && lldb ./test
set +x # turn off tracing
}
Use this simple code to practice the commands above:
#include <stdlib.h>
int main() {
int i = 10;
int j = 5;
int* p = NULL;
*p = 10; // segfault, null pointer dereference
return 0;
}
Top comments (0)