为什么 localloc 会破坏这个 CIL 方法?

Why does localloc break this CIL method?

我有以下一段简化的 CIL 代码。
执行此 CIL 方法时,CLR 将抛出 InvalidProgramException

  .method assembly hidebysig specialname rtspecialname 
          instance void  .ctor(class [mscorlib]System.Collections.Generic.IEnumerable`1<class System.Windows.Input.StylusDeviceBase> styluses) cil managed
  {
    .locals init (class [mscorlib]System.Collections.Generic.IEnumerator`1<class System.Windows.Input.StylusDeviceBase> V_0,
         class System.Windows.Input.StylusDeviceBase V_1)

    ldc.i4.8   // These instructions cause CIL to break 
    conv.u     //
    localloc   //
    pop        //

    ldarg.0
    newobj instance void class [mscorlib]System.Collections.Generic.List`1<class System.Windows.Input.StylusDevice>::.ctor()
    call   instance void class [mscorlib]System.Collections.ObjectModel.ReadOnlyCollection`1<class System.Windows.Input.StylusDevice>::.ctor(class [mscorlib]System.Collections.Generic.IList`1<!0>)
    ldarg.1
    callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<class System.Windows.Input.StylusDeviceBase>::GetEnumerator()
    stloc.0

    .try
    {
       leave.s IL_0040
    }
    finally
    {
       endfinally
    }   

    IL_0040: ret
  } // end of method StylusDeviceCollection::.ctor

我的问题是,为什么这个CIL代码无效?

几个观察结果:
- 如果删除 localloc,代码运行正常。据我所知,localloc 用地址替换堆栈上的参数大小,因此堆栈保持平衡,AFAICT。
- 如果移除 try 和 finally 块,代码运行正常。
- 如果包含 localloc 的第一个指令块移动到 try-finally 块之后,代码运行正常。

所以它看起来像是 localloc 和 try-finally 的组合。

一些背景:

我在 InvalidProgramException 为原始方法抛出之后达到了这一点,这是由于在运行时进行的一些检测。我的调试方法是:

遗憾的是,peverify.exe /IL没有指出任何错误。 我试图对照 ECMA 规范和 Serge Lidin 的专家 .NET IL 书籍,但无法弄清楚哪里出了问题。

我缺少一些基本的东西吗?

编辑:

我稍微更新了有问题的 IL 代码,使其更完整(没有修改指令)。第二块指令,包括 ldargnewobj 等,原样取自工作代码——原始方法代码。

对我来说奇怪的是,通过删除 localloc.try-finally,代码可以工作 - 但据我所知,其中的 none 应该与它们是否存在于代码中相比,更改堆栈的平衡。

这是使用 ILSpy 反编译为 C# 的 IL 代码:

internal unsafe StylusDeviceCollection(IEnumerable<StylusDeviceBase> styluses)
{
    IntPtr arg_04_0 = stackalloc byte[(UIntPtr)8];
    base..ctor(new List<StylusDevice>());
    IEnumerator<StylusDeviceBase> enumerator = styluses.GetEnumerator();
    try
    {
    }
    finally
    {
    }
}

编辑 2:

更多观察:
- 获取 IL 代码的 localloc 块,并将其移动到函数的末尾,代码运行良好 - 因此代码本身似乎没问题。
- 将类似的 IL 代码粘贴到 hello world 测试函数时,问题不会重现。

我很纳闷...

我希望有一种方法可以从 InvalidProgramException 中获取更多信息。似乎 CLR 没有将确切的失败原因附加到异常对象。 我也考虑过使用 CoreCLR 调试版本进行调试,但不幸的是我正在调试的程序与它不兼容...

遗憾的是,我似乎遇到了 CLR 错误...

使用旧版 JIT 编译器时一切正常:

set COMPLUS_useLegacyJit=1

我无法找出可能导致此问题的特定 RyuJit 设置。 我遵循了这篇文章中的建议:
https://github.com/Microsoft/dotnet/blob/master/Documentation/testing-with-ryujit.md

感谢所有帮助过的人!

后果:

有时在我遇到遗留的 JIT 解决方法后,我意识到只有在将 localloc(这是一个不可验证的操作码)检测到从安全透明方法。只有在这种情况下,RyuJit 会抛出 InvalidProgramException,而 Legacy JIT 不会。

在我的复现中,我对有问题的DLL进行了反汇编和重组,并直接修改了功能代码,保持了安全属性的完整性—— 特别是 AllowPartiallyTrustedCallers 程序集属性 - 这解释了为什么没有用一个孤立的例子重现这个问题。

可能是因为在 RyuJIT 中与 Legacy JIT 相比有一些安全强化,这暴露了这个问题,但是,localloc 的事实仍然会导致 CLR 在try-catch 的存在及其与 localloc 的相对位置似乎是一个微妙的错误。

运行 SecAnnotate.exe (.NET Security Annotator tool) 在失败的 DLL 上有助于揭示函数调用之间的安全问题。

有关安全透明代码的更多信息:
https://docs.microsoft.com/en-us/dotnet/framework/misc/security-transparent-code