Компилятор генерирует бесконечный цикл после окончательного блокирования, когда
Я использую стандартный компилятор 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