latency.lab
Module 11 · the decider

Debugging & Undefined Behavior

Level: advancedTime: ~3 hrsPrereq: Modules 02–10

By the end you can

Why this module exists

A real Akuna new-grad candidate cleared four technical rounds and got cut on the C++ debugging round — because they normally coded in Java and reading broken C++ under time pressure is a distinct, trainable skill. This module trains it directly. Do the drills until spotting these bugs is reflexive.

The undefined behavior catalog

UB means the standard imposes no requirements — the program may crash, corrupt data, or appear to work until it doesn't. Memorize these categories; they are the raw material of every debugging round.

ub-catalog
1.  Use-after-free            using memory after delete
2.  Double free               delete the same pointer twice
3.  Dangling pointer/ref      pointer to a destroyed object (e.g. returned local)
4.  Out-of-bounds access      arr[n], vector[i] past the end
5.  Uninitialized read        reading a variable before it is set
6.  Null dereference          *p when p is nullptr
7.  Signed integer overflow   INT_MAX + 1
8.  Iterator invalidation     using an iterator after the container changed
9.  Data race                 unsynchronized shared read/write across threads
10. Returning ref to temporary  const std::string& r = makeTemp();
11. Missing virtual destructor  delete via base pointer, derived not destroyed
12. Object slicing            copying a derived into a base by value

Reading broken code: a method

When handed a file to debug, don't read top to bottom. Scan for the high-probability bug sites in this order:

  1. Every new/delete and pointer. Is each new matched exactly once? Any use after delete? Any returned local address?
  2. Every array/container index and loop bound. Off-by-one? <= where it should be <? Unsigned loop counter going below zero?
  3. Every variable declaration. Initialized before use?
  4. Every class with a destructor. Does it obey the Rule of Five? Base class destructor virtual?
  5. Every loop that erases from a container. Iterator invalidated mid-loop?
  6. Anything touching shared state across threads. Protected by a lock or atomic?

Worked examples

bug1.cpp
std::vector<int> v = {1,2,3};
for (size_t i = 0; i <= v.size(); ++i)   // BUG: <= reads v[3], out of bounds
    std::cout << v[i];
bug2.cpp
class Base { public: ~Base() {} };          // BUG: non-virtual destructor
class Derived : public Base { int* p_ = new int[100]; public: ~Derived(){ delete[] p_; } };
Base* b = new Derived();
delete b;   // only ~Base runs -> Derived's destructor skipped -> leak
bug3.cpp
int sum = 0;                // (fine)
int values[3];
for (int i = 0; i < 3; ++i) sum += values[i];  // BUG: values uninitialized

The tools

Compiler warnings first

Before any tool, recompile with -Wall -Wextra -Wpedantic. A huge fraction of bugs are already flagged there for free.

Sanitizers — your fastest bug-finders

terminal
# AddressSanitizer: catches use-after-free, out-of-bounds, double-free
g++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer bug.cpp -o bug
./bug     # prints the exact line and allocation stack on the first violation

# UndefinedBehaviorSanitizer: catches signed overflow, bad casts, more
g++ -std=c++17 -g -fsanitize=undefined bug.cpp -o bug

# ThreadSanitizer: catches data races
g++ -std=c++17 -g -fsanitize=thread race.cpp -o race
Habit that impresses interviewers

Mention that you build tests under AddressSanitizer and UBSan. It signals you've been burned by memory bugs and know the professional tooling. At a trading firm, ASan/UBSan/TSan in CI is standard practice.

GDB — stepping through

terminal
g++ -std=c++17 -g bug.cpp -o bug   # -g adds debug symbols
gdb ./bug
(gdb) break main         # set a breakpoint
(gdb) run                # start
(gdb) next               # step over one line
(gdb) step               # step into a call
(gdb) print x            # inspect a variable
(gdb) backtrace          # the call stack (great after a crash)

Valgrind — leak and memory-error detection

terminal
valgrind --leak-check=full ./bug   # reports leaks and invalid accesses with line numbers
Build

Exercise 11.1 — The debugging gauntlet

This is the most important exercise in the course for your interviews.

  • Take your own Buffer class from Module 05. Introduce each of these bugs one at a time and find it with the right tool: a leak (Valgrind), a use-after-free (ASan), a signed overflow (UBSan).
  • Write a 60–100 line program with 5 planted bugs from the catalog. Leave it a day. Come back and time yourself finding all 5. Target: under 15 minutes. Repeat weekly with new bugs.
  • Take the base/derived example above, run it under Valgrind to see the leak, then add virtual ~Base() and confirm the leak disappears.
  • Reproduce a data race, catch it with ThreadSanitizer, then fix it with a mutex and confirm TSan goes quiet.

Takeaways

  • Know the UB catalog cold — it's the raw material of every debugging round.
  • Don't read broken code linearly; scan the high-probability sites (pointers, bounds, init, Rule of Five, iterator loops, shared state).
  • Recompile with full warnings before anything else.
  • ASan/UBSan/TSan find memory bugs, UB, and races with exact locations — learn the flags.
  • GDB for stepping and backtraces; Valgrind for leaks. Practice the debugging gauntlet weekly.
Live

Loading starter code…

Checkpoint · Debugging & UB

6 questions · pass at 70%

Q01
A loop uses for (size_t i = 0; i <= v.size(); ++i) use(v[i]);. What is the bug?
WhyValid indices are 0..size()-1. Using <= accesses v[size()], which is out of bounds — undefined behavior. It should be < size().
Q02
You delete a Derived object through a Base* and only ~Base runs. The cause and fix?
WhyDeleting a derived object via a base pointer with a non-virtual base destructor is UB and skips the derived destructor (a leak here). A virtual base destructor fixes it.
Q03
Which sanitizer catches use-after-free and out-of-bounds access?
WhyAddressSanitizer instruments memory to catch use-after-free, heap/stack overflow, and double-free, reporting the exact line and allocation site.
Q04
What should you do FIRST when handed unfamiliar broken C++?
WhyFull warnings catch a large share of bugs instantly and for free, before you invest time in tools or stepping through.
Q05
Which tool catches data races specifically?
WhyThreadSanitizer detects data races by tracking memory accesses and their synchronization across threads.
Q06
Reading a variable before it has been assigned is:
WhyReading an uninitialized automatic variable is undefined behavior; the value is indeterminate. Always initialize. -Wall often warns about it.

Finished Module 11?

Pass the quiz to complete it automatically.

+ Note