Компилятор генерирует бесконечный цикл после окончательного блокирования, когда

Я использую стандартный компилятор VS2015, предназначенный для .Net 4.6.2.

Компилятор испускает бесконечный цикл после неудачного окончательного блока.

Некоторые примеры:

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    IL_0002: nop
    IL_0003: leave.s IL_000c
} // end .try
finally
{
    IL_0005: nop
    IL_0006: br.s IL_000a
    // loop start (head: IL_000a)
        IL_0008: nop
        IL_0009: nop
        IL_000a: br.s IL_0008
    // end loop
} // end handler
// loop start (head: IL_000c)
    IL_000c: br.s IL_000c
// end loop

Release:

  .try
    {
        IL_0000: leave.s IL_0004
    } // end .try
    finally
    {
        // loop start
            IL_0002: br.s IL_0002
        // end loop
    } // end handler
    // loop start (head: IL_0004)
        IL_0004: br.s IL_0004
    // end loop

Код источника С#

    private void _Simple()
    {
        try
        {

        }
        finally
        {
            for (;;) { }
        }
    }

Как вы видите на IL_000c - бесконечный цикл (сгенерированный компилятором)

Хорошо, теперь я покажу вам немного расширенный случай

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: nop
        IL_0004: leave.s IL_000d
    } // end .try
    finally
    {
        IL_0006: nop
        IL_0007: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_000c: throw
    } // end handler
    // loop start (head: IL_000d)
        IL_000d: br.s IL_000d
    // end loop
} // end .try
finally
{
    IL_000f: nop
    IL_0010: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_0015: throw
} // end handler

Release:

.try
{
    .try
    {
        IL_0000: leave.s IL_0008
    } // end .try
    finally
    {
        IL_0002: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_0007: throw
    } // end handler
    // loop start (head: IL_0008)
        IL_0008: br.s IL_0008
    // end loop
} // end .try
finally
{
    IL_000a: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_000f: throw
} // end handler

После того, как вложенный окончательный бесконечный цикл сгенерирован еще раз, но после второго, наконец, нет. (IL_000d)

Источник С#

    private void _DoubleFinallyWithThrowingNewException()
    {
        try
        {
            try
            {

            }
            finally
            {
                throw new Exception();
            }
        }
        finally
        {
            throw new Exception();
        }
    }

Еще раз, теперь есть явное исключение, вызванное методом, вызванным в конце блока.

Debug:

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: nop
        IL_0004: leave.s IL_0010
    } // end .try
    finally
    {
        IL_0006: nop
        IL_0007: ldarg.0
        IL_0008: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_000d: nop
        IL_000e: nop
        IL_000f: endfinally
    } // end handler

    IL_0010: nop
    IL_0011: leave.s IL_001d
} // end .try
finally
{
    IL_0013: nop
    IL_0014: ldarg.0
    IL_0015: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
    IL_001a: nop
    IL_001b: nop
    IL_001c: endfinally
} // end handler

IL_001d: ret

Release:

    .try
{
    .try
    {
        IL_0000: leave.s IL_0010
    } // end .try
    finally
    {
        IL_0002: ldarg.0
        IL_0003: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0008: endfinally
    } // end handler
} // end .try
finally
{
    IL_0009: ldarg.0
    IL_000a: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
    IL_000f: endfinally
} // end handler

IL_0010: ret

Источник С#

    private void ThrowException()
    {
        throw new Exception();
    }

    private void _DoubleFinallyWithThrowingNewExceptionNotInline()
    {
        try
        {
            try
            {

            }
            finally
            {
                ThrowException();
            }
        }
        finally
        {
            ThrowException();
        }
    }

Почему после генерации первого недостижимого окончательного блока создается бесконечный цикл?

Почему EndFinally OpCode не сгенерирован?

@Edit 1

Добавлен несколько msil в режиме Release.

@Edit 2

Добавлен пример с непустым исключением try

Переменная метаданных .maxStack, установленная в 1, и существующие .local переменные немного запутывают - код не связан с этими переменными.

Debug:

.maxstack 1
.locals init (
    [0] object someVar,
    [1] valuetype [mscorlib]System.DateTime
)

IL_0000: nop
.try
{
    IL_0001: nop
    .try
    {
        IL_0002: nop
        IL_0003: ldarg.0
        IL_0004: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0009: nop
        IL_000a: nop
        IL_000b: leave.s IL_0014
    } // end .try
    finally
    {
        IL_000d: nop
        IL_000e: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_0013: throw
    } // end handler
    // loop start (head: IL_0014)
        IL_0014: br.s IL_0014
    // end loop
} // end .try
finally
{
    IL_0016: nop
    IL_0017: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_001c: throw
} // end handler

Предыдущий объект [0] был пропущен, но DateTime все еще существует. Выпуск:

.maxstack 1
.locals init (
    [0] valuetype [mscorlib]System.DateTime
)

.try
{
    .try
    {
        IL_0000: ldarg.0
        IL_0001: call instance void System.Reflection.Emit.FactoryTests::ThrowException()
        IL_0006: leave.s IL_000e
    } // end .try
    finally
    {
        IL_0008: newobj instance void [mscorlib]System.Exception::.ctor()
        IL_000d: throw
    } // end handler
    // loop start (head: IL_000e)
        IL_000e: br.s IL_000e
    // end loop
} // end .try
finally
{
    IL_0010: newobj instance void [mscorlib]System.Exception::.ctor()
    IL_0015: throw
} // end handler`

С#:

private void _ExceptionLeaveReplacementAtFinallyAfterFinallyNonEmpty()
    {
        try
        {
            try
            {
                ThrowException();
            }
            finally
            {
                throw new Exception();
            }
            object someVar = DateTime.Now.GetHashCode();
        }
        finally
        {
            throw new Exception();
        }
    }

Или (Msil идентичен):

    private void _ExceptionLeaveReplacementAtFinallyAfterFinallyNonEmpty()
    {
        try
        {
            try
            {
                ThrowException();
            }
            finally
            {
                throw new Exception();
            }
        }
        finally
        {
            throw new Exception();
        }
        object someVar = DateTime.Now.GetHashCode();

Ответы

Ответ 1

Это по дизайну. Пока вы не можете достичь этого бесконечного цикла:-)
Спасибо за сообщение об этой проблеме, хотя!!!

=== более длинная версия:

Когда "finally" не заканчивается (содержит бросок или бесконечный цикл), код после заявления try становится недостижимым из языка, предполагаемого. Поскольку он недоступен, он не имеет никакого кода там, даже если, например, метод должен вернуть значение.
Фактически, поскольку различные инварианты, которые обычно сохраняются в нормальном коде, не выполняются в недостижимом коде, компилятор защищает удаленный код, который недоступен, даже если он присутствует. Это не просто оптимизация, она часто необходима для правильности. Вместо того, чтобы предотвращать/обнаруживать/исправлять нарушения в недостижимом коде, очистить его просто удалить.

Теперь спецификация IL требует, чтобы код операции "оставить" указывал на действительную целевую команду. В частности, не важно, заблокирована ли ветка окончательно. Но мы не имеем никакого действительного кода, чтобы указать на, поэтому нам нужно ввести "посадочную" часть кода. Он должен быть маленьким. Мы также знаем, что он никогда не будет доступен, но он также не должен ставить под угрозу уже установленную статическую корректность метода.

Бесконечный цикл - это наименьшая часть кода. Кстати, другой возможностью может быть "throw null", но исторически используется бесконечный цикл.

Нет, NOP не будет работать, потому что это сделает следующий верификатор команды доступным, и это может привести к нарушениям других правил IL, таких как "не пропустить конец конца метода, использовать ret".

Ответ 2

Хорошо, поэтому я вырыл Roslyn Source и нашел, где именно это происходит:

В строке 706 существует частный метод под названием RewriteSpecialBlocks в ILBuilder.cs. Он выглядит так:

/// <summary>
/// Rewrite any block marked as BlockedByFinally as an "infinite loop".
/// </summary>
/// <remarks>
/// Matches the code generated by the native compiler in
/// ILGENREC::AdjustBlockedLeaveTargets.
/// </remarks>
private void RewriteSpecialBlocks()
{
    var current = leaderBlock;

    while (current != null)
    {
        // The only blocks that should be marked as BlockedByFinally
        // are the special blocks inserted at the end of exception handlers.
        Debug.Assert(current.Reachability != Reachability.BlockedByFinally ||
            IsSpecialEndHandlerBlock(current));

         if (IsSpecialEndHandlerBlock(current))
        {
            if (current.Reachability == Reachability.BlockedByFinally)
            {
                // BranchLabel points to the same block, so the BranchCode
                // is changed from Nop to Br_s.
                current.SetBranchCode(ILOpCode.Br_s);
            }
            else
            {
                // special block becomes a true nop
                current.SetBranch(null, ILOpCode.Nop);
            }
        }
        current = current.NextBlock;
    }

    // Now that the branch code has changed, the block is no longer special.
    Debug.Assert(AllBlocks(block => !IsSpecialEndHandlerBlock(block)));
}

Этот метод вызывается из здесь, и комментарии показывают, что это все часть удаления недостижимого кода. Он все еще не совсем отвечает, почему он генерирует бесконечный цикл, а не nop.

Ответ 3

Спасибо за подробную информацию здесь. На первый взгляд это похоже на ошибку в компиляторе. Я зарегистрировал следующую проблему, чтобы отслеживать это.

https://github.com/dotnet/roslyn/issues/15297