简体   繁体   中英

C# IL - call constructor

I'm studying C# IL simple example and can't understand something. I've got very simple program:

void Main()
{
   C c = new C(1);
}
class C
{
   public C(){}
   public C(int i){}
}

there is CIL:

IL_0001:  ldc.i4.1    
IL_0002:  newobj      UserQuery+C..ctor
IL_0007:  stloc.0     // c

C..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  nop         
IL_0007:  nop         
IL_0008:  nop         
IL_0009:  ret         

C..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  nop         
IL_0007:  nop         
IL_0008:  nop         
IL_0009:  ret  

I don't understand, how virtual machine will distinguish which one constructor should be call. There is two same labels and the only difference seems to be in pushing argument in main. Is there something more deeper when calling constructor? Maybe compiler are providing some meta-data to distinguish which one should be called?

So let's assume this:

void Main()
{
  C c = new C(1);
}
class C
{
  public C(){}
  public C(int i){ i += 1;}
}

IL_0001:  ldc.i4.1    
IL_0002:  newobj      UserQuery+C..ctor
IL_0007:  stloc.0     // c

C..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  nop         
IL_0007:  nop         
IL_0008:  nop         
IL_0009:  ret         

C..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  nop         
IL_0007:  nop         
IL_0008:  ldarg.1     
IL_0009:  ldc.i4.1    
IL_000A:  add         
IL_000B:  starg.s     01 
IL_000D:  nop         
IL_000E:  ret   

Now, how to distinguish which one to call, at the label-level we cannot distinguish it.

The actual code identifies the constructor to be called by a MethodToken These are unique for each overload.

Your disassembler has an inadequate token-to-string conversion which is giving you only the name, which isn't unique, and cannot be assembled. In constrast, ildasm converts the token into a full signature which is able to roundtrip back to a working assembly (using ilasm ).

I went to make the experiment...

I used the following code:

class Program
{
    static void Main()
    {
        CallConstructorA();
        CallConstructorB();
    }

    static void CallConstructorA()
    {
        GC.KeepAlive(new C());
    }

    static void CallConstructorB()
    {
        GC.KeepAlive(new C(1));
    }
}

class C
{
    public C() { }
    public C(int i)
    {
        GC.KeepAlive(i);
    }
}

The following is MSIL got with Telerik JustDecompile for the class Program:

.class private auto ansi beforefieldinit Test.Program
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor () cil managed 
    {
        IL_0000: ldarg.0
        IL_0001: call instance void [mscorlib]System.Object::.ctor()
        IL_0006: ret
    }

    .method private hidebysig static void CallConstructorA () cil managed 
    {
        IL_0000: nop
        IL_0001: newobj instance void Test.C::.ctor()
        IL_0006: call void [mscorlib]System.GC::KeepAlive(object)
        IL_000b: nop
        IL_000c: ret
    }

    .method private hidebysig static void CallConstructorB () cil managed 
    {
        IL_0000: nop
        IL_0001: ldc.i4.1
        IL_0002: newobj instance void Test.C::.ctor(int32)
        IL_0007: call void [mscorlib]System.GC::KeepAlive(object)
        IL_000c: nop
        IL_000d: ret
    }

    .method private hidebysig static void Main () cil managed 
    {
        .entrypoint
        IL_0000: nop
        IL_0001: call void Test.Program::CallConstructorA()
        IL_0006: nop
        IL_0007: call void Test.Program::CallConstructorB()
        IL_000c: nop
        IL_000d: ret
    }
}

So you can see that the calls are different...

The first one says:

        IL_0001: newobj instance void Test.C::.ctor()

The second says:

        IL_0002: newobj instance void Test.C::.ctor(int32)

So, I'm gessing it is your decompiler that is not showing all the details of the intermediate code. In fact I did try similar code to the one above in LINQPad and there both call looks alike.


For the detail of how the annotation is done in binary... honestly I don't know.

I'm far from an expert in IL

easiest way to find out how, compile your example without passing 1 to the constructor

this line will go away: IL_0001: ldc.i4.1 which mean, i think, it will not pass an argument to the constructor.

do it again by passing 9 instead of 1, this IL_0001: ldc.i4.1 will be replaced by IL_0001: ldc.i4.s 9

Used the exact same code you provided.. I see following IL, which clearly indicates the overload of Constructor being called -

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       9 (0x9)
  .maxstack  1
  .locals init ([0] class ConsoleTest.C c)
  IL_0000:  nop
  IL_0001:  ldc.i4.1
  IL_0002:  newobj     instance void ConsoleTest.C::.ctor(int32)
  IL_0007:  stloc.0
  IL_0008:  ret
} // end of method Program::Main

Note the instance void ConsoleTest.C::.ctor(int32)

And about IL, when in doubt, one can and should always look at the documentation. Since you can Emit IL at runtime, most of the cases, documentation for Emit API will give you sufficient information. For example, in case of NewObj , it clear states that constructor ConstructorInfo is required for NewObj opcode

"Assembly Format newobj ctor "

"The following Emit method overload can use the newobj opcode: ILGenerator.Emit(OpCode, ConstructorInfo) "

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM