我有这个CIL代码序列,我通过使用Mono.Cecil
注入了这些代码。但是,修改后的.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
更改为clt
和br_true.s
,但它也无效。有谁知道是否有可能实现我的目标?感谢。
编辑: 根据ECMA-335,III.1.7.5,可能存在向后分支约束。不确定是否是这种情况。
特别是,如果单次通过分析到达指令,则将其称为位置X,即 紧跟在无条件分支之后,其中X不是早期分支的目标 指令,然后X的评估堆栈的状态,显然,不能从现有的派生 信息。在这种情况下,CLI要求X处的评估堆栈为空。
答案 0 :(得分:2)
您的IL-Code看起来没问题,但我认为CLR可能无法在方法完成后检查堆栈是否已损坏。当某些东西被推入堆栈时,CLR会检查该值是否也从堆栈中弹出。
因此,如果将3个值压入堆栈,则CLR可能无法检查循环是否正在运行三次,因此当方法返回时,CLR不知道堆栈上是否仍有值。
答案 1 :(得分:1)
非常有趣的问题。您正在尝试使用 IL 执行堆栈来存储任意数据项队列。与传统的 IL 代码相比,这引入了一种不寻常的条件,在这种情况下,堆栈的正确平衡严重取决于完全符合以下条件的 runtime 循环迭代次数:刻录到IL中的 ILAsm-time 数据项的数量。如您所述,该程序(此处重复)不起作用。
(实际上,在我的将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 simpleo̲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
]不是较早分支指令的目标。”
但是,不幸的是,这似乎并不是完整的解释,因为只要以一种可以静态识别的平衡方式移除排队的项目,评估堆栈显然就不会 / em> 必须在位置_more
为空。以下代码证明了这一点,尽管在 ECMA-335,III.1.7.5 的执行堆栈中有多个项目,但也可以正常工作,打印3个零。 >-易受攻击的位置_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方法中的指令_more
处-并且所有这些都在单个调用的范围内-执行堆栈上首先有2个,然后是1个,然后是0个“多余”项(请注意,我从您可能期望的迭代中减去了一个,这是因为我在刚开始使用“ excess ”一词来强调每个循环迭代中的 < / em>,则在位置_more
正确预期(并要求)一项 ,即用于假定的出队操作stloc cur
)。
我推测,要使 IL 方法有效,它的任何指令都不能使执行堆栈的主要深度发生变化。换句话说,必须有一个且只能是一个堆栈深度值,该值可以静态确定并分配给该方法的每个指令。凭直觉,否则这种情况可能会使 JIT 任务变得非常棘手,甚至可能无法实现。