Почему *(int*)0=0 не вызывает нарушения прав доступа?
В образовательных целях я пишу набор методов, которые вызывают исключения во время выполнения в C#, чтобы понять, что это за исключения и что их вызывает. Сейчас я работаю с программами, которые вызывают AccessViolationException
,
Самый очевидный способ (для меня) сделать это - записать в защищенную область памяти, например так:
System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0);
Как я и надеялся, это бросило AccessViolationException
, Я хотел сделать это более кратко, поэтому я решил написать программу с небезопасным кодом и сделать (как я думал) то же самое, назначив 0
на нулевой указатель.
unsafe
{
*(int*)0 = 0;
}
По причинам, которые ускользают от меня, это бросает NullReferenceException
, Я немного поиграл с этим и узнал, что используя *(int*)1
вместо этого также бросает NullReferenceException
, но если вы используете отрицательное число, как *(int*)-1
это бросит AccessViolationException
,
Что тут происходит? Почему *(int*)0 = 0
вызвать NullReferenceException
и почему это не вызывает AccessViolationException
?
5 ответов
Исключение нулевой ссылки происходит, когда вы разыменовываете нулевой указатель; CLR не заботит, является ли нулевой указатель небезопасным указателем с целым нулем, вставленным в него, или управляемым указателем (то есть ссылкой на объект ссылочного типа) с нулем, вставленным в него.
Как CLR узнает, что null был разыменован? И как CLR узнает, когда какой-то другой неверный указатель был разыменован? Каждый указатель указывает где-то на странице виртуальной памяти в адресном пространстве виртуальной памяти процесса. Операционная система отслеживает, какие страницы являются действительными, а какие недействительными; при касании недействительной страницы возникает исключение, которое обнаруживается CLR. Затем CLR отображает это как недопустимое исключение доступа или исключение нулевой ссылки.
Если недопустимый доступ к нижним 64 КБ памяти, это исключение null ref. В противном случае это недопустимое исключение доступа.
Это объясняет, почему разыменование ноль и единица дают исключение пустого 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]
конечно, выглядит как разыменование нуля! Так что это ошибка, о которой сообщается.
Это не ответ сам по себе, но если вы декомпилируете WriteInt32
Вы найдете это ловит NullReferenceException
и бросает AccessViolationException
, Таким образом, поведение, вероятно, то же самое, но замаскировано реальным исключением, которое перехватывается, и возникновением другого исключения.
NullReferenceException заявляет, что "Исключение, которое выбрасывается при попытке разыменования нулевой ссылки на объект", так как *(int*)0 = 0
пытается установить ячейку памяти 0x000, используя разыменование объекта NullReferenceException
, Обратите внимание, что это исключение выдается перед попыткой доступа к памяти.
С другой стороны, класс AccessViolationException гласит: "Исключение, которое выдается при попытке чтения или записи в защищенную память", и так как System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0)
не использует разыменование, вместо этого пытается установить память, используя этот метод, объект не разыменовывается, поэтому означает, что нет NullReferenceException
будет брошен.
В MSDN это ясно сказано:
В программах, состоящих полностью из проверяемого управляемого кода, все ссылки являются действительными или нулевыми, и нарушения доступа невозможны. AccessViolationException возникает только тогда, когда проверяемый управляемый код взаимодействует с неуправляемым кодом или с небезопасным управляемым кодом.
Вот как работает CLR. Вместо проверки, если адрес объекта == нуль для каждого доступа к полю, он просто обращается к нему. Если это было нулем - CLR ловит GPF и перебрасывает его как NullReferenceException. Неважно, что это за ссылка.