почему я не получаю ошибку сегментации? - PullRequest
0 голосов
/ 07 января 2019

у меня

x=(int *)malloc(sizeof(int)*(1));

но я все еще могу читать x[20] или x[4].

Как я могу получить доступ к этим значениям? Разве я не получаю ошибку сегментации при доступе к этой памяти?

Ответы [ 2 ]

0 голосов
/ 07 января 2019

Основная предпосылка такова: ответ Сурава Гоша: доступ к памяти, возвращенной из malloc, превышает запрошенный вами размер: неопределенное поведение , поэтому соответствующая реализация может делать практически все, , включая счастливо возвращающие странные значения.

Но, учитывая "нормальную" реализацию в основных операционных системах на "обычных" машинах (gcc / MSVC / clang, Linux / Windows / macOS, x86 / ARM), почему вы иногда получаете ошибки сегментации (или нарушения доступа), и иногда нет?

Практически каждая «обычная» реализация C не выполняет никакой проверки памяти при чтении / записи через указатели 1 ; эти загрузки / хранилища обычно переводятся прямо в соответствующий машинный код, который обращается к памяти в заданном месте без особого учета размера объектов "абстрактной машины C".

Однако на этих машинах ЦП не имеет прямого доступа к физической памяти (ОЗУ) ПК, но введен уровень перевода (MMU) 2 ; всякий раз, когда ваша программа пытается получить доступ к адресу, MMU проверяет, отображено ли там что-нибудь, и есть ли у вашего процесса разрешения на запись туда. Если какая-либо из этих проверок завершится неудачей 3 , вы получите ошибку сегментации, и ваш процесс будет уничтожен. Вот почему значения неинициализированных и NULL указателей обычно дают хорошие segfaults: некоторая память в начале виртуального адресного пространства зарезервирована без отображения только для обнаружения разыменов NULL, и вообще, если вы выбрасываете дротик случайным образом в 32 бит адресное пространство (или, что еще лучше, 64-битное), вы, скорее всего, найдете зоны памяти, которые никогда не были сопоставлены ни с чем.

Несмотря на то, что MMU не может перехватить все ошибки памяти по нескольким причинам.

Прежде всего, гранулярность отображений памяти является довольно грубой по сравнению с большинством распределений "запуска из мельницы"; на страницах памяти ПК (наименьшая единица памяти, которая может быть отображена и имеет атрибуты защиты) обычно имеет размер 4 КБ. Здесь, конечно, есть компромисс: очень маленькие страницы сами по себе требуют много памяти (поскольку есть целевой физический адрес плюс атрибуты защиты, связанные с каждой страницей, и они должны быть где-то сохранены) и замедляют работу MMU 3 . Таким образом, если вы обращаетесь к памяти за «логическими» границами, но все еще в пределах одной и той же страницы памяти, MMU не может вам помочь: с точки зрения оборудования вы все еще обращаетесь к действительной памяти.

Кроме того, даже если вы выходите за пределы последней страницы своего размещения, может оказаться, что следующая страница является «действительной» в отношении оборудования; действительно, это довольно часто для памяти, которую вы получаете из так называемой кучи (malloc & friends).

Это происходит из-за того, что malloc для меньших выделений не запрашивает у ОС «новые» блоки памяти (которые теоретически могут быть выделены с сохранением защитной страницы на обоих концах); вместо этого распределитель в среде выполнения C запрашивает у ОС память в больших последовательных порциях и логически разбивает их на более мелкие зоны (обычно хранящиеся в каких-то связанных списках), которые передаются в malloc и возвращаются обратно free.

Теперь, когда в вашей программе вы выходите за пределы запрошенной памяти, вы, вероятно, не получите никакой ошибки:

  • используемый вами фрагмент памяти не находится вблизи границы страницы, поэтому чтение за пределами допустимого диапазона не вызывает нарушения прав доступа;
  • , даже если она была в конце страницы, следующая страница по-прежнему отображается, поскольку она по-прежнему принадлежит куче; это может быть либо память, которая была передана другому коду вашего процесса (поэтому вы читаете данные какой-то не связанной части вашего кода), либо свободная зона памяти (то есть вы читаете любой мусор, оставленный предыдущим владелец блока, когда он free d), или зона, используемая распределителем для хранения его данных бухгалтерии (поэтому вы читаете части таких данных).

    Во всех этих случаях, за исключением «свободного блока», даже если бы вы писали туда, вы не получили бы ошибку сегментации, но вы могли бы повредить несвязанные данные или структуры данных кучи (что обычно приводит к происходит сбой позже, когда распределитель находит несоответствия в своих данных).


Примечания

  1. Хотя современные компиляторы предоставляют специальные инструментальные сборки для отлова некоторых из этих ошибок; В частности, gcc и clang предоставляют так называемое «очиститель адресов».
  2. Это позволяет ввести прозрачный пейджинг (переключение на зоны памяти диска, которые не используются активно в случае низкой доступности физической памяти) и, что наиболее важно, защиту памяти и разделение адресного пространства (когда выполняется процесс пользовательского режима он "видит" полное виртуальное адресное пространство, содержащее только его содержимое и ничего от других процессов или ядра).
  3. И операционная система не намеренно ставит туда сообщение о том, что процессы пытаются получить доступ к памяти, которая была выгружена.
  4. Учитывая, что каждый доступ к памяти должен проходить через MMU, отображение должно быть очень быстрым, поэтому наиболее часто используемые отображения страниц хранятся в кэше; если вы сделаете страницы очень маленькими, а кеш может содержать столько же записей, то у вас фактически будет меньший диапазон памяти, охватываемый кешем.
0 голосов
/ 07 января 2019

Нет, доступ к недопустимой памяти - неопределенное поведение , а ошибка сегментации - это один из многих побочных эффектов UB. Это не гарантировано.

Тем не менее,

  • Всегда проверяйте успешность malloc(), проверяя возвращаемый указатель на NULL перед использованием возвращаемого указателя.
  • Пожалуйста, посмотрите это: Я разыгрываю результат malloc?
...