Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do C# struct instance methods calling instance methods on a struct field first check ecx?

Tags:

Why does the X86 for the following C# method CallViaStruct include the cmp instruction?

struct Struct {
    public void NoOp() { }
}
struct StructDisptach {

    Struct m_struct;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void CallViaStruct() {
        m_struct.NoOp();
        //push        ebp  
        //mov         ebp,esp  
        //cmp         byte ptr [ecx],al  
        //pop         ebp  
        //ret
    }
}

Here is a more complete program that can be compiled with various (release) decompilations as comments. I expected the X86 for CallViaStruct in both ClassDispatch and StructDispatch types to be the same however the version in StructDispatch (extracted above) includes a cmp instruction while the other does not.

It appears the cmp instruction is an idiom is used to ensure a variable is not null; dereferencing a register with value 0 triggers an av that is turned into a NullReferenceException. However in StructDisptach.CallViaStruct I cannot conceive of a way for ecx to be null given it's pointing at a struct.

UPDATE: The answer I'm looking to accept will include code that causes a NRE to be thrown by StructDisptach.CallViaStruct by having it's cmp instruction dereference a zeroed ecx register. Note this is easy to do with either of the CallViaClass methods by setting m_class = null and impossible to do with ClassDisptach.CallViaStruct as there is no cmp instruction.

using System.Runtime.CompilerServices;

namespace NativeImageTest {

    struct Struct {
        public void NoOp() { }
    }

    class Class {
        public void NoOp() { }
    }

    class ClassDisptach {

        Class m_class;
        Struct m_struct;

        internal ClassDisptach(Class cls) {
            m_class = cls;
            m_struct = new Struct();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaClass() {
            m_class.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //mov         eax,dword ptr [ecx+4]  
            //cmp         byte ptr [eax],al  
            //pop         ebp  
            //ret  
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaStruct() {
            m_struct.NoOp();
            //push        ebp
            //mov         ebp,esp
            //pop         ebp
            //ret
        }
    }

    struct StructDisptach {

        Class m_class;
        Struct m_struct;

        internal StructDisptach(Class cls) {
            m_class = cls;
            m_struct = new Struct();
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaClass() {
            m_class.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //mov         eax,dword ptr [ecx]  
            //cmp         byte ptr [eax],al  
            //pop         ebp  
            //ret  
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public void CallViaStruct() {
            m_struct.NoOp();
            //push        ebp  
            //mov         ebp,esp  
            //cmp         byte ptr [ecx],al  
            //pop         ebp  
            //ret  
        }
    }

    class Program {
        static void Main(string[] args) {
            var classDispatch = new ClassDisptach(new Class());
            classDispatch.CallViaClass();
            classDispatch.CallViaStruct();

            var structDispatch = new StructDisptach(new Class());
            structDispatch.CallViaClass();
            structDispatch.CallViaStruct();
        }
    }
}

UPDATE: Turns out it's possible to use callvirt on a non-virtual function which has a side effect of null checking the this pointer. While this is the case for the CallViaClass callsite (which is why we see the null check there) StructDispatch.CallViaStruct uses a call instruction.

.method public hidebysig instance void  CallViaClass() cil managed noinlining
{
  // Code size       12 (0xc)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class NativeImageTest.Class NativeImageTest.StructDisptach::m_class
  IL_0006:  callvirt   instance void NativeImageTest.Class::NoOp()
  IL_000b:  ret
} // end of method StructDisptach::CallViaClass

.method public hidebysig instance void  CallViaStruct() cil managed noinlining
{
  // Code size       12 (0xc)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldflda     valuetype NativeImageTest.Struct NativeImageTest.StructDisptach::m_struct
  IL_0006:  call       instance void NativeImageTest.Struct::NoOp()
  IL_000b:  ret
} // end of method StructDisptach::CallViaStruct

UPDATE: There was a suggestion that the cmp could be trapping for the case where a null this pointer was not trapped for at the call site. If that were the case then I'd expect the the cmp to occur once at the top of the method. However it appears once for each call to NoOp:

struct StructDisptach {

    Struct m_struct;

    [MethodImpl(MethodImplOptions.NoInlining)]
    public void CallViaStruct() {
        m_struct.NoOp();
        m_struct.NoOp();
        //push        ebp  
        //mov         ebp,esp  
        //cmp         byte ptr [ecx],al  
        //cmp         byte ptr [ecx],al  
        //pop         ebp  
        //ret  
    }
}
like image 240
Christopher King Avatar asked May 09 '16 01:05

Christopher King


People also ask

Why do we write in C?

It was mainly developed as a system programming language to write operating system. The main features of C language include low-level access to memory, simple set of keywords, and clean style, these features make C language suitable for system programming like operating system or compiler development.

Why do we use %d in C?

In C programming language, %d and %i are format specifiers as where %d specifies the type of variable as decimal and %i specifies the type as integer. In usage terms, there is no difference in printf() function output while printing a number using %d or %i but using scanf the difference occurs.

Why do we use semicolon in C?

Role of Semicolon in C: Semicolons are end statements in C. The Semicolon tells that the current statement has been terminated and other statements following are new statements. Usage of Semicolon in C will remove ambiguity and confusion while looking at the code.

Why is C language called C?

After language 'B', Dennis Ritchie came up with another language which was based upon 'B'. As in alphabets B is followed by C and hence he called this language as 'C'.


1 Answers

Short answer: The JITter cannot prove that the struct is not referenced by a pointer, and must at least dereference at least once on every call to NoOp() for correct behavior.


Long answer: Structs are weird.

The JITter is conservative. Wherever possible, it can only optimize the code in ways that it can be absolutely certain produce correct behavior. "Mostly-correct" isn't good enough.

So now here's an example scenario that would break if the JITter optimized away the dereference. Consider the following facts:

First: Remember that structs can (and do!) exist outside C# — a pointer to a StructDispatch could come from unmanaged code, for example. As Lucas pointed out, you can use pointers to cheat; but the JITter can't know for sure that you aren't using pointers to StructDispatch somewhere else in the code.

Second: Remember that in unmanaged code, which is the biggest reason structs exist in the first place, all bets are off. Just because you just read a value from memory doesn't mean it'll be the same value or even be a value the next time you read that same exact address. Threading, and multiprocessing, can literally have something change that value on the next clock tick, to say nothing of non-CPU actors like DMA. A parallel thread could VirtualFree() the page that contains that struct, and the JITter has to guard against that. You asked for reads from memory, so you get reads from memory. My guess is that if you kicked in the optimizer, it would remove one of those cmp instructions, but I highly doubt that it would remove both.

Third: Exceptions are real code too. NullReferenceException doesn't necessarily stop the program; it can be caught and handled. That means that from the JITter's perspective, NRE is more like an if-statement than a goto: It's a kind of condition branch that must be handled and considered on every memory dereference.

So now put those pieces together.

The JITter doesn't know — and can't know — that you're not using unsafe C# or an external source somewhere else to interact with StructDispatch's memory. It doesn't produce separate implementations of CallViaStruct(), one for "probably safe C# code" and one for "possibly risky external code;" it produces the conservative version for possibly risky scenarios, always. This means that it can't just cut out calls to NoOp() in full, because there's no guarantee that StructDispatch isn't, say, mapped to an address that isn't even paged into memory.

It knows that NoOp() is empty and can be elided (the call can go away), but it at least has to simulate the ldfla by poking the memory address of the struct, because there could be code depending on that NRE being raised. Memory dereferences are like if-statements: They can cause a branch, and failing to cause a branch may result in a broken program. Microsoft can't make assumptions and just say, "Your code shouldn't rely on that." Imagine the angry phone call to Microsoft if an NRE wasn't written to a business's error log just because the JITter decided it wasn't an "important enough" NRE to trigger in the first place. The JITter has no choice but to dereference that address at least once to ensure correct semantics.


Classes don't have any of these concerns; there's no enforced memory weirdness with a class. But structs, though, are quirkier.

like image 112
Sean Werkema Avatar answered Oct 23 '22 23:10

Sean Werkema