Почему localloc ломает этот метод CIL?

У меня есть следующий фрагмент сокращенного кода 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 был брошен для оригинального метода, из-за некоторого инструментария, сделанного во время выполнения. Мой подход к отладке этого, чтобы выяснить, что не так с инструментарием:

  • Разборка неисправной DLL с ildasm
  • Применение кода инструментария к методу сбоя
  • Воссоздание DLL из модифицированного IL с ilasm
  • Повторный запуск программы и проверка ее сбоя
  • Продолжайте постепенно сокращать код IL метода сбоя, вплоть до минимального сценария, вызывающего проблему (и стараясь не вносить ошибок по пути...)

К несчастью, peverify.exe /IL не указал ни одной ошибки. Я пытался утешить спецификацию ECMA и книгу Сержа Лидина Expert .NET IL, но не смог понять, что же не так.

Есть что-то основное, что я пропускаю?

Редактировать:

Я немного обновил код IL, чтобы сделать его более полным (без изменения инструкций). Второй блок инструкций, в том числе ldarg, newobj и т. д., берется из рабочего кода как есть - исходный код метода.

Что странно для меня, удалив либо localloc или же .try - finally код работает - но, насколько мне известно, ни один из них не должен изменять балансировку стека по сравнению с тем, присутствуют ли они в коде.

Вот код IL, декомпилированный в C# с помощью ILSpy:

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:

Больше наблюдений:
- Принимая localloc Блок кода IL и перемещение его в конец функции, код работает нормально - так что кажется, что код сам по себе в порядке.
- Эта проблема не воспроизводится при вставке аналогичного кода IL в функцию тестирования Hello World.

Я очень озадачен...

Хотелось бы, чтобы был способ получить больше информации из InvalidProgramException. Кажется, что CLR не присоединяет точную причину сбоя к объекту исключения. Я также думал об отладке с помощью отладочной сборки CoreCLR, но, к сожалению, отлаживаемая программа не совместима с ним...

1 ответ

Решение

К сожалению, кажется, что я нажал на ошибку 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 бросить InvalidProgramException в зависимости от наличия улова и его относительного расположения к localloc Похоже, тонкая ошибка.

Запуск SecAnnotate.exe (средство .NET Security Annotator) в сбойной DLL помог выявить проблемы безопасности между вызовами функций.

Подробнее о прозрачном коде безопасности:
https://docs.microsoft.com/en-us/dotnet/framework/misc/security-transparent-code

Другие вопросы по тегам