Как работает callvirt под капотом? - PullRequest
5 голосов
/ 27 ноября 2010

Я пытаюсь понять, как CLR реализует ссылочные типы и полиморфизм.Я сослался на Essential .Net Vol. 1 от Don Box, который помогает справиться с большинством вещей.Но я застрял / смутился из-за следующей проблемы, когда попытался поиграться с некоторым кодом IL, чтобы лучше понять.

Я постараюсь объяснить проблему как можно лучше.Рассмотрим следующий код

class Base
{
    public void m()
    {
        Console.WriteLine("Base.m");
    }
}
class Derived : Base
{
    public void m()
    {
        Console.WriteLine("Derived.m");
    }
}

Теперь рассмотрим простое консольное приложение с IL основного метода, показанного ниже.Я настроил IL, созданный компилятором вручную, чтобы понять и снова собрать с помощью ILAsm.exe

.class private auto ansi beforefieldinit Console1.Program
       extends [mscorlib]System.Object
{
    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       44 (0x2c)
      .maxstack  1
      .locals init ([0] class Console1.Base d)
      nop
      newobj     instance void Console1.Base::.ctor()
      stloc.0
      ldloc.0
      callvirt   instance void Console1.Derived::m()
      nop
      call       string [mscorlib]System.Console::ReadLine()
      pop
      ret
    } // end of method Program::Main
} // end of class Console1.Program

Я ожидал, что этот код NOT будет запущен, так как ссылка на объект указывает на объектBase, и нет способа, чтобы таблица методов базового объекта имела запись для метода m (), определенного в классе Derived.

Но волшебным образом этот код выполняет Derived.m () !!

Итак, в вышеприведенном коде есть два вопроса, которые я не понимаю:

  1. Какое значение имеет тип, указанный в приведенном ниже коде IL?Я попытался поэкспериментировать, изменив это на различные типы (например, System.Exception !!), и об ошибках не сообщается.Почему ??

    .locals init ([0] класс Console1.Base d)

  2. Как именно работает callvirt?Как звонок был перенаправлен на Derived.m ()?

Заранее спасибо !!

С уважением, Ajay

Ответы [ 5 ]

5 голосов
/ 27 ноября 2010

Я предполагаю, что джиттер понимает, что Derived.m не является виртуальным и, следовательно, никогда не сможет указывать куда-либо еще. Таким образом, callvirt сводится к нулевой проверке и вызову вместо вызова через v-таблицу.

Попробуйте сделать Derived.m виртуальным. Могу поспорить, что это бросит тогда.

Компилятор C # выдает инструкции callvirt даже при вызове не виртуальных методов, если он не может доказать, что this!=null, поэтому он получает нулевую проверку. И в этом случае джиттер достаточно умен, чтобы заменить виртуальный вызов обычным вызовом с фиксированным адресом (или даже встроенным).

И вам следует проверить, проверяется ли ваш код. Я думаю, что это не так.

2 голосов
/ 17 января 2011

Ваш код не поддается проверке (пропустите его через peverify).Я написал сообщение в блоге о том, как callvirt работает скрытно, что может помочь вам понять, что он делает, и как ваш код выполняется.

Имейте в виду, что CLR делаетпопытаться выполнить не проверяемый код, если он запускается как обычная программа;только если это на самом деле вызывает проблему, она не работает.

В вашем примере вызов Derived.m() для экземпляра Base работает, потому что фактическое двоичное представление экземпляров объекта во время выполнения одинаково;this объект в основном один и тот же, и к полям экземпляров объектов не обращаются.

Попробуйте предоставить доступ к полю экземпляра в обоих методах и посмотрите, что произойдет ...

1 голос
/ 27 ноября 2010

Я думаю, что это побочный эффект оптимизации JIT-компилятора.Если бы метод m () был виртуальным, он должен был бы сгенерировать машинный код, чтобы вытащить указатель таблицы методов из объекта, а затем сделать виртуальный вызов.Но этот метод не является виртуальным, и JIT-компилятор уже знает указатель таблицы методов для класса Derived.Таким образом, он обходит указатель извлечения и поставляет его напрямую.Звонок работает так, как вы заметили.Вы можете проверить мои предположения, проверив сгенерированный машинный код.

Да, IL-верификатор здесь не набирает ни одного очка.Вы могли бы сделать это более интересным, если повозить метод Derived.m () с полем, которое объявлено только в Derived.Я видел слишком много сбоя кода Reflection.Emit с AccessViolation, чтобы сильно удивиться этому.Однако это вполне может быть преднамеренным, нет необходимости проверять ИЛ, который все равно падает.Не уверен, что использование таких проверочных лазеек не является (пока) распространенным явлением.К счастью.

1 голос
/ 27 ноября 2010

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

Цель типа в выражении locals - объявить тип локальной переменной. Это предоставляет информацию, необходимую верификатору типа для проверки того, что доступ к элементам локальной переменной работает с объектом правильного типа.

Callvirt может быть реализован несколькими способами. Наиболее вероятный способ заключается в том, как реализованы C ++ vtables: объект содержит таблицу указателей на функции. Каждая функция расположена с предопределенным смещением в таблице. Для вызова функции загружается и вызывается адрес с заранее заданным смещением. Обратите внимание, что в некоторых случаях CLR может выполнять дополнительную оптимизацию, если известен тип объекта. Сделано ли это, я не знаю.

0 голосов
/ 26 сентября 2013

Для получения дополнительной информации о том, как это работает еще глубже, ознакомьтесь с вопросом / ответом StackExchange: Как работает инструкция callvirt .NET для интерфейсов?

...