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).
- The error line,
==PID==ERROR: AddressSanitizer: <kind>, names the kind of bug. - The access:
READorWRITE of size Nand frame#0, the line that did it. If#0is in a library, read down to the first frame in your own file. - 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. - 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).
- The two accesses. One is always a write. Frame
#0under each gives the line. - 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.
- The threads. Each
Tnumber is followed by where it was created. - 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 reported4,992 bytes allocatedthrough 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 |