The problem is that dereferencing a null pointer does not actually have undefined semantics, it has system defined semantics. The compiler should compile source code in such a way as to produce machine code that does whatever that system does when a null pointer is dereferenced.
It should do this in part because very large volumes of code will be compiled that way due to the inability to detect in advance whether a pointer is null or not, and compiling it differently when it is known that a pointer is null makes for inconsistent behavior and vulnerabilities like this one.
This is the way relatively sane simple-minded compilers have worked for a long time, and no compiler should be allowed to pretend that certain constructs do not have reliable or at least consistent system defined behavior that will ensue if they are used.
Similarly if a compiler can tell that a signed addition will overflow in some cases it should be required to do whatever a signed addition overflow does on that architecture, and for the same reason. Issue the appropriate warnings about non-portable code, but do something reliable and reliably useful.
> The problem is that dereferencing a null pointer does not actually have undefined semantics, it has system defined semantics.
No it does not. Look in the standard.
Seriously though, I get what you mean but what is the point of having a systems-defined NULL dereference? The semantics of the NULL pointer (in C) is such that you are not meant to dereference it. If you dereference it, you have a bug no matter what the manifestation on a specific system is.
And there are probably actual systems where it would be hard to specify the behaviour. For example MMU-less systems, where you can derefence byte 0 but it might not be statically clear what kind of value is stored there?
Maybe C allows a systems-defined behaviour to be implemented as undefined behaviour? But then the distinction is kinda moot.
I think butlerm was saying that dereferencing a null pointer has system-defined semantics in machine code, and that C should inherit those semantics.
> If you dereference it, you have a bug no matter what the manifestation on a specific system is.
Sure, if you dereference a null pointer you have a bug. But all sufficiently large programs have bugs, and what happens after you trigger the bug is important. The closer the machine code matches your C code, the more likely you are to be able to diagnose the problem from its symptoms.
> Maybe C allows a systems-defined behaviour to be implemented as undefined behaviour
C standard distinguishes undefined behavior, unspecified behavior and implementation-defined behavior. The first is invalid and inconsistent, the second is valid and inconsistent, the third is valid and consistent (in scope of implementation).
It is rather hard to require invalid but consistent, as the platform itself may produce inconsistent behavior in invalid cases. So what some people want is something like - "be no more inconsistent that would be naive implementation on given platform", but that is both vague and not really useful. And while some of these undefined-based optimizations seems egregious, some are necessary in order to have reasonably performant code (e.g. keeping variable values in registers instead of propagating them from/to memory after each step).
The point of having a system-defined NULL dereference (or whatever else is UB) is predictability both at compile-time and at run-time - given a particular system and a piece of high-level code, you can infer roughly what the compiled low-level code is; this means that given a bug, since odds are good you know what system the code is running on, you probably have a leg up in understanding what went wrong.
It's nice being able to strap your program into GDB and have it breakpoint at the point of a NULL dereference because it triggered a segfault, instead of needing to magically infer why results are wrong anywhere in the program because the compiler decided to inject nasal demons due to some subtle optimization pass.
The interrupt descriptor table in real mode on x86 is stored at the zero page. It is valid to dereference a pointer with value zero under DOS. Modern OS's explicitly avoid mapping at page zero in order to trap NULL pointer dereferences.
That does not mean you cannot access memory mapped at zero, just that the system might have mechanisms to catch errant programs. It is not the responsibility of the C compiler to make the distinction of whether or not accessing memory at zero is errant, but rather the architecture and the system which determine how to respond to a process that attempts to do so.
Again, the C standard came later, and in many cases the standard is about consistency across platforms where there's plenty of useful behaviors of specific platforms that you want to take advantage of. And that the modern practice of optimizing compliers emitting nasal demons gets in the way of.
Imagine you have a function that traverses a non-circular linked list. You implement it with a recursive function. The compiler uses Tail Call Optimization to convert it to a loop. Then your program hands this function a circularly linked list.
Your program now behaves differently after optimization. Before, your program crashed with a stack overflow. Now, your program loops forever.
Should the compiler have not done this? Or is it a bug in your program that you passed an invalid input to this function?
Looping forever is the same as if you ran the recursive version on a hypothetical machine with unlimited stack memory, so it didn't really change semantics.
But we aren't running on a hypothetical machine. That's the whole argument with the "define everything" crowd. Those two programs are different on my machine. Is that my fault or the compiler's fault?
People say "I just want my machine to do what my machine does when I dereference a pointer at address 0, why is my compiler making my program do something different?" Why can't I say the same thing in my case?
No, the argument against undefined behavior is that it makes errors silent. I don't care whether dereferencing a null pointer causes a segfault, an exception or something else, but it shouldn't cause the program to run as if nothing is wrong.
In a multithreaded program, one of the implementations I described crashes my program and one makes everything else but the stuck thread run just fine.
> The problem is that dereferencing a null pointer does not actually have undefined semantics, it has system defined semantics. The compiler should compile source code in such a way as to produce machine code that does whatever that system does when a null pointer is dereferenced.
Null pointer dereference is just a special case of invalid pointer dereference. And that does not have consistent results anyways (may segfault or may just return garbage)
At the system level (not the abstract virtual machine in the current specification) it is reliably known whether a null pointer dereference will return a specified value (such as zeroes), unspecified values (semi-random data), or a result in a system exception (segfault) of some sort.
The compiler must produce code that does that because it cannot determine whether a pointer is null in advance in most cases. So letting it do something different when it knows that a pointer is null (due to some sort of coding mistake) vs. when it doesn't know that the pointer is null is gratuitously non-deterministic behavior.
The safe (if somewhat slower) thing to do is to emit the same code regardless, so that a null pointer dereference has the same effect even when inadvertently inserted into the program.
The same operation should produce the same result in the same program, as much as possible anyway, and if not the same result then a similar one. It is not a reasonable assumption that any real world program will never dereference a null pointer. It is helpful that the consequence of doing so be as stable and predictable as the underlying architecture provides for.
It should do this in part because very large volumes of code will be compiled that way due to the inability to detect in advance whether a pointer is null or not, and compiling it differently when it is known that a pointer is null makes for inconsistent behavior and vulnerabilities like this one.
This is the way relatively sane simple-minded compilers have worked for a long time, and no compiler should be allowed to pretend that certain constructs do not have reliable or at least consistent system defined behavior that will ensue if they are used.
Similarly if a compiler can tell that a signed addition will overflow in some cases it should be required to do whatever a signed addition overflow does on that architecture, and for the same reason. Issue the appropriate warnings about non-portable code, but do something reliable and reliably useful.