For educational purposes, I'm writing a set of methods that cause runtime exceptions in C# to understand what all the exceptions are and what causes them. Right now, I'm tinkering with programs that cause an AccessViolationException
.
The most obvious way (to me) to do this was to write to a protected memory location, like this:
System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0);
Just as I had hoped, this threw an AccessViolationException
. I wanted to do it more concisely, so I decided to write a program with unsafe code, and do (what I thought was) exactly the same thing by assigning 0
to the zero-pointer.
unsafe { *(int*)0 = 0; }
For reasons that elude me, this throws a NullReferenceException
. I played around with it some and found out that using *(int*)1
instead also throws a NullReferenceException
, but if you use a negative number, like *(int*)-1
it will throw an AccessViolationException
.
What's going on here? Why does *(int*)0 = 0
cause a NullReferenceException
, and why doesn't it cause an AccessViolationException
?
A null reference exception happens when you dereference a null pointer; the CLR does not care whether the null pointer is an unsafe pointer with the integer zero stuck into it or a managed pointer (that is, a reference to an object of reference type) with zero stuck into it.
How does the CLR know that null has been dereferenced? And how does the CLR know when some other invalid pointer has been dereferenced? Every pointer points to somewhere in a page of virtual memory in the virtual memory address space of the process. The operating system keeps track of which pages are valid and which are invalid; when you touch an invalid page it raises an exception which is detected by the CLR. The CLR then surfaces that as either an invalid access exception or a null reference exception.
If the invalid access is to the bottom 64K of memory, it's a null ref exception. Otherwise it is an invalid access exception.
This explains why dereferencing zero and one give a null ref exception, and why dereferencing -1 gives an invalid access exception; -1 is pointer 0xFFFFFFFF on 32 bit machines, and that particular page (on x86 machines) is always reserved for the operating system to use for its own purposes. User code cannot access it.
Now, you might reasonably ask why not just do the null reference exception for pointer zero, and invalid access exception for everything else? Because the majority of the time when a small number is dereferenced, it is because you got to it via a null reference. Imagine for example that you tried to do:
int* p = (int*)0; int x = p[1];
The compiler translates that into the moral equivalent of:
int* p = (int*)0; int x = *( (int*)((int)p + 1 * sizeof(int)));
which is dereferencing 4. But from the user's perspective, p[1]
surely looks like a dereference of null! So that is the error that is reported.
This isn't an answer per se, but if you decompile WriteInt32
you find it catches NullReferenceException
and throws an AccessViolationException
. So the behavior is likely the same, but is masked by the real exception being caught and a different exception being raised.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With