Appendix D. Sanitizer, Valgrind, and gdb Card

One page of everything the instruments in this book need: the build lines, how to read the first lines of each report, and the debugger commands. Setting Up Your Workbench installs the tools; Chapters 1, 3, 4, 6, 16 and 20 teach them.

D.1 Build lines

Listing D.1. The book's standard build lines.

# C (Chapter 1 onward)
gcc -std=c17 -Wall -Wextra -Werror -g \
    -fsanitize=address,undefined -o prog prog.c

# C++ (Chapter 12 onward)
g++ -std=c++20 -Wall -Wextra -Werror -g \
    -fsanitize=address,undefined -o prog prog.cpp

Listing D.2. The variations.

# Valgrind: no sanitizers (the two do not mix), -O0
gcc -std=c17 -Wall -Wextra -g -O0 -o prog prog.c
g++ -std=c++20 -Wall -Wextra -g -O0 -o prog prog.cpp

# ThreadSanitizer (Chapter 20): thread replaces
# address,undefined; thread and address cannot combine
g++ -std=c++20 -Wall -Wextra -Werror -g \
    -fsanitize=thread -o prog prog.cpp

# Debug containers (Chapter 16): checks library rules
g++ -std=c++20 -Wall -Wextra -Werror -g \
    -fsanitize=address,undefined \
    -D_GLIBCXX_DEBUG -o prog prog.cpp

# Vector slots past size() poisoned: ASan builds only
g++ -std=c++20 -Wall -Wextra -Werror -g \
    -fsanitize=address,undefined \
    -D_GLIBCXX_SANITIZE_VECTOR -o prog prog.cpp

# Timing (Chapters 5, 10, 16 to 18): after the sanitizer
# build has shown the code correct
g++ -std=c++20 -O2 -DNDEBUG -o study study.cpp

Table D.1. What each flag does.

Flag Effect Chapter
-std=c17, -std=c++20 Language version 1, 12
-Wall -Wextra A wide set of warnings 1
-Werror Warnings stop the build; drop it only when a listing shows a warning, and say so 1
-g Debug information: file names and line numbers in reports and gdb 1
-fsanitize=address,undefined ASan (with LeakSanitizer) and UBSan at run time 1, 6
-O0 No optimization; variables and lines stay where the source puts them 6
-O2 Optimized; for timing, and to see what UB does under the optimizer 1, 5
-DNDEBUG Removes assert; the build that ships 11, 18
-fsanitize=thread ThreadSanitizer 20
-D_GLIBCXX_DEBUG Checked iterators and containers in libstdc++ 16
-D_GLIBCXX_SANITIZE_VECTOR Lets ASan see slots between size() and capacity() 16
-MMD -MP The compiler writes header dependencies for make 7
-Wnon-virtual-dtor Warns about a polymorphic base without a virtual destructor 15
-Wpadded Warns wherever a struct is padded 7

Three run-time habits go with the lines. Put timeout 3 (or timeout 10) in front of any test that could loop forever: a hang becomes exit status 124 (Sections 5.3.4 and 20.5). Run the sanitizer build before the timing build, never instead of it. And when an instrument reports nothing, ask which paths ran: every runtime instrument is silent about code that did not run (Table 6.2).

D.2 Reading an AddressSanitizer report

ASan stops at the first error. Read four parts, in this order (Section 6.1.8).

  1. The error line, ==PID==ERROR: AddressSanitizer: <kind>, names the kind of bug.
  2. The access: READ or WRITE of size N and frame #0, the line that did it. If #0 is in a library, read down to the first frame in your own file.
  3. Where the address is: "located K bytes inside of (or after) an R-byte region". Divide by the element size to get an index: 472 bytes inside a 480-byte region of doubles is element 59 of 60.
  4. The history: "freed by thread T0 here" and "previously allocated by thread T0 here", each with its stack. Allocated at one line, freed at another, used at a third.

The shadow-byte map at the end shows which 8-byte groups may be touched (fd freed heap, fa heap redzone). For a stack overflow of an array, the frame description names the variable and its byte range: [32, 56) 'temps' <== Memory access at offset 56 overflows this variable.

Table D.2. ASan error kinds met in this book.

Kind Usually means Where seen
stack-buffer-overflow Read or write past a local array Setup; 2.2.2
heap-buffer-overflow Past the end of a malloc or new block 7.3.1, 11.4.2
heap-use-after-free Access through a pointer to a freed block 6.1.8, 9.3.3, 18.3.2
attempting double-free A second free or delete of one block 6.1.6, 14.3.2
stack-overflow Recursion that does not reach its base case, or is too deep 3.3.2, 10.3.1, 17.1.8
SEGV on unknown address 0x000000000000 A null pointer dereferenced or called 4.3.3, 9.3.1, 12.3.2
new-delete-type-mismatch Deleted through a base without a virtual destructor 15.3.1
alloc-dealloc-mismatch delete on memory from new[], or similar 12.1.6, 14.5
container-overflow A vector slot past size() (needs -D_GLIBCXX_SANITIZE_VECTOR) 12.1.5, 16.2
LeakSanitizer: detected memory leaks Blocks never freed, reported at exit 6.1.8, 10.3.2

LeakSanitizer runs when the program exits. A direct leak has no pointer to it anywhere; an indirect leak is reachable only from another leaked block, such as the nodes of a lost list. Fix direct leaks first. LeakSanitizer does not run under gdb, and when output is piped to a file the program's last lines may be missing, because LSan ends the process before buffered output is flushed (Section 6.1.8).

Never silence a report to make a run pass. Two ASAN_OPTIONS settings appear in the book, each for a stated reason: detect_leaks=0 when running a sanitizer build under gdb (Section 3.2), and alloc_dealloc_mismatch=0 for one step of the Chapter 14 lab, so that the later tests can run after the first defect is recorded. Section 15.3.1 shows an agent recommending new_delete_type_mismatch=0; the report was real.

D.3 Reading a UBSan report

UBSan prints one line per event and lets the program continue: file.c:LINE:COL: runtime error: <what>. The line and column point at the operation.

Table D.3. UBSan messages met in this book.

Message begins Meaning Section
index 3 out of bounds for type 'double [3]' Array index past the end, where the size is known Setup
signed integer overflow Signed arithmetic outside the type's range 1.3.1, 5.5
shift exponent 32 is too large for 32-bit type 'int' Shift count at or past the width 9.3.2
load of null pointer of type Read through NULL 4.3.3
reference binding to null pointer A C++ reference made from a null pointer 12.4, 13.4
division by zero Integer division by zero 2 (Instructor notes)

Two silences to remember. gcc's -fsanitize=undefined does not enable the floating-point division check, so 0.0 / 0 quietly gives NaN (Section 2.2.2). And unsigned wraparound is defined behavior, so UBSan says nothing when w * h wraps to 0 (Section 13.4).

D.4 Reading a ThreadSanitizer report

TSan reports a data race: two threads touching one location, at least one writing, with no lock, join or atomic ordering them (Serebryany and Iskhodzhanov 2009). It sees only the orderings of the run you give it, so run it several times under load. Read four passes, every time (Section 20.1.9).

  1. The two accesses. One is always a write. Frame #0 under each gives the line.
  2. The location. A global by name; for a member of an object on the stack, "stack of main thread", and the source line tells you which member.
  3. The threads. Each T number is followed by where it was created.
  4. The locks. An access line ending "(mutexes: write M0)" held a lock. A locked access against an unlocked one is the signature of a forgotten lock.

A program can print the right answer and still be reported; TSan checks whether an ordering exists, not whether the result is wrong. The exit status after a report is 66, so a test script can fail on any warning. TSan says nothing about a race condition without a data race, a lost wake-up, or a lock held across slow I/O (Sections 20.3.2 and 20.3.3); those need review, forced-timing tests and measurement.

D.5 Reading Valgrind

Valgrind runs your unchanged machine code on a simulated processor and watches every allocation and access (Nethercote and Seward 2007). Build without sanitizers at -O0 (Listing D.2). Unlike ASan, it lets the program continue after an error, so a bad value may print.

Listing D.3. The Valgrind commands used in the book.

valgrind ./prog                      # errors only
valgrind --leak-check=full ./prog    # plus leak stacks
valgrind --track-origins=yes ./prog  # where uninitialized
                                     # bytes came from
valgrind -q --leak-check=full --error-exitcode=1 ./prog
                                     # for make targets

The messages you will read most: Invalid read of size N and Invalid write of size N (with the block's history below), Invalid free(), Conditional jump or move depends on uninitialised value(s), and Syscall param write(buf) points to uninitialised byte(s) (Section 7.3.3). Read ERROR SUMMARY last; "All heap blocks were freed -- no leaks are possible" is about leaks only.

Table D.4. Valgrind's leak categories (Section 6.1.8).

Category What remains at exit What to do
definitely lost No pointer to the block A real leak: find the allocation stack with --leak-check=full
indirectly lost Reachable only through a definitely lost block Usually fixed by fixing the definitely lost block
possibly lost Only a pointer into the middle of the block Treat as a leak until you can explain the pointer
still reachable A pointer remains, but the block was never freed Free it, or write down why not

Two warning signs. total heap usage: 0 allocs means the wrong binary ran: Valgrind on an ASan build prints ASan runtime does not come first in initial library list and then calls the program clean (Section 6.1.8). And "no leaks are possible" on code that leaks means the leaking path did not run; add the input or the failure sweep that reaches it (Section 6.6).

Check before you trust. Valgrind's byte totals depend on where standard output goes. The C library allocates one buffer for stdout: in our runs 4,096 bytes when output went to a pipe, 1,024 bytes when it went to a terminal. The same program reported 4,992 bytes allocated through a pipe and 1,920 on a terminal (Section 11.4.2). When you predict a total, or compare yours with the book's, say which, and count that buffer as one of the allocations.

D.6 Debug containers

-D_GLIBCXX_DEBUG replaces the standard containers with checked versions that know the library's rules. A use of an invalidated iterator aborts with a message such as attempt to increment a singular iterator, often several steps before ASan could see anything, because the stale step may touch only live memory. -D_GLIBCXX_SANITIZE_VECTOR, added to an ASan build, poisons a vector's slots between size() and capacity(), so a read of a slot that was erased but not yet freed becomes a container-overflow report. Section 16.2 runs the same bug three ways: the plain ASan build printed a wrong answer with no report, the sanitize-vector build reported the container overflow, and the debug build stopped at the bad increment. Run tests on all three builds when a container changes inside a loop.

D.7 gdb

Start gdb on a build with -g. For a sanitizer build, set ASAN_OPTIONS=detect_leaks=0 first, since LeakSanitizer does not work under gdb. gdb shows what is in memory, never why: compare every observation with your own drawing.

Table D.5. gdb commands used in this book.

Command What it does Ch
gdb -q ./prog Start gdb without the banner 3
gdb -q -batch -ex run -ex bt --args ./prog a b Run with arguments, print a backtrace, exit 10, 16
break main, break 16, break Sensor::unit Stop at a function, a line, a member function 3, 15
run, run 3 Start the program, with arguments if given 3
next Run one line, stepping over calls 3
step Run one line, stepping into a call 3
continue Run to the next breakpoint or the end 3
bt, bt 3, bt -2 All frames newest first; the newest 3; the oldest 2 3, 10
frame k Select frame k 3
info locals Show the selected frame's locals 3
p expr (or print) Evaluate a C or C++ expression, with types 4, 6
p *p, p p - a The pointee; the element distance between two pointers 4
p *p@4 Four elements starting at *p 4
x/4dw a Four 4-byte words in decimal from a 4
x/4fg t Four 8-byte floating-point values 4
x/8cb buf Eight bytes as characters 5
ptype /o struct reading The layout, with offsets and padding holes 7
print *this The object a member function was called on 15
set print object on Print the dynamic type behind a base pointer 15
info vtbl p The virtual table of the object p points to 15

The x command is x/NFU address: count N, format F, unit U. The letters used in this book are formats d (signed decimal), x (hexadecimal), f (floating point) and c (character), and units b (1 byte), h (2), w (4) and g (8) (Table 4.3).

D.8 Which instrument for which question

Table D.6. First choice of instrument.

Question First instrument Then
Out of bounds, use after free, double free ASan (standard build) Valgrind; gdb at the line
Leak, on which path LeakSanitizer; Valgrind --leak-check=full Failure sweep for error paths
Uninitialized bytes, where from Valgrind --track-origins=yes Hand trace
Overflow, bad shift, null load UBSan Compare -O0 and -O2
Invalidated iterator -D_GLIBCXX_DEBUG -D_GLIBCXX_SANITIZE_VECTOR with ASan
Data race TSan, repeated under load A shared-variable table
Cost Operation count or counter Timing at -O2 across sizes, log-log slope
What a line did to memory gdb p and x/ Your drawing