Почему *(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 возникает только тогда, когда проверяемый управляемый код взаимодействует с неуправляемым кодом или с небезопасным управляемым кодом.

См. Справку AccessViolationException.

Вот как работает CLR. Вместо проверки, если адрес объекта == нуль для каждого доступа к полю, он просто обращается к нему. Если это было нулем - CLR ловит GPF и перебрасывает его как NullReferenceException. Неважно, что это за ссылка.

Другие вопросы по тегам