В вычислительной технике ошибка сегментации (часто сокращается до segfault ) или нарушение доступа — это ошибка или состояние отказа, вызванное оборудованием с защитой памяти , уведомляющее операционную систему (ОС) о том, что программное обеспечение попыталось получить доступ к ограниченной области памяти (нарушение доступа к памяти). На стандартных компьютерах x86 это форма общей ошибки защиты . Ядро операционной системы в ответ обычно выполняет некоторые корректирующие действия, как правило, передавая ошибку нарушающему процессу , отправляя процессу сигнал . Процессы в некоторых случаях могут устанавливать пользовательский обработчик сигналов, что позволяет им восстанавливаться самостоятельно, [1] но в противном случае используется обработчик сигналов ОС по умолчанию, что обычно приводит к ненормальному завершению процесса ( сбою программы ), а иногда и к дампу ядра .
Ошибки сегментации — это распространенный класс ошибок в программах, написанных на таких языках, как C , которые обеспечивают низкоуровневый доступ к памяти и не содержат или почти не содержат проверок безопасности. Они возникают в основном из-за ошибок в использовании указателей для адресации виртуальной памяти , в частности, несанкционированного доступа. Другой тип ошибки доступа к памяти — ошибка шины , которая также имеет различные причины, но сегодня встречается гораздо реже; они происходят в основном из-за неправильной адресации физической памяти или из-за невыровненного доступа к памяти — это ссылки на память, которые оборудование не может адресовать, а не ссылки, которые процессу не разрешено адресовать.
Во многих языках программирования есть механизмы, разработанные для предотвращения ошибок сегментации и повышения безопасности памяти. Например, Rust использует модель на основе владения [2] для обеспечения безопасности памяти. [3] Другие языки, такие как Lisp и Java , используют сборку мусора , [4] которая позволяет избежать определенных классов ошибок памяти, которые могут привести к ошибкам сегментации. [5]
Ошибка сегментации возникает, когда программа пытается получить доступ к области памяти , к которой ей не разрешен доступ, или пытается получить доступ к области памяти неразрешенным способом (например, пытается записать данные в область, доступную только для чтения , или перезаписать часть операционной системы ).
Термин «сегментация» имеет различные применения в вычислениях; в контексте «ошибки сегментации» он относится к адресному пространству программы . [6] При защите памяти только собственное адресное пространство программы доступно для чтения, и из него только стек и часть сегмента данных программы , предназначенная для чтения/записи, доступны для записи, в то время как данные, предназначенные только для чтения, выделенные в сегменте const и сегменте кода , недоступны для записи. Таким образом, попытка чтения за пределами адресного пространства программы или запись в сегмент адресного пространства, предназначенный только для чтения, приводит к ошибке сегментации, отсюда и название.
В системах, использующих сегментацию аппаратной памяти для предоставления виртуальной памяти , ошибка сегментации возникает, когда оборудование обнаруживает попытку сослаться на несуществующий сегмент или сослаться на местоположение за пределами границ сегмента, или сослаться на местоположение способом, не разрешенным разрешениями, предоставленными для этого сегмента. В системах, использующих только подкачку , ошибка недействительной страницы обычно приводит к ошибке сегментации, а ошибки сегментации и ошибки страницы являются ошибками, вызванными системой управления виртуальной памятью . Ошибки сегментации также могут возникать независимо от ошибок страницы: незаконный доступ к допустимой странице является ошибкой сегментации, но не ошибкой недействительной страницы, а ошибки сегментации могут возникать в середине страницы (следовательно, без ошибки страницы), например, при переполнении буфера , которое остается в пределах страницы, но незаконно перезаписывает память.
На аппаратном уровне ошибка изначально возникает в блоке управления памятью (MMU) при несанкционированном доступе (если указанная память существует), как часть его функции защиты памяти или при ошибке недопустимой страницы (если указанная память не существует). Если проблема заключается не в недопустимом логическом адресе, а в недопустимом физическом адресе, вместо этого возникает ошибка шины , хотя они не всегда различаются.
На уровне операционной системы эта ошибка перехватывается, и сигнал передается процессу-нарушителю, активируя обработчик этого сигнала. Разные операционные системы имеют разные имена сигналов, указывающих на то, что произошла ошибка сегментации. В операционных системах типа Unix сигнал, называемый SIGSEGV (сокращенно от segmentation violence ), отправляется процессу-нарушителю. В Microsoft Windows процесс-нарушитель получает исключение STATUS_ACCESS_VIOLATION .
Условия, при которых происходят нарушения сегментации, и то, как они проявляются, специфичны для оборудования и операционной системы: разное оборудование вызывает разные неисправности для заданных условий, а разные операционные системы преобразуют их в разные сигналы, которые передаются процессам. Непосредственной причиной является нарушение доступа к памяти, в то время как глубинной причиной обычно является какая -то программная ошибка . Определение первопричины — отладка ошибки — может быть простым в некоторых случаях, когда программа будет постоянно вызывать ошибку сегментации (например, разыменование нулевого указателя ), в то время как в других случаях ошибку может быть трудно воспроизвести, и она будет зависеть от выделения памяти при каждом запуске (например, разыменование висячего указателя ).
Ниже приведены некоторые типичные причины ошибки сегментации:
Они, в свою очередь, часто вызваны ошибками программирования, которые приводят к недопустимому доступу к памяти:
В коде C ошибки сегментации чаще всего возникают из-за ошибок в использовании указателей, особенно при динамическом выделении памяти C. Разыменование нулевого указателя, приводящее к неопределенному поведению , обычно вызывает ошибку сегментации. Это происходит потому, что нулевой указатель не может быть допустимым адресом памяти. С другой стороны, дикие указатели и висячие указатели указывают на память, которая может существовать или не существовать, и может быть или не быть доступной для чтения или записи, и, таким образом, может привести к временным ошибкам. Например:
char * p1 = NULL ; // Нулевой указатель char * p2 ; // Дикий указатель: вообще не инициализирован. char * p3 = malloc ( 10 * sizeof ( char )); // Инициализированный указатель на выделенную память // (предполагая, что malloc не дал сбой) free ( p3 ); // p3 теперь является висячим указателем, так как память была освобождена
Разыменование любой из этих переменных может вызвать ошибку сегментации: разыменование нулевого указателя обычно приводит к ошибке сегментации, в то время как чтение из дикого указателя может вместо этого привести к случайным данным, но без ошибки сегментации, а чтение из висячего указателя может привести к допустимым данным в течение некоторого времени, а затем к случайным данным по мере их перезаписи.
Действие по умолчанию для ошибки сегментации или ошибки шины — ненормальное завершение процесса, который ее вызвал. Может быть создан файл ядра для помощи в отладке, а также могут быть выполнены другие действия, зависящие от платформы. Например, системы Linux , использующие патч grsecurity, могут регистрировать сигналы SIGSEGV для отслеживания возможных попыток вторжения с использованием переполнений буфера .
В некоторых системах, таких как Linux и Windows, программа может сама обрабатывать ошибку сегментации. [7] В зависимости от архитектуры и операционной системы, запущенная программа может не только обрабатывать событие, но и извлекать некоторую информацию о его состоянии, например, получать трассировку стека , значения регистров процессора , строку исходного кода, когда она была запущена, адрес памяти, к которому был осуществлен недопустимый доступ [8] и было ли действие чтением или записью. [9]
Хотя ошибка сегментации обычно означает, что в программе есть ошибка, которую необходимо исправить, также возможно намеренно вызвать такую ошибку в целях тестирования, отладки, а также для эмуляции платформ, где необходим прямой доступ к памяти. В последнем случае система должна иметь возможность разрешить запуск программы даже после возникновения ошибки. В этом случае, когда система позволяет, можно обработать событие и увеличить счетчик программ процессора, чтобы «перепрыгнуть» через неисправную инструкцию для продолжения выполнения. [10]
Запись в память только для чтения приводит к ошибке сегментации. На уровне ошибок кода это происходит, когда программа записывает в часть своего собственного сегмента кода или в часть сегмента данных только для чтения , поскольку они загружаются ОС в память только для чтения.
Вот пример кода ANSI C , который обычно вызывает ошибку сегментации на платформах с защитой памяти. Он пытается изменить строковый литерал , что является неопределенным поведением согласно стандарту ANSI C. Большинство компиляторов не обнаружат это во время компиляции и вместо этого скомпилируют это в исполняемый код, который приведет к сбою:
int main ( void ) { char * s = "привет мир" ; * s = 'H' ; }
Когда программа, содержащая этот код, компилируется, строка "hello world" помещается в раздел rodata исполняемого файла программы : раздел сегмента данных, доступный только для чтения . При загрузке операционная система помещает ее вместе с другими строками и константными данными в сегмент памяти, доступный только для чтения. При выполнении переменная s устанавливается так, чтобы указывать на местоположение строки, и делается попытка записать символ H через переменную в память, что приводит к ошибке сегментации. Компиляция такой программы с помощью компилятора, который не проверяет назначение мест, доступных только для чтения, во время компиляции, и запуск ее в операционной системе типа Unix приводит к следующей ошибке времени выполнения :
$ gcc segfault.c -g -o segfault $ ./segfault Ошибка сегментации
Обратная трассировка основного файла из GDB :
Программа получила сигнал SIGSEGV , ошибка сегментации . 0x1c0005c2 в main () при segfault . c : 6 6 * s = 'H' ;
Этот код можно исправить, используя массив вместо указателя на символ, поскольку это выделяет память в стеке и инициализирует ее значением строкового литерала:
char s [] = "привет, мир" ; s [ 0 ] = 'H' ; // эквивалентно, *s = 'H';
Несмотря на то, что строковые литералы не должны изменяться (это имеет неопределенное поведение в стандарте C), в C они имеют static char []
тип [11] [12] [13], поэтому в исходном коде (который указывает на этот массив) нет неявного преобразования char *
, в то время как в C++ они имеют static const char []
тип , и, следовательно, есть неявное преобразование, поэтому компиляторы, как правило, обнаруживают эту конкретную ошибку.
В языках C и C-подобных нулевые указатели используются для обозначения «указателя на отсутствие объекта» и в качестве индикатора ошибки, а разыменование нулевого указателя (чтение или запись через нулевой указатель) является очень распространенной ошибкой программы. Стандарт C не говорит, что нулевой указатель совпадает с указателем на адрес памяти 0, хотя на практике это может иметь место. Большинство операционных систем отображают адрес нулевого указателя таким образом, что доступ к нему вызывает ошибку сегментации. Такое поведение не гарантируется стандартом C. Разыменование нулевого указателя является неопределенным поведением в C, и соответствующая реализация может предполагать, что любой разыменованный указатель не является нулевым.
int * ptr = NULL ; printf ( "%d" , * ptr );
Этот пример кода создает нулевой указатель , а затем пытается получить доступ к его значению (прочитать значение). Это приводит к ошибке сегментации во время выполнения во многих операционных системах.
Разыменование нулевого указателя и последующее присвоение ему значения (запись значения в несуществующую цель) также обычно приводит к ошибке сегментации:
int * ptr = NULL ; * ptr = 1 ;
Следующий код включает разыменование нулевого указателя, но при компиляции часто не приводит к ошибке сегментации, поскольку значение не используется, и поэтому разыменование часто оптимизируется путем устранения мертвого кода :
int * ptr = NULL ; * ptr ;
Следующий код обращается к массиву символов s
за его верхней границей. В зависимости от компилятора и процессора это может привести к ошибке сегментации.
char s [] = "привет, мир" ; char c = s [ 20 ];
Другой пример — рекурсия без базового случая:
int main ( void ) { return main (); }
что приводит к переполнению стека, что приводит к ошибке сегментации. [14] Бесконечная рекурсия не обязательно приводит к переполнению стека в зависимости от языка, оптимизаций, выполненных компилятором, и точной структуры кода. В этом случае поведение недостижимого кода (оператор return) не определено, поэтому компилятор может исключить его и использовать оптимизацию хвостового вызова , которая может привести к отсутствию использования стека. Другие оптимизации могут включать перевод рекурсии в итерацию, что, учитывая структуру функции-примера, приведет к тому, что программа будет работать вечно, при этом, вероятно, не переполняя свой стек.