Что такое segmentation fault
Перейти к содержимому

Что такое segmentation fault

  • автор:

Segmentation fault

Ошибка сегментации (англ. Segmentation fault или сокращённо segfault ) — ошибка программного обеспечения, возникающая при попытке обращения к недоступным для записи участкам памяти либо при попытке изменения памяти запрещённым способом. В системах на основе процессоров Motorola 68000 эти ошибки, как правило, известны как ошибки адреса или шины.

Сегментная адресация памяти является одним из подходов к управлению и защите памяти в операционной системе. Для большинства целей она была вытеснена страничной памятью, однако в документациях по традиции используют термин «Ошибка сегментации». Некоторые операционные системы до сих пор используют сегментацию на некоторых логических уровнях, а страничная память используется в качестве основной политики управления памятью.

В UNIX-подобных операционных системах процесс, обращающийся к недействительным участкам памяти, получает сигнал Microsoft Windows, процесс, получающий доступ недействительным участкам памяти, создаёт исключение STATUS_ACCESS_VIOLATION, и, как правило, показывает пользователю окно с предложением отправить отчёт об ошибке Microsoft.

Пример

Вот пример кода ANSI C, который приводит к ошибке сегментации на платформах с защитой памяти:

; *s = Когда программа, содержащая этот код, скомпилирована, строка «hello world» размещена в секции программы с бинарной пометкой «только для чтения». При запуске операционная система помещает её с другими строками и константами в сегмент памяти, предназначенный только для чтения. После запуска переменная s указывает на адрес строки, а попытка присвоить значение символьной константы H через переменную в памяти приводит к ошибке сегментации.

Компиляция и запуск таких программ на

$ gcc segfault.c -g -o segfault $ ./segfault Segmentation fault 
Program received signal SIGSEGV, Segmentation fault. 0x1c0005c2 in main () at segfault.c:6 6 *s = 'H'; 

В отличие от этого, gcc 4.1.1 на Linux возвращает ошибку ещё во время компиляции:

$ gcc segfault.c -g -o segfault segfault.c: In function ‘main’: segfault.c:4: error: assignment of read-only location 

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

Этот пример кода создаёт нулевой указатель и пытается присвоить значение для несуществующих цели. Это вызывает ошибки сегментации во время выполнения программы на многих системах.

Ещё один способ вызвать ошибку сегментации заключается в том, чтобы вызвать функцию main рекурсивно, что приведёт к переполнению стека:

См. также

Ссылки

  • http://www.faqs.org/qa/qa-673.html (англ.)
  • http://c-faq.com/null/null1.html (англ.)
  • http://c-faq.com/null/varieties.html (англ.)

Микроядро • Монолитное ядро • Гибридное ядро • Пространство ядра (kernel space) • Модульное ядро • Наноядро • Драйвер • Пространство пользователя • Область пользователя • Экзоядро

Защита памяти • Сегментация памяти • Страничная память (Paging) • Блок управления памятью • Ошибка сегментации • Общая ошибка защиты

Wikimedia Foundation . 2010 .

Полезное

Смотреть что такое «Segmentation fault» в других словарях:

  • Segmentation fault — A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to … Wikipedia
  • Segmentation Fault — Bei Computern tritt eine Schutzverletzung (v. engl. segmentation fault oder segmentation violation, kurz auch segfault) auf, wenn ein Computerprogramm auf eine Ressource (insbesondere auf Speicher) zuzugreifen versucht, die vor einem solchen… … Deutsch Wikipedia
  • Segmentation Fault — Erreur de segmentation Pour les articles homonymes, voir Segmentation. Une erreur de segmentation (en anglais segmentation fault, parfois appelé en abrégé segfault), est un plantage d une application qui a tenté d accéder à un emplacement mémoire … Wikipédia en Français
  • Segmentation fault — Erreur de segmentation Pour les articles homonymes, voir Segmentation. Une erreur de segmentation (en anglais segmentation fault, parfois appelé en abrégé segfault), est un plantage d une application qui a tenté d accéder à un emplacement mémoire … Wikipédia en Français
  • segmentation fault — noun A software error that occurs when a program attempts to access a memory location that it is not permitted to access. Syn: access violation, segfault … Wiktionary
  • Segmentation — may mean: *Market segmentation, in economics Biology *A morphogenesis process that divides a metazoan body into a series of semi repetitive segments *Segmentation (biology), the structure that results from said processComputing *Segmentation… … Wikipedia
  • Segmentation (informatique) — Pour les articles homonymes, voir Segmentation. En informatique, la segmentation est une technique gérée par l unité de segmentation de la MMU, utilisée sur les systèmes d exploitation modernes, qui divise la mémoire physique (dans le cas de la… … Wikipédia en Français
  • General protection fault — This article is about the x86 exception. For the webcomic, see General Protection Fault (webcomic). A general protection fault (GPF) in the Intel x86 and AMD x86 64 architectures, and other unrelated architectures, is a fault (a type of… … Wikipedia
  • Segmention Fault — Bei Computern tritt eine Schutzverletzung (v. engl. segmentation fault oder segmentation violation, kurz auch segfault) auf, wenn ein Computerprogramm auf eine Ressource (insbesondere auf Speicher) zuzugreifen versucht, die vor einem solchen… … Deutsch Wikipedia
  • Erreur De Segmentation — Pour les articles homonymes, voir Segmentation. Une erreur de segmentation (en anglais segmentation fault, parfois appelé en abrégé segfault), est un plantage d une application qui a tenté d accéder à un emplacement mémoire qui ne lui était pas… … Wikipédia en Français
  • Обратная связь: Техподдержка, Реклама на сайте
  • �� Путешествия

Экспорт словарей на сайты, сделанные на PHP,
WordPress, MODx.

  • Пометить текст и поделитьсяИскать в этом же словареИскать синонимы
  • Искать во всех словарях
  • Искать в переводах
  • Искать в ИнтернетеИскать в этой же категории

Почему возникает ошибка segmentation fault?

Но компилятор выдает ошибку. В чем я неправ? Как использовать динамически выделенную память? Strcpy ? Ошибка возникает при использовании printf . Есть у меня еще программа, но там другой компилятор, не gcc. При таком коде тоже возникает ошибка:

char *array = (char*)malloc(6*sizeof(char)); array= "Hello"; 

Отслеживать
Maxim Gusev
задан 28 окт 2015 в 10:28
Maxim Gusev Maxim Gusev
789 2 2 золотых знака 5 5 серебряных знаков 13 13 бронзовых знаков

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

В этой программе

# include # include int main()

вы сначала динамически выделяете память под символьный массив

 char *a = (char*) malloc(6*sizeof(char)); 

Следовательно указатель a теперь содержит адрес первого байта этого участка памяти.

Затем вы присваиваете первый символ строкового литерала «Hello» первому байту памяти, адресуемой указателем a

 *a=*str; 

Теперь у вас в выделенной памяти содержится

'H' и некоторый "мусор" 

Так как остальные байты выделенной памяти не были инициализированы.

В функции printf вы используете спецификатор формата %s , который предполагает, что соответствующий аргумент является строкой, то есть символьным массивом, имеющем завершающий нулевой символ ‘\0’ .

Однако ваша строка не содержит завершающий ноль. Поэтому данная функция пытается вывести на консоль все символы даже за пределом выделенного вами участка памяти, пока не встретит символ завершения строки ‘\0’ .

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

Более того вы в качестве аргумента указали этот первый символ строки вместо адреса

printf ("%s\n", *a); ^^^ 

Должно быть по крайней мере

printf ("%s\n", a); ^^ 

Что касается данного фрагмента кода

char *array = (char*)malloc(6*sizeof(char)); array= "Hello"; 

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

Для этого фрагмента кода причиной ошибки скорей всего является то, что вы пытались освободить память с помощью функции free , записав

free( array ); 

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

Правильно было бы написать

#include //. char *array = (char*)malloc(6*sizeof(char)); strcpy( array, "Hello" ); //. free( array ); 

И в первой программе вместо

 *a=*str; 

вам следует написать

 strcpy( a, str ); 

предварительно включив заголовок

Segmentation Fault (распределение памяти компьютера)

Когда я делаю ошибку в коде, то обычно это приводит к появлению сообщения “segmentation fault”, зачастую сокращённого до “segfault”. И тут же мои коллеги и руководство приходят ко мне: «Ха! У нас тут для тебя есть segfault для исправления!» — «Ну да, виноват», — обычно отвечаю я. Но многие ли из вас знают, что на самом деле означает ошибка “segmentation fault”?

Чтобы ответить на этот вопрос, нам нужно вернуться в далёкие 1960-е. Я хочу объяснить, как работает компьютер, а точнее — как в современных компьютерах осуществляется доступ к памяти. Это поможет понять, откуда же берётся это странное сообщение об ошибке.

Вся представленная ниже информация — основы компьютерной архитектуры. И без нужды я не буду сильно углубляться в эту область. Также я буду применять всем известную терминологию, так что мой пост будет понятен всем, кто не совсем на «вы» с вычислительной техникой. Если же вы захотите изучить вопрос работы с памятью подробнее, то можете обратиться к многочисленной доступной литературе. А заодно не забудьте покопаться в исходном коде ядра какой-нибудь ОС, например, Linux. Я не буду излагать здесь историю вычислительной техники, некоторые вещи не будут освещаться, а некоторые сильно упрощены.

Немного истории

Когда-то компьютеры были очень большими, весили тонны, при этом обладали одним процессором и памятью примерно на 16 Кб. Стоил такой монстр порядка $150 000 и мог выполнять лишь одну задачу за раз: в каждый момент времени выполнялся только один какой-то процесс. Архитектуру памяти в те времена можно схематически представить так:

То есть на ОС приходилась, скажем, четверть всей доступной памяти, а остальной объём отдавался под пользовательские задачи. В то время роль ОС заключалась в простом управлении оборудованием с помощью прерываний ЦПУ. Так что операционке нужна была память для себя, для копирования данных с устройств и для работы с ними (режим PIO). Для вывода данных на экран нужно было использовать часть основной памяти, ведь видеоподсистема либо не имела своей оперативки, либо обладала считанными килобайтами. А уже сама программа выполнялась в области памяти, идущей сразу после ОС, и решала свои задачи.

Совместный доступ к ресурсам

Главная проблема заключалась в том, что устройство, стоящее $150 000, было однозадачным и тратило целые дни на обработку нескольких килобайт данных.

Из-за непомерной стоимости мало кто мог позволить себе приобрести сразу несколько компьютеров, чтобы обрабатывать одновременно несколько задач. Поэтому люди начали искать способы совместного доступа к вычислительным ресурсам одного компьютера. Так наступила эра многозадачности. Обратите внимание, что в те времена ещё никто не помышлял о многопроцессорных компьютерах. Так как же можно заставить компьютер с одним ЦПУ выполнять несколько разных задач?

Решением стало использование планировщика задач (scheduling): пока один процесс прерывался, ожидая завершения операций ввода/вывода, ЦПУ мог выполнять другой процесс. Я не буду здесь больше касаться планировщика задач, это слишком обширная тема, не имеющая отношения к памяти.

Если компьютер способен поочерёдно выполнять несколько задач, то распределение памяти будет выглядеть примерно так:

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

Когда один программист будет писать код для выполнения задачи В, он должен знать границы выделяемых сегментов памяти. Допустим, задача В занимает в памяти отрезок от 10 до 12 Кб, тогда каждый адрес памяти должен быть жёстко закодирован в пределах этих границ. Но если компьютер будет выполнять сразу три задачи, то память будет поделена на большее количество сегментов, и значит сегмент для задачи В может оказаться сдвинут. Тогда код программы придётся переписывать, чтобы она могла оперировать меньшим объёмом памяти, а также изменить все указатели.

Здесь всплывает и иная проблема: что если задача В обратится к сегменту памяти, выделенному для задачи А? Такое легко может произойти, ведь при работе с указателями памяти достаточно сделать маленькую ошибку, и программа будет обращаться к совершенно другому адресу, нарушив целостность данных другого процесса. При этом задача А может работать с очень важными с точки зрения безопасности данными. Нет никакого способа помешать В вторгнуться в область памяти А. Наконец, вследствие ошибки программиста задача В может перезаписать область памяти ОС (в данном случае от 0 до 4 Кб).

Адресное пространство

Чтобы можно было спокойно выполнять несколько задач, хранящихся в памяти, нам нужна помощь от ОС и оборудования. В частности, адресное пространство. Это некая абстракция памяти, выделяемая ОС для какого-то процесса. На сегодняшний день это фундаментальная концепция, которая используется везде. По крайней мере, во ВСЕХ компьютерах гражданского назначения принят именно этот подход, а у военных могут быть свои секреты. Персоналки, смартфоны, телевизоры, игровые приставки, умные часы, банкоматы — ткните в любой аппарат, и окажется, что распределение памяти в нём осуществляется по принципу «код-стек-куча» (code-stack-heap).

Адресное пространство содержит всё, что нужно для выполнения процесса:

  • Машинные инструкции, которые должен выполнить ЦПУ.
  • Данные, с которыми будут работать эти машинные инструкции.

  • Стек (stack) — это область памяти, в которой программа хранит информацию о вызываемых функциях, их аргументах и каждой локальной переменной в функциях. Размер области может меняться по мере работы программы. При вызове функций стек увеличивается, а при завершении — уменьшается.
  • Куча (heap) — это область памяти, в которой программа может делать всё, что заблагорассудится. Размер области может меняться. Программист имеет возможность воспользоваться частью памяти кучи с помощью функции malloc() , и тогда эта область памяти увеличивается. Возврат ресурсов осуществляется с помощью free() , после чего куча уменьшается.
  • Кодовый сегмент (code) — это область памяти, в которой хранятся машинные инструкции скомпилированной программы. Они генерируются компилятором, но могут быть написаны и вручную. Обратите внимание, что эта область памяти также может быть разделена на три части (текст, данные и BSS). Эта область памяти имеет фиксированный размер, определяемый компилятором. В нашем примере пусть это будет 1 Кб.

Виртуализация памяти

Допустим, задача А получила в своё распоряжение всю доступную пользовательскую память. И тут возникает задача В. Как быть? Решение было найдено в виртуализации.

Напомню одну из предыдущих иллюстраций, когда в памяти одновременно находятся А и В:

Допустим, А пытается получить доступ к памяти в собственном адресном пространстве, например по индексу 11 Кб. Возможно даже, что это будет её собственный стек. В этом случае ОС нужно придумать, как не подгружать индекс 1500, поскольку по факту он может указывать на область задачи В.

На самом деле, адресное пространство, которое каждая программа считает своей памятью, является памятью виртуальной. Фальшивкой. И в области памяти задачи А индекс 11 Кб будет фальшивым адресом. То есть — адресом виртуальной памяти.

Каждая программа, выполняющаяся на компьютере, работает с фальшивой (виртуальной) памятью. С помощью некоторых чипов ОС обманывает процесс, когда он обращается к какой-либо области памяти. Благодаря виртуализации ни один процесс не может получить доступ к памяти, которая ему не принадлежит: задача А не влезет в память задачи В или самой ОС. При этом на пользовательском уровне всё абсолютно прозрачно, благодаря обширному и сложному коду ядра ОС.

Таким образом, каждое обращение к памяти регулируется операционной системой. И это должно осуществляться очень эффективно, чтобы не слишком замедлять работу различных выполняющихся программ. Эффективность обеспечивается с помощью аппаратных средств, преимущественно — ЦПУ и некоторых компонентов вроде MMU. Последний появился в виде отдельного чипа в начале 1970-х, а сегодня MMU встраиваются непосредственно в процессор и в обязательном порядке используются операционными системами.

Вот небольшая программка на С, демонстрирующая работу с адресами памяти:

#include #include int main(int argc, char **argv)

На моей машине LP64 X86_64 она показывает такой результат:

Code is at 0x40054c
Stack is at 0x7ffe60a1465c
Heap is at 0x1ecf010

Как я и описывал, сначала идёт кодовый сегмент, затем куча, а затем стек. Но все эти три адреса фальшивые. В физической памяти по адресу 0x7ffe60a1465c вовсе не хранится целочисленная переменная со значением 3. Никогда не забывайте, что все пользовательские программы манипулируют виртуальными адресами, и только на уровне ядра или аппаратных драйверов допускается использование адресов физической памяти.

Переадресация

Переадресация (транслирование, перевод, преобразование адресов) — это термин, обозначающий процесс сопоставления виртуального адреса физическому. Занимается этим модуль MMU. Для каждого выполняющегося процесса операционка должна помнить соответствия всех виртуальных адресов физическим. И это довольно непростая задача. По сути, ОС приходится управлять памятью каждого пользовательского процесса при каждом обращении. Тем самым она превращает кошмарную реальность физической памяти в полезную, мощную и лёгкую в использовании абстракцию.

Давайте рассмотрим подробнее.

Когда запускается процесс, ОС бронирует для него фиксированный объём физической памяти, пусть это будет 16 Кб. Начальный адрес этого адресного пространства сохраняется в специальной переменной base . А в переменной bounds записывается размер выделенной области памяти, в нашем примере — 16 Кб. Эти два значения записываются в каждую таблицу процессов — PCB (Process Control Block).

Итак, это виртуальное адресное пространство:

А это его физический образ:

ОС решает выделить диапазон физических адресов от 4 до 20 Кб, то есть значение base равно 4 Кб, а значение bounds равно 4 + 16 = 20 Кб. Когда процесс ставится в очередь на выполнение (ему выделяется процессорное время), ОС считывает из PCB значения обеих переменных и копирует их в специальные регистры ЦПУ. Далее процесс запускается и пытается обратиться, допустим, к виртуальному адресу 2 Кб (в своей куче). К этому адресу ЦПУ добавляет значение base , полученное от ОС. Следовательно, физический адрес будет 2+ 4 = 6 Кб.

Физический адрес = виртуальный адрес + base

Если получившийся физический адрес (6 Кб) выбивается из границ выделенной области (4—20 Кб), это означает, что процесс пытается обратиться к памяти, которая ему не принадлежит. Тогда ЦПУ генерирует исключение и сообщает об этом ОС, которая обрабатывает данное исключение. В этом случае система обычно сигнализирует процессу о нарушении: SIGSEGV, Segmentation Fault. Этот сигнал по умолчанию прерывает выполнение процесса (это можно настраивать).

Перераспределение памяти

Если задача А исключена из очереди на выполнение, то это даже лучше. Это означает, что планировщик попросили выполнить другую задачу (допустим, В). Пока выполняется В, операционка может перераспределить всё физическое пространство задачи А. Во время выполнения пользовательского процесса ОС зачастую теряет управление процессором. Но когда процесс делает системный вызов, процессор снова возвращается под контроль ОС. До этого системного вызова операционка может что угодно делать с памятью, в том числе и целиком перераспределять адресное пространство процесса в другой физический раздел.

В нашем примере это осуществляется достаточно просто: ОС перемещает 16-килобайтную область в другое свободное место подходящего размера и просто обновляет значения переменных base и bounds для задачи А. Когда процессор возвращается к её выполнению, процесс переадресации всё ещё работает, но физическое адресное пространство уже изменилось.

С точки зрения задачи А ничего не меняется, её собственное адресное пространство по-прежнему расположено в диапазоне 0-16 Кб. При этом ОС и MMU полностью контролируют каждое обращение задачи к памяти. То есть программист манипулирует виртуальной областью 0-16 Кб, а MMU берёт на себя сопоставление с физическими адресами.

После перераспределения образ памяти будет выглядеть так:

Программисту теперь не нужно заботиться о том, с какими адресами памяти будет работать его программа, не нужно переживать о конфликтах. ОС в связке с MMU снимают с него все эти заботы.

Сегментация памяти

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

  • Мы предполагаем, что каждое виртуальное адресное пространство имеет размер в 16 Кб. Это не имеет никакого отношения к действительности.
  • ОС приходится поддерживать список свободных диапазонов физической памяти размером по 16 Кб, чтобы выделять их для новых запускаемых процессов или перераспределения текущих выделенных областей. Как можно эффективно осуществлять всё это, не ухудшив производительность всей системы?
  • Мы выделяем по 16 Кб каждому процессу, но ведь не факт, что каждый из них будет использовать всю выделенную область. Так что мы просто теряем кучу памяти на пустом месте. Это называется внутренней фрагментацией (internal fragmentation) — память резервируется, но не используется.

Для решения некоторых из этих проблем давайте рассмотрим более сложную систему организации памяти — сегментацию. Смысл её прост: принцип “base and bounds” распространяется на все три сегмента памяти — кучу, кодовый сегмент и стек, причём для каждого процесса, вместо того чтобы рассматривать образ памяти как единую уникальную сущность.

В результате мы больше не теряем память между стеком и кучей:

Как вы могли заметить, свободное пространство в виртуальной памяти задачи А больше не размещено в памяти физической. И память теперь используется гораздо эффективнее. ОС теперь должна запоминать для каждой задачи три пары base и bounds , по одной для каждого сегмента. MMU, как и раньше, занимается переадресацией, но оперирует уже тремя base
и тремя bounds .

Допустим, у кучи задачи А параметр base равен 126 Кб, а bounds — 2 Кб. Пусть задача А обращается к виртуальному адресу 3 Кб (в куче). Тогда физический адрес определяется как 3 – 2 Кб (начало кучи) = 1 Кб + 126 Кб (сдвиг) = 127 Кб. Это меньше 128, а значит ошибки обращения не будет.

Совместное использование сегментов

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

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

При этом оба процесса не подозревают, что делят с кем-то свою память. Такой подход стал возможен благодаря внедрению битов защиты сегмента (segment protection bits).

Для каждого создаваемого физического сегмента ОС регистрирует значение bounds , которое используется MMU для последующей переадресации. Но в то же время регистрируется и так называемый флаг разрешения (permission flag).

Поскольку сам код нельзя модифицировать, то все кодовые сегменты создаются с флагами RX. Это значит, что процесс может загружать эту область памяти для последующего выполнения, но в неё никто не может записывать. Другие два сегмента — куча и стек — имеют флаги RW, то есть процесс может считывать и записывать в эти свои два сегмента, однако код из них выполнять нельзя. Это сделано для обеспечения безопасности, чтобы злоумышленник не мог повредить кучу или стек, внедрив в них свой код для получения root-прав. Так было не всегда, и для высокой эффективности этого решения требуется аппаратная поддержка. В процессорах Intel это называется “NX bit”.

Флаги могут быть изменены в процессе выполнения программы, для этого используется mprotect().

Под Linux все эти сегменты памяти можно посмотреть с помощью утилит /proc//maps или /usr/bin/pmap.

Вот пример на PHP:

$ pmap -x 31329 0000000000400000 10300 2004 0 r-x-- php 000000000100e000 832 460 76 rw--- php 00000000010de000 148 72 72 rw--- [ anon ] 000000000197a000 2784 2696 2696 rw--- [ anon ] 00007ff772bc4000 12 12 0 r-x-- libuuid.so.0.0.0 00007ff772bc7000 1020 0 0 ----- libuuid.so.0.0.0 00007ff772cc6000 4 4 4 rw--- libuuid.so.0.0.0 . . 

Здесь есть все необходимые подробности относительно распределения памяти. Адреса виртуальные, отображаются разрешения для каждой области памяти. Каждый совместно используемый объект (.so) размещён в адресном пространстве в виде нескольких частей (обычно код и данные). Кодовые сегменты являются исполняемыми и совместно используются в физической памяти всеми процессами, которые разместили подобный совместно используемый объект в своём адресном пространстве.

Shared Objects — это одно из крупнейших преимуществ Unix- и Linux-систем, обеспечивающее экономию памяти.

Также с помощью системного вызова mmap() можно создавать совместно используемую область, которая преобразуется в совместно используемый физический сегмент. Тогда у каждой области появится индекс s, означающий shared.

Ограничения сегментации

Итак, сегментация позволила решить проблему неиспользуемой виртуальной памяти. Если она не используется, то и не размещается в физической памяти благодаря использованию сегментов, соответствующих именно объёму используемой памяти.

Но это не совсем верно.

Допустим, процесс запросил у кучи 16 Кб. Скорее всего, ОС создаст в физической памяти сегмент соответствующего размера. Если пользователь потом освободит из них 2 Кб, тогда ОС придётся уменьшить размер сегмента до 14 Кб. Но вдруг потом программист запросит у кучи ещё 30 Кб? Тогда предыдущий сегмент нужно увеличить более чем в два раза, а возможно ли это будет сделать? Может быть, его уже окружают другие сегменты, не позволяющие ему увеличиться. Тогда ОС придётся искать свободное место на 30 Кб и перераспределять сегмент.

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

Фрагментация может привести к тому, что какой-нибудь процесс запросит такой объём памяти, который будет больше любого из свободных участков. И в этом случае ОС придётся отказать процессу в выделении памяти, даже если суммарный объём свободных областей будет существенно больше.

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

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

Так что сегментация памяти влечёт за собой немало проблем, связанных с управлением памятью и многозадачностью. Нужно как-то улучшить возможности сегментации и исправить недостатки. Это достигается с помощью ещё одного подхода — страниц виртуальной памяти.

Разбиение памяти на страницы

Как было сказано выше, главный недостаток сегментации заключается в том, что сегменты очень часто меняют свой размер, и это приводит к фрагментации памяти, из-за чего может возникнуть ситуация, когда ОС не выделит для процессов нужные области памяти. Эта проблема решается с помощью страниц: каждое размещение, которое ядро делает в физической памяти, имеет фиксированный размер. То есть страницы — это области физической памяти фиксированного размера, ничего более. Это сильно облегчает задачу управления свободным объёмом и избавляет от фрагментации.

Давайте рассмотрим пример: виртуальное адресное пространство объёмом 16 Кб разбито на страницы.

Мы не говорим здесь о куче, стеке или кодовом сегменте. Просто делим память на куски по 4 Кб. Затем то же самое делаем с физической памятью:

ОС хранит таблицу страниц процесса (process page table), в которой представлены взаимосвязи между страницей виртуальной памяти процесса и страницей физической памяти (страничный кадр, page frame).

Теперь мы избавились от проблемы поиска свободного места: страничный кадр либо используется, либо нет (unused). И ядру не в пример легче найти достаточное количество страниц, чтобы выполнить запрос процесса на выделение памяти.

Страница — это мельчайшая и неделимая единица памяти, которой может оперировать ОС.

У каждого процесса есть своя таблица страниц, в которой представлена переадресация. Здесь уже используются не значения границ области, а номер виртуальной страницы (VPN, virtual page number) и сдвиг (offset).

Пример: размер виртуального пространства 16 Кб, следовательно, нам нужно 14 бит для описания адресов (2 14 = 16 Кб). Размер страницы 4 Кб, значит нам нужно 4 Кб (16/4), чтобы выбрать нужную страницу:

Когда процесс хочет использовать, например, адрес 9438 (вне границ 16 384), то он запрашивает в двоичном коде 10.0100.1101.1110:

Это 1246-й байт в виртуальной странице номер 2 («0100.1101.1110»-й байт в «10»-й странице). Теперь ОС достаточно просто обратиться к таблице страниц процесса, чтобы найти эту страницу номер 2. В нашем примере она соответствует восьмитысячному байту физической памяти. Следовательно, виртуальный адрес 9438 соответствует физическому адресу 9442 (8000 + сдвиг 1246).

Как уже было сказано, каждый процесс обладает лишь одной таблицей страниц, поскольку у каждого процесса собственная переадресация, как и у сегментов. Но где же именно хранятся все эти таблицы? Наверное, в физической памяти, где же ещё им быть?

Если сами таблицы страниц хранятся в памяти, то для получения VPN надо обращаться к памяти. Тогда количество обращений к ней удваивается: сначала мы извлекаем из памяти номер нужной страницы, а затем обращаемся к самим данным, хранящимся в этой странице. И если скорость доступа к памяти невелика, то ситуация выглядит довольно грустно.

Буфер быстрой переадресации (TLB, Translation-lookaside Buffer)

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

Для поддержания производительности снова используется помощь оборудования. Как и при сегментации, мы аппаратными методами помогаем ядру эффективно осуществлять переадресацию. Для этого используется TLB, входящий в состав MMU, и представляющий собой простой кэш для некоторых VPN-переадресаций. TLB позволяет ОС не обращаться к памяти лишний раз, чтобы получить физический адрес из виртуального.

Аппаратный MMU инициируется при каждом обращении к памяти, извлекает из виртуального адреса VPN и запрашивает у TLB, хранится ли в нём переадресация с этого VPN. Если да, то его роль выполнена. Если нет, то MMU находит нужную таблицу страниц процесса, и если она ссылается на валидный адрес, то обновляет данные в TLB, чтобы тот предоставлял их при следующем обращении.

Как вы понимаете, если в кэше отсутствует нужная переадресация, то это замедляет обращение к памяти. Можно предположить, что чем больше размер страниц, тем больше вероятность, что в TLB окажутся нужные данные. Но тогда мы будем тратить больше памяти на каждую страницу. Так что здесь нужен какой-то компромисс. Современные ядра умеют использовать страницы разных размеров. Например, Linux способен оперировать «огромными» страницами по 2 Мб вместо традиционных 4 Кб.

Также рекомендуется хранить данные компактно, в смежных адресах памяти. Если вы раскидаете их по всей памяти, то куда чаще в TLB не будет обнаруживаться нужной переадресации, либо он будет постоянно переполняться. Это называется эффективностью пространственной локальности (spacial locality efficiency): данные, которые расположены в памяти сразу за вашими, могут размещаться в той же физической странице, и тогда благодаря TLB вы получите выигрыш в производительности.

Кроме того, TLB в каждой записи хранит так называемые ASID (Address Space Identifier, идентификатор адресного пространства). Это нечто вроде PID, идентификатора процесса. Каждый процесс, поставленный в очередь на выполнение, имеет собственный ASID, и TLB может управлять обращением любого процесса к памяти, без риска ошибочных обращений со стороны других процессов.

Повторимся снова: если пользовательский процесс пытается обратиться к неправильному адресу, тот наверняка будет отсутствовать в TLB. Следовательно, будет запущена процедура поиска в таблице страниц процесса. В ней хранится переадресация, но с неправильным набором битов. В х86-системах переадресации имеют размер 4 Кб, то есть битов в них немало. А значит есть вероятность найти правильный бит, равно как и другие вещи, наподобие бита изменения («грязного бита», dirty bit), битов защиты (protection bit), бита обращения (reference bit) и т.д. И если запись помечена как неправильная, то ОС по умолчанию выдаст SIGSEGV, что приведёт к ошибке “segmentation fault”, даже если о сегментах уже и речи не идёт.

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

Заключение

Теперь вы знаете, что стоит за сообщением “segmentation fault”. Раньше операционки использовали сегменты для размещения пространства виртуальной памяти в пространстве физической. Когда пользовательский процесс хочет обратиться к памяти, то он просит MMU переадресовать его. Но если полученный адрес ошибочен, — находится вне пределов физического сегмента, или если сегмент не имеет нужных прав (попытка записи в read only-сегмент), — то ОС по умолчанию отправляет сигнал SIGSEGV, что приводит к прерыванию выполнения процесса и выдаче сообщения “segmentation fault”. В каких-то ОС это может быть “General protection fault”. Вы можете изучить исходный код Linux для х86/64-платформ, отвечающий за ошибки доступа к памяти, в частности — за SIGSEGV. Также можете посмотреть, как на этой платформе осуществляется сегментирование. Вы откроете для себя интересные моменты относительно разбиения на страницы, дающие куда больше возможностей, чем при использовании классических сегментов.

  • распределение памяти
  • сегментация памяти
  • segmentation fault
  • Блог компании NIX
  • Веб-разработка
  • Программирование

What is a segmentation fault?

What is a segmentation fault? Is it different in C and C++? How are segmentation faults and dangling pointers related?

734k 141 141 gold badges 912 912 silver badges 1287 1287 bronze badges
asked Feb 27, 2010 at 9:23
Rajendra Uppal Rajendra Uppal
19.3k 15 15 gold badges 59 59 silver badges 57 57 bronze badges
Just a memory dump when something goes wrong!
Apr 25, 2015 at 1:18

Typically called by attempting to dereference a null pointer, so a segmentation fault is often analogous to a Java NullPointerException .

Dec 18, 2017 at 9:08

Segmentation comes from Memory Segmentation. You’re accessing a segment of memory that does not belong to you.

May 13, 2020 at 10:33

17 Answers 17

Segmentation fault is a specific kind of error caused by accessing memory that “does not belong to you.” It’s a helper mechanism that keeps you from corrupting the memory and introducing hard-to-debug memory bugs. Whenever you get a segfault you know you are doing something wrong with memory – accessing a variable that has already been freed, writing to a read-only portion of the memory, etc. Segmentation fault is essentially the same in most languages that let you mess with memory management, there is no principal difference between segfaults in C and C++.

There are many ways to get a segfault, at least in the lower-level languages such as C(++). A common way to get a segfault is to dereference a null pointer:

int *p = NULL; *p = 1; 

Another segfault happens when you try to write to a portion of memory that was marked as read-only:

char *str = "Foo"; // Compiler marks the constant string as read-only *str = 'b'; // Which means this is illegal and results in a segfault 

Dangling pointer points to a thing that does not exist anymore, like here:

char *p = NULL; < char c; p = &c; >// Now p is dangling 

The pointer p dangles because it points to the character variable c that ceased to exist after the block ended. And when you try to dereference dangling pointer (like *p=’A’ ), you would probably get a segfault.

1 1 1 silver badge
answered Feb 27, 2010 at 9:36
103k 44 44 gold badges 261 261 silver badges 355 355 bronze badges

The last example is particularly nasty, when I build: int main() < char *p = 0; < char c = 'x'; p = &c; >printf( «%c\n»,*p); return 0; > With either gcc or several other compilers, it ‘appears’ to work. No warnings on compile. No segfault. This is because the ‘>’ out of scope, doesn’t actually delete the data, just marks it as free to be used again. The code can run fine on a production system for years, you alter another part of the code, change compiler or something else and BOOOOOM!

Apr 13, 2010 at 9:06

Sorry for the bump but just a side note. none of your examples necessarily cause a segfault, in fact it’s just undefined behavior 😉

Sep 15, 2012 at 3:01

@oldrinb: It is impossible to write code that necessarily causes a segfault. Not least because there are systems out there that operate without memory protection, thus cannot tell whether a piece of memory actually «belongs to you», and thus don’t know segfaults, only undefined behaviour. (classic AmigaOS, for example)

May 29, 2014 at 18:03

@ChrisHuang-Leaver, you need to understand that c is local, it means that it have been pushed on the stack after < and pop-ed out of it after >. the dangling pointer is just a reference to an offset which is now out of the stack. that’s why modifying it in a simple program will never trigger any segfault. on the other hand it may lead to segfault in a more complex use case, where other function calls might lead the stack to grow and contain the data pointed to by the dangling pointer. writing to that data (local vars) would lead to undefined behavior (segfault &Co)

Jan 19, 2016 at 21:23

@ChrisHuang-Leaver, normally when you get out of scope, the compiler has to recover some stack space to free the unused stack space, but this doesn’t happen always (with gcc being one of this compilers). Also, the allocated stack space is normally reused again, so I have heard of no operating systems that return unused stack pages to the system, making that space subject for a SIGSEGV , so I won’t expect such a signal from mangling with the stack.

Jul 22, 2016 at 11:59

It would be worth noting that segmentation fault isn’t caused by directly accessing another process memory (this is what I’m hearing sometimes), as it is simply not possible. With virtual memory every process has its own virtual address space and there is no way to access another one using any value of pointer. Exception to this can be shared libraries which are same physical address space mapped to (possibly) different virtual addresses and kernel memory which is even mapped in the same way in every process (to avoid TLB flushing on syscall, I think). And things like shmat 😉 — these are what I count as ‘indirect’ access. One can, however, check that they are usually located long way from process code and we are usually able to access them (this is why they are there, nevertheless accessing them in a improper way will produce segmentation fault).

Still, segmentation fault can occur in case of accessing our own (process) memory in improper way (for instance trying to write to non-writable space). But the most common reason for it is the access to the part of the virtual address space that is not mapped to physical one at all.

And all of this with respect to virtual memory systems.

answered Jul 3, 2011 at 23:22
konrad.kruczynski konrad.kruczynski
46.6k 6 6 gold badges 36 36 silver badges 47 47 bronze badges

With shared memory/memory mapped files it is possible for someone else to mess with your memory. In WIN32 there are nasty API’s like ‘WriteProcessMemory’ too!

Feb 17, 2014 at 23:46

@paulm: Yes, I know. This is what I had on mind in «And things like shmat 😉 — these are what I count as ‘indirect’ access.»

Feb 18, 2014 at 10:08

In a virtual memory operating system there’s no way (normally, so please, operating system implementors, don’t flame me for this) for a process to access another process virtual memory, not being some kind of memory attach system call that allows you to access. Virtual memory addresses normally mean different things depending on the process being considered.

Jul 22, 2016 at 12:02

A segmentation fault is caused by a request for a page that the process does not have listed in its descriptor table, or an invalid request for a page that it does have listed (e.g. a write request on a read-only page).

A dangling pointer is a pointer that may or may not point to a valid page, but does point to an «unexpected» segment of memory.

answered Feb 27, 2010 at 9:27
Ignacio Vazquez-Abrams Ignacio Vazquez-Abrams
779k 153 153 gold badges 1346 1346 silver badges 1364 1364 bronze badges
This is true, but would it really help you if you already didn’t know what a segmentation fault is?
Feb 27, 2010 at 9:37

To be honest, as other posters have mentioned, Wikipedia has a very good article on this so have a look there. This type of error is very common and often called other things such as Access Violation or General Protection Fault.

They are no different in C, C++ or any other language that allows pointers. These kinds of errors are usually caused by pointers that are

  1. Used before being properly initialised
  2. Used after the memory they point to has been realloced or deleted.
  3. Used in an indexed array where the index is outside of the array bounds. This is generally only when you’re doing pointer math on traditional arrays or c-strings, not STL / Boost based collections (in C++.)

answered Feb 27, 2010 at 20:35
Component 10 Component 10
10.3k 7 7 gold badges 47 47 silver badges 64 64 bronze badges

A segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (for example, attempting to write to a read-only location, or to overwrite part of the operating system).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *