【问题标题】:.NET CIL manipulation of evaluation stack.NET CIL 操作评估堆栈
【发布时间】:2012-10-07 05:57:15
【问题描述】:

我有通过使用Mono.Cecil 注入的这个CIL 代码序列。但是,修改后的 .NET C# 应用程序将无法运行。

目标: 手动从堆栈中加载和弹出值以显示在Console.WriteLine

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

这是我修改的简单 main() 程序:

.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 
}

但是,上面的代码无法运行。我尝试将blt.s 更改为cltbr_true.s,但它也不起作用。有谁知道是否有可能实现我的目标?谢谢。

编辑: 根据 ECMA-335, III.1.7.5,可能存在后向分支约束。不知道是不是这样。

特别是,如果单遍分析到达一条指令,称为位置 X,那 紧跟在无条件分支之后,并且 X 不是先前分支的目标 指令,那么 X 处的评估堆栈的状态显然不能从现有的 信息。在这种情况下,CLI 要求 X 处的评估堆栈为空。

【问题讨论】:

  • 运行程序时遇到的实际错误是什么?您是否尝试在修改后的程序上运行 peverify?
  • 您给验证者方式带来了太多困难,无法检查堆栈是否平衡。它必须深入研究代码以分析循环运行的频率。它不会那样做。

标签: c# .net cil mono.cecil


【解决方案1】:

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

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

【讨论】:

  • 我怀疑是因为堆栈在过程中是不稳定的,并且必须在“调用”之前清空。太习惯二进制汇编提供的灵活性了。
【解决方案2】:

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

(事实上,在我使用link.exe/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] 不是先前分支指令的目标。”

不幸的是,这似乎不是完整的解释,因为只要您以可以静态识别的平衡方式移除排队的项目,评估堆栈显然 不会 在位置 _more 处必须为空。以下代码证明了这一点,该代码也可以正常工作,打印 3 个零,尽管在 ECMA-335, III.1.7.5的执行堆栈上有多个项目>-易受攻击的位置_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 还使用术语“向后分支约束”,但不清楚该短语是在规范中还是在原始贡献中找到的。规范中出现“...earlier 分支指令”这句话似乎可能暗示了这一点。无论哪种方式,它都会引发一个诱人的问题,即是否可以通过重新排列代码来避免错误,以便没有(技术上)与 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”这个词,试图强调在每个单独的循环迭代中,一个项目在位置_more,即用于假定的出队操作stloc cur的正确预期和必需的)。

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

【讨论】:

    猜你喜欢
    • 2019-07-23
    • 1970-01-01
    • 2021-03-08
    • 2021-05-02
    • 2015-02-14
    • 2023-03-11
    • 2023-03-17
    • 1970-01-01
    • 2015-01-01
    相关资源
    最近更新 更多