The Toolbox

Type This First

Run this on any program from any previous chapter (or just use /bin/ls):

$ readelf -h /bin/ls
$ nm /bin/ls 2>/dev/null || echo "stripped"
$ file /bin/ls
$ size /bin/ls
$ strace -c ls /tmp 2>&1 | tail -20

Five tools, five different views of the same binary.


/proc: The Kernel Tells You Everything

Every running process has a directory under /proc/[pid]/. Not real files — the kernel generates them on demand.

/proc/[pid]/maps      Memory layout (virtual address ranges)
/proc/[pid]/smaps     Detailed per-mapping info (RSS, shared, private)
/proc/[pid]/status    Process summary (state, memory, threads)
/proc/[pid]/exe       Symlink to the actual executable
/proc/[pid]/fd/       Open file descriptors
$ sleep 1000 &
$ cat /proc/$!/maps
555555554000-555555556000 r--p 00000000 08:01 131074  /usr/bin/sleep
555555556000-555555558000 r-xp 00002000 08:01 131074  /usr/bin/sleep
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0       [stack]

Fun Fact

pmap is basically a pretty-printer for /proc/[pid]/maps.


Process Inspection: strace, ltrace, pmap

strace traces system calls — every interaction between your program and the kernel:

$ strace -e trace=write echo "hello"
write(1, "hello\n", 6)                 = 6

ltrace traces library calls (malloc, free, printf):

$ ltrace -e malloc+free ls /tmp
malloc(132)  = 0x55a1234
free(0x55a1234)

pmap shows the memory map with sizes and permissions: pmap -x <pid>.


Binary Analysis Tools

readelf -h a.out        ELF header (type, arch, entry point)
readelf -S a.out        Section headers (.text, .data, .bss...)
readelf -l a.out        Program headers (segments)
readelf -s a.out        Symbol table
readelf -d a.out        Dynamic section (.so dependencies)

objdump -d a.out        Disassembly
objdump -d -M intel a.out  Intel syntax (more readable)

nm a.out                Symbol list (T=text, D=data, B=bss, U=undefined)
size a.out              Section sizes (.text, .data, .bss)
strings a.out           Embedded string literals
file a.out              File type and architecture

Debugging: GDB Essentials

$ gcc -g -o prog prog.c && gdb ./prog
break main             Breakpoint at function
break prog.c:42        Breakpoint at line
run / run arg1         Start execution
next (n)               Step over
step (s)               Step into
continue (c)           Continue to next breakpoint
print x / print/x ptr  Print variable (decimal / hex)
bt                     Backtrace (call stack)
info registers         All register values
info proc mappings     Memory map
x/16xb 0x7fff...      Examine 16 bytes in hex
x/4xg $rsp            4 quad-words at stack pointer
watch counter          Break when variable changes

What do you think happens?

If you set a watchpoint with watch counter and two threads modify it, will GDB catch both? (Hint: hardware watchpoints are per-CPU.)


Memory Debugging

Valgrind (10-50x slowdown, very thorough):

$ valgrind --leak-check=full ./prog
==12345== Invalid read of size 4
==12345==    at 0x1091A2: main (prog.c:10)

Catches: leaks, use-after-free, buffer over-reads, uninitialized reads.

AddressSanitizer (2-3x slowdown, compile-time instrumentation):

$ gcc -g -fsanitize=address -o prog prog.c && ./prog
==12345==ERROR: AddressSanitizer: heap-buffer-overflow

Catches: buffer overflows, use-after-free, double-free.

UBSan (minimal overhead):

$ gcc -g -fsanitize=undefined -o prog prog.c && ./prog
prog.c:5: runtime error: signed integer overflow

Catches: signed overflow, null deref, misaligned access.


Rust-Specific Tools

cargo-geiger counts unsafe blocks in your code and dependencies:

$ cargo geiger
Functions  Expressions  Impls  Traits  Methods
2/5        14/60        0/0    0/0     1/3

Miri interprets your code and detects UB, even in unsafe:

$ cargo +nightly miri run
error: Undefined Behavior: dereferencing null pointer

cargo-bloat shows what makes your binary big:

$ cargo bloat --release
 5.3%  12.1%  3.2KiB  std::io::Write::write_fmt

godbolt.org — type C or Rust, see assembly instantly. Color-coded source-to-asm mapping. The single best tool for understanding compiler output.


Quick Reference: Problem to Tool

+-----------------------------------+---------------------------+
| Problem                           | Tool                      |
+-----------------------------------+---------------------------+
| "What's in this binary?"          | file, readelf -h          |
| "What sections does it have?"     | readelf -S, size          |
| "What symbols are exported?"      | nm, readelf -s            |
| "What does the assembly look like"| objdump -d, godbolt.org   |
| "What strings are embedded?"      | strings                   |
| "What syscalls does it make?"     | strace                    |
| "What libraries does it call?"    | ltrace, ldd               |
| "Where is its memory?"            | /proc/pid/maps, pmap      |
| "Why does it crash?"              | gdb, bt, info registers   |
| "Does it leak memory?"            | valgrind --leak-check     |
| "Does it overflow buffers?"       | -fsanitize=address        |
| "Does it have undefined behavior?"| -fsanitize=undefined, miri|
| "How much unsafe in my Rust?"     | cargo-geiger              |
| "Why is my Rust binary big?"      | cargo-bloat               |
+-----------------------------------+---------------------------+

Task

  1. Pick any program you compiled in a previous chapter.
  2. Run it through at least THREE tools from this chapter.
  3. For each tool, write down one thing you learned that you didn't know before.
  4. Bonus: Compile a buggy C program from Chapter 25 with -fsanitize=address. Does it catch the bug?
  5. Bonus: Run strace on ls, then on ls | cat. Does the write count change? Why?