5

I have this sequence of CIL codes which I injected through the use of Mono.Cecil. However, the modified .NET C# application will not run.

Objective: Manually load and pop values from stack to display in Console.WriteLine

 for (int i = 0; i < 3; i++)
        {
            int z = some value popped manually from stack;                 
            Console.WriteLine(z);
        }

This is the simple main() program I modified:

.method private hidebysig static void Main(string[] args) cil managed
{

    .entrypoint
    .maxstack 5
    .locals init (
        [0] int32 num,
        [1] int32 num2)
    L_0000: ldc.i4.6 //manually push value 6 to stack
    L_0001: ldc.i4.5 //manually push value 5 to stack
    L_0002: ldc.i4.4 //manually push value 4 to stack
    L_0003: ldc.i4.0 //push int i initial value 0 to stack 
    L_0004: stloc.0 //pop and store to int i variable to variable num
    L_0005: br.s L_0013
    L_0007: nop 
    L_0008: stloc.1 //pop the pushed values 6,5 and 4 to variable num2
    L_0009: ldloc.1 //load value of num2 to stack
    L_000a: call void [mscorlib]System.Console::WriteLine(int32) //pop value of num2 and print
    L_000f: ldloc.0 //load previous value in variable num to stack
    L_0010: ldc.i4.1 //load incremental value 1 to stack
    L_0011: add //pop and add the top 2 values, result is pushed to stack
    L_0012: stloc.0 //store the new result to variable num. (int i)
    L_0013: ldloc.0 //push int i variable value to stack
    L_0014: ldc.i4.3 //push value 3 to stack as number of times to loop
    L_0015: blt.s L_0007 //branch less than (pop and cmp the top 2 values in stack)
    L_0017: ret 
}

However, the above code cannnot run. I tried changing blt.s to clt and br_true.s but it doesn't work either. Does anyone know if it is possible to attain my objective? Thanks.

EDIT: According to ECMA-335, III.1.7.5, there might be a backward branch constraint. Not sure if this is the case.

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.

4

2 回答 2

2

你的 IL 代码看起来不错,但我认为 CLR 可能无法在方法完成后检查堆栈是否损坏。当某些东西被压入堆栈时,CLR 会检查该值是否也从堆栈中弹出。

因此,如果您将 3 个值压入堆栈,CLR 可能无法检查您的循环是否运行了 3 次,因此当方法返回时,CLR 不知道堆栈上是否仍有值。

于 2012-10-17T09:20:48.233 回答
1

非常有趣的问题。您正在尝试使用IL执行堆栈来存储任意数据项的队列。与传统的IL代码相比,这引入了一种不寻常的情况,其中堆栈的正确平衡关键取决于运行时循环迭代的数量与烧入 IL的ILAsm 时间数据项的数量完全匹配。正如您所指出的,该程序(在此重复)不起作用。

(事实上​​,在我使用link.exewith/LTCG的构建中,链接器甚至无法生成程序集,给出“ fatal error C1352: Invalid or corrupt MSIL in function.”)

.method public static void ExecStackResidual()      // !!! FAILS - BAD EXAMPLE - NO !!!
{
    .locals init (int32 i, int32 cur)

    ldc.i4.6        // enqueue item  -- NO!
    ldc.i4.5        // enqueue item  -- NO!
    ldc.i4.4        // enqueue item  -- NO!

    ldc.i4.0
    stloc i
    br _next

_more:
    stloc cur       // de-queue item  -- NO!

    ldloc cur
    box int32
    call void Debug::WriteLine(object)

    ldloc i
    ldc.i4.1
    add
    stloc i

_next:
    ldloc i
    ldc.i4.3
    blt _more
    ret 
}

由于您的代码中有一个简单的逻辑缺陷或一个错误,问题是n̲o̲t̲ 。这可以通过以下事实证明有争议的部分工作正常,打印 3 个零。

    //-- ldc.i4.6       // commented out
    //-- ldc.i4.5       // commented out
    //-- ldc.i4.4       // commented out

    ldc.i4.0
    stloc i
    br _next

_more:
    //-- stloc cur      // commented out

    ldloc cur
    box int32
    call void Debug::WriteLine(object)

    ldloc i
    ldc.i4.1
    add
    stloc i

_next:
    ldloc i
    ldc.i4.3
    blt _more
    ret 

OP 做了一些调查并发现了ECMA-335, III.1.7.5,这似乎与这里有关,因为工作示例和失败示例之间的主要区别在于后者存在需要有一个非空的评估堆栈(又名“序列点”)在位置_more,并且该位置确实如此,以引用规范......

“...立即遵循无条件分支 [此处,br _next],其中 [ _more] 不是先前分支指令的目标。”

然而不幸的是,这似乎不是完整的解释,因为只要您以可以静态识别的平衡方式删除排队的项目,评估堆栈显然不必在 location 为空_more。下面的代码证明了这一点,它也可以正常工作,打印 3 个零,尽管ECMA-335, III.1.7.5 -vulnerable location的执行堆栈上有几个项目_more

    ldc.i4.6       // enqueue item  -- ok
    ldc.i4.5       // enqueue item  -- ok
    ldc.i4.4       // enqueue item  -- ok

    ldc.i4.0
    stloc i
    br _next

_more:
    //-- stloc cur      // de-queue item  -- still commented out

    ldloc cur
    box int32
    call void Debug::WriteLine(object)

    ldloc i
    ldc.i4.1
    add
    stloc i

_next:
    ldloc i
    ldc.i4.3
    blt _more

    pop         // de-queue item  -- required
    pop         // de-queue item  -- required
    pop         // de-queue item  -- required
    ret 

OP 还使用术语“向后分支约束”,但不清楚该短语是在规范中找到还是在原始贡献中找到。规范中出现“...较早的分支指令”这句话似乎可能暗示了这一点。无论哪种方式,它都会引发一个诱人的问题,即是否可以通过重新排列代码来避免错误,以便没有(技术上)与ECMA-335,III.1.7.5约束中的“早期”(技术性)匹配的位置。

一个相关的想法是规范中的“无条件分支”仅表示br家庭指令。为了解决这个问题br,我们可以在方法主体中嵌入一条ret指令,如下所示。正如您可能猜到的那样,这无济于事。尽管规范没有明确说明,但它显然打算ret作为“无条件分支”包含在内。这是常识,因此以下示例仍然不起作用

    // !!! FAILS - BAD EXAMPLE - NO
    ldc.i4.6        // enqueue item  -- NO!
    ldc.i4.5        // enqueue item  -- NO!
    ldc.i4.4        // enqueue item  -- NO!

    ldc.i4.0
    stloc i

_next:
    ldloc i
    ldc.i4.3
    blt _more
    ret 

_more:
    stloc cur       // de-queue item  -- NO! -- still follows an "unconditional branch"

    ldloc cur
    box int32
    call void Debug::WriteLine(object)

    ldloc i
    ldc.i4.1
    add
    stloc i
    br _next

总而言之,我认为你不会让这项技术发挥作用,因为它的基本要求是(a.)硬编码到IL中的事实(即,排队数据项的数量)必须完全符合 (b.) 需要运行时解释的事实(即循环迭代次数)。

我认为这个问题的一个更基本的总结,与ECMA描述相反,是所有失败的例子都意味着一个(或多个)方法指令所经历的执行堆栈上的项目数不是固定的,而是在方法执行时在不同时间获取不同的值,无论您如何实现,这是始终严格禁止的基本情况。对我来说,这似乎是更普遍的不可侵犯的约束。

例如,在_more演示方法中的指令处——并且都在单个调用的范围内——执行堆栈上将首先有 2 个,然后是 1,然后是 0 个“多余”项(请注意,我每次迭代都从你可能已经预料到了,这是因为我之前使用了“ excess ”这个词,试图强调这样一个事实,即在每个单独的循环迭代中,一个项目在 location正确预期的,也是必需的_more,即用于假定的出队操作stloc cur)。

我推测,为了使IL方法有效,它的任何指令都必须无法体验到执行堆栈的主要深度的变化。换句话说,必须有一个唯一的堆栈深度值,可以静态确定并分配给每个方法的指令。直观地说,否则这种情况可能会使JIT任务变得非常棘手,甚至可能被证明是不可能的。

于 2019-02-11T02:38:14.233 回答