Search code examples
.net.net-assemblydisassemblycil

Understanding CIL & the workings of ldelem.ref


I would like an explanation of what ldelem.ref does. So far I have that it loads the element at index onto the top of the stack as an O.

What is the index? And I am thinking that the type O means that the type of the object will remain whatever it was, for example if it was a string it will remain a string.

I have an example here below of some code I am working on and I would really appreciate some understanding. I commented what I believe I know. So in this case is what

.locals init (
    string V_0,
    bool V_1,
    string V_2,
    bool V_3,
    string V_4,
    string V_5,
    string V_6)              // Declared 6 variables

.try 
{
    IL_0000:  nop
    IL_0001:  nop                   // Does nothing - Debug build
    IL_0002:  ldarg.0               // Loads Argument 0 into memory/stack
    IL_0003:  ldc.i4.0              // Push Constant Value 0 into memory [Possibly from a variable]
    IL_0004:  ldelem.ref            // Loads element at index onto the top of the stack as an O
    IL_0005:  stloc.0               // Pop value from stack into local Variable 0
    IL_0006:  ldloc.0               // Load local variable 0 onto stack
    IL_0007:  ldstr      "del"      // Loads string "del" in to top of stack
    IL_000c:  call       bool [mscorlib]System.String::op_Equality(string, string)  // Compares strings to see if they are equal
    IL_0011:  stloc.1               // Pop value from stack into local variable 1
    IL_0012:  ldloc.1               // Load local variable 1 onto the stack
    IL_0013:  brfalse.s  IL_004e    // If variable 1 is true keep going else jump to IL_004e

What is ldelem.ref doing here? Is the op_Equality comparing the string "del" with the contents of variable 0? I am taking it that after the call is done, the Boolean value of the operation is then stored at the top of the stack and stloc.1 pops the Boolean value and stores it in variable 1, then ldloc.1 loads that variable onto the stack and brfalse.s checks the Bool value and if false "jumps" to IL_004e, is this the case?


Solution

  • ldelem.ref pushes a reference to an array element onto the stack. This means that it makes a copy of it and isn't the actual stored reference. It may be useful to read the System.Reflection.Emit documentation to understand more.

    Another thing that might be useful to understand is that each MSIL instruction requires N values from the stack, where N is determined by the specific instruction used (among other things). This can then be used to mentally group things for easier comprehension.

    IL_0002:  ldarg.0               
    IL_0003:  ldc.i4.0              
    IL_0004:  ldelem.ref 
    

    ldelem.ref requires the stack to have, in order: An array reference, an index into that array. It will pop those values, then push the reference onto the stack. The stack now contains a single thing.

    IL_0005:  stloc.0 
    

    The only value on the stack is now popped and put in local storage. The stack is empty.

    IL_0006:  ldloc.0               
    IL_0007:  ldstr      "del"      
    IL_000c:  call       bool [mscorlib]System.String::op_Equality(string, string)  
    

    The call instruction will pop as many items on the stack as parameters (including the instance, if the method invoked is an instance method). In this case, it needs two string parameters and will pop both the contents of the local variable as well as the literal string "del". The static method op_Equality returns a bool, so that will be pushed onto the stack. The stack now contains a single thing.

    IL_0011:  stloc.1               
    

    The only value on the stack is now popped and put in local storage. The stack is empty.

    IL_0012:  ldloc.1               
    IL_0013:  brfalse.s  IL_004e    
    

    The value is then loaded and branching logic applied.

    In C#, the MSIL is doing something equivalent to the following:

    if (array[0] == "del")
    

    Everything in MSIL is a balancing act. Due to this being a debug build, there may be more nop instructions and more use of locals than an optimized build (which is good for you, because you can follow things a little easier).