Is an empty evaluation stack required before an exception block?
Asked Answered
B

1

5

When I remove Ldstr "a" and Call Console.WriteLine (before Ret), the code runs fine, otherwise an InvalidProgramException is thrown upon invocation. Does this mean that an empty evaluation stack is required?

class Program
{
    delegate void Del();

    static void Main(string[] args)
    {
        DynamicMethod dynamicMethod = new DynamicMethod("", null, Type.EmptyTypes);
        ILGenerator ilGen = dynamicMethod.GetILGenerator();
        ilGen.Emit(OpCodes.Ldstr, "a");

        ilGen.BeginExceptionBlock();
        ilGen.Emit(OpCodes.Ldstr, "b");
        ilGen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null));
        ilGen.BeginCatchBlock(typeof(Exception));
        ilGen.EndExceptionBlock();

        ilGen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null));
        ilGen.Emit(OpCodes.Ret);

        ((Del)dynamicMethod.CreateDelegate(typeof(Del))).Invoke();
    }
}
Bodwell answered 30/10, 2014 at 16:27 Comment(1)
I suspect this is the case (that the evaluation stack must be empty before introducing a new exception frame) but I can't locate an official reference right now.Ironmonger
H
7

To understand what you're doing, I suggest you to make as minimal example as possible.

ilGen.Emit(OpCodes.Ldstr, "a");

ilGen.BeginExceptionBlock();
ilGen.BeginCatchBlock(typeof(Exception));
ilGen.EndExceptionBlock();

ilGen.Emit(OpCodes.Pop);
ilGen.Emit(OpCodes.Ret);

After that, you can use AssemblyBuilder to dump the given code into the executable. If that's done, ildasm will show what has been generated.

// Code size       17 (0x11)
  .maxstack  2
  IL_0000:  ldstr      "a"
  .try
  {
    IL_0005:  leave      IL_000f
  }  // end .try
  catch [mscorlib]System.Exception 
  {
    IL_000a:  leave      IL_000f
  }  // end handler
  IL_000f:  pop
  IL_0010:  ret

As you can see, we will reach to the leave instruction which jumps to pop. You can then google about leave, which states that:

The leave instruction is similar to the br instruction, but it can be used to exit a try, filter, or catch block whereas the ordinary branch instructions can only be used in such a block to transfer control within it. The leave instruction empties the evaluation stack and ensures that the appropriate surrounding finally blocks are executed.

However, why doesn't the following work then?

ilGen.Emit(OpCodes.Ldstr, "a");

ilGen.BeginExceptionBlock();
ilGen.BeginCatchBlock(typeof(Exception));
ilGen.EndExceptionBlock();

//ilGen.Emit(OpCodes.Pop);
ilGen.Emit(OpCodes.Ret);

I suspect it might not be "physical limit", but a verification issue. Let's run peverify ourapp.exe and see what we get:

[IL]: Error: [C:\temp\test.exe : Program::Main][offset 0x00000005] Attempt to en
ter a try block with nonempty stack.
1 Error(s) Verifying C:\temp\test.exe

At this point, you might be like, wat? With a little bit of googling, you can come up with an error code of 0x801318A9. Quick scan through SSCLI2.0 sources:

case ReaderBaseNS::RGN_TRY:
    // Entering a try region, the evaluation stack is required to be empty.
    if (!m_readerStack->empty()) {
        BADCODE(MVER_E_TRY_N_EMPTY_STACK);                    
    }
    break;

Now, this is cool, but if you're geeky, you might wonder why does the evaluation stack has to be empty?

For that, you probably want to take a look at ECMA C# and Common Language Infrastructure Standards. I suspect you could find the reason from PartitionIII CIL.pdf

Hampson answered 30/10, 2014 at 20:18 Comment(1)
peverify, awesome!Dodds

© 2022 - 2024 — McMap. All rights reserved.