msil ".maxstack 1" pushes more than 1 value

418 views Asked by At

I have a working IL code:

.method public hidebysig static void Main(string[] args) cil managed
    {
        .entrypoint
        .maxstack  1
        ldc.i4.s 10
        ldc.i4.s 5
        ldc.i4.s 15
        ldc.i4.s 5
        add
        call void [mscorlib]System.Console::WriteLine(int32)
        ret
    }

I set .maxstack to 1 and pushed 4 values on the eval stack. Why does it work?

2

There are 2 answers

0
Hans Passant On BEST ANSWER

The CLI spec is fairly opaque about the intended usage of the .maxstack directive. Some hint that they foresaw the need for it but didn't nail down what the exact rules need to be. We can glean some insight in exactly how it is used from the jitter source that's included with the SSCLI20 distribution. The relevant C++ code in clr/src/fjit/fjit.cpp:

   /* set up the operand stack */
   size = methodInfo->maxStack+1; //+1 since for a new obj intr, +1 for exceptions
#ifdef _DEBUG
   size++; //to allow writing TOS marker beyond end;
#endif
   if (size > opStack_size) {
       if (opStack) delete [] opStack;
       opStack_size = size+4; //+4 to cut down on reallocations
       New(opStack,OpType[opStack_size]);
   }

Note the elbow-room they leave to avoid having to repeatedly re-allocate the operand stack data structure, size+4 is enough to explain your observation. That's not the only reason, it also matters what method was jitted before and if it had a large .maxstack then nothing goes wrong either.

Fjit.cpp is just a sample implementation, so no guarantee that this works the same way in the jitter you use. But otherwise enough to provide insight, the directive is there to help the jitter avoid having to solve the chicken-and-egg problem, having to allocate the data structure before jitting the code. It is an optimization.

It can bomb, there's of course no point in intentionally lying about it.

0
Brian Reichle On

The first thing that came to mind while reading your question was that the assembler is probably using the tiny method header (Partition II 25.4.2), rendering the .maxstack moot.

The IL of a method in a module is immediately preceded by the method header which comes in two flavours, fat and tiny. If a method has no locals, no exception handlers, less that 64 bytes of IL and a max stack of 8 or less, then it can use a tiny header. The tiny header does not specify the .maxstack, instead any method with a tiny header is assumed to have a .maxstack of 8.

If you really want to test what happens when overflowing the evaluation stack, then you should ensure the method does not use the tiny header by adding a local variable or exception handler.