C# .NET CIL对求值堆栈的操作
我通过使用C# .NET CIL对求值堆栈的操作,c#,.net,cil,mono.cecil,C#,.net,Cil,Mono.cecil,我通过使用Mono.Cecil注入了这个CIL代码序列。但是,修改后的.NET C#应用程序将不会运行 目标: 手动从堆栈加载并弹出值,以显示在控制台中。WriteLine for (int i = 0; i < 3; i++) { int z = some value popped manually from stack; Console.WriteLine(z); }
Mono.Cecil
注入了这个CIL代码序列。但是,修改后的.NET C#应用程序将不会运行
目标:
手动从堆栈加载并弹出值,以显示在控制台中。WriteLine
for (int i = 0; i < 3; i++)
{
int z = some value popped manually from stack;
Console.WriteLine(z);
}
但是,上述代码无法运行。我尝试将blt.s
更改为clt
和br_true.s
,但也不起作用。有人知道我的目标能否实现吗?谢谢。
编辑:
根据ECMA-335,III.1.7.5,可能存在向后分支约束。不确定是否是这样
特别是,如果单通道分析到达一条指令,将其称为位置X,即
紧跟在无条件分支之后,其中X不是早期分支的目标
指令,则X处计算堆栈的状态显然不能从现有的
信息。在这种情况下,CLI要求X处的计算堆栈为空
您的IL代码看起来不错,但我认为CLR可能无法在方法完成后检查堆栈是否损坏。当某些内容被推送到堆栈上时,CLR会检查该值是否也从堆栈中弹出
因此,如果将3个值推送到堆栈上,CLR可能无法检查循环是否运行了三次,因此当方法返回时,CLR不知道堆栈上是否还有值 非常有趣的问题。您正试图使用IL执行堆栈来存储任意数据项队列。与传统的IL代码相比,这引入了一种不寻常的情况,在这种情况下,堆栈的正确平衡主要取决于运行时循环迭代次数,该迭代次数与烧录到IL中的ILAsm时间数据项的数量完全匹配。正如您所指出的,该程序(此处重复)不起作用 (事实上,在我使用
link.exe
和/LTCG
的构建中,链接器甚至无法生成程序集,导致“致命错误C1352:函数中的MSIL无效或损坏”
)
问题在于n̲o̲t̲是由于一个简单的逻辑缺陷造成的,或者是由于代码中的一个bug造成的。这可以通过如下事实来证明:注释有争议的部分效果很好,打印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,这似乎与此相关,因为工作示例和失败示例之间的主要区别在于后者存在要求在\u more
位置有一个非空评估堆栈(也称为“序列点”),那个位置确实,引用规范
“…立即跟随无条件分支[此处,br\u next
],其中[\u more
]不是早期分支指令的目标。”
然而,不幸的是,这似乎不是完全的解释,因为只要您以一种可以静态识别的平衡方式删除排队的项目,计算堆栈显然不必在位置\u more
处为空。以下代码证明了这一点,也可以正常工作,打印3个零,尽管在执行堆栈上的ECMA-335,III.1.7.5-易受攻击位置\u 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描述相反,问题的一个更基本的总结是,所有失败示例都意味着执行堆栈上一条(或多条)方法指令所经历的项数不是固定的,相反,当方法执行时,在不同的时间获得不同的值,这是基本情况,无论您如何实现它,它都将被严格禁止。在我看来,这似乎是更普遍的不可侵犯的限制
例如,在demo方法中的指令\u more
中,在一次调用的范围内,执行堆栈上首先有2个,然后是1个,然后是0个“多余”项(请注意,我从您可能期望的每个迭代中减去一个,这是因为我使用了“多余”一词)在此之前,我们试图强调一个事实,即在每个单独的循环迭代中,在位置\u 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
// !!! 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