Почему не * (int *) 0 = 0 вызывает нарушение доступа?
В образовательных целях я пишу набор методов, которые вызывают исключения во время выполнения на С#, чтобы понять, что все исключения и что их вызывает. Прямо сейчас, я занимаюсь программами, которые вызывают AccessViolationException
.
Самый очевидный способ (для меня) - записать в защищенную память, например:
System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0);
Как я и надеялся, это бросило AccessViolationException
. Я хотел сделать это более сжато, поэтому решил написать программу с небезопасным кодом и сделать то, что я думал, точно так же, назначив 0
нулевому указателю.
unsafe
{
*(int*)0 = 0;
}
По причинам, которые ускользают от меня, это вызывает a NullReferenceException
. Я немного поиграл с ним, и выяснил, что вместо *(int*)1
вместо *(int*)1
вызывается NullReferenceException
, но если вы используете отрицательное число, например *(int*)-1
, оно выдает AccessViolationException
.
Что здесь происходит? Почему *(int*)0 = 0
вызывает NullReferenceException
, и почему он не вызывает AccessViolationException
?
Ответы
Ответ 1
Исключение пустой ссылки происходит, когда вы разыскиваете нулевой указатель; CLR не заботится о том, является ли нулевой указатель небезопасным указателем с нулевым в нем нулевым значением или управляемым указателем (то есть ссылкой на объект ссылочного типа) с нулевым вложением в него.
Как CLR знает, что null был разыменован? И как CLR знает, когда какой-либо другой недействительный указатель был разыменован? Каждый указатель указывает где-то на странице виртуальной памяти в адресном пространстве виртуальной памяти процесса. Операционная система отслеживает, какие страницы действительны и являются недопустимыми; когда вы касаетесь недействительной страницы, возникает исключение, обнаруженное CLR. CLR затем создает это как исключение недопустимого доступа или исключение с нулевой ссылкой.
Если недопустимый доступ находится в нижней части 64K памяти, это исключение null ref. В противном случае это недопустимое исключение доступа.
Это объясняет, почему разыменование нуля и один дают исключение null ref и почему разыменование -1 дает недопустимое исключение доступа; -1 - указатель 0xFFFFFFFF на 32-битных машинах, и эта конкретная страница (на машинах x86) всегда зарезервирована для использования операционной системой в своих целях. Пользовательский код не может получить к нему доступ.
Теперь вы можете разумно спросить, почему не просто сделать исключение ссылочной ссылки для нулевого указателя и исключение недопустимого доступа для всего остального? Поскольку большая часть времени, когда небольшое число разыменовывается, происходит потому, что вы дошли до него с помощью нулевой ссылки. Представьте себе, например, что вы пытались сделать:
int* p = (int*)0;
int x = p[1];
Компилятор переводит это в моральный эквивалент:
int* p = (int*)0;
int x = *( (int*)((int)p + 1 * sizeof(int)));
который является разыменованием 4. Но с точки зрения пользователя p[1]
, безусловно, выглядит как разыменование нуля! Итак, это сообщение об ошибке.
Ответ 2
Это не ответ сам по себе, но если вы декомпилируете WriteInt32
, вы обнаружите, что он ловит NullReferenceException
и выбрасывает AccessViolationException
. Таким образом, поведение, вероятно, одно и то же, но замаскировано реальным исключением, и возникает другое исключение.
Ответ 3
NullReferenceException утверждает, что "Исключение, которое возникает при попытке разыменовать ссылку на нулевой объект", так как *(int*)0 = 0
пытается установить ячейку памяти 0x000 с помощью разыменования объекта, она выкинет NullReferenceException
. Обратите внимание, что это исключение выдается перед попыткой получить доступ к памяти.
С другой стороны, класс AccessViolationException указывает, что: "Исключение, которое возникает при попытке прочитать или записать защищенный memory", и поскольку System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0)
не использует разыменования, вместо этого пытается установить память с помощью этого метода, объект не разыменован, поэтому означает, что не будет выбрано NullReferenceException
.
Ответ 4
MSDN четко говорит:
В программах, состоящих полностью из проверяемого управляемого кода, все ссылки являются действительными или нулевыми, а нарушения прав доступа - невозможно. AccessViolationException возникает только при проверке управляемый код взаимодействует с неуправляемым кодом или с небезопасным управляемым кодом код.
См. AccessViolationException справка.
Ответ 5
Вот как работает CLR. Вместо проверки, если адрес объекта == null для каждого доступа к полю, он просто получает к нему доступ. Если это было null - CLR ловит GPF и реконструирует его как NullReferenceException. Независимо от того, что это за ссылка.