Почему * (int *) 0 = 0 не вызывает нарушения прав доступа? - PullRequest
26 голосов
/ 30 декабря 2011

В образовательных целях я пишу набор методов, которые вызывают исключения во время выполнения в 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 ]

29 голосов
/ 30 декабря 2011

Исключение нулевой ссылки происходит, когда вы разыменовываете нулевой указатель; 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], безусловно, выглядит как разыменование нуля! Так что это ошибка, о которой сообщается.

4 голосов
/ 30 декабря 2011

Это не ответ сам по себе, но если вы декомпилируете WriteInt32, вы обнаружите, что он ловит NullReferenceException и выдает AccessViolationException. Таким образом, поведение, вероятно, такое же, но оно скрыто за тем, что реальное исключение перехватывается и возникает другое исключение.

2 голосов
/ 30 декабря 2011

NullReferenceException утверждает, что "Исключение, которое выдается при попытке разыменовать нулевую ссылку на объект" , так как *(int*)0 = 0 пытается установить ячейку памяти 0x000, используя разыменование объекта будет выбрасывать NullReferenceException. Обратите внимание, что это исключение выдается перед попыткой доступа к памяти.

Класс AccessViolationException , с другой стороны, утверждает, что «Исключение, которое выдается при попытке чтения или записи в защищенную память» , а поскольку System.Runtime.InteropServices.Marshal.WriteInt32(IntPtr.Zero, 0) делает не использовать разыменование, вместо этого пытается установить память с помощью этого метода, объект не разыменовывается, поэтому значение NullReferenceException не будет выброшено.

1 голос
/ 30 декабря 2011

В MSDN ясно сказано:

В программах, состоящих полностью из проверяемого управляемого кода, все ссылки являются действительными или нулевыми, а нарушения доступа невозможны.AccessViolationException возникает только тогда, когда проверяемый управляемый код взаимодействует с неуправляемым кодом или с небезопасным управляемым кодом.

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

0 голосов
/ 30 декабря 2011

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

...