Ошибка сегментации
Эта статья нуждается в дополнительных цитатах для проверки . ( ноябрь 2011 г. ) |
В вычислительной технике ошибка сегментации (часто сокращается до segfault ) или нарушение доступа — это ошибка или состояние сбоя, вызванное аппаратным обеспечением с защитой памяти , уведомляющее операционную систему (ОС) о том, что программное обеспечение попыталось получить доступ к ограниченной области памяти ( нарушение доступа к памяти). На стандартных компьютерах x86 это разновидность общей неисправности защиты . операционной системы Ядро в ответ обычно выполняет некоторые корректирующие действия, обычно передавая ошибку процессу - нарушителю , отправляя этому процессу сигнал . В некоторых случаях процессы могут устанавливать собственный обработчик сигналов, позволяющий им восстанавливаться самостоятельно. [1] но в противном случае используется обработчик сигналов ОС по умолчанию, что обычно вызывает ненормальное завершение процесса ( сбой программы ), а иногда и дамп ядра .
Ошибки сегментации — это распространенный класс ошибок в программах, написанных на таких языках, как C , которые обеспечивают низкоуровневый доступ к памяти и практически не выполняют проверки безопасности. Они возникают в первую очередь из-за ошибок в использовании указателей для адресации виртуальной памяти , в частности несанкционированного доступа. Другой тип ошибки доступа к памяти — это ошибка шины , которая также имеет различные причины, но сегодня встречается гораздо реже; они происходят в первую очередь из-за неправильной адресации физической памяти или из-за неправильного доступа к памяти — это ссылки на память, к которым оборудование не может обратиться, а не ссылки, к которым процессу не разрешено обращаться.
Многие языки программирования имеют механизмы, позволяющие избежать ошибок сегментации и повысить безопасность памяти. Например, в Rust используется принцип владения [2] модель для обеспечения безопасности памяти. [3] Другие языки, такие как Lisp и Java , используют сбор мусора . [4] что позволяет избежать определенных классов ошибок памяти, которые могут привести к ошибкам сегментации. [5]
Обзор [ править ]
Ошибка сегментации возникает, когда программа пытается получить доступ к ячейке памяти , к которой ей не разрешен доступ, или пытается получить доступ к ячейке памяти неразрешенным способом (например, попытка записи в ячейку, доступную только для чтения , или перезаписать часть операционной системы ).
Термин «сегментация» имеет различное применение в вычислительной технике; в контексте «ошибки сегментации» это относится к адресному пространству программы . [6] При защите памяти доступно для чтения только собственное адресное пространство программы, и из них доступны для записи только стек и часть сегмента данных программы, предназначенная для чтения/записи, в то время как данные только для чтения, выделенные в константном сегменте и сегменте кода. не доступны для записи. Таким образом, попытка чтения за пределами адресного пространства программы или запись в сегмент адресного пространства, доступный только для чтения, приводит к ошибке сегментации, отсюда и название.
В системах, использующих аппаратную сегментацию памяти для предоставления виртуальной памяти , ошибка сегментации возникает, когда оборудование обнаруживает попытку обратиться к несуществующему сегменту, или обратиться к местоположению за пределами сегмента, или обратиться к местоположению в мода, не разрешенная разрешениями, предоставленными для этого сегмента. В системах, использующих только подкачку , ошибка недопустимой страницы обычно приводит к ошибке сегментации, а ошибки сегментации и ошибки страниц являются ошибками, вызываемыми системой управления виртуальной памятью . Ошибки сегментации также могут возникать независимо от ошибок страниц: незаконный доступ к допустимой странице является ошибкой сегментации, но не ошибкой недопустимой страницы, а ошибки сегментации могут возникать в середине страницы (следовательно, отсутствие ошибки страницы), например, в переполнение буфера , которое остается на странице, но незаконно перезаписывает память.
На аппаратном уровне ошибка первоначально возникает блоком управления памятью (MMU) при незаконном доступе (если ссылочная память существует), как часть его функции защиты памяти или ошибке неверной страницы (если ссылочная память не существует). ). Если проблема не в недопустимом логическом адресе, а в недопустимом физическом адресе, ошибка шины вместо этого возникает , хотя они не всегда распознаются.
На уровне операционной системы эта ошибка фиксируется и сигнал передается процессу-нарушителю, активируя обработчик процесса для этого сигнала. Различные операционные системы имеют разные имена сигналов, указывающие на то, что произошла ошибка сегментации. В Unix-подобных операционных системах сигнал под названием SIGSEGV (сокращенно от «нарушение сегментации ») отправляется процессу-нарушителю. В Microsoft Windows процесс-нарушитель получает исключение STATUS_ACCESS_VIOLATION .
Причины [ править ]
Условия, при которых происходят нарушения сегментации, и то, как они проявляются, зависят от аппаратного обеспечения и операционной системы: разное оборудование выдает разные ошибки для данных условий, а разные операционные системы преобразуют их в разные сигналы, которые передаются процессам. Непосредственной причиной является нарушение доступа к памяти, а основной причиной обычно является программная ошибка какая-либо . Определение основной причины — отладка ошибки — может быть простым в некоторых случаях, когда программа постоянно вызывает ошибку сегментации (например, разыменование нулевого указателя ), тогда как в других случаях ошибку может быть трудно воспроизвести и она зависит от распределения памяти. при каждом запуске (например, разыменование висячего указателя ).
Ниже приведены некоторые типичные причины ошибок сегментации:
- Попытка доступа к несуществующему адресу памяти (вне адресного пространства процесса)
- Попытка доступа к памяти, к которой у программы нет прав (например, к структурам ядра в контексте процесса).
- Попытка записи в постоянную память (например, сегмент кода)
Это, в свою очередь, часто вызвано ошибками программирования, которые приводят к недопустимому доступу к памяти:
- Разыменование нулевого указателя , который обычно указывает на адрес, не входящий в адресное пространство процесса.
- Разыменование или присвоение неинициализированного указателя ( дикий указатель , который указывает на случайный адрес памяти)
- Разыменование или присвоение освобожденному указателю ( висячий указатель , который указывает на освобожденную/освобожденную/удаленную память)
- Переполнение буфера
- стека Переполнение
- Попытка выполнить программу, которая компилируется неправильно. (Некоторые компиляторы [ который? ] выведет исполняемый файл , несмотря на наличие ошибок времени компиляции.)
В коде C ошибки сегментации чаще всего возникают из-за ошибок в использовании указателей, особенно при выделении динамической памяти C. Разыменование нулевого указателя, приводящее к неопределенному поведению , обычно приводит к ошибке сегментации. Это связано с тем, что нулевой указатель не может быть действительным адресом памяти. С другой стороны, дикие указатели и висячие указатели указывают на память, которая может существовать, а может и не существовать, и может быть доступна или недоступна для чтения или записи, и, таким образом, может привести к временным ошибкам. Например:
char *p1 = NULL; // Null pointer
char *p2; // Wild pointer: not initialized at all.
char *p3 = malloc(10 * sizeof(char)); // Initialized pointer to allocated memory
// (assuming malloc did not fail)
free(p3); // p3 is now a dangling pointer, as memory has been freed
Разыменование любой из этих переменных может вызвать ошибку сегментации: разыменование нулевого указателя обычно приводит к ошибке сегмента, в то время как чтение из дикого указателя может вместо этого привести к случайным данным, но не к ошибке сегмента, а чтение из висячего указателя может привести к получению действительных данных для while, а затем случайные данные по мере их перезаписи.
Обработка [ править ]
Действием по умолчанию в случае сбоя сегментации или ошибки шины является ненормальное завершение процесса, который его вызвал. Для облегчения отладки может быть создан основной файл , а также могут быть выполнены другие действия, зависящие от платформы. Например, системы Linux, использующие патч grsecurity, могут регистрировать сигналы SIGSEGV, чтобы отслеживать возможные попытки вторжения с использованием переполнения буфера .
В некоторых системах, таких как Linux и Windows, программа сама может обрабатывать ошибки сегментации. [7] В зависимости от архитектуры и операционной системы выполняющаяся программа может не только обрабатывать событие, но и извлекать некоторую информацию о его состоянии, например, трассировку стека , значения регистров процессора , строку исходного кода при запуске, адрес памяти, который был недействительный доступ [8] и было ли действие чтением или записью. [9]
Хотя ошибка сегментации обычно означает, что в программе есть ошибка, которую необходимо исправить, также возможно намеренно вызвать такой сбой в целях тестирования, отладки, а также для эмуляции платформ, где необходим прямой доступ к памяти. В последнем случае система должна иметь возможность разрешить запуск программы даже после возникновения ошибки. В этом случае, когда система позволяет, можно обработать событие и увеличить счетчик программы процессора, чтобы «перепрыгнуть» через невыполненную команду и продолжить выполнение. [10]
Примеры [ править ]
Запись в постоянную память [ править ]
Запись в постоянную память вызывает ошибку сегментации. На уровне ошибок кода это происходит, когда программа записывает часть своего собственного сегмента кода или доступную только для чтения часть сегмента данных , поскольку они загружаются ОС в постоянную память.
Вот пример кода ANSI C , который обычно вызывает ошибку сегментации на платформах с защитой памяти. Он пытается изменить строковый литерал , что является неопределенным поведением в соответствии со стандартом ANSI C. Большинство компиляторов не распознают это во время компиляции и вместо этого компилируют это в исполняемый код, который приведет к сбою:
int main(void)
{
char *s = "hello world";
*s = 'H';
}
Когда программа, содержащая этот код, компилируется, строка «hello world» помещается в раздел rodata программы исполняемого файла доступный только для чтения : раздел сегмента данных, . При загрузке операционная система помещает его вместе с другими строками и постоянными данными в сегмент памяти, доступный только для чтения. При выполнении переменная s устанавливается так, чтобы указывать местоположение строки, и предпринимается попытка записать символ H через переменную в память, что приводит к ошибке сегментации. Компиляция такой программы с помощью компилятора, который не проверяет назначение местоположений только для чтения во время компиляции, и запуск ее в Unix-подобной операционной системе приводит к следующей ошибке времени выполнения :
$ gcc segfault.c -g -o segfault
$ ./segfault
Segmentation fault
Трассировка основного файла из GDB :
Program received signal SIGSEGV, Segmentation fault.
0x1c0005c2 in main () at segfault.c:6
6 *s = 'H';
Этот код можно исправить, используя массив вместо указателя на символ, поскольку при этом память выделяется в стеке и инициализируется значением строкового литерала:
char s[] = "hello world";
s[0] = 'H'; // equivalently, *s = 'H';
Несмотря на то, что строковые литералы не должны изменяться (в стандарте C это поведение не определено), в C они имеют static char []
тип, [11] [12] [13] поэтому в исходном коде нет неявного преобразования (что указывает на char *
в этом массиве), тогда как в C++ они имеют static const char []
type, и, таким образом, происходит неявное преобразование, поэтому компиляторы обычно обнаруживают эту конкретную ошибку.
Разыменование нулевого указателя [ править ]
В языках C и C нулевые указатели используются для обозначения «указателя на отсутствие объекта» и в качестве индикатора ошибки, а разыменование нулевого указателя (чтение или запись через нулевой указатель) является очень распространенной ошибкой программы. Стандарт C не говорит, что нулевой указатель — это то же самое, что указатель на адрес памяти 0, хотя на практике это может быть так. Большинство операционных систем сопоставляют адрес нулевого указателя таким образом, что доступ к нему вызывает ошибку сегментации. Такое поведение не гарантируется стандартом C. Разыменование нулевого указателя является неопределенным поведением в C, и соответствующая реализация может предполагать, что любой разыменованный указатель не является нулевым.
int *ptr = NULL;
printf("%d", *ptr);
Этот пример кода создает нулевой указатель , а затем пытается получить доступ к его значению (прочитать значение). Это приводит к ошибке сегментации во время выполнения во многих операционных системах.
Разыменование нулевого указателя и последующее присвоение ему значения (запись значения в несуществующую цель) также обычно приводит к ошибке сегментации:
int *ptr = NULL;
*ptr = 1;
Следующий код включает разыменование нулевого указателя, но при компиляции это часто не приводит к ошибке сегментации, поскольку значение не используется и, следовательно, разыменование часто будет оптимизировано путем устранения неработающего кода :
int *ptr = NULL;
*ptr;
Переполнение буфера [ править ]
Следующий код обращается к массиву символов s
за его верхней границей. В зависимости от компилятора и процессора это может привести к ошибке сегментации.
char s[] = "hello world";
char c = s[20];
Переполнение стека [ править ]
Другой пример — рекурсия без базового случая:
int main(void)
{
return main();
}
что приводит к переполнению стека , что приводит к ошибке сегментации. [14] Бесконечная рекурсия не обязательно может привести к переполнению стека в зависимости от языка, оптимизации, выполняемой компилятором, и точной структуры кода. В этом случае поведение недостижимого кода (оператор return) не определено, поэтому компилятор может исключить его и использовать оптимизацию хвостового вызова , которая может привести к тому, что стек не будет использоваться. Другие оптимизации могут включать перевод рекурсии в итерацию, что, учитывая структуру примера функции, приведет к тому, что программа будет работать вечно, но, вероятно, не переполнит свой стек.
См. также [ править ]
Ссылки [ править ]
- ^ Экспертное программирование на C: глубокие секреты C. Питер Ван дер Линден, стр. 188.
- ^ «Язык программирования Rust — Владение» .
- ^ «Бесстрашный параллелизм с Rust — Блог о языке программирования Rust» .
- ^ Маккарти, Джон (апрель 1960 г.). «Рекурсивные функции символьных выражений и их машинное вычисление, Часть I» . Коммуникации АКМ . 4 (3): 184–195. дои : 10.1145/367177.367199 . S2CID 1489409 . Проверено 22 сентября 2018 г.
- ^ Дхурджати, Динакар; Ковшик, Сумант; Адве, Викрам; Латтнер, Крис (1 января 2003 г.). «Безопасность памяти без проверок во время выполнения и сборки мусора» (PDF) . Материалы конференции ACM SIGPLAN 2003 года по языку, компилятору и инструментам для встраиваемых систем . Том. 38. АКМ. стр. 69–80. дои : 10.1145/780732.780743 . ISBN 1581136471 . S2CID 1459540 . Проверено 22 сентября 2018 г.
- ^ «Отладка ошибок сегментации и проблем с указателями — Cprogramming.com» . www.cprogramming.com . Проверено 3 февраля 2021 г.
- ^ «Чистое восстановление после Segfaults под Windows и Linux (32-разрядная версия, x86)» . Проверено 23 августа 2020 г.
- ^ «Реализация обработчика SIGSEGV/SIGABRT, который печатает трассировку стека отладки» . Гитхаб . Проверено 23 августа 2020 г.
- ^ «Как идентифицировать операции чтения или записи ошибки страницы при использовании обработчика sigaction в SIGSEGV? (LINUX)» . Проверено 23 августа 2020 г.
- ^ «LINUX – НАПИСАНИЕ ОБРАБОТЧИКОВ ОШИБОК» . 12 ноября 2017 года . Проверено 23 августа 2020 г.
- ^ «6.1.4 Строковые литералы». ISO/IEC 9899:1990 – Языки программирования – C.
- ^ «6.4.5 Строковые литералы». ISO/IEC 9899:1999 – Языки программирования – C.
- ^ «6.4.5 Строковые литералы». ISO/IEC 9899:2011 – Языки программирования – C.
- ^ «В чем разница между ошибкой сегментации и переполнением стека?» . Переполнение стека . Проверено 11 ноября 2023 г.
Внешние ссылки [ править ]
- Процесс: граница фокуса и ошибка сегментации [ мертвая ссылка ]
- Часто задаваемые вопросы: ответы пользователей относительно определения ошибки сегментации.
- Объяснение «нулевого указателя»
- Ответ на вопрос: NULL гарантированно равен 0, а нулевой указатель — нет?
- Базовые спецификации открытой группы, выпуск 6 signal.h