Static Analyzer in C
Introduction
Hi everyone! It's been quite a while (2 weeks plus?) since the last time I wrote an article. My usual writing time was used to help collaborate on a game development with my 10 year old daughter.
Check out the video demo of her work in progress titled "Dungeon Survivor" using Godot game engine.
She has started writing her own bearblog but things are a bit disorganized so yeah it is what it is.
Static Analyzer
As usual I'm too lazy to repeat what's been covered well by other references, so I'll just provide the resources for you to read:
- Article from HackerNews : the actual article is gone (cannot be accessed), but what's important is the discussion which provide lots of insights from multiple point of view.
- Github repo for static analysis.
- Github repo for dynamic analysis
- Youtube video on clang static analyzer : watch this to give you a glimpse the "What and How" these static analyzers work.
Section summary: static analyzer catches a lot more bugs and make your C code safer. Nasty errors such as null pointer dereference, double free, use after free, etc are caught earlier at compile time instead of runtime....squash them nasty bugses!
Below is a screenshot from the Youtube video:
This provide us with the "Why" we need static analyzer: bugs are expensive!
The Code for case study
I am going to take the code from Primeagen video: "C must DIE!" as the case study and I will show you how easy we can avoid the nasty null pointer dereference bug.
Red box is the code, it is a particularly nasty bug. Be sure to watch Prime's video for the explanation.
I modified the code so we can print out the output:
#include <stdio.h>
int contains_null_check(int *p)
{
int dead = *p;
if (p == 0) { return *p; }
*p = 4;
return *p;
}
int main(void)
{
/* // the code below runs fine since p is not null pointer.
int x = 69;
int *p = &x;
int result = contains_null_check(p);
printf("Not null ptr check output: %d\n", result);
// */
// /* // the code below triggers error
int *x = NULL;
int *p = x;
int result = contains_null_check(p);
printf("Null ptr check output: %d\n", result);
// */
return 0;
}
Note you can comment and uncomment between x = 69 or *x = NULL to see how differently they behave. If you uncomment the x = 69 then it will print out result = 4 since it has been dereferenced to the value 4 (normal, expected behaviour).
Compiling the program will succeed:
- gcc-14 -o null_ptr_deref -O null_ptr_deref.c
- clang-14 -o null_ptr_deref -O null_ptr_deref.c
However check out the screenshot below:
Compiling using GCC and Clang resulted in different result:
- GCC: segfault.
- Clang: successfully printed out the null pointer, which it shouldn't be possible --> this is undefined behaviour, the most unwanted output from a C program.
How can we improve this? We could catch this error at compile time via using static analyzers.
GCC -fanalyzer and Clang --analyze
Using the same code, lets try compile it with static analyzer active. Please note I usually use these alias:
- Both gcc-14_safest and clang-14_safest got the same compiler flags = -Wall -Werror -Wextra
But for this exercise I'm not using the "_safest" alias to point out how the static analyzers affect the compilation process.
Run the GCC compile command:
gcc-14 -o null_ptr_deref -O null_ptr_deref.c -fanalyzer
Look at the beautiful error-flow trace provided by GCC!
Next the Clang compile command:
clang-14 -o null_ptr_deref -O null_ptr_deref.c --analyze
Clang is able to provide the same error-flow trace but it's in html format which means we have to open it via browser thus we have to leave the terminal --> unnecessary hassle hence I skipped it.
Voila! We successfully caught the bug at compile time! This means the program will not run and we the programmer need to deal with the error.
Conclusion
The ability to catch bugs at compile time makes debugging process so much easier since on runtime we have to test and assert every components of our program to make sure every bugs have been squashed!
On compile time we don't have to do that coz it's known even before we run the program. This is not to say we don't need to write tests and assertions, but it's a huge QoL improvement in the debugging process.
At the end of the post I want to use this opportunity to push one important message: use modern C tools! There are plenty of tools we can use: compiler flags, sanitizers (dynamic analyzers), static analyzers, safer functions, etc. I see plenty of people on the path of learning C are trapped with ANSI-C / the original C89.
Well unless you are a veteran C developer who got decades of experience writing C, you're shooting yourself in the foot and I'd say it's far better to use modern C practices, otherwise maybe Rust is a better language since it enforces safety.
[Update]: check out the part-2 covering C Static Analyzers..all bugs / errors are caught_at_COMPILE_TIME !! Impressive, most impressive!