Подтвердить что ты не робот

Что происходит в ОС, когда мы разыскиваем указатель NULL в C?

Скажем, есть указатель, и мы инициализируем его с помощью NULL.

int* ptr = NULL;
*ptr = 10;

Теперь программа выйдет из строя, поскольку ptr не указывает на какой-либо адрес, и мы присваиваем значение этому, что является недопустимым доступом. Итак, вопрос в том, что происходит внутри операционной системы? Возникает ли ошибка страницы/сегментации? Будет ли ядро ​​даже искать в таблице страниц? Или произошел сбой раньше?

Я знаю, что не делал бы такого в какой-либо программе, но это просто знать, что происходит внутри OS или Compiler в таком случае. И это не дублирующий вопрос.

4b9b3361

Ответ 1

Короткий ответ: это зависит от множества факторов, включая компилятор, архитектуру процессора, конкретную модель процессора и ОС, среди прочих.

Длинный ответ (x86 и x86-64). Переходим на самый низкий уровень: CPU. На x86 и x86-64 этот код обычно компилируется в последовательность команд или команд, например:

movl $10, 0x00000000

Что говорит "хранить постоянное целое число 10 в адресе виртуальной памяти 0". Руководства разработчика программного обеспечения Intel® 64 и IA-32 подробно описывают, что происходит, когда эта инструкция выполняется, поэтому я собираюсь обобщить ее для вас.

Процессор может работать в нескольких режимах, некоторые из которых предназначены для обратной совместимости с более старыми процессорами. Современные операционные системы запускают код пользовательского уровня в режиме с защищенным режимом, который использует paging для преобразования виртуальных адресов в физические адреса.

Для каждого процесса ОС хранит таблицу страниц, которая определяет способ отображения адресов. Таблица страниц хранится в памяти в определенном формате (и защищена так, что они не могут быть изменены с помощью кода пользователя), который понимает ЦП. Для каждого доступа к памяти, который происходит, ЦП переводит его в соответствии с таблицей страниц. Если перевод завершается успешно, он выполняет соответствующее чтение/запись в физическую ячейку памяти.

Интересные вещи случаются, когда перевод адресов не выполняется. Не все адреса действительны, и если какой-либо доступ к памяти создает неверный адрес, процессор вызывает исключение ошибки страницы. Это приводит к переходу из пользовательского режима (ака текущего уровня привилегий (CPL) 3 на x86/x86-64) в режим ядра (иначе CPL 0) в определенное место в коде ядра, как определено таблицей дескриптора прерываний (IDT).

Ядро восстанавливает управление и, основываясь на информации из исключения и таблицы страниц процесса, выясняет, что произошло. В этом случае он понимает, что процесс пользовательского уровня обратился к недопустимой ячейке памяти, а затем реагирует соответствующим образом. В Windows он будет ссылаться на обработку структурированных исключений, чтобы разрешить код пользователя обрабатывать исключение. В системах POSIX ОС будет передавать сигнал SIGSEGV в процесс.

В других случаях ОС будет обрабатывать внутреннюю ошибку страницы и перезапустить процесс из своего текущего местоположения, как будто ничего не произошло. Например, защитные страницы помещаются в нижней части стека, чтобы позволить стеку расти по требованию до предела, вместо предварительного распределения большого объема памяти для стек. Подобные механизмы используются для достижения copy-on-write памяти.

В современных ОС таблицы страниц обычно настраиваются, чтобы сделать адрес 0 неправильным виртуальным адресом. Но иногда это можно изменить, например. на Linux, записав 0 в псевдофайл /proc/sys/vm/mmap_min_addr, после чего можно использовать mmap(2) для сопоставления виртуального адреса 0. В этом случае разыменование нулевого указателя не приведет к сбою страницы.

Вышеупомянутое обсуждение - это все, что происходит, когда исходный код работает в пользовательском пространстве. Но это также может произойти внутри ядра. Ядро может (и, конечно, гораздо более вероятно, чем код пользователя) отображать виртуальный адрес 0, поэтому такой доступ к памяти будет нормальным. Но если он не отображается, то то, что происходит тогда, во многом схоже: процессор вызывает ошибку ошибки страницы, которая ловутся в предопределенную точку ядра, ядро ​​проверяет, что произошло, и реагирует соответственно. Если ядро ​​не может восстановиться из исключения, оно, как правило, будет паниковать каким-либо образом (паника ядра, ядро ​​oops или BSOD в Windows, например), распечатав некоторую отладочную информацию на консольном или последовательном порту и затем остановив.

См. также Много шума по поводу NULL: использование разыменования ядра NULL для примера того, как злоумышленник может использовать ошибку разыменования нулевого указателя изнутри ядра, чтобы получить привилегии root на машине Linux.

Ответ 2

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

Они используют 128-битное линейное адресное пространство для ВСЕХ данных (память и диск) в одной гигантской "вещи". В соответствии с их ОС, "действительный" указатель должен быть помещен на 128-битную границу в этом адресном пространстве. Это, кстати, вызывает увлекательные побочные эффекты для структурированных, упакованных или нет, указателей на дом. Во всяком случае, скрытая на отдельной странице посвященная обработке - это растровое изображение, которое назначает один бит для каждого допустимого местоположения в адресном пространстве процесса, где может быть установлен действительный указатель. ВСЕ коды операций на своем оборудовании и ОС, которые могут генерировать и возвращать действительный адрес памяти и назначать его указателю, будут устанавливать бит, который представляет адрес памяти, в котором находится этот указатель (целевой указатель).

Так почему же кому-то нужно? По этой простой причине:

int a = 0;
int *p = &a;
int *q = p-1;

if (p)
{
// p is valid, p bit is lit, this code will run.
}

if (q)
{
   // the address stored in q is not valid. q bit is not lit. this will NOT run.
}

Что действительно интересно, так это.

if (p == NULL)
{
   // p is valid. this will NOT run.
}

if (q == NULL)
{
   // q is not valid, and therefore treated as NULL, this WILL run.
}

if (!p)
{
   // same as before. p is valid, therefore this won't run
}

if (!q)
{
   // same as before, q is NOT valid, therefore this WILL run.
}

Его то, что вы должны видеть, чтобы поверить. Я даже не могу представить, что домашнее хозяйство было выполнено для поддержания этой битовой карты, особенно при копировании значений указателя или освобождении динамической памяти.

Ответ 3

В процессоре, поддерживающем виртуальную mermory, исключение ошибки страницы обычно выдается, если вы попытаетесь прочитать по адресу памяти 0x0. Вызывается обработчик ошибок на странице ОС, тогда ОС решит, что страница недействительна и прервет вашу программу.

Обратите внимание, что на некоторых CPU вы также можете безопасно получить адрес памяти 0x0.

Как сказано в стандарте C, разыменование нулевого указателя имеет значение undefined, если компилятор способен обнаруживать во время компиляции (или даже время выполнения), что вы разыменовываете нулевой указатель, он может делать все, что захочет, например, прерывать программу с подробным сообщением об ошибке.

(C99, 6.5.3.2.p4) "Если для указателя было присвоено недопустимое значение, поведение унарного оператора * равно undefined.87)"

87): "Среди недействительных значений для разыменования указателя унарным оператором" * "- это нулевой указатель, адрес, неуместно выровненный по типу объекта, на который указывает, и адрес объекта после окончания его жизни."

Ответ 4

В типичном случае int *ptr = NULL; установит ptr, чтобы указать на адрес 0. Стандарт C (и стандарт С++) очень осторожен, чтобы этого не требовать, но он чрезвычайно распространен тем не менее.

Когда вы выполняете *ptr = 10;, CPU обычно генерирует 0 по адресным строкам и 10 в линиях данных, а при установке строки R/W указывается запись (и, если на шине есть такая вещь, утверждают, что память или строка ввода/вывода указывают на запись в память, а не на ввод/вывод).

Предполагая, что ЦП поддерживает защиту памяти (и вы используете ОС, которая его активирует), CPU будет проверять этот (попырованный) доступ, прежде чем это произойдет. Например, современный процессор Intel/AMD использует пейджинговые таблицы, которые отображают виртуальные адреса на физические адреса. В типичном случае адрес 0 не будет отображаться на какой-либо физический адрес. В этом случае ЦП будет генерировать исключение нарушения доступа. Для одного довольно типичного примера Microsoft Windows оставляет первые 4 мегабайта не отображенными, поэтому любой адрес в этом диапазоне обычно приводит к нарушению доступа.

На более старом процессоре (или более старой операционной системе, которая не позволяет функциям защиты ЦП) попытка записи часто будет успешной. Например, в MS-DOS запись через NULL-указатель просто записывает в нулевой адрес. В малой или средней модели (с 16-разрядными адресами для данных) большинство компиляторов записывали некоторый известный шаблон в первые несколько байтов сегмента данных, и когда программа закончилась, они проверили бы, останется ли этот шаблон неповрежденным (и сделайте что-нибудь, чтобы указать, что вы написали с помощью указателя NULL, если он не сработал). В компактной или большой модели (20-битные адреса данных) они обычно просто записывали в нуль без предупреждения.

Ответ 5

Я предполагаю, что это зависит от платформы и компилятора. Указатель NULL может быть реализован с использованием страницы NULL, и в этом случае у вас будет ошибка страницы, или она может быть ниже предела сегмента для сегмента с раскрывающимся списком, и в этом случае у вас будет ошибка сегментации.

Это не окончательный ответ, просто моя гипотеза.