#c# #compiler-construction #cil
#c# #построение компилятора #cil
Вопрос:
Вопрос касается спецификации языка C # и спецификации языка CIL, а также поведения компилятора C # Microsoft и Mono.
Я создаю некоторые инструменты анализа кода (неважно какие), которые работают на CIL.
Рассматривая несколько примеров кода, я замечаю, что операторы кода (try / catch, ifelse, ifthen, циклы, …) генерируют связанные блоки MSIL.
Но я хотел бы быть уверен, что я не могу написать конструкцию кода C #, которая выдает несвязанный MSIL. Более конкретно, могу ли я написать любой оператор C #, который переводится как (что-то похожее на):
IL_0000:
IL_0001:
IL_0002:
// hole
IL_001a:
IL_001b:
Я уже пробовал некоторые странные вещи, используя goto
и вложенные циклы, но, возможно, я не такой сумасшедший, как некоторые пользователи.
Комментарии:
1. @Hans Из комментария к моему (теперь удаленному) ответу он сказал, что
// hole
ссылается на другие инструкции IL, не связанные с рассматриваемым оператором C #. Я попросил его отредактировать вопрос, чтобы прояснить это.2. Единственное, о чем вам нужно беспокоиться в операторах IL, это о том, что каждый отдельный оператор выдается правильно и что, когда управление покидает метод, состояние стека является действительным. Кроме этого, вы можете делать все, что хотите, с точки зрения инструкций по упорядочению. (Это подразумевает, что когда инструкция извлекает что-то из стека, это ожидаемый тип.)
Ответ №1:
Конечно, это тривиально возможно. Что-то вроде:
static void M(bool x)
{
if (x)
return;
else
M(x);
return;
}
Если вы скомпилируете это в режиме отладки, вы получите
IL_0000: nop
IL_0001: ldarg.0
IL_0002: stloc.0
IL_0003: ldloc.0
IL_0004: brfalse.s IL_0008
IL_0006: br.s IL_0011
IL_0008: ldarg.0
IL_0009: call void A::M(bool)
IL_000e: nop
IL_000f: br.s IL_0011
IL_0011: ret
if
Оператор переходит от 0001
к 0009
, а следствием if
является переход к 0011
; оба return
оператора представляют собой один и тот же код, поэтому существует «дыра», содержащая nop
и безусловное ответвление между основной частью if
и следствием.
В более общем плане, вы никогда не должны ничего предполагать о макете IL, созданного компилятором C #. Компилятор не дает никаких гарантий, кроме того, что созданный IL будет законным и, если он безопасен, проверяемым.
Вы говорите, что пишете какие-то инструменты анализа кода; как автор значительной части анализатора C # и кто-то, кто работал над сторонними инструментами анализа в Coverity, небольшой совет: для большинства вопросов, на которые вы обычно хотите получить ответы о программах на C #, дерево синтаксического анализа, созданное Roslyn, является объектом, который вы хотите проанализировать, а не IL. Дерево синтаксического анализа — это конкретное синтаксическое дерево; оно является взаимно однозначным с каждым символом в исходном коде. Может быть очень сложно отобразить оптимизированный IL обратно в исходный код, и может быть очень легко получить ложные срабатывания при анализе IL.
Другими словами: source-to-IL сохраняет семантику, но также и теряет информацию; обычно требуется проанализировать артефакт, в котором содержится наибольшее количество информации.
Если вам по какой-либо причине необходимо управлять своим анализатором на уровне IL, вашей первой задачей, вероятно, должно быть нахождение границ базовых блоков, особенно если вы анализируете свойства достижимости.
«Базовый блок» — это непрерывный фрагмент IL, где конечная точка блока не «переходит» к следующей инструкции — потому что это, например, ответвление, возврат или выбрасывание — и в блоке нет ответвлений никуда, кроме первой инструкции.
Затем вы можете сформировать график базовых блоков для каждого метода, указав, какие из них могут передавать управление каким другим блокам. Это «повышает уровень» вашего анализа; вместо анализа эффектов последовательности инструкций IL теперь вы анализируете эффекты графика базовых блоков.
Если вы расскажете больше о том, какие виды анализа вы проводите, я могу посоветовать дальше.
Ответ №2:
Теоретически да (это исходит из моего опыта). Ваш инструмент анализа не работает с c # напрямую, а работает только с IL-кодом. IL может быть создан кем угодно, не только Visual Studio, но и другими языковыми компиляторами, такими как visual Basic, python. Сеть… и обфускаторы! Обфускаторы являются настоящим виновником: в то время как другие компиляторы пытаются придерживаться спецификаций, обфускаторы делают все возможное, чтобы использовать спецификации и целевую среду выполнения.
Запутанный код может нарушать определенные шаблоны здравого смысла. Рассмотрим этот случай: некоторые интеллектуальные обфускаторы создают недопустимый msil, но jitter переваривает его, потому что случается, что недопустимые части в конце концов не выполняются.
При создании инструмента анализа вы не сможете обрабатывать эти случаи, если вашей целью не является создание деобфускатора.
Комментарии:
1. Обфускатор, который выдает незаконный IL, ходит по тонкому льду; джиттеру разрешено запускать верификатор IL перед проверкой метода и отклонять метод, если он не проходит проверку. Дрожание делает это, если метод находится в контексте низкого доверия, потому что код низкого доверия должен быть проверяемым.
2. Спасибо, Эрик! Вы указали на критический момент: низкое доверие. Это объясняет, почему обфускатор, который мы использовали в прошлом, работал только в особых случаях, когда были установлены все «оптимизации».