Вызов метода экземпляра для пустой ссылки иногда успешен - PullRequest
1 голос
/ 15 ноября 2011

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

Вот немного ИЛ:

.assembly MrSandbox {}
.class private MrSandbox.AClass {
    .field private int32 myField

    .method public int32 GetAnInt() cil managed {
        .maxstack  1
        .locals init ([0] int32 retval)
        ldc.i4.3
        stloc retval
        ldloc retval
        ret
    }

    .method public int32 GetAnotherInt() cil managed {
        .maxstack  1
        .locals init ([0] int32 retval)
        ldarg.0
        ldfld int32 MrSandbox.AClass::myField
        stloc retval
        ldloc retval
        ret
    }
}
.class private MrSandbox.Program {
    .method private static void Main(string[] args) cil managed {
        .entrypoint
        .maxstack  1
        .locals init ([0] class MrSandbox.AClass p,
                      [1] int32 myInt)
        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnotherInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret
    }
}

Теперь, когда этот код запускается, мы получаем то, что я ожидаю, вроде . callvirt будет проверять наличие нуля, где call, однако, здесь при вызове NullReferenceException выбрасывается. Это не ясно для меня, поскольку я ожидал бы System.AccessViolationException вместо этого. Я объясню свои рассуждения в конце этого вопроса.

Если мы заменим код внутри Main(string[] args) на этот (после .locals строк):

        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret

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

Наконец, и вот тут-то и возникает настоящая путаница, замените код в Main(string[] args) следующим (после строк .locals):

        ldnull
        stloc p
        ldloc p
        call instance int32 MrSandbox.AClass::GetAnInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
        pop
        call instance int32 MrSandbox.AClass::GetAnotherInt()
        stloc myInt
        ldloc myInt
        call void [mscorlib]System.Console::WriteLine(int32)
        ret

Теперь, что вы ожидаете от этого кода? Я ожидал, что код выведет 3 на консоль, прочитает ключ с консоли и затем завершится с ошибкой NullReferenceException. Ну, ничего этого не происходит. Вместо этого никакие значения не выводятся на экран, кроме System.AccessViolationException. Почему это противоречиво?

С задним фоном, вот мои вопросы:

1) MSDN указывает, что callvirt будет выдавать NullReferenceException, если obj равно null, но call просто говорит, что оно не должно быть нулевым. Почему тогда он выбрасывает NRE по умолчанию вместо нарушения прав доступа? Мне кажется, что call по контракту попытается получить доступ к памяти и потерпит неудачу, вместо того, чтобы делать то, что callvirt делает, сначала проверяя на ноль.

2) Является ли причиной того, что второй пример работает, из-за того, что он не имеет доступа к полям уровня класса и что call не выполняет проверку на ноль? Если так, как нестатический метод может быть вызван для нулевой ссылки и возвращен успешно? Насколько я понимаю, когда ссылочный тип помещается в стек, только объект типа помещается в кучу. Так вызывается ли метод из объекта типа?

3) Почему разницу в исключениях бросают между первым и последним примером? По моему мнению, третий пример выдает правильное исключение AccessViolationException, поскольку это именно то, что он пытается сделать; доступ к нераспределенной памяти.


До того, как ответы "Поведение неопределено", я знаю, что это не ВСЕ правильный способ написания вещей, я просто надеюсь, что кто-то может помочь пролить некоторое представление о вышеуказанные вопросы.

Спасибо.

Ответы [ 3 ]

5 голосов
/ 15 ноября 2011

1) Процессор равен , что вызывает нарушение прав доступа.CLR перехватывает исключение и переводит его, основываясь на адресе доступа исключения.Любой доступ в пределах первых 64 КБ адресного пространства повторно вызывается как управляемое исключение NullReferenceException.Проверьте этот ответ для справки.

2) Да, CLR не применяет ненулевое это значение.Например, компилятор C ++ / CLI генерирует код, который не выполняет эту проверку, так же, как это делает нативный C ++.Пока метод никогда не использует ссылку this , это не приведет к исключению.Компилятор C # явно генерирует код для проверки значения this перед вызовом метода callvirt.См. этот пост для справки.

3) Вы ошиблись в IL, GetAnotherInt () - это метод экземпляра, но вы забыли написать инструкцию ldloc.Вы получаете AV, потому что указатель ссылки случайный.

1 голос
/ 15 ноября 2011

Это немного странно, так как OP сообщает, что PEverify не дает сбоя.

Последний вызов GetAnotherInt выглядит недействительным.

В этот момент в стеке ничего нет.

Это объясняет как минимум AccessViolationException; P

Не уверен, почему PEVerify позволяет это.

Обновление:

PEVerify действительно терпит неудачу.

[IL]: Error: MrSandbox.Program::Main][offset 0x00000021] Stack underflow.
1 голос
/ 15 ноября 2011

Я не могу ответить 2) точно, но вот для 1) и 3).

A NullReferenceException - то же самое, что и AccessViolationException;в первые дни CLR вообще не было AccessViolationException, и разыменование недействительного указателя, но ненулевой указатель все равно давало NullReferenceException.

Это потому, что на современных компьютерах дешевлеаппаратные средства проверяют работоспособность.Ваше представление о том, какое исключение выдается, основано на идее, что CLR выполняет явные нулевые проверки (if (foo == null) throw new NullReferenceException()), но это не относится к реализации Microsoft для ПК с Windows.

Когда вы разыменовываете недействительныйадрес, ваша программа прервана, потому что она сделала что-то недопустимое;CLR подключается к этому прерыванию и выдает NullReferenceException или AccessViolationException в зависимости от адреса, вызвавшего ошибку.Таким образом, ему не нужно вставлять любую проверку памяти, и она все равно будет вести себя предсказуемо.

Если я правильно помню, доступ к любому адресу в 0xFFFF приведет кNullReferenceException и все, что выше, будет AccessViolationException.Вы можете проверить с помощью небезопасного кода и указателей.Я сам никогда не использовал небезопасный код в C #, поэтому следующий фрагмент может не работать, но я ожидаю, что исправления, необходимые для тестирования, будут тривиальными.(Друг проверял это на платформе .NET Framework 3 или 3.5, когда она была текущей, поэтому есть вероятность, что эти данные не обновлены.)

byte* foo = null;
*foo; // NullReferenceException
byte* bar = 0x10000;
*foo; // AccessViolationException

Мои не слишком длинныеСнимок о вопросе 2 заключается в том, что адрес вызываемого метода определяется во время компиляции, поскольку он не может изменяться.Причина callvirt сбоев в нулевых ссылках заключается в том, что ему нужен доступ к vtable объекта, и для этого ему нужно прочитать заголовок объекта.При обычном call, поскольку вызываемый метод не нужно определять во время выполнения, искать нечего, и CLR можно продолжить напрямую.(По крайней мере, это примерно то, как это работает для C ++, поэтому я полагаю, что это не слишком далеко от того, как работает CLR.)

...