bool var0;
int var1 = 3 + (var0?0:1);
This would be translated to :
L0: ldc.i4.3
L1: ldloc.0
L2: brfalse.s L5
L3: ldc.i4.0
L4: br.s L6
L5: ldc.i4.1
L6 : add
L7: stloc.1
L8 :
According to your correction, I assume that the stack level at the
beginning of L6 should be 0, because it's just after a br.s ! But in
fact it's 1 ! (The code provided above is 100% valid, and does work on
mono of course)
I believe the right algorithm should process the code as follows :
Before L0 : 0
==> After L0 : 1
Before L1 : 1
==> After L1 : 2
Before L2 : 2
==> After L2 : 1 ==> Before L5 = 1
Before L3 : 1
==> After L3 : 2
Before L4 : 2
==> ==> Before L6 = 2
But we can't assume anything here about After L4
Before L5 = 1
==> After L5 = 2
Before L6 = 2
==> After L6 = 1
Before L7 = 1
==> After L7 = 0
Correct me if I'm wrong
I'll try in this post to give more details about how both Mono & .Net
compute stack depth at each instruction.
Mono only supports 100% valid IL code. Mono assumes that the stack
depth at any point can be computed using a single top-down pass. This
is not the case for .Net. Microsoft's implementation needs more than a
single pass for be able to compute the stack depth.
To clarify things, let me give you a simple example
L0 : ldc.i4.3
L1 : br.s L4
L2 : ldc.i4.5
L3 : br.s L5
L4: br.s L2
L5: add
L6 : stloc.0
(This code is not 100% valid, it would not run on Mono, but it works
fine on .Net)
This is how Mono would try to compute stack depth
Before L0: 0
==> Before L1: 1
Before L1:1
==> Before L4 : 1
Before L2 unkown ==> We assume it is 0
==> Before L3 : 1
Before L3 : 1
==> Before L5 : 1
Before L4 : 1
==> Before L2 : 1 ===> ERROR, because we already assumed that Before
L2 is 0 !!!!
Now, this is how .Net would compute stack depth :
Before L0: 0
==> Before L1: 1
Before L1:1
==> Before L4 : 1
Before L4 : 1
==> Before L2 : 1
Before L2 : 1
==> Before L3 : 2
Before L3 : 2
==> Before L5 : 2
Before L5 : 2
==> Before L6 : 1
Before L6 : 1
==> AFTER L6 : 0
I hope this can help you see how it is actually done.
I've tried to make my own corrections to provide a .Net compatible
implementation. It seams to work well :)
I've included a patch file : CodeWriter.patch
The patch includes some other optimizations to avoid the use of
hashtables and int casts : knowing that instruction offsets are not
yet used, I used them as follows:
for (int i = 0; i < instructionCount; i++)
instructions[i].Offset = i;
As you can see, now the Offset property contains the sequential index
of the instruction.
According to my benchmarks, this optimization divides the processing
time by a factor of 2.
PS. Sorry for my potentially bad English
let's have a look to your 'misterious point' example:
L0: call Before
L1: br COND
LOOP: call Body
COND: call Condition
L2: brtrue LOOP
L3: call After
Here how my algorithm works (& I suppose the .Net does the same) :
Before L0 : 0
==> before L1 : C1 (where C1 is a constant depending on L0 instructions)
Before L1 : C1
==> Before COND : C1
before COND : C1
==> before L2 : C2 (C2 is another know constant)
before L2 : C2
==> before LOOP : C3 = C2 - 1 (Here, we have the actual stack depth
of your mysterious point !!!!!)
==> before L3 : C3 = C2 - 1
...etc.
So when we start, we suppose all the points are "mysterious points"
We mark the first instruction as non mysterious with stack depth = 0;
We mark the first instruction of every exception handling block as non
mysterious with stack depth = 0 or 1 (depending of the type of the
block);
then we should start by traversing NON-mysterious points first, and
from them we can know about stack depth of other points.
In case at a certain point, all remaining points are mysterious, than
we take the first one, and assume it has a stack depth of 0.
br.s next
loop : ldc.i4.0
L0: br.s loop
next:
The loop is not referenced from outside. It's a kind of a "dead code".
The loop itself is invalid :
Let's suppose that loop has a stack depth, let's say Unkown1
Before loop : Unkown1
==> after loop : Unkown2 = Unkown1 + 1
before L0 : Unkown2
==> before loop = Unkown2 !!! Which is absurd because Unkown2 = Unkown1 + 1 !!!
In fact, Microsoft's implementation simply ignores unreachable or dead
code (The code actually runs perfectly on .Net)
thus, I believe that you're right about the fact that a single pass
should be enough, but we would have to IGNORE dead instructions.
I think it's sufficient if Cecil is able to read those assemblies - then
one could write a clean-up step (e.g. dead code removal) to fix the
invalid assembly before saving it.
Daniel Grunwald
In fact, the algorithm is not supposed to check all possible paths:
1. As you said, any "No self-respecting compiler would produce such code."
2. We are not developing a PEVerify-like tool.
3. We need to keep the ComputeMaxStack method as simple as possible.
Handling all these cases would add a lot of complexity to the code.
4. We need to keep performance issues in mind...
I agree with you about the fact that if there is a conflict detected,
we probably should throw an exception. Or we can just use a very large
stack size (like methodBody.MaxStack = 100)
Daniel,
For dead code, we cannot just remove it ! But we still have to visit
"dead code" in a top-down fashion (and we should assume that it starts
with a 0 stack depth, because this is how Mono would validate the
stack depth). Have a look at the following code :
L0: ldc.i4.1
L1: br.s L6
L2: ldc.i4.2
L3: ldc.i4.3
L4: add
L5: br.s L7
L6: br.s L3
L7: stloc.0
L8: ...
This code is 100% valid, it has 1 single dead instruction at L2.
If you remove this instruction, your code would be no longer valid !!!!
In fact this is a very good feature that I'm willing to use in my
obfuscator to produce 100% valid code than would have been decompiled
:
int i = 2 + 3;
instead of
int i = 1 + 3;
All decompilers would crash or produce bad code.
Definitely.
--
Jb Evain <j...@nurv.fr>
When browsing the code is a top-down fashion, we can't know any thing
about the stack depth before L3 ! Why are you supposing it has a stack
depth of 1 ?!!!
In fact, since it's not reachable from any previous exception, then It
just has an "unknown stack depth".
=============== ECMA-335, Partition III
1.7.5 Backward branch constraints
It shall be possible, with a single forward-pass through the CIL
instruction stream for any method, to infer the exact state of the
evaluation stack at every instruction (where by “state” we mean the
number and type of each item on the evaluation stack).
In particular, if that single-pass analysis arrives at an instruction,
call it location X, that immediately follows an unconditional branch,
and where X is not the target of an earlier branch instruction, then
the state of the evaluation stack at X, clearly, cannot be derived
from existing information. In this case, the CLI demands that the
evaluation stack at X be empty.
Following on from this rule, it would clearly be invalid CIL if a
later branch instruction to X were to have a
non-empty evaluation stack
===============
> if it were my decision, I would throw an exception that the code is
> invalid, when computing the max stack size.
Actually, I've been using of modified version of the patch I submitted
for a little while :
private void MarkNextInstruction(IntegerQueue visitedInstructions,
int[] stackSizes, int instructionIndex, int stackDepth)
{
if (instructionIndex >= stackSizes.Length)
return;
int curStackDepth = stackSizes[instructionIndex];
if (curStackDepth == -1)
{
visitedInstructions.Add(instructionIndex);
stackSizes[instructionIndex] = stackDepth;
}
else if (curStackDepth != stackDepth)
throw new Exception("Invalid IL code.");
}
By throwing exceptions when detecting a wrong instruction depth, it
helped me a lot emitting correct IL code. No need to use PEVerify
every time I introduces a small change in my code.