Step 11 · Ubuntu · Independent
Compiler options that find bugs
In step 3 you compiled with
clang -g hello.c -o hello. Adding a few more options turns the
compiler from something that merely translates your code into something that
actively hunts for mistakes in it.
Warnings are just options you pass — nothing to install. The sanitizer is different: Ubuntu keeps its runtime library in a separate package, and without it clang compiles your program and then fails to link it. This step installs that package.
Run this
$ curl -fsSL https://aikaryashala.com/system_setup/scripts/install_sanitizers.sh | bash
It writes overflow.c into ~/c-samples — the program
used below — and checks both warnings and the sanitizer actually work on your
machine.
Let the compiler warn you
Most C bugs are things the compiler already had doubts about. It stays quiet unless you ask. This program uses a variable it never gave a value to:
#include <stdio.h>
int main(void)
{
int counted; /* never given a value */
printf("%d\n", counted);
return 0;
}
Compiled the step 3 way, clang says nothing at all:
$ clang -g warn.c -o warn
Ask for warnings and it tells you exactly what is wrong, and where:
$ clang -g -Wall -Wextra warn.c -o warn
warn.c:6:20: warning: variable 'counted' is uninitialized when used here [-Wuninitialized]
6 | printf("%d\n", counted);
| ^~~~~~~
| Option | What it does |
|---|---|
-Wall | Turn on the common warnings. Despite the name, this is not all of them. |
-Wextra | Turn on a further set that catches subtler mistakes. |
-Werror | Treat every warning as an error, so the program refuses to build until it is clean. |
clang -g -Wall -Wextra yourfile.c -o yourprogram. It costs
nothing, and the compiler will catch mistakes that would otherwise take you
an afternoon to find by hand.
A bug warnings cannot catch
Some mistakes only exist while the program is running. This one writes to the fifth slot of a four-slot array:
#include <stdio.h>
int main(void)
{
int numbers[4] = {1, 2, 3, 4};
numbers[4] = 99; /* one past the end of the array */
printf("%d\n", numbers[0]);
return 0;
}
Build and run it the ordinary way:
$ cd ~/c-samples
$ clang -g overflow.c -o overflow
$ ./overflow
1
It printed 1 and exited cleanly. No crash, no complaint — and
that is precisely the danger. The program wrote 99 into memory that did not
belong to it and carried on as if nothing happened. On another day, with
slightly different code, that stray write lands on something that matters and
the program fails somewhere else entirely, for no visible reason.
Turn on AddressSanitizer
-fsanitize=address asks clang to build in checks around every
memory access. The program runs a little slower, and tells you the instant
something goes wrong:
$ clang -g -fsanitize=address overflow.c -o overflow
$ ./overflow
==1234==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd...
WRITE of size 4 at 0x7ffd... thread T0
#0 0x... in main overflow.c:7:16
Address ... is located in stack of thread T0 at offset 48 in frame
#0 0x... in main overflow.c:4
This frame has 1 object(s):
[32, 48) 'numbers' (line 5) <== Memory access at offset 48 overflows this variable
Read that from the top and it tells you everything:
- what — a
stack-buffer-overflow - which kind — a
WRITE of size 4, oneint - where —
overflow.c:7, the exact line - to what — the variable
numbers, declared on line 5
While you are learning, compile with
clang -g -Wall -Wextra -fsanitize=address. Between them,
warnings catch what is visible in the source and the sanitizer catches what
only appears at run time. Turn the sanitizer off when you want the program
to run at full speed.
Optimisation, and why to leave it alone for now
clang can rewrite your code to make it faster. That is what the
-O options control:
| Option | Meaning |
|---|---|
-O0 | No optimisation. The default, and what you want while debugging. |
-O1 -O2 | Progressively more rewriting, for speed. |
-O3 | More still, sometimes making the program larger. |
-Os | Optimise for a small program rather than a fast one. |
You do not need to pass -O0: clang already does no optimisation
unless asked. It is worth knowing the name, because optimised code reorders
and deletes things, so stepping through it in
lldb jumps around confusingly and some
variables simply are not there any more. Debug at -O0; optimise
when the program is correct.
Check it worked
$ cd ~/c-samples && clang -g -fsanitize=address overflow.c -o overflow && ./overflow
You should see the AddressSanitizer report above, not the number
1. If you see 1, the sanitizer did not get built
in — check the command for a typo.