Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 213
Seva Alekseyev Avatar asked Apr 17 '16 21:04

Seva Alekseyev


1 Answers

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:

  • start a target process and run until there is an exception
  • capture the IL bytes & IL-to-native mappings
  • (crudely) disassemble the IL with indicators showing which IL instructions are grouped together with the same mapping.

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).

like image 85
Brian Reichle Avatar answered Oct 03 '22 11:10

Brian Reichle