Окончательный список распространенных причин ошибок сегментации
ПРИМЕЧАНИЕ. У нас есть много вопросов о сегментах ошибок, в основном с одинаковыми ответами, поэтому я пытаюсь свести их в канонический вопрос, как у нас для неопределенной ссылки.
Хотя у нас есть вопрос о том, что такое ошибка сегментации, он охватывает что, но не перечисляет много причин. В верхнем ответе написано "есть много причин", и в нем указан только один, а в большинстве других ответов нет причин.
В общем, я считаю, что нам нужна хорошо организованная вики-группа сообщества по этой теме, в которой перечислены все распространенные причины (а затем и некоторые), чтобы получить ошибки по умолчанию. Цель состоит в том, чтобы помочь в отладке, как упомянуто в отказе от ответа.
Я знаю, что такое ошибка сегментации, но в коде может быть трудно обнаружить, не зная, как они часто выглядят. Хотя, без сомнения, их слишком много, чтобы перечислять их исчерпывающе, каковы наиболее распространенные причины ошибок сегментации в C и 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);
Смотрите также:
- Как определить, была ли переменная неинициализирована / перехватить segfault в C
- Конкатенация строк и int приводит к ошибке сегмента C
Доступ к висячему указателю
Если вы используете 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, вызываемый вашим собственным кодом.
Тем не менее, вы должны всегда предполагать, что проблема в вас, пока не доказано обратное.
Другие причины
Возможные причины ошибок сегментации примерно равны количеству неопределенного поведения, и их слишком много, чтобы перечислить даже стандартную документацию.
Несколько менее распространенных причин для проверки:
- UD2 генерируется на некоторых платформах из-за других UB
- C++ STL map::operator[] сделано для удаляемой записи
DEBUGGING
Инструменты отладки играют важную роль в диагностике причин segfault. Скомпилируйте вашу программу с флагом отладки (-g
), а затем запустите его с помощью отладчика, чтобы определить, где вероятнее всего произошла ошибка.
Последние компиляторы поддерживают сборку с -fsanitize=address
Это обычно приводит к тому, что программа работает примерно в 2 раза медленнее, но может более точно обнаруживать ошибки адреса. Однако другие ошибки (такие как чтение из неинициализированной памяти или утечка ресурсов, не связанных с памятью, такие как файловые дескрипторы) не поддерживаются этим методом, и невозможно использовать много инструментов отладки и ASan одновременно.
Некоторые отладчики памяти
- ГБД | Mac, Linux
- Вальгринд (мемчек)| Linux
- Доктор Память | Windows
Кроме того, рекомендуется использовать инструменты статического анализа для обнаружения неопределенного поведения, но, опять же, они являются инструментом, который просто помогает вам найти неопределенное поведение, и они не гарантируют обнаружение всех случаев неопределенного поведения.
Однако, если вам действительно не повезло, использование отладчика (или, реже, просто перекомпиляция с отладочной информацией) может повлиять на программный код и память в достаточной степени, чтобы ошибка больше не возникала, явление, известное как heisenbug.