Post

Wait! what? UB? wdym?

If you have ever programmed in C or C++, you would have heard the term “UB”; if not, then this article is for you, neophyte. UB stands for Undefined Behavior i.e., the behavior is not defined by standard 1 and can result in anything like a run-time error, segmentation fault, or “demons flying out of your nose. 2

TL;DR

UB (Undefined Behavior) is the unpredictable behavior of the program generated by the compiler, which might vary from architecture to architecture and compiler to compiler. There are no rules that bind the program’s output and can result in the program being terminated by a segmentation  fault or a flight to fall from a high altitude because the altitude variable overflowed. An example of this would be accessing an array out of its bounds or dereferencing a wild pointer.

What exactly is UB?

Ever wasted 3 hours of your life banging the head to the wall while debugging just to find out that a random pointer went uninitialized causing the entire program to hard crash when it got dereferenced?

No?!?

Then you might prefer to continue reading the article.

Yes!?

Then there is a 100% chance that is because of a UB, and I urge to read this article.

For the starters, let’s go with the basic definition. Here’s a definition from C FAQ:

“Anything at all can happen; the Standard imposes no requirements. The program may fail to compile, or it may execute incorrectly (either crashing or silently generating incorrect results), or it may fortuitously do exactly what the programmer intended.”

So, a UB can cause anything… isn’t that bad?
Yes! it is bad, and most of the times, it can be avoided by the programmer by being a bit cautious (excluding the atrocities caused by the compiler itself 3 ).

Let’s take a look at few example for better understanding.

Consider this following code

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void) {
  
  int *p; // A wild pointer has appeared!

  printf("%p\n", p); // Printing the address
  printf("%d\n", *p); // Dereferencing the pointer!

  return 0;
}

Output with gcc (v11.1.0)

1
2
3
❯ gcc wild_ptr.c -o gcc.out && ./gcc.out
(nil)
[1]    535911 segmentation fault (core dumped)  ./gcc.out

Output with clang (v13.0.0)

1
2
3
❯ clang wild_ptr.c -o clang.out && ./clang.out
0x0efe3f0922f0
1

Output with tcc (v0.9.27)

1
2
3
❯ tcc wild_ptr.c -o tcc.out && ./tcc.out
(nil)
[1]    537963 segmentation fault (core dumped)  ./tcc.out

(Wanna try out in more compilers?)

wildptr Here, P is getting assigned to a random address because it is not allocated or initialized with any memory, leading it to point to any block in the address space. This situation might vary, as few “intelligent” compilers might predict this and pre-initialized with NULL.4

Considering the compiler outputs from above; the outputs from GCC and TCC are the same, leading to a Seg fault. While clang, being an “somewhat-intelligent” compiler, makes sure that the program doesn’t crash atleast (phew).

Then Clang gets it right by not crashing! So, clang is the best compiler, right?

If you are thinking like this, consider this example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
  
  // Allocating 10 blocks of char size data
  char* str = malloc(sizeof(char) * 10);

  // causes memory error as size of string
  // being copied is greater than 10
  strcpy(str, "Some String which will overflow");

  printf("%s\n", str);

  return 0;
}

Output with gcc (v11.1.0)

1
2
3
4
5
6
7
8
9
10
11
12
❯ gcc buffer_oveflow.c -o gcc.out
buffer_oveflow.c: In function ‘main’:
buffer_oveflow.c:9:3: warning: ‘__builtin_memcpy’ writing 32 bytes into a region of size 10 overflows the destination [-Wstringop-overflow=]
    9 |   strcpy(str, "Some String which will overflow");
      |   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
buffer_oveflow.c:7:15: note: destination object of size 10 allocated by ‘malloc’
    7 |   char* str = malloc(sizeof(char) * 10);
      |               ^~~~~~~~~~~~~~~~~~~~~~~~~

❯ ./gcc.out
malloc(): corrupted top size
[1]    589156 abort (core dumped)  ./overflow.out

Output with clang (v13.0.0)

1
2
3
4
5
6
❯ clang buffer_overflow.c -o clang.out

❯ ./clang.out
malloc(): corrupted top size
[1]    590478 abort (core dumped)  ./clang.out

In this case, Clang did not produce any error at all5. But, GCC warned the user about the String operation overflow; So, is GCC the best compiler?
If you are asking about the best compiler… we could have a debate until the end of the eternity. Leaving aside the issue of determining which compiler is the best, if we look at the output of both of the binaries generated by the compilers, we can notice that they are pretty much the same.

But this might vary from your computer to my computer, or from GCC or MSVC or Clang. This is the reason it is called a UB, since we are unable to predict what might happen; it’s a programmers nightmare!

So, how do we fix it now?

Most of these errors can be easily fixed by the programmer being cautious about what he is trying to achieve and how the compiler perceives it. It may be a time-consuming task, but it is better to not have any UB lurking inside your code, waiting for a perfect Saturday to manifest itself and ruin your weekend!

For example consider the above code where we derefernced a wild pointer, fixing it is as easy as a stroll in the park. (unless you are an introvert)

  • An easy solution would be to always intialize a poitner with NULL
    1
    2
    3
    
    ...
    int *p = NULL;
    ...
    

    NULL ptr

So, I’ve fixed the UB, now can I access the pointer p?
Do you expect to time travel6 if you jump into the black hole? No! you’ll be shredded into bits before you even reach the event horizon.
Remember DO NOT dereference a NULL pointer, it’s definitely a UB and most of time will lead to a Segmentation fault.

B-but, you just said that you need to always initialize poitners with NULL?
Yes! but don’t dereference it yet, assign or allocate it some memory and then dereference it. Like:

1
2
3
...
p = (int *) malloc(sizeof(int) * 177013);
...

good ptr

Aren’t there any general solutions for this?

Unfortunately, there isn’t any general solution for all of these UBs. The programmer has to identify them manually and come up with a solution.
But there are some practices which you can follow:

  • Enable compiler diagnostic messages (-W<flag>)
  • Use compiler built-in Sanitizers (eg: -fsanitize-address)
  • Use static analyzers to get more warnings
  • Check for memory leaks using valgrind
  • Use a good debugger (like gdb, lldb, …)
  • And many more!

Why do they exist?

Now that you know a bit of what UB is7, I would like to give you my opinion why this obscure construct still exsists and why can’t the standard committee abolish and bring peace and glory! (Note: This opinion is very biased, but here you go.)

  • Speed
    Checking for UB either at runtime or at compile time might just add an extra overload to the program, which would affect its precious runtime speed. Because many low-level drivers and kernels are written in C, it may break a few APIs or functions and bring the entire hell down on the maintainer’s head.
  • Size
    Adding in these run-time checks might just bloat the program with unwanted clutter and lead to a larger executable size.
  • Quality
    Ever thought of using UB as a feature? Yeah! You read it right. There are a few (a very few), programs out there that take advantage of UB to apply ethereal rocket science mechanics to their work. And tampering with these historical relics may call the Cthulhu!
  • History
    Many of the low-level programs are old and are meant to be reliable; putting the programs built with it aside, the language itself is pretty old, and bringing a heavy paradigm shift that might affect the entire structural design of the system is something that cannot be done. :(
  • Laziness
    It’s self explanatory. There was a time when an extra stray double quote(") was declared as a UB by the standard

References and further reading


  1. The ISO C and C++ standard ↩︎

  2. Nasal Demons ↩︎

  3. This might be a case of non-conformant or older version of compiler; Bugs should be exepected when using these age old abominations. ↩︎

  4. This is a rare scenario and is quite considered to be an overhead, refer this for more info. ↩︎

  5. This is a case where I’ve not used any compiler arguments. It could have been a different case if ASan or any other compiler args would have been used. ↩︎

  6. Another connotation bought up by darkblueflow💠#5176 from discord (Thank you!). Refer here ↩︎

  7. This article just barely scratches the surface of few abominations a UB can do. If you wanna read more about it look at the References or just google :D ↩︎

This post is licensed under CC BY 4.0 by the author.