NullReferenceException vs. MSIL
I'm interpreting an exception report from a C# Windows Phone app. A method throws a NullReferenceException
. The method goes:
public void OnDelete(object o, EventArgs a)
{
if (MessageBox.Show(Res.IDS_AREYOUSURE, Res.IDS_APPTITLE, MessageBoxButton.OKCancel) == MessageBoxResult.OK)
m_Field.RequestDelete();
}
It's consistent with m_Field
being null - there's simply nothing else there that can possibly be null. But here's the mysterious part.
The GetILOffset()
from the StackFrame
from the StackTrace
for the exception object returns 0x13. The MSIL for the method, as shown by ILDASM, goes:
IL_0000: call string App.Res::get_IDS_AREYOUSURE()
IL_0005: call string App.Res::get_IDS_APPTITLE()
IL_000a: ldc.i4.1
IL_000b: call valuetype (...) System.Windows.MessageBox::Show(...)
IL_0010: ldc.i4.1
IL_0011: bne.un.s IL_001e
IL_0013: ldarg.0
IL_0014: ldfld class App.Class2 App.Class1::m_Field
IL_0019: callvirt instance void App.Class2::RequestDelete()
IL_001e: ret
Here's what I don't understand. If the offset is indeed 0x13, that means the ldarg
line causes the exception. But the command is documented as not throwing any exceptions. It's callvirt
that should throw, isn't it? Or is the offset relative to something other than the method beginning? ldfld
can also throw, but only if the this
object is null; that's not possible in C# AFAIK.
The docs mention that debug info might get in the way of the offset, but it's a release build.
The DLL that I'm examining with ILDASM is exactly the one that I've shipped to the Windows Phone Store as a part of my XAP.
When the JIT generates the machine code, it also generates MSIL <--> machine code mappings. When you get an exception in the generated code, the run-time will use the mappings to identify the IL offset.
The JIT is allowed to re-order machine instructions as part of its optimizations (when they are enabled), this can result in mappings becoming more approximate and granular. If the field access was brought forward (memory access is relatively slow, sometimes its good to start loading it well before you need it) then the exception may appear to have been thrown by an earlier IL instruction.
I butchered one of my debug utilities to do the following:
I then ran the tool on a dummy process that does roughly what you show in the question, and got the following (release build):
IL_0000: call 0600000B
IL_0005: call 0600000A
IL_000A: ldc.i4.1
IL_000B: call 0A000014
IL_0010: ldc.i4.1
IL_0011: bne.un.s 30
----
IL_0013: ldarg.0
IL_0014: ldfld 04000001
IL_0019: callvirt 06000004
----
IL_001E: ret
As you can see, the ldarg.0
, ldfld
and callvirt
instructions are all covered by the same mapping, so if any of these triggers an exception, they will all map back to the same IL offset (0x13).