GCC Optimization: -fdelete-null-pointer-checks

Let’s say you have the following program:

#include <stdio.h>

int main (int argc, char** argv)
{
	int * p = (int *)argv[1];
	
	printf("%d", *p);
	
	if(p) {
		puts("When life gives you lemons");
	}
}

It is a simple program that does a couple things.

  1. Accepts an argument from the user
  2. Casts the input to pointer of type int, and assigns it to p
  3. Dereferences p and prints it to the screen
  4. Checks if p is not null, and if it isn’t output the string

At optimization level 2 and up(-O2 and -O3) the compiler will actually remove the null pointer check at step 4. This is because of the GCC option, -fdelete-null-pointer-checks. The point of this optimization is to remove redundancy in the code. If the program finds that the pointer was dereferenced earlier on in the code, it will assume that the pointer is not null.

In our case, on line 7, we dereference the pointer so that we can output it’s value onto the screen. Once the compiler sees this line, it will erase all null pointer checks that tests for p if the option is enabled. The compiler thinks that if we are dereferencing a variable then it must have an address that it is pointing to, and think that all future tests are redundant and unecessary.

In an attempt to show what is happening I will compile the program twice. Once with the option enabled and one without it:

gcc -o on Main.c -g -O2 -fno-builtin -fdelete-null-pointer-checks
gcc -o off keep_checks Main.c -g -O2 -fno-builtin -fno-delete-null-pointer-checks

Note that we are using optimization level 2 to compile our programs. This is because level 2 and up have the optimization options that -fdelete-null-pointer-checks depends on.

Let’s take a look at the section in the binary with the option enabled, using objdump:

objdump -f -s -d --source on.exe
00401c80 <_main>:
#include <stdio.h>

int main (int argc, char ** argv)
{
  401c80:	55                   	push   %ebp
  401c81:	89 e5                	mov    %esp,%ebp
  401c83:	83 e4 f0             	and    $0xfffffff0,%esp
  401c86:	83 ec 10             	sub    $0x10,%esp
  401c89:	e8 e2 fc ff ff       	call   401970 <___main>
	int * p = (int *)argv[1];
	
	printf("%d", *p);
  401c8e:	8b 45 0c             	mov    0xc(%ebp),%eax
  401c91:	8b 40 04             	mov    0x4(%eax),%eax
  401c94:	8b 00                	mov    (%eax),%eax
  401c96:	c7 04 24 64 30 40 00 	movl   $0x403064,(%esp)
  401c9d:	89 44 24 04          	mov    %eax,0x4(%esp)
  401ca1:	e8 3a ff ff ff       	call   401be0 <_printf>
	
	if(p) {
		puts("When life gives you lemons");
  401ca6:	c7 04 24 67 30 40 00 	movl   $0x403067,(%esp)
  401cad:	e8 36 ff ff ff       	call   401be8 <_puts>
	}
  401cb2:	c9                   	leave  
  401cb3:	c3                   	ret    
  401cb4:	90                   	nop
  401cb5:	90                   	nop
  401cb6:	90                   	nop
  401cb7:	90                   	nop
  401cb8:	90                   	nop
  401cb9:	90                   	nop
  401cba:	90                   	nop
  401cbb:	90                   	nop
  401cbc:	90                   	nop
  401cbd:	90                   	nop
  401cbe:	90                   	nop
  401cbf:	90                   	nop

and now with the option disabled:

objdump -f -s -d --source off.exe
00401c80 <_main>:
#include <stdio.h>

int main (int argc, char ** argv)
{
  401c80:	55                   	push   %ebp
  401c81:	89 e5                	mov    %esp,%ebp
  401c83:	53                   	push   %ebx
  401c84:	83 e4 f0             	and    $0xfffffff0,%esp
  401c87:	83 ec 10             	sub    $0x10,%esp
  401c8a:	e8 e1 fc ff ff       	call   401970 <___main>
	int * p = (int *)argv[1];
  401c8f:	8b 45 0c             	mov    0xc(%ebp),%eax
  401c92:	8b 58 04             	mov    0x4(%eax),%ebx
	
	printf("%d", *p);
  401c95:	8b 03                	mov    (%ebx),%eax
  401c97:	c7 04 24 64 30 40 00 	movl   $0x403064,(%esp)
  401c9e:	89 44 24 04          	mov    %eax,0x4(%esp)
  401ca2:	e8 39 ff ff ff       	call   401be0 <_printf>
	
	if(p) {
  401ca7:	85 db                	test   %ebx,%ebx
  401ca9:	74 0c                	je     401cb7 <_main+0x37>
		puts("When life gives you lemons");
  401cab:	c7 04 24 67 30 40 00 	movl   $0x403067,(%esp)
  401cb2:	e8 31 ff ff ff       	call   401be8 <_puts>
	}
  401cb7:	8b 5d fc             	mov    -0x4(%ebp),%ebx
  401cba:	c9                   	leave  
  401cbb:	c3                   	ret    
  401cbc:	90                   	nop
  401cbd:	90                   	nop
  401cbe:	90                   	nop
  401cbf:	90                   	nop

We can see that in the binary with the option disabled, two lines are added.

  401ca7:	85 db                	test   %ebx,%ebx
  401ca9:	74 0c                	je     401cb7 <_main+0x37>

This is the code that checks if the condition is true, and if it is, jumps into the code block. As you can see without the option enabled, the compiler will always check if p is null. With the option enabled, the compiler will skip that check entirely as if that piece of code was not there.

Differences Between the Binaries
When we compare the two binaries, we can see that there is almost no difference in file size. In fact, the binary with the option enabled produced a slightly larger binary. In terms of compile time, the difference is insignificant. The difference is very small, but the program with the option enabled slightly faster, since it does not have to validate the pointer.

Dangers of Using -fdelete-null-pointer-checks
The danger of enabling this option is that a pointer might point to nullsometime during run-time and if there is no validation for that pointer, it will cause the program to crash. Since we don’t receive an error message saying that a pointer is pointing to null, we will have a hard time trying to find the problem.

When to Use -fdelete-null-pointer-checks
Pointers should almost always be validated before performing operations with them. The only times I found where this option being enabled would benefit the program would be during Linux kernel development, and other highly specific projects. Most projects don’t need to have this option enabled, and because of this, if you are using optimization level 2 and up, I suggest using the option to disable this, -fno-delete-null-pointer-checks.

Leave a comment