Undefined behaviour
The standard says what a program must do, and it also lists things a program must not do. For the second list it promises nothing: the behaviour is undefined, so the compiler may assume it never happens and optimise accordingly. Lesson 6 showed the arithmetic case; this lesson collects the memory cases and the tool that finds them.
Signed overflow
Adding one to INT_MAX is undefined for a signed int, even though the machine would wrap. UndefinedBehaviorSanitizer adds checks around the operation and reports the exact expression, which is the fastest way to prove that a suspicious line really overflows.
#include <limits.h>
#include <stdio.h>
static int add(int a, int b)
{
return a + b;
}
int main(void)
{
int x = INT_MAX;
printf("%d\n", add(x, 1));
return 0;
}
gcc -fsanitize=undefined -g -Wall -Wextra signed.c -o signed
./signed
signed.c:6:14: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
-2147483648
The wrapped result appears after the report and is not guaranteed; the sanitizer keeps running by default. Lesson 35 shows how to make the run stop on the first error and how the flags combine.
Out of bounds
An index equal to the array size is one past the last element. A loop that uses <= therefore reads memory that is not part of the array, and the optimiser is allowed to assume the loop stops before that. GCC recognises the pattern and reports the iteration:
#include <stdio.h>
int main(void)
{
int values[3] = {1, 2, 3};
for (int i = 0; i <= 3; i++) {
printf("%d\n", values[i]);
}
return 0;
}
gcc -O2 -Wall -Wextra bounds.c -o bounds
bounds.c: In function 'main':
bounds.c:8:9: warning: iteration 3 invokes undefined behavior [-Waggressive-loop-optimizations]
8 | printf("%d\n", values[i]);
| ^~~~~~~~~~~~~~~~~~~~~~~~~
bounds.c:7:23: note: within this loop
7 | for (int i = 0; i <= 3; i++) {
| ~~^~~~
The program is not run here, because the fourth read may return a value from the next variable or crash, depending on the build. The warning is enough to find the boundary: the loop condition should be i < 3, as in lesson 22.
Reading the bits safely
A float and an int have different types, and reading one through a pointer to the other breaks the strict-aliasing rule the optimiser relies on. The standard function memcpy copies the bytes and is the portable way to look at a representation. The result is the IEEE 754 pattern of 1.0f.
#include <stdio.h>
#include <string.h>
int main(void)
{
float value = 1.0f;
int bits;
memcpy(&bits, &value, sizeof bits);
printf("%d\n", bits);
return 0;
}
gcc -Wall -Wextra bits.c -o bits
./bits
1065353216
1.0f is 0x3F800000 in IEEE 754, which is 1065353216 as a signed int. The copy is defined for any two object types of the same size, and the compiler usually turns it into a single move, so the safe version costs nothing.
Type punning and literals
The cast version of the bit reading compiles, runs, and prints the same number on this machine, which is exactly what makes undefined behaviour dangerous. With the aliasing warning enabled, GCC explains which line the optimiser may miscompile:
#include <stdio.h>
static float make_value(void)
{
return 1.0f;
}
int main(void)
{
float value = make_value();
int *bits = (int *) &value;
printf("%d\n", *bits);
return 0;
}
gcc -O2 -Wall -Wextra -Wstrict-aliasing=2 alias.c -o alias
alias.c: In function 'main':
alias.c:11:25: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
11 | int *bits = (int *) &value;
| ^~~~~~
The second mistake is a write into a string literal. The literal may be placed in read-only memory, so the store is undefined; here the process dies with a segmentation fault before printing anything:
#include <stdio.h>
int main(void)
{
char *text = "hello";
text[0] = 'H';
printf("%s\n", text);
return 0;
}
gcc -Wall -Wextra literal.c -o literal
./literal; echo "exit: $?"
exit: 139
The fix for the first is memcpy; the fix for the second is an array initialised from the literal, as lesson 23 showed: char text[] = "hello"; owns its bytes and can be changed.
Exercises
- Compile the bounds program with -O0 and -O2 and compare the warnings and the output.
- Write a program that shifts a signed int into its sign bit and run it with UBSan. Which line does the report name?
- Replace the pointer cast in the alias program with memcpy and check that the warning disappears.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the standard promise for undefined behaviour?
Answer
Nothing at all
The compiler may assume the operation never happens, so any result is possible and no diagnostic is required.
-
Why is signed integer overflow undefined while unsigned overflow is defined?
Answer
The standard defines only unsigned
Unsigned arithmetic is specified modulo one more than the maximum, while signed overflow has no defined result.
-
What is wrong with
for (int i = 0; i <= 3; i++)overint values[3]?Answer
It reads
values[3], one past the arrayThe valid indexes are 0 to 2, so the final pass reads outside the array and the behaviour is undefined.
-
What is the portable way to inspect the bits of a
float?Answer
Copy the bytes with
memcpymemcpycopies the representation without violating strict aliasing, and the compiler usually reduces it to one move. -
This program is built with
gcc -std=c17 -fsanitize=undefined -g -Wall -Wextra signed.c -o signed. Which runtime error does UBSan report?#include <limits.h> #include <stdio.h> static int add(int a, int b) { return a + b; } int main(void) { int x = INT_MAX; printf("%d\n", add(x, 1)); return 0; }Answer
signed integer overflow
Adding one to
INT_MAXoverflows a signed int, which UBSan names directly. -
What does this program print?
#include <stdio.h> #include <string.h> int main(void) { float value = 1.0f; int bits; memcpy(&bits, &value, sizeof bits); printf("%d\n", bits); return 0; }Answer
1065353216
The IEEE 754 pattern of 1.0f is 0x3F800000, which is 1065353216 as a signed int.
-
This program is built with
gcc -std=c17 -O2 -Wall -Wextra bounds.c -o bounds. Which warning flag does GCC print?#include <stdio.h> int main(void) { int values[3] = {1, 2, 3}; for (int i = 0; i <= 3; i++) { printf("%d\n", values[i]); } return 0; }Answer
-Waggressive-loop-optimizations
The optimiser proves the final loop iteration is out of bounds and reports it under
-Waggressive-loop-optimizations. -
This program is built as
literaland run. What does the shell report?#include <stdio.h> int main(void) { char *text = "hello"; text[0] = 'H'; printf("%s\n", text); return 0; }Answer
exit: 139
The literal may live in read-only memory, so the store is undefined and the process dies with a segmentation fault, reported as 139.
-
Which warning flag reports a type-punned pointer under optimisation?
Answer
-Wstrict-aliasingThe optimiser relies on the aliasing rules, and
-Wstrict-aliasing=2points at the cast that breaks them. -
What is the fix for writing into a string you need to modify?
Answer
Declare it as
char text[] = "hello";An array initialised from the literal owns its bytes, so the characters can be changed safely.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev