Here is a line-to-line analysis of your code.
null.ToString();
Here a method is called on a null reference. Of course exception would be thrown.
int? foo = null;
This creates a struct foo of type Nullable<int> on the stack, and has its HasValue property to false.
foo.ToString();
This calls the ToString() method of Nullable<T>, which is overridden to return an empty string ("").(In your question you said that returns 0. This is not true.) Note that calling an overridden method directly on a value type does not cause boxing.
0 == foo
This works and returns false because 0 is converted into a Int32? which contains a value, but foo does not contain any value. See this answer about nullable type conversion about and documentation about lifted operators
null == foo;
Works and returns true because null is converted to Int32? which does not contain any value. This equals foo, which also doesn't contain any value.
null == 0
Works and returns false because null is converted to Int32? and 0 is converted to Int32? but the former contains no value.