Окончательный список общих причин сбоев сегментации

ПРИМЕЧАНИЕ. У нас есть много вопросов segfault, в основном таких же ответы, поэтому я пытаюсь свернуть их в канонический вопрос, например: мы имеем для undefined ссылку.

Хотя у нас есть вопрос, охватывающий что ошибка сегментации есть, он охватывает что, но не перечисляет много причин. В верхнем ответе говорится: "Есть много причин", и только перечисляет один, и большинство других ответов не содержат никаких причин.

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

Я знаю, что такое ошибка сегментации, но это может быть трудно обнаружить в коде, не зная, как часто они выглядят. Хотя, несомненно, слишком много перечислить исчерпывающе, какие наиболее распространенные причины ошибок сегментации в C и С++?

Ответ 1

ПРЕДУПРЕЖДЕНИЕ!

Ниже приведены потенциальные причины ошибки сегментации. Перечислить все причины практически невозможно. Цель этого списка - помочь в диагностике существующего сбоя.

Связь между ошибками сегментации и неопределенным поведением не может быть подчеркнута достаточно! Все приведенные ниже ситуации, которые могут создать ошибку сегментации, являются технически неопределенным поведением. Это означает, что они могут делать что угодно, не только segfault - как кто-то однажды сказал в USENET, " для компилятора законно заставлять демонов вылетать из носа ". Не рассчитывайте, что segfault произойдет, когда у вас будет неопределенное поведение. Вы должны узнать, какие неопределенные поведения существуют в C и/или C++, и избегать написания кода, который имеет их!

Больше информации о неопределенном поведении:


Что такое сегфо?

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

Для более подробного технического объяснения того, что такое ошибка сегментации, см. Что такое ошибка сегментации? ,

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

Этот список также не может заменить вашу собственную работу по отладке. (См. Этот раздел в нижней части ответа.) Это то, что вы можете искать, но ваши средства отладки - единственный надежный способ сосредоточиться на проблеме.


Доступ к пустому или неинициализированному указателю

Если у вас есть указатель, который имеет значение NULL (ptr=0) или он полностью неинициализирован (пока он вообще не настроен на что-либо), попытка доступа или изменения с помощью этого указателя имеет неопределенное поведение.

int* ptr = 0;
*ptr += 5;

Поскольку неудачное размещение (например, с помощью malloc или new) вернет нулевой указатель, вы всегда должны проверять, что ваш указатель не равен NULL, прежде чем работать с ним.

Также обратите внимание, что даже чтение значений (без разыменования) неинициализированных указателей (и переменных в целом) является неопределенным поведением.

Иногда такой доступ к неопределенному указателю может быть довольно тонким, например, при попытке интерпретировать такой указатель как строку в операторе C print.

char* ptr;
sprintf(id, "%s", ptr);

Смотрите также:


Доступ к висячему указателю

Если вы используете malloc или new для выделения памяти, а затем free или delete эту память через указатель, этот указатель теперь считается висящим указателем. Разыменование его (а также простое чтение его значения - при условии, что вы не присвоили ему новое значение, такое как NULL), является неопределенным поведением и может привести к ошибке сегментации.

Something* ptr = new Something(123, 456);
delete ptr;
std::cout << ptr->foo << std::endl;

Смотрите также:


Переполнение стека

[Нет, не сайт, на котором вы сейчас находитесь, как его назвали.] Чрезмерно упрощенно, "стопка" похожа на тот шип, на который вы прикрепляете свой заказ в некоторых закусочных. Эта проблема может возникнуть, если вы, так сказать, накладываете слишком много заказов. На компьютере любая переменная, которая не выделяется динамически, и любая команда, которая еще не обработана ЦП, попадает в стек.

Одной из причин этого может быть глубокая или бесконечная рекурсия, например, когда функция вызывает себя без возможности остановки. Поскольку эта стопка переполнилась, бумаги заказов начинают "отваливаться" и занимать другое место, не предназначенное для них. Таким образом, мы можем получить ошибку сегментации. Другой причиной может быть попытка инициализировать очень большой массив: это только один порядок, но уже достаточно большой.

int stupidFunction(int n)
{
   return stupidFunction(n);
}

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

int stupidArray[600851475143];

Один случай в дикой природе произошел из простого пропуска оператора return в условном выражении, предназначенном для предотвращения бесконечной рекурсии в функции. Мораль этой истории - всегда проверяйте, работает ли ваша проверка ошибок!

Смотрите также:


Дикие указатели

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

int n = 123;
int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people.

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

Смотрите также:


Попытка чтения за концом массива

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

Если вы читаете за концом массива, вы можете оказаться в памяти, которая не инициализирована или принадлежит чему-то другому. Это технически неопределенное поведение. Segfault - это только один из многих возможных неопределенных вариантов поведения. [Честно говоря, если у вас есть segfault здесь, вам повезло. Другие сложнее диагностировать.]

// like most UB, this code is a total crapshoot.
int arr[3] {5, 151, 478};
int i = 0;
while(arr[i] != 16)
{
   std::cout << arr[i] << std::endl;
   i++;
}

Или часто встречающийся, использующий for с <= вместо < (читает 1 байт слишком много):

char arr[10];
for (int i = 0; i<=10; i++)
{
   std::cout << arr[i] << std::endl;
}

Или даже неудачная опечатка, которая прекрасно компилируется (видно здесь) и выделяет только 1 элемент, инициализированный с dim вместо dim.

int* my_array = new int(dim);

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

Смотрите также:


Забывание NUL-терминатора в строке C.

C-строки сами по себе являются массивами с некоторыми дополнительными поведениями. Они должны заканчиваться нулем, означая, что они имеют \0 в конце, чтобы надежно использоваться в качестве строк. Это делается автоматически в некоторых случаях, а не в других.

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

char str[3] = {'f', 'o', 'o'};
int i = 0;
while(str[i] != '\0')
{
   std::cout << str[i] << std::endl;
   i++;
}

С C-строками действительно важно, \0 нибудь изменит. Вы должны принять это, чтобы избежать неопределенного поведения: так что лучше напишите char str[4] = {'f', 'o', 'o', '\0'};


Попытка изменить строковый литерал

Если вы назначите строковый литерал для char *, он не может быть изменен. Например...

char* foo = "Hello, world!"
foo[7] = 'W';

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

Смотрите также:


Несоответствие методов распределения и распределения

Вы должны использовать malloc и free вместе, new и delete вместе, new[] и delete[] вместе. Если вы перепутаете их, вы можете получить ошибки и другое странное поведение.

Смотрите также:


Ошибки в наборе инструментов.

Ошибка в машинном коде бэкенда компилятора вполне способна превратить допустимый код в исполняемый файл, который вызывает ошибки. Ошибка в компоновщике, безусловно, может сделать это тоже.

Особенно страшно, что это не UB, вызываемый вашим собственным кодом.

Тем не менее, вы всегда должны предполагать, что проблема в вас, пока не доказано обратное.


Другие причины

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

Несколько менее распространенных причин для проверки:


DEBUGGING

Инструменты отладки играют важную роль в диагностике причин segfault. Скомпилируйте вашу программу с флагом отладки (-g), а затем запустите ее с вашим отладчиком, чтобы выяснить, где, вероятно, произошла ошибка.

Последние компиляторы поддерживают построение с -fsanitize=address, что обычно приводит к тому, что программа работает примерно в 2 раза медленнее, но может более точно обнаруживать ошибки адреса. Однако другие ошибки (такие как чтение из неинициализированной памяти или утечка ресурсов, не связанных с памятью, такие как файловые дескрипторы) не поддерживаются этим методом, и невозможно использовать много инструментов отладки и ASan одновременно.

Некоторые отладчики памяти

  • GDB | Mac, Linux
  • Вальгринд (мемчек) | Linux
  • Доктор Память | Windows

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

Однако, если вам действительно не повезло, использование отладчика (или, реже, просто перекомпиляция с отладочной информацией) может повлиять на программный код и память в достаточной степени, чтобы ошибка больше не возникала, явление, известное как heisenbug.