Сборка MSIL: неожиданное исключение OutOfMemoryException в конструкторе класса

Я пишу компилятор, который выводит сборки .NET (используя Mono.Cecil, хотя я не верю, что Cecil имеет отношение к этой проблеме). Одна из возможностей компилятора требует, чтобы у класса был сгенерированный компилятором вложенный класс с некоторыми вспомогательными методами; внешний класс имеет статическое поле, поэтому каждый класс фактически имеет синглтон, ссылающийся на объект вложенного класса. Чтобы инициализировать это, любой такой класс имеет конструктор класса для создания экземпляра вложенного класса и сохранения его в поле.

Проблема: когда мой внешний класс является универсальным классом, я также делаю вложенный класс универсальным (поскольку он должен создавать объекты внешнего класса). Сгенерированный IL проходит через peverify нормально и выглядит хорошо, на мой взгляд, но конструктор класса, создающий экземпляр вложенного класса, выбрасывает OutOfMemoryException во время выполнения.

Я разобрал сборку с помощью ildasm, уменьшил ее до минимального воспроизведения (к сожалению, все еще ~180 строк IL) и убедился, что компиляция IL с помощью ilasm создает исполняемый файл, который все еще демонстрирует проблему.

Отладка с помощью Visual Studio или MDbg меня не просветила - я просто получаю OutOfMemoryException без указания почему. Я готов поверить, что мой IL каким-то образом недействителен, но peverify не указывает на проблему. Кто-нибудь может подсказать, в чем проблема?

// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly extern System
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly Repro1
{
  .ver 0:0:0:0
}
.module Repro1
// MVID: {7DA983B6-F5EA-4ACB-8443-C29F25ADDCD4}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x016E0000

.class public abstract auto ansi sealed Repro1
       extends [mscorlib]System.Object
{
  .method assembly static void  '<NemeaProgram>'() cil managed
  {
    .entrypoint
    // Code size       6 (0x6)
    .maxstack  0
    IL_0000:  call       void Rep2::Go()
    IL_0005:  ret
  } // end of method Repro1::'<NemeaProgram>'

} // end of class Repro1

.class public abstract auto ansi sealed Rep1
       extends [mscorlib]System.Object
{
  .class auto ansi nested public TRep
         extends [mscorlib]System.Object
  {
    .class auto ansi nested public '__%NemeaVType'
           extends [mscorlib]System.Object
    {
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // Code size       7 (0x7)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
        IL_0006:  ret
      } // end of method '__%NemeaVType'::.ctor

      .method public newslot virtual instance class Rep1/TRep 
              Create(string Foo) cil managed
      {
        // Code size       10 (0xa)
        .maxstack  8
        IL_0000:  ldarg      Foo
        IL_0004:  newobj     instance void Rep1/TRep::.ctor(string)
        IL_0009:  ret
      } // end of method '__%NemeaVType'::Create

    } // end of class '__%NemeaVType'

    .field famorassem string FData
    .field public static class Rep1/TRep/'__%NemeaVType' '__%NemeaVTypeI'

    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(string Foo) cil managed
    {
      // Code size       17 (0x11)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
      IL_0006:  ldarg.0
      IL_0007:  ldarg      Foo
      IL_000b:  stfld      string Rep1/TRep::FData
      IL_0010:  ret
    } // end of method TRep::.ctor

    .method privatescope specialname rtspecialname static 
            void  '.cctor$PST0600004C'() cil managed
    {
      // Code size       11 (0xb)
      .maxstack  8
      IL_0000:  newobj     instance void Rep1/TRep/'__%NemeaVType'::.ctor()
      IL_0005:  stsfld     class Rep1/TRep/'__%NemeaVType' Rep1/TRep::'__%NemeaVTypeI'
      IL_000a:  ret
    } // end of method TRep::.cctor

  } // end of class TRep

  .class auto ansi nested public TItem
         extends [mscorlib]System.Object
  {
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor() cil managed
    {
      // Code size       7 (0x7)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
      IL_0006:  ret
    } // end of method TItem::.ctor

  } // end of class TItem

} // end of class Rep1

.class public abstract auto ansi sealed Rep2
       extends [mscorlib]System.Object
{
  .class auto ansi nested public TRep<(Rep1/TItem) T>
         extends Rep1/TRep
  {
    .class auto ansi nested public '__%NemeaVType'<(Rep1/TItem) T_vt>
           extends Rep1/TRep/'__%NemeaVType'
    {
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // Code size       7 (0x7)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void Rep1/TRep/'__%NemeaVType'::.ctor()
        IL_0006:  ret
      } // end of method '__%NemeaVType'::.ctor

      .method public virtual instance class Rep1/TRep 
              Create(string Foo) cil managed
      {
        // Code size       10 (0xa)
        .maxstack  8
        IL_0000:  ldarg      Foo
        IL_0004:  newobj     instance void class Rep2/TRep<!T_vt>::.ctor(string)
        IL_0009:  ret
      } // end of method '__%NemeaVType'::Create

    } // end of class '__%NemeaVType'

    .field public static class Rep2/TRep/'__%NemeaVType'<!T> '__%NemeaVTypeI'
    .method public hidebysig specialname rtspecialname 
            instance void  .ctor(string Foo) cil managed
    {
      // Code size       22 (0x16)
      .maxstack  8
      IL_0000:  ldarg.0
      IL_0001:  ldarg      Foo
      IL_0005:  call       instance void Rep1/TRep::.ctor(string)
      IL_0015:  ret
    } // end of method TRep::.ctor

    .method privatescope specialname rtspecialname static 
            void  '.cctor$PST06000055'() cil managed
    {
      // Code size       11 (0xb)
      .maxstack  8
      IL_0000:  newobj     instance void class Rep2/TRep/'__%NemeaVType'<!T>::.ctor()
      IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!T> Rep2/TRep::'__%NemeaVTypeI'
      IL_000a:  ret
    } // end of method TRep::.cctor

  } // end of class TRep

  .method public static void  Go() cil managed
  {
    // Code size       29 (0x1d)
    .maxstack  1
    .locals init ([0] class Rep1/TRep R)
    IL_0000:  ldstr      "Oi"
    IL_0005:  newobj     instance void class Rep2/TRep<class Rep1/TItem>::.ctor(string)
    IL_000a:  stloc      R
    IL_001c:  ret
  } // end of method Rep2::Go

} // end of class Rep2

person DJC_ksd    schedule 23.08.2016    source источник
comment
Ну, что я делаю в таком случае, так это создаю эквивалентный код C#, компилирую его, декомпилирую обратно в MSIL и смотрю, в чем разница :) Конечно, вы можете уже найти ошибку при написании эквивалентного кода C#, но это только бонусные баллы.   -  person Luaan    schedule 23.08.2016
comment
Однако в этом случае я думаю, что проблема в том, что вложенный класс должен иметь аргумент того же типа, что и родительский класс. Вы объявляете аргумент нового типа T_vt без использования родительского T, что определенно не то, что вам нужно, и, вероятно, ошибка, которая может вызывать исключение, которое вы видите (поскольку ваш вложенный класс должен использовать все аргументы универсального типа своего родительского класса).   -  person Luaan    schedule 23.08.2016
comment
Нет, я думаю, что я делаю с аргументами типа вложенного класса правильно. Он объявлен со своим собственным аргументом типа T_vt, и когда внешний класс создает его экземпляр, он делает это, используя newobj instance void class Rep2/TRep/'__%NemeaVType'‹!T›::.ctor() - т.е. передает собственный аргумент типа ‹!T› в качестве аргумента типа во вложенный класс. Таким образом, вложенный класс будет создан с использованием того же фактического аргумента типа. Я попробую воспроизвести на C#, хотя некоторые вещи, которые я делаю, я не уверен, как они будут отображены на C#.   -  person DJC_ksd    schedule 23.08.2016
comment
... хотя оказывается, что вы совершенно правы, вложенный класс действительно должен напрямую использовать аргумент родительского типа. Я подозреваю, что это ответ (хотя раздражает, что peverify не выделяет его!) - если я подтвержу это, я опубликую соответствующий ответ. Спасибо.   -  person DJC_ksd    schedule 23.08.2016
comment
Ну, AFAIK, PEVerify должен только гарантировать, что код IL является управляемо-безопасным, то есть он не позволяет частично доверенному коду делать небезопасные вещи (в основном это означает предотвращение таких проблем, как дисбаланс стека, нарушение безопасности типов и т. д., ). На самом деле он не предназначен для проверки правильности кода - это невозможно даже теоретически. Было бы неплохо, если бы он мог вызывать больше распространенных ошибок, но на самом деле цель инструмента не в этом. Может быть, есть какие-то инструменты, которые помогут вам в этом, но я не знаю ни одного :)   -  person Luaan    schedule 23.08.2016


Ответы (1)


Итак, оказывается, что проблема не в универсальных объявлениях — с объявлением вложенного класса все в порядке. Имя универсального параметра не соответствует универсальному параметру внешнего класса, но это всего лишь соглашение, которого компилятор C# (по понятным причинам) придерживается при распространении универсальных параметров во вложенные классы.

Проблема заключается просто в

 IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!T> Rep2/TRep::'__%NemeaVTypeI'

строка - это недопустимо, потому что он пытается получить доступ к полю в классе Rep2/TRep, который является универсальным, без предоставления каких-либо аргументов типа. Изменение этого на

 IL_0005:  stsfld     class Rep2/TRep/'__%NemeaVType'<!0> Rep2/TRep<!T>::'__%NemeaVTypeI'

решает все вопросы.

Я по-прежнему считаю, что peverify могла выделить это, потому что на самом деле это недействительно — оно не может быть выполнено правильно, поскольку это недопустимая ссылка на поле. Также немного раздражает то, что при выполнении кода вы получаете исключение OutOfMemoryException, а не что-то более подробное.

person DJC_ksd    schedule 23.08.2016