Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't *(int*)0=0 cause an access violation?

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?

like image 910
Peter Olson Avatar asked Dec 30 '11 15:12

Peter Olson


2 Answers

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.

like image 100
Eric Lippert Avatar answered Sep 19 '22 12:09

Eric Lippert


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.

like image 35
Daniel Avatar answered Sep 21 '22 12:09

Daniel